Files
RingBRP/tools/check-creatures.mjs
T
slaguru666andClaude Opus 5 c3002e93c9 check-creatures: validate every actor spec, and fix the vesh talents it found
Nothing validated creature specs. check-kits validates postings and check-scenarios
validates prose; buildActor throws on an unknown skill or item key and is silent about
everything else, so species, marksmanship style, characteristic blocks, species-locked
talents and misspelt field names could all be wrong 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.

tools/creature-schema.mjs derives every allowed value from the catalogues rather than
retyping them: skills from SKILL_CATALOGUE, kit from the item catalogues, species from
SPECIES, stats from CHARACTERISTIC_DICE, and the two marksmanship styles out of the
language file. It reports every problem with a spec rather than dying on the first, and
names the nearest real key. It also reports fields buildActor never reads, because
"armours:" is not an error, it is an actor that silently equips nothing.

On its first run it found six baseline humans carrying vesh biology. "unblinking" is
cat: species, species: vesh, and its own rule text reads "A Vesh has no face to read".
Out of Step is reclassified as anomalous with a Coherence cost, which is what every
talent in that category has and what the Anchor who is three years older than his
birthday says always wanted to be; the other five are swapped to agency and field
talents that fit them.

Not fixed here: the root cause is upstream in postings.mjs, where twelve posting talent
pools still offer unblinking, distributed or quorum to any agent. Picking replacements
across those is a design decision, not a mechanical one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-30 21:09:43 +01:00

84 lines
3.8 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 { NPCS, PREGENS } from "./content.mjs";
import { ROSTER } from "./roster.mjs";
import { readFile } from "node:fs/promises";
import { validate, KNOWN_FIELDS } from "./creature-schema.mjs";
/* Scenario casts are collected by shape rather than by name: a cast is any exported
array whose entries carry characteristics. A new scenario is then covered the day it
is written instead of the day somebody remembers to add it here. */
const SCENARIOS = ["throughtrain", "starter", "lastadmission", "openday"];
const sources = [
{ label: "content.mjs PREGENS", kind: "agent", specs: PREGENS },
{ label: "content.mjs NPCS", kind: "npc", specs: NPCS },
{ label: "roster.mjs ROSTER", kind: "agent", specs: ROSTER }
];
for (const s of SCENARIOS) {
const mod = await import(`./scenario-${s}.mjs`);
for (const [name, value] of Object.entries(mod)) {
if (Array.isArray(value) && value.some(x => x && typeof x === "object" && x.ch)) {
sources.push({ label: `scenario-${s}.mjs ${name}`, kind: "npc", specs: value });
}
}
}
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`);