// lib/parsers/project-discovery.mjs // Discover ultra-suite artifacts inside a project directory. // // Layout (post-v3.0.0 project-directory contract): // .claude/projects/-/ // brief.md // research/-.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, * }} ProjectArtifacts */ /** @returns {ProjectArtifacts} */ export function discoverProject(projectDir) { const out = { projectDir, brief: null, research: [], architecture: { overview: null, gaps: null, looseFiles: [] }, plan: null, progress: 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 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'. */ export function checkPhaseRequirements(artifacts, phase) { const errors = []; 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' }); } return { valid: errors.length === 0, errors, warnings: [], parsed: artifacts }; }