docs(events): Phase 0 — the eleven decisions, and the two documents they reach into
Closes the second half of EVENTS_PLAN.md Phase 0. All of EVENTS.md §N1–N11 were answered by the org lead on 2026-09-01, before any code, which lifts the ⚠ gate from P11 and P12 and unblocks P1. Nine went the way §N recommended. Two did not, and each changes a phase: - N2 — publishing a version and starting a run are `admin` only; a moderator keeps live control of a run already in flight (cancel, abort) and nothing more. Start and stop are deliberately not the same gate: starting commits the deployment to everything a definition contains, unattended, while cancelling is incident response. §K's table and the API surface table are updated, and P3 and P6 carry it. - N4 — this deployment is single-instance and not planned to change, so P2 does not build the `--scale app=2` test. Every claim path in §E 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. §E records what to build first if this deployment is ever scaled. N1 was taken in full, item grants included, which is a reversal of two separate lines in ADMIN_CONTROLS.md §8 rather than one. §8's cut list names "item/gold grants" explicitly and separately from world building, so the new §8a amendment says both out loud rather than letting the grant ride along on the creation reopening. §8's bullet is left standing verbatim — it is the decision that was actually taken — with a pointer to the amendment, and §1's thesis paragraph gets the same pointer. Loot-table changes and arbitrary [set / [get / [add stay permanently excluded, along with the rest of the Tier-H moderation verbs. N3 gives ENGAGEMENT.md §7.1 Q6's "no evaluator yet" an owner: the Event System runner becomes the `kind: 'scheduled'` evaluator, built in that workstream's P2. Nothing in the engagement contract changes. One consequence was derived rather than decided and is flagged for review: POST /admin/events/:id/verify moves to `admin, editor` rather than staying with `start`. A dry run dispatches nothing, and the author who wrote the definition is who should price it against the caps before asking an admin to publish. Verified: every relative link and anchor in the four touched files resolves, including the renamed §N anchor (#n--decisions) and the new #8a. The docs repo has no CI workflow, so the plan's "docs CI link check" was run by hand. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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