'use strict'; /** * Behavioral tests for scripts/gen-adr-index.cjs — the ADR index generator and * lifecycle gate (#2340). * * These drive the real CLI as a subprocess against synthetic ADR corpora in a * temp dir, asserting on exit code and emitted text. No source-grepping: the * runtime behavior is the contract. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { createTempDir, cleanup } = require('./helpers.cjs'); const REPO_ROOT = path.resolve(__dirname, '..'); const SCRIPT_REL = path.join('scripts', 'gen-adr-index.cjs'); const START = ''; const END = ''; /** * Build a throwaway repo whose docs/adr/ contains exactly `files`, and whose * scripts/ holds a copy of the generator + its cli-exit dependency. A unique * mkdtemp per call keeps parallel tests from colliding, and the dir is removed * via `t.after()` so a failing assertion cannot leak it. */ function makeRepo(t, files) { // helpers.cleanup (not raw fs.rmSync) carries the Windows-EBUSY retry budget. const root = createTempDir('gsd-adr-index-'); t.after(() => cleanup(root)); fs.mkdirSync(path.join(root, 'docs', 'adr'), { recursive: true }); fs.mkdirSync(path.join(root, 'scripts', 'lib'), { recursive: true }); fs.copyFileSync(path.join(REPO_ROOT, SCRIPT_REL), path.join(root, SCRIPT_REL)); fs.copyFileSync( path.join(REPO_ROOT, 'scripts', 'lib', 'cli-exit.cjs'), path.join(root, 'scripts', 'lib', 'cli-exit.cjs'), ); for (const [name, body] of Object.entries(files)) { fs.writeFileSync(path.join(root, 'docs', 'adr', name), body); } fs.writeFileSync( path.join(root, 'docs', 'adr', 'README.md'), `# ADRs\n\n## Index\n\n${START}\n${END}\n`, ); return root; } /** * Run the generator in `root`; never throws — returns {status, stdout, stderr}. * * spawnSync (not execFileSync) because BOTH streams matter on BOTH outcomes: * `--write` exits 0 while reporting outstanding violations on stderr, and * execFileSync only surfaces stderr via the thrown error on non-zero exit. */ function run(root, args = []) { const res = spawnSync(process.execPath, [path.join(root, SCRIPT_REL), ...args], { cwd: root, encoding: 'utf8', timeout: 30_000, }); if (res.error) throw res.error; return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' }; } const adr = (title, fields) => `# ${title}\n\n${fields.map((f) => `- ${f}`).join('\n')}\n\n## Context\n\nBody.\n`; test('a clean corpus generates an index and --check passes', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha module', ['**Status:** Accepted', '**Date:** 2026-01-01']), '900-beta.md': adr('ADR-900: Beta module', ['**Status:** Proposed', '**Date:** 2026-01-02']), }); const write = run(root, ['--write']); assert.equal(write.status, 0, `--write failed: ${write.stderr}`); const check = run(root, ['--check']); assert.equal(check.status, 0, `--check failed: ${check.stderr}`); const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); assert.match(readme, /\[ADR-0001\]\(0001-alpha\.md\)/, 'zero-padded id must render as written, not ADR-1'); assert.match(readme, /\[ADR-900\]\(900-beta\.md\)/); assert.match(readme, /### Active decisions\b/); assert.match(readme, /### Proposed\b/); }); test('--check fails when an ADR is added but the index is not regenerated', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']) }); assert.equal(run(root, ['--write']).status, 0); // A new ADR lands without re-running --write. This is the exact drift that let // the hand-maintained index reach 40/65. fs.writeFileSync(path.join(root, 'docs', 'adr', '901-gamma.md'), adr('Gamma', ['**Status:** Accepted'])); const check = run(root, ['--check']); assert.equal(check.status, 1, 'a missing index row must fail CI'); assert.match(check.stderr, /stale/i); }); test('a status outside the vocabulary is rejected and names the offender', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Draft']) }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /0001-alpha\.md/); assert.match(res.stderr, /"Draft" is not one of/); }); test('an ADR with no status field at all is rejected', (t) => { const root = makeRepo(t, { '0001-alpha.md': '# Alpha\n\nNo header fields.\n\n## Context\n\nBody.\n' }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /no `- \*\*Status:\*\* ` field/); }); test('the table header form is parsed as legitimately as the bullet form', (t) => { // ADR-2008 uses a markdown table for its header. Treating that as "missing a // status" would flag a correct ADR. const root = makeRepo(t, { '0001-alpha.md': '# Alpha\n\n| | |\n|---|---|\n| **Status** | Accepted |\n| **Date** | 2026-01-01 |\n\n## Context\n\nBody.\n', }); const res = run(root, ['--write']); assert.equal(res.status, 0, `table-form header must parse: ${res.stderr}`); assert.match(fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'), /### Active decisions\b/); }); test('Superseded must name its successor as a file link, not a bare id', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by ADR-900 (2026-02-01)']), '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /not as a markdown link/); }); test('Superseded with no successor at all is rejected', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Superseded']) }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /names no successor/); }); test('a one-way supersession is rejected and the message names the fix', (t) => { const root = makeRepo(t, { // Beta claims Alpha; Alpha says nothing back. '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /0001-alpha\.md/); assert.match(res.stderr, /does not record it/); assert.match(res.stderr, /\*\*Superseded by:\*\* \[ADR-900\]\(900-beta\.md\)/); }); test('a symmetric supersession pair passes', (t) => { const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md) (2026-02-01)']), }); assert.equal(run(root, ['--check']).status, 1, 'index not yet written'); assert.equal(run(root, ['--write']).status, 0); assert.equal(run(root, ['--check']).status, 0, 'a symmetric pair must pass'); }); test('subsumption is symmetry-checked but does NOT mark the target superseded', (t) => { // The EoS case: ADR-1239 subsumes ADR-1016 as an adapter. ADR-1016 stays // Accepted — collapsing this into supersession would kill a live decision. const root = makeRepo(t, { '900-eos.md': adr('EoS', ['**Status:** Accepted', '**Subsumes as adapters:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Subsumed by:** [ADR-900](900-eos.md)']), }); assert.equal(run(root, ['--write']).status, 0); const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); assert.match(readme, /### Active decisions\b/, 'a subsumed ADR stays Active'); // The subsumer is surfaced in the "Read first" column so EoS is discoverable // from the component ADR. assert.match(readme, /\| \[ADR-0001\]\(0001-alpha\.md\) \|[^|]*\| Accepted \| \[ADR-900\]\(900-eos\.md\) \|/); }); test('a missing subsumption back-link is rejected', (t) => { const root = makeRepo(t, { '900-eos.md': adr('EoS', ['**Status:** Accepted', '**Subsumes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /\*\*Subsumed by:\*\* \[ADR-900\]\(900-eos\.md\)/); }); test("a Proposed ADR's supersession claim is prospective — no back-link demanded", (t) => { // ADR-857 is Proposed and claims to generalize live ADRs. Demanding the // back-link would stamp an Accepted decision as superseded by an unratified one. const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Proposed', '**Supersedes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); assert.equal(run(root, ['--write']).status, 0); const check = run(root, ['--check']); assert.equal(check.status, 0, `a Proposed claimant must not force a back-link: ${check.stderr}`); }); test('ratifying that Proposed ADR to Accepted then demands the back-link', (t) => { const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); const res = run(root, ['--check']); assert.equal(res.status, 1, 'on ratification the back-link becomes required'); assert.match(res.stderr, /does not record it/); }); test('"Supersedes: nothing" asserts no relation even when it name-drops an ADR', (t) => { // ADR-2264 says "Supersedes: nothing; amends the ADR-1239 harness". Reading that // as a supersession claim invents a link the author never made. const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** nothing; amends the [ADR-0001](0001-alpha.md) harness']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); assert.equal(run(root, ['--write']).status, 0); const check = run(root, ['--check']); assert.equal(check.status, 0, `a negated relation field must assert nothing: ${check.stderr}`); }); test('an em-dash relation value asserts no relation', (t) => { // Vacuous unless the negated field also carries a LINK: with a bare em-dash // there is nothing to mis-parse, so the test passes whether or not negation // works. Linking an ADR after the em-dash makes it discriminating — if the // field were read as a real claim, symmetry would demand 0001 record it. const root = makeRepo(t, { '900-beta.md': '# Beta\n\n| | |\n|---|---|\n| **Status** | Accepted |\n| **Supersedes** | — see [ADR-0001](0001-alpha.md) for context |\n\n## Context\n\nBody.\n', '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); assert.equal(run(root, ['--write']).status, 0); const check = run(root, ['--check']); assert.equal(check.status, 0, `an em-dash field must assert nothing: ${check.stderr}`); assert.doesNotMatch(check.stderr, /does not record it/); }); test('a mixed field with one link and one bare id still flags the bare id', (t) => { // Regression: testing `rel.links.length` instead of the specific id meant a // field carrying ANY link silently dropped every bare claim beside it. const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md), ADR-0002']), '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), '0002-gamma.md': adr('Gamma', ['**Status:** Accepted']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /names ADR-2 without a file link/); }); test('a bare id repeated in prose beside its own link is not flagged', (t) => { // The corpus legitimately writes "…([ADR-0001](0001-alpha.md)) — see ADR-0001 // below". That repeat must not be noise. const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** Alpha ([ADR-0001](0001-alpha.md)) — see the ADR-0001 note below']), '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), }); assert.equal(run(root, ['--write']).status, 0); const check = run(root, ['--check']); assert.equal(check.status, 0, `a linked-and-repeated id must not be flagged: ${check.stderr}`); }); test('a dangling "Superseded by" is caught even though the ADR is not Accepted', (t) => { // Regression: the ratification guard skipped every non-Accepted ADR, which // killed the IN direction entirely — a Superseded ADR pointing at a successor // that never claims it went unchecked. const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), '900-beta.md': adr('Beta', ['**Status:** Accepted']), }); const res = run(root, ['--check']); assert.equal(res.status, 1, 'a one-way superseded-by must fail'); assert.match(res.stderr, /does not claim it|does not record it/); }); test('a `## Supersedes` table section counts as the claim (ADR-0174 shape)', (t) => { // The richest form in the corpus declares supersession as a section+table, not // a header field. Reading only the header block reported the repo's // best-documented supersession as missing. const root = makeRepo(t, { '900-beta.md': '# Beta\n\n- **Status:** Accepted\n\n## Supersedes\n\n| ADR | What it said | Why superseded |\n|---|---|---|\n| [ADR-0001](0001-alpha.md) | a thing | a reason |\n\n## Context\n\nBody.\n', '0001-alpha.md': adr('Alpha', ['**Status:** Superseded by [ADR-900](900-beta.md)']), }); assert.equal(run(root, ['--write']).status, 0); const check = run(root, ['--check']); assert.equal(check.status, 0, `a ## Supersedes table must satisfy symmetry: ${check.stderr}`); }); test('a title id that disagrees with the filename is rejected', (t) => { // The real ADR-218 case: renamed to the issue# convention, title left behind. const root = makeRepo(t, { '218-release.md': adr('ADR-0175: Harden release validation', ['**Status:** Accepted']) }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /H1 declares ADR-175 but the filename says 218/); }); test('a relation link to a nonexistent ADR is rejected', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Supersedes:** [ADR-404](404-ghost.md)']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /does not exist in docs\/adr\//); }); test('a bare id naming a nonexistent ADR is steered toward issue syntax', (t) => { // ADR-1610 says "superseding the #597 tier-max ratchet" — #597 is an ISSUE. // Written as "ADR-597" it would be an unresolvable reference. const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted', '**Supersedes:** ADR-597 tier-max ratchet']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /If it is an ISSUE number, write "#597"/); }); test('an ambiguous bare id reports every file it could mean', (t) => { const root = makeRepo(t, { '0011-one.md': adr('One', ['**Status:** Accepted']), '0011-two.md': adr('Two', ['**Status:** Accepted']), '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** ADR-0011']), }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /ambiguous — resolves to 2 files/); assert.match(res.stderr, /0011-one\.md/); assert.match(res.stderr, /0011-two\.md/); }); test('--check fails loudly when the README markers are missing', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']) }); fs.writeFileSync(path.join(root, 'docs', 'adr', 'README.md'), '# ADRs\n\nNo markers here.\n'); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /missing the index markers/); }); test('--write still emits the index while reporting outstanding violations', (t) => { // --write must remain usable as a repair tool on a corpus that is not yet clean, // but must not pretend the corpus is healthy. const root = makeRepo(t, { '900-beta.md': adr('Beta', ['**Status:** Accepted', '**Supersedes:** [ADR-0001](0001-alpha.md)']), '0001-alpha.md': adr('Alpha', ['**Status:** Accepted']), }); const res = run(root, ['--write']); assert.equal(res.status, 0, '--write proceeds'); assert.match(res.stderr, /lifecycle violation\(s\) remain/); assert.match(fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'), /### Active decisions\b/); }); test('an ADR title cannot hijack the README splice with an index marker', (t) => { // Review finding: a title carrying the literal END marker was emitted verbatim // into the table cell, relocating the boundary so the NEXT --write spliced // against the wrong marker and ate the rest of README.md. const root = makeRepo(t, { '0001-alpha.md': adr(`Evil ${END} title`, ['**Status:** Accepted']), }); assert.equal(run(root, ['--write']).status, 0); const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); const endCount = readme.split(END).length - 1; assert.equal(endCount, 1, 'exactly one END marker must survive — the title must not forge another'); // The splice must remain stable across repeated writes. assert.equal(run(root, ['--write']).status, 0); const again = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); assert.equal(again.split(END).length - 1, 1); assert.equal(run(root, ['--check']).status, 0, 'a hostile title must not leave the index permanently stale'); }); test('a title cannot inject raw HTML into the generated index', (t) => { const root = makeRepo(t, { '0001-alpha.md': adr('Alpha module', ['**Status:** Accepted']), }); assert.equal(run(root, ['--write']).status, 0); const readme = fs.readFileSync(path.join(root, 'docs', 'adr', 'README.md'), 'utf8'); assert.ok(!readme.includes(' \\', ['**Status:** Accepted']), ); assertInsertOnly(before, after, 'adding an ADR whose title carries |, <, >, and \\'); const row = after.find((l) => l.includes('200-beta.md')); assert.ok(row, 'the hostile-title ADR must still have a row'); const cells = [...row.matchAll(/(?'), 'angle brackets must be escaped, not emitted raw'); }); // --- regressions ----------------------------------------------------------- // // The --check gate above passes on a corpus that still carries dangling // references: it validates naming, relation symmetry, and index freshness, but // it never resolves a link target and it STRIPS the H1 status bracket // (gen-adr-index.cjs) rather than comparing it. Both defect classes below were // green under `--check` while broken. These assert on the real corpus, so // reverting the repair re-reds them. const ADR_DIR = path.join(REPO_ROOT, 'docs', 'adr'); const STATUS_TOKENS = ['Accepted', 'Proposed', 'Superseded', 'Legacy', 'Retired']; function adrMarkdownFiles() { return fs.readdirSync(ADR_DIR).filter((f) => f.endsWith('.md')); } test('every relative markdown link in docs/adr/ resolves to a file that exists', () => { const dangling = []; for (const file of adrMarkdownFiles()) { const body = fs.readFileSync(path.join(ADR_DIR, file), 'utf8'); for (const match of body.matchAll(/\]\(([^)#:\s]+\.md)(?:#[^)]*)?\)/g)) { const target = match[1]; if (!fs.existsSync(path.resolve(ADR_DIR, target))) { dangling.push(`${file} -> ${target}`); } } } assert.deepEqual( dangling, [], `dangling relative links in docs/adr/ (a link written as reference/x.md from inside docs/adr/ resolves to the nonexistent docs/adr/reference/):\n${dangling.join('\n')}`, ); }); test('no ADR H1 status bracket contradicts its Status field', () => { // The index generator strips a trailing "[Proposed]"-style bracket for // display instead of comparing it, so a stale bracket is invisible to the // gate while still being the first thing a reader sees. const mismatches = []; for (const file of adrMarkdownFiles()) { if (file === 'README.md') continue; const lines = fs.readFileSync(path.join(ADR_DIR, file), 'utf8').split(/\r?\n/); const heading = lines.find((l) => /^#\s/.test(l)) || ''; const bracket = heading.match(/\[(Proposed|Accepted|Superseded|Legacy|Retired)\]\s*$/i); if (!bracket) continue; const statusLine = lines.find((l) => /^\s*[-*]?\s*\*\*Status/.test(l)) || ''; // Resolve by earliest position in the line, not by STATUS_TOKENS order: a // Status field like "Superseded by ADR-X (was Accepted ...)" mentions two // tokens, and array order would pick 'Accepted' and report a false mismatch // against a correct [Superseded] bracket. let token; let tokenAt = Infinity; for (const s of STATUS_TOKENS) { const at = statusLine.search(new RegExp(`\\b${s}\\b`, 'i')); if (at !== -1 && at < tokenAt) { tokenAt = at; token = s; } } if (token && token.toLowerCase() !== bracket[1].toLowerCase()) { mismatches.push(`${file}: H1 says [${bracket[1]}], Status field says ${token}`); } } assert.deepEqual(mismatches, [], `H1 bracket contradicts Status:\n${mismatches.join('\n')}`); }); test('the ADR path cited by src/plan-drift-guard.cts exists', () => { // This module is compiled into the published payload, so a wrong citation // here ships to users. const src = fs.readFileSync(path.join(REPO_ROOT, 'src', 'plan-drift-guard.cts'), 'utf8'); const cited = [...src.matchAll(/docs\/adr\/([A-Za-z0-9._-]+\.md)/g)].map((m) => m[1]); assert.notEqual(cited.length, 0, 'expected plan-drift-guard.cts to cite its governing ADR'); for (const name of cited) { assert.ok( fs.existsSync(path.join(ADR_DIR, name)), `src/plan-drift-guard.cts cites docs/adr/${name}, which does not exist`, ); } }); test('the ADR naming worked example names an ADR file that exists', () => { for (const rel of ['CONTRIBUTING.md', path.join('docs', 'contributor-standards.md')]) { const body = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8'); const examples = [...body.matchAll(/docs\/adr\/(\d+-[a-z0-9-]+\.md)/g)].map((m) => m[1]); assert.notEqual(examples.length, 0, `expected ${rel} to show a worked ADR-naming example`); for (const name of examples) { assert.ok( fs.existsSync(path.join(ADR_DIR, name)), `${rel} illustrates the naming convention with docs/adr/${name}, which does not exist`, ); } } }); // ─── #2705: legacy ADR range is single-sourced and matches disk ───────────── // // The legacy zero-padded ADR range used to be stated two different ways // (contributor-standards.md vs adr/README.md), both wrong. Now adr/README.md is // the single authoritative statement and contributor-standards.md references it. // The legacy set is the zero-padded ADRs whose numbers do NOT correspond to a // same-numbered repository issue; 0174 and 0656 are modern mis-padded files. describe('#2705: legacy ADR range single-sourced and accurate', () => { const STANDARDS = path.join(REPO_ROOT, 'docs', 'contributor-standards.md'); const README = path.join(REPO_ROOT, 'docs', 'adr', 'README.md'); // Zero-padded ADR files on disk (4-digit prefix). function zeroPaddedAdrFiles() { return fs.readdirSync(ADR_DIR) .filter((f) => /^\d{4}-.*\.md$/.test(f)) .map((f) => ({ file: f, num: Number(f.slice(0, 4)) })); } // A zero-padded number is "legacy sequential" iff no same-numbered issue-shaped // ADR exists (i.e. the 4-digit number is NOT an issue number reused). 0001–0012 // are sequential (collisions on disk corroborate); 0174/0656 match issues. function legacyNumbers() { const all = zeroPaddedAdrFiles(); const nums = new Set(all.map((a) => a.num)); const legacy = new Set(); for (const n of nums) { // Sequential legacy range: numbers 1..12 have intra-range duplicates on disk // (0010 x2, 0011 x3) and do not correspond to same-numbered modern files. if (n >= 1 && n <= 12) legacy.add(n); } return legacy; } test('exactly one doc restates the legacy range; the other references it', () => { const readme = fs.readFileSync(README, 'utf8'); const standards = fs.readFileSync(STANDARDS, 'utf8'); // The authoritative "0001-* through 0012-*" statement lives in README. The // range tokens are backtick-delimited in the prose (`0001-*` through `0012-*`), // so match the digits+through+digits ignoring the backtick/asterisk escapes. assert.match(readme, /0001-\*[^A-Za-z0-9]*through[^A-Za-z0-9]*0012-\*/, 'README must carry the authoritative legacy-range statement'); // contributor-standards must NOT restate the range — it must cross-reference README. assert.doesNotMatch( standards, /0001-\*[^A-Za-z0-9]*through[^A-Za-z0-9]*001[12]-\*/, 'contributor-standards.md must not restate the legacy range (single-sourced in README); it should reference it', ); assert.match( standards, /adr\/README\.md/, 'contributor-standards.md must reference docs/adr/README.md for the legacy range', ); }); test('README legacy range matches the on-disk legacy set and excludes mis-padded modern 0174/0656', () => { const readme = fs.readFileSync(README, 'utf8'); // The legacy clause must NOT include 0174 (the prior wrong "(and 0174-*)" clause). assert.doesNotMatch( readme, /through 0012-\* \(and 0174-\*\)/, 'README must not classify 0174 as legacy residue (it is a modern mis-padded ADR)', ); // The legacy set on disk (0001–0012) all exist as zero-padded files. const legacy = legacyNumbers(); for (const n of legacy) { const pad = String(n).padStart(4, '0'); const matches = fs.readdirSync(ADR_DIR).filter((f) => f.startsWith(`${pad}-`)); assert.ok(matches.length > 0, `legacy ADR ${pad}-* must exist on disk`); } }); test('README identifies 0174 and 0656 as mis-padded modern ADRs', () => { const readme = fs.readFileSync(README, 'utf8'); assert.match(readme, /0174/, 'README must mention 0174'); assert.match(readme, /0656/, 'README must mention 0656'); assert.match( readme, /modern.*mis-padded|mis-padded.*modern/i, 'README must identify the zero-padded modern ADRs as mis-padded modern files, not legacy residue', ); }); });