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

@@ -1119,10 +1119,22 @@ Design of record: [`TEAMS.md`](TEAMS.md) Parts 2 and 5. The contract surface a m
### The six event tables — the engine's, game-agnostic (events phase 1)
Design of record: [`EVENTS.md`](EVENTS.md) §D. Nine core tables are specified there; **six land in
Phase 1** — the ones that do not depend on the module contract. `event_action_settings`,
`event_run_budget`, `event_run_resources` and `event_run_participants` arrive with the phases that
give them a writer, rather than as empty tables nothing reads.
Design of record: [`EVENTS.md`](EVENTS.md) §D, which is where the full column list of every event
table lives. **Six land in Phase 1** — the ones that do not depend on the module contract — and are
spelled out below. The rest arrive with the phase that gives each a writer, rather than as empty
tables nothing reads: `event_run_phase_gates` in Phase 5, `event_action_settings` and
`event_run_budget` in Phase 6, `event_run_resources` in Phase 8, and **`event_run_participants` in
Phase 10** — `run_id` + a module-opaque `member_key` that is `UNIQUE` together, a nullable `user_id`
that `SET NULL`s so a record of what happened survives an account deletion, a `DECIMAL(18,4)` score
and a `rank_at` written only when results are published. Core writes it and sources none of it: a
`member_key` → account mapping is one game's, and a module reports both halves on its action's
success envelope.
Phase 10 also put two columns on tables that already existed — `event_runs.results_published_at`
(below) and a nullable `announce_jobs.run_id`, which is what lets an event announce a post the news
pipeline has already announced without either job standing on the other's toes. Everything that
means *"the post's job"* — the post admin panel, its retry button, `posts.announced_at` — still means
the one with a NULL `run_id`.
Core owns the engine; a module owns the meaning. No column below carries a game noun: an action id,
a `scope`, a resource kind and a budget dimension are opaque strings core stores and never
@@ -1202,6 +1214,7 @@ makes a run reproducible and an audit answerable after the definition has moved
| params | JSON NULL | |
| rehearsal | TINYINT(1) NOT NULL DEFAULT 0 | dispatches for real; excluded from the public calendar and from participation history |
| started_at / ended_at | DATETIME NULL | |
| results_published_at | DATETIME NULL | when the run's results table was last ranked and published (phase 10). A stamp rather than a status: *"may I show this table"* and *"when was it settled"* are the same column. `core.results.publish` re-stamps rather than guarding on NULL, because a second publication after a late correction is a real one |
| claimed_by / claim_expires_at | VARCHAR(64) NULL / DATETIME NULL | the lease. Written by the runner |
| started_by | INT NULL FK→users(id) ON DELETE SET NULL | |
| last_error | VARCHAR(500) NULL | |