docs(events): Phase 7 as built -- the module contract at MODULE_API 1.10.0
Code: RunicGateway/website#189. MODULE_API.md gains 1.10.0 in three places: the number itself, the entry in Part 1 with the four call shapes and the six rules that come with them, and the four names in 2.4's call list with the contract-rather-than- implementation notes beside them. EVENTS.md F is marked built, with a new subsection recording what the build settled -- the open unit vocabulary, the undeclared-dimension refusal and why it has its own code, why example-pricing survives the arrival of a budget registry, why restore cannot be read, and why an option source that refuses answers 200. K's Phase 7 note becomes what shipped. The API surface table's option-source row is filled in, and the absent-routes list is down to cleanup. EVENTS_PLAN.md: Phase 7 complete, in the shape phases 0-6 use -- the four org-lead decisions, the three things the build settled, the throwaway-module proof, the React defect only the browser could find, and the route the docs caught the code building one segment shallower than this file specifies. Checked by hand (docs has no CI): every anchor resolves, and every route, identifier and file path named here exists on website#189. Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6t8mrAWhZU5vnyYgZTMtL
This commit is contained in:
@@ -26,13 +26,93 @@ 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.9.0'
|
||||
const MODULE_API_VERSION = '1.10.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.10.0 — the event contract opens to modules: `api.registerEventActions(...)`,
|
||||
`api.registerEventBudgets(...)`, `api.registerEventLeases(...)` and
|
||||
`api.registerEventOptionSources(...)`** (`website/EVENTS.md` §F, `EVENTS_PLAN.md` Phase 7). Four
|
||||
additions and no removal, so minor; a module written against 1.9.0 registers no actions and its
|
||||
deployment simply has fewer verbs an event can use — which is §F's own posture stated as a version
|
||||
rule, because core with none of this installed is still an event engine that can announce, wait, cue
|
||||
a human and publish results.
|
||||
|
||||
**Only one of the four is new machinery.** The ACTION registry has staged core's `core.announce`,
|
||||
`core.wait` and `core.cue` on every boot since Events Phase 1; what it never had was a way in —
|
||||
`loader.js` built its own `api` facade and had no method that delegated to it. So the seam a module
|
||||
now reaches is one that has been exercised on every boot for six phases, rather than one whose first
|
||||
registrant is a stranger. That is the same argument `registerCore()` has made since the module
|
||||
system's Phase 3, and 1.10.0 is when it pays.
|
||||
|
||||
```js
|
||||
api.registerEventBudgets([
|
||||
{ id: 'uo.creatures', label: 'Creatures spawned', unit: 'count' }, // a DIMENSION core can bound
|
||||
])
|
||||
|
||||
api.registerEventActions([{
|
||||
id: 'uo.creature.spawn', // <moduleId>.-prefixed; its OWN id space
|
||||
label: 'Spawn creatures',
|
||||
risk: 'change', // closed: notify | inspect | change | irreversible
|
||||
reversible: 'ledger', // closed: none | self | ledger | override
|
||||
version: 1,
|
||||
budgetMs: 10000,
|
||||
cost: (p) => ({ 'uo.creatures': p.count }), // what ONE invocation consumes
|
||||
params: [
|
||||
{ name: 'creature', type: 'string', required: true,
|
||||
example: 'Orc', source: 'uo.options.creatures' }, // a `source` makes it a dropdown
|
||||
{ name: 'count', type: 'int', required: true, example: 12 },
|
||||
],
|
||||
async perform({ runId, stepId, idempotencyKey, scope, params, actor, verify }) {
|
||||
if (verify) return { ok: true } // dry run: validate, change NOTHING
|
||||
return { ok: true, resources: [{ kind: 'creature', ref: '0x40001234' }] }
|
||||
},
|
||||
async revert({ runId, resources, idempotencyKey }) { return { ok: true } }, // iff reversible: 'ledger'
|
||||
}])
|
||||
|
||||
api.registerEventOptionSources([{
|
||||
id: 'uo.options.creatures', label: 'Creatures',
|
||||
async resolve() { return [{ value: 'Orc', label: 'Orc', group: 'Humanoid' }] },
|
||||
}])
|
||||
|
||||
api.registerEventLeases([{ // DECLARED here; acquired by nothing yet
|
||||
id: 'uo.rate.skillgain', label: 'Skill gain rate',
|
||||
type: 'float', min: 0.5, max: 5, maxDurationMs: 86400000,
|
||||
async read() { return { ok: true, value: 1.0 } },
|
||||
async apply(v, until) { return { ok: true } },
|
||||
async restore(baseline, { expected }) { return { ok: true } },
|
||||
}])
|
||||
```
|
||||
|
||||
**What a module author has to know beyond the four names**, because each is a rule rather than a
|
||||
field:
|
||||
|
||||
- **No shape a failure can take reads as success.** A rejected promise, a throw, a `budgetMs`
|
||||
timeout, a non-object and a missing `ok` are all `{ ok: false, retry: true }`. That is
|
||||
`registerTeamProvider`'s default *inverted*, deliberately: a team provider that refuses leaves core
|
||||
showing what it had, because staleness is cheap, whereas an action that half-ran and was recorded
|
||||
as done is a world change nothing will ever come back for. `retry` is opted OUT of — a module that
|
||||
means "this will never work" says `retry: false`.
|
||||
- **A module cannot spend a budget it did not declare.** A `cost()` naming a dimension no module
|
||||
registered is REFUSED — at save, at the dry run and at dispatch, with its own refusal code, because
|
||||
the fix is a module's declaration and not a deployment's cap. Declaring a dimension is not the same
|
||||
as bounding it: a declared dimension with no operator cap is counted and unbounded.
|
||||
- **`verify: true` must change nothing and must answer honestly.** It rides the same dispatcher a
|
||||
real run uses, because a dry run down a second code path is a dry run of the second path.
|
||||
- **`example` is required on every param, optional ones included** — the same rule
|
||||
`registerEventTriggers` makes of a variable's example, for the same reason: it is the authoring
|
||||
form's placeholder, it is one word at declaration time, and it is unreconstructable afterwards.
|
||||
- **Actions, budgets, leases and option sources are four separate id spaces**, each namespaced
|
||||
`<moduleId>.`. An action names a VERB, a budget a RESOURCE, a lease a VALUE and an option source a
|
||||
CATALOG, so `uo.creatures` may legitimately appear in more than one — reading that as a collision
|
||||
would forbid the most natural set of names a module will ever write.
|
||||
- **A lease is DECLARED at 1.10.0 and acquired by nothing.** Core owns a lease's duration and its
|
||||
conflict check, and both live in the resource ledger, which is `EVENTS_PLAN.md` Phase 8. It is in
|
||||
this version rather than the next so the module contract is one version an author reads once.
|
||||
|
||||
**1.9.0 — a module may ship its own message bodies and rules: `api.registerEngagementSeeds(...)`**
|
||||
(`website/ENGAGEMENT.md` Phase 11b, decision 7). One addition and no removal, so minor; a module
|
||||
written against 1.8.0 keeps working and simply seeds nothing.
|
||||
@@ -142,6 +222,13 @@ 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.10.0 — the event contract** (`website/EVENTS.md` §F). Four additions, no removals and no changed
|
||||
signature, so minor; `module-uo`'s `coreApi: "^1.9.0"` still resolves and it registers no actions
|
||||
until `EVENTS_PLAN.md` Phase 9. `api.registerEventActions([...])`, `api.registerEventBudgets([...])`,
|
||||
`api.registerEventLeases([...])` and `api.registerEventOptionSources([...])` (§2.4). Nothing was added
|
||||
to `ctx`: an action is called BY core, so what a module needs from this contract it is handed in the
|
||||
envelope rather than reaching for.
|
||||
|
||||
**1.7.0 — the engagement contract** (`website/ENGAGEMENT.md` Phase 2). Four additions, no removals
|
||||
and no changed signature, so minor; `module-uo`'s `coreApi: "^1.3.0"` still resolves.
|
||||
`api.registerEventTriggers([...])`, `api.registerAudiences([...])` and
|
||||
@@ -489,6 +576,10 @@ 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 }]) // 1.10.0
|
||||
api.registerEventBudgets([{ id, label, unit }]) // 1.10.0
|
||||
api.registerEventLeases([{ id, label, type, min, max, maxDurationMs, read, apply, restore }]) // 1.10.0
|
||||
api.registerEventOptionSources([{ id, label, resolve }]) // 1.10.0
|
||||
api.onBoot(async (ctx) => {})
|
||||
api.onShutdown(async () => {})
|
||||
```
|
||||
@@ -861,6 +952,36 @@ are in §1.1 under **1.9.0**; four things are contract rather than implementatio
|
||||
in `channels`. Every other key must be one of the rule's channels.
|
||||
- **It is not a send path.** Every value on the object is data. Core still decides who is told.
|
||||
|
||||
**`registerEventActions([...])` / `registerEventBudgets([...])` / `registerEventLeases([...])` /
|
||||
`registerEventOptionSources([...])`** (1.10.0) are the event contract (`EVENTS.md` §F). The full
|
||||
shapes and the six rules that come with them are in §1.1 under **1.10.0**; four things are contract
|
||||
rather than implementation and belong here:
|
||||
|
||||
- **An action is core CALLING THE MODULE**, like `registerTeamProvider` and `registerAnnounceLeg`'s
|
||||
dispatch, and unlike everything above them — but from further away than either, because the thing
|
||||
on the other end may be a shard. That is why `budgetMs` is declared per action and enforced by the
|
||||
dispatcher: without it a `perform()` awaiting a socket that never answers holds a step's claim
|
||||
until its lease expires, and the reclaim then re-dispatches it, which is how one wedged sidecar
|
||||
becomes an infinite loop rather than a failed step.
|
||||
- **`once`, on all four.** A batch is a module's complete statement about what it declares; a second
|
||||
call is a module changing its mind halfway through `register()` rather than adding to it. And they
|
||||
STAGE, like every registration above: a module that registers two budgets and then throws has left
|
||||
nothing behind.
|
||||
- **Everything is optional, and §F says so once because it governs every member.** A module may
|
||||
register no actions, no budgets, no leases and no option sources. Each registration *adds* what an
|
||||
author can reach for; a module that omits one costs its deployment a capability rather than a boot,
|
||||
exactly as a module with no `onBoot` still reaches `started`.
|
||||
- **An option source that refuses degrades its field to free text with a warning.** It never blocks
|
||||
the authoring form and it never raises. The alternative is a screen a module's outage can take
|
||||
away, for a field whose value the operator very often already knows — which is a worse failure than
|
||||
the typo the dropdown exists to prevent.
|
||||
|
||||
**An action whose module is uninstalled goes dormant, never an error.** A step already in a saved
|
||||
spec keeps it and a new step may not add one — the shape `engagement_rules` established for a dormant
|
||||
trigger — and a dormant step blocks the PUBLISH, because a version is what a run pins and a run
|
||||
cannot dispatch a verb nobody registers. A step that reaches dispatch naming one fails `terminal`
|
||||
with the module named and the run degrades: never a silent skip.
|
||||
|
||||
**`onBoot(fn)` / `onShutdown(fn)`** — §2.5.
|
||||
|
||||
### 2.5 Lifecycle
|
||||
|
||||
Reference in New Issue
Block a user