docs(events): the Event System record — the cutover step 16b missed (edge → main)
#232
Reference in New Issue
Block a user
No description provided.
Delete Branch "edge"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 onedgethat are not onmaintoday.This repository shows 46. Step 6 of the cutover (
#231) landed the record onedgerather thancutting
edgeover, so the branch that carries the whole workstream's documentation never reachedmain.The result is that
docsmaincurrently openswebsite/EVENTS.mdwith:…while
mainin six repositories ships the engine, on protocol 7 and MODULE_API 1.10.0.How it was found
Phase 16c —
runicgateway.comand.profile— is the last leg of the events plan. The site'scheckReference.mjsasserts that every canonical document it names still exists indocs, read frommainover the API. Adding the current protocol spec to that list failed:link/v6.mdandlink/v7.mdexist only onedge. Every canonical-document link the site publishespoints at
docs/src/branch/main/…, so 16c cannot cite the protocol of record, the builtEVENTS.md,or
MODULE_API.md1.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.link/v6.mdidempotencyKeyon inbound commands,champ.boss.killedlink/v7.mdwebsite/EVENTS.mdwebsite/EVENTS_PLAN.mdwebsite/MODULE_API.mdwebsite/BACKEND_DESIGN.mdlink/INTEGRATION.md,link/PLAN.md,link/ADMIN_CONTROLS.mdandroid/PLAN.mdwebsite/ENGAGEMENT.md,README.mdevent.triggers, and the indexThe merge is clean. Both
PROJECT_TREE.mdfiles are untouched by it:main's automated syncs(
#229,#230) are newer than anything onedgeandedgenever edited them, so git keepsmain'sside and the sync workflow stays authoritative.
edgestays standingPer 16b's own decision, in this repository as in the other six. It is simply behind
mainby the fourgenerated 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 repositoriesgit merge --no-commit --no-ff origin/edgeontomain→ "Automatic merge went well", no conflicts, noPROJECT_TREE.mdin the diffrunicgateway.com'scheckReference.mjsgoes from✗ canonical doc link/v7.mdto green once this is onmainAI-assisted contribution. Drafted with Claude Code (Opus 5).
🤖 Generated with Claude Code
https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
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>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>`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§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_016wDDVXWMDz82WqE1i969r4detailis a real envelope member now ec55e66720Follows 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_016wDDVXWMDz82WqE1i969r4detailis a real envelope member now' (#227) from docs/events-module-detail into edge 8cb4cb3e01