74 lines
2.3 KiB
JavaScript
74 lines
2.3 KiB
JavaScript
#!/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(`(?<![A-Za-z0-9_])${esc(v)}(?![A-Za-z0-9_])`) });
|
|
}
|
|
}
|
|
const hits = [];
|
|
for (const d of cfg.engineDirs || []) {
|
|
const dir = join(ws, d);
|
|
if (!existsSync(dir)) continue;
|
|
for (const file of walk(dir)) {
|
|
readFileSync(file, "utf8").split("\n").forEach((line, i) => {
|
|
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 <workspaceDir>");
|
|
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");
|
|
}
|