docs(engagement): MODULE_API 1.8.0, the UO trigger catalogue, the news path
The documentation half of Phase 11a (website#177, Module-uo#22). Three files,
each carrying one obligation from 6.0b's Phase 11a row.
**MODULE_API.md -> 1.8.0.** A seventh audience ceiling, `admin`, as a child of
`staff`. One addition, no removal, so minor -- every declaration valid under
1.7.0 is valid now and no stored value changes. 2.4's ceiling bullet now names
seven values rather than six, and says the thing a module author actually needs:
`admin` is the ONLY pair in the whole lattice with real containment, so
`permits('staff','admin')` holds and it is the only place `permits` is true
between two values below `authenticated`.
Also corrects two 2.3 rows that were stale rather than wrong: `ctx.inbox.push`
has not thrown since Phase 7, and both members now name the consumer that
actually calls them.
**modules/uo/API.md gains 5**, the full trigger catalogue -- which 6.0b asked for
in as many words ("the full trigger catalogue, its audiences and its ceilings,
not one entry"). The 24 triggers grouped by ceiling with the reason for each
ceiling; the four rows that deliberately do NOT ship with their recorded reasons;
the two that need a patch tier or a v5 overlay; the three things a rule cannot
express and are therefore done in the mapper; the three registered audiences; and
the ingest ORDERING, which is load-bearing rather than incidental.
**BACKEND_DESIGN.md -- the news publish path.** It described a direct
`pushDispatch.publish('news.post', ...)` from the create/publish-post path. That
call is gone: it is an emit through the engagement engine now, so news push rides
a rule, and core seeds that rule disabled. The note says so as an operator-visible
upgrade step, names the separate one-shot settings key and states what did NOT
change -- the announce legs and the post hooks still fire exactly as they did.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1217,8 +1217,27 @@ registers device endpoints (`/auth/me/devices`); nothing is pushed unless subscr
|
||||
**content-free tickle** — `{ stream, ref }`, no sensitive data — POSTed to each subscribed device's
|
||||
self-hosted **ntfy** endpoint (`utils/pushDispatch`); the app wakes and pulls the real, ownership-
|
||||
checked content over the authenticated API. Two producers fan out through the one publisher: the shard
|
||||
ingest dispatcher (`utils/shardIngest`, beside the SSE broadcast) for shard-derived streams, and the
|
||||
create/publish-post path for `news.post`. The catalog is assembled at boot by
|
||||
ingest dispatcher (`utils/shardIngest`, beside the SSE broadcast) for shard-derived streams, and — from
|
||||
**ENGAGEMENT.md Phase 11** — the **engagement engine** for `news.post`.
|
||||
|
||||
> **The news publish path changed in Phase 11 and it is an operator-visible change.** Publishing a
|
||||
> news post used to call `pushDispatch.publish('news.post', …)` directly from
|
||||
> `admin.controller.js`'s `announceIfNewlyPublished`; it now calls
|
||||
> `utils/newsNotify.emitNewsPost`, which emits core's declared `news.post` trigger and lets the engine
|
||||
> decide. Push therefore rides a **rule** like every other channel, and core seeds that rule
|
||||
> `enabled = 0` beside the four Team ones — so **news push stops on upgrade** until an operator enables
|
||||
> it in Admin → Engagement → Rules, where a banner says so. The `news.post` seed carries its own
|
||||
> one-shot settings key (`engagement_news_rule_seeded`) rather than joining the Team group's, because
|
||||
> the Team key is already stamped on exactly the deployments this affects.
|
||||
>
|
||||
> **The other two things a publish fires are untouched.** `announceJobs.enqueueIfNeeded` (a one-shot
|
||||
> delivery to a channel of the deployment — the in-game town crier, Discord #news, with retry) and
|
||||
> `registries.dispatchPostHook('onSaved')` (idempotent state mirroring, which also runs on delete) are
|
||||
> different kinds of thing and still fire exactly as they did. The emit is gated on the same
|
||||
> `enqueueIfNeeded` job id the push was gated on — the single "newly published news" transition
|
||||
> signal — so an edit or a re-publish still does not re-fire.
|
||||
|
||||
The catalog is assembled at boot by
|
||||
`modules/registries.js` from core's own streams (`config/coreStreams.js` — just `news.post`) plus
|
||||
each installed module's. The seven shard streams and their event→stream mapping left with
|
||||
`module-uo` in Phase 3 and are registered by it; their ids are grandfathered to that module
|
||||
|
||||
@@ -26,13 +26,31 @@ here extends the contract first, in this file, before the module is written agai
|
||||
Core exports a single integer-major semver string from `server/src/modules/version.js`:
|
||||
|
||||
```js
|
||||
const MODULE_API_VERSION = '1.7.0'
|
||||
const MODULE_API_VERSION = '1.8.0'
|
||||
```
|
||||
|
||||
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
|
||||
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
|
||||
module chunk evaluates, which is earlier than any network round trip could answer.
|
||||
|
||||
**1.8.0 — a seventh audience ceiling: `admin`** (`website/ENGAGEMENT.md` Phase 11, decision 1). One
|
||||
addition and no removal, so minor; every declaration valid under 1.7.0 is valid now and no stored
|
||||
value changes. `admin` is a **child of `staff`**, so a module may declare `ceiling: 'admin'` on a
|
||||
trigger or an audience and a `staff`-ceilinged trigger accepts an `admin` audience as a narrowing.
|
||||
|
||||
It exists because the narrowest role-shaped value the lattice had was `staff`, which means **admin,
|
||||
editor AND moderator**. Phase 11's operator-facing triggers — a digest of what staff did in game, the
|
||||
economy thresholds, the world-save counts — are admin-audience everywhere they are described, and
|
||||
ceilinging them at `staff` would have let an operator save a rule that mails the staff audit digest to
|
||||
every moderator in it.
|
||||
|
||||
**What a module author has to know beyond the new name.** `admin` is the **only pair in the whole
|
||||
lattice with real containment** — every admin is staff, which is exactly what every other pair of
|
||||
branches lacks — so it is the only place `permits` is true between two values below `authenticated`.
|
||||
`permits('staff', 'admin')` holds; `permits('admin', 'staff')` does not, and neither direction holds
|
||||
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
|
||||
unchanged, and so is every rule about composition narrowing rather than widening.
|
||||
|
||||
**1.7.0 — the engagement contract** (`website/ENGAGEMENT.md` Phase 2). Four additions, no removals
|
||||
and no changed signature, so minor; `module-uo`'s `coreApi: "^1.3.0"` still resolves.
|
||||
`api.registerEventTriggers([...])` and `api.registerAudiences([...])` (§2.4) ·
|
||||
@@ -257,8 +275,8 @@ module-uo does not need is on the list.
|
||||
| `ctx.teams.publish` | `(event) => Promise<void>` | `model/teams/teamSync` | the Team provider's module (1.6.0) |
|
||||
| `ctx.teams.reconcile` | `({ reason }) => void`, returns at once | `model/teams/teamSync` | after a fresh account link (1.6.0) |
|
||||
| `ctx.teams.activity.push` | `(items) => Promise<void>`, fire-and-forget | `model/teams/teamActivity` | the Team provider's module (1.6.0) |
|
||||
| `ctx.events.emit` | `(triggerId, envelope) => void`, fire-and-forget | `utils/engagementEmit` | a module's own event mapper (1.7.0) |
|
||||
| `ctx.inbox.push` | `(userId, item) => Promise<void>` — **throws until Phase 7** | the in-app channel | not yet (1.7.0) |
|
||||
| `ctx.events.emit` | `(triggerId, envelope) => void`, fire-and-forget | `utils/engagementEmit` | `module-uo`'s `utils/shardEngagement.js`, off the shard feed (1.7.0) |
|
||||
| `ctx.inbox.push` | `(userId, item) => void`, fire-and-forget | the in-app channel (live since Phase 7) | `module-uo` reaches both through `server/core.js` (1.7.0) |
|
||||
|
||||
**`ctx.events.emit(triggerId, envelope)`** fires an event the module DECLARED with
|
||||
`api.registerEventTriggers` (§2.4). It is the push half of the engagement seam
|
||||
@@ -669,10 +687,12 @@ Six things about it are contract rather than implementation:
|
||||
because under one namespace they are the same ids.
|
||||
- **`ceiling` is required and has no default.** It is the audience ceiling (`ENGAGEMENT.md` §5.1a),
|
||||
and there is no safe value to guess: `owner` would silently break a broadcast and `authenticated`
|
||||
would silently widen a staff-only event. The six values are `everyone`, `authenticated`,
|
||||
`subscribers`, `members`, `staff` and `owner`, ordered by **containment and not by size** — a
|
||||
`staff` ceiling does NOT permit `owner`, because fewer people is not less exposure. A default
|
||||
`audience` wider than, or incomparable with, the ceiling is refused at registration.
|
||||
would silently widen a staff-only event. The seven values are `everyone`, `authenticated`,
|
||||
`subscribers`, `members`, `staff`, **`admin`** (1.8.0, a child of `staff`) and `owner`, ordered by
|
||||
**containment and not by size** — a `staff` ceiling does NOT permit `owner`, because fewer people is
|
||||
not less exposure. `staff` → `admin` is the single true refinement in the tree and the only pair
|
||||
below `authenticated` that `permits` accepts. A default `audience` wider than, or incomparable with,
|
||||
the ceiling is refused at registration.
|
||||
- **Every variable needs an `example`, and it is not decoration.** It is what makes previewing and
|
||||
test-sending a template possible without a live game event, which is the reason template systems go
|
||||
untested. A variable without one is refused.
|
||||
|
||||
Reference in New Issue
Block a user