docs(events): Phase 7 as built -- the module contract at MODULE_API 1.10.0
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
This commit is contained in:
@@ -731,6 +731,116 @@ a 403 walk across all four roles on every route.
|
||||
|
||||
### Phase 7 — The module contract (`website` + `docs` + a throwaway test module)
|
||||
|
||||
> **Complete.** `edge` in `website` and `docs`. MODULE_API **1.10.0**, in both halves. Four names
|
||||
> forwarded on the module-facing `api` — `registerEventActions`, `registerEventBudgets`,
|
||||
> `registerEventLeases`, `registerEventOptionSources` — one new route
|
||||
> (`GET /admin/events/catalog/options/:sourceId`), and one rule made real: a `cost()` naming a
|
||||
> dimension no module declared is refused.
|
||||
>
|
||||
> **The seam is narrower than the four names suggest, and that is the point.**
|
||||
> `registerEventActions` has existed since Phase 1 and has staged core's three actions on every boot
|
||||
> since; what it never had was a way in, because `loader.js` builds its own `api` facade and had no
|
||||
> method that delegated to it. So the registry a module now reaches is one that has been exercised on
|
||||
> every boot for six phases rather than one whose first registrant is a stranger — the argument
|
||||
> `registerCore()` has made since the module system's Phase 3, and this is the phase where it pays.
|
||||
>
|
||||
> **The four decisions the org lead settled (2026-09-03), all as recommended:**
|
||||
>
|
||||
> - **Option sources are their own registration**, `registerEventOptionSources([{ id, label,
|
||||
> resolve }])`, modelled on `registerAudiences`. Not a field on the action that names one: a catalog
|
||||
> has more than one consumer — `uo.options.items` is the allowlist for granting an item and for
|
||||
> taking one back — and two actions declaring it separately would be two allowlists that can
|
||||
> disagree.
|
||||
> - **An undeclared dimension is REFUSED, at save, at the dry run and at dispatch.** Fail closed, so
|
||||
> that §F's *"a module cannot spend a budget it did not declare"* is a rule rather than a sentence.
|
||||
> Not at registration time: `cost` is a function of params, so core could only enforce it against
|
||||
> the declared examples, which is a rule about examples rather than about what runs.
|
||||
> - **A lease is DECLARED here and acquired by nothing.** The ledger that holds one, the deadline that
|
||||
> goes down the wire and the drift answer are Phase 8's. Declaring it now keeps the module contract
|
||||
> one version a module author reads once rather than two.
|
||||
> - **Core registers an option source of its own**, `core.options.legs`. `core.announce`'s `leg` param
|
||||
> was a free-text box whose typo was caught at DISPATCH, mid-run — which is exactly the defect Phase
|
||||
> 6's walk hit, an announce leg `"site"` no module registers. The legs are already a registry with
|
||||
> labels in them, so it costs nothing new, and it means the seam's first exercise is not a module's.
|
||||
>
|
||||
> **Three things the build settled:**
|
||||
>
|
||||
> - **The undeclared refusal gets its own code (`undeclared`) and runs BEFORE any cap arithmetic.** A
|
||||
> dimension nobody declared has no cap to be under and no meter to draw on. The separate code is not
|
||||
> tidiness: an operator told "the cap is spent" goes and raises a cap, and nothing changes, because
|
||||
> the fix is a module's declaration.
|
||||
> - **A budget's `unit` is required and its vocabulary is open.** Required because a bare number on a
|
||||
> cap box is ambiguous in the case that matters — 30 of what? — and open because core never
|
||||
> interprets it. Closing the set would make "kilometres" a MODULE_API bump for a noun core does not
|
||||
> read.
|
||||
> - **A dimension nobody declares is SHOWN on the switchboard, not filtered out.** The action is
|
||||
> refused wherever it is used, so the screen has to be able to say which module is incomplete;
|
||||
> hiding the row would make a broken module look like a cheap one. `registerEventBudgets` supplies
|
||||
> the label and unit, while WHICH dimensions an action spends is still discovered by pricing its
|
||||
> declared examples — a registry cannot answer that, because `cost` is a function of params.
|
||||
>
|
||||
> **The plan's own instruction, followed literally.** *"Prove it with a throwaway module, not with
|
||||
> module-uo."* `eventModuleContract.test.js` (17) writes a real module to a real directory, points
|
||||
> `MODULES_DIR` at it and lets the **real loader** scan, validate, `register()` and commit it — so a
|
||||
> test that staged directly, which would have passed just as happily before this phase, is not what is
|
||||
> being run. It covers all five failure shapes dispatched from a module (rejected promise, throw,
|
||||
> `budgetMs` timeout, non-object, missing `ok`), `retry: false`, `await: 'human'`, `holdFor`, the
|
||||
> whole envelope including the idempotency key, `verify: true` writing nothing, the four id spaces,
|
||||
> `once` on a second call, the un-namespaced refusal, a module that registers nothing at all, every
|
||||
> option-source failure mode, and an action going dormant when its module is uninstalled.
|
||||
>
|
||||
> **The defect only the browser could find, and it is a React one.**
|
||||
> `setSources((s) => { if (s[id]) return s; started = true; … })` — the guard was written inside the
|
||||
> state updater and read on the next line. **`setSources` QUEUES its updater rather than running it**,
|
||||
> so `started` was always `false`, the function always returned early, and the request was **never
|
||||
> made**: the field sat on *"Reading the list…"* for ever. Every server test passed, the route
|
||||
> answered correctly by `curl`, and nothing but opening the page could say so. **State is the wrong
|
||||
> tool for a question that must be answered synchronously, at the call** — it is a `useRef` now.
|
||||
>
|
||||
> **And one thing the docs caught the code doing.** The § API surface table has always named this
|
||||
> route `GET /admin/events/catalog/options/:sourceId`; it was built one segment shallower at
|
||||
> `/admin/events/options/:sourceId`, and the divergence surfaced only when the docs half was written.
|
||||
> Moved to match the spec — which is also the better shape, because a source's values ARE catalog
|
||||
> data, fetched separately so a slow source cannot take the catalog with it.
|
||||
>
|
||||
> **Verified:** `npm test` — **1950 tests, 1876 pass, 73 skipped, 1 fail**, that one still the
|
||||
> pre-existing `engagementManifest.test.js` CRLF failure (confirmed by stashing this branch and
|
||||
> watching it fail unchanged; `edge` before: 1921/1847/73/1). **+29 tests**: 17 in
|
||||
> `eventModuleContract.test.js`, 11 in `eventActionRegistry.test.js` (the three new shape checks and
|
||||
> the id spaces), and 1 in `eventsRoles.test.js` — the new route joining the 403 walk. `eventsAdmin`,
|
||||
> `eventAuthorize`, `eventVerify` and `eventRunner` all gained the budget declarations their cap tests
|
||||
> now need, and the test dimensions were renamed `x.*` → `test.*` so they carry the registering
|
||||
> owner's prefix, exactly as their action ids already do. Client: **362 pass**; the client builds.
|
||||
> `npm run routes:manifest` and `npm run swagger` — **one route added, none moved.**
|
||||
> `check:modules` and `check:hosts` green.
|
||||
>
|
||||
> **The live walk, on the local review stack**, driven by a throwaway `rig` module in
|
||||
> `website/modules/` — a real module directory, scanned by the real loader, **deleted before commit**.
|
||||
>
|
||||
> - **`coreApi: "^1.10.0"` resolved and `module-uo`'s `"^1.9.0"` still did**, on the same boot. That
|
||||
> is the additive claim, proved rather than asserted.
|
||||
> - **The catalog served all four registrations** with every callable stripped — two budgets with
|
||||
> their labels and units, one lease, three option sources including core's own.
|
||||
> - **The option route answered four ways**: a module's list, a source that throws
|
||||
> (`200`, `ok: false`, *"could not be read"*), a source nobody registers, and core's `core.options.legs`
|
||||
> — which returned `discord` **and** module-uo's `towncrier`, proving the per-request resolve.
|
||||
> - **The switchboard named the dimension.** `Wisps summoned … count` on the cap box, and the
|
||||
> undeclared one rendered disabled with *"No module declares this as a budget, so a step using this
|
||||
> action is refused."*
|
||||
> - **The save refused the undeclared dimension** by name and phase-step path, before anything was
|
||||
> scheduled; the role floor refused an editor the same world-changing step an admin saved.
|
||||
> - **The dry run priced a MODULE's action** — 3 of a cap of 5 — and refused the two-step version at
|
||||
> `8 of "rig.wisps"`. At dispatch the first step spent 3 and the second came back
|
||||
> `refused: asks for 3 of "rig.wisps"; 3 of 5 is already spent this run`, health `degraded`.
|
||||
> - **The dropdown wrote the exact spelling into the params box.** Picking *Britain Hall* put
|
||||
> `"place": "britain-hall"` into the JSON, one request served two steps on the same action, breaking
|
||||
> the JSON greyed that step's picker to *"Fix the params JSON to pick a value"* while the other
|
||||
> stayed live, and pointing the param at the failing source rendered
|
||||
> *"… could not be read — type the value by hand"* in red beside a field that stayed editable.
|
||||
>
|
||||
> **This PR turns the integration kit red**, on purpose. `checkCoreApi.js` asserts equality against the
|
||||
> `main` sha `ci/core-ref.json` pins, so it stays red until the cutover re-pins it (Phase 16).
|
||||
|
||||
Generalise P1's registry into the public contract: `registerEventActions` with `cost`, `risk`,
|
||||
`reversible`, `budgetMs`, `params` and `perform` / `revert`; `registerEventBudgets`;
|
||||
`registerEventLeases`; and param **option sources**. `MODULE_API_VERSION` → **1.10.0**, with §2.4 and
|
||||
|
||||
Reference in New Issue
Block a user