Files
website/server/src/modules/version.js
wtclaude 769794d183
All checks were successful
PR Checks / bot-tests (pull_request) Successful in 48s
PR Checks / client-build (pull_request) Successful in 59s
PR Checks / server-tests (pull_request) Successful in 6m6s
feat(events): a failed step may say when to retry (MODULE_API 1.15.0)
A module's perform() may answer { ok: false, retry: true, retryAfterMs }
and the runner waits that long instead of the flat EVENT_STEP_RETRY_MS
(Rust PLAN_REDESIGNS §14.3.13, D371). Dangerous Treasures' warm-up is the
first user.

- dispatch.classify reads it on the retry shape only, clamped to 60 000 to
  1 800 000 ms; a value that is not a number is dropped and logged, and the
  step retries on the flat delay.
- It is one attempt of EVENT_STEP_MAX_ATTEMPTS and degrades the run like any
  retry. A run paused or cancelled in the wait is not picked up.
- Every step.retry log line now carries retryAt (and retryAfterMs when
  hinted); the run console reads "retrying at 20:10".
- MODULE_API 1.15.0, additions only; the engagement manifest is regenerated.
  Module-uo (^1.10.0) answers none of it and is unaffected.
- Test harness: resourcesDb.finishedRunsWithNothingLeft was never stubbed in
  eventRunner.test.js, so a tick that reached the cleanup sweep with a
  terminal run waited on the dead pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
2026-10-15 02:39:43 -05:00

213 lines
14 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// The module API version — the single number a module's `coreApi` range is
// checked against (docs/website/MODULE_API.md §1.1).
//
// Bump minor when a member is ADDED to ctx or a new register* call appears;
// major when one is removed, its signature changes, or its behaviour changes
// without a signature change. A core-internal refactor behind an unchanged
// member is not a bump.
//
// Deliberately separate from PROTOCOL_VERSION (which versions the shard wire and
// has nothing to say about a website module) and from any module's own version.
// 1.15.0 — a failed step may say when to try again: `{ ok: false, retry: true,
// retryAfterMs }` (Rust PLAN_REDESIGNS §14.3.13, D371; docs/website/EVENTS.md §F).
// Clamped to one minute–thirty minutes, never refused; absent, the flat
// `EVENT_STEP_RETRY_MS` as before. It is one attempt of `EVENT_STEP_MAX_ATTEMPTS`
// like any retry, and ignored beside `retry: false`. One optional envelope
// member, so minor. Module-uo declares `^1.10.0` and answers none: an envelope
// without it is classified exactly as it was. This took 1.15.0 ahead of
// STEAM_SIGNIN.md, which had claimed it and moves to the next minor.
//
// 1.14.0 —an action param may hold several values: `multiple: true`, with an
// optional `max` (1–100, 20 if left out), on a `string` or `int` param (Rust
// PLAN_REDESIGNS §12.3, D344; docs/website/EVENTS.md §F). The step stores a JSON
// array; a bare value is read as a list of one; `required` means at least one;
// `perform()` and `cost()` are handed the array. Refused on a duration and on a
// template's inputs. The editor draws it as a multi-select.
// One optional flag, so minor. Module-uo declares `^1.10.0` and uses none of it:
// a param that does not say `multiple` is checked, stored and drawn exactly as it
// was, and the declaration the registry hands back does not grow the key.
//
// 1.13.0 — the event step editor (Rust PLAN_REDESIGNS §2; docs/website/EVENTS.md
// §F, §I). Four additions, each optional:
// • `api.registerEventTemplates([{ id, label, description, inputs, build }])` —
// a named starting point for a new event; inputs in, an ordinary draft out,
// and nothing about the template kept in the definition (D192);
// • a param's `duration: 'minutes'`, so the editor can see a timed step and
// keep its phase open long enough for it;
// • a lease's `example` for its value, so a lease step shows that lease's
// example rather than core's one for every lease;
// • an option row's `note`, shown under the field while it is the chosen one.
// Additions only, so minor. Module-uo declares `^1.10.0` and uses none of them:
// its actions, leases and sources come out of the registry exactly as they went
// in, and its steps render with the new controls and nothing else changed.
//
// 1.12.0 — an event action may declare `progress(envelope)`: how a live run's
// step stands, for its public page (docs/website/EVENTS.md §I; RunicNPC D304,
// D305). Core accepts two shapes, `{ label, left, of }` and `{ label, percent }`,
// puts them in the public run as `progress` and renders them as text it does not
// interpret. Optional and additive, so minor: Module-uo declares `^1.10.0` and
// registers no action with it, so its actions and pages are unchanged.
//
// 1.11.0 — `ctx.events.expired({ kind, ref })`, and `expired` as a resource
// ledger status (docs/website/EVENTS.md §L; Rust PLAN_FIXES D183). A game that
// ends something at its own deadline — a Rust zone erased when its time is up —
// could until now reach core only through `ctx.events.reconcile()`, which files
// it `orphaned`: amber, "it vanished", and still claimable for a revert. The new
// call files it as the plan working: terminal, green, never taken back.
//
// Additions only, so minor, and checked against the one module already built on
// 1.10.0: Module-uo never calls it, reads no ledger status, and every existing
// status keeps its meaning — `expired` joins neither `HELD` nor `UNRESOLVED`, so
// the sweep, the manual retry and `cleanup_status` see exactly what they saw.
// 1.10.0 — the event contract opens to modules: `api.registerEventActions`,
// `api.registerEventBudgets`, `api.registerEventLeases` and
// `api.registerEventOptionSources` (docs/website/EVENTS.md §F, EVENTS_PLAN.md
// Phase 7). Four names, and only one of them is new machinery: the ACTION
// registry has staged core's `core.announce`, `core.wait` and `core.cue` on every
// boot since Events Phase 1, and `loader.js` simply had no method that delegated
// to it. What Phase 7 adds is the facade, the three declarations beside it, and
// the fail-closed rule they exist for — a `cost()` naming a dimension no module
// registered is REFUSED at save, at the dry run and at dispatch, so a module
// cannot spend a budget it did not declare.
//
// Additions only, so minor: a module written against 1.9.0 registers no actions
// and the deployment simply has fewer verbs an event can use. That is §F's own
// posture stated as a version rule — core with no module installed is still an
// event engine that can announce, wait, cue a human and publish results.
//
// **A lease is DECLARED here and acquired by nothing.** Core owns a lease's
// duration and its conflict check, and both live in the resource ledger, which is
// Phase 8's. It is in 1.10.0 rather than in 1.11.0 so that the module contract is
// one version a module author reads once, not two.
//
// 1.9.0 - a sixth registration call: `api.registerEngagementSeeds({ templates,
// ruleGroups })` (docs/website/ENGAGEMENT.md Phase 11b, decision 7). A module
// could declare a trigger from 1.7.0 and could never say what the mail should
// READ like: `templateSeeds.js` and `coreRules.js` are core files with core
// arrays in them, so a module's notification was core's generic body or nothing.
// Additions only, so minor: every module written against 1.8.0 keeps working and
// simply seeds nothing.
//
// **What a module has to know about it beyond the new name**, because the two
// halves behave differently on purpose:
//
// - **Templates are re-ensured on every boot**, under `seed_key` /
// `seed_version` / `customized` - so bumping a body's `seedVersion` reaches
// every deployment except the ones where an operator edited that row, and a
// template added in a later module version reaches everyone.
// - **Rules are one-shot, per named GROUP.** Re-ensuring one would resurrect a
// rule an operator deleted and reset one they enabled, so each group carries
// its own settings guard. A rule appended to an existing group therefore
// reaches FRESH INSTALLS ONLY; one that must reach deployments already
// stamped takes a new group key. That is 11a's seed-key finding as an API
// rather than as a warning, and the module makes the choice knowingly.
//
// Two things it deliberately does not permit. A seeded rule is always
// `enabled = 0` - it is not a parameter - which is Q3's invariant surviving
// contact with the largest seed set in the workstream. And a module may not mark
// a template `protected`: that flag means "the system breaks without this body",
// which is true of a password reset and of nothing a module ships, and a module
// setting it would take an operator's delete button away.
//
// It runs from `modules/lifecycle.js` `boot()` rather than `seedDefaults()`, and
// that is forced rather than chosen: core seeds before `app.js` is required, and
// requiring `app.js` is what runs the loader.
// 1.8.0 - a seventh value in the audience ceiling lattice: `admin`, a child of
// `staff` (docs/website/ENGAGEMENT.md Phase 11, decision 1). A module may now
// declare `ceiling: 'admin'` on a trigger or an audience, so the set of values
// `registerEventTriggers` and `registerAudiences` accept grew. Additions only,
// so minor: every declaration valid before is valid now, no stored value
// changes, and module-uo's `coreApi: "^1.3.0"` still resolves.
//
// It exists because Phase 11's operator-facing triggers - `uo.audit.staff_action`,
// `uo.economy.milestone`, `uo.world.saved` - are described everywhere as
// admin-audience, and the narrowest value the lattice had was `staff`, which
// means admin / editor / moderator. Ceilinging them there would have permitted a
// rule that mails the staff audit digest to every editor.
//
// **What a module has to know about it beyond the new name.** `admin` is the one
// pair in the tree with real containment - every admin is staff - so it is the
// only place `permits` is true between two non-`authenticated` values:
// `permits('staff', 'admin')` holds and nothing else of that shape does. A
// trigger ceilinged `staff` therefore accepts an `admin` audience, which is the
// intended narrowing, and the reverse is refused as it should be.
// 1.7.0 — the engagement contract (docs/website/ENGAGEMENT.md Phase 2).
// Additions only, so minor: `api.registerEventTriggers([...])`,
// `api.registerAudiences([...])`, `ctx.events.emit(triggerId, envelope)` and
// `ctx.inbox.push(userId, item)`. module-uo's `coreApi: "^1.3.0"` still resolves.
//
// **As in 1.6.0, the number covered the whole surface and the members arrived by
// phase, and all of them have now arrived.** `ctx.inbox.push` threw until Phase 7
// built the in-app channel and the table behind it — the same choice, for the
// same reason: a member of 1.7.0 that were absent would have made the version a
// lie, and one that silently accepted data into a table that did not exist would
// have been worse than either. **Filling it in is NOT a bump**: the signature is
// the one 1.7.0 declared, and a module written against it needs no change. What a
// module WILL see differently is the throw becoming a write, which is the whole
// point of the phase.
//
// One thing here is not a member and is still part of the contract: a trigger id
// and a notification-stream id share ONE namespace (ENGAGEMENT.md §7.2, settled
// by the org lead in Phase 2). An id has exactly one owner across both facets,
// so a module cannot attach a payload contract to another module's stream. That
// tightens a rule rather than changing a signature, and nothing registrable
// before this bump becomes unregistrable after it — the id grammar was RELAXED
// in the same change (`_` is now legal inside a segment).
//
// 1.6.0 — the Team surface (docs/website/TEAMS.md Part 11). Additions only, so
// minor: `api.registerTeamProvider({ getTeams, getTeamMembers, getTeamLeaders })`,
// `ctx.teams.publish(event)`, `ctx.teams.reconcile({ reason })`,
// `ctx.teams.activity.push(items)`, `api.registerSlashCommands([...])`, and the
// client slots `team.overview` / `team.member.row`. module-uo's `coreApi:
// "^1.3.0"` still resolves.
//
// **The number covers the whole surface; the members arrived by phase, and all of
// them have now arrived.** `activity.push` landed with the Team activity feed
// (§4, phase 3) and `registerSlashCommands` with the Discord commands (§7.1,
// phase 7); until each did, it was present and THREW rather than being absent or,
// worse, silently accepting data into a table that did not exist. Nothing in
// 1.6.0 throws any more.
//
// 1.5.0 — a CLIENT addition: `PublicLayout` takes an optional `shell` prop that
// renders the page body wrapper core's own pages write by hand (MODULE_API.md
// §3.4). Minor, not major: §3.4 makes *changing* a kit component's props a major
// bump because that breaks a call already written, and adding an optional one
// breaks nothing — omitting `shell` is 1.4.0's behaviour exactly. Nothing on the
// server changed; this file bumps for the reason below. Found by the Integration
// Kit's acceptance run (docs/modules/kit-acceptance.md), where a module built
// exactly as the kit teaches rendered outside the site's page column.
//
// 1.4.0 — no member changed. §2.7 gained one prohibition: a module does not open
// a connection to a game server from the website process; it talks to a sidecar,
// which owns the durable copy of the game's state. Minor rather than major
// because the SURFACE is identical to 1.3.0 — module-uo's `coreApi: "^1.3.0"`
// still resolves, and it already complies — but a module written against 1.3.0
// could satisfy every member and still be built the wrong way round, which is
// what this number now says. The one §2.7 rule with no CI behind it: an outbound
// socket is not statically detectable the way an internal require is.
//
// 1.3.0 — three CLIENT additions from Phase 3 slice 3: a nav item may carry an
// `icon`, core declares a `player.invite.accepted` slot, and `window.__rg.api`
// gained `BASE` (which §3.5 always specified and shared.js never published).
// Nothing on the server changed; this file bumps for the reason below.
//
// 1.2.0 — the CLIENT registry gained `registerExtension` and core gained client
// extension slots (MODULE_API.md §3.7): the twin of this half's declareSlot /
// registerExtension, for module content inside a core *page* rather than under a
// core route prefix. Nothing on the server changed, and this file bumps anyway —
// the two halves state ONE version, because a module declares a single `coreApi`
// range and is served one chunk (client/src/modules/version.js).
//
// 1.1.0 — `ctx` gained `activity.log`, `users.getById` and `site.baseUrl`, each
// because module-uo's extraction needed it and none of them could be vendored:
// an admin action a module performs belongs in core's one audit log, the
// extension slot needs the user its prefix names, and §2.7 forbids a module
// reading core's `APP_BASE_URL` for itself. Additions only, so minor.
const MODULE_API_VERSION = '1.15.0'
module.exports = { MODULE_API_VERSION }