diff --git a/tools/check-anatomy.mjs b/tools/check-anatomy.mjs index 0ebd62b..468915b 100644 --- a/tools/check-anatomy.mjs +++ b/tools/check-anatomy.mjs @@ -25,6 +25,7 @@ */ import { readFileSync } from "node:fs"; +import { readFile, readdir } from "node:fs/promises"; import { join, dirname } from "node:path"; import { fileURLToPath } from "node:url"; import { LOCATION_TABLES, BODY_ART } from "../anatomy.mjs"; @@ -314,6 +315,48 @@ for (const [id, table] of Object.entries(LOCATION_TABLES)) { } } +/* ---- the shape comes from bodyPlan, never from species ---- + * + * Three tools read `spec.species` to decide a creature's SHAPE, and all three were wrong + * in the same way, because every creature with a non-human body carries + * `species: "baseline"` and says what it is in `bodyPlan`: + * + * mj-queue.mjs drew a wingless gryphon + * make-portraits.mjs drew a human silhouette for 48 creatures + * bestiary.mjs published "26 of them are not human-shaped" when it is 74 + * + * Each was found by looking at the neighbour of the one before. R-264 settled the rule + * for the harness — `spec.bodyPlan ?? spec.species` — and build-packs writes that to the + * actor's speciesProfile. This stops a fourth: a line that feeds a shape function from + * `species` without naming `bodyPlan` is the whole defect, in one line, every time. + */ +{ + const SHAPE = /\b(?:locationsFor|groundedPlanFor|locationFor)\s*\(|(?:LOCATION_TABLES|BODY_ART)\s*\[/; + const files = (await readdir(new URL("../tools/", import.meta.url))) + .filter(f => f.endsWith(".mjs") && !f.startsWith("check-")) + .map(f => `tools/${f}`) + .concat(["ringbrp.mjs", "rules.mjs", "forge.mjs", "postings.mjs"]); + for (const rel of files) { + let src; + try { src = await readFile(new URL(`../${rel}`, import.meta.url), "utf8"); } catch { continue; } + src.split("\n").forEach((line, i) => { + if (/^\s*(?:\/\/|\*|\/\*)/.test(line)) return; // prose is not code + if (!SHAPE.test(line)) return; + if (!/\bspecies\b/.test(line)) return; // not fed from species at all + /* `plan` as well as `bodyPlan`: the correct form is two lines — a local `plan` + resolved once, then `TABLE[plan] ?? TABLE[species] ?? TABLE.baseline` — and the + species fallback on that second line is right, not a defect. Requiring the + literal `bodyPlan` on the line reported all three FIXED call sites. A guard that + calls the corrected code broken is the failure check-rules was reported for + earlier tonight, one file over. */ + if (/\b(?:bodyPlan|plan)\b/.test(line)) return; + problems.push(`${rel}:${i + 1} feeds a shape function from species — ` + + `a creature's body is in bodyPlan, and every non-human one has species "baseline". ` + + `Use \`spec.bodyPlan ?? spec.species\`, as R-264 settled for the harness.`); + }); + } +} + if (problems.length) { console.error("check-anatomy: FAILED"); for (const p of [...new Set(problems)]) console.error(" " + p);