docs(events): the acceptance walk, and the three contracts it moved (Phase 16a)

Phase 16 is split into 16a (the walk), 16b (the cutover) and 16c
(runicgateway.com + .profile), because the phase as written asked for a walk
"against released artefacts" BEFORE the cutover and all three component repos
release on push to `main`. The walk therefore runs against artefacts built from
`edge` the way a release builds them, and 16b re-verifies against the real bundle.

`EVENTS_PLAN.md` gains the 16a record: the rig, all three deliberate failures
passing, the six defects, the one finding withdrawn, and what each fix was
verified against.

Three contracts move, each because the walk proved the built thing did not match
the written one:

**`link/v6.md` — a refusal does not spend its key.** Rule 2 had two cases, throw
and return, and needed a third: a handler that ran to completion and deliberately
refused did nothing, so freezing that refusal as the key's answer made a refusal
that WAITING FIXES impossible to retry past. The section now carries the case
`uo.world.save` found it with, and the rule the release rests on — do not answer
`*.error` after changing the world. `[bridge status` gains `refused=`.

**`website/MODULE_API.md` — `revert`'s `idempotencyKey` identifies a dispatch; it
is not a key to send on the undo.** The paragraph explained what the key is FOR
and never said what it is not, and `module-uo` read it the other way: every
despawn went out under the key its spawn had used, so a store that keys on the key
alone answered the undo with the DO's reply and teardown became a no-op that
reported success.

**`website/EVENTS.md` §I — the public calendar matches a run that OVERLAPS the
window.** The row promised "upcoming, live and recent" and the built route served
only the first, because it read the start instant and a live run has already
started. The default window now reaches back so "recent" has somewhere to live,
and projections are forecast from now rather than into that tail.

Pairs with `website#`, `Module-uo#` and `servuo-plugins#`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-09 08:31:06 -05:00
parent 8cb4cb3e01
commit 6647287037
4 changed files with 96 additions and 6 deletions

View File

@@ -80,10 +80,31 @@ A repeat of a key still in flight is answered **`bridge.busy`**: nothing runs, a
told to come back. It is deliberately not spelled `bridge.busy.error` — nothing is wrong, the work
is happening.
**2. A key that has begun is never released.** Not even when the handler throws. Releasing it would
let a retry re-run a command that may have applied half of itself, which is the exact failure this
file exists to prevent. A handler that throws stores a `bridge.error` reply instead, so the retry
gets a definite answer and the step fails once rather than looping.
**2. A key that has begun is never released — except on a refusal.** Not when the handler throws.
Releasing it would let a retry re-run a command that may have applied half of itself, which is the
exact failure this file exists to prevent. A handler that throws stores a `bridge.error` reply
instead, so the retry gets a definite answer and the step fails once rather than looping.
**A REFUSAL is the third case**, added by the Phase 16 acceptance walk and amending protocol 7 in
place. A handler that ran to completion and answered `*.error` did not do anything — every refusal
on this plane is a guard: a missing `runId`, an unknown item, a cap, a rate limit, a write that
failed and left the value alone. Remembering it froze the answer for ever, so a refusal that
*waiting fixes* could never be retried past. `uo.world.save` is the case that found it: the shard
saves at most every 300 seconds, the module documents that as "the one refusal on this plane that
waiting fixes", and six attempts over four minutes all replayed one frozen sentence — "the last save
was 227 seconds ago" — because the number was the first reply's, not the clock's. A step's key is
one value for the life of the step, so the operator's retry control could not escape it either.
So a refusal releases the key: nothing happened, and the caller may ask again. The refusal is still
**emitted** to the caller, which is what ends that attempt; it is simply not remembered as the key's
answer. A refusal is recognised by its `kind` ending in `.error`, matched on the suffix so a handler
family added later is covered without extending a list. `bridge.error` is excluded deliberately —
that is the reply the shard writes when a handler THREW, which is the case whose key must be kept.
**This puts a rule on handlers, and it is the rule the release rests on: do not answer `*.error`
after changing the world.** Report a partial change in an `ok` reply, as `item.grant` does with
`granted`/`missed` and `world.despawn` with `removed`/`gone`/`refused`. The shard cannot verify
"nothing happened"; it takes the `.error` kind as the claim.
**3. A replay is stamped with the REPEAT's correlation id.** The sidecar's `reqId` is a fresh
per-process counter, so a retry is waiting on an id the first attempt never used. Replaying the
@@ -102,7 +123,7 @@ evicted key's repeat *would* be applied a second time — so an eviction that dr
its TTL prints a console warning naming the count. If the promise is ever actually breached, an
operator reads it here rather than discovering a doubled spawn in the world.
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated=)`.
`[bridge status` reports `idem(keys= seen= replayed= busy= evicted= uncorrelated= refused=)`.
#### 2.1.1 How the reply is captured