The three checks that read another repository -- checkFacts, checkQuickstart,
checkReference -- fetched source files from the API's `raw` route, which answers
`Cache-Control: public, max-age=21600`. The CDN in front of Gitea caches that,
so the checks can read a blob most of a working day old.
It bit on cutover day. checkFacts reported
FAIL moduleApi
platform.json says : 1.9.0
website main:server/src/modules/version.js says : 1.6.0
against a `main` that says 1.9.0 -- the served copy was two weeks old
(`cf-cache-status: HIT`, `Age: 15713`, `last-modified: 18 Aug`). No edit in this
repository could have made it pass, and the same run reported a bundle triple
that had already been republished as still current: a stale read fails BOTH
ways, and the false pass is the dangerous one.
The `contents` endpoint answers `private, must-revalidate`, which the CDN
bypasses, so it is always the ref's current blob. The cost is a JSON parse and a
base64 decode. checkReference's canonical-document existence loop already used
it, which is why that half was never affected.
PLAN.md 12 records the finding next to the check it constrains.
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.md §6.0b assigns this repo the operator-facing half of Phase 9
(website#176 + docs#191). Two new sections, split along the line that actually
matters when somebody reports it.
"One person stopped receiving email" is the suppression case, and the three
things an operator gets wrong about it: they can still reset their password
(suppression scopes to engagement rules only, so that is the expected shape of
the problem rather than a contradiction); Not sent, Bounced and Failed in the
Send Log mean three different things and only one of them is about your
configuration; and no row at all means they were excluded before anything was
queued, by an opt-in or by the verification gate.
"Everyone stopped receiving email at once" exists to stop the wrong reflex. A
whole-deployment stop is never the suppression list — a wrong password never
suppresses anybody, only the receiving server naming a specific dead mailbox
does — and it says so before an operator starts clearing rows.
Also notes that lifting a suppression asks for the full address because
addresses are stored one way, so it reads as the privacy design rather than a
missing feature.
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>
The Teams page gains the section engagement Phase 6 owes it. The operator-facing
fact is the one that has to land first: Team emails used to send with no
configuration and now arrive switched off, so nobody gets them until a rule is
turned on in Admin → Engagement → Rules.
Also says what each of the four seeded rules sends and why the two roster ones
ship off, that a rule decides whether the site sends at all while members still
choose per Team, why a digest is re-read at send time, and that unsubscribe links
in mail already sent still work — now stopping the emails they came with without
also silencing that Team's push.
Site: RunicGateway/website#TBD · Docs: RunicGateway/docs#TBD
Co-Authored-By: Claude <noreply@anthropic.com>
The operator half of engagement Phase 5b, which 6.0b of ENGAGEMENT.md assigns to
this repo: what the Templates screen is for, how a shipped default is edited in
place without an upgrade taking the edit back, why variables are clicked rather
than typed, the two halves of every message, draft vs published, the preview and
its dark-mode approximation, test sends, duplicating to make a new template, and
the send log.
Written for someone running a site, not someone reading the design document: it
explains what to do and why the refusals exist, and names no version numbers - the
facts check reads authority from each repo's `main`, and none of this is there yet.
Co-Authored-By: Claude <noreply@anthropic.com>
Engagement Phase 1's share of this repo (docs/website/ENGAGEMENT.md
§6.0b). Four pages described a delivery path that no longer exists — one
of them under the heading "There is no SMTP option", which is now the
opposite of true.
notifications-and-email.mdx: the Email section is rewritten around the
three postures the org lead settled on (§7.1 Q5), leading with a relay
and naming smtp.gmail.com:587 with an app password as the migration off
OAuth2. Two cautions carry the failures that produce no error at all —
Implicit TLS left on for port 587, which hangs, and an operator-typed
sender the relay will not accept, which is an SPF/DMARC rejection that
looks like nothing. Send test is what proves both.
troubleshooting.mdx gains those two, plus the enabled toggle, which now
gates every message rather than some of them.
configuration.mdx loses the "set up Google first" ordering constraint,
which is gone with the borrowed client. system-architecture.mdx's
encrypted-at-rest list is corrected: the Gmail refresh token is replaced
by the transport credentials, which are write-only like the sidecar
token.
This lands on `edge`, so nothing here is published while `main` still
carries the Gmail flow. platform.json and capabilities.mjs are untouched
— they belong to Phase 12.
Co-Authored-By: Claude <noreply@anthropic.com>