docs(events): Phase 0 — the eleven decisions, and the two documents they reach into #208
@@ -21,6 +21,12 @@ None of these are *moderation*. A staff member who wants to kick a cheater, ban
|
||||
|
||||
**The thesis up front:** a small, well-guarded set of account/session-moderation verbs plus the help-page queue covers the overwhelming majority of "why do I have to log in to the game for this" moments. World-building and object manipulation (`[add`, `[set`, `[dupe`, decorate, spawners) should stay in the game client — they are target-driven, high-blast-radius, and gain nothing from a web form.
|
||||
|
||||
> ⚠ **Amended 2026-09-01 — see [§8a](#8a-amendment--2026-09-01).** That thesis holds for *ad-hoc staff
|
||||
> action*, and the Event System does not contest it: a GM still does the target-driven parts in-client.
|
||||
> What was reopened is narrower — declared, cap-bounded, ledgered, idempotent creation and **leased**
|
||||
> mutation, both owned by an event run and both undone at teardown. `[set` / `[get` / `[add` and
|
||||
> loot-table changes stay excluded exactly as this paragraph argues.
|
||||
|
||||
---
|
||||
|
||||
## 2. How ServUO admin controls actually work
|
||||
@@ -284,6 +290,11 @@ All resolved by source inspection (ServUO checkout at `<servuo>`). No live-shard
|
||||
|
||||
## 8. Decisions — locked 2026-07-12
|
||||
|
||||
> ⚠ **Partly amended on 2026-09-01. Read [§8a](#8a-amendment--2026-09-01) with this section.** Two of
|
||||
> the exclusions in the first bullet below — world creation, and item/gold grants — were reopened for
|
||||
> the Event System under conditions that did not exist when this was locked. The bullet is left
|
||||
> standing verbatim because it is the decision that was actually taken; §8a is what changed and why.
|
||||
|
||||
- **Scope:** Phase 1 (kick / ban / unban / broadcast) + Phase 2 (help-page queue) + Phase 3 second-wave. **The former Tier-H verbs (firewall, kill/res, jail, item/gold grants, set-access-level) are cut entirely** — not now, not later.
|
||||
- **Authorization:** enforced on the **website** (admin-only + moderator roles). Inbound sidecar commands are applied on the shard as **CoOwner-level** authority, with a hard floor that refuses any target at `AccessLevel >= CoOwner` (Owner-only shield). Write plane defaults **off** in `Bridge.cfg`.
|
||||
- **Attribution:** `web:<actor>` in `CommandLogging` and the `BanDealer` tag; no core edits.
|
||||
@@ -292,6 +303,70 @@ All resolved by source inspection (ServUO checkout at `<servuo>`). No live-shard
|
||||
|
||||
---
|
||||
|
||||
## 8a. Amendment — 2026-09-01
|
||||
|
||||
**What changed:** §8's blanket exclusion of world writes is narrowed, in **two** places, and only for
|
||||
the Event System. Everything §8 decided about the moderation write plane — its verbs, its
|
||||
CoOwner-level authority, its Owner-only shield, its `Bridge.cfg` default-off switch, its attribution
|
||||
and its logging — stands unchanged.
|
||||
|
||||
**Decided by:** the org lead, 2026-09-01, answering
|
||||
[`../website/EVENTS.md`](../website/EVENTS.md) §N1. The design of record is `EVENTS.md`; this section
|
||||
records the reversal against the decision it reverses, so §8 is never read alone.
|
||||
|
||||
### What is now permitted
|
||||
|
||||
| | Permitted | Under what conditions |
|
||||
|---|---|---|
|
||||
| **1** | **Event-owned creation** — creatures, "simple" boss variants, oracle NPCs, temporary gates, temporary decoration | Created by an event run, counted against a module-declared cap dimension, recorded in the resource ledger with the run, step and actor that produced it, and **deleted by serial at teardown** on every terminal path — completion, cancellation and abort alike |
|
||||
| **2** | **Leased mutation** of a plugin-declared allowlist of config values and object properties | Baseline recorded before the write, drift-checked on read-back, and **deadline-enforced by the game itself** — the shard restores baseline when the lease expires whether or not the website ever comes back |
|
||||
| **3** | **Item and gold grants** | As (1): cap-bounded, ledgered and attributable — but `reversible: 'none'`. A granted item is not clawed back |
|
||||
|
||||
### What stays excluded, permanently
|
||||
|
||||
Unchanged by this amendment and not reopened: **loot-table changes**, and **arbitrary `[set` / `[get`
|
||||
/ `[add`**. Both are the "change something with no baseline recorded" case, which is the thing §8 was
|
||||
actually objecting to; no cap makes them safe. `firewall`, `kill`/`res`, `jail` and `set-access-level`
|
||||
also stay cut — this amendment is about what an *event* may create and borrow, not about restoring the
|
||||
Tier-H moderation verbs.
|
||||
|
||||
### Why the reversal, and why item grants are called out separately
|
||||
|
||||
**§8's objection was to an unrecorded change with a large blast radius, made from a web form.** It was
|
||||
locked when the bridge was a read plane with three inbound verbs, none of which were moderation, and
|
||||
when the only way to express "change the world" would have been a passthrough command string. In that
|
||||
setting the objection was correct and it would still be correct today for anything shaped that way —
|
||||
which is exactly why `[set` / `[get` / `[add` remain excluded.
|
||||
|
||||
**The Event System proposes something with four properties that shape did not have**, all of which are
|
||||
required before P12 ships and none of which are optional:
|
||||
|
||||
1. **Declared, not typed.** A module declares a verb with its params and their types; the browser posts
|
||||
an action id and a params object. There is no passthrough field and no place a request body can name
|
||||
a sidecar route.
|
||||
2. **Cap-bounded.** Every dimension is enforced in a conditional `UPDATE` with the cap in the `WHERE`
|
||||
clause — a control a stolen admin session cannot talk its way past, because a role check is exactly
|
||||
what a stolen session has already passed.
|
||||
3. **Ledgered.** Every created object and every borrowed value is recorded in `event_run_resources`
|
||||
before it is confirmed, with its run, step and actor, and with a declared revert.
|
||||
4. **Idempotent on the wire.** A key minted once per step rides core → module → sidecar → plugin, and
|
||||
the plugin answers a repeat with the original result rather than acting twice — so a lost
|
||||
acknowledgement cannot double a spawn or a grant.
|
||||
|
||||
**Item grants are named as their own row because they are a second reversal, not a consequence of the
|
||||
first.** §8's cut list names "item/gold grants" explicitly and separately from world building; reading
|
||||
the creation reopening as carrying grants along with it would be the silent reversal this amendment
|
||||
exists to avoid. They are permitted on the same four conditions as creation — with the difference that
|
||||
they are **not reversible**, which is why the cap and the ledger carry the entire weight for this row
|
||||
and why the action ships declared as `reversible: 'none'` rather than pretending at a revert.
|
||||
|
||||
**What has not changed:** the module boundary is still not a security boundary, and none of the above
|
||||
defends against a hostile module. It defends against a compromised session and an operator mistake —
|
||||
both of which *scheduling* makes larger, because a scheduled change happens while nobody is watching.
|
||||
That is the reason the caps and the leases matter more here than the role check does.
|
||||
|
||||
---
|
||||
|
||||
## 9. Where the code goes
|
||||
|
||||
| File | Responsibility |
|
||||
|
||||
@@ -1676,7 +1676,7 @@ deferred to this phase on purpose:
|
||||
| | Question | Decision |
|
||||
|---|---|---|
|
||||
| §7.2 | one namespace, or two? | **ONE.** A trigger is a payload contract attached to an id that may also carry a subscription toggle |
|
||||
| §7.1 Q6 | time-based triggers | **declare now, build after Phase 9** — `kind: 'scheduled'` is in the contract, the manifest and every stored declaration from today; no evaluator yet |
|
||||
| §7.1 Q6 | time-based triggers | **declare now, build after Phase 9** — `kind: 'scheduled'` is in the contract, the manifest and every stored declaration from today. **The evaluator was assigned on 2026-09-01** ([EVENTS.md §N3](EVENTS.md#n--decisions)): the Event System runner becomes it, built in that workstream P2 |
|
||||
| — | what "narrower" means for a ceiling | an explicit **subset lattice**; two incomparable ceilings have no bound and the save is REFUSED, never guessed |
|
||||
|
||||
**One namespace was the more invasive of the two options and it is worth stating what it bought and
|
||||
@@ -4078,6 +4078,14 @@ day it ships.
|
||||
caller's — so `kind` means something from the moment it is declarable rather than from the moment
|
||||
it is honoured. The lifecycle uses in §8.5 are the highest-value non-game triggers on the list and
|
||||
the first thing anyone will ask for after the IDOC mail works.
|
||||
|
||||
> **The evaluator now has an owner, decided 2026-09-01** ([`EVENTS.md`](EVENTS.md#n--decisions)
|
||||
> §N3). The **Event System runner** becomes the `kind: 'scheduled'` evaluator rather than a ninth
|
||||
> poller being written for it — the runner already materialises due occurrences on a tick, and a
|
||||
> second timer doing the same shape of work is how a codebase acquires two schedulers that
|
||||
> disagree. It is built in that workstream's P2. Nothing in this phase's contract changes: a
|
||||
> `scheduled` declaration means today exactly what it meant on 2026-08-29, and `ctx.events.emit`
|
||||
> still refuses to fire one.
|
||||
7. ✅ **ANSWERED — manual/operator-authored sends.** *"There is no campaign in the normal sense of email
|
||||
marketing. But admins can create all sorts of trigger conditions"*, and separately: *"lists can be
|
||||
built if they are powered by game data — say team X members or governors or whatever — thru the uo
|
||||
|
||||
@@ -475,6 +475,15 @@ the fishing contest on Drachenfels is exactly that shape.
|
||||
| An orphaned claim | Reclaim on lease expiry, **without resetting `attempts`** | Engagement Phase 14's exact defect: a reclaim that reset state made `MAX_ATTEMPTS` unreachable and the row cycled forever, never terminal and therefore never retention-eligible. |
|
||||
| Two events overlapping | `concurrency_key` as a **template rendered from the run's params** — e.g. `invasion:{region}` | a flat definition-id key would wrongly stop the same definition running on two Rust servers, or in two regions, at once. |
|
||||
|
||||
> **This deployment runs one app instance, and every protection above is built anyway**
|
||||
> ([§N4](#n--decisions)). The "two instances" column names the *hardest* contender for each row, not
|
||||
> the only one: the unique index and the CAS equally protect a tick that runs long while the next one
|
||||
> fires, and the lease and its reclaim are what recover a step whose process died mid-dispatch. Both
|
||||
> happen with one container. What the single-instance decision changes is the **test**, not the
|
||||
> design — P2 does not build the `--scale app=2` rig — so the multi-instance property is true by
|
||||
> construction and unproven by experiment. Scaling this deployment, or adopting a rolling deploy that
|
||||
> briefly runs two containers, is the trigger to build that rig before anything else.
|
||||
|
||||
### Idempotency
|
||||
|
||||
```js
|
||||
@@ -825,15 +834,23 @@ live game world on a schedule.
|
||||
### Which role, per surface
|
||||
|
||||
Following the split module-uo already uses — `requireRole('admin','moderator')` for live operations,
|
||||
`requireRole('admin')` for configuration that can break things:
|
||||
`requireRole('admin')` for configuration that can break things — **with one deliberate departure,
|
||||
decided in [§N2](#n--decisions): starting a run is `admin` only, while stopping one is not.**
|
||||
|
||||
| Surface | Role |
|
||||
| --- | --- |
|
||||
| The calendar, run history, the run console, the diagnostic log | `staff` — the tier gate, nothing added |
|
||||
| Authoring and editing a draft | `admin`, `editor` |
|
||||
| Publishing a version, starting a run, live controls | `admin`, `moderator` |
|
||||
| Publishing a version, **starting a run** | `admin` only |
|
||||
| **Live control of a run in flight** — cancel, abort | `admin`, `moderator` |
|
||||
| Any step whose action is above `notify`, and the action switchboard | `admin` only |
|
||||
|
||||
> **Why start and stop are gated differently.** Starting commits the deployment to everything the
|
||||
> definition contains, unattended, up to every cap it declares — it wants the narrowest gate there is.
|
||||
> Cancelling is incident response, and the incident is "the event is doing something wrong at 2am" —
|
||||
> it wants the widest. A split that read consistent, with one role owning both buttons, would behave
|
||||
> badly in exactly the case the moderator role exists for.
|
||||
|
||||
> **Keep the check in one function.** The whole authorisation decision — role, enablement, cap, shard
|
||||
> switch — should live behind a single `mayInvoke(user, action, run)` rather than being spread across
|
||||
> route middleware. Not for tidiness: it is what makes an EM-style delegation model a *later* option
|
||||
@@ -925,15 +942,15 @@ no URL moved.
|
||||
| `GET /admin/events` | staff | definitions, state, next occurrence, health |
|
||||
| `POST /admin/events` | admin, editor | create a draft |
|
||||
| `PUT /admin/events/:id` | admin, editor | edit the draft spec |
|
||||
| `POST /admin/events/:id/publish` | admin, moderator | snapshot a version and go `ready` |
|
||||
| `POST /admin/events/:id/publish` | admin | snapshot a version and go `ready` |
|
||||
| `DELETE /admin/events/:id` | admin | archive — never a hard delete while runs reference it |
|
||||
| `GET /admin/events/:id/versions` | staff | version history |
|
||||
| `POST /admin/events/:id/verify` | admin, moderator | **dry run** — dispatch with `verify: true`, report cost against the caps |
|
||||
| `POST /admin/events/:id/runs` | admin, moderator | start now; optional `rehearsal: true`, optional `scope` |
|
||||
| `POST /admin/events/:id/verify` | admin, editor | **dry run** — dispatch with `verify: true`, report cost against the caps |
|
||||
| `POST /admin/events/:id/runs` | admin | start now; optional `rehearsal: true`, optional `scope` |
|
||||
| `GET /admin/events/runs` | staff | run history across definitions |
|
||||
| `GET /admin/events/runs/:runId` | staff | status, phase, steps, caps, resources, cleanup |
|
||||
| `GET /admin/events/runs/:runId/log` | staff | the diagnostic log |
|
||||
| `POST /admin/events/runs/:runId/pause\|resume\|advance\|cancel` | admin, moderator | `cancel` takes `{ cleanup, reason }` |
|
||||
| `POST /admin/events/runs/:runId/pause\|resume\|advance\|cancel` | admin, moderator | live control of a run in flight; `cancel` takes `{ cleanup, reason }` |
|
||||
| `POST /admin/events/runs/:runId/steps/:stepId/skip\|retry\|confirm` | admin, moderator | `confirm` resolves a GM cue step |
|
||||
| `POST /admin/events/runs/:runId/cleanup` | admin | re-run cleanup over unreverted resources |
|
||||
| `GET /admin/events/catalog` | staff | registered actions, param schemas, risk classes, budget dimensions |
|
||||
@@ -944,6 +961,11 @@ no URL moved.
|
||||
| `GET /public/events/series/:slug` | — | the arc |
|
||||
| `GET /player/events/history` | auth | this account's participation |
|
||||
|
||||
> **Publish and start are `admin`; cancel is not** — that asymmetry is deliberate and decided in
|
||||
> [§N2](#n--decisions). `verify` sits with `admin, editor` rather than with `start`: a dry run
|
||||
> dispatches nothing, and the author who wrote the definition is exactly who should be able to price
|
||||
> it against the caps before asking an admin to publish it.
|
||||
|
||||
A module registers actions server-side and adds **no routes** for them beyond its option endpoints,
|
||||
which is what keeps the browser from being able to name a transport.
|
||||
|
||||
@@ -991,11 +1013,12 @@ that only *terminal* rows are eligible.
|
||||
Seventeen PR-sized slices (P0–P16) on an `edge` branch with one cutover, matching the discipline every
|
||||
previous workstream used. Phases 1–10 and 13–16 ship a complete, useful system that schedules,
|
||||
announces, orchestrates and records — **without creating or changing anything in the game world** —
|
||||
and need no answer to [N1](#n--open-questions). Only P11 and P12 are gated.
|
||||
and needed no answer to [N1](#n--decisions). P11 and P12 were the gated pair; **§N1 was answered on
|
||||
2026-09-01 and the gate is lifted.**
|
||||
|
||||
| Phase | | Repos |
|
||||
| --- | --- | --- |
|
||||
| **P0** | Design of record; answer §N; amend `ADMIN_CONTROLS.md` §8 | `docs` |
|
||||
| **P0** ✓ | Design of record; §N answered 2026-09-01; `ADMIN_CONTROLS.md` §8 amended | `docs` |
|
||||
| **P1** | Schema, CRUD, and the action registry with core as its first registrant | `website` `docs` |
|
||||
| **P2** | The runner — materialise, claim, advance, drain; leases, `missed`, concurrency | `website` |
|
||||
| **P3** | The minimal admin surface — **first demo** | `website` |
|
||||
@@ -1006,8 +1029,8 @@ and need no answer to [N1](#n--open-questions). Only P11 and P12 are gated.
|
||||
| **P8** | The resource ledger, leases and generated cleanup | `website` `docs` |
|
||||
| **P9** | UO wave 1 — the actions that need no protocol change | `module-uo` `docs` |
|
||||
| **P10** | Integrations — the `event.` triggers, participants, results, announce legs | `website` `docs` |
|
||||
| **P11** ⚠ | *Needs N1.* Protocol: idempotency key, lease deadline, participation ledger | `servuo-plugins` `link` `module-uo` `installer` `docs` |
|
||||
| **P12** ⚠ | *Needs N1.* UO wave 2 — the world verbs, owned or borrowed | `servuo-plugins` `link` `module-uo` `docs` |
|
||||
| **P11** | *N1 answered.* Protocol: idempotency key, lease deadline, participation ledger | `servuo-plugins` `link` `module-uo` `installer` `docs` |
|
||||
| **P12** | *N1 answered.* UO wave 2 — the world verbs, owned or borrowed | `servuo-plugins` `link` `module-uo` `docs` |
|
||||
| **P13** | The authoring UI proper — timeline, schema-driven steps, cap meter | `website` |
|
||||
| **P14** | Public calendar, player history, mobile — and the Android module-path fix | `website` `android-app` `docs` |
|
||||
| **P15** | Integration Kit chapter 5 — cannot merge before the cutover exists | `integration-kit` |
|
||||
@@ -1022,10 +1045,36 @@ P15 and P16 are not optional if anything ships to `main`; they are the cost of t
|
||||
|
||||
---
|
||||
|
||||
## N — Open questions
|
||||
## N — Decisions
|
||||
|
||||
Each needs an architectural decision before the phase that depends on it. A recommendation is given
|
||||
for every one.
|
||||
**All eleven were settled by the org lead on 2026-09-01**, before any code, which closes the second
|
||||
half of [`EVENTS_PLAN.md`](EVENTS_PLAN.md) Phase 0 and lifts the ⚠ gate from P11 and P12. Each entry
|
||||
below keeps the recommendation it was decided against, so the reasoning survives alongside the answer.
|
||||
**Nine went the way the recommendation argued; two did not** — N2, where starting a run became
|
||||
`admin`-only, and N4, where the deployment is declared single-instance for good.
|
||||
|
||||
Two answers have consequences outside this document, and both are written where they will be found
|
||||
rather than only here:
|
||||
|
||||
- **N1** is an amendment to [`../link/ADMIN_CONTROLS.md`](../link/ADMIN_CONTROLS.md) §8, recorded there
|
||||
as a dated amendment with its reasoning. §8's locked scope is reopened in two places, not one — see
|
||||
the decision below.
|
||||
- **N3** closes [`ENGAGEMENT.md`](ENGAGEMENT.md) §7.1 Q6, whose "no evaluator yet" row is updated in
|
||||
the same pull request.
|
||||
|
||||
| | Question | Decision |
|
||||
|---|---|---|
|
||||
| N1 | Scope of the §8 reopening | **As recommended, including item grants** — §8 is amended in two places |
|
||||
| N2 | Role split across the four surfaces | **Starting and publishing are `admin` only**; a moderator gets live control of a run already in flight and nothing more |
|
||||
| N3 | Runner as the `kind: 'scheduled'` evaluator | **Yes** — one scheduler, not two |
|
||||
| N4 | More than one app instance? | **No, and not later** — single-instance is a recorded assumption; the claim paths stay, the two-instance test is not built |
|
||||
| N5 | Live console: poll, or core SSE? | **Poll** in v1 |
|
||||
| N6 | Module-declared risk classes? | **No** — four closed values, core-owned |
|
||||
| N7 | Plugin-side runtime and phase persistence | **The shard stays stateless about events**; reconciliation-on-reconnect reunites objects with phases |
|
||||
| N8 | One event invoking another? | **Not as a feature** — composition covers it |
|
||||
| N9 | Does core know a reward is a reward? | **No** — a reward is an ordinary action with its own cap dimension |
|
||||
| N10 | Who verifies the lease allowlist? | **The plugin, at boot** — set, read back, restore; a key that does not take drops itself from the catalog |
|
||||
| N11 | Caps per run, or also per period? | **Per run only** |
|
||||
|
||||
**N1 · Confirm the scope of the §8 reopening.**
|
||||
The direction is settled; what needs writing down is the boundary. Recommendation: permit two things
|
||||
@@ -1036,6 +1085,27 @@ loot-table changes, and arbitrary `[set` / `[get` / `[add` — which is precisel
|
||||
no baseline recorded". This should be an amendment recorded in `ADMIN_CONTROLS.md`, not a silent
|
||||
reversal.
|
||||
|
||||
> **Decided 2026-09-01 — as recommended, item grants included.**
|
||||
>
|
||||
> **`ADMIN_CONTROLS.md` §8 is reopened in two places, and the amendment says both out loud.** §8's
|
||||
> locked scope cut the Tier-H verbs "entirely — not now, not later", and that list names **item/gold
|
||||
> grants** as well as world creation. Permitting an event to grant an item is therefore not a widening
|
||||
> of §8's creation cut; it is a second, independent reversal of a decision that was taken deliberately,
|
||||
> and an amendment that mentioned only the first would be the silent reversal §8 deserves not to get.
|
||||
>
|
||||
> **What makes the grant a different proposition from the one §8 refused** is the four properties it
|
||||
> did not have then, all of which this document requires before P12 ships: it is *declared* by a module
|
||||
> rather than typed as a command, *cap-bounded* by `uo.rewards` in a conditional `UPDATE` a stolen
|
||||
> session cannot talk its way past, *ledgered* in `event_run_resources` with the run, step and actor
|
||||
> that produced it, and *idempotent* on the wire so a lost acknowledgement cannot double it. §8's
|
||||
> objection was to an unrecorded change with no baseline; a recorded, bounded, attributable one is a
|
||||
> different argument. It remains `reversible: 'none'` — a granted item is not clawed back — which is
|
||||
> exactly why the cap and the ledger carry the whole weight here and are non-negotiable.
|
||||
>
|
||||
> **Still permanently excluded, and this decision does not touch them:** loot-table changes, and
|
||||
> arbitrary `[set` / `[get` / `[add`. Those are the "change something with no baseline recorded" case
|
||||
> and no cap makes them safe.
|
||||
|
||||
**N2 · Confirm the role split across the four event surfaces.**
|
||||
Settled in principle — gate on the existing roles, add no permission system — so what is left is the
|
||||
mapping, and it is a judgement call rather than a design one. Recommendation in [§K](#k--security-model):
|
||||
@@ -1043,53 +1113,109 @@ viewing is the staff tier gate; authoring is `admin` + `editor`; publishing, sta
|
||||
are `admin` + `moderator`; anything above `notify` and the action switchboard are `admin` only. The one
|
||||
worth a second look is whether a moderator should be able to start a run at all.
|
||||
|
||||
> **Decided 2026-09-01 — no, and the recommendation was changed because of it.** Publishing a version
|
||||
> and starting a run are **`admin` only**. A moderator keeps **live control of a run already in
|
||||
> flight** — cancel and abort — and nothing more.
|
||||
>
|
||||
> **The asymmetry is the point.** Starting is the act that commits the deployment to everything the
|
||||
> definition contains, unattended, up to every cap it declares; cancelling is the act that stops it.
|
||||
> One of those wants the narrowest possible gate and the other wants the widest, because the moderator
|
||||
> role exists for incident response and the incident here is "the event is doing something wrong at
|
||||
> 2am". Gating the stop button on the same role as the start button would be the version of this that
|
||||
> reads consistent and behaves badly.
|
||||
>
|
||||
> This is the one place the split departs from the `requireRole('admin','moderator')` /
|
||||
> `requireRole('admin')` shape module-uo uses, and [§K](#k--security-model)'s table is the normative
|
||||
> statement of it.
|
||||
|
||||
**N3 · Does the event runner also become the `kind: 'scheduled'` trigger evaluator?**
|
||||
Recommendation: yes. It closes ENGAGEMENT §7.1 Q6, and a second timer doing the same shape of work is
|
||||
how a codebase acquires two schedulers that disagree.
|
||||
|
||||
> **Decided 2026-09-01 — yes, as recommended.** [`ENGAGEMENT.md`](ENGAGEMENT.md) §7.1 Q6's row is
|
||||
> updated from "no evaluator yet" in the same pull request as this decision. The evaluator itself is
|
||||
> built by P2, which is the phase that acquires the tick.
|
||||
|
||||
**N4 · Is this deployment ever more than one app instance?**
|
||||
Affects nothing in the design — every claim is already multi-instance safe — but it decides how hard
|
||||
P2's tests must work, and whether the answer is "safe" or merely "documented".
|
||||
`docker compose up --scale app=2` is how the engagement workstream's Phase 13 acceptance walk found the seed race.
|
||||
|
||||
> **Decided 2026-09-01 — one instance, and not planned to change.** The `app` service runs as a single
|
||||
> container; there is no load balancer, no rolling deploy with two containers briefly alive together,
|
||||
> and no intent to add either. **P2 does not build the two-instance test.**
|
||||
>
|
||||
> **Every claim in [§E](#concurrency) stays exactly as specified, and this decision is not a licence to
|
||||
> simplify them.** They are not only there for a second container: the unique index and the CAS are
|
||||
> equally what protect a tick that runs long while the next one fires, and the lease and its reclaim are
|
||||
> what recover a step whose process died mid-dispatch — both of which happen in a single-instance
|
||||
> deployment. They also cost nothing to keep, being one index and one `WHERE` clause.
|
||||
>
|
||||
> **What this actually buys is a smaller P2 and one honest sentence:** the multi-instance property is
|
||||
> *by construction*, not *proved*. If this deployment is ever scaled, or acquires a rolling deploy, the
|
||||
> event runner is the component most likely to break and it breaks by duplicating world writes rather
|
||||
> than by erroring — so scaling is the trigger to build the test that was skipped here, and this
|
||||
> paragraph is where whoever does that will find out why it does not already exist.
|
||||
|
||||
**N5 · Does the live run console poll, or does core gain SSE?**
|
||||
Recommendation: *poll* in v1. Core has no SSE at all and adding one is a larger core change than this
|
||||
needs. If a live channel is ever built it should be core infrastructure with module-uo's two streams
|
||||
migrating onto it, not an event-system side effect.
|
||||
|
||||
> **Decided 2026-09-01 — poll, as recommended.** The run console polls; core gains no SSE in this
|
||||
> workstream.
|
||||
|
||||
**N6 · May a module declare its own risk classes?**
|
||||
Recommendation: no. Four closed values, core-owned. A module-defined class is a module choosing which
|
||||
gate it sits behind. Budget *dimensions* are the opposite case and are module-declared, because core
|
||||
cannot name what a game counts.
|
||||
|
||||
> **Decided 2026-09-01 — no, as recommended.** Four closed, core-owned risk classes. Budget
|
||||
> dimensions stay module-declared.
|
||||
|
||||
**N7 · Where does the plugin-side runtime live, and does a phase persist across a shard restart?**
|
||||
An asymmetry needing a decision: a spawned creature is an object and persists; a *phase* does not
|
||||
exist on the shard at all. Recommendation: keep the shard **stateless about events** — it owns objects
|
||||
and leases, core owns phases — and make reconciliation-on-reconnect the mechanism that reunites them.
|
||||
A plugin that remembered a phase would be a second event engine.
|
||||
|
||||
> **Decided 2026-09-01 — as recommended.** The shard is stateless about events: it owns objects and
|
||||
> leases, core owns phases, and reconciliation-on-reconnect is what reunites them.
|
||||
|
||||
**N8 · Can one event invoke another?**
|
||||
Recommendation: not as a feature. It already works by composition — a second event's trigger condition
|
||||
can be `event.run.completed` — and a direct edge would need cycle detection, a depth bound and a story
|
||||
about cancelling a parent. Series and arcs cover the storyline case without invocation.
|
||||
|
||||
> **Decided 2026-09-01 — not as a feature, as recommended.** Composition covers it; there is no
|
||||
> direct edge, therefore no cycle detection and no depth bound to design.
|
||||
|
||||
**N9 · Does core need to know a reward is a reward?**
|
||||
Recommendation: **no**. A reward is an ordinary action, enabled or not on the switchboard like any
|
||||
other, with its own cap dimension — a shard that wants none simply never enables it. An action
|
||||
returning a resource that carries a `memberKey` is enough to answer who received what, and inventing a
|
||||
category would put core back in the business of knowing what a game can give.
|
||||
|
||||
> **Decided 2026-09-01 — no, as recommended.** A reward is an ordinary action with its own cap
|
||||
> dimension. Core learns no reward category.
|
||||
|
||||
**N10 · Who verifies the lease allowlist, and how often?**
|
||||
A key that is live-read today can become `static readonly` in a later ServUO release, and the failure
|
||||
is silent — the lease applies and nothing changes. Recommendation: the allowlist ships with the plugin
|
||||
and each key carries a boot-time self-check (set, read back, restore) that drops a key from the
|
||||
advertised catalog if it does not take. Better a capability that disappears loudly than one that lies.
|
||||
|
||||
> **Decided 2026-09-01 — as recommended.** The allowlist ships with the plugin and each key
|
||||
> self-checks at boot, dropping itself from the advertised catalog if the write does not take.
|
||||
|
||||
**N11 · Are caps per run only, or also per period?**
|
||||
Recommendation: **per run only**. A cumulative cap that silently exhausts mid-event is a worse failure
|
||||
than one that refuses at authoring time, and with no delegation there is nobody whose longer-term
|
||||
allowance needs tracking. Per-period is a later addition if it is ever wanted.
|
||||
|
||||
> **Decided 2026-09-01 — per run only, as recommended.** Per-period caps are a later addition if
|
||||
> they are ever wanted; nothing in the schema forecloses one.
|
||||
|
||||
---
|
||||
|
||||
## O — Recommendations beyond the brief
|
||||
|
||||
@@ -6,11 +6,13 @@ is proved. It re-specifies nothing — where this and `EVENTS.md` disagree, `EVE
|
||||
file is the one with the bug.
|
||||
|
||||
**Seventeen phases, P0–P16.** Every phase is independently shippable and leaves the site working.
|
||||
**Two are gated** on the open question `EVENTS.md` §N1 (the `ADMIN_CONTROLS.md` §8 amendment) and are
|
||||
deliberately late: P11 and P12. Everything else — the engine, the scheduler, the calendar, the
|
||||
conditions, the caps, the module contract, the ledger, the first wave of UO actions, the integrations,
|
||||
the authoring UI, the public surface — needs no decision beyond P0. Those fourteen phases reach the
|
||||
game only to *announce*, over verbs the write plane already carries; nothing in them creates or
|
||||
**No phase is gated any more.** P11 and P12 hung on `EVENTS.md` §N1 (the `ADMIN_CONTROLS.md` §8
|
||||
amendment); §N1 and the other ten **were answered by the org lead on 2026-09-01**, which closes P0
|
||||
and lifts the gate. They stay deliberately late all the same — they are the phases that reach into
|
||||
the world, and they belong after the ledger that makes them safe. Everything else — the engine, the
|
||||
scheduler, the calendar, the conditions, the caps, the module contract, the ledger, the first wave of
|
||||
UO actions, the integrations, the authoring UI, the public surface — needed no decision beyond P0.
|
||||
Those fourteen phases reach the game only to *announce*, over verbs the write plane already carries; nothing in them creates or
|
||||
changes a thing in the world.
|
||||
|
||||
---
|
||||
@@ -96,9 +98,9 @@ early on purpose.
|
||||
|
||||
### Phase 0 — Design of record (`docs`)
|
||||
|
||||
> **Half-done.** The pull request that introduced this file and `EVENTS.md` discharges the first
|
||||
> half — the design of record exists. What remains is the decisions, and they are the part that
|
||||
> gates P11 and P12.
|
||||
> **Complete, in two pull requests.** The first introduced this file and `EVENTS.md`; the second
|
||||
> recorded the eleven decisions and amended the two documents they reach into. **All of §N1–N11
|
||||
> were answered by the org lead on 2026-09-01**, before any code, so nothing below is gated.
|
||||
|
||||
Land `docs/website/EVENTS.md`, and answer §N1–N11. Two of those answers have consequences outside this
|
||||
document and should be written where they will be found:
|
||||
@@ -113,6 +115,24 @@ document and should be written where they will be found:
|
||||
**Ships:** the design of record, and two documents that stop contradicting each other.
|
||||
**Verify:** `docs` CI link check; every relative link resolves.
|
||||
|
||||
> **As answered, 2026-09-01.** Nine of the eleven went the way `EVENTS.md` §N recommended. **Two did
|
||||
> not, and each changes a phase below.**
|
||||
>
|
||||
> - **N2 — starting a run is `admin` only.** A moderator gets live control of a run already in
|
||||
> flight — cancel and abort — and nothing more. Starting commits the deployment to everything a
|
||||
> definition contains, unattended; cancelling is incident response, and gating the stop button on
|
||||
> the same role as the start button would behave badly in exactly the case moderators exist for.
|
||||
> **P3 and P6 carry this**, and `EVENTS.md` §K's table is the normative statement of it.
|
||||
> - **N4 — this deployment is single-instance, and not planned to change.** **P2 does not build the
|
||||
> `--scale app=2` test.** Every claim path is built exactly as specified regardless: the unique
|
||||
> index and the CAS equally protect a tick that overruns into the next one, and the lease and its
|
||||
> reclaim recover a step whose process died mid-dispatch. The multi-instance property becomes
|
||||
> true-by-construction rather than proved; `EVENTS.md` §E records what to build first if this
|
||||
> deployment is ever scaled or acquires a rolling deploy.
|
||||
>
|
||||
> **N1 was taken in full, item grants included** — a reversal of *two* separate lines in
|
||||
> `ADMIN_CONTROLS.md` §8 rather than one, and the amendment at §8a says both out loud.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — Schema, CRUD and the core action registry (`website` + `docs`)
|
||||
@@ -155,8 +175,11 @@ Also here: `missed` and the grace window, the lease and its reclaim, `concurrenc
|
||||
run params, and `health` as a column separate from `status`.
|
||||
|
||||
**Ships:** a manually started event that broadcasts, waits, and completes. Demoable over curl.
|
||||
**Verify:** `npm test`, including a **deliberate two-instance test** — the claim paths are the whole
|
||||
point of this phase and a single-process test proves nothing about them.
|
||||
**Verify:** `npm test`. **No two-instance test** — §N4 settled this deployment as single-instance, so
|
||||
the `--scale app=2` rig the engagement workstream used is not built here. The claim paths are still
|
||||
built exactly as `EVENTS.md` §E specifies, and they are still the point of the phase: they are what
|
||||
protects a tick that overruns into the next one, and what recovers a step whose process died
|
||||
mid-dispatch. Test both in-process. The decision is not licence to drop a CAS.
|
||||
|
||||
**Two traps, both already paid for once in this codebase.**
|
||||
- **A reclaim must not reset `attempts`.** Engagement Phase 14's defect: a sweep that returned every
|
||||
@@ -173,6 +196,11 @@ point of this phase and a single-process test proves nothing about them.
|
||||
A list with state and next occurrence; a create/edit form; publish; start now; cancel; and a run
|
||||
console showing the phase, the step list with status and attempts, and the log.
|
||||
|
||||
**Publish and start now are `admin` only from this phase, not from P6** (§N2). Cancel is `admin` +
|
||||
`moderator`. The routes get their real gates here even though the switchboard they will eventually
|
||||
consult does not exist yet — a button that is admin-only later and open now is a gate nobody
|
||||
notices was missing.
|
||||
|
||||
The spec is edited as **structured fields for the parts that exist** (name, description, schedule,
|
||||
phases with their steps) and the step's params as a raw JSON field — a deliberate placeholder that P13
|
||||
replaces with the schema-driven editor. Say so in the UI, so it does not read as the finished thing.
|
||||
@@ -247,7 +275,10 @@ error, not an outage.
|
||||
enablement, cap, and the shard's own switch. Not for tidiness — it is what keeps an EM-style
|
||||
delegation model a later option rather than a redesign.
|
||||
|
||||
The role split from `EVENTS.md` §K is applied to the routes here.
|
||||
The role split from `EVENTS.md` §K is applied to the routes here — completing what P3 started, and
|
||||
including §N2's departure from the module-uo shape: **publishing and starting are `admin` only,
|
||||
while cancelling and aborting a run in flight are `admin` + `moderator`**. Start and stop are
|
||||
deliberately not the same gate.
|
||||
|
||||
**Ships:** an admin switchboard, and a system that cannot be made to do an unbounded amount of
|
||||
anything.
|
||||
@@ -344,9 +375,9 @@ push; the ceiling on `run.failed` proved to exclude a moderator.
|
||||
|
||||
---
|
||||
|
||||
### Phase 11 ⚠ — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `installer` + `docs`)
|
||||
### Phase 11 — Protocol: idempotency, lease deadlines, participation (`servuo-plugins` + `link` + `module-uo` + `installer` + `docs`)
|
||||
|
||||
**Gated on §N1.** A five-repo protocol bump, and the shape TEAMS Phase 1 already walked.
|
||||
**§N1 answered 2026-09-01 — no longer gated.** A five-repo protocol bump, and the shape TEAMS Phase 1 already walked.
|
||||
|
||||
- **Plugin:** an idempotency key on every inbound command with a bounded recent-key set that answers a
|
||||
repeat with the original result; a lease deadline timer that restores baseline **without being
|
||||
@@ -366,9 +397,10 @@ proving baseline returns anyway.
|
||||
|
||||
---
|
||||
|
||||
### Phase 12 ⚠ — UO wave 2: the world verbs (`servuo-plugins` + `link` + `module-uo` + `docs`)
|
||||
### Phase 12 — UO wave 2: the world verbs (`servuo-plugins` + `link` + `module-uo` + `docs`)
|
||||
|
||||
**Gated on §N1.** The capability set the two UO programs demonstrate, each cap-bounded, ledgered, and
|
||||
**§N1 answered 2026-09-01 — no longer gated**, and taken in full, so the item grant row below
|
||||
stands. The capability set the two UO programs demonstrate, each cap-bounded, ledgered, and
|
||||
either owned or borrowed:
|
||||
|
||||
| Verb | Owned or borrowed | Cap dimension |
|
||||
@@ -493,9 +525,9 @@ flowchart TD
|
||||
P8 --> P9["P9 · UO wave 1<br/>no protocol change"]
|
||||
P5 --> P10["P10 · integrations"]
|
||||
P9 --> P10
|
||||
P0 -.->|"§N1"| P11["P11 ⚠ · protocol bump"]
|
||||
P0 -.->|"§N1 answered"| P11["P11 · protocol bump"]
|
||||
P8 --> P11
|
||||
P11 --> P12["P12 ⚠ · UO world verbs"]
|
||||
P11 --> P12["P12 · UO world verbs"]
|
||||
P7 --> P13["P13 · authoring UI"]
|
||||
P4 --> P13
|
||||
P10 --> P14["P14 · public + mobile"]
|
||||
@@ -523,7 +555,8 @@ change. This is a real product and it is roughly a third of the work.
|
||||
**Stop after P10** — everything above plus the module seam, the ledger, the first UO actions and every
|
||||
notification channel. The website orchestrates and announces; a GM does the target-driven parts
|
||||
in-client, exactly as `ADMIN_CONTROLS.md` §8 argued they should. **This is the recommended minimum
|
||||
viable scope**, and it requires no answer to §N1 at all.
|
||||
viable scope**, and it was the scope that required no answer to §N1 at all. §N1 has since been
|
||||
answered in full, so this is now a scope choice rather than a way around an open decision.
|
||||
|
||||
**Stop after P14** — everything, including the world verbs and the public calendar. P15 and P16 are
|
||||
not optional if anything ships to `main`; they are the cost of the `edge` discipline.
|
||||
|
||||
Reference in New Issue
Block a user