feat(events): schema, CRUD and the core action registry (Phase 1) #183
Reference in New Issue
Block a user
No description provided.
Delete Branch "feat/events-phase-1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Event System Phase 1 (
EVENTS_PLAN.md). Docs half: RunicGateway/docs#209.Schema —
db/schema.sql, append-onlySix of the nine core tables:
event_series,event_definitions,event_versions,event_runs,event_run_steps,event_run_log— the ones that do not depend on the module contract.event_action_settings,event_run_budget,event_run_resourcesandevent_run_participantsarrive with the phases that give them a writer, rather than as empty tables nothing reads.The two indexes that carry the weight are in from the start:
UNIQUE (definition_id, scope, scheduled_for)onevent_runs— it, not the claim, is what makes "one run per occurrence per scope" true.scopeis''rather than NULL, because multiple NULLs do not collide in MariaDB and a NULL scope would silently permit two runs of one occurrence.UNIQUE (run_id, phase, seq)onevent_run_steps— materialisation isINSERT IGNOREagainst it, so a tick that overran into the next one cannot double-materialise a phase.The action registry —
modules/registries.js+config/coreEventActions.jsregisterEventActionsstaging and commit, with its own id namespace (an action names a verb, a trigger names an event — one id may legitimately be both), the closedriskandreversiblesets with no default,revert()required iff and only iffreversible: 'ledger', a boundedbudgetMs, and a param shape whose every entry needs a type and anexample.perform/revert/costare stripped from everything the catalog serves, exactly as an audience'sresolveis.Core declares
core.announce,core.waitandcore.cuethrough the same staging area a module will use, so the seam is exercised on every boot long before a module touches it.It is reachable only by
registerCore().loader.jsbuilds its ownapifacade for a module and has no method that delegates here, so no module can call it andMODULE_API_VERSIONis untouched. Phase 7 adds the facade and makes the bump — this PR does not turn the integration kit red.Surface — 13 routes under
/api/v1/admin/eventsReads staff-wide. Publish, archive and run creation are
adminonly from this phase, perEVENTS.md§N2, even though the switchboard they will eventually consult does not exist yet: a button that is admin-only later and open now is a gate nobody notices was missing.The live run controls (pause/resume/advance/cancel, step skip/retry/confirm, cleanup) and
verifyare absent rather than stubbed — nothing is in flight until Phase 2, and a control that returns200and does nothing is worse than one that is not there.Four things the build settled that the plan had left open
All four are recorded in docs#209 and all four are worth a look:
event_definitionsgained aspeccolumn. §D's column list does not name one, because §D describes what a published event is made of. A draft's working copy cannot be anevent_versionsrow: that table is immutable and a run pins one.validate()addsactionVersionanddormant, then refused them as unknown keys on the next call, which would have made the second save of any definition, and publish's own re-validation, impossible. Both are now accepted and recomputed rather than trusted.exampleis required, optional params included, matchingregisterEventTriggers.GET /admin/events/:idandGET /admin/events/series.One thing worth defending explicitly
Core's three
perform()bodies answer{ ok: false, retry: false }rather than{ ok: true }. A truthy stub on an action that did nothing is a recorded world change that did not occur — the exact mistake §F's failure default exists to prevent. They are unreachable in this phase either way.conditions.checkLiteralis exported and reused for step-param type checking: one switch over the six types, so "is this a datetime" has one answer rather than two that drift.Verify
npm test— 1641 tests, 1616 pass, 24 skipped, 1 fail. The one failure isengagementManifest.test.js, and it is pre-existing and environmental:engagement-triggers.jsonis CRLF in a Windows working tree undercore.autocrlf=truewhile the generator writes LF. Content identical, line endings differ; confirmed still failing with this branch's changes stashed, and green on CI's Linux runners. This PR touches neither the triggers nor the generator.eventActionRegistry.test.js,eventSpec.test.js,eventsAdmin.test.js.npm run check:modules— clean; core names no game noun.npm run routes:manifest— +13 routes, zero moved, zero deletions.npm run swagger— regenerated; all 13 routes annotated.EVENTS_PLAN.md Phase 1. Six of the nine core tables — the ones that do not depend on the module contract — plus definitions CRUD, publish, archive, and the action registry with core as its first registrant. **Nothing dispatches.** There is no runner until Phase 2, so a run row is created and stays `scheduled`. That is this phase's correct answer and the surface renders it verbatim rather than hiding it. Schema (`db/schema.sql`, append-only): event_series, event_definitions, event_versions, event_runs, event_run_steps, event_run_log. The four that need a writer — event_action_settings, event_run_budget, event_run_resources, event_run_participants — arrive with the phases that give them one. Registry (`modules/registries.js` + `config/coreEventActions.js`): registerEventActions staging and commit, with its own id namespace, the closed risk and reversibility sets, revert() required iff and only iff reversible: 'ledger', a bounded budgetMs and a param shape whose every entry needs a type and an example. perform/revert/cost are stripped from everything the catalog serves. Core declares core.announce, core.wait and core.cue through the same staging area a module will use. It is reachable ONLY by registerCore(): loader.js builds its own api facade and has no method that delegates here, so no module can call it and MODULE_API_VERSION is untouched. Phase 7 adds the facade and the bump. Surface (13 routes under /api/v1/admin/events): Reads staff-wide; publish, archive and run creation admin-only from this phase per EVENTS.md §N2, even though the switchboard they will consult does not exist yet — a button that is admin-only later and open now is a gate nobody notices was missing. The live run controls and `verify` are absent rather than stubbed, because nothing is in flight yet. Four things the build settled, all recorded in docs: - event_definitions gained a `spec` column. A draft's working copy cannot be an event_versions row: that table is immutable and a run pins one. - The spec validator must accept its own output. It added `actionVersion` and `dormant` and then refused them as unknown keys, which would have made the second save of any definition — and publish's re-validation — impossible. A test caught it; both are now accepted and recomputed. - A param's `example` is required, optional params included, matching registerEventTriggers. It is the authoring form's placeholder. - Two routes the §API-surface table did not name: GET /admin/events/:id and GET /admin/events/series. Core's three perform() bodies answer { ok: false, retry: false } rather than { ok: true }: `ok: true` on an action that did nothing is a recorded world change that did not occur, which is the exact mistake §F's failure default exists to prevent. `conditions.checkLiteral` is exported and reused for step-param type checking — one switch over the six types, so "is this a datetime" has one answer. Verified: 44 new tests, whole server suite, `npm run check:modules`, routes manifest and swagger regenerated (the manifest diff is +13 routes, zero moved). Docs: RunicGateway/docs#209 Co-Authored-By: Claude <noreply@anthropic.com>