'use strict'; /** * Tests for `src/health-diagnostic-rules/roadmap-disk-consistency.cts` * (Phase 11, #3309, ADR-3180 §8.2/§8.3/§8.5) — group "ROADMAP/disk * consistency": W006, W007. * * Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md * Test matrix: .gsd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md * | W006 | ROADMAP phase with no disk dir | reused/representative | roadmap entry added, no matching dir created | * | W007 | disk dir with no ROADMAP entry | reused/representative | dir created, no roadmap entry | * * Fixture provenance (§8.5 + CONTRIBUTING "Fixture provenance (#2371)"): both * rules use CONTENT-SHAPE/MECHANICAL-MUTATION provenance — a realistic * multi-phase ROADMAP.md (mirroring `gsd-core/templates/roadmap.md`'s * heading shape) paired with a matching on-disk phase-dir tree, with exactly * ONE entry perturbed (one dir withheld for W006, one extra dir added for * W007). Every fixture is built via the REAL `buildPlanningSnapshot(cwd)` * against a REAL temp directory (mirrors `tests/planning-snapshot.test.cjs` * and `tests/health-diagnostic-rules/root-existence.test.cjs`) — no * hand-constructed fake `PlanningSnapshot` object. * * TDD RED: `src/health-diagnostic-rules/roadmap-disk-consistency.cts` does * not exist yet at the start of this batch — this file's * `require('../../gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs')` * throws MODULE_NOT_FOUND until this batch's implementation lands. */ const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { createTempDir, cleanup } = require('../helpers.cjs'); const roadmapDiskConsistency = require('../../gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs'); const { RULES } = roadmapDiskConsistency; const { buildPlanningSnapshot } = require('../../gsd-core/bin/lib/planning-snapshot.cjs'); const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = require('../../gsd-core/bin/lib/health-diagnostic.cjs'); const { SCOPE } = require('../../gsd-core/bin/lib/planning-scope.cjs'); function planningDirOf(cwd) { return path.join(cwd, '.planning'); } function writeRoadmap(cwd, content) { fs.mkdirSync(planningDirOf(cwd), { recursive: true }); fs.writeFileSync(path.join(planningDirOf(cwd), 'ROADMAP.md'), content); } function makePhaseDir(cwd, dirName) { fs.mkdirSync(path.join(planningDirOf(cwd), 'phases', dirName), { recursive: true }); } function ruleFor(code) { const rule = RULES.find((r) => r.code === code); assert.ok(rule, `rule ${code} not found in RULES`); return rule; } // ─── RULES shape ──────────────────────────────────────────────────────────── describe('RULES (roadmap-disk-consistency group)', () => { test('exports exactly 2 rules: W006, W007', () => { assert.deepEqual(RULES.map((r) => r.code).sort(), ['W006', 'W007']); }); test('both rules are severity WARNING', () => { assert.equal(ruleFor('W006').severity, SEVERITY.WARNING); assert.equal(ruleFor('W007').severity, SEVERITY.WARNING); }); }); // ─── W006 — ROADMAP phase with no disk dir ───────────────────────────────── describe('W006 — ROADMAP phase with no disk dir', () => { test('fires for exactly the one perturbed phase (3-phase roadmap, dir withheld for phase 2)', (t) => { const cwd = createTempDir('gsd-3309-w006-1-'); t.after(() => cleanup(cwd)); writeRoadmap( cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar', '', '### Phase 3: Baz'].join('\n'), ); makePhaseDir(cwd, '01-foo'); // Phase 2 deliberately has no matching directory. makePhaseDir(cwd, '03-baz'); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W006').check(snapshot); assert.equal(diagnostics.length, 1); assert.deepEqual(diagnostics[0], { code: 'W006', severity: SEVERITY.WARNING, message: 'Phase 2 in ROADMAP.md but no directory on disk', remedy: { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command: 'Create phase directory or remove from roadmap' }, }, }); }); test('does not fire when every declared phase resolves to a directory (padding/token tolerant)', (t) => { const cwd = createTempDir('gsd-3309-w006-2-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar'].join('\n')); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, '02-bar'); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(ruleFor('W006').check(snapshot), []); }); test('does not fire for a sentinel phase id (999.x) even with no matching directory', (t) => { const cwd = createTempDir('gsd-3309-w006-3-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 999.1: Icebox'].join('\n')); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(ruleFor('W006').check(snapshot), []); }); test('does not fire for a phase explicitly marked "not started" (unchecked checklist entry, no dir)', (t) => { const cwd = createTempDir('gsd-3309-w006-4-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## Phases', '', '- [ ] **Phase 5: Widgets** - build them'].join('\n')); const snapshot = buildPlanningSnapshot(cwd); // Sanity: the phase IS declared (so this is genuinely testing the // not-started exclusion, not an empty-declared-phases no-op). assert.ok(snapshot.roadmapDeclaredPhases.value.some((p) => p.phaseId === '5')); assert.deepEqual(ruleFor('W006').check(snapshot), []); }); test('DOES fire for an unrelated checked phase with no dir (not-started exclusion is per-phase, not global)', (t) => { const cwd = createTempDir('gsd-3309-w006-5-'); t.after(() => cleanup(cwd)); writeRoadmap( cwd, ['## Phases', '', '- [ ] **Phase 5: Widgets** - build them', '- [x] **Phase 6: Gadgets** - build them'].join( '\n', ), ); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W006').check(snapshot); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].message, 'Phase 6 in ROADMAP.md but no directory on disk'); }); test('boundary: zero declared phases and zero phase directories produces zero findings', (t) => { const cwd = createTempDir('gsd-3309-w006-6-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## Progress', '', '(no phases declared yet)'].join('\n')); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(snapshot.roadmapDeclaredPhases.value, []); assert.deepEqual(ruleFor('W006').check(snapshot), []); }); test('guard: ROADMAP.md absent does not fire (empty declared-phase list is a non-answer, not "zero declared")', (t) => { const cwd = createTempDir('gsd-3309-w006-7-'); t.after(() => cleanup(cwd)); fs.mkdirSync(planningDirOf(cwd), { recursive: true }); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(ruleFor('W006').check(snapshot), []); }); }); // ─── W007 — disk dir with no ROADMAP entry ───────────────────────────────── describe('W007 — disk dir with no ROADMAP entry', () => { test('fires for exactly the one perturbed directory (3-phase roadmap, one extra orphan dir)', (t) => { const cwd = createTempDir('gsd-3309-w007-1-'); t.after(() => cleanup(cwd)); writeRoadmap( cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar', '', '### Phase 3: Baz'].join('\n'), ); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, '02-bar'); makePhaseDir(cwd, '03-baz'); // Deliberately orphaned: no roadmap entry claims this directory. makePhaseDir(cwd, '04-extra'); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W007').check(snapshot); assert.equal(diagnostics.length, 1); assert.deepEqual(diagnostics[0], { code: 'W007', severity: SEVERITY.WARNING, message: 'Phase 04 exists on disk but not in ROADMAP.md', remedy: { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command: 'Add to roadmap or remove directory' }, }, }); }); test('does not fire when every directory is claimed by a declared phase', (t) => { const cwd = createTempDir('gsd-3309-w007-2-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar'].join('\n')); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, '02-bar'); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(ruleFor('W007').check(snapshot), []); }); test('does not fire for a sentinel directory (999-interim) even with no roadmap entry', (t) => { const cwd = createTempDir('gsd-3309-w007-3-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n')); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, '999-interim'); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(ruleFor('W007').check(snapshot), []); }); test('#3639: does not fire for a BRACKET icebox directory (GSD.999-07-icebox) even with no roadmap entry', (t) => { // Bracket sentinel-ness lives in the MILESTONE portion ({CODE}.999-{PP}); // the convention-less predicate read only the phase token and never saw it. const cwd = createTempDir('gsd-3639-w007-bracket-ice-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## GSD.03 Current 🚧', '', '### Phase 3.1: Foo'].join('\n')); makePhaseDir(cwd, 'GSD.03-01-foo'); makePhaseDir(cwd, 'GSD.999-07-icebox'); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W007').check(snapshot); assert.deepEqual( diagnostics.filter((d) => d.message.includes('999-07-icebox')), [], `a bracket icebox dir is never-on-roadmap by convention — no orphan warning. Got: ${JSON.stringify(diagnostics)}`, ); }); test('#3639: does not fire for a BRACKET pre-milestone directory (GSD.00-01-backlog)', (t) => { const cwd = createTempDir('gsd-3639-w007-bracket-pre-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## GSD.03 Current 🚧', '', '### Phase 3.1: Foo'].join('\n')); makePhaseDir(cwd, 'GSD.03-01-foo'); makePhaseDir(cwd, 'GSD.00-01-backlog'); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W007').check(snapshot); assert.deepEqual( diagnostics.filter((d) => d.message.includes('00-01-backlog')), [], `a bracket pre-milestone dir is never-on-roadmap by convention. Got: ${JSON.stringify(diagnostics)}`, ); }); test('#3639 over-suppression guard: an UNCLAIMED ordinary #1324 digit-continuation dir still fires W007', (t) => { // The disclosed residual (#3639 review): #1324 dirs with a SENTINEL-valued // first decimal (`P0.0-1-x`) are indistinguishable from bracket sentinels // and read as sentinel (suppressed). This row pins that the family is not // BLANKET-suppressed: a NON-sentinel first decimal (`P0.1-2-x`), unclaimed, // must still fire — otherwise the recognizer would silently swallow every // letter-prefixed real dir. const cwd = createTempDir('gsd-3639-w007-over-sup-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n')); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, 'P0.1-2-x'); const snapshot = buildPlanningSnapshot(cwd); const diagnostics = ruleFor('W007').check(snapshot); assert.ok( diagnostics.some((d) => d.message.includes('P0.1-2')), `an unclaimed ordinary #1324 digit-continuation dir must still fire W007 (the message names the extracted token, not the full dir). Got: ${JSON.stringify(diagnostics)}`, ); }); test('boundary: zero declared phases and zero phase directories produces zero findings', (t) => { const cwd = createTempDir('gsd-3309-w007-4-'); t.after(() => cleanup(cwd)); writeRoadmap(cwd, ['## Progress', '', '(no phases declared yet)'].join('\n')); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(snapshot.allPhaseDirNames.value, []); assert.deepEqual(ruleFor('W007').check(snapshot), []); }); test('regression: fires for a genuine orphan directory outside the roadmap-declared window (the W007-inert defect)', (t) => { const cwd = createTempDir('gsd-3309-w007-6-'); t.after(() => cleanup(cwd)); // ROADMAP declares only phase 1 — "04-extra" is not declared anywhere, // so `phaseDirs` (windowed to declared phases) would silently drop it // and W007 would never see it; `allPhaseDirNames` must not. writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n')); makePhaseDir(cwd, '01-foo'); makePhaseDir(cwd, '04-extra'); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual( snapshot.phaseDirs.value, ['01-foo'], 'sanity: the windowed phaseDirs field must NOT include the orphan (confirms the defect this test guards)', ); assert.ok(snapshot.allPhaseDirNames.value.includes('04-extra')); const diagnostics = ruleFor('W007').check(snapshot); assert.equal(diagnostics.length, 1); assert.equal(diagnostics[0].message, 'Phase 04 exists on disk but not in ROADMAP.md'); }); test('guard: ROADMAP.md absent does not fire for a pre-existing phase directory (no false positive)', (t) => { const cwd = createTempDir('gsd-3309-w007-5-'); t.after(() => cleanup(cwd)); fs.mkdirSync(planningDirOf(cwd), { recursive: true }); makePhaseDir(cwd, '01-foo'); const snapshot = buildPlanningSnapshot(cwd); assert.deepEqual(snapshot.roadmapDeclaredPhases, { value: [], scope: SCOPE.UNREADABLE }); assert.deepEqual(ruleFor('W007').check(snapshot), []); }); });