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:
@@ -180,6 +180,21 @@ exports.catalog = (_req, res) => {
|
||||
})),
|
||||
operators: conditionGrammar.vocabulary(),
|
||||
advanceKinds: spec.ADVANCE_KINDS,
|
||||
// **The other three registrations of the module contract** (Phase 7). Served
|
||||
// beside the actions rather than on three routes of their own, because the
|
||||
// step editor needs all four to render one step: the action says what params
|
||||
// it takes, a param's `source` names an option source, and a cap the editor
|
||||
// shows is a budget's label and unit. Four requests to draw one form would
|
||||
// be four chances for the screen to render half of it.
|
||||
//
|
||||
// Each is already stripped of its callables by the registry (`resolve`,
|
||||
// `read`, `apply`, `restore`) — the same rule that keeps `perform` off an
|
||||
// action here. A source's VALUES are not in this payload either: they are a
|
||||
// request of their own (`/options/:sourceId`), because a source can be slow,
|
||||
// can fail, and would otherwise take the whole catalog down with it.
|
||||
budgets: registries.allEventBudgets(),
|
||||
leases: registries.allEventLeases(),
|
||||
optionSources: registries.allEventOptionSources(),
|
||||
limits: {
|
||||
maxPhases: spec.MAX_PHASES,
|
||||
maxStepsPerPhase: spec.MAX_STEPS_PER_PHASE,
|
||||
@@ -463,10 +478,16 @@ exports.actions = async (_req, res) => {
|
||||
configured: Boolean(row),
|
||||
changesWorld: authorize.changesWorld(full),
|
||||
// The dimensions this action can spend, so the screen can offer a cap
|
||||
// box per dimension. Discovered by pricing the action's own declared
|
||||
// examples until §F's `registerEventBudgets` lands in Phase 7 — see
|
||||
// `authorize.dimensionsOf`.
|
||||
dimensions: authorize.dimensionsOf(full),
|
||||
// box per dimension — each now carrying the label and unit its
|
||||
// `registerEventBudgets` declaration gives it (Phase 7), because "30" on
|
||||
// a box is ambiguous in exactly the case that matters: 30 of what?
|
||||
//
|
||||
// `registered: false` says a dimension nobody declares, and it is shown
|
||||
// rather than filtered out: an action that prices an undeclared
|
||||
// dimension is REFUSED at save and at dispatch, so the screen must be
|
||||
// able to show the operator why their action will not run instead of
|
||||
// quietly listing one fewer box than the action has dimensions.
|
||||
dimensions: authorize.budgetsOf(full),
|
||||
caps: row?.caps || {},
|
||||
updatedAt: row?.updated_at || null,
|
||||
updatedBy: row?.updated_by_username || null,
|
||||
@@ -478,6 +499,30 @@ exports.actions = async (_req, res) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/v1/admin/events/catalog/options/:sourceId — the values behind a param's `source`.
|
||||
*
|
||||
* §F's *Param option sources* (Phase 7). Without it the step editor is a JSON
|
||||
* editor with better fonts: a landmark, a creature and an item are all "a string
|
||||
* the operator has to spell right", and an unattended world write scheduled with
|
||||
* a typo in it is the failure this whole feature exists to make unlikely.
|
||||
*
|
||||
* **A refusal is a 200 with `ok: false`, not a 4xx or a 5xx.** §F: a source that
|
||||
* cannot answer degrades its field to free text with a visible warning rather
|
||||
* than blocking the form. A 502 would be true about the module and wrong about
|
||||
* the screen — the operator very often knows the value they want to type, and an
|
||||
* authoring form that a sidecar outage can make unusable is a worse failure than
|
||||
* the typo the dropdown prevents. The client renders the `reason` beside the box.
|
||||
*
|
||||
* `admin, editor`, like the catalog and for the same argument: this is authoring
|
||||
* data, and an editor who can write the step must be able to see the values it
|
||||
* accepts. The registry answers it, so it names no game noun here.
|
||||
*/
|
||||
exports.options = async (req, res) => {
|
||||
const result = await registries.resolveOptionSource(String(req.params.sourceId || ''))
|
||||
return res.json(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT /api/v1/admin/events/actions — set one action's switch and caps.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user