Files
RingBRP/tools/update-readme.mjs
T
slaguru666andClaude Opus 5 32ca982369 R-275: the harness had never read a creature's power
41 statblocks carry a POWER in their tactics and simulate.mjs read none of them,
so a redcap that ignores the cumulative defence penalty has been measured as a
creature that tires -- in check-lethality, in check-focus, and in every figure
published about it. The defect was not that the powers were unimplemented, it was
that nothing said they were not.

powers.mjs classifies all 41: 2 wired, 14 notSimulable with a stated reason, 25
not fight rules. check-powers refuses an unclassified POWER and refuses a
notSimulable without a reason -- and it does not test that the harness imports a
power, it fights the creature with and without and requires the two to disagree.

Moved: the courier 9.4% to 1.0% wiped (it attacks at half while carrying), the
redcap 0.7% to 0.9% (small, because these fights rarely spend a second defence).

ARGENT AND GULES was wired and then un-wired: it tripled the supporter's wipe rate
to 75.2% because the harness has no ground and applied the borough-ground condition
unconditionally. Same reason THE PULL is not wired. I had wired one and refused the
other on identical facts.

Lethality and focus re-recorded, bestiary regenerated.

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

127 lines
7.4 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-powers.mjs",
"check-anatomy.mjs",
"check-lethality.mjs", "check-focus.mjs", "check-firstblood.mjs",
"check-attackers.mjs", "check-fight-tail.mjs", "check-cited.mjs", "check-handouts.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);
}
}
/* A guard that fails, or that prints under a different name, returns null here. Dropping
it silently writes a README advertising fewer guards than the build runs and quietly
lowers the count word — which is how "thirteen guards" was published while fourteen ran
and check-rollable was red. A missing line is now fatal. */
const guardLines = GUARDS.map(name => [name, runGuard(name)]);
const silent = guardLines.filter(([, line]) => !line).map(([name]) => name);
if (silent.length) {
console.error(`update-readme: FAILED — no line from ${silent.join(", ")}. A guard that is `
+ `failing, or that prints under a name other than its own, must not be quietly left out `
+ `of the README. Run \`npm run check\` and fix it first.`);
process.exit(1);
}
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.map(([, l]) => l).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. */
/* Ran out at sixteen, one guard after the comment above warned about hardcoded lists
going stale. It fails loudly rather than silently, which is why this is an inconvenience
and not a defect — but it will run out again, so it now reaches far enough ahead that
the next person to hit it is unlikely to be working this year. */
const COUNT_WORD = ["zero", "one", "two", "three", "four", "five", "six", "seven",
"eight", "nine", "ten", "eleven", "twelve", "thirteen", "fourteen",
"fifteen", "sixteen", "seventeen", "eighteen", "nineteen", "twenty",
"twenty-one", "twenty-two", "twenty-three", "twenty-four"];
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`);