docs(engagement): Phase 13 as built, and Phase 14 — retention #203
@@ -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**.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user