From 24b183642f46a916c8790563a2d7f7a915abf24a Mon Sep 17 00:00:00 2001 From: slaguru666 <111923774+slaguru666@users.noreply.github.com> Date: Mon, 31 Aug 2026 20:32:39 +0100 Subject: [PATCH] all-specs: one place that knows where the actors are MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tools/all-specs.mjs | 57 +++++++++++++++++++++++++++++++++++++++ tools/check-creatures.mjs | 25 +++-------------- 2 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 tools/all-specs.mjs diff --git a/tools/all-specs.mjs b/tools/all-specs.mjs new file mode 100644 index 0000000..8998db1 --- /dev/null +++ b/tools/all-specs.mjs @@ -0,0 +1,57 @@ +/** + * Every actor spec in the repository, collected once. + * + * Three things now need the same list — check-creatures validates it, make-portraits + * draws it, and mj-queue writes prompts for it — and this repository's one standing law + * is that a definition may not exist twice. The collection rule is a definition: a + * scenario cast is any exported array whose entries carry characteristics, so a new + * scenario is covered the day it is written rather than the day somebody remembers to + * add it to a list. That rule belongs in one place. + * + * `kind` is what buildActor will make of the spec: "agent" becomes a character actor, + * "npc" becomes an npc actor. It decides which portrait family a creature is drawn in + * and which prompt template it gets, so it is carried here rather than re-derived. + */ +import { NPCS, PREGENS } from "./content.mjs"; +import { ROSTER } from "./roster.mjs"; + +/** Scenario modules whose casts join the list. */ +export const SCENARIOS = ["throughtrain", "starter", "lastadmission", "openday"]; + +/** + * @returns {Promise>} + * every spec, in a stable order, each tagged with where it came from. + */ +export async function collectSpecs() { + 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 out = []; + for (const { label, kind, specs } of sources) { + for (const spec of specs) out.push({ label, kind, spec }); + } + return out; +} + +/** The same thing grouped, for callers that report per source. */ +export async function collectSources() { + const flat = await collectSpecs(); + const byLabel = new Map(); + for (const row of flat) { + if (!byLabel.has(row.label)) byLabel.set(row.label, { label: row.label, kind: row.kind, specs: [] }); + byLabel.get(row.label).specs.push(row.spec); + } + return [...byLabel.values()]; +} diff --git a/tools/check-creatures.mjs b/tools/check-creatures.mjs index 70e26b1..c833601 100644 --- a/tools/check-creatures.mjs +++ b/tools/check-creatures.mjs @@ -14,30 +14,13 @@ * * 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"; +import { collectSources } from "./all-specs.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 }); - } - } -} +/* 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();