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