check:facts was red on main after account deletion's core and module
steps merged: core's MODULE_API is 1.20.0, Module-uo declares coreApi
^1.20.0, and Module-uo released v1.4.0. Every page reads these from
platform.json, and none needed rewording. Folded into this PR by the org
lead's choice.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Step 5 of docs/website/ACCOUNT_DELETION.md (§4.5). The page explains how
to delete an account on a community's site and in the app, what goes,
what stays and for how long, and who to ask when you cannot sign in. It
cannot delete anything, because we hold no account.
- The facts are one deployment row in collection.mjs (deletion.goes,
.stays with a keep each, .staff), in the website's own words. /privacy
section 3 renders it as prose, the page renders its lists, and
PLAY_DATA_SAFETY.md prints them under the console's deletion questions.
- The app rows that a deletion touches (session, trust token, push,
inbox snapshot) say so. The Data Safety notes now answer "yes" to
account deletion and name the URL. lastUpdated is 2026-10-30.
- D85: linked from the footer's legal bar, /privacy sections 2 and 3,
and /app/'s "Two ways to get it".
- D86: someone who cannot sign in is sent to their community's operator,
never to us.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Play counts data that leaves the device as collected, whoever runs the
server it lands on. The generated Data Safety notes answered "not
collected" because we run no server, which is true of /privacy and the
wrong answer to the form (org lead, 2026-10-29, Android PLAN.md M8).
An audit of the app's edge (every body, query and header in data/api/)
found more leaving the phone than section 2 listed. New rows: the
User-Agent, game-account links and creation, the Contact form, staff
posts and moderation, notification read state and preferences, search
text. The sign-in and trusted-device rows now say what is sent (the
credentials, the device's make and model).
- PlayMapping: a list of console types, plus required and purposes,
checked against PLAY_DATA_TYPES / PLAY_PURPOSES (the first version
filed "Other user-generated content" under Messages).
- The generator answers per data type, lists every type not collected,
and answers the form's security questions.
- The premise test now asserts what is still true: nothing is shared.
- legal.lastUpdated moves to 2026-10-29 with /privacy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
check:facts was red on main: platform.json said Module-Rust v0.6.0 and
releases/latest says v0.6.3. v0.6.1 and v0.6.3 are demo-only fixes (DM26,
DM27; there is no v0.6.2) and still require MODULE_API ^1.19.0, so no
page's prose changes. Every page that shows the version interpolates it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The public demo (demo.runicgateway.com) is live, so the site's demo slot
can be switched on. Two decisions of record, PLAN.md §15:
- D82: /beta reads the demo's GET /api/v1/public/settings demo.logins on
the server, caches it for a minute, and lists the published sign-ins
with the next wipe. If the demo does not answer, /beta links to its
sign-in page instead. Gate two reads as in place.
- D83: a deep link into a game module's pages carries data-demo-game, and
applyBrand.mjs fills it only for games in the new mounted demoGames
list. The Rust features gain links into /rust and /rust/servers/demo.
UO's stay hidden on a Rust demo.
The public-demo not-built entry is removed, because its resolvedBy was met.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
checkFacts turned this PR red on drift from the demo-mode merges, not on
the Node change: website main is at MODULE_API 1.19.0, Module-Rust main
declares coreApi ^1.19.0, and Module-Rust released v0.6.0. Every page
reads the three values from platform.json; verifiedOn moves to today.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Both were released on 2026-10-23. Android-app v0.6.0 is the cutover that
ships the Rust screens and the app's core wording (M20), and its APK asset
is runic-gateway-0.6.0.apk. Module-Rust v0.5.1 brings the weapon names in
the feed. verifiedOn moves to 2026-10-23, and checkFacts agrees on all 41
facts.
androidApk.serviceable stays false. It records a person's confirmation
that the published APK works, and nobody has run v0.6.0 against a real
site yet. A smoke test of the signed APK on the emulator covered install,
launch, the HTTPS rule and one TLS round trip, but no Runic Gateway site
serves HTTPS locally.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
rust-feed (web) and app-rust-server (phone) showed the prefab names the
module printed before Module-Rust#60 ("with ak47u entity"). Both were
retaken from the Rust demo running that fix, after re-running
mockRustDemo.mjs so the feed's times are fresh. They now read "with
Assault Rifle", "with Thompson", "with MP5A4".
The phone shot was taken with the emulator in Pacific/Honolulu so that no
row crosses midnight and picks up a date; the timezone was restored
afterwards. The real Oxide rig has nobody on, so the server reads 0 of
40 online, and app-rust-server's alt now says "how many players are
online" rather than "six".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Nine captures tagged rust (D65): the server list, feed, leaderboard,
live map, permissions and the RunicNPC profile editor on the web, and a
server's feed, its map and the player's Rust account in the app, from
an Android-app edge debug build.
D80: every name in a capture is a role (admin, moderator, player...),
from this phase on. D81: the Rust demo's players are mock data written
by scripts/mockRustDemo.mjs; the server, its status, map, permission
groups and RunicNPC sync state are the real Oxide test server's. The
captions and /modules/rust/ and /app/ say which part is a demo cast.
seedDemo.mjs gains --game rust (server row with a weekly wipe, public
presence for the demo, two NPC profiles, a VIP group). captureScreens.mjs
gains a click list per screen. The homepage shows the Rust live map beside
the UO capture (D76); three Rust docs pages carry their screen.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md §16 phase 17, built on the org lead's answers of 2026-10-22,
recorded as D75-D79 (count of record 79).
- D75: core marketing pages describe what a game module adds in core's
words. "Game intelligence" became "What a game module adds"; each
game's own list moved to src/data/games/ and renders only on the new
/modules/uo/ and /modules/rust/. /modules/ is a card per game.
- The page-side capability check is per game (assertGameCoverage), the
item phase 13 moved here.
- D76: the homepage keeps its game screenshot, labelled. screens.mjs
entries may carry `game` (D67); Screenshot.astro and the app grid
print the game's name and mark the figure data-game.
- D77: /app/ describes the Rust screens (released the same day).
- D78: /runicnpc/, the standalone page, is in the header and footer;
the narrow header was re-measured so five links keep two rows.
- D79: checkTerms.mjs --built reads every built page's visible text
after the build; game routes and data-game elements allow their
game's words. Proven against ten plants. Still the twelfth check.
- Re-read fixes: /architecture/'s "two machines is also the maximum",
Discord "guild" on /integrations/, and collection.mjs against what
Steam sign-in and the Rust module keep (PLAY_DATA_SAFETY.md
regenerated; the Play answers are unchanged).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md §16 phase 16, with the org lead's D72-D74 (count of record 74).
- Eight pages nested in the Rust group at /docs/rust/runicnpc/ (D72): the
overview, Install RunicNPC, Commands, NPC profiles, NPC placements, Cost and
caps, Profile fields (the one field reference the editor, rnpc.profile set,
the data file and the API all share) and The API.
- The API page carries every call, hook and data shape (D62), in the site's
own words, with no decision numbers and no per-call version history (D74).
Signatures print from src/data/runicnpc.mjs through Signature.astro.
- checkReference.mjs gains two sections. Against docs main:runicnpc/API.md:
the API version, 24 call and 10 hook signatures, and the field names of the
profile, RunicNpc_List rows, placements, routes and owners (D73), plus
exactly one <Signature> per call on api.mdx. Against runicnpc-rust
main:plugin/RunicNPC.cs: console verbs, permissions, place options and
config keys. Six planted drifts all failed before it was trusted.
- checkSidebar and checkTerms read nested sidebar groups via flatLabels().
- The Rust overview, Events zones and rewards, and Canonical documents link
the new pages; the canonical documents gain the four runicnpc/ docs.
Both NPC admin screens were walked on the walk site first (D36); nothing was
saved. npm run verify is green.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md §16 phase 15. A Rust group of fifteen pages after Ultima Online
(D70): the module, installing the bridge (installer, Pterodactyl egg, by
hand), connecting the site, verifying the bridge, linking Steam accounts,
permissions and groups, mod config, who sees who is online, events, zones
and rewards, optional plugins, the live map, troubleshooting, and three
reference pages - rust-link's sidecar.toml, the plugin's
RunicGateway.json, and the event kinds.
checkReference.mjs gains three Rust sections rather than a new check:
sidecar.toml keys and RUSTLINK_* overrides from Rust-Link config.rs,
RunicGateway.json from the plugin's PluginConfig, and the event kinds
in three classes against Module-Rust server/catalogue.js (D71). Every
admin screen was walked on the walk site before it was described (D36).
Core pages now hand off to the Rust group instead of linking out to the
docs repository. Three defects the re-read found here are fixed: the UO
/health sample's hard-coded protocol 4 (both games' samples now render
platform.json through <Code>), Authentication's missing Steam tab, and
two pages saying the site cannot reach EventsEnabled - false on Rust.
Installer v0.8.1 moved checkFacts red mid-phase; platform.json follows.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
PLAN.md §16, D63/D64, plus D67-D69 taken 2026-10-22.
- Core docs use core's words: a Team, a game server, the sidecar, the bridge
plugin, the game module. Detail that belonged to Ultima Online moved into
its pages rather than being dropped.
- New "Ultima Online" sidebar group (nine pages): an overview, Connect a
ServUO shard, Verify the bridge, The shard connection, Client files,
Troubleshooting, Bridge.cfg, sidecar.toml, Shard event catalog. Five moved
URLs answer a real 301 (src/config/redirects.mjs, asserted by the served
test).
- Getting started is game-neutral. "Connect your game server" keeps the old
URL and hands off to each game; Rust links docs main:rust-link/INSTALL.md
until phase 15. "Verify the whole stack" stays, neutral (D69).
- D68: GameExample.astro labels a game's real example on a core page with the
game's name; checkTerms allows that game's words inside it.
- scripts/checkTerms.mjs, the twelfth check: 16 game words, docs pages and
both sidebar trees, one allowlisted phrase (Discord's "Guild (server) ID").
Wired into verify and pr-checks.yml.
- canonicalDocs grouped by game; the Rust group carries four documents.
- D67: the screenshot rule from #32 is adopted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
platform.json drops the single protocol/bundle/capability list for a
`games` object (uo, rust) plus a `runicnpc` block. checkFacts.mjs loops
the protocol, plugin-protocol, capability and bundle checks over both
games, reads the schema-2 bundles at v2/<game>/current.json, checks each
module's coreApi, and reads RunicNPC's plugin.toml. 41 facts, up from 19;
clears the two that were red on main (Module API 1.17.0, installer
v0.8.0).
D66: the hero, footer and structured data show the Module API only; each
game's protocol and bundle go on that game's pages, and the docs landing
table splits into shared versions plus one column per game.
The re-read section 1 requires found, and this fixes:
- three hard-coded "1.6.0" Module API versions in the module docs
- module-lifecycle saying modules load alphabetically (the loader now
refuses a second module, W19), and the install pages never mentioning
the one-module rule
- "there is one module" in five places, now two modules both built
inside this project; the Integration Kit keeps its draft banner
- the homepage diagram's ServUO/uo-link sub-labels (D63 wording now)
notBuilt drops `second-module` (resolved by Module-Rust's release) and
rewords `multi-module`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
The last leg of the Asset Bridge (docs/link/v8.md §16, phase 9c). The cutover put
protocol 8 on `main` in every declaration site, which turned this repo red by
construction: `checkFacts.mjs` reads the protocol from `link`'s `main`.
Two checks were failing, and only one of them was expected.
**checkFacts (10)** — protocol 7 → 8 in all three reads (sidecar, overlay, bundle),
the bundle triple to 2026.09.15 / sidecar v2.3.0 / overlay v1.3.0, and the releases
to link v2.3.0, installer v0.2.0, Module-uo v1.3.0. `moduleApi` and the capability
list did not move: core's whole share of eight phases was a deletion.
**checkReference (9)** — nobody had planned for this one. `Bridge.cfg` grew nine keys
across phases 5, 6 and 7 and the reference page had gone on not mentioning them,
which is the failure mode that check exists for. They are listed as two new groups,
`Client assets` and `Spawn files`, because they are two separate consents.
## The prose the bridge changed
- **New page, Administration → Client files.** `Admin → Client files` is a screen an
operator has to press a button on and the site had no page for it. It carries the
rule nothing else states: **nothing here happens on a restart**, so a patched client
keeps serving the old artwork until somebody says so.
- **Protocol versions** said the most recent bump touched five repositories and named
`website`. Protocol 8 touched four and did not reach core — its absence is the
interesting half, and the page now says why. The store-migration paragraph gains 8,
which changed no line of the sidecar's store despite moving megabytes.
- **The bridge** said "two ways in". There are three: bulk reads go over the
request/reply path in pages, one in flight at a time, and the reason they must not
ride the event stream is structural rather than about speed.
- **Requirements** gains `libgdiplus` for Linux shard hosts, and the note that no game
client has to be installed for any of this — a ServUO shard cannot boot without one.
- **Troubleshooting** gains the failure mode that did not exist before: a bestiary with
no pictures, or items reading as numbers.
- **Maintenance** gains the button to press after patching a client, and **Verify the
whole stack** the `doctor` check that landed with it.
- The spawn atlas and the marketplace both carry artwork now, so both capability
descriptions say so, and `canonicalDocs` points at `link/v8.md`.
## Screenshots, retaken against the real thing
The atlas and marketplace shots predate the asset plane, so both were retaken on the
demo deployment against a real shard and a real sidecar, plus the new admin screen:
1,095 portraits imported in 2.1 s, 67,496 names in 1.4 s, 313 item pictures in 0.8 s.
Nothing here is a mock-up. The atlas shot moved 36px down its page because portraits
made the rows taller.
## One defect found on the way
`seedDemo.mjs` pinned the demo deployment at **protocol 4** with a comment explaining
a `module-uo` debt that has since been paid. Its default now comes from
`platform.json`, so the rig cannot drift two protocols behind the platform again.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Phase 16c of the events plan: `runicgateway.com`'s half of the workstream, now
that `main` carries the engine, the module, the app and the bundle.
The checks were already red and named their own answers:
* `checkFacts` — nine values had moved. Protocol 5 → 7 in all three declaration
sites, `moduleApi` 1.9.0 → 1.10.0, the bundle to 2026.09.10 with sidecar
v2.2.0 and overlay v1.2.0, `link` v2.2.0, `Module-uo` v1.2.2.
* `checkReference` — twenty-seven `Bridge.cfg` keys the site listed nowhere: the
events switch and its sweep, the ten caps, the oracle NPC, the two lease keys
and the seven participation keys. They are five new groups rather than an
appendix to an existing one, because `EventsEnabled` is a second consent
switch and belongs beside its own ceilings.
Two pages, matching the treatment Teams has:
* **Scheduled events** (Administration) — where it is and who sees it, authoring
and immutable versions, the switchboard that arrives off, caps as a condition
on an `UPDATE` rather than a role check, the dry run, the run console, what an
event owns versus what it borrows, generated cleanup, the shard's own switches,
and what a player sees.
* **Events architecture** — the two sentences it turns on, what is a table and
what deliberately is not, budgets in SQL, the ledger's two rules, at-most-once
on a wire that can lose an answer, the three layers, and the four omissions.
And the rest of the surface:
* `/privacy` gains **`deploy-events`** — the participation ledger is personal
data and no row named it. Scores and ranks against a module-opaque member key,
linked to an account where one is linked; the diagnostic log swept after 90
days on terminal runs only; the run, its steps and its participants not swept
at all, because they are the record of what was done to a shared world.
`deploy-game-data`'s citation moves from `link/v4.md` to `v7.md`.
* **Protocol versions** — the most recent bump touched *five* repositories, and
the `website` row is the interesting one: core is normally out of a protocol
bump's reach and this one reached it, because what changed was not a game noun
but the shape of a thing core owns the ledger for. The store-migration
paragraph now says four bumps' worth rather than two.
* Two capability entries, so `/`, `/features/` and `/modules/` stop omitting the
subsystem — an Administration item, and an **Event calendar** under Community
with `/site/events` as its deep link. Deliberately *not* `needsModule`: a bare
core can author and run an event, and only the world verbs need a module.
* **`reference/event-catalog` is retitled "Shard event catalog"** and says what
it is not. Two things in the docs were called an event catalog; the route is
unchanged, so nothing outside this repository breaks.
* `canonicalDocs` gains `website/EVENTS.md` and moves `link/v4.md` → `v7.md`.
No screenshots. Capturing the events surfaces means standing the whole rig back
up — game server, sidecar, core, module, a published event with a live run — for
two or three images that no check requires, and the engagement workstream's own
site leg added none either.
`npm run verify` green end to end, including `checkReference` against the
protocol spec that only reached `docs` `main` in RunicGateway/docs#232 — the
seventh cutover step, which 16b had left on `edge`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
ENGAGEMENT.md Phase 14. The deploy-engagement entry in the collection
inventory said "Kept until the operator removes them; nothing here expires
on its own" and, in its detail, "the delivery log, the suppression list and
the per-person rate limits have no retention sweep, so they are as long as
the site is old". That was the true answer until Phase 14 landed a sweep,
and it is the kind of sentence a Play reviewer reads.
It now describes what the code does: a nightly sweep with an
operator-settable horizon per table (180 days for the delivery log, 30 for
the queue and the rate limits), and the two things that deliberately do NOT
expire — an item still waiting to be sent, because it is a message the site
still intends to deliver, and the suppression list, because ageing an entry
out would mean mailing an address that already bounced.
PLAY_DATA_SAFETY.md is unchanged and check:datasafety stays green:
deploy-engagement is deployment-scoped, and the Play answers are generated
from the app-scoped entries, because Play asks what the app collects rather
than what a self-hosted deployment keeps.
All ten checks green, 42 tests pass.
Co-Authored-By: Claude <noreply@anthropic.com>
Phase 13 landed while this branch waited: link/servuo-plugins/website/Module-uo
are all on main, the bundle was republished at 2026.09.01, and Module-uo cut
v1.1.0. These are the four values PLAN.md 12 said could not be written until the
republish existed, plus the two releases that moved with it.
Every one is the value checkFacts.mjs itself reports as the authority's answer:
bundle.tag 2026.08.19 -> 2026.09.01
bundle.sidecar v2.0.0 -> v2.1.0
bundle.overlay v1.0.0 -> v1.1.0
releases.link v2.0.0 -> v2.1.0
releases.Module-uo v1.0.2 -> v1.1.0
verifiedOn 2026-08-19 -> 2026-09-01
No page hardcodes any of them -- every quote is an interpolation of
platform.bundle.* or platform.releases.*, so there is nothing else to re-read.
Co-Authored-By: Claude <noreply@anthropic.com>
Engagement Phase 12b. **This branch is deliberately red and must not be merged until the
Phase 13 cutover has landed link and servuo-plugins on `main` and CI has republished the
bundle.** checkFacts.mjs fetches every value from the source repo's `main`, so it fails
today exactly as designed:
FAIL protocol (sidecar) platform.json 5 link main 4
FAIL protocol (overlay) platform.json 5 servuo-plugins main 4
FAIL moduleApi platform.json 1.9.0 website main 1.6.0
FAIL protocol (bundle) platform.json 5 installer bundles:current.json 4
Every other check is green on this branch: checkLinks 2503, checkReference 22, checkA11y,
checkCsp, checkSidebar, 42 tests.
moduleApi is **1.9.0**, not the 1.7.0 the plan named -- Phase 11 moved it twice after that
sentence was written (1.8.0 for the `admin` ceiling, 1.9.0 for the module seed API).
## Also: the one page whose whole subject is the protocol number was the one page that
## hardcoded it
platform.json's own header says no version number is ever hardcoded in prose, and
`architecture/protocol-versions.mdx` had `4` written out three times -- the headline
sentence and both declaration-site rows. It now imports platform.json like every other
page that quotes a fact, so it moves with the JSON and cannot say 4 while the JSON says 5.
Note the interpolation is OUTSIDE the code spans: MDX does not evaluate an expression
inside backticks, so `PROTOCOL_VERSION: u32 = {platform.protocol}` would have rendered the
braces literally. Verified in the built HTML -- "currently 5", and no `platform.protocol`
survives anywhere in the output.
The same page's "what a bump obliges" section said version 4 was the first bump to need a
store migration, which read as though every bump does. v5 needed none (it only widens
frames the store already keeps whole -- docs link/v5.md), so the sentence now says which
did and which did not, and v5.md joins v4.md under canonical documents.
## The fill-in step, at merge time
Three values are not knowable today because the artefact does not exist yet. After the
bundle republishes, run:
GITEA_TOKEN=<token> node scripts/checkFacts.mjs
and copy what it prints in the "says" column into src/data/platform.json:
* bundle.tag, bundle.sidecar, bundle.overlay -- from installer bundles:current.json
* releases.Module-uo -- if the cutover cuts a new module release
* verifiedOn -- the date you ran it
Then re-read `getting-started/connect-a-game-server` and `administration/the-shard-connection`,
which quote the bundle, before merging. The check is green when all 19 agree.
AI-assisted: written with Claude Code.
Co-Authored-By: Claude <noreply@anthropic.com>
Engagement Phase 12a. The site had pages for where a message goes (Notifications and
email) and what it says (Message templates), and nothing at all for what makes one get
sent -- the four Engagement screens the workstream built.
New page: Engagement rules. Rules, Audiences, the trigger catalog and the send log on
one page, sitting between the two it joins up. Templates already has its own page and
Suppressions is in Troubleshooting, so neither is repeated here.
Two things it exists to state plainly:
* Every rule ships disabled, including the ones a module brings. "Installed" is not
"on", and an upgrade whose Team mail went quiet is the same fact.
* The ceiling is a TREE, not a ladder. The tempting reading -- a staff-only event
could obviously also go to one person -- is wrong, and the example is the argument:
"one person" for cheat detection is the player it was detected on.
Troubleshooting gains the symptom that page answers ("nothing is sent for one
particular event"): the rule is off, the rule is dormant, its own cooldown held it, or
the audience is empty.
Privacy: two rows the engagement work makes necessary, and one sentence it made false.
* app-content claimed "Nothing is cached for offline use". Phase 8 shipped a DataStore
snapshot of the inbox, so it was untrue -- and that row feeds the generated Play Data
Safety answers, which is a store-review matter rather than a doc nit. The snapshot now
has its own row and its own Play mapping (Messages / Other in-app messages; not
collected by us, stored on the device), and app-content's claim is narrowed to
everything else.
* deploy-engagement, for the deployment scope: an address is now used for more than
getting into an account, there is a delivery log holding a one-way hash of it, and
there is a suppression list. Its retention line says what is true rather than what a
reader assumes -- none of these tables has a retention sweep.
PLAY_DATA_SAFETY.md regenerated from the inventory; legal.lastUpdated moved with the page
it dates.
Verified: the whole `verify` chain green -- checkSidebar (plannedSidebar moved with the
live tree), checkFacts 19/19, checkQuickstart 59, checkReference 22, checkLinks 2605,
checkA11y, checkCsp, playDataSafety --check, 42 + 7 tests. Read in a browser as well, in
the served build.
AI-assisted: written with Claude Code.
Co-Authored-By: Claude <noreply@anthropic.com>
Engagement Phase 7 gives the platform a third notification channel — an inbox
on the site itself — so `notifications-and-email.mdx` gains a section for it:
what it is, that it is the one channel on by default, that its body is always
plain text, and that the nightly prune takes read items only.
Two corrections in the same file, both of which were already false before this
phase and would have become misleading with it:
- "Who receives what" said push was opt-OUT. Push stream subscriptions have
always been opt-IN, and engagement Phase 3 made that explicit in the channel
registry. Rewritten as three defaults plus the Team mute that overrides all
three, and pointed at the preferences grid the same phase gave the web.
- `capabilities.mjs` claimed "Web, push and email … push arrives by default".
The web channel did not exist until now and push has never arrived by
default. Reworded to name the on-site inbox as the opt-out one.
The push section now says what a tickle raised by an engagement rule carries,
and that it is still only a pointer.
Code: RunicGateway/website#TBD · Docs: RunicGateway/docs#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
All three links under "Documentation" pointed at /docs/. Three labels — Getting
started, Administration, Building a module — and one destination, which is the
docs home and also what the header's Docs link already opens.
They now land inside the section they name:
Getting started /docs/getting-started/requirements/
Administration /docs/administration/configuration/
Building a module /docs/modules/building-a-module/
The docs home stays the header's link rather than becoming a fourth route to the
same page.
Eleven checks and two suites could not see this, and the reason is worth keeping:
the bug is not a broken link. checkLinks resolves every internal href against the
build and /docs/ resolves — three links to a page that exists are three valid
links. checkSidebar compares the docs tree to the planned tree and never looks at
the footer. checkA11y checks structure, and three correctly marked-up anchors are
correct markup. Nothing asserted that a link goes where its label says.
So the columns move to src/data/footer.mjs, beside legal.mjs and collection.mjs,
and test/footer.test.mjs asserts it: every destination in a column distinct, no
destination repeated across columns, each documentation link inside its own
section prefix, none of them the docs home, and the two Project links still read
from the brand. A column list inside an .astro component cannot be imported by a
test, which is the whole reason for the move.
Verified by reintroducing the bug: the suite fails with "Administration points at
/docs/, which is not inside /docs/administration/". Restored, npm run verify is
green — 42 tests, eleven checks, and the built index.html renders three distinct
hrefs.
Co-Authored-By: Claude <noreply@anthropic.com>
The org lead signed a character into the shard by hand — the step automation
could not reach — so shard-status and app-shard were retaken. The shard page
now reads 1 player online in Britain, and the app's card agrees.
Two things worth noticing in the retake. Presence reaches the public page as
counts and regions rather than names, which is the visibility framework working
unprompted. And the guild board's online column did not move: it refreshes only
when a guild's signature changes, which is the stale-roster defect wearing a
different hat.
A sixth defect, visible in the shipped phone capture: the app interpolates a
count into a fixed plural and says "1 players online". Raised, not fixed —
it wants a plurals resource in the app.
Co-Authored-By: Claude <noreply@anthropic.com>
D4 asked for screenshots of the review stack rather than placeholders. Seventeen
of them: eleven of the site in a browser, six of the app on a phone, all from one
demo deployment wired to a running ServUO shard over a real sidecar, captured on
one day (D42).
The deployment is branded "Runic Gateway Demo" rather than a real community (D43),
and the captures sit beside the claims they support — the homepage, /features/, and
five of the administration pages phase 7 could describe but not show (D44).
The rig is committed rather than remembered (D45):
scripts/seedDemo.mjs content, by driving the site's own API — never SQL,
because a row the product could not have produced is
a screenshot of a product that does not exist
src/data/screens.mjs every capture: route, viewport, scroll, alt, caption
scripts/captureScreens.mjs npm run screens:capture
scripts/checkScreens.mjs the ninth check script, in CI
Shard-side dressing is servuo-plugins' scaffolding (D46), never deployed.
The rig found five things nothing else had. One is fixed upstream — a fresh
module-uo install pinned wire protocol 3 against a sidecar speaking 4, released as
v1.0.2, which this repo's own facts check then caught in platform.json. Four are
raised as product observations and worked around in the rig: a renamed guild
member never reaches the site, a guild deleted while the shard is down is a ghost
row forever, "Houses in danger" cannot show a house that was already collapsing,
and the app's news list prints raw ISO timestamps.
Players online reads 0. Logging a character in needs a UO client driven by hand,
and that is where this stopped — PLAN.md §10 says exactly why, and how to retake
the two frames that would change.
Co-Authored-By: Claude <noreply@anthropic.com>
Twenty pages completing the tree section 10 planned: Modules (8), Architecture
(5) and Reference (7). Four decisions, D38-D41, recorded in PLAN.md section 10.
D39 is the one that shaped the phase. Section 1 forbids re-specifying a
contract, and a Reference section is exactly where that rule is most tempting to
break, so the line is drawn at names: every environment variable, config key,
installer command, visibility rung and canonical document is listed with one
terse line saying what it is FOR, while shapes, semantics and every "why" stay
in the canonical document.
That is only safe because the names are checked. checkReference.mjs compares six
enumerations against the repositories that own them, over the Gitea API, as set
comparisons in BOTH directions -- and the second direction is the one that earns
its keep, because a reference page does not usually rot by describing something
that vanished, it rots by quietly not mentioning what was added since.
The check went green on its first run, which is the least trustworthy possible
outcome, so it was verified by breaking it: seven mutations, all caught. The one
worth keeping is the visibility ladder REORDERED with its membership unchanged
-- it is a security boundary, and a set comparison alone would have passed it.
D41 turns plannedSidebar from a checklist into a checked invariant, and finding
out why was the phase's first defect: it had already drifted, because phase 7
added the Content page under D37 and never updated the list. Nothing failed,
because nothing read it. checkSidebar.mjs now asserts the two trees agree on
groups, labels and order -- order because the order of Getting started IS the
installation path.
Two more things the writing found. PLAN.md's page count was wrong and had been
since section 10 was written ("roughly 38, 37 planned" for a tree of forty).
And module.json's `mounts` and the SPA's paths are different mechanisms that no
single document stated plainly -- module-uo declares admin: ["/shard",
"/uo-link"] while its screen lives at /admin/uo/link, because API routes are
deliberately NOT namespaced while SPA routes are. That is precisely the
distinction the installer got wrong in v0.1.0, and it now has a named home.
D40: the docs link to /architecture/'s drawn diagrams rather than importing
them. Those components carry marketing chrome and depend on diagram.css, which
Starlight does not load; the docs use text diagrams, which paste into an issue.
npm run verify green: 40 pages across 5 groups agree with plannedSidebar, 2390
internal links resolve, 123 repository links point at a branch, 19 facts, 59
quickstart checks, 22 reference enumerations, astro check 0 errors, 36 tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Recovering installer's orphan tag published v0.1.1, which moved the platform
under this branch and turned checkFacts red -- the check working exactly as it
should, since a version this site quotes had changed.
FAIL release installer
platform.json says : v0.1.0
installer releases/latest says : v0.1.1
The 500 that orphaned the tag was a race with the tag push one second earlier,
not a structural failure: re-running the workflow took the built-in orphan-tag
recovery path and published all four assets unchanged.
So the stale-path Aside stops saying "v0.1.0 is still the current download",
which is no longer true, and says the durable thing instead -- v0.1.0 prints
the old path, v0.1.1 prints the real one -- which stays correct however many
releases follow. platform.json and the PLAN.md version table move to v0.1.1,
and the phase 7 findings record the pipeline defect as a fourth finding.
Co-Authored-By: Claude <noreply@anthropic.com>
The three defects phase 7 found are fixed and merged: website#163
(SECRET_ENC_KEY missing from the root .env.example, plus BOT_INTERNAL_KEY in
the README's "set at least" list) and installer#22 + docs#174 (the handoff
printing /admin/shard).
website#163 turned checkQuickstart red here, which is precisely what the
declaration was built to do -- it fails the moment a declared key appears
upstream, so the note describing the omission cannot outlive the defect. The
SECRET_ENC_KEY entry is deleted and notInUpstreamEnvExample is now empty; the
export stays so the next divergence gets an entry rather than passing quietly.
The stale-path Aside on Connect a game server is pinned to v0.1.0 rather than
calling the installer permanently wrong, and now says WHY the old path is worse
than a 404: the SPA has no route for it, so it redirects to the dashboard and
the link looks like it worked.
v0.1.0 is still the current download, and not only because releases lag. The
release run for installer#22 built every artifact and pushed tag v0.1.1, then
took a 500 creating the release -- so the tag is orphaned and no binaries were
published. Raised on installer; nothing is worked around here.
This also recovers 084ee0b, which was pushed to feat/phase-7-docs after PR #10
had already merged f499f2b, and so never reached main.
Co-Authored-By: Claude <noreply@anthropic.com>
Twenty documentation pages: Getting started (7) and Administration (13), the
journey no existing document owns end to end because the repositories are
organised by component and an operator is not.
Four decisions of record, taken before anything was written (D34–D37, PLAN.md
§10 "How phase 7 built the documentation journey"):
- D34 one PR for all twenty pages.
- D35 the install page is SELF-CONTAINED: it prints a complete Compose file and
a complete .env that an operator copies without visiting another repository.
That is a copy of somebody else's file, so it is checked rather than trusted —
scripts/checkQuickstart.mjs re-reads website main:docker-compose.yml and
main:.env.example over the Gitea API and fails on any disagreement, in both
directions: a value that drifts fails, and a service or variable that appears
upstream fails until it is either included or recorded as deliberately omitted
with a reason. Its first run found two stale entries.
- D36 every Administration screen was walked on a real deployment before it was
described — the rig being the quickstart itself, against the published image,
so one run proved the install page and produced the detail the admin pages
needed.
- D37 a thirteenth Administration page, Content, so that every admin nav row has
a home without organising the docs by the app's menu.
What the live deployment disproved, all three now documented:
- The documented Compose deploy does not boot. SECRET_ENC_KEY is required in
production (utils/secretBox.js throws at require time) and is missing from
website's ROOT .env.example — the file Compose reads. It is present in
server/.env.example, which is why dev never hits it. The quickstart carries it,
declared as an upstream omission so the check fails the day it is fixed.
- The installer points operators at a screen that no longer exists: it prints
<site>/admin/shard, and INSTALL.md §5 repeats it, but since the module cutover
the screen is /admin/uo/link. Both the binary and the guide are stale.
- The admin Restart button opens a window.confirm whose text is the honest
warning that a deployment with no supervisor does not come back — which is why
`restart: unless-stopped` is called out as load-bearing rather than left as
boilerplate.
And the defect only a look found, three phases running: the .env block's prose
promised that every highlighted line must be changed, while `mark` given the
variable names highlighted the names alone and left the values unmarked. Every
check passed on a page that was wrong about its own highlighting.
verify green: 890 internal links, 52 branch links, 19 facts, 59 quickstart
checks, 0 astro-check errors.
Co-Authored-By: Claude <noreply@anthropic.com>
PLAN.md §9. Builds /privacy and /terms, links them from the footer on every page,
and generates the Play Data Safety notes from the same inventory the policy renders.
Four decisions taken by the org lead before either page was written, recorded in
§9 under "How phase 6 built the legal pages":
D30 DNS-only records, so the reverse proxy on the host keeps the only access
log. Described qualitatively — the retention belongs to the proxy, and a
policy that quotes a number the deployment does not enforce is worse than
one that does not.
D31 Eighteen or older. Above the children's-consent threshold everywhere in the
EEA, so consent works with no parental-consent machinery this form could not
honestly operate. Four surfaces render it from src/data/legal.mjs, and every
one says plainly that nothing verifies it.
D32 No governing-law clause. Nothing of value is contracted for here.
D33 PLAY_DATA_SAFETY.md is generated from src/data/collection.mjs and checked in
CI, so the published policy and the answers given to Google cannot drift.
/privacy is three separately-scoped sections because "we" means three different
parties: this site (one form, no cookies, no third-party requests), the Android app
(we operate no server it talks to — the rows are what the DEVICE holds), and a
self-hosted deployment (the operator is the controller, not us). Every row names the
file it was read out of, because a policy is the document most likely to be written
from a template and least likely to be re-read against the software.
/terms governs only what we run: this site, the beta list, and the APK we publish.
The software is governed by its licence, and a community's deployment by that
community — a terms page claiming authority over every install of a GPL program is
the thing a generated template gets wrong.
Also here:
- the age clause changed CONSENT_TEXT, so CONSENT_VERSION gained a suffix; rows
written from now on carry the new sentence and older rows keep theirs
- PLANNED_ROUTES is now empty — these were its last two entries, and its reverse
check is what forced the deletion; the list stays for phases 7 and 8
- test/legal.test.mjs asserts the structural promises no build check can see,
including that every mapped Play row still answers "not collected, not shared"
- --check normalises line endings: the repo has no .gitattributes and Windows
checkouts are CRLF, so a byte comparison would fail for every Windows developer
while passing in CI
Verified: npm run verify green end to end (tokens, brand, data safety, astro check,
36 tests, build, 214 links, 19 facts), both pages walked in a browser, and neither
overflows at 390px. One defect the checks could not see and a look could: the
retention line was being pushed to the foot of the tallest card in its row, opening
a void in the middle of the short ones.
Co-Authored-By: Claude <noreply@anthropic.com>
Builds `/app/` and `/beta/`, the SQLite signup store, the rate limiting and the
export CLI of PLAN.md §8, and adds this repository's first test suite.
Four decisions of record, D26–D29 (§8, "How phase 5 built the app and the beta"):
- D26 — the screenshot slot ships empty, reserved for phase 9. §10 promised
`/app/` "the 14 existing screenshots"; they are a July trusted-device smoke
test against an unseeded dev instance, captured before the theming work, and
five of the fourteen are two-factor prompts. Shipping them would break D4.
Phase 9 already builds the rig, so it gains an emulator pass.
- D27 — the public demo is the tester target. `ConnectScreen.kt` gates the whole
app on a validated deployment address, so a tester needs somewhere to point it.
The beta therefore waits on the demo VM, and the page says so.
- D28 — `/beta` handles its own POST; there is no `/api/beta-signup`. An endpoint
cannot report a validation error without JavaScript. §6's diagram is amended.
- D29 — the APK and the beta get equal billing, and the APK link is off:
`androidApk.serviceable` is false because the published v0.5.0 build does not
work. The panel stays and states that plainly rather than being removed.
Three mechanisms the plan did not anticipate:
- `liveBrand()` — a server-rendered page never passes through the boot rewrite,
so `/beta` reads the mounted brand.json itself. Pasting the Play opt-in URL in
takes effect on the next request rather than the next restart.
- `checkLinks.mjs` derives on-demand routes from `prerender = false` in the
source. A PLANNED_ROUTES entry would have been wrong: its reverse check fires
when a route has been built, and an on-demand route never produces a file, so
the entry could never rot out.
- `npm test` — the five existing checks all read built output, and none of this
logic appears there. A honeypot can stop working and leave the build identical.
Also: `checkFacts.mjs` gains the APK assets and `minSdk`, and learns that RFC 2606
reserved domains are not contact addresses; the D13 rule is otherwise unchanged.
Verified end to end against the built server: every outcome renders with no
JavaScript, cross-origin POSTs are refused, a mounted opt-in URL appears without
a restart, and the export CLI round-trips.
Co-Authored-By: Claude <noreply@anthropic.com>
PLAN.md §13 phase 4: /features/, /architecture/, /modules/, /integrations/, and
/community/ — plus the two scope items the phase table never assigned to anyone.
Six decisions taken by the org lead before coding, recorded in PLAN.md §10 as
D20-D25:
- D20 /features/ is the homepage's list with a `detail` line, not a second list.
One data file, two renderings, so they cannot disagree about what exists.
- D21 /architecture/ draws reasons, not reference: three new inline SVGs, one per
boundary. No endpoint tables, no config keys — those are phase 8's and stay
canonical in docs/.
- D22 The deliberate absences of §2 become one tagged data file, rendered on the
three pages that promise them.
- D23 Phase 4 absorbs /community/ (specified in §10 and §14 N3, linked from the
header since phase 1, built by no phase) and checkLinks.mjs.
- D24 `needsModule`: writing the Teams detail exposed a false claim phase 3
shipped. Teams are module-sourced only — teams.module_id is NOT NULL, there is
no create route, sync is gated on providerModuleId() — so the Community group
no longer says a bare core does all of it.
- D25 The per-capability demo affordance brand.json had promised since phase 2 is
a deep link, filled at boot from data-demo-path.
checkLinks.mjs reads the built HTML rather than src/, because half these links
are assembled from data files and template literals. Its PLANNED_ROUTES list is
checked in both directions, so it cannot rot into a permanent exemption.
applyBrand.mjs gained a pass that recomputes deep links from their immutable
path, making it idempotent and reversible; checkBrand.mjs lifts that pattern out
and runs it against the stock markup so the two cannot drift. Both proved
against a real mount, in both directions.
Fixes a cascade bug the checks could not see: [data-demo-url=''] and a scoped
component class are both specificity 0,1,0, so .demo-link's `display` beat the
hide rule and twelve links to a nonexistent demo rendered, each resolving to the
current page. The rule is now !important.
The four diagrams' shared SVG vocabulary moved to src/styles/diagram.css.
Verified from a clean checkout: npm ci, all five checks, astro check (0 errors),
production build, and a live browser pass at desktop and 390px.
Co-Authored-By: Claude <noreply@anthropic.com>
Replaces phase 1's scaffold with the real homepage: hero, the data path as
inline SVG, the self-hosted argument, all five capability groups, and the
get-started CTA. Three decisions the org lead took first are recorded in
PLAN.md as D17-D19.
The data path is drawn generically and captioned specifically (D17): the nodes
say "your game server" and "sidecar", the sub-labels and caption name ServUO and
uo-link. The SVG is aria-hidden because the four numbered steps beside it carry
the same path in prose — one telling, not two.
The capability list is data with a check behind it (D18). Every Game-intelligence
item names the module-uo capability slug it comes from, and the build fails if
the page and platform.json disagree either way. That needed a fifteenth fact in
checkFacts.mjs: §12 named the capability list as an externally-sourced fact and
nothing re-read it, so the chain rested on someone remembering. It also found
that the site was omitting two of the module's eight capabilities — guilds and
city governors are now listed, in the page and in §10.
The hero leads with the emblem (D19), derived from whichever logo.png is in
force so one file still changes the hero, header, tab icon and app icon
together.
Also here, both found by standing the build up rather than by review:
- checkBrand.mjs now enforces the demo slot's markup contract. applyBrand.mjs
reveals the demo link by replacing an exact pair of empty attributes; an
attribute inserted between them produces a build where the mount sets a demo
URL, the boot log says nothing and the link never appears. Both halves are
checked and the literal is derived from the expression applyBrand.mjs uses,
so they cannot drift.
- The header nav overflowed at 390px — four links plus the lockup measured
433px against a 390px viewport, so every phone got a horizontally scrolling
page. Phase 1 left this to phase 3 expecting a disclosure control; it got a
wrap instead, because with four links there is nothing to disclose and a
hamburger costs state, script and duplicate markup.
Verified on a clean checkout of this commit: all four checks, astro check, a
production build, a live /brand/* smoke, and a demo URL mounted and reverted.
Co-Authored-By: Claude <noreply@anthropic.com>
`data/` without a leading slash matches a directory of that name at any
depth, so it silently swallowed src/data/platform.json -- the single file
every page and checkFacts.mjs reads.
The working tree still had it, so `npm run verify` passed locally with all
14 facts green. CI cloned fresh and `astro check` failed on the missing
module. Anchored both patterns to the repository root.
Verified the way it should have been the first time: exported HEAD to a
clean directory, npm ci, and ran the checks there rather than in the tree
that was hiding the problem.
Co-Authored-By: Claude <noreply@anthropic.com>