#!/usr/bin/env node 'use strict'; /** * lint-health-diagnostic-rule-table.cjs — gate: enforces ADR-3180 §8.2's 1:1 * rule-code invariant and §8.5's fixture-proof invariant for * `src/health-diagnostic.cts`'s RULES table (Phase 11, #3309). * * ## What this enforces * * 1. (§8.2 rule 1 — 1:1 code invariant) Every `rule.code` in RULES (exported * from the compiled `gsd-core/bin/lib/health-diagnostic.cjs`) is unique, * and every rule's `severity` is one of `SEVERITY`'s values. The severity * check exists only to confirm the compiled artifact was not hand-edited * to bypass the `Rule.severity` required field TypeScript already * enforces at compile time — "severity is a property of the RULE, never * the emit call." * 2. (§8.5 — fixture-proof invariant) Every code in RULES has a paired test: * the code string (e.g. `'W001'`) appears as a literal AND within a * `describe(`/`test(` block whose title also names that exact code, in * one of the health-diagnostic test files * (`tests/health-diagnostic-rules/*.test.cjs`, * `tests/health-diagnostic.test.cjs`). A mere comment/string mention * outside a titled block does not count as coverage. * * EXCEPTION — `PERMANENTLY_INERT_CODES` (below): a rule whose `check` * always returns `[]` BY DESIGN (the real check lives outside the rule * table entirely, because it needs ambient I/O `Rule.check` cannot * perform — §8.1 rule 1) can never satisfy a real fixture-proof, no * matter how many tests reference its code. Before this exception * existed, W024 "passed" this guard only because an unrelated test title * (the RULES-array shape assertion, "exports exactly 5 rules: W024, ...") * happened to contain the string "W024" — accidental coverage, not proof * the rule can fire. `PERMANENTLY_INERT_CODES` makes that exemption * explicit and auditable instead of relying on a coincidental title * match, and the PASS output now reports exempted codes SEPARATELY from * genuinely fixture-covered ones rather than folding them together. * * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md * ("The lint guard (§8.2 1:1 invariant + §8.5 fixture proof)"). * * ## Deviation from the design doc's original plan * * The design doc assumed fixtures would live as separate files at * `tests/fixtures/health-diagnostic/.*`. That did not happen during * implementation — all 8 rule-group test files * (`tests/health-diagnostic-rules/*.test.cjs`) build fixtures INLINE via * real temp directories (`createTempDir()` from `tests/helpers.cjs`) and a * real, non-mocked `buildPlanningSnapshot(tmpCwd)` call (see * `tests/health-diagnostic-rules/root-existence.test.cjs`). This guard * therefore verifies the fixture-proof invariant STATICALLY against the * test files' own text — mirroring `scripts/lint-fix-has-regression-test.cjs`'s * house style — rather than dynamically re-running fixture-building code * this guard does not own. */ const fs = require('node:fs'); const path = require('node:path'); const { ExitError, runMain } = require('./lib/cli-exit.cjs'); const REPO_ROOT = path.join(__dirname, '..'); const COMPILED_MODULE_REL = 'gsd-core/bin/lib/health-diagnostic.cjs'; const COMPILED_MODULE_PATH = path.join(REPO_ROOT, COMPILED_MODULE_REL); const TEST_GROUP_DIR = path.join(REPO_ROOT, 'tests', 'health-diagnostic-rules'); const SKELETON_TEST_FILE = path.join(REPO_ROOT, 'tests', 'health-diagnostic.test.cjs'); // Matches `describe(`/`test(`/`it(` calls whose first argument is a string // literal, capturing that literal as the block's title. Line/regex-based // (not full AST) per this repo's existing lint-guard house style // (scripts/lint-planning-snapshot-bypass-drift.cjs's scanCode precedent). const TITLED_BLOCK_RE = /\b(describe|test|it)\(\s*(['"`])((?:\\.|(?!\2)[^\\])*)\2/g; // Rule codes whose `check` is a documented PERMANENT no-op (always returns // `[]`) because the real check requires ambient I/O forbidden inside a // `Rule.check(snapshot)` (§8.1 rule 1) — the real check runs elsewhere, // outside the rule table. These can never be proven via a real // diagnostic-firing fixture, so they are exempted from the §8.5 fixture-proof // invariant explicitly here rather than via an accidental test-title match. // Adding an entry is a deliberate, reviewed decision — see each reason. const PERMANENTLY_INERT_CODES = new Map([ [ 'W024', 'readStateHeadFreshness requires a git-log shell-out, forbidden ambient I/O for Rule.check ' + '(§8.1 rule 1) — the real check runs in cmdValidateHealth itself, outside the rule table ' + '(src/verify.cts). This rule-table entry is a permanent no-op by design, not a fixture gap.', ], ]); /** * Load the compiled health-diagnostic module. Throws a clear ExitError * (not a raw MODULE_NOT_FOUND) if `npm run build:lib` has not run. */ function loadCompiledModule(compiledPath = COMPILED_MODULE_PATH) { if (!fs.existsSync(compiledPath)) { throw new ExitError( 2, `lint-health-diagnostic-rule-table: compiled artifact not found at ${COMPILED_MODULE_REL}.\n` + 'Run `npm run build:lib` first.', ); } return require(compiledPath); } /** * §8.2 rule 1 — 1:1 code invariant: every rule.code is unique, and every * rule's severity is a member of SEVERITY's values. * * @param {Array<{code: string, severity: string}>} rules * @param {Record} severity SEVERITY export (code -> value) * @returns {{duplicates: Array<{code: string, count: number}>, badSeverities: Array<{code: string, severity: unknown}>}} */ function checkOneToOneInvariant(rules, severity) { const severityValues = new Set(Object.values(severity)); const counts = new Map(); const badSeverities = []; for (const rule of rules) { counts.set(rule.code, (counts.get(rule.code) || 0) + 1); if (!severityValues.has(rule.severity)) { badSeverities.push({ code: rule.code, severity: rule.severity }); } } const duplicates = [...counts.entries()] .filter(([, count]) => count > 1) .map(([code, count]) => ({ code, count })); return { duplicates, badSeverities }; } /** * Extracts every `describe(`/`test(`/`it(` block title found in `text`. * * @param {string} text * @returns {string[]} */ function extractTitledBlocks(text) { const titles = []; TITLED_BLOCK_RE.lastIndex = 0; let match; while ((match = TITLED_BLOCK_RE.exec(text)) !== null) { titles.push(match[3]); } return titles; } /** * True iff `code` appears verbatim, as a whole token, inside at least one of * `titles`. Whole-token match guards against a shorter code accidentally * substring-matching inside an unrelated longer token. * * @param {string} code * @param {string[]} titles */ function codeAppearsInTitle(code, titles) { const codeRe = new RegExp(`(?:^|[^A-Za-z0-9])${code}(?:$|[^A-Za-z0-9])`); return titles.some((title) => codeRe.test(`|${title}|`)); } /** * Locates every health-diagnostic test file this guard scans for §8.5 * fixture-proof coverage. * * @param {string} repoRoot * @returns {string[]} absolute paths, sorted */ function findHealthDiagnosticTestFiles(repoRoot = REPO_ROOT) { const groupDir = path.join(repoRoot, 'tests', 'health-diagnostic-rules'); const files = []; if (fs.existsSync(groupDir)) { for (const entry of fs.readdirSync(groupDir)) { if (entry.endsWith('.test.cjs')) { files.push(path.join(groupDir, entry)); } } } const skeletonTestFile = path.join(repoRoot, 'tests', 'health-diagnostic.test.cjs'); if (fs.existsSync(skeletonTestFile)) { files.push(skeletonTestFile); } return files.sort(); } /** * §8.5 — fixture-proof invariant: for every code in `rules`, confirm at * least one test file in `testFiles` has a `describe(`/`test(`/`it(` block * whose title names that exact code — UNLESS the code is listed in * `PERMANENTLY_INERT_CODES`, in which case it is reported separately as * `exempted` (visibly, not folded into "covered") and never fails the guard * regardless of test coverage. * * @param {Array<{code: string}>} rules * @param {string[]} testFiles absolute paths to *.test.cjs files to scan * @param {Map} inertCodes PERMANENTLY_INERT_CODES (injectable for tests) * @returns {{uncovered: string[], exempted: string[], testFilesScanned: string[]}} */ function checkFixtureProofInvariant(rules, testFiles, inertCodes = PERMANENTLY_INERT_CODES) { const allTitles = []; for (const file of testFiles) { const text = fs.readFileSync(file, 'utf8'); allTitles.push(...extractTitledBlocks(text)); } const uncovered = []; const exempted = []; for (const rule of rules) { if (inertCodes.has(rule.code)) { exempted.push(rule.code); continue; } if (!codeAppearsInTitle(rule.code, allTitles)) { uncovered.push(rule.code); } } return { uncovered, exempted, testFilesScanned: testFiles }; } function formatRepoRelative(absPath) { return path.relative(REPO_ROOT, absPath).split(path.sep).join('/'); } function main() { const { RULES, SEVERITY } = loadCompiledModule(); const { duplicates, badSeverities } = checkOneToOneInvariant(RULES, SEVERITY); const testFiles = findHealthDiagnosticTestFiles(REPO_ROOT); const { uncovered, exempted } = checkFixtureProofInvariant(RULES, testFiles); const problems = []; if (duplicates.length > 0) { const list = duplicates.map((d) => ` ${d.code} (${d.count} occurrences)`).join('\n'); problems.push( `§8.2 rule 1 violated: ${duplicates.length} duplicated rule code(s) in RULES ` + `(${COMPILED_MODULE_REL}):\n${list}\n` + ' remedy: codes are append-only and 1:1 with a single Rule — rename or remove the duplicate.', ); } if (badSeverities.length > 0) { const list = badSeverities .map((b) => ` ${b.code}: severity=${JSON.stringify(b.severity)}`) .join('\n'); problems.push( `§8.2 rule 3 violated: ${badSeverities.length} rule(s) with a severity not in SEVERITY's values:\n${list}\n` + ' remedy: severity is a property of the RULE — set it to SEVERITY.ERROR/WARNING/INFO.', ); } if (uncovered.length > 0) { const scannedList = testFiles.map(formatRepoRelative).join('\n '); problems.push( `§8.5 violated: ${uncovered.length} rule code(s) with no describe()/test() block naming them ` + `(a comment or bare string mention does not count):\n ${uncovered.join(', ')}\n\n` + ` Searched these test files:\n ${scannedList}\n\n` + ' remedy: add or extend a describe()/test() title in the matching ' + 'tests/health-diagnostic-rules/.test.cjs file so the block title ' + `names the code verbatim (e.g. describe('${uncovered[0]} — ...', () => { ... })), ` + 'and drive the rule to fire against a real fixture built via createTempDir() + buildPlanningSnapshot() ' + '(see tests/health-diagnostic-rules/root-existence.test.cjs).', ); } if (problems.length > 0) { throw new ExitError(1, `${problems.join('\n\n')}\n`); } const coveredCount = RULES.length - exempted.length; const exemptedDetail = exempted .map((code) => `${code} (${PERMANENTLY_INERT_CODES.get(code)})`) .join('; '); console.log( `lint-health-diagnostic-rule-table: PASS — ${RULES.length} rule code(s): ${coveredCount} covered by a ` + `real fixture, ${exempted.length} exempted across ${testFiles.length} test file(s).` + (exempted.length > 0 ? `\n Exempted: ${exemptedDetail}` : ''), ); } runMain(main); module.exports = { loadCompiledModule, checkOneToOneInvariant, extractTitledBlocks, codeAppearsInTitle, findHealthDiagnosticTestFiles, checkFixtureProofInvariant, PERMANENTLY_INERT_CODES, COMPILED_MODULE_PATH, TEST_GROUP_DIR, SKELETON_TEST_FILE, };