docs(events): the step editor and event templates — MODULE_API 1.13.0

The contract half of website feat/event-step-editor (Rust PLAN_REDESIGNS §2):
MODULE_API.md §1.1/§2.4 gain registerEventTemplates, a param's duration, a
lease's example and an option's note; EVENTS.md §F (the note), §I (what the
1.13.0 editor settled) and the API surface (the templates route, draftAhead).
PLAN_REDESIGNS §2.8 records the core half as built and the Module-uo proof.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-12 02:09:55 -05:00
parent 1a3411b19e
commit a2dbce1f89
3 changed files with 107 additions and 5 deletions

View File

@@ -389,6 +389,26 @@ Author a kit weekend from the template on each rig; a lease step with the phase'
shows); a draft edited after publishing (Start asks); a boolean lease (a toggle); and a Module-uo event opened
in the same editor, which must be unchanged apart from the controls.
### 2.8 As built: the core half (2026-10-12)
website `feat/event-step-editor`, MODULE_API **1.13.0** (§8; `website/MODULE_API.md` §1.1, `website/EVENTS.md` §I).
Everything in §2.1–§2.3 and §2.5's core half, with three things settled while building:
- **The phase guard counts the RUN, not the phase.** Teardown gives a held thing back when the run ends, so
a 10-minute lease in phase 1 is safe if phases 2 and 3 take ten minutes between them. The floor is the sum
of every later phase's `after`; a phase with no rule counts as no time, and one that waits on the game makes
the floor unknown, so it is not warned about. Only a phase with **no** rule is given one, and only when a
change can create or lengthen a hold.
- **"Is the draft ahead?" is answered by the server** (`draftAhead`): the working spec against the
published one, as data with keys sorted. A timestamp cannot answer it, because publishing writes the row too.
- **A template's inputs are params**, checked by the same function, so they get the same controls, sources and
duration unit as a step's fields. The draft comes back unsaved, from
`POST /admin/events/catalog/templates/:templateId`.
**The Module-uo proof**, against this branch: the frozen-manifest job (76 routes, all documented; `uo`
registers its 12 actions), the server suite 723/723 and the client suite 45/45 — the same numbers as 1.11.0's.
The Module-Rust half (the kit weekend, `linked`, `kits.uses`, the reward message) follows on `edge`.
---
## 3. Zones: ZoneManager's options, and a dome (§4.4; D166, D167)

View File

@@ -1103,6 +1103,10 @@ visible warning rather than blocking the form. For module-uo it is answered enti
atlas — offline, with the shard down. Without it the authoring UI in [§I](#i--admin-ui-authoring-and-the-calendar)
is a JSON editor with better fonts.
**A row may carry a `note`** (MODULE_API 1.13.0): a sentence, at most 300 characters, that the editor
shows under the field while that row is the chosen value — advice such as *"9 of 41 linked players
have used this kit up"*. It is never a rule; the save path does not read it.
### Integration Kit
A fifth chapter, obeying the kit's own rule — teach and link out, re-specify nothing. The four things
@@ -1342,6 +1346,43 @@ sent a term — so the one screen that picks a spawner was picking from a 2,000-
`searchable`, never the length of the answer: inferring it from a short list reads correctly right
up until the same source is a dropdown on one shard and a search box on another.
### What the step editor settled at MODULE_API 1.13.0
The first Rust player walk (`modules/rust/PLAN_FIXES.md` U-3–U-6, F11) found the Phase 13 form
honest and hard to use, and `modules/rust/PLAN_REDESIGNS.md` §2 reworked it. Core-only and
game-agnostic: Module-uo's steps get the new controls and nothing else.
- **A field is the thing it does.** No ` · int` tag and no bare asterisk; a required field says so in
words, under itself and in the save's list, only once a save has been tried with it empty.
- **A boolean is a toggle when required and three buttons — Not set, Yes, No — when optional**
(Rust D334). The Phase 13 reason for a three-way control stands: "not set" is the action's own
default, and a two-state control posts `false` for every field nobody touched.
- **A choice of two or three is a row of buttons**, whether a lease's `values` or a source that
answered with that few. The free-text box goes only while the source has answered and the value is
one of its choices, so the §F rule — a source that cannot answer leaves a text box — still holds.
- **`core.lease` is drawn as the chosen lease.** Picked by its label, then the value as the lease's
type with its range or `values` and its own `example`, the target as a picker over the lease's
`target.source` (`core.lease`'s own `target` param has no source, on purpose), and the minutes
with the lease's longest hold beside them. The params written are still `core.lease`'s.
- **A timed step keeps its run open** (U-6). A held thing is given back at teardown, when the RUN
ends, so a 10-minute lease in a one-phase event with no advance rule lasted a second. A param
that is a length of time says so (`duration: 'minutes'`). Adding or lengthening a timed step in a
phase with no advance rule gives the phase `after: <the longest hold>`, on screen; a rule the
author chose is never overwritten. Under any phase whose run can end sooner than its longest hold
— counting every later phase's `after`, and nothing for a phase that waits on the game — the
editor says when the hold is given back.
- **Start asks when the draft is ahead** (U-5). A run is pinned to the published version, so an
edit saved and not published did not run, silently. The definition read the editor loads carries
`draftAhead` — the working spec compared with the published one as data, key order ignored — and
the start dialog then asks: **Publish and start**, or **Start version N**, the published one. It
does not choose: publishing moves scheduled occurrences too.
- **A new event may start from a template** (Rust D192). A module's `registerEventTemplates` entry
offers a few inputs; core checks them, the module builds a plan, core validates it as a spec and
the editor opens it as an unsaved draft (`POST /admin/events/catalog/templates/:templateId`).
Nothing records which template made a definition, and the runner never sees one.
- **Core's own examples name no game** (Rust PLAN_REDESIGNS §0.9): `core.lease` offered
`uo.rate.skillgain` and the announcements spoke of Britain and Cove to every deployment.
### Four affordances worth building in from the start
- **Dry run.** *(Built, Phase 6.)* Materialise the steps, dispatch each with `verify: true`, report what *would* happen
@@ -1854,7 +1895,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 |
| `GET /admin/events/:id` | staff | one definition, working spec included — what the editor reads. With **`draftAhead`** (MODULE_API 1.13.0): whether the working spec differs from the published version, so the start dialog can ask which to run. `PUT` and `publish` answer with it too |
| `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` |
@@ -1871,6 +1912,7 @@ no URL moved.
| `POST /admin/events/runs/:runId/steps/:stepId/skip\|retry\|confirm` | admin, moderator | Phase 3. `confirm` resolves a GM cue step; `retry` re-queues the step a paused run is stopped at and resumes it |
| `POST /admin/events/runs/:runId/cleanup` | admin | re-run cleanup over unreverted resources (Phase 8). **`admin` rather than admin+moderator, unlike the seven live controls beside it**: this is not incident response, it asks core to write to the world again, which §K puts in the same row as the world-changing actions themselves. Legal on a TERMINAL run only — a run still in flight has a ledger that is still growing. It is the only thing that clears `revert_attempts`, and it answers **200 whatever it found**, for the dry run's reason: some resources may still be out there, and a 4xx would make that indistinguishable from a bad run id |
| `GET /admin/events/catalog` | staff | registered actions, param schemas, risk classes, budget dimensions — and, since Phase 5, the **trigger** catalog and condition operators the advance form renders. Phase 7 added `budgets`, `leases` and `optionSources`: the other three registrations of the module contract, served beside the actions because the step editor needs all four to draw ONE step, and four requests to draw one form would be four chances to render half of it. Served here rather than borrowed from `/admin/engagement/triggers` because that route is `adminOnly` while a definition is authored by `admin` **and** `editor` |
| `POST /admin/events/catalog/templates/:templateId` | admin, editor | **fill an event template in** (MODULE_API 1.13.0) — the body's `inputs` are checked against the template, the module builds a plan and core validates it as a spec. The answer is a `draft` (`title`, `summary`, `spec`) and **nothing is written**: the editor opens it and the author saves it through `POST /admin/events`. `400` for refused inputs, `404` for an unknown template, `502` when the module fails or builds a plan core cannot accept |
| `GET /admin/events/catalog/options/:sourceId` | staff | **the values behind a param's `source`** (Phase 7) — resolved by the module that registered it, on its own request rather than inside the catalog, because a source can be slow or down and must not take the catalog with it. A refusal is a **`200` with `ok: false` and a `reason`**, deliberately: the field degrades to free text with a warning rather than the form breaking |
| `GET /admin/events/series` | staff | the arcs a definition may belong to |
| `POST /admin/events/series` | admin, editor | create an arc (Phase 4) |

View File

@@ -26,13 +26,41 @@ here extends the contract first, in this file, before the module is written agai
Core exports a single integer-major semver string from `server/src/modules/version.js`:
```js
const MODULE_API_VERSION = '1.12.0'
const MODULE_API_VERSION = '1.13.0'
```
The client half carries the same number (`client/src/modules/version.js`) and a test asserts the two
agree. Duplicated rather than fetched because the value has to be on `window.__rg` before the first
module chunk evaluates, which is earlier than any network round trip could answer.
**1.13.0 — the event step editor** (`website/EVENTS.md` §F, §I; Rust `modules/rust/PLAN_REDESIGNS.md`
§2, D192). Four additions and no removal, so minor. Each is optional, and each is a declaration the
editor, core's own page, renders.
- **`api.registerEventTemplates([{ id, label, description, inputs, build }])`** — a named starting
point for a new event. `inputs` are params (§2.4), at most 8, drawn by the same field a step's
params are, sources and all. On **New event** the author picks a template and fills its inputs;
core checks them, calls `build({ inputs })` (async allowed, 10 s) and validates what comes back —
`{ title, summary?, spec: { phases } }` — as a spec. The editor opens it as an ordinary unsaved
draft. **Nothing about the template is stored**: no column, no link, and the runner never sees
one. A refusal the author should read is `{ ok: false, error }`; a throw, a hang or an invalid
spec is logged and answered with core's own sentence, never the module's stack.
- **A param's `duration: 'minutes'`** — on an `int` or `float` param that is a length of time, so
the editor can see a timed step. A held thing is given back when the run ends, so the editor gives
a phase with no advance rule `after: <the longest hold>` when a timed step is added, and says so
under any phase whose run can end sooner. `core.lease`'s `minutes` declares it.
- **A lease's `example`** for its value: one scalar, shown as the placeholder when an author picks
that lease. The editor draws `core.lease` as the chosen lease's own form — its type's control, its
range, its `values`, its `target.source` as the target's picker and its `maxDurationMs` beside the
minutes — so a lease step no longer shows core's one example for every lease.
- **An option row's `note`** — a sentence, at most 300 characters, the editor shows under the field
while that option is the chosen one. It is advice about the value, never a rule: the save path
does not read it.
**Module-uo is unaffected.** It declares `^1.10.0` and uses none of the four. Its actions, leases and
sources come out of the registry exactly as they went in; its steps render with the new controls and
nothing else. The proof website#209 ran for 1.11.0 was run again against this core (website PR body).
**1.12.0 — an event action's optional `progress(envelope)`** (`website/EVENTS.md` §I; RunicNPC
`runicnpc/PLAN.md` stage 8, D304, D305). One addition and no removal, so minor.
@@ -391,6 +419,10 @@ branches lacks — so it is the only place `permits` is true between two values
between `admin` and `owner`, `members` or `subscribers`. `permits`, `meet` and `meetAll` are otherwise
unchanged, and so is every rule about composition narrowing rather than widening.
**1.13.0 — the event step editor** (`website/EVENTS.md` §F, §I; Rust PLAN_REDESIGNS §2).
`registerEventTemplates`, a param's `duration`, a lease's `example` and an option's `note`; four
optional additions, so minor; see §1.1. Module-uo's `^1.10.0` still resolves.
**1.12.0 — an action's `progress()`** (`website/EVENTS.md` §I, RunicNPC D304). One optional member
on an action, so minor; see §1.1. Core asks it while a run is live and publishes the answer, one of
two shapes, on the public event. Module-uo's `^1.10.0` still resolves.
@@ -806,10 +838,11 @@ api.registerSlashCommands([{ name, description, options, access, handler }]) //
api.registerEventTriggers([{ id, label, kind, subjectKey, audience, ceiling, version, variables }]) // 1.7.0
api.registerAudiences([{ id, label, params, ceiling, resolve }]) // 1.7.0
api.registerEngagementSeeds({ templates, ruleGroups }) // 1.9.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0
api.registerEventBudgets([{ id, label, unit }]) // 1.10.0
api.registerEventLeases([{ id, label, type, min, max, values, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0
api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0
api.registerEventLeases([{ id, label, type, min, max, values, example, target, maxDurationMs, read, apply, restore, inForce }]) // 1.10.0; example 1.13.0
api.registerEventOptionSources([{ id, label, searchable, resolve }]) // 1.10.0; a row's note 1.13.0
api.registerEventTemplates([{ id, label, description, inputs, build }]) // 1.13.0
api.onBoot(async (ctx) => {})
api.onShutdown(async () => {})
```
@@ -1216,6 +1249,13 @@ rather than implementation and belong here:
would hide what the line describes answers `null` for that reader; a public page must never say
what the module's own pages would hide. Core gives it 2 s and drops a late answer. It is a read: it
must change nothing, and it may be called every few seconds for as long as a run is live.
- **A template is a shortcut past the editor, never a second way to make an event** (1.13.0). What
`build()` returns is validated as a spec and opened as an unsaved draft; the author saves it
through the same route, with the same checks, as one they typed. So a template cannot make a
definition the save path would refuse, and changing or removing one touches no definition it
ever made. `build()` may read the module's own data — the Rust kit weekend looks up a kit's
permission — and answers `{ ok: false, error }` with the author's sentence when the inputs make
no sense; anything it throws stays in the log.
- **A module reports who took part on the envelope, and there is no other door.** `participants`
rides back from `perform()` exactly as `resources` does, on both success shapes — including
`await: 'human'`, because a cue's confirm finishes the step without a second dispatch and that is