Files
RingBRP/tools/update-readme.mjs
T
slaguru666andClaude Opus 5 1d915c55cf check-focus: the focus-fire claim is an artifact, not a sentence (R-261)
R-258 put a tactic on a GM-facing page from an unguarded harness, R-259 found it worth
nothing, R-260 found the replacement right for the wrong reason. Three corrections, each
landing as prose with nothing checking it — the arrangement that let ddc4f99 describe a
game nobody was playing.

tools/focus-baseline.json records, per creature, the pack size and the spread-fire and
focus-fire win rates. check-focus re-measures all of it on every build and compares
exactly, with the party, seeds, run count and seeding scheme recorded alongside so numbers
taken under different conditions are refused rather than compared. The bestiary READS the
artifact instead of restating it, and check-bestiary refuses a page that has fallen behind
it. Page, guard and simulator cannot disagree.

The pack size is recorded rather than re-chosen: a fight at 0% or 100% cannot show an
effect, and a guard that picked again each run would let a changed creature move quietly
to a different question and pass. 31 of 47 creatures land in the measurable band; the
other 16 are recorded as pinned, with the rate that pinned them.

It guards the claim as well as the numbers. The page says focus fire helps in every fight
in doubt; check-focus fails if any row's gain reaches zero or stops clearing its own
noise. That failure means rewrite the page, not re-record the baseline.

The run count was chosen by evidence. 1000 x 3 seeds costs 8.5s and takes the suite from
2.5s to 13.7s. I tried 500 to halve it and the claim-check failed — at 500 runs one row
no longer clears its noise, so "without exception" is not supported by that much
sampling. Recording twice at 1000 gives byte-identical files.

Negative-tested four ways, all firing: a creature quietly made nimbler (redcap dodge
75 -> 85, spread 40.6% -> 27.6%), a baseline under different seeds, a hand-edited page,
and the claim failing at 500 runs.

Guard eleven (check-rollable) arrived from another session mid-build; this is twelve.

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

108 lines
6.0 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-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"];
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) 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`);