diff --git a/modules/rust/PLAN_REDESIGNS.md b/modules/rust/PLAN_REDESIGNS.md index 7c89a83..ce412c4 100644 --- a/modules/rust/PLAN_REDESIGNS.md +++ b/modules/rust/PLAN_REDESIGNS.md @@ -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) diff --git a/website/EVENTS.md b/website/EVENTS.md index ac41417..2317fb9 100644 --- a/website/EVENTS.md +++ b/website/EVENTS.md @@ -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: `, 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) | diff --git a/website/MODULE_API.md b/website/MODULE_API.md index 40a5e15..491f4ed 100644 --- a/website/MODULE_API.md +++ b/website/MODULE_API.md @@ -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: ` 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