feat(events): open the event contract to modules (Phase 7)
Some checks failed
PR Checks / bot-tests (pull_request) Successful in 29s
PR Checks / client-build (pull_request) Successful in 36s
PR Checks / server-tests (pull_request) Failing after 8m41s

MODULE_API 1.10.0. Four names forwarded on the module-facing `api` --
registerEventActions, registerEventBudgets, registerEventLeases and
registerEventOptionSources -- one new route, and one rule made real: a
`cost()` naming a dimension no module declared is refused.

Only one of the four is new machinery. The action registry has staged
core's three actions on every boot since Phase 1; 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.

Four decisions, settled 2026-09-03, all as recommended:

- Option sources are their own registration, modelled on registerAudiences,
  because a catalog has more than one consumer.
- An undeclared dimension is refused -- at save, at the dry run and at
  dispatch -- with its own code, because the fix is a module's declaration
  and not a deployment's cap.
- A lease is declared here and acquired by nothing; the ledger is Phase 8.
- Core registers core.options.legs, so an announce leg is a dropdown rather
  than the free-text box whose typo Phase 6's walk caught mid-run.

Proved with a throwaway module through the real loader, not with module-uo:
eventModuleContract.test.js writes a module to a real directory and lets the
loader scan it, covering all five envelope failure shapes, verify: true, the
four id spaces and dormancy on uninstall.

The live walk found the one defect nothing else could: the option-source
loader wrote its "already asked?" guard inside a setState updater and read
it on the next line, so the request was never made and the field sat on
"Reading the list..." for ever. It is a useRef now.

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:
2026-09-03 14:15:04 -05:00
parent 429e657239
commit fd9fb50351
22 changed files with 1825 additions and 117 deletions

View File

@@ -370,6 +370,41 @@ function buildApi(record) {
once('registerEngagementSeeds')
record.staged.registerEngagementSeeds(seeds)
},
// The event contract (API 1.10.0, EVENTS.md §F). **This is the seam Phase 1
// built and did not open**: `registerEventActions` has staged core's three
// actions on every boot since then and no module could reach it, because
// this facade had no method that delegated. The four lines below are what
// Phase 7 ships — core has been going through the same door for six phases,
// so the registry a module now reaches is one that has been exercised on
// every boot rather than one whose first registrant is a stranger.
//
// `once` on all four, for the reason every batch registration above takes
// it: a batch is a module's complete statement about what it declares, and a
// second call is a module changing its mind halfway through `register()`
// rather than adding to it.
//
// The four id spaces are separate and the loader does not police that —
// `registries.apply()` does, per space. An action names a VERB, a budget
// names a RESOURCE, a lease names a VALUE and an option source names a
// CATALOG, so `uo.creatures` may legitimately appear in more than one of
// them and reading that as a collision would forbid the most natural set of
// names a module will ever write.
registerEventActions(actions) {
once('registerEventActions')
record.staged.registerEventActions(actions)
},
registerEventBudgets(budgets) {
once('registerEventBudgets')
record.staged.registerEventBudgets(budgets)
},
registerEventLeases(leases) {
once('registerEventLeases')
record.staged.registerEventLeases(leases)
},
registerEventOptionSources(sources) {
once('registerEventOptionSources')
record.staged.registerEventOptionSources(sources)
},
// The two lifecycle hooks (§2.5). Registered here, dispatched from
// lifecycle.js — this file runs with no database and the hooks run with one.
// Both are optional: a module with no warm-up and nothing to close simply