From ebeae010c1f9e64c24b3ac475c868d2cc8f5d30f Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 1 May 2026 16:43:08 +0200 Subject: [PATCH] feat(ultraplan-local): extend project-discovery with review.md --- .../lib/parsers/project-discovery.mjs | 21 +++++++++- .../tests/lib/project-discovery.test.mjs | 42 +++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/plugins/ultraplan-local/lib/parsers/project-discovery.mjs b/plugins/ultraplan-local/lib/parsers/project-discovery.mjs index 3eb23af..b247862 100644 --- a/plugins/ultraplan-local/lib/parsers/project-discovery.mjs +++ b/plugins/ultraplan-local/lib/parsers/project-discovery.mjs @@ -20,6 +20,7 @@ import { join } from 'node:path'; * architecture: { overview: string|null, gaps: string|null, looseFiles: string[] }, * plan: string|null, * progress: string|null, + * review: string|null, * }} ProjectArtifacts */ @@ -32,6 +33,7 @@ export function discoverProject(projectDir) { architecture: { overview: null, gaps: null, looseFiles: [] }, plan: null, progress: null, + review: null, }; if (!projectDir || !existsSync(projectDir) || !statSync(projectDir).isDirectory()) { @@ -47,6 +49,9 @@ export function discoverProject(projectDir) { const progressPath = join(projectDir, 'progress.json'); if (existsSync(progressPath) && statSync(progressPath).isFile()) out.progress = progressPath; + const reviewPath = join(projectDir, 'review.md'); + if (existsSync(reviewPath) && statSync(reviewPath).isFile()) out.review = reviewPath; + const researchDir = join(projectDir, 'research'); if (existsSync(researchDir) && statSync(researchDir).isDirectory()) { out.research = readdirSync(researchDir) @@ -72,10 +77,11 @@ export function discoverProject(projectDir) { /** * Validate that artifact set is consistent for a given pipeline phase. - * Phase = 'brief' | 'research' | 'plan' | 'execute'. + * Phase = 'brief' | 'research' | 'plan' | 'execute' | 'review'. */ export function checkPhaseRequirements(artifacts, phase) { const errors = []; + const warnings = []; if (phase === 'research' && !artifacts.brief) { errors.push({ code: 'PROJECT_NO_BRIEF', message: 'research phase requires brief.md' }); } @@ -85,5 +91,16 @@ export function checkPhaseRequirements(artifacts, phase) { if (phase === 'execute' && !artifacts.plan) { errors.push({ code: 'PROJECT_NO_PLAN', message: 'execute phase requires plan.md' }); } - return { valid: errors.length === 0, errors, warnings: [], parsed: artifacts }; + if (phase === 'review') { + if (!artifacts.brief) { + errors.push({ code: 'PROJECT_NO_BRIEF', message: 'review phase requires brief.md' }); + } + if (!artifacts.progress) { + warnings.push({ + code: 'PROJECT_NO_PROGRESS', + message: 'review phase: progress.json absent — scope detection will fall back to brief.md mtime', + }); + } + } + return { valid: errors.length === 0, errors, warnings, parsed: artifacts }; } diff --git a/plugins/ultraplan-local/tests/lib/project-discovery.test.mjs b/plugins/ultraplan-local/tests/lib/project-discovery.test.mjs index 7d2805b..3fb3f2a 100644 --- a/plugins/ultraplan-local/tests/lib/project-discovery.test.mjs +++ b/plugins/ultraplan-local/tests/lib/project-discovery.test.mjs @@ -104,3 +104,45 @@ test('checkPhaseRequirements — happy path', () => { const r = checkPhaseRequirements({ brief: 'x', plan: 'y' }, 'plan'); assert.equal(r.valid, true); }); + +test('discoverProject — finds review.md when present', () => { + const root = setupProject({ + 'brief.md': 'b', + 'review.md': 'r', + }); + try { + const a = discoverProject(root); + assert.equal(a.review, join(root, 'review.md')); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('discoverProject — review null when absent', () => { + const root = setupProject({ + 'brief.md': 'b', + }); + try { + const a = discoverProject(root); + assert.equal(a.review, null); + } finally { + rmSync(root, { recursive: true, force: true }); + } +}); + +test('checkPhaseRequirements — review phase needs brief (error) and tolerates missing progress (warning)', () => { + // Missing brief → error + const r1 = checkPhaseRequirements({ brief: null, progress: null }, 'review'); + assert.equal(r1.valid, false); + assert.ok(r1.errors.find(e => e.code === 'PROJECT_NO_BRIEF')); + + // Has brief, no progress → valid (with warning) + const r2 = checkPhaseRequirements({ brief: 'x', progress: null }, 'review'); + assert.equal(r2.valid, true, JSON.stringify(r2)); + assert.ok(r2.warnings.find(w => w.code === 'PROJECT_NO_PROGRESS')); + + // Has both → valid, no warning + const r3 = checkPhaseRequirements({ brief: 'x', progress: 'p' }, 'review'); + assert.equal(r3.valid, true); + assert.equal(r3.warnings.length, 0); +});