docs(modules): the module-rust plan — 18 decisions of record and a 21-phase schedule #249
@@ -1,7 +1,8 @@
|
||||
# `module-rust` — the plan
|
||||
|
||||
**Status:** approved in outline 2026-09-15, not started. Six decisions of record settled; two
|
||||
questions open (§3).
|
||||
**Status:** approved in outline 2026-09-15, not started. **Fourteen decisions of record**; two
|
||||
questions deferred by choice (§3). Audited against the whole contract, not just the game-facing
|
||||
chapters (§7).
|
||||
|
||||
The [dry run](../rust-dryrun.md) designed this module on paper and deliberately did not build it.
|
||||
This is the document that builds it. Where the two disagree, this one is later and wins — but the dry
|
||||
@@ -98,7 +99,7 @@ teardown. This is neither: it is *continuously reconciled state where the websit
|
||||
and its nearest relative in the contract is the Team provider **inverted** — instead of core asking
|
||||
the module what the game knows, the module tells the game what the site knows. The mechanism it
|
||||
borrows is chapter 4's: **every board's current state has exactly one producer, and it runs on
|
||||
connect**, pointed the other way. Whether this deserves a sixth chapter is a question for phase 10,
|
||||
connect**, pointed the other way. Whether this deserves a sixth chapter is a question for phase 19,
|
||||
after it has been built once.
|
||||
|
||||
### R3 — the Kits reward action registers always and refuses with a reason
|
||||
@@ -148,7 +149,7 @@ same declaration under whatever name the variant gives it, and the refusal is th
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Basic functionality is built on **Rust's own clan system**; the
|
||||
third-party **uMod Clans plugin** is an optional layer for a much richer experience, and lands in
|
||||
phase 9 rather than phase 6.
|
||||
phase 17 rather than phase 9.
|
||||
|
||||
The distinction matters more than the names suggest, so it is worth stating precisely:
|
||||
|
||||
@@ -179,13 +180,13 @@ first-party clans answer all three. Feeding the Team provider from first-party c
|
||||
**permanent, not a first step**.
|
||||
|
||||
What the plugin genuinely adds is **alliances and clan/alliance chat** — richer in *features*, not in
|
||||
roster data. That is what phase 9's adapter surfaces, and it sits beside the Team provider rather
|
||||
roster data. That is what phase 17's adapter surfaces, and it sits beside the Team provider rather
|
||||
than under it.
|
||||
|
||||
One route is deliberately not taken: the plugin persists to its own files under `oxide/data/`, and a
|
||||
determined integration could read those directly. **That is reaching into another plugin's private
|
||||
storage, not using an API** — it breaks without warning on any upstream refactor and is not a
|
||||
contract anybody owes us. If phase 9 wants roster data from the plugin, the honest path is an
|
||||
contract anybody owes us. If phase 17 wants roster data from the plugin, the honest path is an
|
||||
upstream pull request adding a read method, not a file reader.
|
||||
|
||||
### R6 — the required base set is Kits, Clans and PopupNotifications, all k1lly0u
|
||||
@@ -218,7 +219,7 @@ including the three things the event work actually needs:
|
||||
It also **raises `OnKitRedeemed(BasePlayer player, string kitName)`**, which the bridge can listen on
|
||||
to report a redemption as an ordinary event, whoever triggered it.
|
||||
|
||||
**Two traps in `GiveKit` that phase 8 must handle, both found by reading it rather than by reasoning
|
||||
**Two traps in `GiveKit` that phase 13 must handle, both found by reading it rather than by reasoning
|
||||
about it.**
|
||||
|
||||
**`GiveKit` returns `null` on a failure path, and `null` is Oxide's idiom for "no opinion".** The
|
||||
@@ -231,11 +232,11 @@ a success.**
|
||||
|
||||
**`GiveKit` takes a `BasePlayer`, so the player must be connected.** There is no offline grant in
|
||||
this API. An event that rewards participants at two in the morning rewards only whoever is online at
|
||||
that moment, silently. Phase 8 has to choose: accept online-only and say so in the action's
|
||||
that moment, silently. Phase 13 has to choose: accept online-only and say so in the action's
|
||||
description, or keep our own persisted pending-grant queue in the bridge and redeem it on next
|
||||
connect. **The queue is the honest answer and it is not free** — it is a second at-most-once store
|
||||
with its own idempotency, which is exactly the machinery chapter 4 says to persist in the world save.
|
||||
Decide it deliberately at phase 8 rather than discovering it from a complaint.
|
||||
Decide it deliberately at phase 13 rather than discovering it from a complaint.
|
||||
|
||||
`PopupNotifications` is small and does exactly one thing: `CreatePopupNotification(string message,
|
||||
BasePlayer player = null, float duration = 0f)`, where a null player makes it global. That is the
|
||||
@@ -244,23 +245,139 @@ server-side notification surface, and it needs no more than that.
|
||||
**The one gap to design around:** the seven first-party hooks carry created, disbanded, added,
|
||||
kicked and left — but **no promote or leader-changed event**. Core's provider requires
|
||||
`getTeamLeaders`, so leadership is read off `LocalClan` at snapshot time rather than tracked from
|
||||
transitions. That makes phase 6 *partly* snapshot-driven where the dry run predicted it would be
|
||||
transitions. That makes phase 9 *partly* snapshot-driven where the dry run predicted it would be
|
||||
fully event-driven — a small correction to that document, recorded here rather than silently.
|
||||
|
||||
### R7 — the notifications and engagement set ships in v1
|
||||
|
||||
**Decided 2026-09-15 (org lead).** `registerNotificationStreams`, `registerEventTriggers`,
|
||||
`registerAudiences` and `registerEngagementSeeds` are all in the first release, plus
|
||||
`registerAnnounceLeg` and `registerPostHook`.
|
||||
|
||||
They are a **matched set**, which is why they are one decision and one phase: a trigger declares the
|
||||
payload contract and the widest audience a rule on it may ever be given, an audience resolves *people*
|
||||
over module data, and seeds ship the bodies and the rules that use them. Three rules from the kit
|
||||
that this phase lives or dies on:
|
||||
|
||||
- **`ceiling` is required, has no default, and is not a ladder.** A `staff` ceiling does **not** permit
|
||||
`owner`, because "one person" for a cheat-detection event is *the player it was detected on*. Fewer
|
||||
people is not less exposure.
|
||||
- **`subjectKey` must name one of your declared variables** — it is what the cooldown keys on. Core
|
||||
refuses the module at boot if it names nothing, which is the good failure; the bad one it prevents
|
||||
is every subject sharing `undefined`.
|
||||
- **A rule group is offered once, per group key.** A rule appended to an existing group reaches
|
||||
**fresh installs only** — so a rule that must reach existing deployments takes a new group key.
|
||||
|
||||
And the emit discipline: **emit on the transition, not on the poll.** Core's cooldown would hide a
|
||||
module that emitted "the server is still up" as news.
|
||||
|
||||
### R8 — multi-server from the start
|
||||
|
||||
**Decided 2026-09-15 (org lead).** The server list is the landing page and everything else hangs
|
||||
under `/rust/servers/:id`. Every gameplay row carries a server id as well as a `wipe_id`, and the
|
||||
module holds one sidecar client per configured server.
|
||||
|
||||
This is the dry run's finding 2 taken at face value: *"one module, one game" is not the same as "one
|
||||
module, one server"*. Retrofitting an `:id` segment through every route, table and page is the
|
||||
expensive version, and a Rust community runs several servers by default.
|
||||
|
||||
### R9 — the live map, with every layer toggleable
|
||||
|
||||
**Decided 2026-09-15 (org lead).** The site offers a live map, and **each layer is an operator switch
|
||||
set to public / players-only / admin-only** through the existing visibility framework
|
||||
([`SHARD_VISIBILITY.md`](../../website/SHARD_VISIBILITY.md)).
|
||||
|
||||
**The map is two problems and they take different paths, which is the thing to get right before
|
||||
building either:**
|
||||
|
||||
- **The map image is static content on the game host** — `proceduralmap.<size>.<seed>.<save>.map`
|
||||
beside the world save, regenerated only on a wipe. That is exactly [kit][kit] ch. 3 §2b's case, and
|
||||
it takes that shape: **request/reply, never events** (a sidecar that broadcast it would write
|
||||
megabytes into its own store and fan them at every client), **one in flight with an explicit busy**,
|
||||
**two stages — what exists, then what changed**, and a **derivation version** separate from the
|
||||
protocol so improving how we read the file invalidates a cached image whose source hash did not
|
||||
move. And **no import on boot**: a wipe is an event the operator knows about and the website does
|
||||
not.
|
||||
- **Everything moving on it is live state** down the ordinary read path — monuments, cargo ship,
|
||||
patrol helicopter, airdrops, locked crates, and player positions.
|
||||
|
||||
**The layer switches are a security boundary, not a preference.** Public player positions in Rust are
|
||||
a competitive-advantage leak — anyone, including people who do not play on the server, could locate
|
||||
players and infer base positions. The default posture is monuments and world events public, player
|
||||
and base layers admin-only, and an operator opening one up is a deliberate act with the consequence
|
||||
stated on the switch.
|
||||
|
||||
**This is the only asset-bridge work in scope.** Item icons and the 2,590 workshop skin ids stay out
|
||||
of v1; kill feeds and kit lists render as text.
|
||||
|
||||
### R10 — the Android app is in this workstream, capability-driven, trailing by one phase
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Full Rust support in the app, not a degradation check. It decides
|
||||
which screens to show from **`GET /api/v1/public/modules`** — each started module's `capabilities`
|
||||
array — and each app leg lands **one phase after** the website surface it consumes is merged, so it
|
||||
is always built against a real endpoint rather than a planned one.
|
||||
|
||||
Two endpoints that are *not* this and are easy to confuse with it, both checked on 2026-09-15:
|
||||
`/api/v1/public/status` returns site mode plus a version block — the first-run probe and
|
||||
version-mismatch guard, not a feature manifest — and `/api/health` on the internal app is a liveness
|
||||
probe returning `{status:'ok'}`. Neither can say which pages exist.
|
||||
|
||||
§2.9's rule governs: **treat an unknown capability as absent, and never infer a URL from one.**
|
||||
|
||||
### R11 — a small read-only set of Discord slash commands
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Questions answered from data the module already holds — server
|
||||
status, wipe schedule, leaderboards, who is online. No write verbs, and the account link stays on the
|
||||
two surfaces R1 names rather than acquiring a third.
|
||||
|
||||
The trap to carry in from the Teams work: **ephemerality is fixed at the deferral**, so a command
|
||||
that might refuse must defer ephemeral or its refusal goes public in the channel. And the registries
|
||||
have no removal path, which is an argument for adding a command late rather than early.
|
||||
|
||||
### R12 — per-wipe detail plus all-time rollups
|
||||
|
||||
**Decided 2026-09-15 (org lead).** Every gameplay row carries `wipe_id`; leaderboards default to the
|
||||
current wipe; a separate rollup accumulates per player across wipes so a returning player's history
|
||||
survives the monthly reset.
|
||||
|
||||
The truncation is a **runtime operation on a module route, never a schema one** — §2.6's leading-verb
|
||||
allowlist forbids `DELETE` and `TRUNCATE` in a fragment precisely because the fragment replays at
|
||||
every boot and would empty the table on each restart.
|
||||
|
||||
### R13 — two extension slots: `admin.users.detail` and `site.footer.status`
|
||||
|
||||
**Decided 2026-09-15 (org lead).** An operator looking at a user sees their linked Steam identity,
|
||||
per-server stats and site-authored permission grants in core's own admin user page; the footer
|
||||
carries a shard-status indicator on every page.
|
||||
|
||||
One module per slot, so claiming them also reserves them. And the naming rule matters for anyone
|
||||
reading this later: **a slot is named for a PLACE, never for a meaning** — `site.footer.status` is
|
||||
"the status-ish spot in the footer", not core knowing what a game server is.
|
||||
|
||||
### R14 — `/rust` on all three tiers
|
||||
|
||||
**Decided 2026-09-15 (org lead).** `public`, `admin` and `player` all mount `/rust`, exactly as
|
||||
`module-uo` mounts `/uo`. Sub-surfaces are path segments: `/rust/servers/:id`, `/rust/map`,
|
||||
`/rust/clans`.
|
||||
|
||||
Chosen deliberately because **prefixes share one namespace with core's own and the loader's collision
|
||||
probe cannot see all of core's** — several core endpoints are mounted at the tier root rather than
|
||||
under a prefix. A noun from our own domain that equals the module id cannot collide, where
|
||||
`/servers` or `/map` very well might.
|
||||
|
||||
## 3. Open questions
|
||||
|
||||
The base set question is closed by R6. Two smaller ones are open, neither blocking:
|
||||
The fourteen decisions above close every question the contract audit raised. Two remain, both
|
||||
deliberately deferred rather than unanswered:
|
||||
|
||||
**Clans is in the base set *and* the Team provider reads first-party clans.** Those are not in
|
||||
conflict and this plan reads them as complementary: the plugin is installed because a community
|
||||
wants alliances and clan chat, while core's Teams are fed from the first-party system that actually
|
||||
publishes membership transitions (R5). Phase 9's adapter surfaces the plugin's extra features beside
|
||||
the provider rather than under it. **Worth confirming** — it is an interpretation, not something that
|
||||
was stated.
|
||||
**Clans is in the base set *and* the Team provider reads first-party clans.** Not in conflict — the
|
||||
plugin is installed because a community wants alliances and clan chat, while core's Teams are fed
|
||||
from the first-party system that actually publishes membership transitions (R5). **Still an
|
||||
interpretation rather than something stated.**
|
||||
|
||||
**Offline reward grants.** `GiveKit` requires a connected `BasePlayer` (R6). Whether phase 8 accepts
|
||||
online-only or builds a persisted pending-grant queue is a real decision with real cost, and it is
|
||||
deliberately deferred to that phase rather than guessed now.
|
||||
**Offline reward grants.** `GiveKit` requires a connected `BasePlayer` (R6). Whether the reward phase
|
||||
accepts online-only or builds a persisted pending-grant queue is a real decision with real cost, and
|
||||
it belongs in that phase with the code in front of it.
|
||||
|
||||
`D:\rust\oxide\plugins\` is **empty**, so the whole base set is a fresh install in phase 0.
|
||||
|
||||
@@ -290,28 +407,43 @@ re-checks it.
|
||||
|
||||
## 5. The phases
|
||||
|
||||
Phases 0–3 produce a working read-only Rust site. 4–5 are the permissions product. 6 is Teams. 7–8
|
||||
are events. 9 is the optional tier. 10 is how any of it reaches somebody who is not us. Each phase
|
||||
ends with its findings written down, as every workstream here does.
|
||||
**Twenty phases, roughly doubled from the first draft** — the contract audit in §7 is why, and the
|
||||
honest reading is that the first list was a game-bridge plan with a website module bolted on, where
|
||||
the kit treats the module as the bulk of the work.
|
||||
|
||||
0–4 produce a working read-only multi-server Rust site that an operator can actually install. 6–7 are
|
||||
the permissions product. 9 is Teams. 10 is the notifications set. 12–13 are events. 14 is the map. 18
|
||||
is how any of it reaches somebody who is not us. The Android legs (5, 8, 11, 15) each trail the
|
||||
website surface they consume by one phase, per R10.
|
||||
|
||||
Each phase ends with its findings written down, as every workstream here does.
|
||||
|
||||
| # | Phase | Repos | Done when |
|
||||
|---|---|---|---|
|
||||
| 0 | **The rig.** Update to the current wipe (the script is fixed), confirm the Oxide build still matches, install the base set, prove a grant made at the console is visible to a plugin | docs | A current server boots with the base mods loaded and `oxide.grant` demonstrably gates something |
|
||||
| 1 | **Protocol 1 and three skeletons.** Bounded drop-oldest queue, one writer thread, reconnect with a tagged epoch, dial-out; sidecar listener + SQLite + always-on token auth + version header; module from `template/` | all 3 + docs | One hello line travels game → sidecar → module, and killing the sidecar does not stall the game |
|
||||
| 2 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots kept distinct at the wire; `wipe_id` everywhere; every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, with no negotiation |
|
||||
| 3 | **The first pages.** Server list, per-server status, killfeed, leaderboard; `capabilities`; nav | Module-rust | The site renders the last thing the game said while the game is off |
|
||||
| 4 | **Identity** (R1) | 3 + docs | A player links an account in-game and the site names their Steam ID |
|
||||
| 5 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
|
||||
| 6 | **Teams provider** (R5). Rust's first-party clans; membership event-driven, leadership read off `LocalClan` at snapshot | Module-rust + 2 | Core's reconciler is answered from live transitions, `complete` claimed only per reachable server |
|
||||
| 7 | **Events: one budget, one lease.** [kit][kit] ch. 5's own ordering — the lease before the action | Module-rust + 2 | The leased value is observed changing in the running game and restored, per key |
|
||||
| 8 | **Events: the Kits reward action** (R3) | all 3 | A retried step grants loot once, and the ledger and the world agree |
|
||||
| 9 | **Optional mod integrations**, the uMod **Clans** plugin first (R5). One adapter per plugin, each detecting via `[PluginReference]` and degrading to absent | rust-plugins + docs | A server missing every optional mod still runs the module, Teams included |
|
||||
| 10 | **The installer** (R4). `--game servuo|rust`, the bundle payload as a variant, an Oxide prerequisite check in `doctor`, the protocol pairing refusal carried over | installer + docs | An operator sets a Rust server up with the released binary and nothing hand-copied |
|
||||
| 11 | **Docs, kit feedback, cutover** | docs + Integration-kit | `docs/` describes what shipped; R2's missing chapter answered either way |
|
||||
| 0 | **The rig.** Update to the current wipe (the script is fixed), confirm the Oxide build still matches, install the base set (R6), prove a console grant reaches a plugin | docs | A current server boots with Kits, Clans and PopupNotifications loaded and `oxide.grant` demonstrably gates something |
|
||||
| 1 | **Protocol 1, three skeletons, and every bundle seam at once.** Plugin: bounded drop-oldest queue, one writer thread, tagged reconnect epoch, dial-out. Sidecar: listener, SQLite, always-on token auth, version header, rpc correlation. Module: `id: rust`, `/rust` on all three tiers (R14), `schema.sql` **and `purge.sql`**, the vite aliases and shims, `checkExternals`, `checkImports`, the swagger fragment and its staleness check, explicit `onBoot`/`onShutdown`, `capabilities` | all 3 + docs | One hello line travels game -> sidecar -> module; killing the sidecar does not stall the game; all five guards green on an untouched skeleton |
|
||||
| 2 | **Packaging and release.** `release.yml`, the install manifest, the `sha256`, the host allowlist — and a real install into a running core from a manifest URL | Module-Rust + docs | An operator installs the empty module from Admin -> Modules and it reaches `started` |
|
||||
| 3 | **The read path.** First hook wave from [`HOOKS.md`](HOOKS.md); events and snapshots distinct at the wire; `wipe_id` **and server id** on every row (R8); all-time rollups (R12); every board re-emitted on connect | all 3 + docs | A restarted sidecar is fully populated within one connection, and a wipe does not erase a player's history |
|
||||
| 4 | **The first pages.** Server list as the landing page, `/rust/servers/:id` beneath it, killfeed, leaderboard; nav rows; the UI kit (`PublicLayout` `shell`, `PageHeader` props); `capabilities`; the `site.footer.status` slot (R13) | Module-Rust | The site renders the last thing each server said while every server is off |
|
||||
| 5 | **Android leg A** (R10). Capability-driven shell from `GET /api/v1/public/modules`, plus the phase-4 screens | Android-app | The app renders a Rust site it has never seen, and a UO site unchanged |
|
||||
| 6 | **Identity** (R1), and the `admin.users.detail` slot (R13) | 3 + docs | A player links an account in-game; an operator sees the Steam id inside core's own user page |
|
||||
| 7 | **Site-owned permissions** (R2). Groups and grants authored on the site; full set pushed on connect, deltas after; drift reported | all 3 + docs | A grant made on the website gates a third-party plugin in-game, and survives a wipe |
|
||||
| 8 | **Android leg B** (R10). Identity and permission surfaces | Android-app | A player links from the app |
|
||||
| 9 | **Teams from first-party clans** (R5). Membership event-driven, leadership read off `LocalClan` at snapshot; **`declareModuleSlot` × 3** for core's `team.notify` / `team.activity` / `team.forum` | Module-Rust + 2 | The clan page is ours, core's contributions land in places we named, and every slot empty still reads correctly |
|
||||
| 10 | **Notifications and engagement** (R7). Streams, triggers with `ceiling` and `subjectKey`, audiences, engagement seeds, announce leg, post hook | Module-Rust + docs | An operator turns on a rule, edits a body, and a wipe announcement reaches the right people and nobody else |
|
||||
| 11 | **Android leg C** (R10). Inbox and notification preferences for Rust triggers | Android-app | A Rust notification arrives on a phone and can be switched off there |
|
||||
| 12 | **Events: one budget, one verified lease.** [kit][kit] ch. 5's own ordering — the lease before the action | Module-Rust + 2 | The leased value is observed changing in the running game and restored, per key |
|
||||
| 13 | **Events: the Kits reward action** (R3, R6). Includes the offline-grant decision (§3) | all 3 | A retried step grants loot once, and the ledger and the world agree |
|
||||
| 14 | **The live map** (R9). The map image over the bridge — request/reply, two-stage, one in flight, its own derivation version, no import on boot — plus the live layers and a per-layer public/players/admin switch | all 3 + docs | The map renders for the current wipe, and a player layer is invisible until an operator deliberately opens it |
|
||||
| 15 | **Android leg D** (R10). Map and events | Android-app | The map renders on a phone with the same layer gates |
|
||||
| 16 | **Discord slash commands** (R11). A small read-only set, every refusal deferred ephemeral | Module-Rust + docs | A refusal does not go public in the channel |
|
||||
| 17 | **Optional mod integrations.** The uMod **Clans** adapter first — alliances and clan chat, beside the provider rather than under it (R5) — then others, each detecting via `[PluginReference]` and degrading to absent | Rust-Plugins + docs | A server missing every optional mod still runs the module, Teams included |
|
||||
| 18 | **The installer** (R4). `--game servuo|rust`, the bundle payload as a variant, an Oxide prerequisite check in `doctor`, the protocol pairing refusal carried over | installer + docs | An operator sets a Rust server up with the released binary and nothing hand-copied |
|
||||
| 19 | **Docs, kit feedback, cutover.** `docs/`; **`.profile`** (three repos were added); **`runicgateway.com`** (a second game is a headline change); and the Integration-kit question R2 raised | docs + Integration-kit + .profile + runicgateway.com | `docs/` describes what shipped, the front door names the new repos, and R2's missing chapter is answered either way |
|
||||
|
||||
### Why the lease comes before the reward action
|
||||
|
||||
The stated priority is Kits rewards, and this plan still schedules a lease first. [kit][kit] ch. 5 is
|
||||
The stated priority is Kits rewards, and this plan still schedules a lease first (12 before 13). [kit][kit] ch. 5 is
|
||||
explicit about it and the reasoning survives restating: a lease is *a value that already existed,
|
||||
changed for a while, and put back*, so reading it first gives you the baseline for nothing. An action
|
||||
makes something that did not exist. The lease proves the whole command path — correlation, the
|
||||
@@ -321,7 +453,7 @@ failures.
|
||||
|
||||
Reversible on request.
|
||||
|
||||
### What phase 8 must declare honestly
|
||||
### What phase 13 must declare honestly
|
||||
|
||||
**A kit grant cannot be `reversible: 'ledger'`.** That value is a promise that core may come back and
|
||||
have the thing undone, on every terminal path including an abort — and there is no honest way to
|
||||
@@ -355,93 +487,89 @@ is invalidated at once, and **"gone, and that is fine" is a success**, not a fai
|
||||
|
||||
## 7. Contract coverage audit
|
||||
|
||||
Added 2026-09-15 after re-reading the whole kit rather than only chapters 3–5. **The first draft of
|
||||
§5 was built from the game-facing chapters and under-planned the website module by a wide margin.**
|
||||
Every element of the contract is listed below with where it lands; the unplanned ones are the point
|
||||
of the table.
|
||||
Added 2026-09-15 after re-reading the whole kit rather than only chapters 3-5. **The first draft of
|
||||
§5 was built from the game-facing chapters and under-planned the website module by a wide margin** —
|
||||
the template makes eight of the ten non-event registrations and the plan covered three. Every element
|
||||
is listed with the phase it now lands in; the column worth reading is *"was it in the first draft"*,
|
||||
because that is the shape of the mistake.
|
||||
|
||||
### The server handshake — ten registrations plus two hooks (ch. 2)
|
||||
|
||||
| Call | Planned? | Where |
|
||||
| Call | In the first draft | Phase |
|
||||
|---|---|---|
|
||||
| `registerRoutes` | yes | 3 |
|
||||
| `registerTeamProvider` | yes | 6 |
|
||||
| `registerEventBudgets` / `OptionSources` / `Leases` / `Actions` | yes | 7–8 |
|
||||
| `onBoot` / `onShutdown` | **implicit only** | make explicit in 1 |
|
||||
| `registerExtension` | **no** | the dry run's own manifest declared `admin.users.detail` |
|
||||
| `registerNotificationStreams` | **no** | — |
|
||||
| `registerEventTriggers` | **no** | — |
|
||||
| `registerAudiences` | **no** | — |
|
||||
| `registerEngagementSeeds` | **no** | — |
|
||||
| `registerAnnounceLeg` | **no** | — |
|
||||
| `registerPostHook` | **no** | — |
|
||||
| `registerSlashCommands` | **no** | — |
|
||||
| `registerRoutes` | yes | 4 |
|
||||
| `registerTeamProvider` | yes | 9 |
|
||||
| `registerEventBudgets` / `OptionSources` / `Leases` / `Actions` | yes | 12-13 |
|
||||
| `onBoot` / `onShutdown` | implicit only | 1 |
|
||||
| `registerExtension` | **no** | 4 (`site.footer.status`), 6 (`admin.users.detail`) |
|
||||
| `registerNotificationStreams` | **no** | 10 |
|
||||
| `registerEventTriggers` | **no** | 10 |
|
||||
| `registerAudiences` | **no** | 10 |
|
||||
| `registerEngagementSeeds` | **no** | 10 |
|
||||
| `registerAnnounceLeg` | **no** | 10 |
|
||||
| `registerPostHook` | **no** | 10 |
|
||||
| `registerSlashCommands` | **no** | 16 |
|
||||
|
||||
**The template registers eight of these and the plan covered three.** Triggers, audiences and
|
||||
engagement seeds are in `template/server/index.js` as a matched set — a trigger declares the payload
|
||||
and its audience `ceiling`, an audience resolves people over module data, and seeds ship the bodies
|
||||
and rules. None of the three was in §5.
|
||||
Triggers, audiences and seeds are one phase because they are a **matched set** in
|
||||
`template/server/index.js`, not three independent gaps — see R7.
|
||||
|
||||
### The bundle's own parts
|
||||
|
||||
| Part | Planned? | Note |
|
||||
| Part | In the first draft | Phase |
|
||||
|---|---|---|
|
||||
| `module.json` `id` / `coreApi` / `capabilities` | yes | 1, 3 |
|
||||
| **`mounts` and prefix choice** | **no** | shares one namespace with core's, and the loader's probe cannot see core's root-mounted endpoints |
|
||||
| `schema.sql` | yes | 2 |
|
||||
| **`purge.sql`** | **no** | required whenever a schema ships |
|
||||
| **`swagger-fragment.json` + generator + `check:swagger`** | **no** | fixed filename, merged verbatim, must be committed and checked current |
|
||||
| **`vite.config.js` aliases / shims / `checkExternals`** | **no** | the kit calls this the highest-risk mechanical detail in the system |
|
||||
| **`checkImports.js`** | **no** | the guard that makes a reach into core's tree fail the build |
|
||||
| **`release.yml`, install manifest, `sha256`, host allowlist** | **no** | there was no packaging phase at all |
|
||||
| `module.json` `id` / `coreApi` / `capabilities` | yes | 1, 4 |
|
||||
| `mounts` and prefix choice | **no** | 1 (R14) |
|
||||
| `schema.sql` | yes | 1, 3 |
|
||||
| `purge.sql` | **no** | 1 |
|
||||
| `swagger-fragment.json` + generator + `check:swagger` | **no** | 1 |
|
||||
| `vite.config.js` aliases / shims / `checkExternals` | **no** | 1 |
|
||||
| `checkImports.js` | **no** | 1 |
|
||||
| `release.yml`, install manifest, `sha256`, host allowlist | **no** | **2** — there was no packaging phase at all |
|
||||
|
||||
Phase 1 carries most of these on purpose. [kit][kit] ch. 1's whole argument is to get every seam
|
||||
working at once with almost nothing in them, so that afterwards you break exactly one at a time.
|
||||
|
||||
### The client half
|
||||
|
||||
| Part | Planned? | Note |
|
||||
| Part | In the first draft | Phase |
|
||||
|---|---|---|
|
||||
| `registry.registerRoutes` / `registerNav` | yes | 3 |
|
||||
| **`declareModuleSlot`** | **no** | the kit: *"you will need it the moment your game has anything like a guild"* — so phase 6 |
|
||||
| `registerFeatureProvider` | **no** | how a nav row becomes conditional |
|
||||
| **UI kit discipline** | **no** | `PublicLayout` needs `shell`; `PageHeader` takes `eyebrow`/`title`/`lead`/`center` and drops unknown props silently |
|
||||
| `registry.registerRoutes` / `registerNav` | yes | 4 |
|
||||
| `declareModuleSlot` | **no** | 9 — the kit: *"you will need it the moment your game has anything like a guild"* |
|
||||
| `registerFeatureProvider` | **no** | 4 |
|
||||
| UI kit discipline (`PublicLayout` `shell`, `PageHeader` props) | **no** | 4 |
|
||||
|
||||
### Beyond the module
|
||||
|
||||
| Item | Planned? | Note |
|
||||
| Item | In the first draft | Phase |
|
||||
|---|---|---|
|
||||
| Sidecar rpc correlation | implicit | needed by 7–8; state it in 1 |
|
||||
| **Asset bridge (ch. 3 §2b)** | **no** | Rust's item icons and 2,590 skin ids are exactly the "content already on the game host" case |
|
||||
| **`.profile` landing page** | **no** | three repos added is precisely when it is owed |
|
||||
| **`runicgateway.com`** | **no** | a second game is a headline platform change |
|
||||
| **Android app** | **no** | it feature-detects `capabilities` and must render a site whose module it has never heard of |
|
||||
| `docs/` | yes | 11 |
|
||||
| Sidecar rpc correlation | implicit | 1, stated |
|
||||
| Asset bridge (ch. 3 §2b) | **no** | 14 — **map image only** (R9) |
|
||||
| `.profile` landing page | **no** | 19 |
|
||||
| `runicgateway.com` | **no** | 19 |
|
||||
| Android app | **no** | 5, 8, 11, 15 (R10) |
|
||||
| `docs/` | yes | every phase, plus 19 |
|
||||
|
||||
### What this does to the phase list
|
||||
**What the audit cost the schedule: twelve phases.** That is the honest number, and it is worth
|
||||
recording because the under-planning had one cause — reading the chapters that describe the game
|
||||
bridge and treating the module as the thin part, when the kit says in its first paragraph that the
|
||||
website module is most of the work.
|
||||
|
||||
The gaps are not evenly distributed: most land in the website module, which the original §5 treated
|
||||
as three phases and which the kit treats as the bulk of the work. The phase list needs a packaging
|
||||
phase, a notifications-and-engagement phase, and explicit bundle-mechanics work inside phase 1 —
|
||||
plus decisions on the optional surfaces (slash commands, extension slots, the asset bridge) before
|
||||
they can be scheduled. Those decisions are §8.
|
||||
## 8. Questions the audit raised — all answered
|
||||
|
||||
## 8. Questions this plan cannot answer for itself
|
||||
Every question §7 produced was put to the org lead on 2026-09-15 and answered the same day. They are
|
||||
recorded as R7–R14 in §2 rather than repeated here:
|
||||
|
||||
Open, in rough order of how much they change the schedule. None is blocking phase 0.
|
||||
| Question | Answer | Decision |
|
||||
|---|---|---|
|
||||
| Notifications and engagement in v1? | the full matched set | R7 |
|
||||
| How many servers does the UI support? | multi-server from the start | R8 |
|
||||
| The asset bridge? | **only** the live map — not item icons, not skins | R9 |
|
||||
| Android in this workstream? | full app, capability-driven, trailing by one phase | R10 |
|
||||
| Discord slash commands? | a small read-only set | R11 |
|
||||
| What survives a wipe? | per-wipe detail plus all-time rollups | R12 |
|
||||
| Extension slots? | `admin.users.detail` **and** `site.footer.status` | R13 |
|
||||
| Which mount prefixes? | `/rust` on all three tiers | R14 |
|
||||
|
||||
1. **Notifications and engagement** — does v1 ship triggers, audiences and engagement seeds, or is
|
||||
that a later workstream? The template treats them as core furniture; skipping them means a Rust
|
||||
site that cannot mail anybody about anything.
|
||||
2. **Discord slash commands** — the platform has a bot and `registerSlashCommands` exists. Does the
|
||||
Rust module claim any?
|
||||
3. **Extension slots** — the dry run's manifest declared `admin.users.detail` (a user's Rust
|
||||
identities inside core's admin user page). Keep it?
|
||||
4. **The asset bridge** — item icons and skins over the bridge, or not in v1? It is a whole
|
||||
protocol's worth of work and the site renders without it.
|
||||
5. **Android** — does the app get Rust support in this workstream, or does it stay UO-only until the
|
||||
module ships?
|
||||
6. **Which mount prefixes**, given they share a namespace with core's own.
|
||||
7. **Server scale** — how many Rust servers should the first release support in the UI? The dry run
|
||||
says the module holds one client per server; the pages differ a lot between one and six.
|
||||
8. **Wipe retention** — when a wipe truncates gameplay data, what survives? All-time rollups, or a
|
||||
clean slate each month?
|
||||
The two in §3 are deferred by choice rather than unanswered.
|
||||
|
||||
[kit]: https://gitea.whitlocktech.com/RunicGateway/Integration-kit
|
||||
|
||||
Reference in New Issue
Block a user