145 lines
6.6 KiB
JavaScript
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;
|