docs(engagement): Phase 13 as built, and Phase 14 — retention #203

Merged
whitlocktech merged 2 commits from docs/engagement-phase-13-as-built into main 2026-09-01 18:55:14 +00:00
Showing only changes of commit cff73ad863 - Show all commits

View File

@@ -1,8 +1,10 @@
# The Engagement System — findings and plan
**Status:** design of record. **The workstream is BUILT and CUT OVER (2026-09-01).** Phases 0
through 13 are done and on `main` in every repository; **Phase 14 (retention) is scoped and not
started**, and Phase 13's own acceptance walk has not been run. Each phase carries its own as-built
**Status:** design of record. **The workstream is BUILT, CUT OVER and ACCEPTED (2026-09-01).**
Phases 0 through 13 are done and on `main` in every repository, and **Phase 13's acceptance walk
passed** — a clean install from `main`, released artefacts only, one email and one in-app item to the
linked owner and nothing to anyone else. It found two defects in shipped core; both are recorded in
Phase 13's as-built and neither is fixed there. **Phase 14 (retention) is scoped and not started.** Each phase carries its own as-built
section below, and the PRs are named there rather than here.
The cutover cut `link` **v2.1.0**, the plugin overlay **v1.1.0**, `module-uo` **v1.1.0** and the
@@ -3806,10 +3808,55 @@ repo's checks read the *source repos' `main`*, so running them on an `edge` PR w
for the whole window by design. Phase 12's split exists for the same reason. What it costs is real
though — 12b's fill-in was verified locally and merged on that evidence alone.
**Still open after this phase:** the acceptance walk above — the clean install from `main`, and the
`Greatly` transition producing exactly one email and one in-app item to the linked owner and nothing
to anyone else — has **not** been run. `.profile/README.md` (step 8) landed: the org lead called the
engagement system a headline capability. And Phase 12's second finding became **Phase 14**.
#### The acceptance walk — run 2026-09-01, PASSED
Every clause of the acceptance line above, on a rig built the way an operator would build one rather
than the way a developer would.
**A clean install from `main` alone.** The **released** installer v0.1.1, downloaded and checked
against its `SHA256SUMS` — the trust anchor the docs name, since releases are unsigned — resolved
bundle **2026.09.01** and reported the pair it was about to deploy: overlay **v1.1.0** protocol 5,
sidecar **v2.1.0** protocol 5. `--verify` first (`add=23 change=1`, nothing written), then the real
run. `dotnet build` on the synced tree: **0 errors** — the released overlay compiles against stock
ServUO 57.4. The shard booted, printed `[Bridge] enabled=True`, and dialled the sidecar; the site
reported `status=connected pluginConnected=true`. The website was a **fresh `main` clone** and
`module-uo` **v1.1.0 installed from its release manifest** through the `MODULES` declaration — sha
verified and unpacked by core, never copied in by hand. SMTP was configured against a mail catcher
and its test send arrived.
**One email and one in-app item, to the linked owner, and nothing to anyone else.** Forcing
`0x400143DB` from IDOC to **Greatly** produced `event dispatched {rules:1, enqueued:2, deduped:0,
cooled:0, capped:0, cancelled:0}`, and after the rule's grace window both rows sent on the first
attempt. **Every row in every delivery table names the linked user and no other** — send log 2,
outbox 2, inbox 1, and nothing for the admin or for the second, unlinked player. The mail was the
in-universe body, correctly rendered: *"A notice concerning “Yewcross”"*, the stage in words, the
detail line, an absolute link to `/uo/houses` and a working unsubscribe link — no empty href, which
is the Phase 11b defect class staying fixed.
**Two defects it found, both in shipped core, neither visible to any test:**
1. **The one-shot rule-group guard is not atomic.** `moduleSeeds.js` `seedRuleGroup()` reads its
settings stamp, inserts the whole group, and writes the stamp **after the loop**. Two processes
booting within the same moment both read "not seeded" and both insert: the rig ended up with
**52 UO rules where the module ships 26**, under one stamp. The function's own comment states the
cost — *"a duplicate rule is two mails per event"* — which is exactly what its guard fails to
prevent. It was reached here by two app processes starting milliseconds apart, which is what a
`--scale app=2` or a rolling restart does on purpose. The fix is to claim before inserting: an
atomic conditional write of the stamp, or an insert idempotent per (owner, group, trigger).
Same shape as Phase 4a's `foundRows` finding — a guard that reads before it claims.
2. **A module-seeded rule that ships a `digest` body cannot be saved from the Rules screen.**
`checkSeedRule` permits `digest` explicitly, in as many words — *"a template slot rather than a
channel … allowed here and absent from `channels`"* — and sixteen in-universe UO rules ship one.
`engagementRules.model.js` then rejects any `templateKeys` entry that is not one of the rule's
channels, so an operator who opens **House — decay warning** and presses Save gets a 400 naming a
key they never typed. The only way to save is to delete the digest body, which silently removes
digest support from that rule. The two validators have to agree about what `digest` is.
Neither is fixed here: this phase's PR is the documentation of a cutover, and a core change belongs
in its own. Both are recorded for the org lead to place.
**Also still open:** nothing else. `.profile/README.md` (step 8) landed — the org lead called the
engagement system a headline capability — and Phase 12's second finding became **Phase 14**.
---