fix: read depends_on from CONTEXT-DRAFT.md when CONTEXT.md absent (#1795)
This commit is contained in:
parent
afe5f58ea6
commit
d587c91305
2 changed files with 85 additions and 5 deletions
|
|
@ -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 });
|
||||
|
|
|
|||
|
|
@ -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 ===');
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue