docs(website): demo mode step 2 built — DM18, MODULE_API 1.19.0

DM18 (org lead, 2026-10-24): core's event save never asked a module
about a step's params, so the console-command deny list's save-time
refusal is a new core hook, demoRefuses(params), asked at create, save
and publish on the demo. MODULE_API 1.19.0.

- DEMO_MODE.md: §3.4 says where the save-time check lives; DM18 row;
  step 2 marked built (website#224, Module-Rust#62) with the module's
  bridge recognition and fail-closed rules; the Rust fixture section.
- MODULE_API.md: 1.19.0 in §1.1, the changelog, the registerEventActions
  signature and the action rules.
- BACKEND_DESIGN.md §5.1: event steps on the demo.

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-26 03:41:26 -05:00
parent de7775eaab
commit 1a769f1e6c
3 changed files with 53 additions and 8 deletions

View File

@@ -26,13 +26,27 @@ 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.18.0'
const MODULE_API_VERSION = '1.19.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.19.0 — the public demo refuses a step at save** (`website/DEMO_MODE.md` §3.4, DM18). One optional
member on an event action, so minor.
- **`demoRefuses(params)`** (§2.4, `registerEventActions`): a function of a step's checked params that
returns a sentence, or `null` to let the step through. Asked only with `DEMO_MODE` on, for every step
when a definition is created, saved or published. The first sentence refuses the whole write
`403 { code: 'DEMO_LOCKED', message: <the sentence>, errors: [<the sentence>] }`, so core's one
refusal toast shows it. A hook that throws refuses too, with core's own sentence, because a lock that
cannot decide fails closed.
- The hook covers the **save**. The module still refuses in `perform()` (behind `ctx.demoMode`),
because a step can be saved before the module's list grows.
- A module that declares it needs `coreApi` `^1.19.0`: an older core rejects nothing but never calls
it, so the save would go through. Module-uo's `^1.10.0` still resolves and declares none.
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3; website#222). Three optional
additions and no removal, so minor. With `DEMO_MODE` unset, none of them changes anything a module sees,
except that `demoLocks` is validated.
@@ -538,6 +552,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.19.0 — a step refused at save on the demo** (`website/DEMO_MODE.md` §3.4, DM18). An event action's
optional `demoRefuses(params)`, asked at create, save and publish with `DEMO_MODE` on; one optional
member, so minor; see §1.1. Module-uo's `^1.10.0` still resolves.
**1.18.0 — the public demo** (`website/DEMO_MODE.md` §3.2, §3.4, §7.3). `demoLocks` in `module.json`,
`ctx.demoMode` and `api.onDemoSeed`; three optional additions, so minor; see §1.1. Module-uo's
`^1.10.0` still resolves.
@@ -981,7 +999,7 @@ 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; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0; a dry run's detail.warnings 1.16.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress, demoRefuses }]) // 1.10.0; progress 1.12.0; a param's duration 1.13.0; a param's multiple + max 1.14.0; a failure's retryAfterMs 1.15.0; a dry run's detail.warnings 1.16.0; demoRefuses 1.19.0
api.registerEventBudgets([{ id, label, unit }]) // 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
@@ -1396,6 +1414,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.
- **`demoRefuses(params)` is the demo's save-time lock, and it is not the only one** (1.19.0). Asked
only with `DEMO_MODE` on, at create, save and publish, with the step's checked params; a sentence
refuses the write `403 DEMO_LOCKED` and is shown as written, so phrase it for the person saving
(`"quit" is turned off on the demo site.`). It is a pure check: no database, no game, nothing changed.
A throw refuses. Because a definition can predate the list it is checked against, the module checks
again in `perform()` behind `ctx.demoMode`. It never leaves the process: the catalog strips it with
the other callables.
- **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