// ── Core's own event actions ─────────────────────────────────────────────── // // EVENTS.md §F, and Phase 1 of EVENTS_PLAN.md. The twin of config/coreTriggers.js // and registered through the same staging area a module will use in Phase 7 — // which is the entire reason these three exist this early. A registry whose first // real registrant is a module is a registry that has already drifted, and §F's // claim that core is "an event engine that can announce, wait, cue a human and // publish results" with NO module installed is only true if core declares the // verbs that do it. // // **Three actions, and between them they cover the three things an event can do // that name no game noun at all**: tell people something, let time pass, and ask // a human to go and do something. A deployment with no game module installed has // a working event system made of exactly these. // // **Nothing here dispatches yet.** Phase 1 builds the registry, the id grammar, // the risk classes and the param validation; Phase 2 builds `utils/eventRunner.js` // and is what calls `perform()`. The bodies below therefore answer with the // envelope §F defines for a refusal — and specifically NOT with `{ ok: true }`, // which is the one wrong answer a placeholder can give: `ok: true` on an action // that did nothing is a recorded world change that did not occur, which is the // exact mistake the envelope's failure default exists to prevent. `retry: false` // because a missing runner is not a transient condition. // // **This file must not touch the database.** It is required from `registerCore()`, // which runs under `routeManifest.js` and `swagger.js` against a dead pool // (MODULE_API.md §2.2). It is pure data plus three functions that are not called. // A factory rather than one shared function, because `perform`'s argument is // §F's dispatch envelope — `{ runId, stepId, idempotencyKey, scope, params, // actor, verify }` — and it does not carry the action's own id. Closing over it // is what lets the refusal name which action refused. const notWiredYet = (actionId) => async () => ({ ok: false, retry: false, error: `${actionId} is declared in Phase 1 and dispatched from Phase 2`, }) const ACTIONS = [ { id: 'core.announce', label: 'Announce', description: 'Publish a line of text to an announce leg — Discord, the in-game town crier, or any leg a module has registered.', // Nothing in the world changes and nothing is created: a message goes out. // That is what makes the default `on_failure` for this step `retry -> skip` // (§L) rather than `pause`, and it is the honest class even though the // message itself cannot be unsent. risk: 'notify', // A sent announcement is gone. `none` rather than `ledger` is not an // omission — there is no undo to write, and declaring `ledger` would put a // row in the cleanup ledger that teardown could never resolve. reversible: 'none', version: 1, params: [ { // A leg id, checked against the announce-leg registry at dispatch rather // than here: legs are registered by modules, and this file is evaluated // before any module has registered anything. name: 'leg', type: 'string', required: true, example: 'discord', description: 'The announce leg to publish on. Registered legs only.', }, { name: 'title', type: 'string', required: false, example: 'The gates of Britain open at dusk', description: 'Optional heading, for legs that render one.', }, { name: 'body', type: 'string', required: true, example: 'A caravan has been sighted on the road east of Cove.', description: 'The announcement itself. Plain text.', }, ], perform: notWiredYet('core.announce'), }, { id: 'core.wait', label: 'Wait', description: 'Let a fixed amount of time pass before the next step of this phase runs.', // `inspect` rather than `notify`: nothing is sent and nobody is told. It is // the weakest class the closed set has for an action that is not a broadcast. risk: 'inspect', reversible: 'none', version: 1, params: [ { name: 'seconds', type: 'int', required: true, example: 300, description: 'How long to wait. The runner sets the next step due_at from this.', }, ], // A wait is a genuine no-op at dispatch, and it will stay one: the delay is // the NEXT step's `due_at`, which the runner owns, not something this // function sleeps through. A `perform` that slept would hold a step's claim // for the duration and turn a five-minute pause into a five-minute lease. perform: notWiredYet('core.wait'), }, { id: 'core.cue', label: 'Cue a human', description: 'Post an instruction for staff and wait for someone to confirm it was done before the run advances.', // The action itself only posts an instruction. Whatever the human then does // is outside this system entirely, which is precisely why the cue exists: // it is how an event uses a capability no module has automated. risk: 'notify', reversible: 'none', version: 1, params: [ { name: 'instruction', type: 'string', required: true, example: 'Open the north gate and read the herald script in Britain bank.', description: 'What the staff member is being asked to do.', }, { name: 'assignee', type: 'string', required: false, example: 'Event Team', description: 'Who the cue is addressed to. A label, not an account.', }, ], // Phase 2 gives this its parking semantics — a cue step does not complete // when `perform` answers, it completes when a human presses confirm, and the // control that does so is Phase 3's. Both of those are what make this the // one action whose runtime shape is deliberately not decided here. perform: notWiredYet('core.cue'), }, ] module.exports = { ACTIONS }