import { readdir, readFile, writeFile, mkdir } from "node:fs/promises"; import path from "node:path"; const root = path.resolve(import.meta.dirname, ".."); const sourceRoots = ["main.cjs", "preload.cjs", "src/main", "src/renderer", "src/shared"]; const extensions = new Set([".js", ".cjs", ".mjs"]); async function filesBelow(entry) { const absolute = path.join(root, entry); const stat = await import("node:fs/promises").then(({ stat }) => stat(absolute)); if (stat.isFile()) return [entry]; const result = []; for (const child of await readdir(absolute, { withFileTypes: true })) { const relative = path.join(entry, child.name); if (child.isDirectory()) result.push(...await filesBelow(relative)); else if (extensions.has(path.extname(child.name))) result.push(relative); } return result; } function analyze(relative, source) { const lines = source.split(/\r?\n/).length; const branches = (source.match(/\b(?:if|else if|for|while|case|catch)\b|\?\?/g) || []).length; const functions = (source.match(/\b(?:async\s+)?function\b|=>|\b(?:async\s+)?[A-Za-z_$][\w$]*\s*\([^)]*\)\s*\{/g) || []).length; const ipcHandlers = (source.match(/\bregister\(\s*["']/g) || []).length; const responsibilities = [ ["inventory", /inventory|workload/i], ["deployment", /deploy|rollback|activation/i], ["git", /\bgit|repository/i], ["ipc", /ipc|register\(/i], ["renderer", /render|modal|document\./i], ["security", /key|credential|signature|checksum/i], ["updates", /update|release|artifact/i], ].filter(([, pattern]) => pattern.test(source)).map(([name]) => name); return { file: relative.replaceAll("\\", "/"), lines, branches, functions, ipcHandlers, responsibilities, hotspotScore: branches + Math.max(0, responsibilities.length - 2) * 10 }; } const files = (await Promise.all(sourceRoots.map(filesBelow))).flat(); const results = []; for (const file of files) results.push(analyze(file, await readFile(path.join(root, file), "utf8"))); results.sort((a, b) => b.hotspotScore - a.hotspotScore || b.lines - a.lines); const report = { generatedAt: new Date().toISOString(), thresholds: { preferredMaximumLines: 750, justificationRequiredLines: 1000 }, over750: results.filter((item) => item.lines > 750), over1000: results.filter((item) => item.lines > 1000), cyclomaticHotspots: results.filter((item) => item.branches >= 75).slice(0, 20), mixedResponsibilityModules: results.filter((item) => item.responsibilities.length >= 4), ipcHotspots: results.filter((item) => item.ipcHandlers >= 10), }; const reportDir = path.join(root, "reports"); await mkdir(reportDir, { recursive: true }); await writeFile(path.join(reportDir, "architecture-audit.json"), `${JSON.stringify(report, null, 2)}\n`); const table = (items) => items.length ? items.map((item) => `| \`${item.file}\` | ${item.lines} | ${item.branches} | ${item.functions} | ${item.ipcHandlers} | ${item.responsibilities.join(", ")} |`).join("\n") : "No findings."; const markdown = `# ForgeFlow architecture audit\n\nGenerated ${report.generatedAt}. Complexity is a deterministic decision-point count used for hotspot ranking, not a claim of exact McCabe complexity.\n\n## Files above 750 lines\n\n| File | Lines | Decisions | Functions | IPC handlers | Responsibilities |\n|---|---:|---:|---:|---:|---|\n${table(report.over750)}\n\n## Files above 1,000 lines\n\n| File | Lines | Decisions | Functions | IPC handlers | Responsibilities |\n|---|---:|---:|---:|---:|---|\n${table(report.over1000)}\n\n## Cyclomatic hotspots\n\n| File | Lines | Decisions | Functions | IPC handlers | Responsibilities |\n|---|---:|---:|---:|---:|---|\n${table(report.cyclomaticHotspots)}\n\n## Interpretation\n\nFiles above 750 lines require decomposition. Files above 1,000 lines are release blockers unless a concrete technical exception is documented. Mixed responsibility and IPC hotspot lists are available in the JSON report.\n`; await writeFile(path.join(reportDir, "architecture-audit.md"), markdown); console.log(`Audited ${results.length} source files; ${report.over750.length} exceed 750 lines and ${report.over1000.length} exceed 1,000 lines.`);