#!/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(/ 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;