diff --git a/website/ENGAGEMENT.md b/website/ENGAGEMENT.md index 02ff3ad..320afec 100644 --- a/website/ENGAGEMENT.md +++ b/website/ENGAGEMENT.md @@ -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**. ---