no-mistakes(review): Harden onboarding projection routing

This commit is contained in:
Jeremy McSpadden
2026-07-03 15:04:10 -05:00
committed by Codesmith
parent a5298c1fc0
commit 2a38ea5331
2 changed files with 63 additions and 18 deletions

View File

@@ -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,

View File

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