From ec55e66720d7e96f6819deaa20d4d7fd10d04031 Mon Sep 17 00:00:00 2001 From: wtclaude Date: Tue, 8 Sep 2026 18:50:04 -0500 Subject: [PATCH] docs(events): `detail` is a real envelope member now MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the finding recorded a commit ago: §H told a module the revert contract accepts a `detail`, `classify()` had never read one, and `module-uo` had been answering one since Phase 12b — so `uo.item.grant`'s report of which recipients missed out was written into nothing. Fixed in `website#197` by making the member real rather than by deleting the reporting, because §H's sentence was right and only its example was wrong. * **§1.1, 1.10.0** gains `detail` as a third envelope member beside Phase 10's two: optional, on both SUCCESS shapes, carried and never interpreted, objects only, 4KB, dropped-and-logged rather than failing the step. * **§2.4** gains 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** records 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. * **§H's wipes row** no longer claims `detail` is unread. MODULE_API stays 1.10.0, amended in place — still on `edge`. The failure channel is unchanged and is still `error` alone. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4 --- website/EVENTS.md | 43 ++++++++++++++++++++++++++++++++----------- website/MODULE_API.md | 25 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/website/EVENTS.md b/website/EVENTS.md index 85da4e9..42d291e 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -1139,16 +1139,37 @@ one paragraph each and all four are invisible until an outage. > - **§H names a `detail` member on an envelope and core has never read one** — see the correction > below. -> **`detail` is not an envelope member.** The Rust wipes row in [§H](#h--rust--oxide-compatibility) -> says the revert contract must accept `{ ok: true, detail: 'resource no longer exists' }`, and -> `events/dispatch.js`'s `classify()` reads `ok`, `retry`, `error`, `await`, `holdFor`, `resources` -> and `participants` — nothing else. A reason under any other name is dropped in silence and the -> operator sees `" refused"`. The sentence §H was making is right and its example is -> wrong: a revert of something that no longer exists is a success, and it is spelled `{ ok: true }`. -> **`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 one is the one that matters: which recipients did -> not receive the item is reported nowhere else. +> **`detail` was not an envelope member, and now it is** (`website#197`). §H's Rust wipes row told a +> module the revert contract accepts `{ ok: true, detail: 'resource no longer exists' }`; +> `events/dispatch.js`'s `classify()` read `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 `detail: { granted, missed, why }` and `uo.world.save` answers +> `detail: { 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. +> +> **Fixed by making the member real rather than by deleting the reporting**, because §H's sentence +> was right and only its example was wrong. `detail` is now optional on both SUCCESS shapes, +> **carried and never interpreted** — nothing in the dispatcher, the runner or the browser reads a +> key out of it — and the runner writes it as a `step.detail` run-log line. Its own log kind rather +> than a field on `resource.recorded`, because the grant that forced it ledgers nothing +> (`reversible: 'none'`) and reports no participants, so it would have had nowhere to ride. +> +> Two things about the fix are worth keeping. **Anything wrong with a `detail` is dropped and logged, +> never a failure** — a step that did what it was asked must not be re-run because its module's +> commentary was malformed, which is a world write repeated for a log line. And **the console's +> renderer was half the fix**: `describeLogLine`'s default returns a kind *word*, so a `step.detail` +> row falling through would have rendered as the literal string `step.detail` — the channel existing +> and showing nothing, which is the failure it was built to fix. It renders the module's keys +> generically; a switch on known keys would be the browser learning one module's vocabulary. +> **`module-uo` needed no change**: the code it already shipped started working. The failure channel +> is unchanged and is still `error` alone. + +> **A revert of something gone is spelled `{ ok: true }`**, and §H said `{ ok: true, detail: ... }` +> until Phase 15. `detail` is a real member now, but it is a *diagnostic line*, not the way a revert +> reports success — the success is `ok`, and a module that put its answer only in `detail` would be +> reverting nothing. --- @@ -1223,7 +1244,7 @@ more than one. Nothing here contradicts it. | Participation | The hard part | Substantially easier — hooks carry attacker and victim. | | **Rewards** | An item into a backpack. `reversible: 'none'` — once given it is gone. | A kit, a permission group, currency via an economics plugin, a cosmetic. **Several of those are revocable**, so a Rust reward may be `reversible: 'override'` — a weekend VIP group is a lease with a deadline, not a gift. Core sees the difference as one enum value it never interprets. | | Several servers | One shard | `run.scope` is in the run's unique key, so one definition fans out to six servers without colliding with itself. Caps are per-run, so a fan-out to six servers is six separate budgets rather than one shared pool. | -| Wipes | Never | Monthly, and a wipe invalidates every ledgered resource for that server at once. The revert contract must accept "gone, and that is fine" as a successful revert, spelled `{ ok: true }` — **not** `{ ok: true, detail: ... }`, which this row said until Phase 15 and which `classify()` has never read (see [§F](#f--the-module-contract)). A wipe also resets leased values to their defaults, a second reason restore must be idempotent. | +| Wipes | Never | Monthly, and a wipe invalidates every ledgered resource for that server at once. The revert contract must accept "gone, and that is fine" as a successful revert, spelled `{ ok: true }`. This row said `{ ok: true, detail: 'resource no longer exists' }` until Phase 15, when `detail` turned out to be a member nothing read; it is a real one now, but it is a diagnostic line beside the answer and never the answer itself (see [§F](#f--the-module-contract)). A wipe also resets leased values to their defaults, a second reason restore must be idempotent. | | Identity | In-game `[link` code | Steam — still `rust-dryrun` finding 1's open gap. Events neither closes it nor depends on it: `event_run_participants` carries a module-opaque `member_key`. | **The agnosticism is provable, not merely asserted.** Make `event_definitions.owner_module` nullable diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 8bf08ca..9ef276e 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -225,6 +225,24 @@ field: leaves the ledger alone: **"I do not know" is never read as "it is gone"**, and a resource a module reports missing becomes `orphaned` rather than `reverted`, because nobody asked for it to go. +**A third joined in Phase 15, also on an envelope:** + +- **`detail` on an action's SUCCESS envelope** (`EVENTS.md` §F). An optional object a module may + answer with, carried to the run log as a `step.detail` line and **never interpreted by core** — + nothing reads a key out of it in the dispatcher, the runner or the browser. It exists because a + module knows things about its own verb core cannot compute and had no other way to say them: + `uo.item.grant` reaches the players a run's participation ledger holds, and *which of them missed + out* was reported nowhere at all. On both success shapes, like `resources` and `participants`, + because `await: 'human'` is a success and a cue's confirm finishes the step without a second + dispatch. + + Objects only, 4KB of serialised JSON, dropped rather than truncated, and **anything wrong with it + is dropped and logged rather than failing the step** — a step that did what it was asked must not + be re-run because its module's commentary was malformed, which would be a world write repeated for + a log line. It is additive and optional: a module that never answers one is behaving exactly as + before. **Found by writing the integration kit's chapter 5** (`EVENTS_PLAN.md` Phase 15), whose + template made the same mistake `module-uo` had — see §F. + **1.9.0 — a module may ship its own message bodies and rules: `api.registerEngagementSeeds(...)`** (`website/ENGAGEMENT.md` Phase 11b, decision 7). One addition and no removal, so minor; a module written against 1.8.0 keeps working and simply seeds nothing. @@ -1135,6 +1153,13 @@ 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's `detail` is carried and never read.** An optional object on either success shape, + bounded at the dispatcher and written to the run log verbatim beside the action id. Core reads no + key out of it — a switch on known keys anywhere in core would be core learning one module's + vocabulary, which is the thing this whole contract exists to prevent. It is the answer to *"what + actually happened"* for a verb whose answer is neither a resource nor a participant, and before + Phase 15 there was no such answer: `EVENTS.md` §H named the member, `classify()` had never read + one, and a module that used it wrote into nothing. - **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