docs(website): add the branching model, per-phase docs, and the site phases

Three additions the org lead called for, all in ENGAGEMENT.md.

The branching model (new §6.0a). Every phase PR in every repo targets `edge`;
`main` is touched exactly once, by the cutover. Two blocking findings checked
on 2026-08-28: every existing `edge` is stale (0 ahead of `main`, behind by 16
docs / 9 module-uo / 7 installer / 7 servuo-plugins / 5 website / 3 link) and
must be fast-forwarded before the first phase PR, and three repos have no
`edge` at all — android-app, runicgateway.com and Integration-kit. Also records
that android-app's pr-checks.yml triggers only on PRs into `main`, so Phase 8
lands with no CI and the cutover is its first real build, as happened to all
nine M12 phase PRs.

Documentation as a phase deliverable (new §6.0b). A phase-by-phase table
assigning the specific docs each phase owes, in `docs/` and in every other
repo, so nothing is deferred to a cleanup pass. §7.4 becomes the inventory
that table draws from rather than a list of things to do at the end.

Two new phases. Phase 12 is runicgateway.com, which is not optional polish:
scripts/checkFacts.mjs fetches each fact's authority from the source repo's
`main`, so `protocol` 4 to 5 and `moduleApi` 1.6.0 to 1.7.0 fail its build on
their own. The site also currently documents the opposite of what Phase 1
ships — notifications-and-email.mdx carries a "There is no SMTP option" aside —
its capabilities list claims a web notification channel that will not exist
until Phase 7, and PLAY_DATA_SAFETY.md and /privacy generate from one inventory
that an engagement mailer materially changes. Because checkFacts reads `main`,
the site stays green through the whole `edge` period and breaks at the cutover,
so Phase 12 must be written before Phase 13 and merged in the same window.

Phase 13 is the cutover itself, ordered rather than per-repo-independent: docs,
then servuo-plugins and link together (a protocol bump has three declaration
sites and a `main` holding a v5 sidecar with a v4 overlay cannot pair), then
website, module-uo, Integration-kit, android-app, and runicgateway.com last
because every fact it fetches has to be true on `main` first.

Adds an eighth open question (fix the Android CI trigger, or accept the cutover
as its first build) and a Phase -1 to the sequencing diagram for the edge
fast-forward.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-28 18:53:51 -05:00
parent 04863993aa
commit 80284c0a2e

View File

@@ -1,9 +1,13 @@
# The Engagement System — findings and plan
**Status:** design of record for the next workstream. No code written yet. The five scope decisions below
are settled; the seven questions in §7.1 are open and none of them block Phase 1 or Phase 2. Per CLAUDE.md
are settled; the eight questions in §7.1 are open and none of them block Phase 1 or Phase 2. Per CLAUDE.md
§ Conventions, no implementation starts without the org lead's approval of the phase it belongs to.
**Branching:** every phase lands on **`edge`** in its repo; `main` is touched once, by the cutover
(Phase 13). §6.0a records the blocking precondition — every existing `edge` is stale and three repos
have none.
**Scope decisions, settled by the org lead (2026-08-28):**
1. **The in-app channel is in scope.** It does not exist today and has to be built, not adapted.
@@ -1006,14 +1010,71 @@ Knock-on obligations:
## Part 6 — The phased plan
Same shape as the API v2 router-split plan: grouped, reviewable increments, one acceptance check per
phase, and an explicit note on which guardrails apply. **Every phase carries the standing obligations**
`npm test --prefix server` green, `npm run swagger` regenerated when a route changes,
`npm run routes:manifest -- --check` clean, `npm run check:modules` clean, a matching `docs/` edit,
Conventional Commits, the AI-disclosure trailer, and a branch cut from a freshly-pulled `main`.
phase, and an explicit note on which guardrails apply.
**Every phase carries the standing obligations**`npm test --prefix server` green, `npm run swagger`
regenerated when a route changes, `npm run routes:manifest -- --check` clean, `npm run check:modules`
clean, **the documentation edits §6.0b assigns it**, Conventional Commits, the AI-disclosure trailer,
and a branch cut from a freshly-pulled base.
**Stage A (12) is prerequisite. Stage B (36) is the engagement system. Stage C (78) is the in-app
channel. Stage D (9) is deliverability. Stage E (1011) is the shard enrichment and runs in parallel
from day one.**
from day one. Stage F (1213) is the public site and the cutover.**
### 6.0a The branching model — everything lands on `edge`, then one cutover to `main`
**Every phase PR in every repo targets `edge`. `main` is touched exactly once, by the cutover
(Phase 13).** This is the model the module system, protocol v3, Teams and the M12 theming workstream
each used, and it is the right one here for a specific reason: this workstream changes a wire protocol,
a module API version and the mail path simultaneously, and those three land in different repos on
different days. `main` must never hold a half-applied set of them.
**Two operational findings, both checked on 2026-08-28 and both blocking before Phase 1:**
1. **Every existing `edge` is stale.** `git rev-list --left-right --count origin/main...origin/edge`
says `edge` is **0 ahead** and behind `main` by: `docs` 16, `module-uo` 9, `installer` 7,
`servuo-plugins` 7, `website` 5, `link` 3. They are leftovers from previous cutovers that were never
refreshed after merging. **Fast-forward each `edge` to `main` before the first phase PR** — it is
lossless (0 ahead), and skipping it means the cutover diff carries stale content or conflicts that
have nothing to do with this workstream.
2. **Three repos have no `edge` at all** and need one cut from `main`: `android-app` (its M12 branch
was deleted after that cutover), `runicgateway.com`, and `Integration-kit`.
**Android CI does not run on `edge`.** `android-app/.gitea/workflows/pr-checks.yml` triggers only on
PRs into `main`, so every Phase 8 PR lands with **zero CI** and the cutover is the first real run. That
was true of all nine M12 phase PRs and it is true again here. Either fix the trigger as Phase 8's first
commit or budget for the cutover being the first honest build — decide deliberately rather than
discovering it.
**The cutover is per-repo but not independent.** Phase 13 names the order, because a `main` that has the
v5 sidecar and the v4 overlay is a shard that cannot pair.
### 6.0b Documentation is a phase deliverable, not an appendix
Every phase below owes specific documentation, and the phase is **not done until it lands in the same
PR** (or, for cross-repo docs, a companion PR in the same review window). CLAUDE.md's rule — "a code
change is not complete until `docs/` reflects it" — is the floor; this table is the assignment.
| Phase | `docs/` | Other repos |
| --- | --- | --- |
| **1** Remove Gmail OAuth2, SMTP | `website/BACKEND_DESIGN.md` §7 **rewritten** (not amended — it documents Gmail OAuth2 as *the* mechanism); route tables lose `/admin/email/connect/*` | `website/README.md` + `.env.example` wherever they point at Connect Gmail · **`runicgateway.com`**: `notifications-and-email.mdx` (its "There is no SMTP option" aside is now false), `configuration.mdx:62`, `troubleshooting.mdx:101`, `system-architecture.mdx:117` · a release note |
| **2** Trigger registry | `website/MODULE_API.md` §1.1 (**1.7.0** + correct the stale "1.6.0 has only ever been on `edge`" paragraph), §2.3 (`ctx.events`, `ctx.inbox`), §2.4 (`registerEventTriggers`), §7.3's dormant-rule note · `website/ENGAGEMENT.md` §4.3 kept true | `Integration-kit`: `ci/core-ref.json` re-pinned (the equality check goes red **on purpose**) + chapter 2 gains a "registering a trigger" section · **`runicgateway.com`**: `platform.json.moduleApi` → 1.7.0 |
| **3** Channel preferences | `website/BACKEND_DESIGN.md` route table · `android/PLAN.md` §11 | — |
| **4** Engine | `website/ENGAGEMENT.md` (rules/cooldown/outbox as built) · `BACKEND_DESIGN.md` table inventory | — |
| **5a/5b** Templates + editor | `website/ENGAGEMENT.md` §4.6 · a template-authoring section in `BACKEND_DESIGN.md` or its own doc | **`runicgateway.com`**: a new admin docs page for the template editor |
| **6** Email channel + Teams migration | `website/TEAMS.md` §6.3/§6.4 **rewritten** — the Team pipeline it describes no longer exists as its own thing | **`runicgateway.com`**: `administration/teams.mdx` notification section |
| **7** In-app channel (core+web) | `website/BACKEND_DESIGN.md` routes + tables · `website/ENGAGEMENT.md` | **`runicgateway.com`**: `notifications-and-email.mdx` gains the in-app channel |
| **8** In-app (Android) | `android/PLAN.md` | `android-app/README.md` |
| **9** Deliverability | `website/BACKEND_DESIGN.md` §7 · a suppression/bounce operator section | **`runicgateway.com`**: `troubleshooting.mdx` gains bounce/suppression · **`PLAY_DATA_SAFETY.md` + `/privacy`** — see Phase 12 |
| **10** Protocol bump | `link/INTEGRATION.md` §Housing (table + example) · `link/PLAN.md` §5/§7 · a `link/v5.md` if the bump earns its own design doc, as v3 and v4 did | `servuo-plugins/overlay.toml` · **`runicgateway.com`**: `platform.json.protocol` → 5, `bundle.*`, `architecture/protocol-versions.mdx` |
| **11** module-uo triggers | `modules/uo/API.md` · `modules/uo/README.md` | `module-uo/README.md` |
| **12** Public site | — | **`runicgateway.com`**, in full — see the phase |
| **13** Cutover | `README.md` index rows · every doc's status line | `.profile/README.md` if this is a headline capability |
**One thing this table is protecting against.** `runicgateway.com` appears in eight rows, and it is the
only repo here whose checks are *fetching* these values rather than being told them — see Phase 12.
---
@@ -1221,29 +1282,132 @@ until the v5 overlay is deployed, which the `required: false` declaration alread
`Greatly` on the live rig produces one email to the linked owner and nothing to anyone else; a second
transition inside the cooldown produces nothing; a refresh back to `LikeNew` inside the delay window
cancels the pending mail.
**Guardrails:** `check:modules` proves core gained no UO identifier across all eleven phases.
**Guardrails:** `check:modules` proves core gained no UO identifier across every phase to this point.
---
### Phase 12 — `runicgateway.com`: the public site and the docs journey
**This phase is not optional polish, and it is not "update the marketing copy". Two of its checks
will fail the build on their own**, and they read from `main`, which fixes exactly when this has to
land (see the timing note below).
**The mechanical half — the site's own checks go red at cutover.** `scripts/checkFacts.mjs` fetches
each value's authority from the **`main` branch of the source repo** and fails the build on any
disagreement (`link main:sidecar/src/main.rs`, `servuo-plugins main:overlay.toml`,
`website main:server/src/modules/version.js`, `Module-uo main:module.json`, and the `bundles` branch).
So `src/data/platform.json` needs:
- `protocol: 4`**5** (Phase 10) — asserted three times over: the sidecar, the overlay, the bundle.
- `moduleApi: "1.6.0"`**"1.7.0"** (Phase 2).
- `bundle.tag` / `bundle.sidecar` / `bundle.overlay` — whatever Phase 10's republished bundle carries.
- `verifiedOn` moved, and the page that quotes each value re-read rather than the JSON edited to make
the check pass — which is what the file's own header comment tells you in as many words.
**The content half — the site currently documents the opposite of what Phase 1 ships.**
`src/content/docs/docs/administration/notifications-and-email.mdx` carries an
`<Aside type="note" title="There is no SMTP option">` and the sentence *"Gmail over OAuth2 is the only
supported delivery path today."* Also `configuration.mdx:62` ("it is Gmail over OAuth2, it reuses the
Google authentication client"), `troubleshooting.mdx:101` ("Connect Gmail in **Settings → Email
delivery**"), and `system-architecture.mdx:117` ("the Gmail refresh token"). All four become false the
day Phase 1 merges to `main`.
**The capability claim to fix while here.** `src/data/capabilities.mjs`'s Notifications item already
says *"Web, push and email, chosen per stream by each person"***"Web" is not true today** (§0.1) and
becomes true at Phase 7. One list feeds `/`, `/features/` and `/modules/`, so this is one edit, and
`assertDetailCoverage()` will make sure the detail line comes with it.
**Legal and Play Data Safety.** `PLAY_DATA_SAFETY.md` and `/privacy` are generated from **one
inventory** (`scripts/playDataSafety.mjs`, `src/data/legal.mjs`), and an engagement mailer changes what
that inventory has to say: an email address is now used for **more than account function**, there is a
send log (`engagement_sends`, address hashes) and a suppression list. Play's Data Safety form
distinguishes "app functionality" from "communications/marketing", and getting that wrong is a store
review problem rather than a doc nit. Phase 9's decisions are the input, so this lands with or after
Phase 9 — **and it is the one part of Phase 12 with an external deadline attached to it.**
**New docs pages** for the admin docs journey: the engagement rules screen, the template editor, and
per-channel notification preferences. `scripts/checkSidebar.mjs` and `checkQuickstart.mjs` both hold
this to the actual site, and `checkQuickstart` is a **two-way** drift check against the website repo's
own setup — Phase 1 changes email setup, so expect it to have an opinion.
**Timing — this is the sequencing trap.** Because `checkFacts` reads `main`, the site stays **green for
the entire `edge` period** and goes red **the instant the cutover lands**. So Phase 12's work must be
**written and reviewed before the cutover and merged inside the same window** — not "after we ship".
Left until afterwards, the public site is broken and publishing false statements about the product at
exactly the moment anyone would look at it.
**Acceptance:** `node scripts/checkFacts.mjs`, `checkLinks`, `checkSidebar`, `checkQuickstart`,
`checkReference`, `checkA11y`, `checkCsp`, `checkBrand` and `npm test` all green **against the
post-cutover `main` of every source repo**; `grep -ri "gmail" src/` returns only historical/legal
references; `/privacy` and `PLAY_DATA_SAFETY.md` regenerate from the amended inventory with no manual
edit; the Notifications capability line is true of the shipped system.
---
### Phase 13 — The cutover: `edge` → `main`, in order
One PR per repo, all in one window. **The order is not cosmetic** — a `main` holding a v5 sidecar and a
v4 overlay is a shard that cannot pair, and the installer refuses it by design.
1. **`docs`** — the design of record and every doc the phases produced, so the reference exists before
the code that needs it.
2. **`servuo-plugins`** and **`link`** together — the emitter and `overlay.toml` and
`PROTOCOL_VERSION` are one protocol bump with three declaration sites (CLAUDE.md). Then CI
publishes the paired bundle.
3. **`website`** — core: the transport abstraction, the trigger registry, `MODULE_API_VERSION` 1.7.0,
the engine, the templates, the in-app channel, the Teams migration.
4. **`module-uo`** — its `coreApi` range and its triggers, after the core it declares against.
5. **`Integration-kit`** — `ci/core-ref.json` to the new website `main` sha. Its equality check is red
until this lands, **on purpose**; moving the pin is the acknowledgement that someone re-read the
chapters.
6. **`android-app`** — the in-app inbox. First real CI run (§6.0a).
7. **`runicgateway.com`** — last, because every fact it fetches has to be true on `main` first.
8. **`.profile/README.md`** — only if this is a headline capability.
**Acceptance:** a clean install from `main` alone stands the whole stack up — installer pairs a v5
sidecar with a v5 overlay, the site boots, SMTP is configured, a `Greatly` house transition on the
local rig produces exactly one email and one in-app item to the linked owner and nothing to anyone
else, and `runicgateway.com` builds green with no fact disagreeing with its authority. Every `edge`
is then fast-forwarded to `main` again so the next workstream starts from a clean one — the step
§6.0a found had been skipped after every previous cutover.
---
### Sequencing
```
Phase -1 fast-forward every edge to main; create edge in android-app,
runicgateway.com and Integration-kit ← blocking, §6.0a
Stage A 1 ── 2
Stage B └─ 3 ── 4 ── 5a ── 5b ── 6
Stage C └─ 7 ── 8 (8 = app-store cadence)
Stage D └─ 9
Stage E 10 ──────────────────────────────────── 11 (10 parallel from day one; 11 needs 6 + 10)
Stage F 12 ── 13 (12 written before 13, merged in its window)
── all of the above onto `edge` ──
13 is the only thing that touches `main`
```
**Phase -1 is blocking and takes minutes.** Every `edge` is 0 ahead / 316 behind `main` (§6.0a), so
the fast-forward is lossless; skipping it means the cutover diff carries other workstreams' leftovers.
Phases 1, 2 and 10 can start immediately and in parallel. **Phase 1 and Phase 6 are the two that touch
mail people actually receive** and should each land alone, on their own release: Phase 1 because it can
silently stop email for the live deployment (§1.2a), Phase 6 because it rewrites the pipeline behind
notifications that are going out today. Both get the local rig exercised before merge, not only tests.
mail people actually receive** and should each land alone: Phase 1 because it can silently stop email
for the live deployment (§1.2a), Phase 6 because it rewrites the pipeline behind notifications going
out today. Both get the local rig exercised before merge, not only tests.
5a can land before 4 if that sequencing is more convenient — the seeded templates and renderer have no
dependency on the engine, only on Phase 1's channel interface. The order above simply keeps the engine
provable before anything renders through it.
**Phase 12 is the one with a deadline rather than a dependency.** `runicgateway.com`'s checks read the
source repos' `main`, so the site stays green through the whole `edge` period and breaks at the cutover.
Its work therefore has to be finished *before* Phase 13 and merged *inside* the same window — the
failure mode of leaving it until after is a public site making false claims about the product on the
day it ships.
---
## Part 7 — Open questions and forward-compat notes
@@ -1273,6 +1437,11 @@ provable before anything renders through it.
feature, it is genuinely useful, and it is also the thin end of the wedge the brief rules out.
Recommendation: in, but as a `scheduled` trigger with an explicit staff audience and no list
building — never a separate "campaigns" surface.
8. **Android CI on `edge`** (§6.0a). `android-app/.gitea/workflows/pr-checks.yml` triggers only on PRs
into `main`, so Phase 8 lands with zero CI and Phase 13 is its first real build — as happened to all
nine M12 phase PRs. Fix the trigger as Phase 8's first commit, or accept it deliberately?
Recommendation: fix it. It is a two-line workflow change and the alternative is finding out about a
Kotlin compile error during the cutover window.
### 7.2 One namespace, or two?
@@ -1303,7 +1472,10 @@ The real forward-compat note is different, and belongs in `MODULE_API.md`:
> resume it"). A rule whose trigger is unregistered must therefore show as **dormant** in the admin UI,
> never as an error and never auto-deleted.
### 7.4 Documentation obligations
### 7.4 Documentation obligations — the whole surface
**§6.0b assigns each of these to the phase that causes it; this is the inventory it draws from.**
Nothing here is "documentation to do at the end" — a phase is not done until its rows have landed.
- `docs/website/ENGAGEMENT.md` — this document, as the design of record.
- `docs/website/BACKEND_DESIGN.md` — §7 (Email) **rewritten, not amended**: it currently documents Gmail
@@ -1318,9 +1490,19 @@ The real forward-compat note is different, and belongs in `MODULE_API.md`:
- `docs/website/TEAMS.md` — §6.3/§6.4 rewritten once Phase 6 migrates the Team pipeline.
- `docs/link/INTEGRATION.md` + `docs/link/PLAN.md` — Phase 10's wire fields.
- `docs/android/PLAN.md` — §11 gains the in-app inbox and the per-channel preferences.
- `docs/modules/uo/API.md` + `README.md` — Phase 11's triggers.
- `integration-kit/``ci/core-ref.json` moved to the new pin; chapter 2 gains a section on registering
a trigger, since the kit currently teaches none of the registries. The kit **teaches and never
re-specifies**, so it links to `MODULE_API.md` rather than restating the contract.
- **`runicgateway.com`** — the largest single obligation and the only one whose checks *fetch* their
facts rather than being told them, so it fails on its own. Full detail in Phase 12; in summary:
`src/data/platform.json` (`protocol` 4→5, `moduleApi` 1.6.0→1.7.0, the bundle triple) ·
`administration/notifications-and-email.mdx` (whose "There is no SMTP option" aside becomes false) ·
`configuration.mdx`, `troubleshooting.mdx`, `architecture/system-architecture.mdx`,
`architecture/protocol-versions.mdx` · `src/data/capabilities.mjs` (the Notifications line claims a
web channel that does not exist until Phase 7) · new admin pages for rules, templates and per-channel
preferences · **`PLAY_DATA_SAFETY.md` + `/privacy`**, both generated from one inventory that an
engagement mailer materially changes.
- `.profile/README.md` — only if this lands as a headline capability.
---