docs(engagement): Phase 11b — the seed API, the in-universe voice, the governor's letter
Four decisions settled by the org lead before any 11b code, plus MODULE_API
1.9.0 and what building it found.
DECISION 7 is the one 11a could not have discovered, because 11a seeded nothing.
The plan's own sentence — "24 rules, all enabled = 0; bespoke template bodies" —
described work with NO MECHANISM to land in: templateSeeds.js and coreRules.js
are core files with core arrays, and registries.js has no registerTemplates.
Adds api.registerEngagementSeeds({ templates, ruleGroups }). Templates re-ensure
every boot; rule groups are one-shot per named group, which is 11a's seed-key
finding stated as an API rather than as a warning.
DECISION 8: the game-powered families read in-universe, with a per-family sender
rather than one voice. The crown writes about the crown's business and nothing
else. This is also why the bodies had to be the module's: §5.2 says core stays
ignorant of a module's domain, and check:modules reads identifiers, never prose —
so the boundary is honoured deliberately here rather than leaned on.
DECISION 9: nine bodies stay plain. A failed-login notice written as a letter is
indistinguishable in register from the phishing mail it warns about.
DECISION 10 corrects a fact the plan had wrong by omission. §8.6 records that
uo.points.rank_changed cannot address a person, and the same reasoning was
silently assumed to cover the governor. city.update's `governor` is written by
BridgeJson.Actor(), which emits acct and webId — the new governor is addressable
today, no protocol change. uo.governor.appointed is the 25th trigger.
Also records what building it found: the no-conditionals/empty-string collision
that the presentational fragments exist for, the render sweep that found three
defects no check could see (an optional in a subject line, multi-optional detail
lines, a trailing fragment used as a leading one), the trigger-bound template
that could not reference its own unsubscribe link, and the trigger-id/template-key
grammar mismatch.
docs/modules/uo/API.md gains §5.7 and §5.8.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -157,8 +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 **24 event triggers** and
|
||||
**3 audiences** through `api.registerEventTriggers` / `api.registerAudiences`
|
||||
it is the one an operator interacts with by name. `module-uo` declares **25 event triggers** and
|
||||
**3 audiences** through `api.registerEventTriggers` / `api.registerAudiences`, and ships
|
||||
**32 message bodies and 25 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.
|
||||
|
||||
@@ -180,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` | Each is about one person's own property, account or character. All ten resolve through the frame's `ownerAcct` to `shard_account_links`; an unlinked game account is nobody to notify |
|
||||
| `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 |
|
||||
| `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 |
|
||||
@@ -257,3 +258,76 @@ load-bearing. Three mappings read a row the state write is about to delete or re
|
||||
`house.remove` drops the house whose stored `ownerAcct` is the only place a collapsed house's owner
|
||||
appears (the frame carries a serial alone); and `guild.leave` / `guild.remove` need the roster and
|
||||
board mirrors to name who left and which guild it was. Resolving afterwards finds nobody, every time.
|
||||
|
||||
### 5.7 The shipped bodies (Phase 11b)
|
||||
|
||||
Declaring a trigger says what an event IS. It says nothing about what the message reads like, and
|
||||
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
|
||||
[`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
|
||||
(ENGAGEMENT.md Phase 11b, decision 8) was a sender per family rather than one voice across all of
|
||||
them: a shard where Lord Blackthorn writes to you personally about a champion spawn is a shard where
|
||||
the letter about your governorship means nothing.
|
||||
|
||||
| Sender | Families |
|
||||
| --- | --- |
|
||||
| **Lord Blackthorn's court** | the governorship, the elections — the crown's business and nothing else |
|
||||
| **the Office of Deeds** | houses |
|
||||
| **the Merchants' Guild** | vendors |
|
||||
| **a guild herald** | guild departures and dissolutions |
|
||||
| **the town crier** | champion spawns |
|
||||
| **a guildmaster** | skills, quests |
|
||||
| **the Chronicler of the Dead** | deaths and murders |
|
||||
| **the keeper of the rolls** | leaderboards |
|
||||
|
||||
Each ships an `email` body and an `inapp` body **in the same voice** — one rule fires on both at once,
|
||||
and a player who reads the inbox item and then the mail must not meet two different narrators. The
|
||||
**digest** stays core's generic `notify.digest`: a day of events rolled into a list is not a letter
|
||||
from anybody.
|
||||
|
||||
**Nine stay plain, and the line is where fiction costs something real** (decision 9). Both
|
||||
account-security triggers, `uo.server.up`/`down`, and the five staff- and admin-ceilinged ones point
|
||||
at core's `notify.event` / `inapp.event` and author nothing — which is also §4.6.1 property 1 being
|
||||
exercised at scale. A failed-login notice written as *"a stranger sought entry to thy account"* is
|
||||
indistinguishable in register from the phishing mail it warns about, and a moderator reading
|
||||
`uo.cheat.detected` at two in the morning wants a name, a rule and a timestamp rather than a scroll.
|
||||
|
||||
**An operator whose shard is not Blackthorn's Britannia edits these rows.** The template editor is
|
||||
where, and `customized = 1` then protects the edit from every later seed — the bodies are defaults,
|
||||
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
|
||||
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.8 The presentational fragments, and why they exist
|
||||
|
||||
A template has **no conditionals**, by design, and an unset optional interpolates to the **empty
|
||||
string**. That is right for a structural body and wrong for a sentence: *"Be it known that ,
|
||||
recorded to thy name, is this day found ."* So the ternary stays in `shardEngagement.js` and its
|
||||
result arrives as a declared optional — Phase 5a's `forWhom` precedent. Two shapes, and the `example`
|
||||
on each declaration shows which it is:
|
||||
|
||||
- a **LABEL** always has a value and carries a sentence's spine. `houseLabel` is the name and region,
|
||||
falling back to the seal number, because a warning has to name something the owner can act on.
|
||||
- a **TRAILING FRAGMENT** may be empty and leads with **its own space**, so `{{slainBy}}.` closes as
|
||||
*"has fallen."* either way.
|
||||
|
||||
They are declared `required: false` deliberately: a required variable missing **refuses the emit**,
|
||||
and a dropped notification is worse than a cosmetic hole. Nothing at runtime therefore notices a
|
||||
mapper that forgot one, so `server/test/engagementSeeds.test.js` asserts every label is supplied on
|
||||
every path that emits its trigger.
|
||||
|
||||
A whole **detail line** works the same way one level up (`ledgerLine`, `whereLine`): four optional
|
||||
numbers assembled into a sentence by the mapper, and absent entirely when the frame carried none of
|
||||
them — the same argument `place()` makes for coordinates. A pre-v5 vendor frame otherwise renders
|
||||
*"On hand: gold. Charged each period: gold."*
|
||||
|
||||
Reference in New Issue
Block a user