Seven guards checked that content is WELL FORMED. None checked what it DOES, so a change to a damage modifier, a hit-point formula or the armour value on a service vest could double a creature's lethality without touching one line of that creature - and nothing in the build would notice, because the creature did not change. Built as a regression test rather than the hand-declared bands the creature-forge plan described. Bands are the wrong shape here: declaring "keepers: dangerous" across forty-six creatures means inventing forty-six judgements, and after --spread it is clear the interesting question is not "is this dangerous" - that has no single answer - but "is this the same as it was". A recorded baseline answers exactly that, needs no authoring, and cannot be argued with. Every creature is measured solo against a FROZEN party of four, two armed postings and two trades, at a fixed seed, so the only thing that can move a number is a change to the rules or to the creature. Tolerances are 6 points of wipe rate and 0.35 agents, which is outside the noise floor of 200 runs - a guard that cries wolf gets deleted. Verified by tampering: told the baseline the nuckelavee was harmless and the guard caught it at +16.5 points and +1.67 agents down, exit 1. Runs in half a second, so it joins the pre-build checks rather than being something to remember to run. Also: the README's guard block was a hardcoded list of four while the suite was seven. It had silently stopped mentioning every guard added after it was written, including check-creatures and check-scenarios. Now all eight, and the prose says eight. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
76 lines
4.1 KiB
JavaScript
76 lines
4.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 { 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 guardLines = ["check-rules.mjs", "check-kits.mjs", "check-lang.mjs",
|
|
"check-templates.mjs", "check-behaviour.mjs", "check-scenarios.mjs",
|
|
"check-creatures.mjs", "check-lethality.mjs"].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); }
|
|
|
|
await writeFile(readme, withGuards, "utf8");
|
|
console.log(`update-readme: OK — ${items} items, ${Object.keys(ROLES).length} postings, `
|
|
+ `${Object.keys(TRADES).length} trades, ${guardLines.length} guard lines`);
|