feat(ultraplan-local): extend project-discovery with review.md

This commit is contained in:
Kjell Tore Guttormsen 2026-05-01 16:43:08 +02:00
commit ebeae010c1
2 changed files with 61 additions and 2 deletions

View file

@ -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 };
}

View file

@ -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);
});