docs(engagement): Phase 13 as built, and Phase 14 — retention #203
Reference in New Issue
Block a user
No description provided.
Delete Branch "docs/engagement-phase-13-as-built"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes the workstream's documentation, and opens the one phase it leaves behind.
Phase 13 was the only phase with no "As built" section
§6.0b makes that a phase's own deliverable, and this one has more to record than "it merged":
Integration-kitandrunicgateway.comlanded afterwards (Integration-kit#9, runicgateway.com#28), with Module-uo#27 clearing a leftover from step 4 — acore-refpin still naming a branch the cutover had deleted.rawAPI route is CDN-cached for six hours, and a stale read fails both ways. It failedrunicgateway.comfor amoduleApithat was correct — a fortnight-old blob,cf-cache-status: HIT,Age: 15713— and the same run reported the already-republished bundle triple as still current. The false red is the cheap half; the false green is the one that matters for a repo whose whole bargain is going red when the platform moves. All three cross-repository checks readcontentsnow.ci/core-ref.jsonis where the date is written down.registerEngagementSeedsdoes not validate the body it seeds, and the kit's own example body was malformed because of it. Found only by running the template'sregister()through core's realstage()validators — which the kit's CI does not do and cannot: it runs against fakes, and a fake accepts what core refuses.edgewas deleted rather than fast-forwarded. Recorded as the convention, on the grounds that a branch which does not exist cannot be the stale one somebody branches from — which was §6.0a's actual failure. The two that survived (installer,Integration-kit) are level withmainas of today.Phase 12's second finding becomes Phase 14
Scoped, not started, and it needs your approval like any other phase. Four tables have no retention sweep, and they are not one problem with one horizon — the table in the section says why each is different:
engagement_cooldowns—engagementCooldowns.db.prune()already exists and has no caller. The horizon must exceed the longestcooldown_secondson any enabled rule, or a pruned row turns the next fire into a first fire.engagement_outbox— only terminal rows are eligible; ascheduledrow may legitimately be days out.engagement_sends— two live readers: the per-rule hourly cap readsidx_engs_rule_window, so a horizon under an hour breaks Q3's ceiling, and the Send Log screen is the operator's only answer to "was this person told".engagement_suppressions— probably should not expire at all. Ageing out abouncerow means the next send re-mails an address that already hard-bounced, which is how a sender loses a domain's reputation.The shape already exists twice in the tree (
teamActivityPrune,userNotificationsPrune). The phase owesrunicgateway.comtoo:/privacyand the generated Play Data Safety answers currently publish "kept until the operator removes them; nothing here expires on its own" — true today, false the moment this lands.Two things I'd want from you before any code: the four horizons (or three, plus "suppressions do not expire"), and whether the send log's retention is allowed to be shorter than the operator screen's usefulness — the alternative is that the screen learns to say "older entries have been swept" rather than showing a silently truncated history.
Bookkeeping
link/v5.mdand this repo's index row both said protocol 5 was built but unreleased, andINTEGRATION.mdstill named v2.0.0 + v1.0.0 as the shipped pairingStill open after this
Phase 13's acceptance walk has not been run — the clean install from
mainand theGreatlytransition producing exactly one email and one in-app item to the linked owner and nothing to anyone else. It is named as outstanding in the as-built rather than quietly ticked.🤖 Generated with Claude Code
Phase 13 was the only phase in the workstream with no as-built section, which 6.0b makes a phase's own deliverable. It has one now, and it has something to record beyond "it merged": * the window missed steps 5 and 7 (Integration-kit, runicgateway.com); they landed afterwards, with a third PR clearing a leftover from step 4; * Gitea's `raw` API route is CDN-cached for six hours, and a stale read fails BOTH ways -- it failed a correct site AND reported an already-republished bundle as current. All three cross-repo checks read `contents` now; * the Integration-kit equality check never goes red on its own. It clones the ref the kit itself pins, so the kit sat three minor versions behind, green, for the whole workstream. Step 5 is the only thing that makes anyone look; * `registerEngagementSeeds` does not validate the body it seeds, and the kit's own example was malformed. Only a run through core's REAL registry found it, which the kit's CI does not do and cannot; * every `edge` was deleted rather than fast-forwarded. Recorded as the convention, because a branch that does not exist cannot be branched from stale -- which was 6.0a's actual failure. Phase 12's second finding becomes **Phase 14**, scoped and not started: four tables with no retention sweep, and they are not one problem with one horizon. `engagementCooldowns.db.prune()` already exists with no caller; the send log has two live readers (the hourly cap's index and the operator screen) that bound how short its horizon may be; and suppressions probably should NOT expire at all, because ageing out a bounce re-mails an address that already hard-bounced. The phase owes `/privacy` and the Play answers, which today say "nothing here expires on its own" -- true now, false the moment it lands. Also: ticks on the phase headings that were done and unmarked (4, 9, 10, 11, 12, 13), a status header that still said everything from Phase 10 on was design, and three status lines that outlived the cutover -- `link/v5.md` and this README's index row both said protocol 5 was built but unreleased, and INTEGRATION.md named the v2.0.0/v1.0.0 pairing as the shipped one. Co-Authored-By: Claude <noreply@anthropic.com>The walk ran and PASSED. Released artefacts only: the released installer, checksum-verified, resolving bundle 2026.09.01; the overlay compiled against stock ServUO 57.4 with zero errors; a fresh `main` clone of website with module-uo v1.1.0 installed from its release manifest rather than copied in. Forcing a house to Greatly produced exactly one email and one in-app item to the linked owner and nothing to anyone else -- every row in every delivery table names that one user. Two defects, both in shipped core, neither visible to any test: * `seedRuleGroup()` reads its settings stamp, inserts the whole group, and writes the stamp AFTER the loop. Two processes booting in the same moment both read "not seeded" and both insert -- 52 UO rules where the module ships 26, under one stamp. The function's own comment names the cost: a duplicate rule is two mails per event. A `--scale app=2` or a rolling restart does this on purpose. * `checkSeedRule` permits a `digest` template key in as many words, 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, and the only way to save is to delete the digest body. Neither is fixed here -- this PR documents a cutover, and a core change belongs in its own. Co-Authored-By: Claude <noreply@anthropic.com>