docs(events): Phase 1 as built — schema, CRUD and the core action registry
The docs half of RunicGateway/website#<n>. Three files.
**BACKEND_DESIGN.md** gains the six event tables, column by column, and the
eleven admin routes. Written where the other table groups are, in the same
shape, because the argument for a column belongs beside the column.
**EVENTS.md** records four things the build settled that §D and §F had left
open:
- `event_definitions.spec`, the working copy. §D's column list does not name
one because §D describes what a PUBLISHED event is made of — but "editing a
draft is free; no version exists yet" means the draft has to live somewhere,
and it cannot be an `event_versions` row: that table is immutable and a run
pins one, so a mutable unpublished row in it would be exactly what
versioning exists to prevent.
- A param's `example` is REQUIRED, on optional params too, the same rule
`registerEventTriggers` makes of a variable's example and for the same
reason: it is the authoring form's placeholder, one word at declaration
time and unreconstructable afterwards.
- The authoring side of dormancy. §F said what happens at DISPATCH; the save
path draws the same line one step earlier, in the shape `engagement_rules`
established — a saved step may keep an unregistered action, a new step may
not add one, and a dormant step blocks the publish rather than the save.
- Publish re-validates against the registries as they stand at that moment,
not from the save that wrote the spec.
Plus two routes the § API surface table did not name — `GET /admin/events/:id`
(the list serves a summary; the editor needs the tree) and `GET
/admin/events/series` (a form cannot offer a value it cannot enumerate) — and a
note stating which of that table's rows Phase 1 deliberately did not build.
**EVENTS_PLAN.md** marks Phase 1 complete, names those four settlements, and
states the two deliberate absences so a reviewer does not read them as gaps:
the live run controls are not stubbed, and core's three `perform()` bodies
answer `{ ok: false }` rather than `{ ok: true }` — `ok: true` on an action
that did nothing is a recorded world change that did not occur.
`api-route-inventory.json` is NOT resynced here. It has been stale since
engagement Phase 2 and is 47 routes behind; catching it up in this PR would
bury a 13-route change under an unrelated 47.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -400,8 +400,8 @@ tables carry no module prefix.
|
||||
|
||||
| Table | Holds | Why a table |
|
||||
| --- | --- | --- |
|
||||
| `event_definitions` | id, title, slug, summary, storyline body, image, `owner_module` **nullable**, `state` `ENUM('draft','ready','archived')`, `current_version_id`, `series_id`, `concurrency_key`, `grace_seconds`, timezone, created/updated by. | The thing that is listed, searched, scheduled and audited. Three states, not five: an admin publishes their own work, so there is nobody to submit it to. |
|
||||
| `event_series` | id, name, description, ordering. Definitions optionally belong to one. | **The arc.** "Royal Spy Mission → Risky Partner → Message From the Void" is continuity that exists nowhere in the tooling this replaces. One small table buys it. |
|
||||
| `event_definitions` | id, title, slug, summary, storyline body, image, `owner_module` **nullable**, `state` `ENUM('draft','ready','archived')`, `current_version_id`, `series_id`, `series_order`, `spec` (the working copy — see below), `concurrency_key`, `grace_seconds`, timezone, created/updated by. | The thing that is listed, searched, scheduled and audited. Three states, not five: an admin publishes their own work, so there is nobody to submit it to. |
|
||||
| `event_series` | id, name, slug, description, ordering. Definitions optionally belong to one, at their own `series_order` within it. | **The arc.** "Royal Spy Mission → Risky Partner → Message From the Void" is continuity that exists nowhere in the tooling this replaces. One small table buys it. |
|
||||
| `event_versions` | `definition_id`, `version`, `spec` JSON — phases, steps, schedule, conditions, announcements — `published_at`, `published_by`. Immutable. | A run pins one. This is what makes a run reproducible and an audit answerable after an edit. |
|
||||
| `event_runs` | `definition_id`, `version_id`, `scope` (module-opaque), `status`, `health`, `current_phase`, `scheduled_for`, `timezone`, `started_at`, `ended_at`, `cleanup_status`, `claimed_by`, `claim_expires_at`, `started_by`. **`UNIQUE (definition_id, scope, scheduled_for)`** | The unique index — not the claim — is what makes "one run per occurrence per scope" true under two instances. `scope` is in the key so a worldwide event fans out to many servers without colliding with itself. |
|
||||
| `event_run_steps` | `run_id`, `phase`, `seq`, `action_id`, `params` JSON, `action_version`, `status`, `due_at`, `attempts`, `on_failure`, `idempotency_key`, `claimed_by`, `claim_expires_at`, `last_error`. `INDEX (status, due_at)` | The work queue, claimed with the outbox's compare-and-set. |
|
||||
@@ -411,6 +411,14 @@ tables carry no module prefix.
|
||||
| `event_run_participants` | `run_id`, `user_id` nullable `SET NULL`, `member_key` module-opaque, `score`, `rank`, `joined_at`, `meta` JSON. `UNIQUE (run_id, member_key)` | Results and profile history read it. `SET NULL` not `CASCADE`, matching `engagement_sends`: a record of what happened must survive an account deletion. |
|
||||
| `event_run_log` | `run_id`, `step_id` nullable, `kind` (closed set), `phase`, `detail` JSON, `at`. | `activity_log.detail` is `TEXT` and unqueryable. "Why didn't phase 3 start?" must be a query. |
|
||||
|
||||
> **`spec` on `event_definitions` is Phase 1's one addition to this table's column list**, and it
|
||||
> follows from "editing a draft is free; no version exists yet" below. A draft's working spec has to
|
||||
> live somewhere, and it cannot be an `event_versions` row: that table is immutable and a run pins
|
||||
> one, so a mutable unpublished row in it would be precisely what versioning exists to prevent.
|
||||
> Publishing copies the column into a version and leaves it standing as the next draft. `series_order`
|
||||
> is the same kind of addition — `event_series.ordering` places a series among the others, and a
|
||||
> definition's place *within* its arc is the column an editor drags.
|
||||
|
||||
### Not tables, deliberately
|
||||
|
||||
- **Phases** — configuration in `event_versions.spec`, materialised as steps when a run starts. A
|
||||
@@ -592,7 +600,11 @@ api.registerEventLeases([{
|
||||
game words a chess ladder has no use for.
|
||||
- **Params are validated at save *and* at dispatch, against the declared version.** A step stores the
|
||||
`action_version` it was authored against; a bump makes it render a warning in the editor rather
|
||||
than dispatch a mistyped parameter.
|
||||
than dispatch a mistyped parameter. A param's `example` is **required**, on the optional params as
|
||||
well as the required ones — the same rule `registerEventTriggers` makes of a variable's example and
|
||||
for the same reason. It is the authoring form's placeholder, it is one word at declaration time,
|
||||
and it is unreconstructable afterwards; a blank box is how an unattended world write comes to be
|
||||
scheduled with a typo in it.
|
||||
- **Resources are named by the module and owned by core.** `kind` and `ref` are opaque strings core
|
||||
stores verbatim — `ctx.teams.activity.push`'s exact treatment. Core does the remembering; the
|
||||
module does the meaning.
|
||||
@@ -610,7 +622,11 @@ api.registerEventLeases([{
|
||||
**no concept of "the game being up"** — only `{ ok: false, retry: true }` — because a module with
|
||||
six sidecars cannot answer that question in the singular.
|
||||
- **An action whose module is uninstalled goes dormant, never an error.** A step naming it fails
|
||||
`terminal` with the module named and the run degrades — never a silent skip.
|
||||
`terminal` with the module named and the run degrades — never a silent skip. The authoring side
|
||||
draws the same line one step earlier, in the shape `engagement_rules` established for a dormant
|
||||
trigger: **a step already in a saved spec may keep an unregistered action and a new step may not
|
||||
add one**, so an uninstall is never destructive after the fact — and a dormant step blocks the
|
||||
*publish*, because a version is what a run pins and a run cannot dispatch a verb nobody registers.
|
||||
- **Actions and budgets are their own id spaces.** An action names a verb, a trigger names an event,
|
||||
a budget names a resource dimension.
|
||||
|
||||
@@ -924,7 +940,7 @@ without stealing an edit.
|
||||
| Edit | Effect |
|
||||
| --- | --- |
|
||||
| Editing a `draft` | Free. No version exists yet. |
|
||||
| Publishing | Snapshots the whole spec into an immutable `event_versions` row and points `current_version_id` at it. |
|
||||
| Publishing | Snapshots the whole spec into an immutable `event_versions` row and points `current_version_id` at it. The spec is **re-validated against the registries as they stand at that moment**, not trusted from the save that wrote it: a module uninstalled in between must block the publish rather than produce a run that fails at dispatch with the world half-changed. |
|
||||
| Editing a `ready` definition with no live run | Creates the next version on publish. Future runs use it. |
|
||||
| Editing while a run is live | Creates the next version. **The live run keeps the version it pinned** and is unaffected. The editor says so. |
|
||||
| Changing what a *running* event does | **Not an edit.** The live controls are pause, resume, skip, force-advance and cancel — each logged, each attributable, none mutating a version. Anything more expressive is a cancel and a new run, because a half-executed spec edited mid-flight is neither reproducible nor auditable. |
|
||||
@@ -940,6 +956,7 @@ no URL moved.
|
||||
| Route | Gate | |
|
||||
| --- | --- | --- |
|
||||
| `GET /admin/events` | staff | definitions, state, next occurrence, health |
|
||||
| `GET /admin/events/:id` | staff | one definition, working spec included — what the editor reads |
|
||||
| `POST /admin/events` | admin, editor | create a draft |
|
||||
| `PUT /admin/events/:id` | admin, editor | edit the draft spec |
|
||||
| `POST /admin/events/:id/publish` | admin | snapshot a version and go `ready` |
|
||||
@@ -955,6 +972,7 @@ no URL moved.
|
||||
| `POST /admin/events/runs/:runId/cleanup` | admin | re-run cleanup over unreverted resources |
|
||||
| `GET /admin/events/catalog` | staff | registered actions, param schemas, risk classes, budget dimensions |
|
||||
| `GET /admin/events/catalog/options/:sourceId` | staff | a module's option list for a param |
|
||||
| `GET /admin/events/series` | staff | the arcs a definition may belong to |
|
||||
| `GET/PUT /admin/events/actions` | admin | which actions are enabled on this deployment, and their per-run caps |
|
||||
| `GET /public/events` | — | the calendar: upcoming and live, by category, scope and series |
|
||||
| `GET /public/events/:slug` | — | one event: storyline, venue, schedule, live phase, results |
|
||||
@@ -966,6 +984,21 @@ no URL moved.
|
||||
> dispatches nothing, and the author who wrote the definition is exactly who should be able to price
|
||||
> it against the caps before asking an admin to publish it.
|
||||
|
||||
> **Two rows above were added by Phase 1 rather than decided in §N**, and both are derived from
|
||||
> what the surface needs rather than from a new policy. `GET /admin/events/:id` exists because the
|
||||
> list route serves a summary and the editor needs the whole authored tree; `GET
|
||||
> /admin/events/series` exists because a definition carries `series_id` and a form cannot offer a
|
||||
> value it cannot enumerate. Both are staff reads of data the list route already exposes, so neither
|
||||
> widens the surface's reach.
|
||||
|
||||
**What Phase 1 built, and what it deliberately did not.** Definitions CRUD, publish, archive, the
|
||||
version history, the action catalog, the series read and the run reads are live. Every route that
|
||||
acts on a run *in flight* — pause, resume, advance, cancel, step skip/retry/confirm, cleanup — is
|
||||
absent rather than stubbed, because nothing is in flight until the runner exists: a control that
|
||||
answers `200` and does nothing is worse than one that is not there. `verify` and `GET/PUT
|
||||
/admin/events/actions` are absent for the same kind of reason — there are no caps to price against
|
||||
and no switchboard to serve until the phase that builds them.
|
||||
|
||||
A module registers actions server-side and adds **no routes** for them beyond its option endpoints,
|
||||
which is what keeps the browser from being able to name a transport.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user