From c1664e52aebb5b75cf772a6a09d60eddb301f3d4 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Mon, 18 May 2026 22:41:25 -0400 Subject: [PATCH] feat(sdk): missing phase dir surfaces NO_PHASE_DIR reason (#3184) Cycle 3 of ~15: when resolvePhaseDir returns null, predicate returns a typed NO_PHASE_DIR reason. Previously the empty reasons array was indistinguishable from other failure modes. Co-Authored-By: Claude Opus 4.7 (1M context) --- sdk/src/query/phase-uat-passed.test.ts | 18 ++++++++++++++++++ sdk/src/query/phase-uat-passed.ts | 8 +++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/sdk/src/query/phase-uat-passed.test.ts b/sdk/src/query/phase-uat-passed.test.ts index ba164f15f..71b4ba4f7 100644 --- a/sdk/src/query/phase-uat-passed.test.ts +++ b/sdk/src/query/phase-uat-passed.test.ts @@ -43,6 +43,24 @@ describe('isPhaseUatPassed', () => { expect(result.reasons.length).toBe(0); }); + it('returns passed=false with NO_PHASE_DIR reason when phase has no directory', async () => { + const localTmp = await mkdtemp(join(tmpdir(), 'gsd-uat-c3-')); + try { + const otherPhaseDir = join(localTmp, '.planning', 'phases', '06-other'); + await mkdir(otherPhaseDir, { recursive: true }); + await writeFile(join(otherPhaseDir, '06-HUMAN-UAT.md'), UAT_PASS_CONTENT); + + // Query phase 5 which has NO directory in this fixture + const result = await isPhaseUatPassed(localTmp, '5'); + expect(result.passed).toBe(false); + expect(result.items.length).toBe(0); + expect(result.reasons.length).toBe(1); + expect(result.reasons[0].code).toBe(REASON_CODE.NO_PHASE_DIR); + } finally { + await rm(localTmp, { recursive: true, force: true }); + } + }); + it('returns passed=false with NON_PASS_RESULT reason when single UAT item has result: issue', async () => { const nonPassContent = `--- status: complete diff --git a/sdk/src/query/phase-uat-passed.ts b/sdk/src/query/phase-uat-passed.ts index e27d8f9b5..1fd90c234 100644 --- a/sdk/src/query/phase-uat-passed.ts +++ b/sdk/src/query/phase-uat-passed.ts @@ -11,6 +11,7 @@ import { resolvePhaseDir } from './phase-list-queries.js'; export const REASON_CODE = Object.freeze({ NON_PASS_RESULT: 'non_pass_result', + NO_PHASE_DIR: 'no_phase_dir', } as const); export type ReasonCode = typeof REASON_CODE[keyof typeof REASON_CODE]; @@ -62,7 +63,12 @@ export async function isPhaseUatPassed( }> { const dir = await resolvePhaseDir(phase, projectDir, workstream); if (!dir) { - return { passed: false, reasons: [], reasonsHuman: [], items: [] }; + return { + passed: false, + reasons: [{ code: REASON_CODE.NO_PHASE_DIR }], + reasonsHuman: [], + items: [], + }; } const files = await readdir(dir);