Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-source-health.mjs
T

115 lines
3.8 KiB
JavaScript

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