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>
110 lines
6.1 KiB
JavaScript
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`);
|