fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index (#3276)
* fix(3266): preserve wave 0 and bucket plans by depends_on DAG in phase-plan-index Fixes two cooperating bugs in the phase-plan-index builder: 1. Wave 0 collapse: `parseInt(...) || 1` coerced parsed value `0` to `1` due to JS falsy default. Fixed with `Number.isNaN` guard. 2. depends_on ignored: wave-bucketing used only the `wave:` frontmatter field. Now replaced with Kahn's topological-level algorithm over `depends_on`: source nodes (no in-phase deps) → lowest level; each plan's level = max(deps' levels) + 1. Declared `wave:` that disagrees with computed level emits a non-fatal warning on the result. Cycle detection throws GSDError. `PlanInfo` gains `depends_on: string[]`. `PhasePlanIndex` gains `warnings?: string[]`. Both TS (`sdk/src/query/phase.ts`) and CJS twin (`get-shit-done/bin/lib/phase.cjs`) fixed identically. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add changeset for #3276 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(phase): resolve depends_on against canonical plan id (#3276 CR) Build a secondary `canonicalToId` index alongside `planMap` so that a dependency declared as '03-01' resolves to a descriptive plan stored under '03-01-auth-hardening', preventing silent wave-ordering failures. Applied at both DAG construction sites in phase.cjs and the SDK's phase.ts (k014 parity). Regression test added. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -129,7 +129,7 @@ describe('execute-phase docs: user-facing wave flag', () => {
|
||||
});
|
||||
|
||||
describe('phase-plan-index: wave grouping behavior', () => {
|
||||
test('phase-plan-index groups plans by wave frontmatter field', () => {
|
||||
test('phase-plan-index groups plans by wave (DAG-bucketing: P002 depends on P001)', () => {
|
||||
// allow-test-rule: behavioral — calls gsd-tools and asserts structured output
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -138,12 +138,13 @@ describe('phase-plan-index: wave grouping behavior', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '01-alpha');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
// Wave 1 plan
|
||||
// Wave 1 plan — no dependencies
|
||||
fs.writeFileSync(path.join(phaseDir, 'P001-PLAN.md'), [
|
||||
'---',
|
||||
'wave: 1',
|
||||
'objective: First wave task',
|
||||
'autonomous: true',
|
||||
'depends_on: []',
|
||||
'---',
|
||||
'',
|
||||
'# Plan 001',
|
||||
@@ -153,12 +154,14 @@ describe('phase-plan-index: wave grouping behavior', () => {
|
||||
'<task>Do the thing</task>',
|
||||
].join('\n'));
|
||||
|
||||
// Wave 2 plan
|
||||
// Wave 2 plan — depends on P001 so DAG places it in level 1 → wave 2
|
||||
fs.writeFileSync(path.join(phaseDir, 'P002-PLAN.md'), [
|
||||
'---',
|
||||
'wave: 2',
|
||||
'objective: Second wave task',
|
||||
'autonomous: true',
|
||||
'depends_on:',
|
||||
' - P001',
|
||||
'---',
|
||||
'',
|
||||
'# Plan 002',
|
||||
@@ -185,6 +188,8 @@ describe('phase-plan-index: wave grouping behavior', () => {
|
||||
assert.ok(p002, 'P002 should be in plans array');
|
||||
assert.equal(p001.wave, 1, 'P001 should have wave=1');
|
||||
assert.equal(p002.wave, 2, 'P002 should have wave=2');
|
||||
// No mismatch warning: declared wave 2 matches topo level 2
|
||||
assert.strictEqual(data.warnings, undefined, 'no warnings when declared wave matches DAG');
|
||||
} finally {
|
||||
cleanup(tmpDir);
|
||||
}
|
||||
|
||||
@@ -393,44 +393,52 @@ files-modified: [prisma/schema.prisma, src/lib/db.ts]
|
||||
assert.strictEqual(output.plans[0].has_summary, false, 'no summary yet');
|
||||
});
|
||||
|
||||
test('groups multiple plans by wave', () => {
|
||||
test('groups multiple plans by wave (DAG-bucketing: 03-03 depends on 03-01 and 03-02)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '03-01-PLAN.md'),
|
||||
`---
|
||||
wave: 1
|
||||
autonomous: true
|
||||
objective: Database setup
|
||||
---
|
||||
|
||||
## Task 1: Schema
|
||||
`
|
||||
[
|
||||
'---',
|
||||
'wave: 1',
|
||||
'autonomous: true',
|
||||
'objective: Database setup',
|
||||
'depends_on: []',
|
||||
'---',
|
||||
'',
|
||||
'## Task 1: Schema',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '03-02-PLAN.md'),
|
||||
`---
|
||||
wave: 1
|
||||
autonomous: true
|
||||
objective: Auth setup
|
||||
---
|
||||
|
||||
## Task 1: JWT
|
||||
`
|
||||
[
|
||||
'---',
|
||||
'wave: 1',
|
||||
'autonomous: true',
|
||||
'objective: Auth setup',
|
||||
'depends_on: []',
|
||||
'---',
|
||||
'',
|
||||
'## Task 1: JWT',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '03-03-PLAN.md'),
|
||||
`---
|
||||
wave: 2
|
||||
autonomous: false
|
||||
objective: API routes
|
||||
---
|
||||
|
||||
## Task 1: Routes
|
||||
`
|
||||
[
|
||||
'---',
|
||||
'wave: 2',
|
||||
'autonomous: false',
|
||||
'objective: API routes',
|
||||
'depends_on:',
|
||||
' - 03-01',
|
||||
' - 03-02',
|
||||
'---',
|
||||
'',
|
||||
'## Task 1: Routes',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
||||
@@ -440,6 +448,8 @@ objective: API routes
|
||||
assert.strictEqual(output.plans.length, 3, 'should have 3 plans');
|
||||
assert.deepStrictEqual(output.waves['1'], ['03-01', '03-02'], 'wave 1 has 2 plans');
|
||||
assert.deepStrictEqual(output.waves['2'], ['03-03'], 'wave 2 has 1 plan');
|
||||
// No mismatch warning: declared wave 2 matches topo level 2
|
||||
assert.strictEqual(output.warnings, undefined, 'no warnings when declared wave matches DAG');
|
||||
});
|
||||
|
||||
test('detects incomplete plans (no matching summary)', () => {
|
||||
@@ -477,6 +487,42 @@ objective: API routes
|
||||
assert.deepStrictEqual(output.incomplete, [], 'plan should not be marked incomplete');
|
||||
});
|
||||
|
||||
// #3266 CR — depends_on canonical-id mismatch: a plan named
|
||||
// '03-01-auth-hardening-PLAN.md' is stored with id '03-01-auth-hardening',
|
||||
// but a dependency declared as '03-01' was never resolving to it, silently
|
||||
// putting the dependent plan in the same wave as its prerequisite.
|
||||
test('depends_on short canonical prefix resolves against descriptive plan filename (#3266)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
// Plan 01: descriptive filename — id becomes '03-01-auth-hardening'
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '03-01-auth-hardening-PLAN.md'),
|
||||
`---\nwave: 1\n---\n## Task 1\n`,
|
||||
);
|
||||
// Plan 02: depends on the canonical prefix '03-01' (not the full stem)
|
||||
fs.writeFileSync(
|
||||
path.join(phaseDir, '03-02-followup-PLAN.md'),
|
||||
`---\ndepends_on:\n - '03-01'\n---\n## Task 1\n`,
|
||||
);
|
||||
|
||||
const result = runGsdTools('phase-plan-index 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
const waves = output.waves;
|
||||
|
||||
// Plan 01 must be in an earlier wave than plan 02
|
||||
const wave01 = Object.keys(waves).find(w => waves[w].some(id => id.startsWith('03-01')));
|
||||
const wave02 = Object.keys(waves).find(w => waves[w].some(id => id.startsWith('03-02')));
|
||||
assert.ok(wave01 !== undefined, 'plan 03-01-auth-hardening should appear in waves');
|
||||
assert.ok(wave02 !== undefined, 'plan 03-02-followup should appear in waves');
|
||||
assert.ok(
|
||||
Number(wave01) < Number(wave02),
|
||||
`03-02 must be in a later wave than 03-01 (got wave01=${wave01}, wave02=${wave02})`,
|
||||
);
|
||||
});
|
||||
|
||||
test('detects checkpoints (autonomous: false)', () => {
|
||||
const phaseDir = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(phaseDir, { recursive: true });
|
||||
|
||||
Reference in New Issue
Block a user