diff --git a/src/resources/extensions/gsd/migrate/writer.ts b/src/resources/extensions/gsd/migrate/writer.ts index 27b627ba9..4fa12eaf9 100644 --- a/src/resources/extensions/gsd/migrate/writer.ts +++ b/src/resources/extensions/gsd/migrate/writer.ts @@ -483,6 +483,45 @@ export async function writeGSDDirectory( counts.research++; } + // For fully-completed milestones (all slices done), write a pass-through + // validation file so deriveState() doesn't enter validating-milestone + // phase for historical milestones that predate the validation gate (#819). + const allSlicesDone = milestone.slices.length > 0 && milestone.slices.every(s => s.done); + if (allSlicesDone) { + const validationPath = join(mDir, `${milestone.id}-VALIDATION.md`); + const validationContent = [ + `---`, + `verdict: pass`, + `migrated: true`, + `---`, + ``, + `# ${milestone.id} Validation`, + ``, + `Migrated milestone — all slices were completed in the original project.`, + ``, + ].join('\n'); + await saveFile(validationPath, validationContent); + paths.push(validationPath); + counts.other++; + + // Also write a milestone summary if one doesn't exist + const summaryPath = join(mDir, `${milestone.id}-SUMMARY.md`); + const summaryContent = [ + `---`, + `status: done`, + `migrated: true`, + `---`, + ``, + `# ${milestone.id}: ${milestone.title}`, + ``, + `Migrated from .planning — ${milestone.slices.length} slices completed.`, + ``, + ].join('\n'); + await saveFile(summaryPath, summaryContent); + paths.push(summaryPath); + counts.other++; + } + // Slices for (const slice of milestone.slices) { const sDir = join(mDir, 'slices', slice.id); diff --git a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts index f86dae777..fca6a533b 100644 --- a/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +++ b/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts @@ -11,6 +11,7 @@ import { writeGSDDirectory } from '../migrate/writer.ts'; import { generatePreview } from '../migrate/preview.ts'; import { parseRoadmap, parsePlan, parseSummary } from '../files.ts'; import { deriveState } from '../state.ts'; +import { invalidateAllCaches } from '../cache.ts'; import type { GSDProject, GSDMilestone, @@ -207,6 +208,7 @@ async function main(): Promise { // (e) deriveState console.log(' --- deriveState ---'); + invalidateAllCaches(); const state = await deriveState(base); assertEq(state.phase, 'executing', 'incomplete: deriveState phase is executing'); assertTrue(state.activeMilestone !== null, 'incomplete: deriveState has activeMilestone'); @@ -262,14 +264,18 @@ async function main(): Promise { assertTrue(!existsSync(join(m, 'M001-RESEARCH.md')), 'complete: M001-RESEARCH.md NOT written (null)'); // No REQUIREMENTS.md since empty requirements assertTrue(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)'); + // Completed milestone should have VALIDATION and SUMMARY from migration (#819) + assertTrue(existsSync(join(m, 'M001-VALIDATION.md')), 'complete: M001-VALIDATION.md written for completed milestone'); + assertTrue(existsSync(join(m, 'M001-SUMMARY.md')), 'complete: M001-SUMMARY.md written for completed milestone'); - // deriveState: all slices done, all tasks done — needs validation then milestone summary - // Without VALIDATION file, it should be 'validating-milestone' + // deriveState: all slices done, all tasks done — migration now writes + // VALIDATION.md and SUMMARY.md for completed milestones (#819), + // so the milestone should be fully complete. + invalidateAllCaches(); const state = await deriveState(base); - // All slices are done in roadmap. No VALIDATION or SUMMARY exists. - // deriveState should return 'validating-milestone' since validation gate precedes completion. - assertEq(state.phase, 'validating-milestone', 'complete: deriveState phase is validating-milestone'); - assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone'); + assertEq(state.phase, 'complete', 'complete: deriveState phase is complete (validation + summary written by migration)'); + // When all milestones are complete, activeMilestone points to the last entry (for display) + assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone (last entry)'); assertEq(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001'); // generatePreview for complete project