From bb01f46c6bf394db78a03117eaa3c6a0f6fa30e3 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 5 Jul 2026 16:59:23 -0400 Subject: [PATCH] fix(#2022): gate roadmap update-plan-progress checkbox on verification passed (#2030) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#2022): gate roadmap update-plan-progress checkbox on verification passed cmdRoadmapUpdatePlanProgress stamped the phase checkbox + completion date the moment all summaries landed, with NO verification gate — unlike cmdPhaseComplete (phase.cts:1436) which checks readVerificationStatus. Since update-plan-progress is called after every wave and every plan, the checkbox fired before gsd-verifier confirmed the phase. - src/roadmap.cts: isComplete now requires summaryCount >= planCount AND readVerificationStatus(phaseDir).status === 'passed'. - tests/roadmap.test.cjs: 2 regression tests (no VERIFICATION.md → not complete; gaps_found → not complete) + updated 3 existing complete tests to include a passed VERIFICATION.md. Closes #2022 * docs(#2022): backfill changeset pr 2030 --- .changeset/2022-roadmap-verify-gate.md | 5 ++ src/roadmap.cts | 11 +++- tests/roadmap.test.cjs | 79 +++++++++++++++++++++++++- 3 files changed, 93 insertions(+), 2 deletions(-) create mode 100644 .changeset/2022-roadmap-verify-gate.md diff --git a/.changeset/2022-roadmap-verify-gate.md b/.changeset/2022-roadmap-verify-gate.md new file mode 100644 index 000000000..bd31bfd92 --- /dev/null +++ b/.changeset/2022-roadmap-verify-gate.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2030 +--- +**`roadmap update-plan-progress` no longer checks the phase checkbox without verification** — the command stamped the phase-level ROADMAP checkbox and completion date the moment the last plan summary landed (called routinely after every wave and every plan), with **no verification gate** — unlike `phase.complete` which correctly requires `readVerificationStatus(...).status === 'passed'`. Now `isComplete` requires both all plan summaries AND a passed verification, matching the `cmdPhaseComplete` contract, so the checkbox only fires after `gsd-verifier` has confirmed the phase. (#2022) diff --git a/src/roadmap.cts b/src/roadmap.cts index 654493111..740794042 100644 --- a/src/roadmap.cts +++ b/src/roadmap.cts @@ -34,6 +34,9 @@ const { countMatchedSummaries } = coreUtils; // eslint-disable-next-line @typescript-eslint/no-require-imports import frontmatter = require('./frontmatter.cjs'); const { extractFrontmatter, parseMustHavesBlock } = frontmatter; +// eslint-disable-next-line @typescript-eslint/no-require-imports +import verificationMod = require('./verification.cjs'); +const { readVerificationStatus } = verificationMod; // ─── Types ──────────────────────────────────────────────────────────────────── @@ -504,7 +507,13 @@ function cmdRoadmapUpdatePlanProgress(cwd: string, phaseNum: string | null | und return; } - const isComplete = summaryCount >= planCount; + // Verification gate (#2022): do NOT check the phase checkbox or stamp a + // completion date until the phase's verification status is 'passed', matching + // cmdPhaseComplete's gate (phase.cts:1436). Previously the checkbox fired the + // moment the last plan summary landed — before gsd-verifier had verified. + const phaseDir = path.join(cwd, phaseInfo!.directory); + const verificationPassed = readVerificationStatus(phaseDir).status === 'passed'; + const isComplete = summaryCount >= planCount && verificationPassed; const status = isComplete ? 'Complete' : summaryCount > 0 ? 'In Progress' : 'Planned'; const today = realClock.today(); diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index 83ef41f8d..da0ebfe65 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -814,6 +814,80 @@ describe('roadmap update-plan-progress command', () => { assert.ok(!/\[x\] \*\*Phase 30/.test(roadmapContent), 'phase checkbox must not be checked'); }); + test('#2022 — all summaries present but verification NOT passed → checkbox NOT checked', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] **Phase 1: Test** - description + +### Phase 1: Test +**Goal:** Test goal +**Plans:** TBD + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Test | v1.0 | 0/1 | Planned | - | +` + ); + + // 1 plan + 1 summary (all summaries present) but NO VERIFICATION.md → the + // verification gate (#2022) must prevent the checkbox from being checked. + const p1 = path.join(tmpDir, '.planning', 'phases', '01-test'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan 1'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary 1'); + + const result = runGsdTools('roadmap update-plan-progress 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.complete, false, 'must NOT be complete without verification'); + assert.strictEqual(output.status, 'In Progress', 'status should be In Progress (not Complete)'); + + const roadmapContent = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(roadmapContent.includes('[ ] **Phase 1'), 'phase checkbox must remain unchecked'); + assert.ok(!/\[x\] \*\*Phase 1/.test(roadmapContent), 'phase checkbox must NOT be checked without verification'); + assert.ok(roadmapContent.includes('1/1'), 'plan count should still be updated'); + }); + + test('#2022 — verification status NOT passed → checkbox NOT checked', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] **Phase 1: Test** + +### Phase 1: Test +**Goal:** Test goal + +## Progress + +| Phase | Milestone | Plans Complete | Status | Completed | +|-------|-----------|----------------|--------|-----------| +| 1. Test | v1.0 | 0/1 | Planned | - | +` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-test'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan 1'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary 1'); + // Verification exists but status is gaps_found (not passed) + fs.writeFileSync(path.join(p1, '01-VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Verification\n'); + + const result = runGsdTools('roadmap update-plan-progress 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.complete, false, 'must NOT be complete (verification gaps_found)'); + assert.strictEqual(output.status, 'In Progress'); + const roadmapContent = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + assert.ok(!roadmapContent.includes('[x]'), 'checkbox must NOT be checked (verification not passed)'); + }); + test('updates progress and checks checkbox on completion', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'), @@ -833,11 +907,12 @@ describe('roadmap update-plan-progress command', () => { ` ); - // Create phase dir with 1 plan, 1 summary (complete) + // Create phase dir with 1 plan, 1 summary (complete) + verification passed (#2022 gate) const p1 = path.join(tmpDir, '.planning', 'phases', '01-test'); fs.mkdirSync(p1, { recursive: true }); fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan 1'); fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary 1'); + fs.writeFileSync(path.join(p1, '01-VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification\n'); const result = runGsdTools('roadmap update-plan-progress 1', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); @@ -878,6 +953,7 @@ describe('roadmap update-plan-progress command', () => { fs.mkdirSync(p3, { recursive: true }); fs.writeFileSync(path.join(p3, '03-01-PLAN.md'), '# Plan 1'); fs.writeFileSync(path.join(p3, '03-01-SUMMARY.md'), '# Summary 1'); + fs.writeFileSync(path.join(p3, '03-VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification\n'); const result = runGsdTools('roadmap update-plan-progress 03', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`); @@ -959,6 +1035,7 @@ describe('roadmap update-plan-progress command', () => { fs.mkdirSync(p50, { recursive: true }); fs.writeFileSync(path.join(p50, '50-01-PLAN.md'), '# Plan'); fs.writeFileSync(path.join(p50, '50-01-SUMMARY.md'), '# Summary'); + fs.writeFileSync(path.join(p50, '50-VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification\n'); const result = runGsdTools('roadmap update-plan-progress 50', tmpDir); assert.ok(result.success, `Command failed: ${result.error}`);