#!/usr/bin/env node import fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const FILENAME = fileURLToPath(import.meta.url); const REPO_ROOT = path.resolve(FILENAME, "..", ".."); export const SOURCE_HEALTH_RULES = Object.freeze([ { id: "api-app-composition", path: "apps/api/src/app.ts", maxLines: 450 }, { id: "api-route-modules", path: "apps/api/src/routes", maxLines: 500, glob: /\.ts$/ }, ]); export const SOURCE_HEALTH_ALLOWLIST = Object.freeze([]); export async function countLines(filePath) { const content = await fs.readFile(filePath, "utf8"); if (content.length === 0) { return 0; } return content.endsWith("\n") ? content.split("\n").length - 1 : content.split("\n").length; } async function listFiles(target, matcher) { const fullPath = path.join(REPO_ROOT, target); const stat = await fs.stat(fullPath); if (stat.isFile()) { return [fullPath]; } const entries = await fs.readdir(fullPath, { withFileTypes: true }); return entries .filter((entry) => entry.isFile() && matcher.test(entry.name)) .map((entry) => path.join(fullPath, entry.name)) .sort((left, right) => left.localeCompare(right)); } export async function evaluateSourceHealth({ rules = SOURCE_HEALTH_RULES, allowlist = SOURCE_HEALTH_ALLOWLIST } = {}) { const allowed = new Map(allowlist.map((entry) => [entry.path, entry])); const matchedAllowlistPaths = new Set(); const results = []; for (const rule of rules) { const files = await listFiles(rule.path, rule.glob ?? /[\s\S]/); for (const file of files) { const relativePath = path.relative(REPO_ROOT, file); const lineCount = await countLines(file); const exception = allowed.get(relativePath); if (exception) { matchedAllowlistPaths.add(relativePath); } const allowlistReason = exception?.reason?.trim() ?? ""; results.push({ ruleId: rule.id, path: relativePath, lines: lineCount, maxLines: rule.maxLines, allowed: Boolean(exception && allowlistReason), reason: allowlistReason || null, problem: exception && !allowlistReason ? "allowlist entry requires a reason" : null, pass: exception ? Boolean(allowlistReason) : lineCount <= rule.maxLines, }); } } for (const entry of allowlist) { if (!matchedAllowlistPaths.has(entry.path)) { results.push({ ruleId: "source-health-allowlist", path: entry.path, lines: 0, maxLines: 0, allowed: false, reason: entry.reason?.trim() || null, problem: "stale allowlist entry", pass: false, }); } } return results; } export async function runSourceHealthGate({ logger = console } = {}) { const results = await evaluateSourceHealth(); const failures = results.filter((result) => !result.pass); for (const result of results) { const status = result.pass ? "PASS" : "FAIL"; const allowance = result.allowed ? ` allowlisted=${JSON.stringify(result.reason)}` : ""; const problem = result.problem ? ` problem=${JSON.stringify(result.problem)}` : ""; const writer = result.pass ? logger.log : logger.error; writer(`${status} source_health rule=${result.ruleId} path=${result.path} lines=${result.lines} max=${result.maxLines}${allowance}${problem}`); } logger.log(`Summary: ${results.length - failures.length} passed, ${failures.length} failed`); return { results, failures }; } async function main() { const outcome = await runSourceHealthGate(); if (outcome.failures.length > 0) { process.exit(1); } } const invokedDirectly = process.argv[1] && path.resolve(process.argv[1]) === FILENAME; if (invokedDirectly) { main().catch((error) => { // eslint-disable-next-line no-console -- CLI gate must surface fatal errors on stderr. console.error(error); process.exit(1); }); }