fix: read depends_on from CONTEXT-DRAFT.md when CONTEXT.md absent (#1795)

This commit is contained in:
TÂCHES 2026-03-21 11:39:48 -06:00 committed by GitHub
parent afe5f58ea6
commit d587c91305
2 changed files with 85 additions and 5 deletions

View file

@ -333,9 +333,9 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
// Check milestone-level dependencies before promoting to active.
// Without this, a queued milestone with depends_on in its CONTEXT
// frontmatter would be promoted to active even when its deps are unmet
// (the dep check only existed in the has-roadmap path previously).
const deps = parseContextDependsOn(contextContent);
// or CONTEXT-DRAFT frontmatter would be promoted to active even when
// its deps are unmet.
const deps = parseContextDependsOn(contextContent ?? draftContent);
const depsUnmet = deps.some(dep => !completeMilestoneIds.has(dep));
if (depsUnmet) {
registry.push({ id: mid, title, status: 'pending', dependsOn: deps });
@ -397,8 +397,10 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
} else if (!activeMilestoneFound) {
// Check milestone-level dependencies before promoting to active
const contextFile = resolveMilestoneFile(basePath, mid, "CONTEXT");
const draftFile = resolveMilestoneFile(basePath, mid, "CONTEXT-DRAFT");
const contextContent = contextFile ? await cachedLoadFile(contextFile) : null;
const deps = parseContextDependsOn(contextContent);
const draftContent = draftFile && !contextContent ? await cachedLoadFile(draftFile) : null;
const deps = parseContextDependsOn(contextContent ?? draftContent);
const depsUnmet = deps.some(dep => !completeMilestoneIds.has(dep));
if (depsUnmet) {
registry.push({ id: mid, title, status: 'pending', dependsOn: deps });

View file

@ -42,6 +42,12 @@ function writeContext(base: string, mid: string, frontmatter: string): void {
writeFileSync(join(dir, `${mid}-CONTEXT.md`), `---\n${frontmatter}\n---\n`);
}
function writeContextDraft(base: string, mid: string, frontmatter: string): void {
const dir = join(base, '.gsd', 'milestones', mid);
mkdirSync(dir, { recursive: true });
writeFileSync(join(dir, `${mid}-CONTEXT-DRAFT.md`), `---\n${frontmatter}\n---\n`);
}
function writeSlicePlan(base: string, mid: string, sid: string, content: string): void {
const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
mkdirSync(join(dir, 'tasks'), { recursive: true });
@ -391,7 +397,79 @@ async function main(): Promise<void> {
}
}
// ─── Test Group 9: parseContextDependsOn preserves case ───────────────
// ─── Test Group 9: draft-context-deps ────────────────────────────────
// M001 is incomplete, M002 has only CONTEXT-DRAFT.md (no CONTEXT.md) with
// depends_on: [M001] → M002 should remain pending, not be promoted to active.
console.log('\n=== draft-context-deps: depends_on read from CONTEXT-DRAFT.md ===');
{
const base = createFixtureBase();
try {
// M001: incomplete (one slice, no SUMMARY)
writeRoadmap(base, 'M001', `# M001: First Milestone
**Vision:** First milestone still in progress.
## Slices
- [ ] **S01: Incomplete Slice** \`risk:low\` \`depends:[]\`
> After this: Done.
`);
writeSlicePlan(base, 'M001', 'S01', `# S01: Incomplete Slice
**Goal:** Test draft dep blocking.
**Demo:** Tests pass.
## Tasks
- [ ] **T01: Do work** \`est:15m\`
First task still in progress.
`);
// M002: only CONTEXT-DRAFT.md (no CONTEXT.md), depends on M001
writeRoadmap(base, 'M002', `# M002: Second Milestone
**Vision:** Second milestone blocked by M001 via draft context.
## Slices
- [ ] **S01: Blocked Slice** \`risk:low\` \`depends:[]\`
> After this: Done.
`);
writeContextDraft(base, 'M002', 'depends_on: [M001]');
const state = await deriveState(base);
assertEq(state.registry[0]?.status, 'active', 'draft-context-deps: M001 is active');
assertEq(state.registry[1]?.status, 'pending', 'draft-context-deps: M002 is pending (dep-blocked via draft)');
assertEq(state.activeMilestone?.id, 'M001', 'draft-context-deps: activeMilestone is M001');
} finally {
cleanup(base);
}
}
// ─── Test Group 10: draft-context-deps-no-roadmap ──────────────────────
// Same as above but without roadmaps — milestones discovered from directory only.
console.log('\n=== draft-context-deps-no-roadmap: depends_on from draft without roadmap ===');
{
const base = createFixtureBase();
try {
// M001: exists as directory only (no roadmap, no summary)
const m001Dir = join(base, '.gsd', 'milestones', 'M001');
mkdirSync(m001Dir, { recursive: true });
// M002: only CONTEXT-DRAFT.md, depends on M001
writeContextDraft(base, 'M002', 'depends_on: [M001]');
const state = await deriveState(base);
const m002Entry = state.registry.find(e => e.id === 'M002');
assertEq(m002Entry?.status, 'pending', 'draft-no-roadmap: M002 is pending (dep-blocked via draft)');
} finally {
cleanup(base);
}
}
// ─── Test Group 11: parseContextDependsOn preserves case ──────────────
// Direct unit test: verify the parsed dep ID matches the input exactly
console.log('\n=== parseContextDependsOn: preserves case of unique IDs ===');
{