From c7596e44b46408da973b27c357918c3e98d8fcc6 Mon Sep 17 00:00:00 2001 From: Mimmo Lahia Date: Tue, 15 Sep 2026 20:47:06 +1000 Subject: [PATCH] =?UTF-8?q?Add=20check:placeholders=20=E2=80=94=20enforce?= =?UTF-8?q?=20the=20card=20placeholder=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docs say: 'Library names, handles, statistics, and source labels inside cards are illustrative placeholders. Replace them with your own verified copy before publishing a video.' Nothing enforced it, so an unreplaced card publishes an invented statistic under a fabricated source line. Scanning the shipped library finds 135 fail-level placeholders across the 406 cards, 125 of them 'SOURCE - ... 2026' citation lines. This scans video-projects/ only, never the style library, since the library is supposed to contain placeholders. Statistics are warnings rather than failures because the script cannot know which are legitimately the user's. npm run check:placeholders npm run check:placeholders -- --list Exit 1 on anything found, so it works as a publish gate. --- package.json | 3 +- scripts/check-placeholders.mjs | 110 +++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 scripts/check-placeholders.mjs diff --git a/package.json b/package.json index a78f3b0b..e47c20d9 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:placeholders": "node scripts/check-placeholders.mjs" }, "engines": { "node": ">=22" diff --git a/scripts/check-placeholders.mjs b/scripts/check-placeholders.mjs new file mode 100644 index 00000000..e3183f1c --- /dev/null +++ b/scripts/check-placeholders.mjs @@ -0,0 +1,110 @@ +#!/usr/bin/env node +/** + * check-placeholders.mjs — fail a build that still contains the kit's + * illustrative placeholder content. + * + * The kit's own docs say: + * + * "Library names, handles, statistics, and source labels inside cards are + * illustrative placeholders. Replace them with your own verified copy + * before publishing a video." + * + * Nothing enforced that. This does. Ship an unreplaced card and you publish an + * invented statistic, under a fabricated source line, on your own channel. + * + * node scripts/check-placeholders.mjs scan video-projects/ + * node scripts/check-placeholders.mjs path/to/project scan one project + * node scripts/check-placeholders.mjs --list show what it looks for + * + * The style library is NOT scanned by default and must not be: its cards are + * supposed to contain placeholders. Only your built project has to be clean. + * + * Exit 0 = clean. Exit 1 = placeholders found. Exit 2 = bad usage. + */ +import { readFileSync, existsSync, readdirSync, statSync } from "node:fs"; +import { join, dirname, relative } from "node:path"; +import { fileURLToPath } from "node:url"; + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); + +/* Each rule: what it catches, and why it matters if it ships. */ +const FAIL = [ + { id: "source-label", re: /SOURCE\s*[·|\-–—:]\s*[^<>\n]{0,60}/gi, + why: "fabricated citation line — attributes a number to a source that does not exist" }, + { id: "placeholder-handle", re: /@(handle|yourchannel|yourbrand|username|channel)\b/gi, + why: "placeholder social handle" }, + { id: "shipped-handle", re: /@ronnymitchell\b/gi, + why: "a specific handle carried in the library cards — replace it with your own" }, + { id: "font-placeholder", re: /["']FontName["']/g, + why: "blueprint font placeholder — the card will fall back to a system face" }, + { id: "lorem", re: /lorem ipsum|your name here|example\.com|replace me/gi, + why: "unreplaced dummy copy" }, +]; + +/* Not failures. Numbers a human has to look at before publishing. */ +const WARN = [ + { id: "statistic", re: />\s*(\d{1,3}(?:\.\d+)?\s*%|\d+(?:\.\d+)?\s*[BMK]\b|\d+\s*[x×]\b)\s* !a.startsWith("--")) || join(ROOT, "video-projects"); +if (!existsSync(target)) { + console.log(`check-placeholders: nothing to scan — ${relative(ROOT, target) || target} does not exist yet.`); + console.log("Build a project first, then run this before you publish."); + process.exit(0); +} + +const SKIP = new Set(["node_modules", ".git", "_preview", "style-library", "docs", "examples", "tests"]); +const EXT = /\.(html?|css|json|md|txt)$/i; + +function walk(dir, out = []) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + if (!SKIP.has(entry.name)) walk(join(dir, entry.name), out); + } else if (EXT.test(entry.name)) { + out.push(join(dir, entry.name)); + } + } + return out; +} + +const files = statSync(target).isDirectory() ? walk(target) : [target]; +let fails = 0, warns = 0; + +for (const file of files) { + const lines = readFileSync(file, "utf8").split("\n"); + const rel = relative(ROOT, file); + lines.forEach((line, i) => { + for (const rule of FAIL) { + for (const m of line.matchAll(rule.re)) { + fails++; + console.log(`FAIL ${rel}:${i + 1} [${rule.id}] ${m[0].trim().slice(0, 70)}`); + console.log(` ${rule.why}`); + } + } + for (const rule of WARN) { + for (const m of line.matchAll(rule.re)) { + warns++; + console.log(`warn ${rel}:${i + 1} [${rule.id}] ${m[0].replace(/[<>]/g, "").trim()}`); + } + } + }); +} + +console.log(""); +if (fails) { + console.log(`${fails} placeholder(s) must be replaced before publishing. ${warns} statistic(s) to verify.`); + console.log("The kit's docs: 'Replace them with your own verified copy before publishing a video.'"); + process.exit(1); +} +console.log(`No placeholders. ${warns} statistic(s) flagged for a human to verify.`); +process.exit(0);