fix: generate validation and summary files for completed milestones during migration (#819) (#838)

The .planning → .gsd migration creates roadmaps and summaries but not
VALIDATION files. deriveState() requires a terminal validation file
(verdict: pass) to consider a milestone complete. Without it, every
migrated milestone enters validating-milestone phase, blocking progress
to the actual current milestone.

For milestones where all slices are done, write a pass-through
VALIDATION.md (verdict: pass, migrated: true) and SUMMARY.md so
deriveState() skips them correctly.

Updated integration test to verify VALIDATION/SUMMARY files are written
and deriveState returns 'complete' phase with activeMilestone pointing
to the last completed entry (expected behavior).
This commit is contained in:
Tom Boucher 2026-03-17 09:47:57 -04:00 committed by GitHub
parent 1aebc06c46
commit b44896e187
2 changed files with 51 additions and 6 deletions

View file

@ -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);

View file

@ -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<void> {
// (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<void> {
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