feat(facts): phase 13 - per-game facts, Rust and RunicNPC checked, main green again
All checks were successful
PR checks / checks (pull_request) Successful in 8m25s
All checks were successful
PR checks / checks (pull_request) Successful in 8m25s
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/<game>/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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -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-<tag>.json` — not under `bundles/`. Fetching the directory 404s.
|
||||
// One manifest per game on the installer's `bundles` branch, at
|
||||
// `v2/<game>/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,
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user