docs(events): Phase 10 as built — the integrations, and a ceiling per firing

Five files. `EVENTS.md` §F gains "What Phase 10 settled" and §D/§J are corrected
to what was built; `EVENTS_PLAN.md` records the phase as built with its six
decisions, the live walk and the three defects it found; `MODULE_API.md` folds
two envelope members into 1.10.0 in place; `ENGAGEMENT.md` §8.5's "come back for
X" row is resolved; `BACKEND_DESIGN.md` gains the eleventh event table and the
two columns Phase 10 put on tables that already existed.

The four contract findings, three of which are rules rather than facts about
events:

- a firing may carry a ceiling of its own and it may only ever NARROW, applied
  at the send-time G24 gate as meet(declared, emitted). §I promised a rehearsal
  "ceilinged to staff" and nothing implemented it;
- two incomparable ceilings refuse rather than resolve, which is §5.1a rule 3
  reused rather than re-argued;
- a module reports participants on the success envelope and there is no other
  door;
- a trigger that is not also a STREAM has no push and says nothing about it, so
  every stream must be a trigger while a trigger need not be a stream.

And a trigger's `description` is read by a recipient, not only by an operator:
it is the structural projection's `intro` fallback, so a line naming its own
audience ends up in that audience's inbox.

- [x] AI-assisted: Claude Code (Opus 5).

Code: RunicGateway/website#TBD.

Co-Authored-By: Claude <noreply@anthropic.com>

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
2026-09-04 13:06:06 -05:00
parent c2fba44ffd
commit e12cc0dbb1
5 changed files with 247 additions and 13 deletions

View File

@@ -47,6 +47,13 @@ a human and publish results.
> API for the first time. 1.10.0 has not shipped, so the whole module contract reaches an author as
> one version they read once, which was the argument for putting the lease declaration here in the
> first place.
>
> **Phase 10 amended it a second time, under the same rule**, with `participants` on the success
> envelope and an optional narrowing `ceiling` on `ctx.events.emit`'s. `main` still declares 1.9.0,
> so 1.10.0 remains unshipped and the whole event contract — actions, budgets, leases, option
> sources, reconcile, participants — still reaches an author as one number. The `ceiling` member is
> the one of the two that widens something 1.9.0 already shipped, and it is additive and optional:
> a module that never passes it is emitting exactly what it emitted before.
**Only one of the four is new machinery.** The ACTION registry has staged core's `core.announce`,
`core.wait` and `core.cue` on every boot since Events Phase 1; what it never had was a way in —
@@ -273,9 +280,28 @@ unchanged, and so is every rule about composition narrowing rather than widening
**1.10.0 — the event contract** (`website/EVENTS.md` §F). Four additions, no removals and no changed
signature, so minor; `module-uo`'s `coreApi: "^1.9.0"` still resolves and it registers no actions
until `EVENTS_PLAN.md` Phase 9. `api.registerEventActions([...])`, `api.registerEventBudgets([...])`,
`api.registerEventLeases([...])` and `api.registerEventOptionSources([...])` (§2.4). Nothing was added
to `ctx`: an action is called BY core, so what a module needs from this contract it is handed in the
envelope rather than reaching for.
`api.registerEventLeases([...])` and `api.registerEventOptionSources([...])` (§2.4). Almost nothing
was added to `ctx`: an action is called BY core, so what a module needs from this contract it is
handed in the envelope rather than reaching for`ctx.events.reconcile()` (Phase 8) is the one
exception, because only the module knows when the game it talks to has restarted.
Two members joined it in Phase 10, both on an envelope:
- **`participants` on an action's SUCCESS envelope** (`EVENTS.md` §D, §J). An action may answer
`{ ok: true, participants: [{ memberKey, userId?, score?, meta?, joinedAt? }] }` and core records
them against the run, on both success shapes, beside `resources`. `memberKey` is required and
module-opaque; `userId` is optional and is the module's own answer to "is this player a website
account", because core cannot map one and a core that guessed would be one game's identity model
compiled into core. A bad entry is dropped and logged, never a retry: a retried step re-dispatches
a world write that already happened.
- **`ceiling` on `ctx.events.emit`'s envelope** (`EVENTS.md` §I). An optional audience ceiling for
THIS firing, which may only ever narrow: the send-time G24 gate applies `meet(declared, emitted)`,
so a rule wider than the meet is refused and one narrower is unaffected. Two incomparable ceilings
meet to null and every rule is refused, which is §5.1a rule 3's posture rather than a guess about
which branch was meant. The case that forced it is core's own — a rehearsal fires the same
lifecycle triggers as a real run and must not mail every subscriber — and it is on the shared
envelope rather than in `events/` because "this particular firing is narrower than the kind
usually is" is a fact any emitter can have.
**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.
@@ -1020,6 +1046,19 @@ rather than implementation and belong here:
rule generalises past that one pairing — an action is the near end of a call with a far end, and
the near end has to outlive it. This is why `uo.broadcast`, whose whole safety property is that it
is attempted once, declares 15000.
- **A module reports who took part on the envelope, and there is no other door.** `participants`
rides back from `perform()` exactly as `resources` does, on both success shapes — including
`await: 'human'`, because a cue's confirm finishes the step without a second dispatch and that is
therefore the only moment its participants can be recorded. There is deliberately no
`ctx.events.participants` and no route: a second write path into a run core is mid-tick on would be
a second thing that can race the step claim. One step may report at most 5000, the same bound the
engagement engine puts on a list of users a caller may assert, and a member reported twice in one
step is recorded once with the duplicate named.
- **A declaration's ceiling bounds the kind of event; a firing's `ceiling` bounds the occasion.** An
emitter that already knows this particular firing must not reach as far as the declaration allows
passes one, and the gate takes the meet. It only narrows — passing a wider value changes nothing —
and passing something incomparable with the declaration refuses every rule rather than resolving
to either. See §1.1 under 1.10.0.
- **`once`, on all four.** A batch is a module's complete statement about what it declares; a second
call is a module changing its mind halfway through `register()` rather than adding to it. And they
STAGE, like every registration above: a module that registers two budgets and then throws has left