no-mistakes(review): Fix onboard planning detection
This commit is contained in:
committed by
Codesmith
parent
e0196d5369
commit
6b0b5f1b2c
@@ -34,8 +34,9 @@ INIT=$(gsd_run init onboard)
|
||||
if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
|
||||
```
|
||||
|
||||
Parse JSON fields: `planning_exists`, `project_exists`, `roadmap_exists`, `state_exists`,
|
||||
`has_existing_code`, `has_package_file`, `is_brownfield`, `has_codebase_map`,
|
||||
Parse JSON fields: `planning_exists`, `project_exists`, `requirements_exists`,
|
||||
`roadmap_exists`, `state_exists`, `has_existing_code`, `has_package_file`,
|
||||
`is_brownfield`, `has_codebase_map`,
|
||||
`codebase_map_files_present`, `missing_codebase_map_files`, `has_docs_candidates`,
|
||||
`doc_candidate_count`, `onboarding_summary_exists`, `text_mode`, `agents_installed`,
|
||||
`missing_agents`, `has_git`, `git_worktree_root`, `in_nested_subdir`.
|
||||
@@ -148,12 +149,14 @@ Run this top-level command, then rerun /gsd:onboard:
|
||||
|
||||
Exit.
|
||||
|
||||
If `project_exists` is true and either `roadmap_exists` or `state_exists` is false, print:
|
||||
If `project_exists` is true and either `requirements_exists`, `roadmap_exists`, or
|
||||
`state_exists` is false, print:
|
||||
|
||||
```text
|
||||
Existing PROJECT.md was found, but planning is incomplete.
|
||||
|
||||
Planning file status:
|
||||
- REQUIREMENTS.md: {requirements_exists ? "present" : "missing"}
|
||||
- ROADMAP.md: {roadmap_exists ? "present" : "missing"}
|
||||
- STATE.md: {state_exists ? "present" : "missing"}
|
||||
|
||||
@@ -166,7 +169,8 @@ Run one of these top-level commands, then rerun /gsd:onboard:
|
||||
Exit. Do not write `.planning/onboarding/SUMMARY.md` and do not print the onboarding
|
||||
complete status for partial planning.
|
||||
|
||||
If `project_exists`, `roadmap_exists`, and `state_exists` are all true, continue.
|
||||
If `project_exists`, `requirements_exists`, `roadmap_exists`, and `state_exists` are all
|
||||
true, continue.
|
||||
|
||||
## 6. Write Onboarding Summary
|
||||
|
||||
@@ -184,6 +188,7 @@ Summary contents:
|
||||
## Artifact Status
|
||||
|
||||
- Project: .planning/PROJECT.md
|
||||
- Requirements: .planning/REQUIREMENTS.md
|
||||
- Roadmap: .planning/ROADMAP.md
|
||||
- State: .planning/STATE.md
|
||||
- Codebase map: {has_codebase_map ? ".planning/codebase/ (complete)" : ".planning/codebase/ (incomplete or skipped)"}
|
||||
@@ -232,7 +237,7 @@ Print:
|
||||
Created / confirmed / status:
|
||||
- .planning/codebase/ {has_codebase_map ? "(complete)" : "(incomplete or skipped; missing: {missing_codebase_map_files})"}
|
||||
- .planning/PROJECT.md
|
||||
- .planning/REQUIREMENTS.md
|
||||
- .planning/REQUIREMENTS.md {requirements_exists ? "(present)" : "(missing; onboarding incomplete)"}
|
||||
- .planning/ROADMAP.md
|
||||
- .planning/STATE.md
|
||||
- .planning/onboarding/SUMMARY.md
|
||||
|
||||
13
src/init.cts
13
src/init.cts
@@ -109,6 +109,10 @@ const REQUIRED_CODEBASE_MAP_FILES = [
|
||||
'INTEGRATIONS.md', 'CONCERNS.md',
|
||||
];
|
||||
|
||||
const PLANNING_DOC_SEGMENTS = new Set([
|
||||
'adr', 'adrs', 'prd', 'prds', 'spec', 'specs', 'rfc', 'rfcs',
|
||||
]);
|
||||
|
||||
function hasCodeFilesInternal(dir: string, depth = 0): boolean {
|
||||
if (depth > 3) return false;
|
||||
let entries: fs.Dirent[];
|
||||
@@ -157,12 +161,11 @@ function listPlanningDocCandidates(cwd: string): string[] {
|
||||
if (!entry.isFile() || !entry.name.toLowerCase().endsWith('.md')) continue;
|
||||
const upperName = entry.name.toUpperCase();
|
||||
const relLower = rel.toLowerCase();
|
||||
const pathSegments = relLower.split('/');
|
||||
if (
|
||||
/(^|[-_ ])(ADR|PRD|SPEC|RFC)([-_ ]|\.)/i.test(entry.name) ||
|
||||
/^\d{4}[-_].+\.md$/i.test(entry.name) ||
|
||||
relLower.includes('/adr') ||
|
||||
relLower.includes('/prd') ||
|
||||
relLower.includes('/spec') ||
|
||||
pathSegments.some((segment) => PLANNING_DOC_SEGMENTS.has(segment)) ||
|
||||
upperName === 'REQUIREMENTS.md'
|
||||
) {
|
||||
candidates.add(toPosixPath(rel));
|
||||
@@ -899,6 +902,7 @@ function cmdInitOnboard(cwd: string, raw: boolean): void {
|
||||
|
||||
project_exists: pathExistsInternal(cwd, '.planning/PROJECT.md'),
|
||||
planning_exists: fs.existsSync(planningRoot(cwd)),
|
||||
requirements_exists: fs.existsSync(path.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
||||
roadmap_exists: fs.existsSync(path.join(planningDir(cwd), 'ROADMAP.md')),
|
||||
state_exists: fs.existsSync(path.join(planningDir(cwd), 'STATE.md')),
|
||||
config_exists: fs.existsSync(path.join(planningDir(cwd), 'config.json')),
|
||||
@@ -920,6 +924,9 @@ function cmdInitOnboard(cwd: string, raw: boolean): void {
|
||||
onboarding_summary_path: '.planning/onboarding/SUMMARY.md',
|
||||
|
||||
project_path: '.planning/PROJECT.md',
|
||||
requirements_path: toPosixPath(
|
||||
path.relative(cwd, path.join(planningDir(cwd), 'REQUIREMENTS.md')),
|
||||
),
|
||||
roadmap_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'ROADMAP.md'))),
|
||||
state_path: toPosixPath(path.relative(cwd, path.join(planningDir(cwd), 'STATE.md'))),
|
||||
codebase_dir: toPosixPath(path.relative(cwd, path.join(planningRoot(cwd), 'codebase'))),
|
||||
|
||||
@@ -50,6 +50,21 @@ describe('init onboard public CLI projection', () => {
|
||||
assert.strictEqual(parsed.text_mode, false);
|
||||
});
|
||||
|
||||
test('detects planning docs in top-level ADR and PRD folders', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, 'prd'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'prd', 'product.md'), '# Product Requirements\n');
|
||||
fs.mkdirSync(path.join(tmpDir, 'adr'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'adr', 'decision.md'), '# Architecture Decision\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.has_docs_candidates, true);
|
||||
assert.strictEqual(parsed.doc_candidate_count, 2);
|
||||
assert.deepStrictEqual(parsed.doc_candidates, ['adr/decision.md', 'prd/product.md']);
|
||||
});
|
||||
|
||||
test('reports complete codebase map and onboarding summary in existing planning', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'codebase'), { recursive: true });
|
||||
for (const name of ['STACK', 'ARCHITECTURE', 'STRUCTURE', 'CONVENTIONS', 'TESTING', 'INTEGRATIONS', 'CONCERNS']) {
|
||||
@@ -58,6 +73,7 @@ describe('init onboard public CLI projection', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning', 'onboarding'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'onboarding', 'SUMMARY.md'), '# Onboarding Summary\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\n');
|
||||
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');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ workflow: { text_mode: true } }));
|
||||
@@ -80,6 +96,7 @@ describe('init onboard public CLI projection', () => {
|
||||
const parsed = JSON.parse(result.output);
|
||||
assert.strictEqual(parsed.planning_exists, true);
|
||||
assert.strictEqual(parsed.project_exists, true);
|
||||
assert.strictEqual(parsed.requirements_exists, true);
|
||||
assert.strictEqual(parsed.roadmap_exists, true);
|
||||
assert.strictEqual(parsed.state_exists, true);
|
||||
assert.strictEqual(parsed.has_codebase_map, true);
|
||||
@@ -89,6 +106,22 @@ describe('init onboard public CLI projection', () => {
|
||||
assert.strictEqual(parsed.text_mode, true);
|
||||
});
|
||||
|
||||
test('reports missing requirements in otherwise existing planning', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, '.planning'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'PROJECT.md'), '# Project\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.project_exists, true);
|
||||
assert.strictEqual(parsed.requirements_exists, false);
|
||||
assert.strictEqual(parsed.roadmap_exists, true);
|
||||
assert.strictEqual(parsed.state_exists, true);
|
||||
});
|
||||
|
||||
test('ignores generated and vendor directories when detecting existing code', () => {
|
||||
fs.mkdirSync(path.join(tmpDir, 'node_modules', 'pkg'), { recursive: true });
|
||||
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'pkg', 'index.ts'), 'export const ignored = true;\n');
|
||||
@@ -151,6 +184,9 @@ describe('/gsd:onboard command contract', () => {
|
||||
assert.ok(content.includes('new-project'), 'workflow must route to new-project');
|
||||
assert.ok(content.includes('.planning/onboarding/SUMMARY.md'), 'workflow must create onboarding summary');
|
||||
assert.match(content, /overwrite|idempotent|do not overwrite/i, 'workflow must protect existing planning');
|
||||
assert.ok(content.includes('requirements_exists'), 'workflow must parse requirements existence');
|
||||
assert.match(content, /REQUIREMENTS\.md: \{requirements_exists \? "present" : "missing"\}/, 'workflow must report missing requirements in partial planning');
|
||||
assert.match(content, /If `project_exists` is true and .*`requirements_exists`.* is false/s, 'workflow must gate partial planning on missing requirements');
|
||||
assert.ok(content.includes('--text'), 'workflow must document text-mode fallback');
|
||||
assert.ok(!content.includes('execute-phase'), 'onboarding must not execute implementation phases');
|
||||
assert.ok(!content.includes('gsd:ship'), 'onboarding must not ship work');
|
||||
|
||||
Reference in New Issue
Block a user