fix(#4801): init.manager resolves archived phase directories through findPhaseInternal (#4893)

* test(#4801): failing-first — an archived phase directory must resolve and report complete in init.manager

* fix(#4801): init.manager resolves archived phase directories through findPhaseInternal

The private current-milestone-only scan (matchPhaseDirs over listMilestonePhaseDirs)
counted archived phase directories as missing, so an archived phase with a passed
verification reported no_directory/phase_complete:false. findPhaseInternal — already
imported, already the shared primitive for five other init commands — searches the live
directory first and falls back through the workstream-scoped archive (#2855); the private
copy and its single-consumer entries list are retired.

* chore(#4801): backfill changeset PR number (4893)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-20 06:45:38 -04:00
committed by GitHub
parent 88b5775dc8
commit a8394713d1
5 changed files with 120 additions and 25 deletions

View File

@@ -5396,3 +5396,93 @@ describe('#4786: suffix-less hand-written plan lists are recognized, not duplica
);
});
});
// ─── #4801: archived phase directories resolve in init.manager ──────────────
describe('#4801: init.manager resolves archived phase directories', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject('gsd-4801-');
});
afterEach(() => {
cleanup(tmpDir);
});
function writeState4801() {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '---\nstatus: active\n---\n# State\n');
}
function writeRoadmap4801() {
fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), [
'# Roadmap',
'',
'### Phase 3: shipped',
'',
'Goal: shipped before archive',
'',
'### Phase 5: live',
'',
'Goal: in progress',
'',
'### Phase 7: never started',
'',
'Goal: not started',
'',
].join('\n'));
}
function seedArchivedPhase() {
// An archived, shipped phase: PLAN + SUMMARY + a NEWER passing VERIFICATION
// (mtime discipline per the #3057 fixture — the verification must postdate
// the summary for the completion projection to read it as fresh).
const arch = path.join(tmpDir, '.planning', 'milestones', 'v0.1-phases', '03-shipped');
fs.mkdirSync(arch, { recursive: true });
fs.writeFileSync(path.join(arch, '03-01-PLAN.md'), '# Plan\n');
fs.writeFileSync(path.join(arch, '03-01-SUMMARY.md'), '# Summary\n');
fs.writeFileSync(path.join(arch, '03-VERIFICATION.md'), '---\nstatus: passed\n---\n\n# Verification\n');
const older = new Date('2026-01-01T00:00:00.000Z');
const newer = new Date('2026-01-01T00:01:00.000Z');
fs.utimesSync(path.join(arch, '03-01-SUMMARY.md'), older, older);
fs.utimesSync(path.join(arch, '03-VERIFICATION.md'), newer, newer);
}
test('#4801: an archived phase directory resolves and reports complete', () => {
writeState4801();
writeRoadmap4801();
seedArchivedPhase();
// A live in-progress phase and a never-started phase for contrast.
const live = path.join(tmpDir, '.planning', 'phases', '05-live');
fs.mkdirSync(live, { recursive: true });
fs.writeFileSync(path.join(live, '05-01-PLAN.md'), '# Plan\n');
const output = JSON.parse(runGsdTools(['query', 'init.manager'], tmpDir).output);
const rows = new Map(output.phases.map((p) => [String(p.number), p]));
const archived = rows.get('3');
assert.ok(archived, 'the archived phase must appear in the enumeration');
assert.notStrictEqual(archived.disk_status, 'no_directory',
'an archived phase directory must resolve — no_directory means never started');
assert.strictEqual(archived.phase_complete, true,
'an archived phase with a passed verification reports complete');
const neverStarted = rows.get('7');
assert.strictEqual(neverStarted.disk_status, 'no_directory',
'a never-started phase still reports no_directory');
});
test('#4801: a live in-progress phase is unchanged by the archived-resolution swap', () => {
writeState4801();
writeRoadmap4801();
seedArchivedPhase();
const live = path.join(tmpDir, '.planning', 'phases', '05-live');
fs.mkdirSync(live, { recursive: true });
fs.writeFileSync(path.join(live, '05-01-PLAN.md'), '# Plan\n');
const output = JSON.parse(runGsdTools(['query', 'init.manager'], tmpDir).output);
const row = output.phases.find((p) => String(p.number) === '5');
assert.strictEqual(row.phase_complete, false);
assert.ok(['planned', 'empty', 'no_directory'].includes(row.disk_status),
`live plan-less phase stays incomplete; got ${row.disk_status}`);
});
});