feat(kit): the engagement contract, taught and built — cutover 5 of 7 #9
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/engagement-contract"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Engagement Phase 13, step 5 of 7 — the step the cutover order named and skipped. Steps 1–4 and 6 are merged; 7 (
runicgateway.com) is next.The kit was pinned to website
963d734— MODULE_API 1.6.0, the Teams cutover — and the platform is on 1.9.0. Three registrations and two calls arrived in between, and a reader building against this book would have found no mention of any of them.checkCoreApiis green against the new pin. That check is equality on purpose, and it is worth naming what it does not do: it clones this ref, so a core that moves past the pin never turns the repo red on its own. Nothing goes red until someone moves the pin. Between cutovers the kit is not wrong, it is dated — and this file is where the date is written down.What chapter 2 gained
A new section, Telling core something happened, and it is the seam where a module is most tempted to reach past the boundary. The rule it turns on is one sentence: you declare what CAN happen; core decides who is told.
ceilingis required, has no default, and is a containment tree rather than a size ladder. Astaffceiling does not permitowner— "one person" for a cheat-detection event is the player it was detected on. Fewer people is not less exposure.seedVersion; rule groups are offered once, per group key. So a rule appended to an existing group reaches fresh installs only — the guarantee, not a limitation to route around.ctx.events.emitbinds the owner from the calling module;ctx.inbox.pushis the direct write, for when there is nothing for an operator to decide.What the template builds
One trigger, one audience over the clan roster it already had, one seeded body, one seeded rule group, and an emitter in
boot.jsthat fires on the transition, not on the poll — with the two traps inside that function (read before write; a first boot is not a change) written where someone copying it will read them. Five new tests (the server suite goes 47 → 52), including the audience that resolves to nobody when its query throws, the rule that a seeded rule may not seed itself enabled, and the block-shape check below — which was verified by breaking it.Three claims the re-read found wrong, corrected here rather than shipped
subjectKeyagainst the declared variables and refuses the module. The draft taught a cooldown silently keyed onundefined— the thing the check exists to prevent, and a consequence a reader will never actually meet.emitthrows outside production and only drops-and-logs inside it. Teaching the second half alone leaves a developer meeting a throw the book says cannot happen.level: 2where the block registry takes'h2', and no blockids at all.The third is the one worth keeping.
registerEngagementSeedschecks thatblocksis a non-empty array and stops; the body is validated by the block registry, which runs in the editor and the renderer. That body would have registered, seeded, and failed the first time an operator opened it.It was found by running the template's
register()through core's real registry at the pinned ref — which CI does not do and cannot: thetemplatejob checks the version and runs the template against fakes, and a fake accepts what core refuses. So the gap is now named three times over — in the chapter, beside the code, and in the pin's own comment, which says a pin move is a run against a real core rather than an edit here — and the rule that bit has a test that fails on it (verified by breaking it).One check fixed
checkLinkswalked.core/. Bumping this pin means cloning core into that directory first —.gitignorereserves the name for exactly that — and the walk then reported nine broken links in someone else's README. CI never saw it: the clone happens in thetemplatejob and the check runs inprose. A failure only a person could meet, on the path this PR asks people to walk.🤖 Generated with Claude Code
The kit was pinned to website 963d734 -- MODULE_API 1.6.0, the Teams cutover -- and the platform is on 1.9.0. Three registrations and two calls arrived in between, and a reader building against this book would have found no mention of any of them: a module can now declare what its game can announce, and never who is told. Moving `ci/core-ref.json` is the mechanism for exactly this. The pin is now 66bb3b9a (website `main`, the engagement cutover) and `template/module.json` declares `^1.9.0`. What chapter 2 gained, under "Telling core something happened": * a TRIGGER is a payload contract, not a notification stream -- the two share one id namespace and are constantly confused; * `ceiling` is required, has no default, and is a CONTAINMENT tree rather than a size ladder (a `staff` ceiling does not permit `owner`); * an AUDIENCE resolver returns user ids and nothing else, resolves to NOBODY on failure, and takes CONSTANT params -- the constraint worth knowing before you design around it; * templates re-ensure per seedVersion, rule groups are offered ONCE per group key, so a rule appended to an existing group reaches fresh installs only; * `ctx.events.emit` binds the owner and is fire-and-forget; `ctx.inbox.push` is the direct write, for when there is nothing for an operator to decide. The template builds all of it: one trigger, one audience over the clan roster it already had, one seeded body and one seeded rule group, and an emitter in `boot.js` that fires on the TRANSITION rather than on the poll. Seven new tests, including the audience that resolves to nobody when its query throws. Three claims were wrong and are corrected here rather than shipped: * core validates `subjectKey` against the declared variables and refuses the module; the draft taught a cooldown keyed on `undefined`, which the check exists to prevent and a reader will never see. * `emit` throws OUTSIDE production and only drops-and-logs inside it. Teaching the second half alone leaves a developer meeting a throw the book says cannot happen. * the seeded body itself was malformed -- heading `level: 2` where the block registry takes 'h2', and no block ids at all. The third is the one worth keeping: `registerEngagementSeeds` checks that `blocks` is a non-empty array and stops, so that body would have registered, seeded, and failed the first time an operator opened it. Found by running the template's `register()` through core's real registry at the pinned ref -- which CI does not do, and cannot: the template job checks the version and runs the template against fakes. A fake accepts what core refuses. The gap is now named in the chapter, beside the code, and in the pin's own comment, and the rule that bit has a test that fails on it. Also: `checkLinks` skipped `.core/`. Bumping this pin means cloning core into that directory first, and the walk then reported nine broken links in someone else's README. CI never saw it -- the clone happens in the `template` job and the check runs in `prose` -- so it was a failure only a person could meet. Co-Authored-By: Claude <noreply@anthropic.com>