docs(engagement): Phase 11b's live walk — three decisions and six defects
The walk is the last piece of 11b and it was not a formality. It found six
defects, four of them in code shipped by earlier phases, and none of the six was
visible in a unit test: each is a disagreement between two things that agree with
each other in a fixture.
Three of the fixes were decisions rather than repairs, all settled by the org
lead before any code:
11 `uo.house.idoc_warning` ships delay_seconds: 900 and nothing could cancel
it -> add `uo.house.refreshed`, a 26th trigger with a body and a rule
12 a rule with a cooldown delivered on exactly ONE of its channels
-> `channel` joins the cooldown key; a cooldown is per DELIVERY
13 `uo.vendor.expiring` could not fire, because the market sweep does not
diff fees -> widen BridgeMarket.Signature() with exempt +
periodsRemaining
Files:
website/ENGAGEMENT.md the three decisions, the four repairs, and what the
walk proved rung by rung; the 11b bullet and the §8.6
family table now read 26 triggers / 34 bodies
website/BACKEND_DESIGN.md engagement_cooldowns gains `channel` in its PRIMARY
KEY, with the migration's information_schema guard and
why MariaDB forces one
link/v5.md the sweep has to DIFF the fees or the frame never
comes -- stated as the general rule for the next
enrichment, since it is emit cadence and not shape
modules/uo/API.md §5.7a the cancel-shaped trigger and the Ageless-vs-
LikeNew ServUO fact; §5.7b every link comes from
config/clientPaths.js, and the two mistakes that made
every call-to-action a dead link
Pairs with website#<core>, Module-uo#<uo> and servuo-plugins#<plugin>.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -157,9 +157,9 @@ stored payload, whose actors carry `acct` and `webId`; `/public/shard/idoc` retu
|
||||
## 5. Engagement triggers and audiences (ENGAGEMENT.md Phase 11)
|
||||
|
||||
Not an HTTP surface, and it is here anyway: it is the other thing this module registers with core, and
|
||||
it is the one an operator interacts with by name. `module-uo` declares **25 event triggers** and
|
||||
it is the one an operator interacts with by name. `module-uo` declares **26 event triggers** and
|
||||
**3 audiences** through `api.registerEventTriggers` / `api.registerAudiences`, and ships
|
||||
**32 message bodies and 25 rules** through `api.registerEngagementSeeds`
|
||||
**34 message bodies and 26 rules** through `api.registerEngagementSeeds`
|
||||
([`MODULE_API.md`](../../website/MODULE_API.md) §2.4). Core never learns a word of the vocabulary —
|
||||
it holds an id, a label, a variable list, a ceiling and, for an audience, a `resolve` it may call.
|
||||
|
||||
@@ -181,7 +181,7 @@ ceiling does not permit `owner`, because fewer people is not less exposure.
|
||||
|
||||
| Ceiling | Triggers | Why that ceiling |
|
||||
| --- | --- | --- |
|
||||
| `owner` | `uo.house.idoc_warning`, `uo.house.collapsed`, `uo.vendor.expiring`, `uo.vendor.sale`, `uo.account.login_failed`, `uo.account.unlinked`, `uo.skill.capped`, `uo.quest.complete`, `uo.character.death`, `uo.character.murdered`, `uo.governor.appointed` | Each is about one person's own property, account, character or office. All eleven resolve through an account on the frame to `shard_account_links`; an unlinked game account is nobody to notify |
|
||||
| `owner` | `uo.house.idoc_warning`, `uo.house.refreshed`, `uo.house.collapsed`, `uo.vendor.expiring`, `uo.vendor.sale`, `uo.account.login_failed`, `uo.account.unlinked`, `uo.skill.capped`, `uo.quest.complete`, `uo.character.death`, `uo.character.murdered`, `uo.governor.appointed` | Each is about one person's own property, account, character or office. All twelve resolve through an account on the frame to `shard_account_links`; an unlinked game account is nobody to notify |
|
||||
| `members` | `uo.guild.left`, `uo.guild.disbanded` | The guild's roster, resolved to website users through `shard_account_links` and carried on the emit as `recipientUserIds` — "the members of *this* guild" is a different answer every firing, which a saved segment cannot express |
|
||||
| `authenticated` | `uo.governor.elected`, `uo.election.opened`, `uo.champ.started`, `uo.champ.boss_up`, `uo.server.up`, `uo.server.down`, `uo.points.rank_changed` | Public shard news. Each defaults to `subscribers`; the ceiling permits an operator to widen to everyone signed in, which for "the shard is back up" is a defensible thing to want |
|
||||
| `staff` | `uo.page.new`, `uo.cheat.detected` | `uo.cheat.detected` is the declaration the lattice was written for: under a flat "fewer people is narrower" ordering, a `staff` ceiling would also permit `owner`, and the rule an operator could then save mails the cheat report to the player who was detected |
|
||||
@@ -265,7 +265,7 @@ Declaring a trigger says what an event IS. It says nothing about what the messag
|
||||
until Phase 11b there was no way for a module to say: `templateSeeds.js` and `coreRules.js` are core
|
||||
files with core arrays in them. `api.registerEngagementSeeds({ templates, ruleGroups })`
|
||||
([`MODULE_API.md`](../../website/MODULE_API.md) §1.1, **1.9.0**) is the mechanism; this module is its
|
||||
first caller, with **32 bodies and 25 rules** in
|
||||
first caller, with **34 bodies and 26 rules** in
|
||||
[`server/config/engagementSeeds.js`](https://gitea.whitlocktech.com/RunicGateway/Module-uo/src/branch/main/server/config/engagementSeeds.js).
|
||||
|
||||
**Sixteen families read from inside Britannia, with a per-family sender.** The org lead's decision
|
||||
@@ -302,13 +302,63 @@ not fixtures.
|
||||
|
||||
Two mechanical notes that will bite whoever adds the twenty-sixth trigger:
|
||||
|
||||
- **All 25 rules are in ONE seed group, `triggers-v1`, and a group is seeded once.** A rule appended
|
||||
- **All 26 rules are in ONE seed group, `triggers-v1`, and a group is seeded once.** A rule appended
|
||||
to it later reaches **fresh installs only** — never a deployment already stamped. A rule that must
|
||||
reach existing deployments takes a new group key.
|
||||
- **A trigger id and a template key have different grammars.** `uo.champ.boss_up` is a legal trigger
|
||||
id and an illegal template key (core's key pattern admits `.` and `-`, not `_`), so its body is
|
||||
keyed `uo.champ.boss-up`. Registration refuses the mistake at boot.
|
||||
|
||||
### 5.7a `uo.house.refreshed` — the trigger that exists to cancel one
|
||||
|
||||
Added by the live walk (ENGAGEMENT.md Phase 11b, decision 11), and it is the only trigger in this
|
||||
module whose primary job is not to say something.
|
||||
|
||||
`uo.house.idoc_warning`'s seeded rule carries `delay_seconds: 900` so that a player who repairs the
|
||||
house inside the quarter-hour is never told it is in peril. That is only true if something CANCELS
|
||||
the pending row, and until this trigger existed nothing could: `cancel_on` named `uo.house.collapsed`
|
||||
— the outcome where the warning is pointless — and the mapper returned early on every transition that
|
||||
was not a late decay stage, so a refresh reached the engine as silence. The wire had carried the
|
||||
transition all along.
|
||||
|
||||
```
|
||||
house.decay Greatly -> Ageless (the owner logged back in)
|
||||
-> uo.house.refreshed (owner-audienced, subject = the house serial)
|
||||
-> cancels every scheduled engagement_outbox row for
|
||||
(the warning's rule, that house, that owner)
|
||||
-> and, if the operator enabled its own rule, sends the Office of Deeds'
|
||||
one non-warning letter
|
||||
```
|
||||
|
||||
Three things about it are load-bearing:
|
||||
|
||||
- **Its `subjectKey` is `houseSerial`, the same as the warning's.** `outboxDb.cancel` matches on
|
||||
`(rule, subject_key)`, so a refresh carrying any other subject would cancel nothing at all.
|
||||
- **It fires on `Ageless` as well as `LikeNew`, and `Ageless` is the common case.** A condemned house
|
||||
cannot be refreshed — `BaseHouse.RefreshDecay()` refuses `DecayType.Condemned` outright — so the
|
||||
rescue is the owner logging in. Their newest house then becomes `AutoRefresh` and reads `Ageless`;
|
||||
an older one becomes `ManualRefresh` and reads `LikeNew`. Reading only the second misses most
|
||||
rescues.
|
||||
- **The cancellation does not depend on its own rule.** `cancel_on` is read off the WARNING's rule,
|
||||
so an operator who wants the cancellation without the reassurance letter simply leaves the new rule
|
||||
disabled — which, every seeded rule shipping disabled, is what a fresh install already does.
|
||||
|
||||
### 5.7b Every link a body offers comes from `config/clientPaths.js`
|
||||
|
||||
A notification's call-to-action is a path into this module's own SPA routes, and there is exactly one
|
||||
place that knows them. The live walk found every one of them wrong, in two independent ways:
|
||||
|
||||
- the declared `example`s read `/shard/…`, taken from `module.json`'s `mounts` — but
|
||||
`registry.registerRoutes` prefixes a module's client routes with its **ID** and nothing else, so
|
||||
the real paths are `/uo/houses`, `/uo/market`, `/player/uo/characters`. Every example was a 404,
|
||||
and an example is what the template editor previews and test-sends with;
|
||||
- and no `url` variable was ever populated by the mapper, so the buttons rendered with an empty href
|
||||
and dropped out of the text part entirely.
|
||||
|
||||
`clientPaths.js` is now the single source for both the declarations and the bodies. `client/src/entry.jsx`'s
|
||||
own `registerNav` is the cross-check: the hrefs it hands the sidebar are these, and if the two ever
|
||||
disagree the sidebar is right.
|
||||
|
||||
### 5.8 The presentational fragments, and why they exist
|
||||
|
||||
A template has **no conditionals**, by design, and an unset optional interpolates to the **empty
|
||||
|
||||
Reference in New Issue
Block a user