docs(engagement): scope Phase 11 — six decisions, three carve-outs, the 11a/11b split #194

Merged
whitlocktech merged 1 commits from docs/engagement-phase-11-decisions into edge 2026-09-01 05:05:41 +00:00
Member

Phase 11's own text asks for its 11a/11b split to be confirmed with the org lead "at the start of the phase rather than assumed here". Confirming it turned up five further questions the plan had not asked, three of which change what ships. This records all six before any code, on Phase 2's precedent.

No code in this PR. 11a follows.

The three carve-outs

Phase 11 inverted the burden of proof — a row that does not ship needs a reason recorded — so all three are written into §8.6 and the phase. All three came from checking §8.6's rows against what the website can address, which is a different question from whether the wire carries the event. In every case the event keeps being ingested.

Row Why it does not ship
uo.guild.joined Duplicates core's team.member.joined. A UO guild is a Team and module-uo is this deployment's Team provider, so teamSync.notifyRoster already emits on every roster reconcile and core already seeds a (disabled) rule for it. An operator enabling the obvious two rules would get two mails for one join. uo.guild.left and uo.guild.disbanded still ship — core has no team.member.left and no disband event at all. The trade given up is latency (~15 min vs real-time), which is the right thing to give up: a duplicate mail is a defect discovered in production.
uo.link.requested No addressable recipient by construction — the game account is not yet linked, which is the entire point of the event, so shard_account_links has no row and owner resolves to nobody. And ttlSec defaults to five minutes; every channel here is slower than that in the worst case and allowed to be. The player is looking at the code on screen when it fires.
uo.points.rank_changed — the personal half points.board's top[] entries are {rank, serial, name, points} — a mobile serial — and shard_account_links is keyed by game account. No general serial→account map exists (shard_online covers only who is logged in right now, shard_guild_members only guilded characters), so "you were pushed out of the top ten" would reach some players and silently not others. The board-changed feed ships at subscribers; the owner half waits for something that actually needs a serial→account map.

The three that change mechanism

1. The ceiling lattice gains admin, as a child of staffMODULE_API_VERSION → 1.8.0.

§8.6 describes uo.audit.staff_action, uo.economy.milestone and uo.world.saved as admin-audience, and link/v5.md §4 classifies account.login.result as admin, by omission — "there is no rung below admin that a frame carrying an IP address and an auth verdict belongs on". The lattice had no such rung: its narrowest staff-ish value is staff, which ceilings.js defines as admin / editor / moderator. Ceilinging the operator-facing family there would let a rule mail the staff audit digest to every editor.

admin is the only genuine refinement in the whole tree — every admin is staff, which is exactly the containment every other pair of leaves lacks — so it is a child rather than a seventh leaf, and permits / meet / meetAll need no change beyond the new PARENT entry. Cheap now because nothing anywhere stores an admin audience yet; expensive after cutover.

2. §7.1 Q9 — news.post continuity: the emit replaces the raw tickle, and core seeds the rule DISABLED.

The Phase 6 Team precedent applied unchanged. The consequence is stated plainly in the doc because it is deliberate: news push stops on upgrade until an operator switches the seeded rule on. Keeping the raw tickle beside the emit "for one release" was considered and refused — it is an exception with a deadline nobody owns, and Phase 6 already refused the analogous carve-out. Two mitigations become phase deliverables rather than nice-to-haves:

  • EngagementRules.jsx's existing banner must name news alongside Teams (11a)
  • Phase 13's release note must carry it as an upgrade step (11b)

3. The split is confirmed and specified.

  • 11a — declarations, mappings, ceilings. Server only, no rule or template seeded. module-uo's 24 declarations + the wire-kind → ctx.events.emit mapper hung off shardIngest.ingest; core's admin ceiling, the news.post emitter, 1.8.0, manifest regen. Acceptance here is the catalogue-shaped half: no staff- or admin-ceiling trigger by name in a player's catalogue, and check:modules clean.
  • 11b — the 24 seeded rules (all enabled = 0), the template bodies, and the live five-rung walk.

Two mechanisms recorded, not decisions

uo.vendor.expiring and uo.economy.milestone are threshold events, but conditions.js compares a declared variable against a literal only — no arithmetic, no relative time — so "within 24 hours of dismissal" is not expressible as a rule condition. Nor can the trigger fire per frame: vendor.listing is a sweep frame re-emitted on any price change and economy.supply arrives every five minutes. So the crossing is tracked module-side, in the in-process tracker shardStreams.js already uses for champActive/cityGovernor, with periodsRemaining/hoursRemaining declared as ints so an operator can still narrow with lte.

Also names module-uo's first three registered audiences (§5.1a said "Phase 11" and never said which): uo.guild.members, uo.governors, uo.linked.accounts — distinguished in the doc from the members audience the guild triggers use, which travels on the envelope as recipientUserIds because its answer differs per firing.

Shipped set

24 triggers after the carve-outs, across five audience kinds (owner, members, subscribers, staff, admin).


AI disclosure

  • This contribution was AI-assisted (Claude Code / Claude Opus 5). Commits carry a Co-Authored-By: Claude trailer.

🤖 Generated with Claude Code

Phase 11's own text asks for its 11a/11b split to be confirmed with the org lead "at the start of the phase rather than assumed here". Confirming it turned up five further questions the plan had not asked, three of which change what ships. This records all six **before any code**, on Phase 2's precedent. No code in this PR. 11a follows. ## The three carve-outs Phase 11 inverted the burden of proof — a ✅ row that does *not* ship needs a reason recorded — so all three are written into §8.6 and the phase. All three came from checking §8.6's rows against **what the website can address**, which is a different question from whether the wire carries the event. In every case the event keeps being ingested. | Row | Why it does not ship | | --- | --- | | `uo.guild.joined` | Duplicates core's `team.member.joined`. A UO guild *is* a Team and `module-uo` is this deployment's Team provider, so `teamSync.notifyRoster` already emits on every roster reconcile and core already seeds a (disabled) rule for it. An operator enabling the obvious two rules would get two mails for one join. **`uo.guild.left` and `uo.guild.disbanded` still ship** — core has no `team.member.left` and no disband event at all. The trade given up is latency (~15 min vs real-time), which is the right thing to give up: a duplicate mail is a defect discovered in production. | | `uo.link.requested` | No addressable recipient **by construction** — the game account is not yet linked, which is the entire point of the event, so `shard_account_links` has no row and `owner` resolves to nobody. And `ttlSec` defaults to five minutes; every channel here is slower than that in the worst case and allowed to be. The player is looking at the code on screen when it fires. | | `uo.points.rank_changed` — the personal half | `points.board`'s `top[]` entries are `{rank, serial, name, points}` — a **mobile serial** — and `shard_account_links` is keyed by game **account**. No general serial→account map exists (`shard_online` covers only who is logged in right now, `shard_guild_members` only guilded characters), so "you were pushed out of the top ten" would reach some players and silently not others. **The board-changed feed ships at `subscribers`**; the owner half waits for something that actually needs a serial→account map. | ## The three that change mechanism **1. The ceiling lattice gains `admin`, as a child of `staff` — `MODULE_API_VERSION` → 1.8.0.** §8.6 describes `uo.audit.staff_action`, `uo.economy.milestone` and `uo.world.saved` as admin-audience, and `link/v5.md` §4 classifies `account.login.result` as *admin, by omission* — "there is no rung below admin that a frame carrying an IP address and an auth verdict belongs on". The lattice had no such rung: its narrowest staff-ish value is `staff`, which `ceilings.js` defines as **admin / editor / moderator**. Ceilinging the operator-facing family there would let a rule mail the staff audit digest to every editor. `admin` is the **only genuine refinement in the whole tree** — every admin is staff, which is exactly the containment every other pair of leaves lacks — so it is a child rather than a seventh leaf, and `permits` / `meet` / `meetAll` need no change beyond the new `PARENT` entry. Cheap now because nothing anywhere stores an `admin` audience yet; expensive after cutover. **2. §7.1 Q9 — `news.post` continuity: the emit replaces the raw tickle, and core seeds the rule DISABLED.** The Phase 6 Team precedent applied unchanged. **The consequence is stated plainly in the doc because it is deliberate: news push stops on upgrade** until an operator switches the seeded rule on. Keeping the raw tickle beside the emit "for one release" was considered and refused — it is an exception with a deadline nobody owns, and Phase 6 already refused the analogous carve-out. Two mitigations become phase deliverables rather than nice-to-haves: - `EngagementRules.jsx`'s existing banner must name news alongside Teams (11a) - Phase 13's release note must carry it as an upgrade step (11b) **3. The split is confirmed and specified.** - **11a** — declarations, mappings, ceilings. Server only, **no rule or template seeded**. `module-uo`'s 24 declarations + the wire-kind → `ctx.events.emit` mapper hung off `shardIngest.ingest`; core's `admin` ceiling, the `news.post` emitter, 1.8.0, manifest regen. Acceptance here is the catalogue-shaped half: no staff- or admin-ceiling trigger by name in a player's catalogue, and `check:modules` clean. - **11b** — the 24 seeded rules (all `enabled = 0`), the template bodies, and the live five-rung walk. ## Two mechanisms recorded, not decisions `uo.vendor.expiring` and `uo.economy.milestone` are **threshold** events, but `conditions.js` compares a declared variable against a **literal** only — no arithmetic, no relative time — so "within 24 hours of dismissal" is not expressible as a rule condition. Nor can the trigger fire per frame: `vendor.listing` is a sweep frame re-emitted on any price change and `economy.supply` arrives every five minutes. So the crossing is tracked **module-side**, in the in-process tracker `shardStreams.js` already uses for `champActive`/`cityGovernor`, with `periodsRemaining`/`hoursRemaining` declared as ints so an operator can still narrow with `lte`. Also names `module-uo`'s first three **registered** audiences (§5.1a said "Phase 11" and never said which): `uo.guild.members`, `uo.governors`, `uo.linked.accounts` — distinguished in the doc from the `members` audience the guild triggers use, which travels on the envelope as `recipientUserIds` because its answer differs per firing. ## Shipped set 24 triggers after the carve-outs, across five audience kinds (`owner`, `members`, `subscribers`, `staff`, `admin`). --- ### AI disclosure - [x] This contribution was AI-assisted (Claude Code / Claude Opus 5). Commits carry a `Co-Authored-By: Claude` trailer. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
wtclaude added 1 commit 2026-09-01 00:47:47 +00:00
Phase 11 was widened on 2026-08-31 to ship every  row of §8.6 rather than a
single rule, and the phase text asked for its 11a/11b split to be confirmed with
the org lead before any code. Confirming it turned up five further questions the
plan had not asked, three of which change what ships — so this records all six
before implementation starts, on Phase 2's precedent.

The three carve-outs all came from checking §8.6's rows against what the WEBSITE
can address rather than against what the wire carries:

  • uo.guild.joined duplicates core's team.member.joined — a UO guild IS a Team
    and teamSync already emits on every roster reconcile, so an operator who
    enables the obvious two rules gets two mails for one join. `left` and
    `disbanded` still ship; core has no equivalent for either.
  • uo.link.requested has no addressable recipient by construction (the account
    is not yet linked) and a ~5-minute TTL no channel can beat.
  • uo.points.rank_changed ships `subscribers` only: points.board's top[] names
    a mobile serial and shard_account_links is keyed by account, so the personal
    half would reach some players and silently not others.

The other three:

  • The lattice gains `admin`, as a child of `staff` — the operator-facing family
    is described as admin-audience everywhere and `staff` is admin/editor/
    moderator, so ceilinging it there would mail the staff audit digest to every
    editor. It is the only genuine refinement in the tree, so permits/meet/
    meetAll are untouched. MODULE_API_VERSION → 1.8.0.
  • §7.1 Q9 (news.post continuity): the emit REPLACES the raw tickle and core
    seeds the rule disabled — the Phase 6 precedent. News push stops on upgrade;
    the admin banner and the release note are the mitigations, and both are named
    as phase deliverables.
  • The split is confirmed and specified: 11a is declarations/mappings/ceilings,
    server only, no rule or template seeded; 11b is the seeded set and the live
    walk.

Also records the threshold-trigger mechanism uo.vendor.expiring and
uo.economy.milestone need — rule conditions compare against literals only, so
"within 24 hours of dismissal" is not expressible as a condition and the crossing
is tracked module-side — and names module-uo's first three registered audiences.

Co-Authored-By: Claude <noreply@anthropic.com>
whitlocktech merged commit dc7d7c9a40 into edge 2026-09-01 05:05:41 +00:00
whitlocktech deleted branch docs/engagement-phase-11-decisions 2026-09-01 05:05:42 +00:00
Sign in to join this conversation.
No description provided.