From f65f92cf7626d7d19b03bfd19fa805c5b6a02dca Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 20:07:42 -0700 Subject: [PATCH 1/2] fix(gsd): import all-done milestones as complete during DB migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit migrateHierarchyToDb imported milestones with all-done roadmap slices as "active" when SUMMARY.md was missing. This let plan-milestone overwrite already-completed work. Now checks parsed roadmap slices — if all are done, imports as "complete" even without SUMMARY.md. Closes #3390 Closes #3379 Co-Authored-By: Claude Opus 4.6 (1M context) --- src/resources/extensions/gsd/md-importer.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/resources/extensions/gsd/md-importer.ts b/src/resources/extensions/gsd/md-importer.ts index dfea9ad7c..b1054b5a7 100644 --- a/src/resources/extensions/gsd/md-importer.ts +++ b/src/resources/extensions/gsd/md-importer.ts @@ -530,11 +530,6 @@ export function migrateHierarchyToDb(basePath: string): { // Ghost milestone: no CONTEXT, ROADMAP, or SUMMARY → skip if (!hasRoadmap && !hasContext && !hasSummary) continue; - // Determine milestone status - let milestoneStatus = 'active'; - if (hasSummary) milestoneStatus = 'complete'; - else if (hasParked) milestoneStatus = 'parked'; - // Determine milestone title from roadmap H1 or CONTEXT heading let milestoneTitle = ''; let roadmapContent: string | null = null; @@ -544,6 +539,16 @@ export function migrateHierarchyToDb(basePath: string): { roadmap = parseRoadmap(roadmapContent); milestoneTitle = roadmap.title; } + + // Determine milestone status + let milestoneStatus = 'active'; + if (hasSummary) milestoneStatus = 'complete'; + else if (hasParked) milestoneStatus = 'parked'; + // Import milestones with all-done roadmap slices as complete (#3390, #3379) + // even when SUMMARY.md is missing — the roadmap checkboxes are authoritative. + else if (roadmap && roadmap.slices.length > 0 && roadmap.slices.every(s => s.done)) { + milestoneStatus = 'complete'; + } if (!milestoneTitle && hasContext) { const contextContent = readFileSync(contextPath!, 'utf-8'); const h1Match = contextContent.match(/^#\s+(.+)/m); From fe848933516c81bcb03a3c9903bc9f30d4d91773 Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Mon, 6 Apr 2026 22:26:51 -0700 Subject: [PATCH 2/2] test: add regression test for importing done milestones as complete Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gsd/tests/import-done-milestones.test.ts | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/resources/extensions/gsd/tests/import-done-milestones.test.ts diff --git a/src/resources/extensions/gsd/tests/import-done-milestones.test.ts b/src/resources/extensions/gsd/tests/import-done-milestones.test.ts new file mode 100644 index 000000000..6ec953714 --- /dev/null +++ b/src/resources/extensions/gsd/tests/import-done-milestones.test.ts @@ -0,0 +1,42 @@ +/** + * Regression test for #3699 — import milestones with all-done slices as complete + * + * During DB migration, milestones whose roadmap slices are all marked done + * should be imported with status "complete" instead of "active". + */ + +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const importerSrc = readFileSync( + join(__dirname, '..', 'md-importer.ts'), + 'utf-8', +); + +describe('import done milestones as complete (#3699)', () => { + test('all-slices-done check sets milestoneStatus to complete', () => { + // The importer should check if all roadmap slices are done + assert.match(importerSrc, /roadmap\.slices\.every\(s\s*=>\s*s\.done\)/, + 'should check roadmap.slices.every(s => s.done)'); + }); + + test('milestoneStatus is set to complete when all slices done', () => { + // Find the all-done guard and verify it sets 'complete' + const everyIdx = importerSrc.indexOf('roadmap.slices.every(s => s.done)'); + assert.ok(everyIdx > -1, 'all-slices-done check should exist'); + const afterCheck = importerSrc.slice(everyIdx, everyIdx + 200); + assert.match(afterCheck, /milestoneStatus\s*=\s*'complete'/, + 'should set milestoneStatus to complete when all slices are done'); + }); + + test('roadmap.slices.length > 0 guard prevents false positives', () => { + assert.match(importerSrc, /roadmap\.slices\.length\s*>\s*0/, + 'should guard against empty slices array'); + }); +});