diff --git a/package.json b/package.json index a78f3b0b..839749e9 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "test:media": "node scripts/smoke-media.mjs", "validate:short-form": "node .claude/skills/short-form-edit/scripts/validate-plan.mjs", "validate:footage": "node .claude/skills/short-form-edit/scripts/validate-footage.mjs", - "preflight:all": "node scripts/preflight-all.mjs" + "preflight:all": "node scripts/preflight-all.mjs", + "check:cards": "node scripts/check-cards.mjs" }, "engines": { "node": ">=22" diff --git a/scripts/check-cards.mjs b/scripts/check-cards.mjs new file mode 100644 index 00000000..9acee8aa --- /dev/null +++ b/scripts/check-cards.mjs @@ -0,0 +1,149 @@ +#!/usr/bin/env node +/** + * check-cards.mjs — enforce the style-library card contract mechanically. + * + * GUIDE.md defines what every card MUST do, and style-library/SKILL.md warns that + * "the catalog contains draft resources, not a promise that every card has passed + * rendered QA". Nothing checked the contract itself, so a card that Agent 3 cannot + * mount, or whose placeholder copy overflows its declared slot, is only discovered + * at render time. + * + * node scripts/check-cards.mjs every style + * node scripts/check-cards.mjs 03-mimmo one style + * node scripts/check-cards.mjs --rules what it enforces + * + * FAIL = the card will not mount or will render wrong. WARN = style hygiene. + * Exit 1 on any failure. This does NOT render: a clean pass still needs a human + * to watch the output, per the kit's own quality gates. + */ +import { readFileSync, existsSync, readdirSync } from "node:fs"; +import { join, dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); +const LIB = join(ROOT, "style-library"); + +const RULES = [ + ["root-attrs", "FAIL", "root div carries id, data-composition-id, data-start, data-width=1920, data-height=1080"], + ["timeline-key", "FAIL", "exactly one window.__timelines[...] registration, keyed to the composition id"], + ["slots-declared","FAIL", "every data-slot in the HTML is declared in style.json, and every declared slot exists"], + ["slot-overflow", "FAIL", "default slot copy fits the declared maxChars / maxItems"], + ["gsap-missing", "FAIL", "a card using gsap loads it itself, so it runs standalone"], + ["determinism", "FAIL", "no Date.now(), unseeded Math.random(), or fetch()"], + ["file-missing", "FAIL", "every card file named in style.json exists on disk"], + ["tier-bg", "FAIL", "tier1 root is opaque; tier2 root is transparent (it overlays the speaker)"], + ["hardcoded", "WARN", "colours and font families come from tokens.css, not literals"], +]; + +if (process.argv.includes("--rules")) { + for (const [id, sev, why] of RULES) console.log(`${sev.padEnd(5)} ${id.padEnd(16)} ${why}`); + process.exit(0); +} + +const only = process.argv.slice(2).find((a) => !a.startsWith("--")); +const styles = readdirSync(LIB, { withFileTypes: true }) + .filter((d) => d.isDirectory() && !d.name.startsWith("_") && existsSync(join(LIB, d.name, "style.json"))) + .map((d) => d.name) + .filter((n) => !only || n === only); + +if (!styles.length) { console.error(only ? `no style "${only}"` : "no styles found"); process.exit(2); } + +let fails = 0, warns = 0; +const say = (sev, where, rule, msg) => { + if (sev === "FAIL") fails++; else warns++; + console.log(`${sev} ${where} [${rule}] ${msg}`); +}; + +for (const style of styles) { + const dir = join(LIB, style); + const manifest = JSON.parse(readFileSync(join(dir, "style.json"), "utf8")); + console.log(`\n=== ${style} — ${manifest.cards?.length ?? 0} card(s) ===`); + + for (const card of manifest.cards ?? []) { + const file = join(dir, card.file); + const where = relative(ROOT, file); + if (!existsSync(file)) { say("FAIL", where, "file-missing", `declared by ${card.id} but not on disk`); continue; } + const html = readFileSync(file, "utf8"); + + // root attributes + composition id + const root = html.match(/