feat(events): targeted leases, value sets and searchable sources (Phase 12b)
Some checks failed
PR Checks / client-build (pull_request) Successful in 40s
PR Checks / server-tests (pull_request) Failing after 5m46s
PR Checks / bot-tests (pull_request) Successful in 8m28s

The core half of Phase 12b, and the half Phase 12a did not need. A targeted
lease is a shape `core.lease` did not have.

Every lease before this named a SINGLE value, so the lease id WAS the target and
none of the four callables took one. `Spawner.MaxCount` is not that shape: it is
one capability over thousands of spawners, and a reservation on the id alone
would let one run turning up one spawner refuse every other run every other
spawner. So a lease may declare a `target`, the callables are handed it, and the
ledger ref becomes `<lease id>#<target>` -- which puts the two-events-one-target
refusal at the granularity the world actually has while leaving it coming from
the same unique index it always did.

Extending core rather than giving the module a lease verb of its own is what §F
decided in Phase 8 ("the verb is core's"): a lease verb per module would
re-implement `maxDurationMs` and the conflict check once per module, advisory
everywhere and wrong in the first one that forgot. Half that objection no longer
holds -- the target check comes free from the index whichever verb reserves the
row -- and the other half still does.

Three readers of a lease ref, not one. `cleanup.restoreLease` and
`ledger.normalise` both looked a lease up by the whole `row.ref`, and both were
correct for exactly as long as a ref was a bare id. Left alone, a targeted row
would have missed in both -- cleanup reporting "no module registers the lease"
and refusing to restore a world that really was changed, which is the worst
failure this table has. All three now go through `eventLeaseForRef`.

`values` closes a `string` lease's set. `min`/`max` bound the numeric types and
nothing bounded `string`, so the only check on a string lease's value was the
game side's -- a refusal arriving unattended, mid-run, from a step nobody is
watching. Refused on any other type: a set beside `min`/`max` would be a second
bound with no rule about which wins.

Option sources become searchable, and the first one that needed it forced this
phase's shape. `resolveOptionSource(id)` took no argument and every source
answered a flat list bounded at 2,000; module-uo's spawner target is 6,707 spawn
points, so a flat list would have dropped two thirds of the world and said
nothing about which two thirds -- the failure 12a named for decoration, arriving
for real. `resolve({ q })` is additive: every source is passed a term, none is
required to read one, and a `searchable` flag says which do, because inferring it
from a truncated answer reads correctly right up until a small deployment's list
happens to fit.

`MODULE_API_VERSION` stays 1.10.0, amended IN PLACE (org lead, 2026-09-07) -- the
shape every phase since P10 has used while this workstream sits on `edge`.

The swagger regeneration carries one incidental change: the committed spec said
the session cookie is `rg_rig`, which is neither the documented default nor what
this repo's own `server/.env` sets. It was generated somewhere with that env var
set. The regeneration corrects it to `rg_token`.

2010 pass, 0 fail (89 DB-skipped), with `modules/uo` parked as the core suite
requires. Six new tests cover the targeted-lease shape, both refusal directions,
the value set, and the search term.

Refs: docs/link/v7.md §11, docs/website/MODULE_API.md, EVENTS_PLAN.md Phase 12b

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
This commit is contained in:
2026-09-07 08:06:37 -05:00
parent d0178c6419
commit 37f4623068
9 changed files with 512 additions and 21 deletions

View File

@@ -396,6 +396,56 @@ test('a lease whose module is uninstalled is unresolved, never assumed restored'
assert.match([...store.rows.values()][0].last_error, /no module registers the lease "demo.rate"/)
})
test('a targeted lease is restored through the lease its ref NAMES, target in hand', async () => {
// Phase 12b. A targeted row's ref is `<lease id>#<target>`, and both readers of
// one — this sweep and `ledger.normalise` — looked their lease up by the whole
// string. Left alone, every property lease would have come back "no module
// registers the lease", which is the worst answer this table has: it leaves a
// spawner turned up for good AND blames an uninstalled module for it.
let seen = null
registerLease({
id: 'demo.spawner.count',
type: 'int',
min: 0,
max: 50,
target: { label: 'Which spawner' },
restore: async (baseline, opts) => { seen = { baseline, opts }; return { ok: true } },
})
addResource({
kind: 'override',
ref: 'demo.spawner.count#003f11b8-9bfa',
step_id: null,
payload: { baseline: 3, applied: 30 },
})
const summary = await cleanup.cleanupRun(RUN)
assert.equal(summary.reverted, 1)
assert.equal(seen.baseline, 3)
assert.equal(seen.opts.expected, 30)
// The half that makes the restore land on the right object rather than on some
// other run's spawner: the module is handed the target, not asked to parse the
// ref core composed.
assert.equal(seen.opts.target, '003f11b8-9bfa')
})
test('an unregistered TARGETED lease still names itself, ref and all', async () => {
// The fail-closed direction is unchanged by the parser: a row whose module is
// gone stays unresolved with the reason on it, and the reason quotes the ref
// the operator will actually see in the console rather than the bare id.
addResource({
kind: 'override',
ref: 'demo.spawner.count#003f11b8-9bfa',
step_id: null,
payload: { baseline: 3, applied: 30 },
})
const summary = await cleanup.cleanupRun(RUN)
assert.equal(summary.failed, 1)
assert.match(
[...store.rows.values()][0].last_error,
/no module registers the lease "demo\.spawner\.count#003f11b8-9bfa"/,
)
})
// ── The sweep ──────────────────────────────────────────────────────────────
test('the sweep only touches TERMINAL runs', async () => {