/** * 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(/(? 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`);