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>
84 lines
3.8 KiB
JavaScript
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`);
|