#!/usr/bin/env node // tools/asset-catalog/scripts/lint-adr.mjs // Source: Plan 02-06 task 2; extended Plan 03-07 task 1 with --no-matrix flag. // // Validates a Michael Nygard format ADR (D-14) and verifies that every MX-* // citation in the ADR resolves to a row in MATRIX-rows.json (Pitfall 6 // mitigation per RESEARCH §455-463). Also checks Phaser 4 caveat coverage. // // Phase 3 ADRs (e.g. 0002-persistence-layer.md) are not engine ADRs and do not // cite MX-* row IDs. Pass --no-matrix to skip the MX-* citation, MATRIX.md // reference, and Phaser 4 caveat checks; only the four required Michael Nygard // sections (Status / Context / Decision / Consequences) are then enforced. // // Usage: // node tools/asset-catalog/scripts/lint-adr.mjs [] [--no-matrix] // Exit codes: 0 success, 1 validation failure, 2 usage error. import { existsSync, readFileSync } from 'node:fs'; function printUsage() { process.stderr.write( 'Usage: lint-adr.mjs [] [--no-matrix]\n', ); process.stderr.write( ' adr-path: path to ADR .md file (e.g. docs/adr/0002-persistence-layer.md)\n', ); process.stderr.write( ' matrix-rows-json-path: path to MATRIX-rows.json ' + '(default docs/extracted-engine/MATRIX-rows.json)\n', ); process.stderr.write( ' --no-matrix: skip MX-* citation + Phaser-4 caveat + MATRIX.md ' + 'reference checks (Phase 3+ non-engine ADRs)\n', ); } const args = process.argv.slice(2); // --help / -h short-circuit BEFORE positional filtering so `lint-adr.mjs --help` // still exits 0 (regression: pre-Plan 03-07 behavior). `--help` would otherwise // be filtered out as a flag and adrArg would be undefined → exit 2. if (args.includes('--help') || args.includes('-h')) { printUsage(); process.exit(0); } const noMatrix = args.includes('--no-matrix'); const positional = args.filter((a) => !a.startsWith('-')); const adrArg = positional[0]; const matrixArg = positional[1] ?? 'docs/extracted-engine/MATRIX-rows.json'; if (!adrArg) { printUsage(); process.exit(2); } if (!existsSync(adrArg)) { process.stderr.write(`ADR not found: ${adrArg}\n`); process.exit(1); } const adr = readFileSync(adrArg, 'utf-8'); let errors = 0; // Required Michael Nygard sections (per D-14). Each must appear at least once // as an H2 (## Section). Order is conventional but not enforced — graders may // occasionally swap Decision/Context, but all four must be present. const REQUIRED_SECTIONS = [ '## Status', '## Context', '## Decision', '## Consequences', ]; for (const section of REQUIRED_SECTIONS) { if (!adr.includes(section)) { process.stderr.write(`ADR missing required section: ${section}\n`); errors++; } } // Phaser 4 caveat (RESEARCH §908-922): the ADR MUST explicitly address Phaser // 4 so Phase 6 doesn't silently re-open the engine choice. Match // "Phaser 4" / "Phaser 4.1" / "phaser 4" / "phaser4" — case-insensitive. // // Skipped for non-engine ADRs (--no-matrix). if (!noMatrix) { if (!/[Pp]haser\s*4/.test(adr)) { process.stderr.write( `ADR missing Phaser 4 discussion (Phaser 4 caveat per RESEARCH §908-922)\n`, ); errors++; } } // Pitfall 6: every MX-* citation in the ADR must resolve in MATRIX-rows.json, // AND the ADR must cite at least 3 unique MX-* IDs (Plan 02-06 must_haves // requirement: "ADR cites at least 3 specific MX-* row IDs"). // // Skipped for non-engine ADRs (--no-matrix). const mxIds = [...adr.matchAll(/\bMX-[A-Z]+-\d+\b/g)].map((m) => m[0]); const uniqueIds = [...new Set(mxIds)]; if (!noMatrix) { if (uniqueIds.length < 3) { process.stderr.write( `ADR cites ${uniqueIds.length} unique MX-* row IDs; need ≥ 3 ` + `(Pitfall 6 + Plan 02-06 must_haves)\n`, ); errors++; } if (existsSync(matrixArg)) { let rows; try { rows = JSON.parse(readFileSync(matrixArg, 'utf-8')); } catch (e) { process.stderr.write( `MATRIX-rows.json invalid JSON at ${matrixArg}: ${e.message}\n`, ); process.exit(1); } if (!Array.isArray(rows)) { process.stderr.write( `MATRIX-rows.json at ${matrixArg}: top-level must be an array\n`, ); process.exit(1); } const validIds = new Set(rows.map((r) => r.rowId)); for (const id of uniqueIds) { if (!validIds.has(id)) { process.stderr.write( `ADR cites ${id} but it does not resolve in ${matrixArg} ` + `(Pitfall 6 mitigation)\n`, ); errors++; } } } else { process.stderr.write( `MATRIX-rows.json not found at ${matrixArg}; ` + `skipping rowId citation resolution check\n`, ); } // References section should mention MATRIX.md (Plan 02-06 must_haves + // RESEARCH §"ADR Format + Citation Discipline"). if (!/MATRIX\.md/.test(adr)) { process.stderr.write( `ADR References section should mention MATRIX.md\n`, ); errors++; } } if (errors > 0) { process.stderr.write(`\nlint-adr: ${errors} error(s)\n`); process.exit(1); } const summary = noMatrix ? `OK: ADR ${adrArg} validated (no-matrix mode — Michael Nygard sections present)\n` : `OK: ADR ${adrArg} validated (${uniqueIds.length} unique MX-* citations resolved)\n`; process.stdout.write(summary); process.exit(0);