docs(link): protocol 7 part b -- what an event borrows, and the one-shots

`link/v7.md` gains §11-§14: the two targeted lease planes, the two one-shots, the
routes, and what the build found in already-merged code. `EVENTS.md` §G's five
part-b rows are marked built, three of them carrying a correction. `MODULE_API.md`
records the three contract members 12b amends into 1.10.0. `EVENTS_PLAN.md` has
Phase 12b as built.

THE DESIGN POINT, WRITTEN DOWN

A borrowed value whose home is the world save does not come back on a restart the
way a config value does. 11b's fail-safe -- a lease that never reaches disk makes
a restart a free restore -- depends entirely on the leased value being
memory-only too, and for a spawner property or a seasonal status it is not: a
restart preserves the CHANGE and destroys only the timer that would have undone
it. So those two planes' holds are persisted and their deadlines re-armed, and
the config plane's still are not. The same argument, applied where its premise is
false.

FIVE CORRECTIONS TO EVENTS.md

- `Spawner.Amount` does not exist. The property is `MaxCount`, and
  `MinDelay`/`MaxDelay` are TimeSpans, so the wire carries seconds.
- The seasonal toggle is not "small and safe". Safe, yes -- ServUO does it to
  itself from a staff gump -- but `OnStatusChange()` generates or removes world
  content for six of the eight permitted types.
- It is a THREE-value enum over nine named events, not a nine-value enum.
  (Caught in 12a's survey; the row is corrected here now it is built.)
- `TreasuresOfTokuno` is excluded, because `IsActive()` reads its own `DropEra`
  rather than `Status`. A lease on it applies cleanly, reads back, restores
  cleanly and changes nothing -- §N10's "capability that lies", and the one
  instance no runtime probe can catch.
- The grant row said failure aborts rather than retries. Protocol 6 changed that:
  an idempotency key means a repeat is answered by the original reply, so a
  retried grant cannot be one winner receiving two.

And the config-lease row is closed at one key. Counted on ServUO 57.4: 156
non-Bridge `Config.Get` call sites, 82 outside a field declaration, all but four
of those inside a `Configure()` or static constructor and cached at boot anyway.
The self-check ships regardless -- it exists for the operator whose OWN scripts
read config live.

MODULE_API 1.10.0, AMENDED IN PLACE

`target` on a lease declaration, `values` on a string lease, and `searchable` +
`{ q }` on option sources. Amended rather than bumped for the reason every phase
since P10 has: 1.10.0 has never reached `main`, so no deployment can tell the
difference, and the cutover is what publishes the whole of it.

The `target` entry records why this extends `core.lease` rather than giving the
module a lease verb of its own: §F settled that in Phase 8, and half its
objection no longer holds (the two-events-one-target refusal comes from the
ledger's unique index whichever verb reserves the row) while the other half --
`maxDurationMs` re-implemented per module -- still does.

§14 records the protocol-pin defect 11a and 12a both shipped, and why the test
that guards it passed anyway: it asserts the three declarations agree with each
other, which all three being equally stale satisfies.

CHECKS

`docs` has no CI. Every relative link in the new sections was resolved by hand;
the files are CRLF in the working tree and each diff is content-sized
(`--numstat` matches the real change), so nothing carries the `\r\r\n` full-file
rewrite.

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:08:35 -05:00
parent 8415581a72
commit 4169a0df83
4 changed files with 450 additions and 20 deletions

View File

@@ -1440,6 +1440,8 @@ anywhere in it.
#### Phase 12a — what an event OWNS
**Built and merged to `edge`**`servuo-plugins#23`, `link#38`, `Module-uo#31`, `docs#221`.
Five verbs that put something in the world, ledger its serial, and delete it at teardown.
| Verb | Cap dimension | Author's action |
@@ -1540,6 +1542,76 @@ caller in the whole tree.
three-state toggle over nine named events, which is a different form to author and a different one
to cap.
**Built**`website#—`, `servuo-plugins#—`, `link#—`, `Module-uo#—`, `docs#—`, all onto `edge`.
Spec of record: [`../link/v7.md`](../link/v7.md) §11§14.
**It is FIVE repos, and `website` is the one 12a did not need** (org lead, 2026-09-07: *"you can do
the 5 repos and no API bump since it is still on edge"*). A targeted lease is a shape `core.lease`
did not have. Every lease before it named a single value, so the lease id WAS the target and none of
the four callables took one; `Spawner.MaxCount` 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 the declaration gains a `target`, the callables gain it, and the ledger ref becomes
`<lease id>#<target>` — which puts the two-events-one-target refusal at the granularity the world
actually has, and leaves 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 already decided**
(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 of
that objection no longer holds — the target check comes free from the index whichever verb reserves
the row — and the other half still does. **`MODULE_API_VERSION` stays 1.10.0, amended in place**, the
shape every phase since P10 has used while this workstream sits on `edge`.
Two more contract members came with it, both forced by this phase rather than chosen: **`values` on a
`string` lease** (the seasonal status is a three-value enum and nothing bounded `string`, so its only
check was the game side's — a refusal arriving unattended, mid-run) and **searchable option sources**
(see below).
**The spawner dropdown was the phase's one genuine blocker, and the answer was a core change** (org
lead, 2026-09-07). `resolveOptionSource(id)` took no argument and every source answered a flat list
bounded at 2,000. This tree has **6,707 spawn points**, so a flat list would have dropped two thirds
of the world and said nothing about which two thirds — the exact 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. The atlas also keeps `<UniqueId>` again (parser
version 4), because it is the only name for one particular spawner that exists off the shard — a
serial is assigned when the world is built, so a lease addressable only by serial could have had no
dropdown at all.
**Three plan assumptions the tree disproved.**
- **`Spawner.Amount` does not exist.** The property is `MaxCount`, and `MinDelay`/`MaxDelay` are
`TimeSpan` rather than numbers, so the wire carries seconds. `Spawner` and `XmlSpawner` share all
four names, which is why one catalog covers both — and why a catalog naming only one of them would
have worked until the day it did not.
- **A property lease's hold must be PERSISTED**, and the config lease's must not. 11b's fail-safe is
*"a lease that never reaches disk means a restart is a free restore"*, which depends on the leased
value being memory-only too. A spawner is in the world save and a seasonal status is in
`Saves/Misc/SeasonalEvents.bin`, so a restart preserves the CHANGE and destroys only the timer that
would undo it. The Bridge gains its **third** save file, written by the same `EventSink.WorldSave`
as what it describes; a deadline that passed while the shard was down fires at once.
- **The seasonal toggle is not "small and safe", and one of the nine lies.** `OnStatusChange()`
generates or removes world content for six of them — safe, since ServUO does it to itself from a
staff gump, but not small. And `TreasuresOfTokuno` is excluded: `IsActive()` reads its own
`DropEra` rather than `Status`, so a lease on it applies cleanly, reads back, restores cleanly and
does nothing at all. That is §N10's "capability that lies" in its purest form and the one instance
no runtime probe can catch, so it is excluded by name at both ends.
**Who receives a grant is answered on the SHARD.** The website has the list in
`event_run_participants`, but a module cannot read core's tables, so the alternative was a new core
surface handing participants to a module's `perform()`. It is not needed: protocol 6 part b's
participation ledger already holds them, keyed by the same character serials core stores as
`member_key`. A run with no ledger open is a 404; a run whose ledger is open and empty is a 200 with
`granted: 0`, because an event nobody attended still happened. And **the grant is retryable** — §G
called it un-retryable before protocol 6 existed, and an idempotency key means a repeat is answered
by the original reply.
**One defect in already-merged code, and it would have broken everything.** The website's protocol
pin never left **5**: `uo_link_config.protocol` reaches the sidecar as `X-UOLink-Version` and an exact
mismatch is a `409`, so on any real deployment every sidecar call would have been refused from Phase
11a onward. It survived two phases because both live walks set the column by hand while standing the
rig up. 12b carries all three declaration sites to 7. The test that guards them asserts they agree
WITH EACH OTHER, which is a real check they once failed — but all three being equally stale passes
it, and nothing in `module-uo` can anchor it to the wire.
**Ships:** the invasion.
**Verify:** the whole rig, running a real multi-phase event with spawns, a lease, and a full teardown
back to baseline — plus a deliberate mid-event GM edit of a leased property, confirming `drifted`