106 lines
3.6 KiB
JavaScript
106 lines
3.6 KiB
JavaScript
// lib/parsers/project-discovery.mjs
|
|
// Discover ultra-suite artifacts inside a project directory.
|
|
//
|
|
// Layout (post-v3.0.0 project-directory contract):
|
|
// .claude/projects/<YYYY-MM-DD>-<slug>/
|
|
// brief.md
|
|
// research/<NN>-<slug>.md (sorted by filename)
|
|
// architecture/overview.md (opt-in, owned by separate ultra-cc-architect plugin)
|
|
// plan.md
|
|
// progress.json
|
|
|
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
/**
|
|
* @typedef {{
|
|
* projectDir: string,
|
|
* brief: string|null,
|
|
* research: string[],
|
|
* architecture: { overview: string|null, gaps: string|null, looseFiles: string[] },
|
|
* plan: string|null,
|
|
* progress: string|null,
|
|
* review: string|null,
|
|
* }} ProjectArtifacts
|
|
*/
|
|
|
|
/** @returns {ProjectArtifacts} */
|
|
export function discoverProject(projectDir) {
|
|
const out = {
|
|
projectDir,
|
|
brief: null,
|
|
research: [],
|
|
architecture: { overview: null, gaps: null, looseFiles: [] },
|
|
plan: null,
|
|
progress: null,
|
|
review: null,
|
|
};
|
|
|
|
if (!projectDir || !existsSync(projectDir) || !statSync(projectDir).isDirectory()) {
|
|
return out;
|
|
}
|
|
|
|
const briefPath = join(projectDir, 'brief.md');
|
|
if (existsSync(briefPath) && statSync(briefPath).isFile()) out.brief = briefPath;
|
|
|
|
const planPath = join(projectDir, 'plan.md');
|
|
if (existsSync(planPath) && statSync(planPath).isFile()) out.plan = planPath;
|
|
|
|
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)
|
|
.filter(f => f.endsWith('.md'))
|
|
.sort()
|
|
.map(f => join(researchDir, f));
|
|
}
|
|
|
|
const archDir = join(projectDir, 'architecture');
|
|
if (existsSync(archDir) && statSync(archDir).isDirectory()) {
|
|
const overviewPath = join(archDir, 'overview.md');
|
|
const gapsPath = join(archDir, 'gaps.md');
|
|
if (existsSync(overviewPath)) out.architecture.overview = overviewPath;
|
|
if (existsSync(gapsPath)) out.architecture.gaps = gapsPath;
|
|
const all = readdirSync(archDir).filter(f => f.endsWith('.md'));
|
|
out.architecture.looseFiles = all
|
|
.filter(f => f !== 'overview.md' && f !== 'gaps.md')
|
|
.map(f => join(archDir, f));
|
|
}
|
|
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Validate that artifact set is consistent for a given pipeline phase.
|
|
* 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' });
|
|
}
|
|
if (phase === 'plan' && !artifacts.brief) {
|
|
errors.push({ code: 'PROJECT_NO_BRIEF', message: 'plan phase requires brief.md' });
|
|
}
|
|
if (phase === 'execute' && !artifacts.plan) {
|
|
errors.push({ code: 'PROJECT_NO_PLAN', message: 'execute phase requires plan.md' });
|
|
}
|
|
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 };
|
|
}
|