2 Commits

Author SHA1 Message Date
c26d06a782 docs(events): detail is a real envelope member now
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
2026-09-08 18:50:04 -05:00
5d48e7f256 docs(events): what the integration kit's fifth chapter settled (Phase 15)
§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
2026-09-08 18:11:39 -05:00
3 changed files with 97 additions and 1 deletions

View File

@@ -1110,6 +1110,67 @@ a second module's author will get wrong are the envelope's failure default, the
passthrough, recording a resource *before* confirming it, and under-declaring `cost`. All four are
one paragraph each and all four are invisible until an outage.
> **Built in Phase 15** (`Integration-kit#10`), held unmerged until the cutover by the same mechanism
> Teams phase 11 was: `checkCoreApi` asserts EQUALITY between `template/module.json`'s `coreApi` and
> the `MODULE_API_VERSION` of the core `ci/core-ref.json` pins, so a template declaring `^1.10.0`
> against a `main` still on 1.9.0 is red on purpose from the day the branch opens. The pin move is
> the kit's leg of P16.
>
> **It is three chapters, not one.** 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 in it told anyone 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 (request/reply correlation, the at-most-once store belonging where the
> state is, and a deadline sent as a duration rather than an absolute time) and chapter 4 gains *"A
> command that changes the world runs at most once"* (the key store persisted in the world save, the
> ownership registry, and the expiry this side arms and re-arms at load). Both open by saying they
> are skippable until you want chapter 5.
>
> **The template ships one of each of the four declarations**, and the phase's value came from
> running them through core's real registry and real `events/dispatch.js` classifier at `edge` rather
> than from the prose. That found two things:
>
> - **An idempotency key belongs on a command and never on a question.** A read that carries one is
> answered by an at-most-once store with the FIRST read's reply, forever — so a 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. The narrower rule that falls
> out is worth stating with the others: a key is for a write whose repetition would be a second
> EFFECT, and a write that merely SETS a value to X is idempotent by its own nature.
> - **§H names a `detail` member on an envelope and core has never read one** — see the correction
> below.
> **`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.
---
## G — UO implementation plan: the gap list
@@ -1183,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 `{ ok: true, detail: 'resource no longer exists' }` — "gone, and that is fine" is a successful revert. 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

View File

@@ -1796,6 +1796,16 @@ confirming it, and under-declaring `cost`.
**Like Teams Phase 11, this cannot merge until the cutover exists** — the kit is pinned to a `main`
sha, and the contract it teaches is not on `main` until then.
> **Built** as `Integration-kit#10` + `docs#226`, red on `checkCoreApi` by design. **It is three
> chapters, not one, and the template gains code.** The book taught a read-only data path end to end
> and never told anyone to build a command path, so chapters 3 and 4 gain one section each (§2a; *"A
> command that changes the world runs at most once"*), both skippable until you want chapter 5. The
> template ships one budget, one option source, one lease and one ledgering action plus
> `server/sidecarClient.js` — named for the filename `noGameConnection.test.js` already anticipated,
> with a real timeout, a real key passthrough and a simulated transport in one replaceable function.
> Running those through core's REAL registry and dispatcher at `edge` is what found the two defects
> §F now records; the prose found neither.
---
### Phase 16 — Acceptance walk and cutover

View File

@@ -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