Files
RingBRP/tools/update-readme.mjs
T
slaguru666andClaude Opus 5 ce1103ec0d R-267: a guarded home for the first disabling blow
Another session verified R-266's measurement, agreed the frame was better than
its own, and declined to publish it because the figure had no guarded lineage a
doubter could re-derive. It was right, and the fix costs 0.7s of build time --
which is the number I should have checked before calling it a reading tool and
not a guard.

first-blood.mjs gains --update/--check and is now both the reader and the
measurement of record; check-firstblood.mjs is a thin wrapper over it, the same
shape as check-bestiary. Compares the recorded figures exactly, then asserts only
what a page would claim: first blood lands within 5 points of even in the cut,
and its swing exceeds the six-a-side line's by more than both noises.

Recording it moved the swing from the scratch run's 43 to 40 against a
seed-to-seed spread of 2.1 -- high by more than its own noise, which is the
argument in miniature. update-readme's COUNT_WORD could not reach thirteen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-13 09:42:31 +01:00

110 lines
6.1 KiB
JavaScript

/**
* Rewrite the README's counted sections from the real data.
*
* Those tables have been wrong three times, because a catalogue that grows every
* session and a number typed by hand are not the same kind of thing. A stale count in
* a README is the same defect as a stale comment in the engine: a claim nothing checks.
*
* Only the marked regions are touched; the prose around them is left alone.
*/
import { readFile, writeFile } from "node:fs/promises";
import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { SKILL_CATALOGUE, WEAPONS, ARMOURS, GEAR, VEHICLES, NPCS, TALENTS }
from "./content.mjs";
import { ROLES, TRADES } from "../postings.mjs";
import { ICON_MAP } from "./icons.mjs";
import { RULES_PAGES } from "./rules-text.mjs";
const root = new URL("../", import.meta.url);
const readme = new URL("README.md", root);
let text = await readFile(readme, "utf8");
const items = WEAPONS.length + ARMOURS.length + GEAR.length + VEHICLES.length;
// The guard lines are quoted from the guards themselves rather than paraphrased.
const runGuard = name => {
try {
return execFileSync(process.execPath, [fileURLToPath(new URL(name, import.meta.url))],
{ encoding: "utf8" }).split("\n").find(l => l.startsWith(name.replace(".mjs", "")));
} catch { return null; }
};
/* All of them, in the order `npm run check` runs them. This list was four long while the
suite was seven, so the README advertised a subset and silently stopped mentioning
every guard added after it was written — including the three that catch the most. */
const GUARDS = ["check-rules.mjs", "check-kits.mjs", "check-lang.mjs",
"check-templates.mjs", "check-behaviour.mjs", "check-scenarios.mjs",
"check-rollable.mjs", "check-creatures.mjs", "check-anatomy.mjs",
"check-lethality.mjs", "check-focus.mjs", "check-firstblood.mjs",
"check-bestiary.mjs"];
/* The list above went stale the moment a guard was added without touching this file —
which is what the comment above it already warned about, and which happened again
with check-anatomy. So it is no longer trusted: the suite of record is the `check`
script in package.json, and this refuses to write a README that advertises a
different set than the one the build actually runs. */
{
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
const actual = [...(pkg.scripts?.check ?? "").matchAll(/(check-[\w-]+\.mjs)/g)].map(m => m[1]);
const missing = actual.filter(g => !GUARDS.includes(g));
const phantom = GUARDS.filter(g => !actual.includes(g));
if (missing.length || phantom.length) {
console.error("update-readme: FAILED — the README's guard list does not match `npm run check`");
if (missing.length) console.error(" run by the build, not advertised: " + missing.join(", "));
if (phantom.length) console.error(" advertised, not run by the build: " + phantom.join(", "));
process.exit(1);
}
}
const guardLines = GUARDS.map(runGuard).filter(Boolean);
const box = `| | |
|---|---|
| **Postings** | ${Object.keys(ROLES).length}, each with a key characteristic, trained skills, talents and its own kit |
| **Trades** | ${Object.keys(TRADES).length} ordinary jobs the department recruits from, plus induction |
| **Catalogue** | ${items} items — ${GEAR.length} gear, ${WEAPONS.length} weapons, ${ARMOURS.length} armour, ${VEHICLES.length} vehicles |
| **Skills / talents** | ${SKILL_CATALOGUE.length} skills, ${TALENTS.length} talents |
| **Bestiary** | ${NPCS.length} statblocks across folklore, horror and the far side |
| **Eras** | modern, antique, future, anomalous — the agency is old and never abolishes anything |
| **Icons** | ${Object.keys(ICON_MAP).length}, all generated from \`tools/icons.mjs\`; no external art |
| **Starter scenario** | **BLACK PLATFORM**, a six-player action starter, importable as a single Adventure |
| **Compendia** | pregens, NPCs, skills, weapons, armour, gear, vehicles, powers, tables, rules, the starter |
| **Rules journal** | ${RULES_PAGES.length} pages, generated from \`rules.mjs\` so the book cannot drift from the code |`;
const replaceRegion = (src, marker, body) => {
const open = `<!-- ${marker}:start -->`, close = `<!-- ${marker}:end -->`;
const i = src.indexOf(open), j = src.indexOf(close);
if (i < 0 || j < 0) return null;
return src.slice(0, i + open.length) + "\n" + body + "\n" + src.slice(j);
};
// First run: wrap the existing hand-written regions in markers.
if (!text.includes("<!-- box:start -->")) {
text = text.replace(/\| \| \|\n\|---\|---\|\n(?:\|.*\n)+/,
m => `<!-- box:start -->\n${m}<!-- box:end -->\n`);
text = text.replace(/```\ncheck-rules:[\s\S]*?```/,
m => `<!-- guards:start -->\n${m}\n<!-- guards:end -->`);
}
const withBox = replaceRegion(text, "box", box);
if (!withBox) { console.error("update-readme: box markers missing"); process.exit(1); }
const withGuards = replaceRegion(withBox, "guards",
"```\n" + guardLines.join("\n") + "\n```");
if (!withGuards) { console.error("update-readme: guard markers missing"); process.exit(1); }
/* The guard BLOCK was generated and the sentences introducing it were not, so the README
listed nine guards while three places in the prose still said eight. Count words go
stale the same way a hardcoded list does — the lesson two comments up, one layer out. */
const COUNT_WORD = ["zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
"fifteen", "sixteen"];
const word = COUNT_WORD[guardLines.length];
if (!word) { console.error(`update-readme: ${guardLines.length} guards has no count word`); process.exit(1); }
const counted = withGuards.replace(/\b(zero|one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen|fifteen|sixteen) guards\b/g,
`${word} guards`);
await writeFile(readme, counted, "utf8");
console.log(`update-readme: OK — ${items} items, ${Object.keys(ROLES).length} postings, `
+ `${Object.keys(TRADES).length} trades, ${guardLines.length} guard lines`);