diff --git a/modules/rust/PLAN.md b/modules/rust/PLAN.md index bfc74eb..8a7ddef 100644 --- a/modules/rust/PLAN.md +++ b/modules/rust/PLAN.md @@ -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....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