feat(rust): the world verbs, their budgets and the reconcile watch (phase 13a, protocol 9) #15

Merged
whitlocktech merged 3 commits from feat/phase-13a-world into edge 2026-09-24 10:11:04 +00:00
Member

Phase 13a (docs/modules/rust/PLAN.md §28, decisions D80–D97).

  • registerEventActions: rust.zone.open, rust.crate.place and rust.npc.place. All are reversible: 'ledger' with revert() and reconcile(), and budgetMs is 15 000, above the client's 12 s. A test asserts the order.
    • One placing verb per kind (D97). Core infers an action's cap boxes by pricing its examples once, so a single verb could only ever show the crates cap.
  • registerEventBudgets: rust.prefabs, rust.npcs and rust.zone.minutes, each declared beside the verb that spends it.
  • Option sources: rust.options.monuments (live, searchable; kind plus instance, D93) and rust.options.crates / .npcs (mirrored, so they answer with every server off). They join the lease sources in the one batch core accepts.
  • A location is a monument (which names its server) or coordinates plus a server, exactly one. The bounds are mirrored, so a bad step is refused in the dry run. Zone minutes are required (D96).
  • Refs are <serverId>:<id>, because revert and reconcile get no params.
    • The undo sends no idempotency key, which was module-uo's teardown mistake.
    • A lost answer is reverted by its key on every server.
    • reconcile asks the plugin. A server that cannot be asked keeps its rows.
  • The watch: a changed bootId or wipeId calls ctx.events.reconcile(), but only once the plugin's hello says worldReady.
  • The permission mirror keeps the plugin's notLanded grants out of what it records as pushed, and the admin page says so (D85).

Two defects a mid-run restart found, fixed

  • The watch reconciled before the game had loaded its save, and crates were orphaned while still standing (§28.7).
  • revert() read a 200 carrying world.error as success.

Tests: 279 server and 41 client pass. check:imports, check:bundle (with eventWorld.js added), check:swagger, check:engagement and check:externals are clean. No route or swagger change.

Walked through real core on both rigs; see §28.6.

  • AI-assisted: Claude Code (Claude Opus 5.5)

🤖 Generated with Claude Code

https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY

Phase 13a (`docs/modules/rust/PLAN.md` §28, decisions D80–D97). - **`registerEventActions`**: `rust.zone.open`, `rust.crate.place` and `rust.npc.place`. All are `reversible: 'ledger'` with `revert()` and `reconcile()`, and `budgetMs` is 15 000, above the client's 12 s. A test asserts the order. - **One placing verb per kind (D97).** Core infers an action's cap boxes by pricing its examples once, so a single verb could only ever show the crates cap. - **`registerEventBudgets`**: `rust.prefabs`, `rust.npcs` and `rust.zone.minutes`, each declared beside the verb that spends it. - **Option sources**: `rust.options.monuments` (live, searchable; kind plus instance, D93) and `rust.options.crates` / `.npcs` (mirrored, so they answer with every server off). They join the lease sources in the one batch core accepts. - **A location is a monument (which names its server) or coordinates plus a server, exactly one.** The bounds are mirrored, so a bad step is refused in the dry run. Zone minutes are required (D96). - **Refs are `<serverId>:<id>`**, because `revert` and `reconcile` get no params. - The undo sends no idempotency key, which was module-uo's teardown mistake. - A lost answer is reverted by its key on every server. - `reconcile` asks the plugin. A server that cannot be asked keeps its rows. - **The watch**: a changed `bootId` or `wipeId` calls `ctx.events.reconcile()`, but only once the plugin's hello says `worldReady`. - **The permission mirror** keeps the plugin's `notLanded` grants out of what it records as pushed, and the admin page says so (D85). ## Two defects a mid-run restart found, fixed - The watch reconciled before the game had loaded its save, and crates were orphaned while still standing (§28.7). - `revert()` read a 200 carrying `world.error` as success. Tests: 279 server and 41 client pass. `check:imports`, `check:bundle` (with `eventWorld.js` added), `check:swagger`, `check:engagement` and `check:externals` are clean. No route or swagger change. Walked through real core on both rigs; see §28.6. - [x] AI-assisted: Claude Code (Claude Opus 5.5) 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
wtclaude added 3 commits 2026-09-24 08:16:16 +00:00
- registerEventActions: rust.zone.open and rust.prefab.place, both
  reversible 'ledger' with revert() and reconcile(), budgetMs 15000 above the
  client's 12 s. A location is a monument (kind + instance, carrying its
  server) or raw coordinates, exactly one (D87, D93); bounds mirrored from the
  plugin so a bad step is refused on the form (D95); zone minutes required and
  held by the game (D96).
- registerEventBudgets: rust.prefabs, rust.npcs and rust.zone.minutes, each
  beside the verb that spends it (D79, D89).
- Option sources rust.options.monuments (live, searchable) and
  rust.options.prefabs (mirrored, answers with every server off), registered in
  the one batch core accepts alongside the lease sources.
- Refs are <serverId>:<id>, since revert and reconcile get no params. The undo
  sends no idempotency key; a lost answer is reverted by key on every server.
  reconcile asks the plugin, and a server that cannot be asked keeps its rows.
- The refresh's bootId/wipeId watch calls ctx.events.reconcile() on a restart
  or a wipe, never on a first sighting or a reconnect (§11.1).
- The permission mirror keeps the plugin's new notLanded grants out of what it
  records as pushed, and the admin page says so (D85).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
Core learns which caps an action accepts by pricing its declared examples
once, and drops a dimension priced at zero. A single rust.prefab.place whose
cost moved between rust.prefabs and rust.npcs by its prefab param could only
ever show the crates cap, so D89's separate dial for fights was unreachable.

Two verbs, each pricing exactly one dimension, with the prefab source split
to match (rust.options.crates / rust.options.npcs). The switchboard can now
allow crates and leave NPCs off. The plugin's world.place is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
fix(rust): hold the reconcile until the world is loaded, and never read a refusal as a revert
All checks were successful
PR Checks / client-build (pull_request) Successful in 20s
PR Checks / server-tests (pull_request) Successful in 25s
PR Checks / frozen-manifest (pull_request) Successful in -1m10s
ba636c8939
Two defects the phase 13a walk found by restarting the rig mid-run:

- The watch asked core to reconcile the moment a new boot id appeared, which
  is before the game has loaded its save — every crate looked gone and was
  orphaned. It now waits for the plugin's hello to say `worldReady`; an older
  plugin that never says is taken as ready.
- revert() read any 200 as success. On this bridge a refusal is a 200
  carrying world.error (`not-ready` while loading), so every row would have
  been marked reverted with the game still holding every crate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
whitlocktech merged commit 9753dda4af into edge 2026-09-24 10:11:04 +00:00
whitlocktech deleted branch feat/phase-13a-world 2026-09-24 10:11:05 +00:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RunicGateway/Module-Rust#15
No description provided.