docs(site): the engagement system — cutover 7 of 7 (edgemain) #28

Merged
whitlocktech merged 17 commits from edge into main 2026-09-01 18:04:23 +00:00
Member

Engagement Phase 13, step 7 of 7 — the last one. The public site, which the merge order put last because every fact it fetches has to be true on main first. It now is: steps 1–6 are merged, the bundle was republished as 2026.09.01, and checkFacts agrees with all nineteen authorities.

17 commits, 18 files, a clean fast-forward (main is 0 commits ahead). Seven PRs, #21 through #27.

What lands

Area What
platform.json protocol 4 → 5, Module API 1.6.0 → 1.9.0, bundle 2026.08.19 → 2026.09.01 (sidecar v2.1.0, overlay v1.1.0), link v2.1.0, Module-uo v1.1.0
Administration two new pages — Message templates and Engagement rules — plus rewrites of Notifications and email (Gmail's connect flow is gone; SMTP is the baseline), Teams (Team email is engagement rules now), Troubleshooting (suppression, bounces) and Configuration
Architecture Protocol versions stops hardcoding the number it is about; System architecture follows the transport change
Legal /privacy and the generated Play Data Safety answers, re-derived from one inventory that an engagement mailer changes
Checks the three cross-repository checks read Gitea's contents endpoint instead of raw

This is the first CI run these checks have ever had

pr-checks.yml triggers on PRs into main only, so not one of the seven phase PRs was gated — the same shape as android-app's trigger before Phase 8 fixed it. Everything below was run locally against this head instead, and is green:

checkSidebar   5 groups, 42 pages        checkFacts       19/19 vs their authorities
checkScreens   17 screens                checkQuickstart  59 vs website main
checkTokens    56 files                  checkReference   22 enumerations
checkBrand     19 /brand/ URLs           checkA11y        52 pages
checkLinks     2605 links                checkCsp         52 pages, 538 inline hashes
unit tests     42 pass                   served headers   7 pass

The one thing worth reading twice

checkFacts failed this branch for a moduleApi that was correct. Gitea's raw API route answers Cache-Control: public, max-age=21600, so the CDN served a copy of website's version.js from two weeks earlier (cf-cache-status: HIT, Age: 15713) and no edit here could have made it pass. The same run reported the already-republished bundle triple as still current — a stale read fails both ways, and the false pass is the dangerous half for a repo whose whole bargain is going red when the platform moves.

contents answers private, must-revalidate, which the CDN bypasses. Without that fix this PR would have been red for a reason nobody could find in the diff. PLAN.md §12 records it next to the check it constrains.

  • AI-assisted: written with Claude Code.

🤖 Generated with Claude Code

**Engagement Phase 13, step 7 of 7 — the last one.** The public site, which the merge order put last *because every fact it fetches has to be true on `main` first*. It now is: steps 1–6 are merged, the bundle was republished as **2026.09.01**, and `checkFacts` agrees with all nineteen authorities. 17 commits, 18 files, a clean fast-forward (`main` is 0 commits ahead). Seven PRs, #21 through #27. ### What lands | Area | What | | --- | --- | | **`platform.json`** | protocol **4 → 5**, Module API **1.6.0 → 1.9.0**, bundle **2026.08.19 → 2026.09.01** (sidecar v2.1.0, overlay v1.1.0), `link` v2.1.0, `Module-uo` v1.1.0 | | **Administration** | two new pages — **Message templates** and **Engagement rules** — plus rewrites of *Notifications and email* (Gmail's connect flow is gone; SMTP is the baseline), *Teams* (Team email is engagement rules now), *Troubleshooting* (suppression, bounces) and *Configuration* | | **Architecture** | *Protocol versions* stops hardcoding the number it is about; *System architecture* follows the transport change | | **Legal** | `/privacy` and the generated **Play Data Safety** answers, re-derived from one inventory that an engagement mailer changes | | **Checks** | the three cross-repository checks read Gitea's `contents` endpoint instead of `raw` | ### This is the first CI run these checks have ever had `pr-checks.yml` triggers on PRs into `main` only, so not one of the seven phase PRs was gated — the same shape as `android-app`'s trigger before Phase 8 fixed it. Everything below was run locally against this head instead, and is green: ``` checkSidebar 5 groups, 42 pages checkFacts 19/19 vs their authorities checkScreens 17 screens checkQuickstart 59 vs website main checkTokens 56 files checkReference 22 enumerations checkBrand 19 /brand/ URLs checkA11y 52 pages checkLinks 2605 links checkCsp 52 pages, 538 inline hashes unit tests 42 pass served headers 7 pass ``` ### The one thing worth reading twice `checkFacts` failed this branch for a `moduleApi` that was correct. Gitea's `raw` API route answers `Cache-Control: public, max-age=21600`, so the CDN served a copy of website's `version.js` from two weeks earlier (`cf-cache-status: HIT`, `Age: 15713`) and no edit here could have made it pass. The same run reported the *already-republished* bundle triple as still current — a stale read fails **both** ways, and the false pass is the dangerous half for a repo whose whole bargain is going red when the platform moves. `contents` answers `private, must-revalidate`, which the CDN bypasses. Without that fix this PR would have been red for a reason nobody could find in the diff. `PLAN.md` §12 records it next to the check it constrains. - [x] AI-assisted: written with Claude Code. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 17 commits 2026-09-01 18:00:43 +00:00
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>
Reviewed-on: #21
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>
Reviewed-on: #22
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>
Reviewed-on: #23
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>
Reviewed-on: #24
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>
Reviewed-on: #25
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 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>
Reviewed-on: #26
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>
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>
Reviewed-on: #27
whitlocktech merged commit a0af01026e into main 2026-09-01 18:04:23 +00:00
whitlocktech deleted branch edge 2026-09-01 18:04:26 +00:00
Sign in to join this conversation.
No description provided.