Merge pull request 'docs(events): the Event System design of record and its phased plan' (#207) from docs/events-system into main
Reviewed-on: #207
This commit is contained in:
1151
website/EVENTS.md
Normal file
1151
website/EVENTS.md
Normal file
File diff suppressed because it is too large
Load Diff
529
website/EVENTS_PLAN.md
Normal file
529
website/EVENTS_PLAN.md
Normal file
@@ -0,0 +1,529 @@
|
||||
# The Event System — phased implementation plan
|
||||
|
||||
**Derived from** [`EVENTS.md`](EVENTS.md) revision 5, which is the design of record. This document
|
||||
decides *order*: what lands in which pull request, what each one ships on its own merit, and how each
|
||||
is proved. It re-specifies nothing — where this and `EVENTS.md` disagree, `EVENTS.md` wins and this
|
||||
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
|
||||
changes a thing in the world.
|
||||
|
||||
---
|
||||
|
||||
## Before anything: three facts about the ground
|
||||
|
||||
**1. `edge` is free, and it is stale.** The engagement workstream is fully cut over as of 2026-09-01 —
|
||||
website `main` at `6331b36` (engagement Phase 14, retention), docs `#205` merged, `module-uo`'s `ci/core-ref.json`
|
||||
re-pinned to `main`. No pull request is open in any of the six repos bar an automated
|
||||
`PROJECT_TREE.md` sync.
|
||||
|
||||
But `edge` is **0 commits ahead of `main` and several behind it** in `website`, `docs` and
|
||||
`module-uo` — the cutover merged `edge` into `main` and nothing moved `edge` afterwards. So the first
|
||||
act of P1 is to **reset each `edge` to its `main`**, not to branch off it as it stands; branching off
|
||||
a stale `edge` would silently revert the engagement retention work the moment this workstream cut
|
||||
over. Verify per repo before starting:
|
||||
|
||||
```bash
|
||||
git rev-list --count origin/main..origin/edge # must be 0
|
||||
git rev-list --count origin/edge..origin/main # if > 0, edge is stale — reset it
|
||||
```
|
||||
|
||||
**2. A `MODULE_API_VERSION` bump turns the integration kit red, on purpose.** `ci/core-ref.json` pins
|
||||
a `main` sha and `checkCoreApi.js` asserts **equality** with what that sha declares. P7 bumps the
|
||||
contract to 1.10.0, so the kit goes red from P7 until the cutover re-pins it. That is the mechanism
|
||||
working — it forces someone to re-read the chapters — and it must be stated in P7's PR body so nobody
|
||||
"fixes" it. The same is true of `module-uo`'s frozen-manifest check for the length of the `edge`
|
||||
window.
|
||||
|
||||
**3. `servuo-plugins` has no CI build.** The plugin compiles only inside ServUO, and the dynamic
|
||||
rebuild can silently reload a stale `Scripts.dll` — so "it booted clean" is not evidence the new code
|
||||
is live. Every plugin-touching phase (P11, P12) verifies against the local tree at
|
||||
`C:\Users\colby\Desktop\ServUO` with a build-offline-first step, and confirms new code is live rather
|
||||
than trusting a clean boot.
|
||||
|
||||
---
|
||||
|
||||
## Cross-cutting obligations, every phase
|
||||
|
||||
Not repeated per phase below. A PR that skips one of these is not done.
|
||||
|
||||
| Obligation | Command / rule |
|
||||
| --- | --- |
|
||||
| `docs/` updated in the same PR | CLAUDE.md: a code change is not complete until `docs/` reflects it |
|
||||
| Server tests | `cd website/server && npm test` |
|
||||
| OpenAPI regenerated when a route changed | `npm run swagger` → committed `swagger/swagger-output.json` |
|
||||
| Route manifest proves no URL moved | `npm run routes:manifest`, zero-line diff in the PR |
|
||||
| No game vocabulary in core | `npm run check:modules` (website root) |
|
||||
| module-uo, when touched | `npm run check:imports`, `npm run check:swagger`, `npm run build --prefix client` **before** `npm run check:externals`, then both test suites |
|
||||
| Conventional Commits + AI disclosure | `type(scope): summary`; `Co-Authored-By:` trailer; tick the PR-template box |
|
||||
| Branch | from an up-to-date `edge`, never a stale one |
|
||||
|
||||
---
|
||||
|
||||
## Ordering rationale — why this shape
|
||||
|
||||
Three choices in the ordering are deliberate and worth stating, because the obvious alternative is
|
||||
wrong in each case.
|
||||
|
||||
**A demoable surface lands at P3, not at P13.** The rich authoring UI is late (P13) and a *minimal*
|
||||
admin surface is early (P3). Splitting the UI in two looks like duplicated work and is not: this
|
||||
project reviews in a browser — the org lead's own review stack exists for exactly that — and ten
|
||||
backend phases with nothing to click is ten phases without feedback. P3 is a list, a form, a start
|
||||
button and a run console. P13 is the timeline editor, the option-source dropdowns and the cap meter,
|
||||
built once the schemas they render actually exist.
|
||||
|
||||
**The module contract (P7) comes after the engine, not before it.** The temptation is to design the
|
||||
seam first. But `registerEventActions` is shaped by what the runner actually needs to hand an action
|
||||
and what it does with the answer, and both are unknown until P2 and P6 are built. Core registers its
|
||||
own `core.announce` / `core.wait` / `core.cue` through the *same* registry from P1 — exactly as
|
||||
`registries.registerCore()` already does for streams and triggers — so the seam is exercised on every
|
||||
boot long before a module uses it, and P7 is a generalisation of something working rather than a
|
||||
guess.
|
||||
|
||||
**Caps (P6) come before the module contract, and before any world write.** A cap is cheap to add to an
|
||||
empty system and expensive to retrofit onto a live one, and it is the single control that bounds the
|
||||
two failures scheduling makes worse: a typo and a compromised session. P6 is a small phase placed
|
||||
early on purpose.
|
||||
|
||||
---
|
||||
|
||||
## The phases
|
||||
|
||||
### 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.
|
||||
|
||||
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:
|
||||
|
||||
- **N1** is an amendment to [`../link/ADMIN_CONTROLS.md`](../link/ADMIN_CONTROLS.md) §8, recorded
|
||||
there as an amendment with its date and reasoning — **not** a silent reversal. §8 is cited by name
|
||||
in four places; a decision that contradicts it and leaves it standing is worse than either decision.
|
||||
- **N3** (does the event runner become the `kind: 'scheduled'` evaluator) closes
|
||||
`ENGAGEMENT.md` §7.1 Q6, which currently reads "no evaluator yet". If the answer is yes, that row
|
||||
changes in the same PR.
|
||||
|
||||
**Ships:** the design of record, and two documents that stop contradicting each other.
|
||||
**Verify:** `docs` CI link check; every relative link resolves.
|
||||
|
||||
---
|
||||
|
||||
### Phase 1 — Schema, CRUD and the core action registry (`website` + `docs`)
|
||||
|
||||
The six tables that do not depend on the module contract: `event_definitions`, `event_series`,
|
||||
`event_versions`, `event_runs`, `event_run_steps`, `event_run_log`. Admin CRUD, publish (which
|
||||
snapshots a version), archive. `router/v1/admin/events.router.js` + `events.controller.js`, models as
|
||||
`.model.js` / `.db.js` pairs under `model/events/`.
|
||||
|
||||
**The registry lands here, with core as its first registrant.** `modules/registries.js` gains
|
||||
`registerEventActions` staging and commit, and `registries.registerCore()` registers three core-owned
|
||||
actions: `core.announce` (post to an announce leg / broadcast target), `core.wait` (a timed no-op) and
|
||||
`core.cue` (post an instruction and wait for a human). None of them execute yet — P2 is what runs a
|
||||
step — but the registry, the id grammar, the risk classes and the param validation are all live and
|
||||
exercised on every boot.
|
||||
|
||||
**Ships:** nothing user-visible; the site is unchanged. A run row can be created and stays
|
||||
`scheduled` forever, which is correct for this phase and must be visible as such rather than looking
|
||||
broken.
|
||||
**Verify:** `npm test`; the registry's collision and validation paths tested the way
|
||||
`registries.js`'s existing members are; routes manifest and swagger regenerated.
|
||||
|
||||
**Trap:** `register()` must not touch the database (`MODULE_API.md` §2.2) — `routeManifest.js` and
|
||||
`swagger.js` both require `app.js` against a dead pool. Core's own action registration is subject to
|
||||
the same rule.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2 — The runner (`website`)
|
||||
|
||||
`utils/eventRunner.js`, the eighth poller: same `setInterval` + `unref()` + `stop()` shape as the
|
||||
other seven, wired into `server.js`'s start and shutdown beside `engagementWorker`.
|
||||
|
||||
Its tick, in order: **materialise** due occurrences (`INSERT IGNORE` against
|
||||
`UNIQUE (definition_id, scope, scheduled_for)`); **advance** runs (CAS `scheduled → starting`,
|
||||
`running → ending`, materialise the phase's steps); **drain** due steps (CAS `pending → running` with a
|
||||
lease, dispatch, classify, record). Executes the three core actions from P1.
|
||||
|
||||
Also here: `missed` and the grace window, the lease and its reclaim, `concurrency_key` rendered from
|
||||
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.
|
||||
|
||||
**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
|
||||
stale row to its start state made `MAX_ATTEMPTS` unreachable, so the row cycled forever, never
|
||||
terminal, therefore never retention-eligible.
|
||||
- **The unique index, not the claim, is what prevents a double run.** The claim decides *who*
|
||||
advances an occurrence; the index is what stops two existing.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3 — The minimal admin surface (`website`)
|
||||
|
||||
`client/src/routes/admin/views/EventsAdmin.jsx`, `EventEditor.jsx`, `EventRun.jsx`, plus the nav rows.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
`core.cue`'s confirm button lands here, which is what makes the GM cue usable.
|
||||
|
||||
**Ships:** an admin can author, schedule, start and watch an event that announces things and cues a
|
||||
human. **This is the first phase with a demo**, and it is the one to put in front of the org lead
|
||||
before building further.
|
||||
**Verify:** the client test suite; a browser walk on the local review stack.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4 — Schedule, recurrence and the calendar (`website` + `docs`)
|
||||
|
||||
The closed recurrence shapes — `once`, `weekly`, `monthly` (nth weekday), `manual` — computed in the
|
||||
definition's **IANA timezone** and stored as UTC in `scheduled_for`. `event_series` becomes usable: a
|
||||
definition may belong to a series, and the series has an ordering.
|
||||
|
||||
An admin calendar view (month + list), filtered by state, scope and series.
|
||||
|
||||
**Ships:** recurring and scheduled events, and the calendar that replaces the thing this feature
|
||||
exists to replace.
|
||||
**Verify:** `npm test` with **DST-crossing cases as explicit fixtures** — a Friday 20:00 event in
|
||||
`Europe/Berlin` computed across the March and October transitions, and one in a zone with no DST at
|
||||
all.
|
||||
|
||||
**Trap:** the temptation is a cron string. There is no cron parser in the server dependency tree, the
|
||||
only precedent is in the bot (a different process), and a cron expression is the one field an operator
|
||||
cannot proofread. Closed shapes render as a form.
|
||||
|
||||
---
|
||||
|
||||
### Phase 5 — Conditions and phase advancement (`website` + `docs`)
|
||||
|
||||
Phase advance on `{ after: '30m' }` and on `{ on: '<triggerId>', where: <conditions>, count: n }`,
|
||||
reusing `engagement/conditions.js` **unchanged** — its grammar, its type checking against the
|
||||
declaration, its depth and list bounds, and its operator labels.
|
||||
|
||||
The runner subscribes to the trigger stream it already has; `event_run_log` gains
|
||||
`condition.evaluated` rows for both outcomes.
|
||||
|
||||
**The diagnosis panel lands here**, and it is the phase's real deliverable: the operator question
|
||||
*"why didn't phase 3 start?"* answered in the condition builder's own words — `gte` renders as *"is at
|
||||
least"*, `present` as *"is present"* — with the tally, the elapsed time and the last related event.
|
||||
|
||||
**Ships:** multi-phase events that advance on what happens in the game rather than only on a clock.
|
||||
**Verify:** `npm test`; a rig run where a phase legitimately does not advance, confirming the panel
|
||||
explains why without a server log.
|
||||
|
||||
**Trap:** a condition is validated at **save** against the trigger's declaration, with the offending
|
||||
variable named — not at evaluation. A predicate that silently reads `undefined` is a phase that
|
||||
silently never advances, and the day you find out is the night of the event.
|
||||
|
||||
---
|
||||
|
||||
### Phase 6 — Enablement, caps and `mayInvoke` (`website` + `docs`)
|
||||
|
||||
`event_action_settings` (one row per registered action, everything above `notify` disabled by default)
|
||||
and `event_run_budget` with the conditional increment:
|
||||
|
||||
```sql
|
||||
UPDATE event_run_budget SET consumed = consumed + ?
|
||||
WHERE run_id = ? AND dimension = ? AND consumed + ? <= cap
|
||||
```
|
||||
|
||||
A breach is `refused` — the step does not run, does not retry, and is surfaced to the author with the
|
||||
dimension and the numbers, because "you asked for 40 and this deployment allows 30" is an authoring
|
||||
error, not an outage.
|
||||
|
||||
**The whole authorisation decision moves behind one function**, `mayInvoke(user, action, run)`: role,
|
||||
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.
|
||||
|
||||
**Ships:** an admin switchboard, and a system that cannot be made to do an unbounded amount of
|
||||
anything.
|
||||
**Verify:** `npm test`, including two concurrent steps against one cap proving neither over-spends;
|
||||
a 403 walk across all four roles on every route.
|
||||
|
||||
---
|
||||
|
||||
### Phase 7 — The module contract (`website` + `docs` + a throwaway test module)
|
||||
|
||||
Generalise P1's registry into the public contract: `registerEventActions` with `cost`, `risk`,
|
||||
`reversible`, `budgetMs`, `params` and `perform` / `revert`; `registerEventBudgets`;
|
||||
`registerEventLeases`; and param **option sources**. `MODULE_API_VERSION` → **1.10.0**, with §2.4 and
|
||||
§1.1 written the way every other member is.
|
||||
|
||||
Dispatch through the envelope: a rejected promise, a throw, a timeout, a non-object and a missing
|
||||
`ok` are all read as `{ ok: false, retry: true }` — the inverse of `registerTeamProvider`'s default,
|
||||
because here the expensive mistake is recording a world change that did not happen.
|
||||
|
||||
`verify: true` — dry run — is a required parameter a module must honour, with the test in the kit.
|
||||
|
||||
**Prove it with a throwaway module, not with module-uo.** A contract validated only against the module
|
||||
it was carved out of has not been validated, and P9 should be the *second* consumer of this seam.
|
||||
|
||||
**Ships:** the seam. Core still does everything it did before.
|
||||
**Verify:** `npm test`; the throwaway module exercising every failure shape; `check:modules` green.
|
||||
|
||||
> **This phase turns the integration kit red** and that is the mechanism, not a bug. `checkCoreApi.js`
|
||||
> asserts equality against the pinned `main` sha; it stays red until the cutover re-pins it. Say so in
|
||||
> the PR body.
|
||||
|
||||
---
|
||||
|
||||
### Phase 8 — The resource ledger, leases and cleanup (`website` + `docs`)
|
||||
|
||||
`event_run_resources` with `lease_until`, the `drifted` status, and
|
||||
`UNIQUE (owner_module, kind, ref)` among non-reverted rows — which is what makes two events unable to
|
||||
lease one target, and produces a `refused` step at authoring time rather than a corrupted baseline at
|
||||
runtime.
|
||||
|
||||
**Record before confirm.** The step writes a `pending` row, dispatches, and promotes it on the answer.
|
||||
Recording afterwards makes every object whose acknowledgement was lost invisible to cleanup forever —
|
||||
so reverting a resource that does not exist must be a *success*.
|
||||
|
||||
Cleanup steps are **generated from the ledger** at teardown, on every terminal path — completion,
|
||||
cancellation and abort alike. `cleanup_status` is its own column: a run reaches `completed` with
|
||||
`cleanup_status = 'incomplete'` and stays on the admin screen, rather than being held `running`.
|
||||
|
||||
Reconcile-on-reconnect: the runner asks each ledgered resource's module what is still in force.
|
||||
|
||||
**Ships:** the safety property the whole world-write half depends on. Also useful on its own — the
|
||||
platform gains a durable record of what it changed.
|
||||
**Verify:** `npm test`; a rig run that kills the process mid-run and confirms cleanup completes on
|
||||
restart; a run whose revert fails and stays visible.
|
||||
|
||||
---
|
||||
|
||||
### Phase 9 — UO wave 1: the actions that need no protocol change (`module-uo` + `docs`)
|
||||
|
||||
`module-uo` registers its first event actions over the write plane that already exists:
|
||||
`uo.broadcast`, `uo.towncrier.post`, `uo.news.post`. Option sources answered **from the spawn
|
||||
atlas** — `uo.options.regions`, `uo.options.landmarks`, `uo.options.creatures` — which cost nothing
|
||||
new and work with the shard down.
|
||||
|
||||
**Ships:** the first end-to-end event against a real shard: scheduled, announced in-game and on the
|
||||
site, cued to a GM, completed, recorded.
|
||||
**Verify:** the whole rig — ServUO + sidecar + website — running a real two-phase event.
|
||||
|
||||
**Trap worth writing into the action declarations.** These three verbs have *different* idempotency.
|
||||
`towncrier` and `news` are keyed by id and re-posting replaces, so a retry is safe. **`broadcast` is
|
||||
not** — a retry is a second announcement to everyone online — and there is no idempotency key on the
|
||||
wire until P11. So `uo.broadcast` ships with `on_failure: 'skip'` rather than a retry, and the
|
||||
declaration says why.
|
||||
|
||||
---
|
||||
|
||||
### Phase 10 — Integrations (`website` + `docs`)
|
||||
|
||||
Core registers its own `event.` triggers — `run.scheduled`, `run.started`, `phase.changed`,
|
||||
`run.ending`, `run.completed`, `run.cancelled`, and `run.failed` at `ceiling: 'admin'` — with seeded
|
||||
templates through the mechanism `registerEngagementSeeds` already provides. Events owns **none** of the
|
||||
delivery.
|
||||
|
||||
`event_run_participants`, results publication, and the `core.announce.post` action that links an
|
||||
existing post to a run and enqueues it through `announce_jobs` — so the in-game town crier and Discord
|
||||
both come free as already-registered legs with retry and classification.
|
||||
|
||||
`ENGAGEMENT.md` §8.6's row — *"a scheduled event is starting · ❌ needs a manual/scheduled trigger
|
||||
type"* — is resolved in this PR.
|
||||
|
||||
**Ships:** every announcement channel the platform has, for every event, per user preference.
|
||||
**Verify:** `npm test`; a mail-catcher rig confirming an event announcement reaches email, in-app and
|
||||
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`)
|
||||
|
||||
**Gated on §N1.** 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
|
||||
asked**; a run-scoped participation ledger; `champ.boss.killed` as a first-class kind.
|
||||
- **`overlay.toml`** protocol version bumped **in this PR** — the installer refuses to pair a sidecar
|
||||
and an overlay that disagree, so a bump in a later PR means the next bundle silently fails to
|
||||
compose.
|
||||
- **Sidecar:** `PROTOCOL_VERSION` bumped, the new fields carried, the new kind stored and served.
|
||||
- **`installer`:** joins the phase because of the pairing above.
|
||||
- **`docs`:** a new `docs/link/v6.md` as the spec of record, plus `INTEGRATION.md`.
|
||||
|
||||
**Ships:** the game side can refuse a duplicate command and can put the world back on its own. Both
|
||||
are safety properties, not features.
|
||||
**Verify:** against the local ServUO tree with a **build-offline-first** step and confirmation the new
|
||||
code is live; a deliberate duplicate command; a lease whose website is killed before it expires,
|
||||
proving baseline returns anyway.
|
||||
|
||||
---
|
||||
|
||||
### 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
|
||||
either owned or borrowed:
|
||||
|
||||
| Verb | Owned or borrowed | Cap dimension |
|
||||
| --- | --- | --- |
|
||||
| Named, hued creatures from the atlas's ~800 constructible types | owned — deleted by serial | `uo.creatures` |
|
||||
| "Simple" boss variants, as event-owned creature templates | owned | `uo.bosses` |
|
||||
| Oracle NPCs with scripted dialogue | owned | `uo.npcs` |
|
||||
| Temporary gates | owned, with a deadline | `uo.gate.minutes` |
|
||||
| Temporary decoration lockdown | owned | `uo.decor` |
|
||||
| A live config value | **borrowed** — a lease | — |
|
||||
| A property on an existing object | **borrowed** — a lease | — |
|
||||
| Seasonal-event toggle | borrowed | — |
|
||||
| World save | neither — a one-shot | — |
|
||||
| Item grant | owned, `reversible: 'none'` | `uo.rewards` |
|
||||
|
||||
**The lease allowlist ships with a boot-time self-check.** 258 `Config.Get` call sites in ServUO split
|
||||
between live reads and values cached at type initialisation, and a lease on the second kind applies
|
||||
cleanly and does nothing. Each key sets, reads back and restores at boot, and drops itself from the
|
||||
advertised catalog if it does not take — a capability that disappears loudly beats one that lies.
|
||||
|
||||
**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`
|
||||
rather than a silent revert of their change.
|
||||
|
||||
---
|
||||
|
||||
### Phase 13 — The authoring UI proper (`website`)
|
||||
|
||||
Replaces P3's placeholders. The phase timeline (**not** a node graph — the condition grammar has no
|
||||
branching and a canvas would advertise power the engine does not have); the step editor rendering each
|
||||
action's declared params, with option-source dropdowns; the live cap meter; dry run; rehearsal.
|
||||
|
||||
**Ships:** an administrator can build the invasion without touching JSON, which is the acceptance
|
||||
criterion the whole feature was asked for.
|
||||
**Verify:** the client suite; a browser walk authoring a multi-phase event end to end with no raw JSON
|
||||
at any point.
|
||||
|
||||
---
|
||||
|
||||
### Phase 14 — Public, player and mobile (`website` + `android-app` + `docs`)
|
||||
|
||||
`GET /public/events`, `/public/events/:slug`, `/public/events/series/:slug`,
|
||||
`/player/events/history`, and an `events` capability string.
|
||||
|
||||
**One prerequisite fix, in this phase and not after it.** The Android app hardcodes
|
||||
`api/v1/public/shard/stream` — a *module* path — and reads `/public/modules` nowhere, so it cannot
|
||||
render a site whose module it has never heard of. That is a small fix and it blocks the app seeing
|
||||
events at all.
|
||||
|
||||
**Ships:** the public calendar, event pages with their storyline and results, participation history,
|
||||
and the app.
|
||||
**Verify:** the client and app suites; an emulator walk.
|
||||
|
||||
---
|
||||
|
||||
### Phase 15 — Integration Kit chapter 5 (`integration-kit`)
|
||||
|
||||
An event-capable module. Teach and link out; re-specify nothing — `EVENTS.md` and `MODULE_API.md` stay
|
||||
normative.
|
||||
|
||||
The four things a second module's author will get wrong, one paragraph each, all invisible until an
|
||||
outage: the envelope's failure default, the idempotency passthrough, recording a resource **before**
|
||||
confirming it, and under-declaring `cost`.
|
||||
|
||||
**Like Teams Phase 11, this cannot merge until the cutover exists** — the kit is pinned to a `main`
|
||||
sha, and the contract it teaches is not on `main` until then.
|
||||
|
||||
---
|
||||
|
||||
### Phase 16 — Acceptance walk and cutover
|
||||
|
||||
**The walk first, against released artefacts, not a working tree.** The whole rig — ServUO, sidecar,
|
||||
website, emulator — running a real multi-phase event, including three deliberate failures:
|
||||
|
||||
1. a mid-run process restart, proving the run resumes and no step double-executes;
|
||||
2. a sidecar kill mid-phase, proving the run degrades rather than failing and world writes park;
|
||||
3. a cap breach, proving `refused` and an author who is told why.
|
||||
|
||||
Then `edge` → `main`, in the order every previous cutover used: the protocol side first, the module,
|
||||
core, docs, then the kit's re-pin and `runicgateway.com`.
|
||||
|
||||
**Two documents that are cutover-window work by construction.**
|
||||
- **`runicgateway.com`** — `checkFacts` reads `main`, so any claim about events is unverifiable until
|
||||
the cutover lands. Same 12a/12b split the engagement workstream needed.
|
||||
- **`.profile`** — the org landing page is updated when the *shape* of the project changes, which a new
|
||||
subsystem is.
|
||||
|
||||
---
|
||||
|
||||
## What this plan does not do
|
||||
|
||||
Stated so the omissions are choices rather than oversights.
|
||||
|
||||
- **No node-graph editor** (P13). The engine has no branching.
|
||||
- **No delegation, grants or proposal queue.** Permissions gate on the existing admin roles. P6's
|
||||
`mayInvoke` keeps it a cheap later option.
|
||||
- **No points.** No core points system exists and no write path to any loyalty board exists.
|
||||
- **No event invoking another event.** It already works by composition — a second event's condition can
|
||||
be `event.run.completed` — and a direct edge would need cycle detection and a story about cancelling
|
||||
a parent.
|
||||
- **No core SSE.** The run console polls. Adding a live channel to core is a larger change than this
|
||||
feature needs, and if it is ever built it should be core infrastructure with module-uo's two streams
|
||||
migrating onto it.
|
||||
- **No mutation of shard-owned content without a baseline.** Loot tables and arbitrary
|
||||
`[set` / `[get` / `[add` stay excluded.
|
||||
|
||||
---
|
||||
|
||||
## Dependency summary
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
P0["P0 · design of record"] --> P1["P1 · schema + registry"]
|
||||
P1 --> P2["P2 · the runner"]
|
||||
P2 --> P3["P3 · minimal admin UI<br/><b>first demo</b>"]
|
||||
P3 --> P4["P4 · schedule + calendar"]
|
||||
P2 --> P5["P5 · conditions + diagnosis"]
|
||||
P2 --> P6["P6 · enablement + caps"]
|
||||
P6 --> P7["P7 · module contract<br/>MODULE_API 1.10.0"]
|
||||
P7 --> P8["P8 · ledger + leases + cleanup"]
|
||||
P8 --> P9["P9 · UO wave 1<br/>no protocol change"]
|
||||
P5 --> P10["P10 · integrations"]
|
||||
P9 --> P10
|
||||
P0 -.->|"§N1"| P11["P11 ⚠ · protocol bump"]
|
||||
P8 --> P11
|
||||
P11 --> P12["P12 ⚠ · UO world verbs"]
|
||||
P7 --> P13["P13 · authoring UI"]
|
||||
P4 --> P13
|
||||
P10 --> P14["P14 · public + mobile"]
|
||||
P13 --> P16["P16 · walk + cutover"]
|
||||
P12 --> P16
|
||||
P14 --> P16
|
||||
P7 --> P15["P15 · kit chapter 5"]
|
||||
P15 --> P16
|
||||
```
|
||||
|
||||
**The critical path is P0 → P1 → P2 → P6 → P7 → P8 → P11 → P12 → P16.** Everything else can run
|
||||
beside it. P3, P4, P5 and P10 are the phases that could be picked up by a second pair of hands without
|
||||
blocking the spine.
|
||||
|
||||
---
|
||||
|
||||
## If only part of this gets built
|
||||
|
||||
Three defensible stopping points, in case scope has to shrink.
|
||||
|
||||
**Stop after P5** — a scheduled, multi-phase, condition-advanced event engine that announces through
|
||||
core's own actions and cues a human for anything in-world. No module contract, no ledger, no protocol
|
||||
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.
|
||||
|
||||
**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