diff --git a/src/onboard-projection.cts b/src/onboard-projection.cts index 26ba11305..8e4ec8f7f 100644 --- a/src/onboard-projection.cts +++ b/src/onboard-projection.cts @@ -212,8 +212,14 @@ function mapFinalStatus(mapReadiness: MapReadiness): string { return 'missing'; } -function planningMissing(requirementsExists: boolean, roadmapExists: boolean, stateExists: boolean): string[] { +function planningMissing( + projectExists: boolean, + requirementsExists: boolean, + roadmapExists: boolean, + stateExists: boolean, +): string[] { const missing: string[] = []; + if (!projectExists) missing.push('PROJECT.md'); if (!requirementsExists) missing.push('REQUIREMENTS.md'); if (!roadmapExists) missing.push('ROADMAP.md'); if (!stateExists) missing.push('STATE.md'); @@ -228,6 +234,7 @@ function nextAction(params: { projectExists: boolean; mapReadiness: MapReadiness; onboardingSummaryExists: boolean; + hasPlanningArtifacts: boolean; missingPlanningFiles: string[]; }): OnboardNextAction { if (params.isBrownfield && params.needsCodebaseMap) { @@ -238,6 +245,22 @@ function nextAction(params: { }; } + if (!params.projectExists && params.fastMode && params.mapReadiness === 'fast') { + return { + kind: 'complete-map-before-new-project', + command: '/gsd:map-codebase', + reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.', + }; + } + + if (params.hasPlanningArtifacts && params.missingPlanningFiles.length > 0) { + return { + kind: 'partial-planning', + missing: params.missingPlanningFiles, + reason: 'Project planning exists but required planning files are missing.', + }; + } + if (params.hasDocsCandidates && !params.projectExists) { return { kind: 'ingest-docs', @@ -254,14 +277,6 @@ function nextAction(params: { }; } - if (!params.projectExists && params.fastMode && params.mapReadiness === 'fast') { - return { - kind: 'complete-map-before-new-project', - command: '/gsd:map-codebase', - reason: 'The fast map is enough for lightweight onboarding, but project setup still requires the complete codebase map.', - }; - } - if (!params.projectExists) { return { kind: 'new-project', @@ -270,14 +285,6 @@ function nextAction(params: { }; } - if (params.missingPlanningFiles.length > 0) { - return { - kind: 'partial-planning', - missing: params.missingPlanningFiles, - reason: 'Project planning exists but required planning files are missing.', - }; - } - if (!params.onboardingSummaryExists) { return { kind: 'write-summary', @@ -315,7 +322,13 @@ function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOpti const roadmapExists = fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')); const stateExists = fs.existsSync(path.join(planningDir(cwd), 'STATE.md')); const onboardingSummaryExists = pathExistsInternal(cwd, '.planning/onboarding/SUMMARY.md'); - const missingPlanningFiles = planningMissing(requirementsExists, roadmapExists, stateExists); + const hasPlanningArtifacts = projectExists || requirementsExists || roadmapExists || stateExists; + const missingPlanningFiles = planningMissing( + projectExists, + requirementsExists, + roadmapExists, + stateExists, + ); return { commit_docs: options.commitDocs, @@ -341,6 +354,7 @@ function buildOnboardProjection(cwd: string, options: BuildOnboardProjectionOpti projectExists, mapReadiness: mapReadinessValue, onboardingSummaryExists, + hasPlanningArtifacts, missingPlanningFiles, }), needs_codebase_map: needsCodebaseMap, diff --git a/tests/onboard-command.test.cjs b/tests/onboard-command.test.cjs index 30bb98716..563a87967 100644 --- a/tests/onboard-command.test.cjs +++ b/tests/onboard-command.test.cjs @@ -165,6 +165,37 @@ describe('init onboard public CLI projection', () => { assert.deepStrictEqual(parsed.missing_fast_codebase_map_files, []); }); + test('routes fast mapped repositories with planning docs to complete map before ingest', () => { + fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true }); + for (const name of ['STACK', 'INTEGRATIONS', 'ARCHITECTURE', 'STRUCTURE']) { + fs.writeFileSync(path.join(tmpDir, '.planning', 'codebase', `${name}.md`), `# ${name}\n`); + } + fs.writeFileSync(path.join(tmpDir, 'package.json'), '{"name":"fixture"}\n'); + fs.mkdirSync(path.join(tmpDir, 'docs', 'adr'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, 'docs', 'adr', '0001-runtime.md'), '# ADR: Runtime\n'); + + const result = runGsdTools(['init', 'onboard', '--fast', '--raw'], tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'complete-map-before-new-project'); + assert.strictEqual(parsed.next_action.command, '/gsd:map-codebase'); + }); + + test('routes planning artifacts without PROJECT.md to partial planning', () => { + fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true }); + fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), '# Requirements\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), '# Roadmap\n'); + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), '# State\n'); + + const result = runGsdTools('init onboard --raw', tmpDir, { HOME: tmpDir }); + assert.ok(result.success, `init onboard should succeed: ${result.error}`); + + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.next_action.kind, 'partial-planning'); + assert.deepStrictEqual(parsed.next_action.missing, ['PROJECT.md']); + }); + test('projects the next action for code, docs, greenfield, partial planning, and summary states', () => { fs.mkdirSync(path.join(tmpDir, 'src'), { recursive: true }); fs.writeFileSync(path.join(tmpDir, 'src', 'server.ts'), 'export const server = true;\n');