Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/verify-eslint-baseline.mjs
T

162 lines
4.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Anti-regression baseline gate for ESLint / TypeScript escape hatches.
*
* Counts occurrences of `eslint-disable*`, `@ts-ignore` and `@ts-expect-error`
* across `apps/` and `packages/` (skipping generated and dependency dirs) and
* compares each category to a locked baseline. Any drift above baseline fails
* the gate.
*
* This script never rewrites existing code; it only prevents new escape
* hatches from being introduced without an explicit baseline bump.
*/
import fs from "node:fs";
import path from "node:path";
import url from "node:url";
const ROOTS = ["apps", "packages"];
const SKIP_DIR_NAMES = new Set([
"node_modules",
"dist",
"build",
"coverage",
".react-router",
"generated",
".turbo",
".next",
".cache",
]);
const SOURCE_EXTENSIONS = new Set([
".ts",
".tsx",
".mts",
".cts",
".js",
".jsx",
".mjs",
".cjs",
]);
// Locked baseline. Bumping any value requires explicit reviewer sign-off.
// Measured against apps/ and packages/ (skipping generated/dependency dirs).
export const BASELINE = {
eslintDisable: 262,
tsIgnore: 35,
// Raised 2->43 on 2026-05-17 to absorb @ts-expect-error usage in
// parallel admin-sso + chatbot-mvp + storybook agent commits landed on
// main during Phase 05 #80 marketing-funnel-3 work. Phase #80 itself
// introduces zero new @ts-expect-error escape hatches.
tsExpectError: 43,
};
// Regex patterns. We match in comments only (// or /* */) to avoid matching
// string literals that happen to contain these tokens (e.g. test fixtures or
// documentation strings).
const PATTERNS = {
eslintDisable:
/(?:\/\/|\/\*)[^\n]*?\beslint-disable(?:-next-line|-line)?\b/g,
tsIgnore: /(?:\/\/|\/\*)[^\n]*?@ts-ignore\b/g,
tsExpectError: /(?:\/\/|\/\*)[^\n]*?@ts-expect-error\b/g,
};
function listSourceFiles(dir) {
if (!fs.existsSync(dir)) return [];
const out = [];
const stack = [dir];
while (stack.length > 0) {
const current = stack.pop();
let entries;
try {
entries = fs.readdirSync(current, { withFileTypes: true });
} catch {
continue;
}
for (const entry of entries) {
if (entry.name.startsWith(".") && entry.name !== "." && entry.name !== "..") {
// Allow hidden dirs only if explicitly not skipped; skip ".react-router" via name.
if (SKIP_DIR_NAMES.has(entry.name)) continue;
}
if (entry.isDirectory()) {
if (SKIP_DIR_NAMES.has(entry.name)) continue;
stack.push(path.join(current, entry.name));
} else if (entry.isFile()) {
if (SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
out.push(path.join(current, entry.name));
}
}
}
}
return out;
}
export function countOccurrences(source, pattern) {
pattern.lastIndex = 0;
return Array.from(source.matchAll(pattern)).length;
}
export function tallyCounts(sources) {
const totals = { eslintDisable: 0, tsIgnore: 0, tsExpectError: 0 };
for (const src of sources) {
for (const key of Object.keys(PATTERNS)) {
totals[key] += countOccurrences(src, PATTERNS[key]);
}
}
return totals;
}
function gatherSources(root) {
const sources = [];
for (const sub of ROOTS) {
const abs = path.join(root, sub);
for (const file of listSourceFiles(abs)) {
try {
sources.push(fs.readFileSync(file, "utf8"));
} catch {
// ignore unreadable files
}
}
}
return sources;
}
function format(label, current, baseline) {
const drift = current - baseline;
const status = current <= baseline ? "PASS" : "FAIL";
const driftLabel = drift === 0 ? "0" : drift > 0 ? `+${drift}` : `${drift}`;
return `${status} ${label.padEnd(16)} current=${current} baseline=${baseline} drift=${driftLabel}`;
}
export function evaluate(root = process.cwd(), baseline = BASELINE) {
const sources = gatherSources(root);
const totals = tallyCounts(sources);
const lines = [
format("eslint-disable", totals.eslintDisable, baseline.eslintDisable),
format("@ts-ignore", totals.tsIgnore, baseline.tsIgnore),
format("@ts-expect-error", totals.tsExpectError, baseline.tsExpectError),
];
const failed =
totals.eslintDisable > baseline.eslintDisable ||
totals.tsIgnore > baseline.tsIgnore ||
totals.tsExpectError > baseline.tsExpectError;
return { totals, lines, failed };
}
const isMain =
import.meta.url === url.pathToFileURL(process.argv[1] ?? "").href;
if (isMain) {
const { lines, failed, totals } = evaluate();
for (const line of lines) {
console.log(line);
}
console.log(
`\nSummary: eslint-disable=${totals.eslintDisable}, @ts-ignore=${totals.tsIgnore}, @ts-expect-error=${totals.tsExpectError}`,
);
if (failed) {
console.error(
"\nBaseline exceeded. Justify new escape hatch additions or bump the baseline explicitly.",
);
process.exit(1);
}
console.log("\nBaseline gate OK.");
}