Phase 11a declared 24 triggers and stopped where the plan said it would. Standing
11b up found that the next 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 in them, and there was no
registerTemplates or registerRules anywhere in registries.js.
So a module could say what an event's payload was and could never say what the
mail should read like. That is tolerable for one trigger and not for a catalogue,
and it is decisive once the bodies carry domain prose core must not contain (§5.2).
Adds api.registerEngagementSeeds({ templates, ruleGroups }) — MODULE_API 1.9.0.
The module supplies data; core keeps seedOne's customized skip, its seed_version
comparison and the block registry's validation, which is the whole argument for a
registry over the ctx.query a module already holds: a copy of any of those living
outside engagement/ would drift the first time core improved the original, and the
drift would surface as a mail somebody already received.
The two halves behave differently, deliberately:
- Templates re-ensure on every boot, so a bumped seedVersion reaches every
deployment except the ones where an operator edited that row.
- Rule groups are ONE-SHOT, each under its own settings guard — re-ensuring
would resurrect a rule an operator deleted and reset one they enabled. This is
11a's seed-key finding stated as an API rather than as a warning: a rule
appended to an existing group reaches fresh installs only, and one that must
reach stamped deployments takes a new group key.
Three prohibitions, each a shipped mistake that would only surface as mail: a
seeded rule is always enabled = 0 (Q3's invariant, ignored rather than refused so
a typo cannot take a module offline at boot); a module may not mark a template
protected; and a rule may only name its own trigger ids and its own or core's
template keys, with template keys namespaced because the key column is UNIQUE.
Runs from modules/lifecycle.js boot() rather than seedDefaults(), and that is
forced rather than chosen: server.js seeds before it requires app.js, and
requiring app.js is what runs the loader — at the moment core seeds, no module has
registered anything. Placed after the installed_modules reconcile (so a disabled
or failed module is skipped) and before the onBoot dispatch (so a module warming a
cache may assume its rules exist).
16 new tests; 1549 core tests green; check:modules clean.
Refs docs#/ENGAGEMENT.md Phase 11b decision 7.
Co-Authored-By: Claude <noreply@anthropic.com>
140 lines
8.6 KiB
JavaScript
140 lines
8.6 KiB
JavaScript
// The module API version — the single number a module's `coreApi` range is
|
|
// checked against (docs/website/MODULE_API.md §1.1).
|
|
//
|
|
// Bump minor when a member is ADDED to ctx or a new register* call appears;
|
|
// major when one is removed, its signature changes, or its behaviour changes
|
|
// without a signature change. A core-internal refactor behind an unchanged
|
|
// member is not a bump.
|
|
//
|
|
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
|
|
// has nothing to say about a website module) and from any module's own version.
|
|
|
|
// 1.9.0 - a sixth registration call: `api.registerEngagementSeeds({ templates,
|
|
// ruleGroups })` (docs/website/ENGAGEMENT.md Phase 11b, decision 7). A module
|
|
// could declare a trigger from 1.7.0 and could never say what the mail should
|
|
// READ like: `templateSeeds.js` and `coreRules.js` are core files with core
|
|
// arrays in them, so a module's notification was core's generic body or nothing.
|
|
// Additions only, so minor: every module written against 1.8.0 keeps working and
|
|
// simply seeds nothing.
|
|
//
|
|
// **What a module has to know about it beyond the new name**, because the two
|
|
// halves behave differently on purpose:
|
|
//
|
|
// - **Templates are re-ensured on every boot**, under `seed_key` /
|
|
// `seed_version` / `customized` - so bumping a body's `seedVersion` reaches
|
|
// every deployment except the ones where an operator edited that row, and a
|
|
// template added in a later module version reaches everyone.
|
|
// - **Rules are one-shot, per named GROUP.** Re-ensuring one would resurrect a
|
|
// rule an operator deleted and reset one they enabled, so each group carries
|
|
// its own settings guard. A rule appended to an existing group therefore
|
|
// reaches FRESH INSTALLS ONLY; one that must reach deployments already
|
|
// stamped takes a new group key. That is 11a's seed-key finding as an API
|
|
// rather than as a warning, and the module makes the choice knowingly.
|
|
//
|
|
// Two things it deliberately does not permit. A seeded rule is always
|
|
// `enabled = 0` - it is not a parameter - which is Q3's invariant surviving
|
|
// contact with the largest seed set in the workstream. And a module may not mark
|
|
// a template `protected`: that flag means "the system breaks without this body",
|
|
// which is true of a password reset and of nothing a module ships, and a module
|
|
// setting it would take an operator's delete button away.
|
|
//
|
|
// It runs from `modules/lifecycle.js` `boot()` rather than `seedDefaults()`, and
|
|
// that is forced rather than chosen: core seeds before `app.js` is required, and
|
|
// requiring `app.js` is what runs the loader.
|
|
|
|
// 1.8.0 - a seventh value in the audience ceiling lattice: `admin`, a child of
|
|
// `staff` (docs/website/ENGAGEMENT.md Phase 11, decision 1). A module may now
|
|
// declare `ceiling: 'admin'` on a trigger or an audience, so the set of values
|
|
// `registerEventTriggers` and `registerAudiences` accept grew. Additions only,
|
|
// so minor: every declaration valid before is valid now, no stored value
|
|
// changes, and module-uo's `coreApi: "^1.3.0"` still resolves.
|
|
//
|
|
// It exists because Phase 11's operator-facing triggers - `uo.audit.staff_action`,
|
|
// `uo.economy.milestone`, `uo.world.saved` - are described everywhere as
|
|
// admin-audience, and the narrowest value the lattice had was `staff`, which
|
|
// means admin / editor / moderator. Ceilinging them there would have permitted a
|
|
// rule that mails the staff audit digest to every editor.
|
|
//
|
|
// **What a module has to know about it beyond the new name.** `admin` is the one
|
|
// pair in the tree with real containment - every admin is staff - so it is the
|
|
// only place `permits` is true between two non-`authenticated` values:
|
|
// `permits('staff', 'admin')` holds and nothing else of that shape does. A
|
|
// trigger ceilinged `staff` therefore accepts an `admin` audience, which is the
|
|
// intended narrowing, and the reverse is refused as it should be.
|
|
|
|
// 1.7.0 — the engagement contract (docs/website/ENGAGEMENT.md Phase 2).
|
|
// Additions only, so minor: `api.registerEventTriggers([...])`,
|
|
// `api.registerAudiences([...])`, `ctx.events.emit(triggerId, envelope)` and
|
|
// `ctx.inbox.push(userId, item)`. module-uo's `coreApi: "^1.3.0"` still resolves.
|
|
//
|
|
// **As in 1.6.0, the number covered the whole surface and the members arrived by
|
|
// phase, and all of them have now arrived.** `ctx.inbox.push` threw until Phase 7
|
|
// built the in-app channel and the table behind it — the same choice, for the
|
|
// same reason: a member of 1.7.0 that were absent would have made the version a
|
|
// lie, and one that silently accepted data into a table that did not exist would
|
|
// have been worse than either. **Filling it in is NOT a bump**: the signature is
|
|
// the one 1.7.0 declared, and a module written against it needs no change. What a
|
|
// module WILL see differently is the throw becoming a write, which is the whole
|
|
// point of the phase.
|
|
//
|
|
// One thing here is not a member and is still part of the contract: a trigger id
|
|
// and a notification-stream id share ONE namespace (ENGAGEMENT.md §7.2, settled
|
|
// by the org lead in Phase 2). An id has exactly one owner across both facets,
|
|
// so a module cannot attach a payload contract to another module's stream. That
|
|
// tightens a rule rather than changing a signature, and nothing registrable
|
|
// before this bump becomes unregistrable after it — the id grammar was RELAXED
|
|
// in the same change (`_` is now legal inside a segment).
|
|
//
|
|
// 1.6.0 — the Team surface (docs/website/TEAMS.md Part 11). Additions only, so
|
|
// minor: `api.registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders })`,
|
|
// `ctx.teams.publish(event)`, `ctx.teams.reconcile({ reason })`,
|
|
// `ctx.teams.activity.push(items)`, `api.registerSlashCommands([...])`, and the
|
|
// client slots `team.overview` / `team.member.row`. module-uo's `coreApi:
|
|
// "^1.3.0"` still resolves.
|
|
//
|
|
// **The number covers the whole surface; the members arrived by phase, and all of
|
|
// them have now arrived.** `activity.push` landed with the Team activity feed
|
|
// (§4, phase 3) and `registerSlashCommands` with the Discord commands (§7.1,
|
|
// phase 7); until each did, it was present and THREW rather than being absent or,
|
|
// worse, silently accepting data into a table that did not exist. Nothing in
|
|
// 1.6.0 throws any more.
|
|
//
|
|
// 1.5.0 — a CLIENT addition: `PublicLayout` takes an optional `shell` prop that
|
|
// renders the page body wrapper core's own pages write by hand (MODULE_API.md
|
|
// §3.4). Minor, not major: §3.4 makes *changing* a kit component's props a major
|
|
// bump because that breaks a call already written, and adding an optional one
|
|
// breaks nothing — omitting `shell` is 1.4.0's behaviour exactly. Nothing on the
|
|
// server changed; this file bumps for the reason below. Found by the Integration
|
|
// Kit's acceptance run (docs/modules/kit-acceptance.md), where a module built
|
|
// exactly as the kit teaches rendered outside the site's page column.
|
|
//
|
|
// 1.4.0 — no member changed. §2.7 gained one prohibition: a module does not open
|
|
// a connection to a game server from the website process; it talks to a sidecar,
|
|
// which owns the durable copy of the game's state. Minor rather than major
|
|
// because the SURFACE is identical to 1.3.0 — module-uo's `coreApi: "^1.3.0"`
|
|
// still resolves, and it already complies — but a module written against 1.3.0
|
|
// could satisfy every member and still be built the wrong way round, which is
|
|
// what this number now says. The one §2.7 rule with no CI behind it: an outbound
|
|
// socket is not statically detectable the way an internal require is.
|
|
//
|
|
// 1.3.0 — three CLIENT additions from Phase 3 slice 3: a nav item may carry an
|
|
// `icon`, core declares a `player.invite.accepted` slot, and `window.__rg.api`
|
|
// gained `BASE` (which §3.5 always specified and shared.js never published).
|
|
// Nothing on the server changed; this file bumps for the reason below.
|
|
//
|
|
// 1.2.0 — the CLIENT registry gained `registerExtension` and core gained client
|
|
// extension slots (MODULE_API.md §3.7): the twin of this half's declareSlot /
|
|
// registerExtension, for module content inside a core *page* rather than under a
|
|
// core route prefix. Nothing on the server changed, and this file bumps anyway —
|
|
// the two halves state ONE version, because a module declares a single `coreApi`
|
|
// range and is served one chunk (client/src/modules/version.js).
|
|
//
|
|
// 1.1.0 — `ctx` gained `activity.log`, `users.getById` and `site.baseUrl`, each
|
|
// because module-uo's extraction needed it and none of them could be vendored:
|
|
// an admin action a module performs belongs in core's one audit log, the
|
|
// extension slot needs the user its prefix names, and §2.7 forbids a module
|
|
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
|
|
const MODULE_API_VERSION = '1.9.0'
|
|
|
|
module.exports = { MODULE_API_VERSION }
|