#!/usr/bin/env node // Zero-Coupling Law: engine dirs must not name catalog cards or noun words. import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; import { join, relative } from "node:path"; import { loadCatalog, fail } from "./lib.mjs"; const META_NOUNS = new Set(["TEXT", "ALL", "EMPTY"]); export function auditNames(catalog) { return catalog .filter((e) => e.kind === "card" || (e.kind === "word" && e.role === "noun" && !META_NOUNS.has(e.name))) .map((e) => e.name) .filter((n) => n.length >= 2); } export function variants(name) { const words = name.split(/[^A-Za-z0-9]+/).filter(Boolean); const lower = words.map((w) => w.toLowerCase()); const cap = (w) => w[0].toUpperCase() + w.slice(1); return new Set([ name, lower.join("_"), lower[0] + lower.slice(1).map(cap).join(""), lower.map(cap).join(""), lower.join("_").toUpperCase(), ]); } const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); function walk(dir, out = []) { for (const e of readdirSync(dir)) { if (e === "node_modules" || e === "target" || e.startsWith(".")) continue; const p = join(dir, e); statSync(p).isDirectory() ? walk(p, out) : out.push(p); } return out; } export function audit(ws, catalog) { const cfg = JSON.parse(readFileSync(join(ws, ".course.json"), "utf8")); const matchers = []; for (const name of auditNames(catalog)) { for (const v of variants(name)) { matchers.push({ name, re: new RegExp(`(? { const seen = new Set(); for (const m of matchers) { if (!seen.has(m.name) && m.re.test(line)) { seen.add(m.name); hits.push(`${relative(ws, file)}:${i + 1}: names "${m.name}"`); } } }); } } return hits; } if (import.meta.main) { const ws = process.argv[2]; if (!ws) fail("usage: coupling-audit.mjs "); const hits = audit(ws, loadCatalog()); if (hits.length) fail("coupling-audit: engine names specific content\n" + hits.map((h) => " " + h).join("\n")); console.log("coupling-audit: ok"); }