Files
projectbrain-Public/scripts/projectbrain-csi-audit.mjs
T

145 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
import fs from "node:fs";
import path from "node:path";
const root = process.cwd();
const args = new Set(process.argv.slice(2));
const asJson = args.has("--json");
const read = (file) => fs.readFileSync(path.join(root, file), "utf8");
const exists = (file) => fs.existsSync(path.join(root, file));
const normalizePath = (file) => file.replaceAll(path.sep, "/");
const walk = (dir, filter = () => true) => {
const out = [];
const base = path.join(root, dir);
if (!fs.existsSync(base)) return out;
const stack = [base];
while (stack.length) {
const current = stack.pop();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const full = path.join(current, entry.name);
if (entry.isDirectory()) {
if (!["node_modules", "dist", ".git", ".next", "coverage"].includes(entry.name)) stack.push(full);
} else if (filter(full)) {
out.push(normalizePath(path.relative(root, full)));
}
}
}
return out.sort();
};
const sourceFiles = walk("src", (file) => /\.(ts|tsx)$/.test(file));
const backendFiles = walk("backend/src", (file) => /\.(ts|tsx)$/.test(file));
const docs = walk("docs", (file) => file.endsWith(".md"));
const routeText = exists("src/app/routes.tsx") ? read("src/app/routes.tsx") : "";
const allTextFiles = [...sourceFiles, ...backendFiles].filter((file) => exists(file));
const implementationFiles = allTextFiles.filter((file) => ![
"src/shared/lib/mock-data.ts",
"src/shared/lib/projectAudit.ts",
].includes(file));
const countPattern = (files, pattern) => files.reduce((count, file) => count + ((read(file).match(pattern) ?? []).length), 0);
const filesWith = (files, pattern) => files.filter((file) => pattern.test(read(file)));
const firstMatches = (files, pattern, limit = 20) => {
const out = [];
for (const file of files) {
const lines = read(file).split(/\r?\n/);
for (const [index, line] of lines.entries()) {
if (pattern.test(line)) out.push({ file, line: index + 1, text: line.trim().slice(0, 180) });
pattern.lastIndex = 0;
if (out.length >= limit) return out;
}
}
return out;
};
const rawConsoleMatches = firstMatches(implementationFiles, /console\./g, 50);
const containedConsoleMatches = rawConsoleMatches.filter((match) => normalizePath(match.file) === "src/shared/lib/appLogger.ts");
const uncontainedConsoleMatches = rawConsoleMatches.filter((match) => normalizePath(match.file) !== "src/shared/lib/appLogger.ts");
const directMockDataFiles = filesWith(implementationFiles, /mockData/).filter((file) => file !== "src/shared/lib/projectbrainSelectors.ts");
const anyCastMatches = firstMatches(implementationFiles, /\bas any\b/g, 50);
const localhostMatches = firstMatches(implementationFiles, /localhost|127\.0\.0\.1/g, 50);
const localStorageMatches = firstMatches(implementationFiles, /localStorage/g, 50);
const auditCatalogText = exists("src/shared/lib/projectAudit.ts") ? read("src/shared/lib/projectAudit.ts") : "";
const queuedAuditItems = (auditCatalogText.match(/queued_for_/g) ?? []).length;
const v59AuditItems = (auditCatalogText.match(/implemented_in_v59/g) ?? []).length;
const report = {
generatedAt: new Date().toISOString(),
totals: {
frontendFiles: sourceFiles.length,
backendFiles: backendFiles.length,
docs: docs.length,
routes: (routeText.match(/<Route\s/g) ?? []).length,
},
signals: {
todoFixme: countPattern(implementationFiles, /TODO|FIXME|HACK|XXX/g),
directMockDataFiles: directMockDataFiles.length,
consoleUsage: countPattern(implementationFiles, /console\./g),
uncontainedConsoleUsage: uncontainedConsoleMatches.length,
localStorageUsage: countPattern(implementationFiles, /localStorage/g),
anyCasts: countPattern(implementationFiles, /\bas any\b/g),
localhostReferences: countPattern(implementationFiles, /localhost|127\.0\.0\.1/g),
queuedAuditItems,
v59AuditItems,
},
details: {
directMockDataFiles,
uncontainedConsoleMatches,
containedConsoleMatches,
anyCastMatches,
localStorageMatches,
localhostMatches,
},
gates: {
auditCenterRoute: routeText.includes("/audit"),
auditCatalog: exists("src/shared/lib/projectAudit.ts"),
auditDocs: exists("docs/PROJECTBRAIN_CSI_AUDIT_200_FINDINGS.md"),
blueprintEngineRoute: routeText.includes("/blueprint"),
v57Docs: exists("docs/STABILIZATION_PASS_V57_FULL_CSI_AUDIT.md"),
v58Docs: exists("docs/STABILIZATION_PASS_V58_QUEUE_BURNDOWN.md"),
v59Docs: exists("docs/STABILIZATION_PASS_V59_AUDIT_QUEUE_COMPLETION.md"),
appLogger: exists("src/shared/lib/appLogger.ts"),
},
};
const pass =
report.gates.auditCenterRoute &&
report.gates.auditCatalog &&
report.gates.auditDocs &&
report.gates.appLogger &&
report.totals.routes > 20 &&
report.signals.uncontainedConsoleUsage === 0 &&
report.signals.directMockDataFiles <= 1 &&
report.signals.anyCasts <= 3 &&
report.signals.localhostReferences === 0 &&
report.signals.queuedAuditItems === 0 &&
report.gates.v59Docs;
if (asJson) {
console.log(JSON.stringify({ pass, ...report }, null, 2));
} else {
console.log(`ProjectBrain CSI audit: ${pass ? "PASS" : "WARN"}`);
console.log(`Frontend files: ${report.totals.frontendFiles}`);
console.log(`Backend files: ${report.totals.backendFiles}`);
console.log(`Docs: ${report.totals.docs}`);
console.log(`Routes: ${report.totals.routes}`);
console.log(`TODO/FIXME/HACK/XXX markers: ${report.signals.todoFixme}`);
console.log(`Implementation files with direct mockData usage: ${report.signals.directMockDataFiles}`);
console.log(`console.* usages: ${report.signals.consoleUsage} (${report.signals.uncontainedConsoleUsage} uncontained)`);
console.log(`localStorage usages: ${report.signals.localStorageUsage}`);
console.log(`'as any' casts: ${report.signals.anyCasts}`);
console.log(`localhost references: ${report.signals.localhostReferences}`);
console.log(`queued audit items: ${report.signals.queuedAuditItems}`);
console.log(`V59 audit completions: ${report.signals.v59AuditItems}`);
console.log(`Audit Center route: ${report.gates.auditCenterRoute ? "yes" : "no"}`);
console.log(`Audit catalog: ${report.gates.auditCatalog ? "yes" : "no"}`);
console.log(`Audit docs: ${report.gates.auditDocs ? "yes" : "no"}`);
console.log(`App logger: ${report.gates.appLogger ? "yes" : "no"}`);
console.log(`V59 docs: ${report.gates.v59Docs ? "yes" : "no"}`);
if (!pass) {
if (directMockDataFiles.length) console.log(`Direct mockData files: ${directMockDataFiles.join(", ")}`);
if (uncontainedConsoleMatches.length) console.log(`Uncontained console usage: ${uncontainedConsoleMatches.map((m) => `${m.file}:${m.line}`).join(", ")}`);
}
}
if (!pass) process.exitCode = 1;