docs(link): protocol 6 part b -- leases, participation, and what the walk found

Six files. `v6.md` gains sections 7-10: one version with two halves, the lease
plane, the participation ledger, and 11b's verification.

Three corrections to what the plan and the design of record assumed.

Phase 11b is FIVE repos, not four, and in the opposite direction from 11a's
correction: a lease's ledger row had no reconcile path anywhere, because the step
that made it names `core.lease` and that is core's own action. `website` joins.

EVENTS.md's §D frames the 258 `Config.Get` call sites as splitting into two
patterns. Measured on 57.4: of the 158 non-Bridge sites in `Scripts/`, roughly
eight are read live. The allowlist is not a curated subset of a large pool, it is
nearly the whole of what exists. And `Config.Set` has exactly one caller in the
entire tree, so on a stock shard a GM cannot drift a configuration lease even
deliberately -- which is why proving `drifted` needed a scaffolding verb.

§G's "participation attribution is now the largest piece of new UO work" closes,
and the live-config-lease row goes to built-with-one-key.

§10.1 records the defect the phase's own deferral found in 11a's shipped code:
`bridge.busy` answered 200 instead of 425 because the frame carried two `kind`
fields and parsers take the last. Unreachable in 11a by construction; produced on
the first collision here.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-09-04 19:32:11 -05:00
parent 1521c5af7e
commit eabaf2635f
6 changed files with 546 additions and 23 deletions

View File

@@ -205,6 +205,18 @@ live-read — never "any config key". A module must not advertise a lease it can
because *"the setting applied and nothing happened"* is the worst failure this feature has. That is a
testable obligation, and the test is mechanical: for each key in the catalog, apply, observe, restore.
> **Measured in Phase 11b, and it is far more lopsided than "two patterns" suggests.** Of the 158
> non-Bridge `Config.Get` call sites in `Scripts/` on ServUO 57.4, roughly **eight** are read live.
> The rest are cached at type initialisation. So the allowlist is not a curated subset of a large
> pool — it is nearly the whole of what is available, and the catalog Phase 12 inherits will be
> short for reasons no amount of care can change.
>
> A second measurement, which decides how `drifted` gets tested at all: **`Config.Set` has exactly
> one caller in the whole tree** (`Server/ScriptCompiler.cs`). No in-game command, gump or console
> verb writes a config key, so on a stock shard a GM cannot drift a *configuration* lease even
> deliberately. The compare-and-set is still required — Phase 12's object-property leases are
> trivially driftable — but the config half of it is proved with scaffolding, not by a GM.
Beyond configuration the same pattern covers any per-object property whose current value is readable
before it is written — an existing spawner's `Amount` / `MinDelay` / `MaxDelay`, a named creature's
stats, a `SeasonalEventSystem` entry's status. Loot stays excluded for the reason it always was: it
@@ -807,6 +819,10 @@ api.registerEventLeases([{
// not an error: answer { ok: false, drifted: true, current }.
return { ok: true }
},
// Optional (Phase 11b). "Does the game side still have any record of this
// hold?" — a DIFFERENT question from `read`, and the only thing that takes a
// lease's ledger row out at reconcile.
async inForce() { return { ok: true, held: true } },
}])
```
@@ -829,6 +845,16 @@ api.registerEventLeases([{
renders. A dimension an action prices but nobody declares is **shown** on that screen rather than
filtered out, because the action is refused and the operator needs to be told which module is
incomplete.
- **`inForce()` is the fourth, it is optional, and it is not `read()` with a comparison**
(Phase 11b). It answers *"does the game side still have any record of this hold?"*, which none of
the other three do — and it had to exist because a lease's ledger row has no reconcile path
otherwise: the step that made it names `core.lease`, which is core's own action, so there is
nowhere a module could hang the answer. A value that DIFFERS from what the run applied is drift,
which `restore()` reports so the row lands `drifted` with the current value beside it; inferring
absence from a changed value would orphan the row first and destroy that signal. Only an explicit
`{ ok: true, held: false }` takes a row out; a throw, a timeout and a lease with no `inForce()`
all leave the ledger alone. It matters most for exactly the case core could not see before: a
config lease is memory-only on the shard, so a restart reverts it *and* clears the record.
- **A lease declares all three callables, and `restore` is not optional even though `read` could
stand in for it.** They answer different questions: `read` is *"what is it now"*, `restore` is
*"put this back, and tell me if someone else has moved it"* — the drift check, which is the one
@@ -1095,13 +1121,13 @@ a capability exists.
| Online population, per region | ✅ | `presence.online` with `byFacet` and `byRegion`. |
| Name landmarks, regions, creatures for authoring | ✅ | The spawn atlas — and it answers the "meeting location" field every EM listing carries. |
| Detect a boss defeated | ✅ **built (protocol 6)** | `champ.boss.killed`, fired from `EventSink.CreatureDeath` and detected by type, with the altar attributed from the sweep. The inference this replaces was more fragile than "slightly": `bossUp` also drops when a GM resets a spawn, when a boss despawns, and after a sidecar reconnect clears the diff cache. And it was silent about who fought — the new kind carries the damage table, which exists at the death and nowhere else. |
| **Participation attribution** | 🔗 weak + 🔧 | Composable only loosely from `region.enter` + `mob.killed`, and **not trustworthy enough to publish results on**: nothing scopes a kill or an arrival to a run, nothing separates a passer-by from an attendee, nothing survives a relog. A run-scoped participation ledger on the plugin side is the honest answer — and with points cut, this is now the *largest* remaining piece of new UO work. |
| **Participation attribution** | ✅ built | Protocol 6 part b. Presence in a declared area plus kill credit inside it, keyed by character serial, **persisted in the world save** so a restart mid-event does not lose it. The area is a map, a point and a radius rather than a region name — the most specific region containing an event is routinely anonymous. Kill credit goes to every damager standing in the area, not to the killer: a last hit is a poor description of who fought something. |
| Oracle NPC with scripted dialogue | 🔧 📡 | PEC caps this at 5 NPCs × 5 lines. **This is literally a web form** — arguably a better fit for browser authoring than spawning is, and it is how most story events actually work. |
| Temporary gate to a venue | 🔧 📡 | PEC caps at 4 hours and forbids cross-facet gating to restricted areas. Inherently temporary, so it maps onto a run's lifetime and the ledger with no friction. |
| Temporary decoration lockdown | 🔧 📡 | Permanent decoration prohibited in the program and should be prohibited here. Ledgered and reverted like anything else. |
| Named, hued creatures from an allowlist | 🔧 📡 | PEC's core capability, and its cap is the useful part: common creatures, custom name and hue, **capped at 30**. A bounded one-shot spawn with each serial ledgered — **not a spawner**, which PEC withholds precisely because it is unbounded over time. |
| "Simple" boss variants | 🔧 📡 | An enhanced regular mob, capped at 24. The defensible form is an **event-owned creature template** — the event declares what it spawns, stats included, and never touches a creature it did not create. |
| **Lease a live config value** — rates, toggles, caps | 🔧 📡 | `Server/Config.cs` is a runtime typed key-value store, so this works — **for keys read live**. The plugin ships a verified allowlist, because a lease on one of the `static readonly` keys applies cleanly and does nothing. The single most transferable action in the whole set. |
| **Lease a live config value** — rates, toggles, caps | ✅ built (one key) | Protocol 6 part b: the registry, the deadline timer, compare-and-set restore and `lease.list`, proved end to end against one verified live-read key. **The allowlist is far shorter than this table assumed** — of the 158 non-Bridge `Config.Get` call sites in `Scripts/`, roughly *eight* are read live, so the split below is nearer 95/5 than half and half. Phase 12 adds the rest with the boot-time self-check. |
| Lease a property on an existing object | 🔧 📡 | Practical, and an earlier revision was wrong to rule it out. The before-image lives in the website's database and survives a shard restart; a save just persists current state; a deleted target makes restore a no-op. The one real hazard — a GM editing the same property mid-event — is answered by compare-and-set restore and the `drifted` state. |
| Grant an event item | 🔧 📡 | An ordinary action, not a special contract member. Admin-gated and capped like any other, and `reversible: 'none'` *for UO specifically* — an object in a backpack cannot be recalled. The constructible allowlist is the plugin's; every label and icon comes from `shard_clilocs` and `item_id`. Failure aborts rather than retries: a retried grant is one winner receiving two. |
| Toggle a ServUO seasonal event | 🔧 📡 | Small and safe: `SeasonalEventSystem.GetEntry(type).Status` over a nine-value enum, already persisted across saves. |
@@ -1973,10 +1999,13 @@ the platform's largest missing safety property. Today the moderation write plane
happened and nothing about what it produced. It is also what makes the reward audit answerable — who
received what, from which step, in which run.
**Participation attribution is now the largest piece of new UO work.**
With points cut, spawning is no longer the hard part — attribution is. Results, profile history, the
**Participation attribution was the largest piece of new UO work.***Built in Phase 11b.*
With points cut, spawning was never the hard part — attribution was. Results, profile history, the
calendar's "what happened" and any future recognition all rest on trustworthy "who took part", and UO
gives no run-scoped attribution that can be synthesised from the website side.
gives no run-scoped attribution that can be synthesised from the website side. So the shard counts it:
presence in a declared area plus kill credit inside it, keyed by character serial, and **persisted in
the world save** — which made it the Bridge's first persisted state, because a run spans hours and an
in-memory tally would regress every attendee's score after one restart.
**The GM cue step makes the system useful before any protocol change.**
"Post the instruction, wait for a human to confirm, advance" needs no module, no protocol and no world