docs(runicnpc): stage 8 built and walked; MODULE_API 1.12.0 progress(), map.live's RunicNPC rows

- website/MODULE_API.md: 1.12.0, an event action's optional
  progress(): its envelope, its two answer shapes, and the rule that it
  answers for one reader and never fails loudly.
- website/EVENTS.md §I: a live occurrence's progress lines.
- rust-link/PROTOCOL.md §17.3: map.live's RunicNPC rows (name, key,
  boss, health, add) and the world layer's boss.
- runicnpc/PLAN.md: stage 8's design renamed live -> progress, and its
  "Built" section, with the branches, the tests and the walk on both
  rigs in the browser and on the emulator.

Also recorded in the plan: a core finding outside this stage. The
public page never shows a live run's phase label, because phaseLabel
looks phases up by id while specs key them by key.

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-06 08:23:21 -05:00
parent 6a87ce4d23
commit 941e4ae6e5
4 changed files with 112 additions and 11 deletions

View File

@@ -26,13 +26,37 @@ 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.11.0'
const MODULE_API_VERSION = '1.12.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.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.
- **What core does with it.** While a run is live, core asks each step whose action declares
`progress` how it stands, and publishes the answers on the public event as the live occurrence's
`progress`.
- **The envelope.** It carries the step's `runId`, `stepId`, `idempotencyKey`, `scope` and `params`.
It also carries `resources`, the step's own ledger rows that are still held, as `{ kind, ref,
payload }`. And it carries `viewer: { userId }`, the reader's account id or null, so the module
applies its own visibility settings.
- **The answer is one of two shapes, or `null`:** `{ label, left, of }` or `{ label, percent }`.
- `label` is at most 80 characters.
- `left` and `of` are integers, with `0 ≤ left ≤ of` and `of ≥ 1`.
- `percent` is 0 to 100, rounded.
- **Core interprets neither.** The page and the app render "label: left of of left" and
"label: percent%". Core never learns what a bandit is (D305).
- **Every failure is a line not shown, never an error.** A throw, an answer later than 2 s, or any
other shape is dropped and logged. Answers are cached per run and reader for 5 s, so a busy page
does not reach a game once per reader.
- **Module-uo is unaffected.** It declares `^1.10.0` and registers no action with it.
- **A module may declare it while still declaring an older `coreApi`.** An older core ignores the
unknown member, because the action shape is copied field by field, so the action still works with
no progress line.
**1.11.0 — `ctx.events.expired({ kind, ref })`** (`website/EVENTS.md` §L; Rust
`modules/rust/PLAN_FIXES.md` F14, D183). One addition and no removal, so minor. A game that ends
something at its own deadline — a Rust zone the plugin erases when its time is up — tells core, and core
@@ -367,6 +391,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.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.
**1.11.0 — `ctx.events.expired`** (`website/EVENTS.md` §L, Rust PLAN_FIXES D183). One addition, so
minor; see §1.1. The resource ledger gains an `expired` status, and nothing that existed changes
meaning — `expired` joins neither the statuses that hold a target nor the ones that leave a run's
@@ -778,7 +806,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 }]) // 1.10.0
api.registerEventActions([{ id, label, risk, reversible, cost, params, perform, revert, reconcile, progress }]) // 1.10.0; progress 1.12.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
@@ -1183,6 +1211,11 @@ rather than implementation and belong here:
actually happened"* for a verb whose answer is neither a resource nor a participant, and before
Phase 15 there was no such answer: `EVENTS.md` §H named the member, `classify()` had never read
one, and a module that used it wrote into nothing.
- **`progress()` is read by anybody, so it answers for one reader and never fails loudly** (1.12.0).
The envelope names the reader (`viewer.userId`, null for a stranger). A module whose own settings
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 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