fix(sdk): expand short-form depends_on for decimal-phase plans (#3488)
The DAG resolver in phase-plan-index only matched full-stem
('03-01-auth-hardening') and canonical-prefix ('03-01') forms when
resolving `depends_on` references, so plans in decimal phases
(e.g. `99.9-test`, `02.2-cross-repo`) declaring short-form
`depends_on: [01]` had their edges silently dropped. Dependents
collapsed into wave 1 and the SDK emitted a misleading
"declared wave: N but depends_on DAG places it in wave 1" warning
that pointed at the wave declaration rather than the broken reference.
Add a tertiary short-form index keyed on the trailing `-NN` of each
plan's canonical ID — derived per plan via `lastIndexOf('-')` so it
handles integer, letter-suffixed, and decimal phase IDs uniformly.
Emit a dedicated `Plan X: unresolved depends_on reference 'NN' — no
matching plan in phase` warning whenever a dep fails all three lookup
forms, so a dropped edge can no longer hide behind the wave-mismatch
warning.
Regression coverage added in `sdk/src/query/phase.test.ts` for the
decimal-phase short-form case, the integer-phase short-form case, and
the unresolved-reference warning.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -558,4 +558,94 @@ describe('phasePlanIndex', () => {
|
||||
'Ignored noncanonical plan files: 16-PLAN-01-eval-harness.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('#3488: integer-phase short-form depends_on [01] resolves to same-phase plan', async () => {
|
||||
const phase17 = join(tmpDir, '.planning', 'phases', '17-int-short');
|
||||
await mkdir(phase17, { recursive: true });
|
||||
await writeFile(join(phase17, '17-01-PLAN.md'), [
|
||||
'---',
|
||||
'phase: 17',
|
||||
'plan: 01',
|
||||
'wave: 1',
|
||||
'autonomous: true',
|
||||
'depends_on: []',
|
||||
'---',
|
||||
'<objective>Plan A.</objective>',
|
||||
].join('\n'));
|
||||
await writeFile(join(phase17, '17-02-PLAN.md'), [
|
||||
'---',
|
||||
'phase: 17',
|
||||
'plan: 02',
|
||||
'wave: 2',
|
||||
'autonomous: true',
|
||||
'depends_on: [01]',
|
||||
'---',
|
||||
'<objective>Plan B — short-form dep.</objective>',
|
||||
].join('\n'));
|
||||
|
||||
const result = await phasePlanIndex(['17'], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const waves = data.waves as Record<string, string[]>;
|
||||
const warnings = (data.warnings as string[] | undefined) ?? [];
|
||||
|
||||
expect(waves['1']).toEqual(['17-01']);
|
||||
expect(waves['2']).toEqual(['17-02']);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('#3488: decimal-phase short-form depends_on [01] resolves to same-phase plan', async () => {
|
||||
const phase18 = join(tmpDir, '.planning', 'phases', '99.9-test');
|
||||
await mkdir(phase18, { recursive: true });
|
||||
await writeFile(join(phase18, '99.9-01-PLAN.md'), [
|
||||
'---',
|
||||
'phase: 99.9-test',
|
||||
'plan: 01',
|
||||
'wave: 1',
|
||||
'autonomous: true',
|
||||
'depends_on: []',
|
||||
'---',
|
||||
'<objective>Plan A.</objective>',
|
||||
].join('\n'));
|
||||
await writeFile(join(phase18, '99.9-02-PLAN.md'), [
|
||||
'---',
|
||||
'phase: 99.9-test',
|
||||
'plan: 02',
|
||||
'wave: 2',
|
||||
'autonomous: true',
|
||||
'depends_on: [01]',
|
||||
'---',
|
||||
'<objective>Plan B — short-form dep on decimal phase.</objective>',
|
||||
].join('\n'));
|
||||
|
||||
const result = await phasePlanIndex(['99.9'], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const waves = data.waves as Record<string, string[]>;
|
||||
const warnings = (data.warnings as string[] | undefined) ?? [];
|
||||
|
||||
expect(waves['1']).toEqual(['99.9-01']);
|
||||
expect(waves['2']).toEqual(['99.9-02']);
|
||||
expect(warnings).toEqual([]);
|
||||
});
|
||||
|
||||
it('#3488: unresolved depends_on reference emits distinct warning (not wave-mismatch)', async () => {
|
||||
const phase19 = join(tmpDir, '.planning', 'phases', '19-unresolved');
|
||||
await mkdir(phase19, { recursive: true });
|
||||
await writeFile(join(phase19, '19-01-PLAN.md'), [
|
||||
'---',
|
||||
'phase: 19',
|
||||
'plan: 01',
|
||||
'wave: 1',
|
||||
'autonomous: true',
|
||||
'depends_on: [does-not-exist]',
|
||||
'---',
|
||||
'<objective>Plan with bogus dep.</objective>',
|
||||
].join('\n'));
|
||||
|
||||
const result = await phasePlanIndex(['19'], tmpDir);
|
||||
const data = result.data as Record<string, unknown>;
|
||||
const warnings = (data.warnings as string[] | undefined) ?? [];
|
||||
|
||||
// A clear, dedicated warning naming the unresolved reference must surface.
|
||||
expect(warnings.some(w => /unresolved/i.test(w) && w.includes('does-not-exist'))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user