feat(docs): phase 15 - the Rust group, its reference pages checked against their sources
All checks were successful
PR checks / checks (pull_request) Successful in 2m5s
All checks were successful
PR checks / checks (pull_request) Successful in 2m5s
PLAN.md §16 phase 15. A Rust group of fifteen pages after Ultima Online (D70): the module, installing the bridge (installer, Pterodactyl egg, by hand), connecting the site, verifying the bridge, linking Steam accounts, permissions and groups, mod config, who sees who is online, events, zones and rewards, optional plugins, the live map, troubleshooting, and three reference pages - rust-link's sidecar.toml, the plugin's RunicGateway.json, and the event kinds. checkReference.mjs gains three Rust sections rather than a new check: sidecar.toml keys and RUSTLINK_* overrides from Rust-Link config.rs, RunicGateway.json from the plugin's PluginConfig, and the event kinds in three classes against Module-Rust server/catalogue.js (D71). Every admin screen was walked on the walk site before it was described (D36). Core pages now hand off to the Rust group instead of linking out to the docs repository. Three defects the re-read found here are fixed: the UO /health sample's hard-coded protocol 4 (both games' samples now render platform.json through <Code>), Authentication's missing Steam tab, and two pages saying the site cannot reach EventsEnabled - false on Rust. Installer v0.8.1 moved checkFacts red mid-phase; platform.json follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
@@ -7,6 +7,10 @@
|
||||
* this is the machinery that makes writing the NAMES down safe anyway — the same bargain
|
||||
* checkQuickstart.mjs struck for the quickstart, applied to six more sources.
|
||||
*
|
||||
* Phase 15 (PLAN.md §16) added the Rust group's three: rust-link's `sidecar.toml` and its
|
||||
* environment overrides, the bridge plugin's `RunicGateway.json`, and the event kinds with the
|
||||
* class each is served under (D71).
|
||||
*
|
||||
* Each enumeration in `src/data/reference.mjs` is compared against its authority, read from
|
||||
* the repository that owns it over the Gitea API — never from a working tree, per §1's
|
||||
* process rule. Every comparison is a SET comparison in both directions:
|
||||
@@ -39,6 +43,10 @@ import {
|
||||
bridgeCfg,
|
||||
visibilityLadder,
|
||||
canonicalDocs,
|
||||
rustSidecarConfig,
|
||||
rustSidecarEnv,
|
||||
rustPluginConfig,
|
||||
rustEventKinds,
|
||||
} from '../src/data/reference.mjs';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
@@ -103,6 +111,45 @@ const cfgKeysOf = (text) =>
|
||||
.filter(Boolean)
|
||||
.map((m) => m[1]);
|
||||
|
||||
/**
|
||||
* A sidecar's TOML keys, parsed from its serde structs rather than from a sample file,
|
||||
* because the sample is GENERATED by the binary on first run and no committed copy is
|
||||
* authoritative. Each `pub name: T` inside a `struct XCfg` is one key, and the struct name
|
||||
* gives the section. Both sidecars are written this way, so one parser serves both.
|
||||
*/
|
||||
function tomlKeysOf(configRs) {
|
||||
const keys = [];
|
||||
for (const m of configRs.matchAll(/struct\s+(\w+)Cfg\s*\{([\s\S]*?)\n\}/g)) {
|
||||
const section = m[1].toLowerCase();
|
||||
for (const f of m[2].matchAll(/pub\s+(\w+)\s*:/g)) keys.push(`${section}.${f[1]}`);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/** The body of a C# class, by brace counting from its opening brace. */
|
||||
function classBody(source, name) {
|
||||
const start = source.search(new RegExp(`class\\s+${name}\\b`));
|
||||
if (start < 0) return null;
|
||||
const open = source.indexOf('{', start);
|
||||
let depth = 0;
|
||||
for (let i = open; i < source.length; i++) {
|
||||
if (source[i] === '{') depth++;
|
||||
else if (source[i] === '}' && --depth === 0) return source.slice(open + 1, i);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The quoted strings inside `const NAME = Object.freeze([ … ])`, comments removed first —
|
||||
* the module's comments are prose, and prose has apostrophes.
|
||||
*/
|
||||
function frozenListOf(source, name) {
|
||||
const m = source.match(new RegExp(`const\\s+${name}\\s*=\\s*Object\\.freeze\\(\\[([\\s\\S]*?)\\]\\)`));
|
||||
if (!m) return null;
|
||||
const code = m[1].replace(/\/\/.*$/gm, '');
|
||||
return [...code.matchAll(/'([^'\n]+)'/g)].map((x) => x[1]);
|
||||
}
|
||||
|
||||
async function run() {
|
||||
if (!TOKEN) {
|
||||
console.error('checkReference: GITEA_TOKEN is not set. This check cannot run anonymously.');
|
||||
@@ -122,13 +169,12 @@ async function run() {
|
||||
// Parsed from the serde structs rather than from a sample file, because the sample is
|
||||
// GENERATED by the binary on first run and no committed copy is authoritative. Each
|
||||
// `pub name: T` inside a `struct XCfg` is one key, and the struct name gives the section.
|
||||
const configRs = await raw('link', 'sidecar/src/config.rs');
|
||||
const sidecarKeys = [];
|
||||
for (const m of configRs.matchAll(/struct\s+(\w+)Cfg\s*\{([\s\S]*?)\n\}/g)) {
|
||||
const section = m[1].toLowerCase();
|
||||
for (const f of m[2].matchAll(/pub\s+(\w+)\s*:/g)) sidecarKeys.push(`${section}.${f[1]}`);
|
||||
}
|
||||
compareSets('sidecar.toml', Object.keys(sidecarConfig), sidecarKeys, 'link main:sidecar/src/config.rs');
|
||||
compareSets(
|
||||
'sidecar.toml',
|
||||
Object.keys(sidecarConfig),
|
||||
tomlKeysOf(await raw('link', 'sidecar/src/config.rs')),
|
||||
'link main:sidecar/src/config.rs',
|
||||
);
|
||||
|
||||
// ── 3. Installer commands ─────────────────────────────────────────────────
|
||||
const cliRs = await raw('installer', 'src/cli.rs');
|
||||
@@ -175,6 +221,72 @@ async function run() {
|
||||
else fail(`canonical doc ${docPath}`, `not found in docs main (HTTP ${res.status})`);
|
||||
}
|
||||
|
||||
// ── 7. Rust: rust-link's sidecar.toml, and the variables that override it ──
|
||||
//
|
||||
// PLAN.md §16, phase 15. The same parser as uo-link's — both sidecars declare their file
|
||||
// as serde structs — plus the environment overrides, which a Pterodactyl egg sets and an
|
||||
// operator meets on the panel before they ever see the file.
|
||||
const rustConfigRs = await raw('Rust-Link', 'sidecar/src/config.rs');
|
||||
compareSets(
|
||||
'rust-link sidecar.toml',
|
||||
Object.keys(rustSidecarConfig),
|
||||
tomlKeysOf(rustConfigRs),
|
||||
'Rust-Link main:sidecar/src/config.rs',
|
||||
);
|
||||
compareSets(
|
||||
'rust-link environment variable',
|
||||
Object.keys(rustSidecarEnv),
|
||||
[...rustConfigRs.matchAll(/get\("(RUSTLINK_[A-Z_]+)"\)/g)].map((m) => m[1]),
|
||||
'Rust-Link main:sidecar/src/config.rs',
|
||||
);
|
||||
for (const [envName, key] of Object.entries(rustSidecarEnv)) {
|
||||
if (!(key in rustSidecarConfig)) fail(`rust-link environment variable ${envName}`, `says it overrides "${key}", which is not a key listed here`);
|
||||
}
|
||||
|
||||
// ── 8. Rust: the bridge plugin's RunicGateway.json ────────────────────────
|
||||
//
|
||||
// The public fields of `PluginConfig`, which the framework serialises as the file.
|
||||
const pluginCs = await raw('Rust-Plugins', 'overlay/oxide/plugins/RunicGateway.cs');
|
||||
const pluginBody = classBody(pluginCs, 'PluginConfig');
|
||||
if (!pluginBody) {
|
||||
fail('RunicGateway.json', 'could not find class PluginConfig in Rust-Plugins main:overlay/oxide/plugins/RunicGateway.cs');
|
||||
} else {
|
||||
compareSets(
|
||||
'RunicGateway.json',
|
||||
Object.values(rustPluginConfig).flatMap((group) => Object.keys(group)),
|
||||
[...pluginBody.matchAll(/public\s+[\w<>[\],]+\s+(\w+)\s*=/g)].map((m) => m[1]),
|
||||
'Rust-Plugins main:overlay/oxide/plugins/RunicGateway.cs PluginConfig',
|
||||
);
|
||||
}
|
||||
|
||||
// ── 9. Rust: the event kinds, and who may see each ────────────────────────
|
||||
//
|
||||
// D71: checked against the module's own catalogue, because the module is what enforces
|
||||
// the boundary (the same reasoning as check 5). Three comparisons, each a security
|
||||
// statement: a kind listed public here must be public there, a presence kind must be one
|
||||
// of the module's presence kinds, and a staff kind must be staff there. A kind moved from
|
||||
// staff to public upstream fails this check twice over, which is the point.
|
||||
const catalogue = await raw('Module-Rust', 'server/catalogue.js');
|
||||
const upPublic = frozenListOf(catalogue, 'PUBLIC_KINDS');
|
||||
const upStaff = frozenListOf(catalogue, 'STAFF_KINDS');
|
||||
const upPresence = frozenListOf(catalogue, 'PRESENCE_KINDS');
|
||||
if (!upPublic || !upStaff || !upPresence) {
|
||||
fail('Rust event kinds', 'could not find PUBLIC_KINDS, STAFF_KINDS and PRESENCE_KINDS in Module-Rust main:server/catalogue.js');
|
||||
} else {
|
||||
const hint = 'Module-Rust main:server/catalogue.js';
|
||||
compareSets('Rust presence kind', Object.keys(rustEventKinds.presence), upPresence, `${hint} PRESENCE_KINDS`);
|
||||
compareSets(
|
||||
'Rust public kind',
|
||||
Object.keys(rustEventKinds.public),
|
||||
upPublic.filter((k) => !upPresence.includes(k)),
|
||||
`${hint} PUBLIC_KINDS (less its presence kinds)`,
|
||||
);
|
||||
compareSets('Rust staff kind', Object.keys(rustEventKinds.staff), upStaff, `${hint} STAFF_KINDS`);
|
||||
for (const k of upPresence) {
|
||||
if (!upPublic.includes(k)) fail(`Rust presence kind ${k}`, 'is a presence kind upstream but not a public one — the page explains presence as a narrowing of public, so re-read it');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────────
|
||||
if (failures.length === 0) {
|
||||
console.log(`checkReference: ${checked.length} enumeration check(s) passed against their sources.`);
|
||||
|
||||
Reference in New Issue
Block a user