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:
@@ -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 (1–2) is prerequisite. Stage B (3–6) is the engagement system. Stage C (7–8) is the in-app
|
||||
channel. Stage D (9) is deliverability. Stage E (10–11) is the shard enrichment and runs in parallel
|
||||
from day one.**
|
||||
from day one. Stage F (12–13) 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 / 3–16 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.
|
||||
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user