Files
RingBRP/tools/check-creatures.mjs
T
slaguru666andClaude Opus 5 24b183642f all-specs: one place that knows where the actors are
check-creatures collects every spec in the repo, including the rule that a scenario
cast is any exported array whose entries carry characteristics — so a new scenario is
covered the day it is written. That rule is a definition, and two more tools are about
to need it.

Lifted into tools/all-specs.mjs and imported back. check-creatures reports the same
76 specs across the same 8 sources; this is a move, not a change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 20:32:39 +01:00

67 lines
3.1 KiB
JavaScript

/**
* check-creatures — every actor spec in the repo must be one the engine can build.
*
* The five guards before this one check rules, kits, language, templates and scenario
* prose. None of them looks at an actor spec. buildActor throws on an unknown skill or
* item key, so those are caught; species, marksmanship style, characteristic blocks,
* species-locked talents and misspelt field names are not, and a spec can be wrong in
* any of those ways and still build, pack and play. Every packed actor once shipped as
* species "hominid" and the whole cast was hit-located as something the rules do not
* contain — silently, for several versions.
*
* This collects every spec that reaches buildActor and validates all of them against
* tools/creature-schema.mjs, reporting every problem rather than dying on the first.
*
* node tools/check-creatures.mjs
*/
import { readFile } from "node:fs/promises";
import { validate, KNOWN_FIELDS } from "./creature-schema.mjs";
import { collectSources } from "./all-specs.mjs";
/* Scenario casts are collected by shape rather than by name — see all-specs.mjs, which
owns that rule now that make-portraits and mj-queue need the same list. */
const sources = await collectSources();
const problems = [];
const seenKeys = new Map();
let count = 0;
for (const { label, kind, specs } of sources) {
for (const spec of specs) {
count++;
for (const p of validate(spec, { kind })) problems.push(`${label} — ${p}`);
// Actor ids are derived from the key, so two specs sharing one produce two actors
// with the same _id and the second silently replaces the first in the pack.
if (spec?.key) {
if (seenKeys.has(spec.key)) {
problems.push(`${label} — duplicate key "${spec.key}", already used by ${seenKeys.get(spec.key)}`);
} else seenKeys.set(spec.key, label);
}
}
}
/* The schema's list of fields buildActor reads is a copy of a fact that lives in
build-packs.mjs, and a copy is a thing that drifts. If buildActor learns to read a new
spec field, every spec using it would be reported as writing a field nothing reads —
exactly backwards. Compare the two and say so. */
{
const src = await readFile(new URL("./build-packs.mjs", import.meta.url), "utf8");
// The lookbehind matters: \b alone also matches the "spec." inside a path like
// "./expand-spec.mjs", and this guard duly reported that buildActor reads `spec.mjs`.
const read = new Set([...src.matchAll(/(?<![-\w])spec\.([a-zA-Z][a-zA-Z0-9]*)/g)].map(m => m[1]));
const missing = [...read].filter(f => !KNOWN_FIELDS.has(f));
if (missing.length) {
problems.push(`creature-schema.mjs KNOWN_FIELDS is out of date — buildActor reads `
+ `${missing.map(f => `spec.${f}`).join(", ")}, which the schema calls inert`);
}
}
if (problems.length) {
console.error("check-creatures: FAILED");
for (const p of problems) console.error(" " + p);
process.exit(1);
}
console.log(`check-creatures: OK — ${count} actor specs across ${sources.length} sources, `
+ `every characteristic, skill, kit key, talent, species and style resolves, no duplicate keys`);