From 9938e71e07d25a44bbeae597fa2dcac2c32f9785 Mon Sep 17 00:00:00 2001
From: wtclaude
Date: Thu, 22 Oct 2026 08:09:22 -0500
Subject: [PATCH] feat(facts): phase 13 - per-game facts, Rust and RunicNPC
checked, main green again
platform.json drops the single protocol/bundle/capability list for a
`games` object (uo, rust) plus a `runicnpc` block. checkFacts.mjs loops
the protocol, plugin-protocol, capability and bundle checks over both
games, reads the schema-2 bundles at v2//current.json, checks each
module's coreApi, and reads RunicNPC's plugin.toml. 41 facts, up from 19;
clears the two that were red on main (Module API 1.17.0, installer
v0.8.0).
D66: the hero, footer and structured data show the Module API only; each
game's protocol and bundle go on that game's pages, and the docs landing
table splits into shared versions plus one column per game.
The re-read section 1 requires found, and this fixes:
- three hard-coded "1.6.0" Module API versions in the module docs
- module-lifecycle saying modules load alphabetically (the loader now
refuses a second module, W19), and the install pages never mentioning
the one-module rule
- "there is one module" in five places, now two modules both built
inside this project; the Integration Kit keeps its draft banner
- the homepage diagram's ServUO/uo-link sub-labels (D63 wording now)
notBuilt drops `second-module` (resolved by Module-Rust's release) and
rewords `multi-module`.
Co-Authored-By: Claude Opus 5.5
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
---
PLAN.md | 58 ++++++-
scripts/checkFacts.mjs | 159 +++++++++++++-----
scripts/seedDemo.mjs | 2 +-
src/components/Footer.astro | 5 +-
src/components/StructuredData.astro | 4 +-
src/components/architecture/ModuleSeam.astro | 21 ++-
src/components/home/Capabilities.astro | 4 +-
src/components/home/DataPath.astro | 27 +--
src/components/home/Hero.astro | 4 +-
.../docs/administration/managing-modules.mdx | 4 +
.../administration/the-shard-connection.mdx | 2 +-
.../docs/architecture/protocol-versions.mdx | 10 +-
.../getting-started/connect-a-game-server.mdx | 4 +-
.../getting-started/install-a-game-module.mdx | 2 +-
.../docs/getting-started/requirements.mdx | 2 +-
src/content/docs/docs/index.mdx | 29 +++-
.../docs/docs/modules/installing-modules.mdx | 7 +
.../docs/docs/modules/module-lifecycle.mdx | 15 +-
.../docs/docs/modules/the-integration-kit.mdx | 4 +-
.../docs/docs/modules/the-module-api.mdx | 3 +-
.../docs/docs/modules/the-module-manifest.mdx | 13 +-
src/data/capabilities.mjs | 9 +-
src/data/notBuilt.mjs | 24 +--
src/data/platform.json | 102 ++++++++---
src/data/reference.mjs | 2 +-
src/pages/architecture.astro | 4 +-
src/pages/community.astro | 7 +-
src/pages/features.astro | 9 +-
src/pages/integrations.astro | 2 +-
29 files changed, 384 insertions(+), 154 deletions(-)
diff --git a/PLAN.md b/PLAN.md
index d6b22db..18eaa9a 100644
--- a/PLAN.md
+++ b/PLAN.md
@@ -237,7 +237,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
-somewhere other than the thing it decided. The count of record is **sixty-five**:
+somewhere other than the thing it decided. The count of record is **sixty-six**:
| # | Where | What it settled |
|---|---|---|
@@ -255,6 +255,7 @@ somewhere other than the thing it decided. The count of record is **sixty-five**
| D58 | §6, "How phase 12 delivered it" | `node_modules` ships in three layers because Cloudflare refuses a request body over 100 MB, and the workflow counts layers before it pushes |
| D59 | §6, "How phase 12 delivered it" | The container publishes on every interface (amending D55), with `SITE_BIND_ADDR` to narrow it |
| D60–D65 | §16, "The six decisions" | Per-game sidebar groups, RunicNPC inside Rust with its own page, the RunicNPC API carried in full with a drift check, game names allowed but game vocabulary confined, a twelfth check that enforces it (amending D53), Rust screenshots last and from the rigs |
+| D66 | §16, "How phase 13 built it" | The hero and footer show the Module API only; a protocol and a bundle belong to a game |
---
@@ -1609,11 +1610,12 @@ a mechanism rather than diligence:
| Fact | Authority |
|---|---|
- | Protocol version | `link` `main:sidecar/src/main.rs` → `PROTOCOL_VERSION` |
- | Overlay protocol | `servuo-plugins` `main:overlay.toml` → `protocol` |
+ | Protocol version, per game | the game's sidecar repo (`link`, `Rust-Link`) `main:sidecar/src/main.rs` → `PROTOCOL_VERSION` |
+ | Plugin protocol, per game | the game's plugin repo (`servuo-plugins`, `Rust-Plugins`) `main:overlay.toml` → `protocol` |
| Module API | `website` `main:server/src/modules/version.js` → `MODULE_API_VERSION` |
- | Module capability list | `Module-uo` `main:module.json` → `capabilities` (added in phase 3) |
- | Bundle + component pins | `installer` branch `bundles`, **root** `current.json` |
+ | Module capabilities + `coreApi`, per game | `Module-uo` / `Module-Rust` `main:module.json` (capabilities added in phase 3, both games and `coreApi` in phase 13) |
+ | Bundle + component pins, per game | `installer` branch `bundles`, **`v2/servuo/current.json`** and **`v2/rust/current.json`** (phase 13; the root `current.json` is the frozen schema-1 manifest) |
+ | RunicNPC API, floors, required plugins | `runicnpc-rust` `main:plugin.toml`, and the Rust bundle's `npc` block (phase 13) |
| Release versions | Gitea releases API per repo |
Same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`: when the platform
@@ -1974,6 +1976,52 @@ module collects, Steam ids first, before anything is claimed there. `checkTerms.
**Phase 18 — screenshots** (D65), plus the emulator pass for `/app/`'s Rust screens. This is the only
phase that needs the rigs.
+### How phase 13 built it
+
+One decision, **D66**, taken 2026-10-22 when the restructure reached the homepage. The count of
+record is now **sixty-six**.
+
+**D66 — the hero and the footer show the Module API only.** Both used to read "Protocol 8 · Bundle
+2026.09.15", which was a description of the platform while there was one game. There are now two
+protocols and two bundles, and neither belongs on the front door. The hero and footer show **Module
+API**, the one version every deployment shares. The structured data's `softwareVersion` reads the
+same value. Each game's protocol and bundle are on that game's pages, and the docs landing page
+splits its table in two: shared versions, then one column per game. The alternative was per-game
+chips on the hero ("Ultima Online: protocol 8, bundle … · Rust: protocol 14, bundle …"). It was
+declined as busier and as putting game facts on the front door.
+
+**`platform.json` is per game.** `protocol`, `bundle` and `moduleUoCapabilities` are gone. In their
+place is a `games` object (`uo` and `rust`, each with its protocol, its sidecar and plugin repos, its
+module's capabilities and `coreApi`, and its bundle pins), plus a `runicnpc` block for the plugin's
+API version, framework floors and required plugins. `releases` stays flat, so every repository's
+release is read the same way. **41 facts** are now checked, up from 19. Checks 1, 2, 4 and 5 loop
+over the games, and the bundle reads `v2//current.json` and refuses anything but schema 2.
+
+**What the re-read found** (§1 obliges a re-read of every page that quotes a changed value):
+
+- **Three hard-coded Module API versions in prose**, all saying 1.6.0, on *The module API* and *The
+ module manifest*. §12 has forbidden that since phase 1. Nothing caught it because `checkFacts`
+ guards `platform.json` and not the prose. All three now read `platform.json`. The manifest page's
+ worked example of a caret range now quotes the two modules' real `coreApi` ranges, which are
+ checked facts as of this phase.
+- ***Module lifecycle* said modules load alphabetically.** That stopped being the whole truth on
+ 2026-09-23, when the installer began refusing a second module, and stopped being true at all on
+ 2026-10-22, when the loader began refusing one (W19). The page now says what the loader does. *Installing
+ modules* and *Managing modules* had never mentioned the one-module rule at all, and now do.
+- **"There is one module" was false in five places.** Those were the `/architecture/` seam drawing and its
+ "what this does not yet prove", `/community/`, the `/features/` lede, the Game intelligence
+ summary, and the homepage's list of absences. Each now says two modules exist, both built inside
+ this project, and that the Integration Kit keeps its draft banner until someone outside does it.
+- **The homepage diagram's sub-labels** said "ServUO today" and "uo-link". They became false the day
+ Rust shipped, so they changed here rather than waiting for phase 17. They now carry D63's wording.
+ The caption names both games.
+
+**One planned item moved to phase 17.** §16 put the Rust half of `capabilities.mjs`'s *page* coverage
+assertion in this phase. But a coverage assertion needs a list of Rust capabilities on a page to
+cover, and phase 17 is the phase that writes that list. Phase 13 does the half that has something to
+check: `checkFacts` now holds Module-Rust's declared capabilities against `platform.json`, so a
+capability the module adds turns this repo red today. The page-side assertion lands with the page.
+
---
[api]: https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/website/MODULE_API.md
diff --git a/scripts/checkFacts.mjs b/scripts/checkFacts.mjs
index f6e3a33..f0cda8d 100644
--- a/scripts/checkFacts.mjs
+++ b/scripts/checkFacts.mjs
@@ -92,33 +92,42 @@ function extract(text, re, what, authority) {
return m[1];
}
+// Every game the site quotes facts for (PLAN.md §16). Checks 1, 2, 4 and 5 run once per
+// game, and a row's label carries the game's key so a failure names which one moved.
+const GAMES = Object.entries(platform.games);
+
// ---------------------------------------------------------------------------
-// 1. The wire protocol, from the sidecar itself
+// 1. Each game's wire protocol, from its sidecar
// ---------------------------------------------------------------------------
async function checkProtocol() {
- const authority = 'link main:sidecar/src/main.rs';
- const src = await raw('link', 'sidecar/src/main.rs', 'main');
- const value = extract(
- src,
- /pub const PROTOCOL_VERSION:\s*u32\s*=\s*(\d+)\s*;/,
- 'PROTOCOL_VERSION',
- authority
- );
- record('protocol (sidecar)', platform.protocol, Number(value), authority);
+ for (const [key, game] of GAMES) {
+ const authority = `${game.sidecarRepo} main:sidecar/src/main.rs`;
+ const src = await raw(game.sidecarRepo, 'sidecar/src/main.rs', 'main');
+ const value = extract(
+ src,
+ /pub const PROTOCOL_VERSION:\s*u32\s*=\s*(\d+)\s*;/,
+ 'PROTOCOL_VERSION',
+ authority
+ );
+ record(`${key} protocol (sidecar)`, game.protocol, Number(value), authority);
+ }
}
// ---------------------------------------------------------------------------
-// 2. The overlay's declared protocol — the third declaration site
+// 2. Each game plugin's declared protocol — the third declaration site
//
// CLAUDE.md: a protocol bump has to land in overlay.toml in the same PR as the emitters,
-// or the installer refuses to pair the sidecar with the overlay. If these two ever
-// disagree, the site must not print either number.
+// or the installer refuses to pair the sidecar with the plugin. If these two ever
+// disagree, the site must not print either number. Both games keep the file under the
+// same name at the root of their plugin repository.
// ---------------------------------------------------------------------------
async function checkOverlayProtocol() {
- const authority = 'servuo-plugins main:overlay.toml';
- const toml = await raw('servuo-plugins', 'overlay.toml', 'main');
- const value = extract(toml, /^\s*protocol\s*=\s*(\d+)\s*$/m, 'protocol', authority);
- record('protocol (overlay)', platform.protocol, Number(value), authority);
+ for (const [key, game] of GAMES) {
+ const authority = `${game.pluginRepo} main:overlay.toml`;
+ const toml = await raw(game.pluginRepo, 'overlay.toml', 'main');
+ const value = extract(toml, /^\s*protocol\s*=\s*(\d+)\s*$/m, 'protocol', authority);
+ record(`${key} protocol (plugin)`, game.protocol, Number(value), authority);
+ }
}
// ---------------------------------------------------------------------------
@@ -137,7 +146,7 @@ async function checkModuleApi() {
}
// ---------------------------------------------------------------------------
-// 4. The capabilities the installed module actually declares
+// 4. The capabilities each game's module actually declares
//
// §12 names "module-uo's capability list" as one of the facts platform.json holds, and it
// was the one fact nothing re-read. That mattered from phase 3 onwards, because the
@@ -150,36 +159,109 @@ async function checkModuleApi() {
// array is not a changed capability set. A slug appearing or disappearing is.
// ---------------------------------------------------------------------------
async function checkModuleCapabilities() {
- const authority = 'Module-uo main:module.json';
- const manifest = JSON.parse(await raw('Module-uo', 'module.json', 'main'));
- const declared = [...(manifest.capabilities || [])].sort();
- const expected = [...platform.moduleUoCapabilities].sort();
+ for (const [key, game] of GAMES) {
+ const authority = `${game.module.repo} main:module.json`;
+ const manifest = JSON.parse(await raw(game.module.repo, 'module.json', 'main'));
+ const declared = [...(manifest.capabilities || [])].sort();
+ const expected = [...game.module.capabilities].sort();
- record('moduleUoCapabilities', expected.join(' '), declared.join(' '), authority);
+ record(`${key} module capabilities`, expected.join(' '), declared.join(' '), authority);
+ // The range the module declares against core. The manifest docs use it as the worked
+ // example of a caret range resolving across minor bumps, so it is a quoted fact.
+ record(`${key} module coreApi`, game.module.coreApi, manifest.coreApi, authority);
+ }
}
// ---------------------------------------------------------------------------
-// 5. The current bundle
+// 5. Each game's current bundle
//
-// The manifests live at the ROOT of the `bundles` branch — `current.json`,
-// `bundle-.json` — not under `bundles/`. Fetching the directory 404s.
+// One manifest per game on the installer's `bundles` branch, at
+// `v2//current.json` (schema 2). The ROOT `current.json` is the schema-1 manifest
+// from before the installer learned a second game; it is frozen and no longer read here,
+// because a check against a file that stopped moving passes forever.
+//
+// Each bundle is protocol-checked by CI when it is published, so its protocol is a third
+// independent read of the same number rather than a duplicate of checks 1 and 2.
// ---------------------------------------------------------------------------
-async function checkBundle() {
- const authority = 'installer bundles:current.json';
- const current = JSON.parse(await raw('installer', 'current.json', 'bundles'));
+async function readBundle(game) {
+ const file = `v2/${game.bundleGame}/current.json`;
+ const authority = `installer bundles:${file}`;
+ const current = JSON.parse(await raw('installer', file, 'bundles'));
+ if (current.schema !== 2) {
+ throw new Error(`${authority} is schema ${current.schema}, and this check reads schema 2.`);
+ }
+ return { current, authority };
+}
- record('bundle.tag', platform.bundle.tag, current.bundle, authority);
- record('bundle.sidecar', platform.bundle.sidecar, current.link?.tag, authority);
- record('bundle.overlay', platform.bundle.overlay, current.overlay?.tag, authority);
+async function checkBundle() {
+ const { uo, rust } = platform.games;
+
+ {
+ const { current, authority } = await readBundle(uo);
+ record('uo bundle.tag', uo.bundle.tag, current.bundle, authority);
+ record('uo bundle.sidecar', uo.bundle.sidecar, current.sidecar?.tag, authority);
+ record('uo bundle.overlay', uo.bundle.overlay, current.payload?.tag, authority);
+ record('uo bundle.servuoMin', uo.bundle.servuoMin, current.payload?.compat?.min_version, authority);
+ record('uo protocol (bundle)', uo.protocol, current.protocol, authority);
+ }
+
+ {
+ const { current, authority } = await readBundle(rust);
+ const compat = current.payload?.compat ?? {};
+ record('rust bundle.tag', rust.bundle.tag, current.bundle, authority);
+ record('rust bundle.sidecar', rust.bundle.sidecar, current.sidecar?.tag, authority);
+ record('rust bundle.plugin', rust.bundle.plugin, current.payload?.tag, authority);
+ record('rust bundle.oxideMin', rust.bundle.oxideMin, compat.frameworks?.oxide?.min_version, authority);
+ record('rust bundle.carbonMin', rust.bundle.carbonMin, compat.frameworks?.carbon?.min_version, authority);
+ record(
+ 'rust bundle.requiresPlugins',
+ [...rust.bundle.requiresPlugins].sort().join(' '),
+ [...(compat.requires_plugins ?? [])].sort().join(' '),
+ authority
+ );
+ record('rust bundle.runicnpc', rust.bundle.runicnpc, current.npc?.tag, authority);
+ record('rust protocol (bundle)', rust.protocol, current.protocol, authority);
+ // The bundle carries RunicNPC's API too; check 5b reads it from the plugin's own repo.
+ record('runicnpc api (bundle)', platform.runicnpc.api, current.npc?.api, authority);
+ }
+}
+
+// ---------------------------------------------------------------------------
+// 5b. RunicNPC's own declarations
+//
+// RunicNPC stands alone as well as shipping in the Rust bundle (D61), so its facts are
+// read from its own `plugin.toml` too — the file its release workflow folds into the
+// manifest the installer and the egg read. The API version is the number a plugin author
+// calling `RunicNpc_*` relies on, and the site quotes it beside the API reference (D62).
+// ---------------------------------------------------------------------------
+async function checkRunicNpc() {
+ const authority = 'runicnpc-rust main:plugin.toml';
+ const toml = await raw('runicnpc-rust', 'plugin.toml', 'main');
+ const npc = platform.runicnpc;
+
+ record('runicnpc api', npc.api, Number(extract(toml, /^\s*api\s*=\s*(\d+)\s*$/m, 'api', authority)), authority);
record(
- 'bundle.servuoMin',
- platform.bundle.servuoMin,
- current.overlay?.servuo?.min_version,
+ 'runicnpc oxideMin',
+ npc.oxideMin,
+ extract(toml, /^\s*min_oxide_version\s*=\s*"([^"]+)"/m, 'min_oxide_version', authority),
+ authority
+ );
+ record(
+ 'runicnpc carbonMin',
+ npc.carbonMin,
+ extract(toml, /^\s*min_carbon_version\s*=\s*"([^"]+)"/m, 'min_carbon_version', authority),
+ authority
+ );
+ const required = extract(toml, /^\s*requires_plugins\s*=\s*\[([^\]]*)\]/m, 'requires_plugins', authority)
+ .split(',')
+ .map((name) => name.trim().replace(/^"|"$/g, ''))
+ .filter(Boolean);
+ record(
+ 'runicnpc requiresPlugins',
+ [...npc.requiresPlugins].sort().join(' '),
+ required.sort().join(' '),
authority
);
- // The bundle is protocol-checked by CI when it is published, so this is a third
- // independent read of the same number rather than a duplicate of check 1.
- record('protocol (bundle)', platform.protocol, current.protocol, authority);
}
// ---------------------------------------------------------------------------
@@ -349,6 +431,7 @@ async function main() {
checkModuleApi,
checkModuleCapabilities,
checkBundle,
+ checkRunicNpc,
checkReleases,
checkWebsiteHasNoReleases,
checkAndroidApk,
diff --git a/scripts/seedDemo.mjs b/scripts/seedDemo.mjs
index b9161cc..4d4763d 100644
--- a/scripts/seedDemo.mjs
+++ b/scripts/seedDemo.mjs
@@ -69,7 +69,7 @@ const UOLINK_TOKEN = process.env.UOLINK_TOKEN || '';
// `platform.json` means the number is the one `checkFacts.mjs` verified against `link`'s
// `main` — so the rig cannot quietly drift two protocols behind the platform again, which is
// exactly what the literal did.
-const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.protocol);
+const UOLINK_PROTOCOL = Number(process.env.UOLINK_PROTOCOL || platform.games.uo.protocol);
const DRY = process.argv.includes('--dry-run');
diff --git a/src/components/Footer.astro b/src/components/Footer.astro
index 54a2b9e..c01d27b 100644
--- a/src/components/Footer.astro
+++ b/src/components/Footer.astro
@@ -62,8 +62,9 @@ const isExternal = (href: string) => href.startsWith('http');
- Protocol {platform.protocol}
- Bundle {platform.bundle.tag}
+ {/* D66, as on the hero: the shared version only. Each game's protocol and bundle
+ are on that game's pages. */}
+ Module API {platform.moduleApi}
diff --git a/src/components/StructuredData.astro b/src/components/StructuredData.astro
index 00bde27..7eb90c6 100644
--- a/src/components/StructuredData.astro
+++ b/src/components/StructuredData.astro
@@ -53,7 +53,9 @@ const application = {
// client. Not "Windows" — the installer runs there, the platform does not require it.
operatingSystem: 'Linux, Windows, Android',
license: 'https://www.gnu.org/licenses/gpl-3.0.html',
- softwareVersion: platform.bundle.tag,
+ // D66: the Module API is the one version every deployment shares. Each game has its own
+ // bundle, so no single bundle tag describes the platform any more.
+ softwareVersion: platform.moduleApi,
publisher: { '@id': url('/#organization') },
// Self-hosted and free, and `offers` is the only way the vocabulary can say so. Omitting
// it reads as "price unknown"; stating zero is simply true.
diff --git a/src/components/architecture/ModuleSeam.astro b/src/components/architecture/ModuleSeam.astro
index f8f90b7..67236a0 100644
--- a/src/components/architecture/ModuleSeam.astro
+++ b/src/components/architecture/ModuleSeam.astro
@@ -8,9 +8,11 @@ import platform from '../../data/platform.json';
* The other two draw runtime shapes. This one draws a code boundary, and it is here because
* it is the claim the whole project rests on: that a community platform can be built once
* and pointed at any game. An evaluator has every reason to read that as marketing, so the
- * page draws the seam and then says plainly what does and does not prove it — one module
- * exists, the second is a paper exercise, and the exit criterion for calling the contract
- * proven is written down (§2, and the entries `/modules/` renders from `notBuilt.mjs`).
+ * page draws the seam and then says plainly what does and does not prove it — two modules
+ * exist, both built inside this project, and the exit criterion for calling the contract
+ * proven by an outsider is written down (§2, §16, and the entries `/modules/` renders from
+ * `notBuilt.mjs`). Until phase 13 this said one module and a paper exercise; Module-Rust's
+ * release made that false.
*
* The Module API version is read from `platform.json` like every other number on this site
* (§12). It is the one place a version genuinely belongs in this diagram: the seam is
@@ -73,7 +75,7 @@ import platform from '../../data/platform.json';
Game module
- one per deployment; UO today
+ one per deployment; UO or Rust todayRoutes
@@ -130,11 +132,12 @@ import platform from '../../data/platform.json';
What this does not yet prove
- One module exists and it is Ultima Online. A second, for a different game, is a
- written dry-run that was deliberately never implemented — it exists to test whether
- the contract generalises on paper. Until somebody builds the second one, the seam is
- a well-argued design rather than a demonstrated one, and this site says so wherever
- it comes up.
+ Two modules exist: Ultima Online, and Rust, which this project built second, from
+ the Integration Kit. Where the Rust module needed something the contract did not
+ yet offer, the gap was closed in core, for every module, rather than in a fork — so
+ the seam now has two games on its far side instead of one. What two modules built
+ inside this project cannot prove is that somebody outside it can do the same, and
+ that is why the Integration Kit still says draft.
diff --git a/src/components/home/Capabilities.astro b/src/components/home/Capabilities.astro
index 23569c9..000b505 100644
--- a/src/components/home/Capabilities.astro
+++ b/src/components/home/Capabilities.astro
@@ -17,7 +17,7 @@ import { capabilityGroups, assertCapabilityCoverage } from '../../data/capabilit
* what exists while quietly omitting the well-known things that do not is the exact failure
* §1 is written to prevent.
*/
-assertCapabilityCoverage(platform.moduleUoCapabilities);
+assertCapabilityCoverage(platform.games.uo.module.capabilities);
---
@@ -52,7 +52,7 @@ assertCapabilityCoverage(platform.moduleUoCapabilities);
Some things people reasonably expect are deliberately not built — a Matrix
- integration, more than one game module active at once, a second game module. They are
+ integration, more than one game module active at once, a finished Integration Kit. They are
listed rather than left out, on features and{' '}
integrations.
diff --git a/src/components/home/DataPath.astro b/src/components/home/DataPath.astro
index 9461072..5da52cc 100644
--- a/src/components/home/DataPath.astro
+++ b/src/components/home/DataPath.astro
@@ -1,12 +1,10 @@
---
-import platform from '../../data/platform.json';
-
/**
* The data path (PLAN.md §13 phase 3), drawn as inline SVG per §11's motif rule — hand-drawn
* geometry, used where it explains something, and no raster anywhere.
*
* ---------------------------------------------------------------------------------------
- * THE LABELS ARE GENERIC, WITH UO AS THE CAPTION
+ * THE LABELS ARE GENERIC; THE CAPTION NAMES THE GAMES
* ---------------------------------------------------------------------------------------
* The org lead settled this before the diagram was drawn. The nodes say "your game server"
* and "sidecar", not "ServUO shard" and "uo-link", because §10's rule is that a reader
@@ -14,10 +12,12 @@ import platform from '../../data/platform.json';
* repositories in order to connect a game server — and because the tagline promises a
* platform, not a UO product.
*
- * It does NOT hide what actually ships. The sub-labels and the caption name ServUO and
- * uo-link outright, because §1 says the technical truth wins and today there is exactly one
- * implementation of this shape. An operator running a shard has to see themselves in the
- * picture on the first screen.
+ * It does NOT hide what actually ships: the caption names the games that ship on this shape,
+ * because §1 says the technical truth wins and an operator has to see their game in the
+ * picture on the first screen. Until phase 13 the sub-labels named ServUO and uo-link, which
+ * was true while there was exactly one implementation. With two, naming one would mislead,
+ * and D63 (PLAN.md §16) keeps each game's vocabulary on that game's own pages — so the
+ * caption names the games and the diagram names nothing else.
*
* ---------------------------------------------------------------------------------------
* WHY THE SVG IS aria-hidden
@@ -74,11 +74,11 @@ import platform from '../../data/platform.json';
Your game server
- ServUO today · opens no inbound port
+ opens no inbound portSidecar
- uo-link · the only network-facing part
+ the only network-facing partRunic Gateway
@@ -90,8 +90,9 @@ import platform from '../../data/platform.json';
- Today that game server is a ServUO shard and that sidecar is uo-link. The shape is the
- contract; the implementations are what plug into it.
+ Two games ship on this shape today, Ultima Online and Rust, each with its own plugin
+ and its own sidecar. The shape is the contract; the implementations are what plug
+ into it.
@@ -109,8 +110,8 @@ import platform from '../../data/platform.json';
The sidecar
A small service beside the game, and the only piece of the bridge anything else can
- reach. It speaks a versioned wire protocol — protocol {platform.protocol} today — so
- a mismatched pair is refused rather than misread, and it answers only your website's
+ reach. It speaks a versioned wire protocol — each game pins its own — so a
+ mismatched pair is refused rather than misread, and it answers only your website's
backend, over an authenticated WebSocket and REST.
diff --git a/src/components/home/Hero.astro b/src/components/home/Hero.astro
index 265ef75..be5151c 100644
--- a/src/components/home/Hero.astro
+++ b/src/components/home/Hero.astro
@@ -55,9 +55,9 @@ import platform from '../../data/platform.json';
- Protocol {platform.protocol}
+ {/* D66: only the version every deployment shares. A protocol and a bundle belong
+ to a game, so they live on that game's pages rather than on the front door. */}
Module API {platform.moduleApi}
- Bundle {platform.bundle.tag}
diff --git a/src/content/docs/docs/administration/managing-modules.mdx b/src/content/docs/docs/administration/managing-modules.mdx
index c6da49d..30251f5 100644
--- a/src/content/docs/docs/administration/managing-modules.mdx
+++ b/src/content/docs/docs/administration/managing-modules.mdx
@@ -41,6 +41,10 @@ Two consequences of how boots work:
Paste the new release's install-manifest URL and press Install. The bundle is verified
against its `sha256`, unpacked over the old one, and takes effect at the restart.
+**A site runs one module.** Installing a module with a different id beside the one you have
+is refused with `409`, and nothing is downloaded. To change game, remove the installed module
+first.
+
An upgrade **deliberately leaves the state alone**: upgrading an enabled module must not
silently switch it off, and re-installing a disabled one must not silently switch it on.
diff --git a/src/content/docs/docs/administration/the-shard-connection.mdx b/src/content/docs/docs/administration/the-shard-connection.mdx
index ad7de3f..56957fa 100644
--- a/src/content/docs/docs/administration/the-shard-connection.mdx
+++ b/src/content/docs/docs/administration/the-shard-connection.mdx
@@ -26,7 +26,7 @@ on:
| **Base URL (REST)** | `http://:8080` — point-in-time queries |
| **WebSocket URL (feed)** | `ws://:8080/ws` — the live event feed |
| **Auth token** | The sidecar's token |
-| **Protocol** | {platform.protocol} today |
+| **Protocol** | {platform.games.uo.protocol} today |
Saving restarts the ingest client, so a change takes effect immediately.
diff --git a/src/content/docs/docs/architecture/protocol-versions.mdx b/src/content/docs/docs/architecture/protocol-versions.mdx
index 7f67737..a53edc2 100644
--- a/src/content/docs/docs/architecture/protocol-versions.mdx
+++ b/src/content/docs/docs/architecture/protocol-versions.mdx
@@ -10,7 +10,11 @@ The loopback wire protocol between the game plugin and the sidecar is a **versio
compatibility contract**, not a build dependency. Nothing compiles the three sides together,
so the number is what stops a mismatch from being discovered as corrupted data.
-The current protocol is **{platform.protocol}**.
+Each game has its own protocol, numbered independently: {platform.games.uo.name} is on
+**{platform.games.uo.protocol}** and {platform.games.rust.name} on
+**{platform.games.rust.protocol}**. The two numbers are not comparable — they version two
+different wires. The examples below are the {platform.games.uo.name} bridge's; the
+{platform.games.rust.name} bridge declares its number in the same three kinds of place.
## Three declaration sites
@@ -18,8 +22,8 @@ The same number is written down in three places, and they must move together.
| Where | What declares it |
|---|---|
-| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION`, currently {platform.protocol} — what the sidecar speaks |
-| `servuo-plugins/overlay.toml` | `protocol`, currently {platform.protocol} — what the plugin overlay speaks |
+| `link/sidecar/src/main.rs` | `PROTOCOL_VERSION`, currently {platform.games.uo.protocol} — what the sidecar speaks |
+| `servuo-plugins/overlay.toml` | `protocol`, currently {platform.games.uo.protocol} — what the plugin overlay speaks |
| The bundle manifest | Copied from `overlay.toml` by CI, so a released pair carries its own claim |