docs(events): what the integration kit's fifth chapter settled (Phase 15) #226

Merged
whitlocktech merged 1 commits from docs/events-p15-kit into edge 2026-09-08 23:26:41 +00:00
Member

EVENTS.md §F's Integration Kit paragraph gains what building it produced, and detail becomes a real envelope member. Pairs with Integration-kit#10 and website#197.

It is three chapters, not one

The plan said "a fifth chapter". Building it found that was not enough: the book as it stood taught a read-only data path end to end — a sidecar that listens and stores, a plugin that never blocks the game thread, a module that reads its own tables. Nothing anywhere told a reader to build a command path. So a chapter 5 teaching a module to send an idempotency key would have been addressing it to a sidecar with nowhere to put it.

Chapter 3 gains §2a and chapter 4 gains "A command that changes the world runs at most once". Both open by saying they are skippable until you want chapter 5.

Two defects, and the prose found neither

Both came from running the kit template's real declarations through core's real registry and its real envelopes through core's real events/dispatch.js classifier at edge.

1. An idempotency key belongs on a command, never on a question. A read that carries one is answered by an at-most-once store with the first read's reply, forever. The lease applied correctly, the game changed correctly, and the module could no longer see either — read() reported the pre-run baseline and inForce() said nothing was held. Recorded in §F, with the narrower rule that falls out of it: a key is for a write whose repetition would be a second effect; a write that merely sets a value to X is idempotent by its own nature.

2. detail was not an envelope member, and §H said it was. classify() reads ok, retry, error, await, holdFor, resources and participants — and had never read a detail. module-uo took §H at its word twice: uo.item.grant answers { granted, missed, why } and uo.world.save answers { started: true }, and both were writing into nothing. The grant is the one that mattered — a grant reaches the players a run's participation ledger holds, and which of them missed out is knowable only to the module, so an operator saw a step done and never learned four of twelve got nothing.

The second one is now fixed, not just recorded

Fixed by making the member real rather than by deleting the reporting, because §H's sentence was right and only its example was wrong. website#197 carries it; this PR documents it.

Where What changed
MODULE_API.md §1.1, 1.10.0 detail as a third envelope member beside Phase 10's two — optional, both success shapes, objects only, 4KB, dropped-and-logged rather than failing the step
MODULE_API.md §2.4 the contract rule: core reads no key out of it, because a switch on known keys anywhere in core would be core learning one module's vocabulary
EVENTS.md §F the finding and the fix, including the half that is easy to miss — the run console's describeLogLine default returns a kind word, so the new line would have rendered as the literal string step.detail: the channel existing and showing nothing
EVENTS.md §H the wipes row no longer claims detail is unread, and no longer spells a successful revert with one

module-uo needed no change — the code it already shipped started working. MODULE_API_VERSION stays 1.10.0, amended in place; still on edge. The failure channel is unchanged and is still error alone.

EVENTS_PLAN.md Phase 15 is annotated with what it actually was.

  • AI-assisted: written with Claude Code (Claude Opus 5).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

`EVENTS.md` §F's Integration Kit paragraph gains what building it produced, and `detail` becomes a real envelope member. Pairs with **Integration-kit#10** and **website#197**. ## It is three chapters, not one The plan said "a fifth chapter". Building it found that was not enough: the book as it stood taught a **read-only** data path end to end — a sidecar that listens and stores, a plugin that never blocks the game thread, a module that reads its own tables. Nothing anywhere told a reader to build a **command** path. So a chapter 5 teaching a module to send an idempotency key would have been addressing it to a sidecar with nowhere to put it. Chapter 3 gains §2a and chapter 4 gains *"A command that changes the world runs at most once"*. Both open by saying they are skippable until you want chapter 5. ## Two defects, and the prose found neither Both came from running the kit template's real declarations through **core's real registry** and its real envelopes through **core's real `events/dispatch.js` classifier** at `edge`. **1. An idempotency key belongs on a command, never on a question.** A read that carries one is answered by an at-most-once store with the *first* read's reply, forever. The lease applied correctly, the game changed correctly, and the module could no longer see either — `read()` reported the pre-run baseline and `inForce()` said nothing was held. Recorded in §F, with the narrower rule that falls out of it: a key is for a write whose repetition would be a second *effect*; a write that merely *sets* a value to X is idempotent by its own nature. **2. `detail` was not an envelope member, and §H said it was.** `classify()` reads `ok`, `retry`, `error`, `await`, `holdFor`, `resources` and `participants` — and had never read a `detail`. **`module-uo` took §H at its word twice**: `uo.item.grant` answers `{ granted, missed, why }` and `uo.world.save` answers `{ started: true }`, and both were writing into nothing. The grant is the one that mattered — a grant reaches the players a run's participation ledger holds, and *which of them missed out* is knowable only to the module, so an operator saw a step `done` and never learned four of twelve got nothing. ## The second one is now fixed, not just recorded **Fixed by making the member real rather than by deleting the reporting**, because §H's sentence was right and only its example was wrong. `website#197` carries it; this PR documents it. | Where | What changed | | --- | --- | | `MODULE_API.md` §1.1, 1.10.0 | `detail` as a third envelope member beside Phase 10's two — optional, both success shapes, objects only, 4KB, dropped-and-logged rather than failing the step | | `MODULE_API.md` §2.4 | the contract rule: core reads **no key** out of it, because a switch on known keys anywhere in core would be core learning one module's vocabulary | | `EVENTS.md` §F | the finding and the fix, including the half that is easy to miss — the run console's `describeLogLine` default returns a kind *word*, so the new line would have rendered as the literal string `step.detail`: the channel existing and showing nothing | | `EVENTS.md` §H | the wipes row no longer claims `detail` is unread, and no longer spells a successful revert with one | **`module-uo` needed no change** — the code it already shipped started working. `MODULE_API_VERSION` stays **1.10.0, amended in place**; still on `edge`. The failure channel is unchanged and is still `error` alone. `EVENTS_PLAN.md` Phase 15 is annotated with what it actually was. - [x] AI-assisted: written with Claude Code (Claude Opus 5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 1 commit 2026-09-08 23:12:50 +00:00
§F's Integration Kit paragraph gains what building it produced, and §H loses an
envelope member that does not exist.

**It is three chapters, not one.** The book taught a read-only data path end to
end and never told anyone to build a command path, so a chapter 5 teaching a
module to send an idempotency key would have addressed it to a sidecar with
nowhere to put it. Chapters 3 and 4 each gain one section, both skippable.

**Two defects, both found by running the template through core's real registry
and real dispatcher rather than by writing prose:**

  * **An idempotency key belongs on a command, never on a question.** A read
    carrying one is answered by an at-most-once store with the FIRST read's
    reply, forever — the lease applied correctly and the module could no longer
    see it.

  * **§H named a `detail` member on an envelope and `classify()` has never read
    one.** The sentence §H was making is right and its example was wrong: a
    revert of something gone is `{ ok: true }`. Corrected in place, with the
    finding recorded in §F.

That second one has a consequence outside this PR: **`module-uo` took §H at its
word twice.** `uo.item.grant` answers `detail: { granted, missed, why }` and
`uo.world.save` answers `detail: { started: true }`, and neither reaches a
screen or the ledger. The grant is the one that matters — which recipients did
not receive the item is reported nowhere else. Recorded here rather than fixed;
the fix is a Module-uo change and is the org lead's call.

Pairs with Integration-kit#10, which is red on `checkCoreApi` by design and
merges in the P16 cutover with its pin move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
whitlocktech merged commit 82e22ea365 into edge 2026-09-08 23:26:41 +00:00
whitlocktech deleted branch docs/events-p15-kit 2026-09-08 23:26:44 +00:00
Sign in to join this conversation.
No description provided.