docs(events): the Event System record — the cutover step 16b missed (edgemain) #232

Merged
whitlocktech merged 46 commits from edge into main 2026-09-10 02:55:11 +00:00
Member

What this is

The seventh step of the Phase 16b cutover, found missing while starting 16c.

16b cut six repositories over — link, servuo-plugins, website, Module-uo, Android-app,
Integration-kit — and every one of them shows 0 commits on edge that are not on main today.
This repository shows 46. Step 6 of the cutover (#231) landed the record on edge rather than
cutting edge over, so the branch that carries the whole workstream's documentation never reached
main.

The result is that docs main currently opens website/EVENTS.md with:

Status: Investigation and architecture proposal, revision 5. No code written.
Read against: the working trees on 2026-09-01 · MODULE_API_VERSION 1.9.0 · sidecar protocol 5.

…while main in six repositories ships the engine, on protocol 7 and MODULE_API 1.10.0.

How it was found

Phase 16crunicgateway.com and .profile — is the last leg of the events plan. The site's
checkReference.mjs asserts that every canonical document it names still exists in docs, read from
main over the API. Adding the current protocol spec to that list failed:

✗ canonical doc link/v7.md

link/v6.md and link/v7.md exist only on edge. Every canonical-document link the site publishes
points at docs/src/branch/main/…, so 16c cannot cite the protocol of record, the built EVENTS.md,
or MODULE_API.md 1.10.0 until this lands.

What it moves

Nothing new is written here — this is the merge, unchanged, of work already reviewed across #216#231.

File
link/v6.md new — protocol 6: idempotencyKey on inbound commands, champ.boss.killed
link/v7.md new — protocol 7: the world verbs, leases, participation, the item catalog
website/EVENTS.md +1341 — the design of record as built
website/EVENTS_PLAN.md +1739 — seventeen phases, each with its record
website/MODULE_API.md +367 — the events half of the contract, 1.10.0
website/BACKEND_DESIGN.md +210 — the events schema, routes and security contract
link/INTEGRATION.md, link/PLAN.md, link/ADMIN_CONTROLS.md the wire protocol and the write plane
android/PLAN.md +179 — the app's events screens
website/ENGAGEMENT.md, README.md the event. triggers, and the index

The merge is clean. Both PROJECT_TREE.md files are untouched by it: main's automated syncs
(#229, #230) are newer than anything on edge and edge never edited them, so git keeps main's
side and the sync workflow stays authoritative.

edge stays standing

Per 16b's own decision, in this repository as in the other six. It is simply behind main by the four
generated tree-sync commits after this merges; the next workstream cuts a fresh branch from main.

Verification

  • git rev-list --count origin/main..origin/edge → 46 here, 0 in all six cutover repositories
  • git merge --no-commit --no-ff origin/edge onto main"Automatic merge went well", no conflicts, no PROJECT_TREE.md in the diff
  • runicgateway.com's checkReference.mjs goes from ✗ canonical doc link/v7.md to green once this is on main

AI-assisted contribution. Drafted with Claude Code (Opus 5).

🤖 Generated with Claude Code

https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4

## What this is The **seventh step of the Phase 16b cutover**, found missing while starting 16c. 16b cut six repositories over — `link`, `servuo-plugins`, `website`, `Module-uo`, `Android-app`, `Integration-kit` — and every one of them shows **0 commits on `edge` that are not on `main`** today. This repository shows **46**. Step 6 of the cutover (`#231`) landed the record *on `edge`* rather than cutting `edge` over, so the branch that carries the whole workstream's documentation never reached `main`. The result is that `docs` `main` currently opens `website/EVENTS.md` with: > **Status:** Investigation and architecture proposal, revision 5. **No code written.** > **Read against:** the working trees on 2026-09-01 · `MODULE_API_VERSION` 1.9.0 · sidecar protocol 5. …while `main` in six repositories ships the engine, on **protocol 7** and **MODULE_API 1.10.0**. ## How it was found Phase **16c** — `runicgateway.com` and `.profile` — is the last leg of the events plan. The site's `checkReference.mjs` asserts that every canonical document it names still exists in `docs`, read from `main` over the API. Adding the current protocol spec to that list failed: ``` ✗ canonical doc link/v7.md ``` `link/v6.md` and `link/v7.md` exist only on `edge`. Every canonical-document link the site publishes points at `docs/src/branch/main/…`, so 16c cannot cite the protocol of record, the built `EVENTS.md`, or `MODULE_API.md` 1.10.0 until this lands. ## What it moves Nothing new is written here — this is the merge, unchanged, of work already reviewed across `#216`–`#231`. | File | | |---|---| | `link/v6.md` | new — protocol 6: `idempotencyKey` on inbound commands, `champ.boss.killed` | | `link/v7.md` | new — protocol 7: the world verbs, leases, participation, the item catalog | | `website/EVENTS.md` | +1341 — the design of record as built | | `website/EVENTS_PLAN.md` | +1739 — seventeen phases, each with its record | | `website/MODULE_API.md` | +367 — the events half of the contract, 1.10.0 | | `website/BACKEND_DESIGN.md` | +210 — the events schema, routes and security contract | | `link/INTEGRATION.md`, `link/PLAN.md`, `link/ADMIN_CONTROLS.md` | the wire protocol and the write plane | | `android/PLAN.md` | +179 — the app's events screens | | `website/ENGAGEMENT.md`, `README.md` | the `event.` triggers, and the index | The merge is clean. Both `PROJECT_TREE.md` files are untouched by it: `main`'s automated syncs (`#229`, `#230`) are newer than anything on `edge` and `edge` never edited them, so git keeps `main`'s side and the sync workflow stays authoritative. ## `edge` stays standing Per 16b's own decision, in this repository as in the other six. It is simply behind `main` by the four generated tree-sync commits after this merges; the next workstream cuts a fresh branch from `main`. ## Verification - `git rev-list --count origin/main..origin/edge` → 46 here, 0 in all six cutover repositories - `git merge --no-commit --no-ff origin/edge` onto `main` → *"Automatic merge went well"*, no conflicts, no `PROJECT_TREE.md` in the diff - `runicgateway.com`'s `checkReference.mjs` goes from `✗ canonical doc link/v7.md` to green once this is on `main` --- **AI-assisted contribution.** Drafted with Claude Code (Opus 5). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
wtclaude added 46 commits 2026-09-10 02:52:16 +00:00
Closes the second half of EVENTS_PLAN.md Phase 0. All of EVENTS.md §N1–N11 were
answered by the org lead on 2026-09-01, before any code, which lifts the ⚠ gate
from P11 and P12 and unblocks P1.

Nine went the way §N recommended. Two did not, and each changes a phase:

- N2 — publishing a version and starting a run are `admin` only; a moderator
  keeps live control of a run already in flight (cancel, abort) and nothing
  more. Start and stop are deliberately not the same gate: starting commits the
  deployment to everything a definition contains, unattended, while cancelling
  is incident response. §K's table and the API surface table are updated, and
  P3 and P6 carry it.
- N4 — this deployment is single-instance and not planned to change, so P2 does
  not build the `--scale app=2` test. Every claim path in §E is built exactly as
  specified regardless — the unique index and the CAS equally protect a tick
  that overruns into the next one, and the lease and its reclaim recover a step
  whose process died mid-dispatch. §E records what to build first if this
  deployment is ever scaled.

N1 was taken in full, item grants included, which is a reversal of two separate
lines in ADMIN_CONTROLS.md §8 rather than one. §8's cut list names "item/gold
grants" explicitly and separately from world building, so the new §8a amendment
says both out loud rather than letting the grant ride along on the creation
reopening. §8's bullet is left standing verbatim — it is the decision that was
actually taken — with a pointer to the amendment, and §1's thesis paragraph gets
the same pointer. Loot-table changes and arbitrary [set / [get / [add stay
permanently excluded, along with the rest of the Tier-H moderation verbs.

N3 gives ENGAGEMENT.md §7.1 Q6's "no evaluator yet" an owner: the Event System
runner becomes the `kind: 'scheduled'` evaluator, built in that workstream's P2.
Nothing in the engagement contract changes.

One consequence was derived rather than decided and is flagged for review:
POST /admin/events/:id/verify moves to `admin, editor` rather than staying with
`start`. A dry run dispatches nothing, and the author who wrote the definition
is who should price it against the caps before asking an admin to publish.

Verified: every relative link and anchor in the four touched files resolves,
including the renamed §N anchor (#n--decisions) and the new #8a. The docs repo
has no CI workflow, so the plan's "docs CI link check" was run by hand.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #208
The docs half of RunicGateway/website#<n>. Three files.

**BACKEND_DESIGN.md** gains the six event tables, column by column, and the
eleven admin routes. Written where the other table groups are, in the same
shape, because the argument for a column belongs beside the column.

**EVENTS.md** records four things the build settled that §D and §F had left
open:

  - `event_definitions.spec`, the working copy. §D's column list does not name
    one because §D describes what a PUBLISHED event is made of — but "editing a
    draft is free; no version exists yet" means the draft has to live somewhere,
    and it cannot be an `event_versions` row: that table is immutable and a run
    pins one, so a mutable unpublished row in it would be exactly what
    versioning exists to prevent.
  - A param's `example` is REQUIRED, on optional params too, the same rule
    `registerEventTriggers` makes of a variable's example and for the same
    reason: it is the authoring form's placeholder, one word at declaration
    time and unreconstructable afterwards.
  - The authoring side of dormancy. §F said what happens at DISPATCH; the save
    path draws the same line one step earlier, in the shape `engagement_rules`
    established — a saved step may keep an unregistered action, a new step may
    not add one, and a dormant step blocks the publish rather than the save.
  - Publish re-validates against the registries as they stand at that moment,
    not from the save that wrote the spec.

Plus two routes the § API surface table did not name — `GET /admin/events/:id`
(the list serves a summary; the editor needs the tree) and `GET
/admin/events/series` (a form cannot offer a value it cannot enumerate) — and a
note stating which of that table's rows Phase 1 deliberately did not build.

**EVENTS_PLAN.md** marks Phase 1 complete, names those four settlements, and
states the two deliberate absences so a reviewer does not read them as gaps:
the live run controls are not stubbed, and core's three `perform()` bodies
answer `{ ok: false }` rather than `{ ok: true }` — `ok: true` on an action
that did nothing is a recorded world change that did not occur.

`api-route-inventory.json` is NOT resynced here. It has been stale since
engagement Phase 2 and is 47 routes behind; catching it up in this PR would
bury a 13-route change under an unrelated 47.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #209
The docs half of Event System Phase 2. Pairs with `RunicGateway/website`
`feat/events-phase-2`.

EVENTS.md gains what the runner settled: the parked step (`running` with a NULL
lease), the two success-envelope members `await: 'human'` and `holdFor`, the
answer for a run whose concurrency key is held, `n` in §L's `retry(n)`, the rule
that all three `on_failure` dispositions write the step `failed`, and the health
transition on the first retry rather than the eventual failure.

EVENTS_PLAN.md marks Phase 2 complete and records the four org-lead decisions
and the three things the build settled on its own.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #210
EVENTS.md: the six controls that exist and the two that do not, and why;
retry as one control with its two guards; what a cancel does to a parked cue;
the run status re-read between steps; the two derived response fields.

EVENTS_PLAN.md: Phase 3 complete, the four org-lead decisions, and the three
things the build settled — including the mid-batch pause defect and the retry
guard that was reading the wrong end of the phase.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
Reviewed-on: #211
EVENTS.md: the materialise leg as two halves and why the horizon is what makes
the missed sweep meaningful; the recurrence shapes as built, incl. the two DST
rules and why nth has no fifth; publishing as the schedule switch and the
re-pin; the calendar's run-vs-projection distinction and the
reader's-zone/event's-zone split; the four new routes; P1-P4 ticked.

EVENTS_PLAN.md: Phase 4 marked complete with the five org-lead decisions, the
three the build settled, the test-harness defect the phase introduced and
fixed, and the verified numbers.

Website: RunicGateway/website#PENDING

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #212
EVENTS.md gains the eleventh table, a new SS-E subsection on advance conditions
and the two writers a gate has, the diagnosis panel in SS-I, the "a condition
never fires" row in SS-L, and the observability rows for the gate table and its
three new log kinds. The `advance` route leaves the not-built list; the catalog
route now serves triggers.

The SS-D count said "nine" over a list of ten from the revision that added
`event_series`; with the gate table it is eleven, and it now says so.

EVENTS_PLAN.md marks Phase 5 complete in the shape Phases 0-4 use: the four
org-lead decisions, the three things the build settled, the MariaDB
left-to-right SET evaluation defect that only a real database found, the live
walk, and the three defects that walk turned up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
Reviewed-on: #213
EVENTS.md

§D — the two tables move from "arrive with a later phase" to built, with the
two facts that are not obvious from the column list: a missing settings row is
the risk-class default rather than "disabled", and a NULL cap is uncapped and
still a row, so a MISSING row keeps its one meaning. event_versions gains
verified_at/verified_by, and the note on why two mutable columns do not break
an immutable table.

§E — a new subsection, "Enablement and caps, in front of every dispatch": where
the check sits and why, the retry that does not pay twice (and the failure that
gets no refund), the partial spend that is unwound, the budget seeded from every
phase at creation, the tightest cap, and the one place a demoted user's access
deliberately is not re-checked.

§K — the amendment. "Nothing above notify" becomes "nothing that changes the
world", with the reason: read literally it shipped core.wait disabled. The same
line is the role floor. Plus mayInvoke as built — why user and run may be null,
why the cap check can write, why the role floor is in the model rather than in
middleware, and the example-pricing stand-in until registerEventBudgets lands.

§I, §L, § API surface and § Observability — the switchboard and the dry run
marked built; the two refusal rows and the held-occurrence row; the three new
log kinds and the run's fourth derived field; why the whole-plan cost check is
the one finding no other path makes.

EVENTS_PLAN.md — Phase 6 complete in the shape Phases 0–5 use: the four org-lead
decisions, the two derived calls flagged for review, the three things the build
settled, the one thing this phase deliberately cannot demonstrate with a module
(registerEventActions is Phase 7's seam), the two defects the live walk found,
the verified numbers and the walk itself.

Checked by hand — docs has no CI: every anchor resolves, and every route,
column, env var and file path named here exists on website#XXX. Diff is
151/23 and 118/2 with no CRLF artefact, verified against the real content diff
rather than --numstat alone.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
Reviewed-on: #214
Code: RunicGateway/website#189.

MODULE_API.md gains 1.10.0 in three places: the number itself, the entry in
Part 1 with the four call shapes and the six rules that come with them, and
the four names in 2.4's call list with the contract-rather-than-
implementation notes beside them.

EVENTS.md F is marked built, with a new subsection recording what the build
settled -- the open unit vocabulary, the undeclared-dimension refusal and
why it has its own code, why example-pricing survives the arrival of a
budget registry, why restore cannot be read, and why an option source that
refuses answers 200. K's Phase 7 note becomes what shipped. The API surface
table's option-source row is filled in, and the absent-routes list is down
to cleanup.

EVENTS_PLAN.md: Phase 7 complete, in the shape phases 0-6 use -- the four
org-lead decisions, the three things the build settled, the throwaway-module
proof, the React defect only the browser could find, and the route the docs
caught the code building one segment shallower than this file specifies.

Checked by hand (docs has no CI): every anchor resolves, and every route,
identifier and file path named here exists on website#189.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
Reviewed-on: #215
The docs half of Event System Phase 8. Code: RunicGateway/website#NNN.

EVENTS.md

Section D's ledger row is corrected in two places and section L gains a new
subsection recording what the build settled:

- Rule 1 needed a mechanism, because a spawn's ref does not exist until the
  module answers. What goes in before the dispatch is a PLACEHOLDER keyed by the
  step's idempotency key, and that is why revert() takes the key at all.
- A LEASE does not use it and gets rule 1 in a stronger form: its target is the
  lease id the step already names, so `core.lease` writes the real row first --
  the only moment the two-events-one-target refusal can happen before the world
  has been written to.
- The unique key is held by three statuses and released by three (amended
  2026-09-03). "Among non-reverted rows" was written before the six statuses had
  their meanings; taken literally it makes `drifted` and `orphaned` hold a target
  for ever, so one bad night would disable a lease permanently with no control
  able to clear it.
- MariaDB has no partial index, so the encoding is a STORED generated column
  reading `status` ALONE -- TEAMS.md 2.5's correction, because MariaDB refuses
  ON DELETE SET NULL on a foreign key whose column is a base column of one.
- Cleanup is one sweep, not synthetic step rows, and it runs from one place.
- What that sweep SELECTS cost two live-walk defects in opposite directions, and
  the pair is the finding worth keeping: deriving "is there anything to do" from
  a summary column instead of from the rows stranded a lease outright, and then
  made the retry bound mean one attempt. Both are recorded with the reasoning.

Section F gains the lease's verb and the reconcile bullet; section I's "cancel
takes { reason } and not { cleanup }" becomes what shipped; section K's role
table puts re-running cleanup in the admin-only row; section L's cancel and
cleanup-fails rows are updated; Observability names the six new log kinds; and
the API surface table's absent-routes list is now empty.

EVENTS_PLAN.md

Phase 8 marked complete in the shape Phases 0-7 use: the four org-lead
decisions, the four things the build settled, the verified numbers, and the live
walk -- including the three defects only it could find and the two properties
that needed the process to die.

MODULE_API.md

1.10.0 gains `reconcile()` and `ctx.events.reconcile()` IN PLACE rather than a
new version. A protocol owes a bump once it has landed on `main`; while it is on
`edge` it is amended in place, which is the rule the Teams workstream arrived
at, applied to a module API for the first time. The rule list grows from six to
ten: the lease's verb is core's, `until` goes down the wire, revert is
idempotent and reverting something that does not exist is a success, revert is
also called with the key and an empty list, and reconcile is optional where
revert is required.

Checked by hand

`docs` has no CI, so: every anchor in EVENTS.md resolves (checked by generating
the heading slugs and diffing), and every route, identifier, env var and file
path named here exists on the website branch -- `core.lease`,
`core.options.leases`, `ctx.events.reconcile`, `EVENT_REVERT_MAX_ATTEMPTS`,
`event_run_resources`, `live_marker`, `uq_evres_target`, the six log kinds, and
the route in both routes.manifest.json and the OpenAPI spec. Diffs are 144/21,
139/16 and 67/9 with NO CRLF artefact, verified by comparing --numstat against
the real content diff rather than trusting it.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #216
`EVENTS_PLAN.md` Phase 9 as built, plus the two findings that are contract rather
than facts about UO.

- **MODULE_API.md §2.4: `budgetMs` must exceed the timeout of whatever the action
  talks to.** The dispatcher classifies a budget timeout as `retry`
  unconditionally and cannot ask the action, so an action whose own client gives
  up after core's deadline never classifies its own failure and `retry: false` is
  unreachable. The default (10s) is shorter than module-uo's sidecar client (12s),
  the wrong way round.
- **EVENTS.md §F gains "What Phase 9 settled, as the contract's first module
  registrant"**: the rule above; that `on_failure` is not a retry policy; that
  `reconcile()` can answer from a stamp rather than a query; and that a module's
  reconcile trigger must fire on a live signal, never a replay.

The plan block records the four org-lead decisions, the live walk, the three
defects it found — two of them data loss — and the Phase 8 defect in core it
reproduced and deliberately did not fix here.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #217
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)
Reviewed-on: #218
`link/v6.md` is the spec of record. It is the first bump that adds a promise
rather than data: a command carrying an `idempotencyKey` is executed at most once,
and a repeat is answered with the original reply.

Also in this PR:

- `INTEGRATION.md` — §2 gains v6, the write plane gains a "retrying a command
  safely" section with the four rules a caller needs, §7 gains 425 and the
  paragraph on why it is not 409, and the event catalog gains `champ.boss.killed`.
- `PLAN.md` — §5.5 records that a per-KILL damage table needs no override even
  though a per-HIT event does, which is the observation the new kind rests on;
  §7 documents the key on the inbound frames.
- `EVENTS.md` — §A row 7 and two §G capability rows close, and the failure table's
  "an action succeeds but the ack is lost" loses its "does not exist today".
- `EVENTS_PLAN.md` — Phase 11 splits into 11a and 11b, and `installer` leaves the
  phase: PLAN.md §7.4 made sure no protocol version is hardcoded there, so the
  bundle gate is version-agnostic and needed no change for 5 either.

The live walk's defect is written up in v6.md §6.1 because the naive check
confirms it: an active ChampionSpawn registers an unnamed region over its own
spawn area, so the innermost region containing a champion boss is guaranteed to
have no name — and region registration is deferred, so a lookup at spawn time
answers "Britain" while one at the kill does not.

CRLF preserved throughout; links and anchors checked by hand, this repo has no CI.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #219
Six files. `v6.md` gains sections 7-10: one version with two halves, the lease
plane, the participation ledger, and 11b's verification.

Three corrections to what the plan and the design of record assumed.

Phase 11b is FIVE repos, not four, and in the opposite direction from 11a's
correction: a lease's ledger row had no reconcile path anywhere, because the step
that made it names `core.lease` and that is core's own action. `website` joins.

EVENTS.md's §D frames the 258 `Config.Get` call sites as splitting into two
patterns. Measured on 57.4: of the 158 non-Bridge sites in `Scripts/`, roughly
eight are read live. The allowlist is not a curated subset of a large pool, it is
nearly the whole of what exists. And `Config.Set` has exactly one caller in the
entire tree, so on a stock shard a GM cannot drift a configuration lease even
deliberately -- which is why proving `drifted` needed a scaffolding verb.

§G's "participation attribution is now the largest piece of new UO work" closes,
and the live-config-lease row goes to built-with-one-key.

§10.1 records the defect the phase's own deferral found in 11a's shipped code:
`bridge.busy` answered 200 instead of 425 because the frame carried two `kind`
fields and parsers take the last. Unreachable in 11a by construction; produced on
the first collision here.

Co-Authored-By: Claude <noreply@anthropic.com>
Reviewed-on: #220
`link/v7.md` is the spec of record: one command family for five verbs, the
persisted ownership registry and why it is forced rather than chosen, the oracle
that reuses ServUO's own dialogue vocabulary without its command-scripting
field, and the decoration index.

`EVENTS_PLAN.md` splits Phase 12 into 12a and 12b (org lead, 2026-09-07), on the
line section G already draws between what an event OWNS and what it BORROWS. Ten
verbs, four repos and a protocol bump is one review and one walk too many, and
the halves prove different things. One protocol version across both, the 11a/11b
shape.

Three corrections the survey forced on `EVENTS.md`:

The config lease catalog does NOT grow in 12b, and the plan's promise of "the
rest of the allowlist" was written before anyone counted. Measured on ServUO
57.4: 156 non-Bridge `Config.Get` call sites; 82 sit outside a field
declaration, but all but four of those are inside a `Configure()` or a static
constructor and are cached at boot exactly as the field initialisers are. The
live, event-useful reads are `PlayerCaps.SkillCap` (11b shipped it) and vendor
bribe decay. So 12b's lease work is object-property leases.

Section G calls the seasonal toggle "a nine-value enum". `EventStatus` has THREE
values; it is `EventType` that has nine entries. The verb is a three-state
toggle over nine named events, which is a different form to author and to cap.

Section G costed the reopening at one protocol bump. It is two: the idempotency
guarantee had to land before the verbs that depend on it, which is v6's own
argument.

`README.md` gains rows for v6 and v7. v6's was missing -- the index has been one
protocol behind since 11a.

Refs: EVENTS_PLAN.md Phase 12a

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #221
`link/v7.md` gains §11-§14: the two targeted lease planes, the two one-shots, the
routes, and what the build found in already-merged code. `EVENTS.md` §G's five
part-b rows are marked built, three of them carrying a correction. `MODULE_API.md`
records the three contract members 12b amends into 1.10.0. `EVENTS_PLAN.md` has
Phase 12b as built.

THE DESIGN POINT, WRITTEN DOWN

A borrowed value whose home is the world save does not come back on a restart the
way a config value does. 11b's fail-safe -- a lease that never reaches disk makes
a restart a free restore -- depends entirely on the leased value being
memory-only too, and for a spawner property or a seasonal status it is not: a
restart preserves the CHANGE and destroys only the timer that would have undone
it. So those two planes' holds are persisted and their deadlines re-armed, and
the config plane's still are not. The same argument, applied where its premise is
false.

FIVE CORRECTIONS TO EVENTS.md

- `Spawner.Amount` does not exist. The property is `MaxCount`, and
  `MinDelay`/`MaxDelay` are TimeSpans, so the wire carries seconds.
- The seasonal toggle is not "small and safe". Safe, yes -- ServUO does it to
  itself from a staff gump -- but `OnStatusChange()` generates or removes world
  content for six of the eight permitted types.
- It is a THREE-value enum over nine named events, not a nine-value enum.
  (Caught in 12a's survey; the row is corrected here now it is built.)
- `TreasuresOfTokuno` is excluded, because `IsActive()` reads its own `DropEra`
  rather than `Status`. A lease on it applies cleanly, reads back, restores
  cleanly and changes nothing -- §N10's "capability that lies", and the one
  instance no runtime probe can catch.
- The grant row said failure aborts rather than retries. Protocol 6 changed that:
  an idempotency key means a repeat is answered by the original reply, so a
  retried grant cannot be one winner receiving two.

And the config-lease row is closed at one key. Counted on ServUO 57.4: 156
non-Bridge `Config.Get` call sites, 82 outside a field declaration, all but four
of those inside a `Configure()` or static constructor and cached at boot anyway.
The self-check ships regardless -- it exists for the operator whose OWN scripts
read config live.

MODULE_API 1.10.0, AMENDED IN PLACE

`target` on a lease declaration, `values` on a string lease, and `searchable` +
`{ q }` on option sources. Amended rather than bumped for the reason every phase
since P10 has: 1.10.0 has never reached `main`, so no deployment can tell the
difference, and the cutover is what publishes the whole of it.

The `target` entry records why this extends `core.lease` rather than giving the
module a lease verb of its own: §F settled that in Phase 8, and half its
objection no longer holds (the two-events-one-target refusal comes from the
ledger's unique index whichever verb reserves the row) while the other half --
`maxDurationMs` re-implemented per module -- still does.

§14 records the protocol-pin defect 11a and 12a both shipped, and why the test
that guards it passed anyway: it asserts the three declarations agree with each
other, which all three being equally stale satisfies.

CHECKS

`docs` has no CI. Every relative link in the new sections was resolved by hand;
the files are CRLF in the working tree and each diff is content-sized
(`--numstat` matches the real change), so nothing carries the `\r\r\n` full-file
rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #222
EVENTS.md I gains what the phase settled -- the rule by which a form gives way to
the JSON box, why the condition builder is the engagement one rather than a
second one shaped like it, and the live cap meter as a fifth affordance beside
the dry run it must not be mistaken for. The screens table's two unfinished rows
are marked built; rehearsal is marked reachable, which it had not been since
Phase 10 shipped it.

The API surface table gains POST /admin/events/price, and a paragraph on why it
is a route rather than arithmetic in the browser: cost() is a server function of
params, and the dry run both dispatches and records, neither of which belongs
behind a keystroke.

EVENTS_PLAN.md carries Phase 13 as built, with the four decisions, the two
defects it closes in already-merged code, and the six things the build settled.

BACKEND_DESIGN.md's /events/* rows are Phase 1's surface and have not moved
since; a note says so and points at EVENTS.md as canonical rather than
re-listing eleven routes that file already owns.

Checks: docs has no CI. Every relative link and in-page anchor in the new
sections was resolved by hand; the files are CRLF in the working tree and each
diff is content-sized (8/0, 64/5, 67/0), so none carries the \r\r\n full-file
rewrite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #223
EVENTS.md gains what the phase settled -- a new §I section on the public
surface, the four API surface rows filled in, `listed` in §D, and the §J
rows for player profiles and mobile. EVENTS_PLAN.md records the 14a/14b
split and 14a as built. BACKEND_DESIGN.md points at the two tiers the
Phase 14a reads live on. MODULE_API.md records core's own capability list
beside a module's -- the same word, a separate list, and why.

Two things this phase corrected in the document rather than in code.

"Venue" was never a field. §I's screens table and the API surface table had
both described one since the first revision; there has never been a column,
a spec key, an input on Phase 13's form, or a string anywhere in either
repo. Rather than add a field on the way past to a public page, both
descriptions dropped it.

And the six public triggers' missing url variable, which this document has
carried as a promise since Phase 10, is now kept: `eventUrl`, carrying
`?run=`, arriving with the page it points at.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
EVENTS.md §I records that participation history is mounted twice and why --
the third screen to need the notificationPaths.js mapping, for the reason
the first two did -- and the three defects the live walk found with both
suites green on either side of it. EVENTS_PLAN.md says the same in one
paragraph under Phase 14a's verify line.

Two of the three were one mistake in two files: a split reading a STATUS
where it should read a clock.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #224
Events Phase 14b, docs half. `docs/android/PLAN.md` gains **M13** — written before
the code, as the plan requires — and `EVENTS_PLAN.md` and `EVENTS.md` record 14b
as built.

M13 records the six org-lead decisions, the two defects the milestone fixes, and
the three the emulator walk found.

The prerequisite fix is the larger half of the milestone: the app could not tell a
module that is not installed from a lookup that failed, so on a site with no `uo`
module every shard row rendered and every one of them 404'd. **Absence of an
answer is not an answer of absence** — a successful module list that omits the
string hides the rows, a failed read keeps the last answer the host gave, and a
host that has never answered leaves the gate open. Capability and feature are two
gates that compose and answer different questions.

A second, older defect is recorded and fixed: the website path → route table had
been wrong since the module-system cutover on 2026-08-12, because core's `NAV` is
eight rows rather than sixteen and the nine shard rows moved to `/uo/*`. Two tests
in the app's suite had been passing vacuously since that day.

§5 gains the capability gate beside the feature gate, with the two failure
directions spelled out; §6.1 gains the three public event reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #225
§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
Reviewed-on: #226
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
Reviewed-on: #227
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
Reviewed-on: #228
`EVENTS_PLAN.md` gains the 16b record: the six steps and why core lands before
the module, the four decisions taken, the releases cut, and the re-verify against
artefacts an operator would actually download.

Three things in it are worth more than the chronology.

A job's log IS readable on this Gitea, through the web route rather than the
API. Every earlier phase diagnosed CI by reproducing jobs locally on the belief
that logs were unreachable; reading one turned four red jobs into four known
causes in about ten minutes. Three were the ten-minute Cloudflare outage in the
middle of the window and a runner that could not resolve sh.rustup.rs -- and
because neither release pushed its tag before dying, the orphaned-tag failure
mode did not occur and a plain workflow_dispatch recovered both.

The seventh defect of this phase: `server-tests` had been red on every events PR
since Phase 10, always the same single test, and the workstream merged over it
eight times. `announce.js` asked for `hour12: true`, which is not the same
request as a 12-hour clock -- for a locale whose default cycle is h23, Node 20
resolves it to h11 and midnight renders "0:00 am", while Node 22+ resolves it to
h12. Same ICU on both sides, so it is V8's ECMA-402 behaviour and not locale
data; the image ships node:20-alpine and a dev machine is newer, so it rendered
correctly for everyone who reviewed it and wrongly for every real recipient. The
rule is now written down: `hour12` is a request about a locale's preference,
`hourCycle` is a request about the clock -- ask for the clock.

And the re-verify itself: the released installer resolved bundle 2026.09.10,
verified both checksums, did a first install into a stock 57.4 tree, the overlay
compiled 0/0 -- which no release had ever been asked to prove -- the shard came
up with the events plane on and dialed the sidecar, the whole protocol-7 event
plane answered, and an event published on released `main` ran to `completed`
with its results published and its finished run visible on /site/events two
minutes later. That last line is 16a's calendar fix holding on `main`.

The module's own install through core's https installer is named as the one leg
still owed: it cannot run until Module-uo#34 cuts the release it fetches.

Diff is 102/1, content only -- no CRLF rewrite (checked against --numstat).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
The leg the first commit named as still owed is walked. Module-uo#34 cut v1.2.1,
so the module could arrive the way an operator's does: core fetched the release
MANIFEST over https from the allowlisted host, verified its sha256, and mounted
it; the four values the installer printed then produced `status: connected`,
`pluginConnected: true`, `protocol: 7`. Released core, released module, released
sidecar, released overlay.

That rig confirmed both Phase 16a fixes in the shipped artefacts rather than in a
working tree -- the atlas imports off a stock tree, and a world verb's teardown
leaves the shard answering `owned: [], pruned: 0`, which is the check the no-op
teardown hid behind.

It also found two more defects, both in the released bundle (Module-uo#35). The
aggregator discarded the UniqueId, so `uo.options.spawners` was empty and no
Phase 12b property lease was authorable at all -- while `PARSER_VERSION = 4`'s
own note said a point keeps that field and named Phase 12b as the reason. And a
landmark option value named 23 places at once: 558 landmarks under 320 distinct
`facet/name`, resolved by `.find()`, so an author who picked "Entrance - Destard"
got Blighted Grove with a successful run and no warning.

Both are recorded as one class, because that is the useful part: an option source
that answers empty, or answers with a value that does not identify one thing,
disables a feature silently. Nothing errors; the form simply cannot express the
thing, and a test that checks the parser, or the query, or the column in
isolation passes throughout.

Diff is 45/6, content only -- no CRLF rewrite (checked against --numstat).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
Reviewed-on: #231
whitlocktech merged commit 2db5737f22 into main 2026-09-10 02:55:11 +00:00
Sign in to join this conversation.
No description provided.