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>
67 lines
3.1 KiB
JavaScript
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`);
|