feat(events): open the event contract to modules (Phase 7)
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:
@@ -486,6 +486,13 @@ export const api = {
|
||||
archiveEvent: (id) => req(`/admin/events/${id}`, { method: 'DELETE' }),
|
||||
listEventVersions: (id) => req(`/admin/events/${id}/versions`),
|
||||
eventCatalog: () => req('/admin/events/catalog'),
|
||||
// Phase 7. The values behind a param's `source` — resolved by the module that
|
||||
// registered the source, on a request of its own rather than inside the
|
||||
// catalog, because a source can be slow or down and must not take the whole
|
||||
// editor with it. A refusal comes back 200 with `ok: false`, so this never
|
||||
// throws for the case the screen is meant to render: the field degrades to
|
||||
// free text with the reason beside it.
|
||||
eventOptions: (sourceId) => req(`/admin/events/catalog/options/${encodeURIComponent(sourceId)}`),
|
||||
// Phase 6. The dry run is admin+editor: it dispatches nothing, and the author
|
||||
// who wrote the definition is who should be able to price it against the caps
|
||||
// before asking an admin to publish it. A report with findings comes back 200
|
||||
|
||||
Reference in New Issue
Block a user