docs: the engagement workstream — cutover 1 of 7 (edge → main)
#200
@@ -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