diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 4eff4a2e9..4f3e1142f 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -717,6 +717,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { const planCount = phaseInfo.plans.length; const summaryCount = phaseInfo.summaries.length; + let requirementsUpdated = false; // Update ROADMAP.md: mark phase complete if (fs.existsSync(roadmapPath)) { @@ -755,11 +756,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { // Update REQUIREMENTS.md traceability for this phase's requirements const reqPath = path.join(cwd, '.planning', 'REQUIREMENTS.md'); if (fs.existsSync(reqPath)) { - // Extract Requirements line from roadmap for this phase - const reqMatch = roadmapContent.match( - new RegExp(`Phase\\s+${escapeRegex(phaseNum)}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i') + // Extract the current phase section from roadmap (scoped to avoid cross-phase matching) + const phaseEsc = escapeRegex(phaseNum); + const phaseSectionMatch = roadmapContent.match( + new RegExp(`(#{2,4}\\s*Phase\\s+${phaseEsc}[:\\s][\\s\\S]*?)(?=#{2,4}\\s*Phase\\s+|$)`, 'i') ); + const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : ''; + const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i); + if (reqMatch) { const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean); let reqContent = fs.readFileSync(reqPath, 'utf-8'); @@ -771,14 +776,15 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqEscaped}\\*\\*)`, 'gi'), '$1x$2' ); - // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete | + // Update traceability table: | REQ-ID | Phase N | Pending/In Progress | → | REQ-ID | Phase N | Complete | reqContent = reqContent.replace( - new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'), + new RegExp(`(\\|\\s*${reqEscaped}\\s*\\|[^|]+\\|)\\s*(?:Pending|In Progress)\\s*(\\|)`, 'gi'), '$1 Complete $2' ); } fs.writeFileSync(reqPath, reqContent, 'utf-8'); + requirementsUpdated = true; } } } @@ -884,6 +890,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { date: today, roadmap_updated: fs.existsSync(roadmapPath), state_updated: fs.existsSync(statePath), + requirements_updated: requirementsUpdated, }; output(result, raw); diff --git a/tests/phase.test.cjs b/tests/phase.test.cjs index 7532c3c9b..76a21a68e 100644 --- a/tests/phase.test.cjs +++ b/tests/phase.test.cjs @@ -1255,6 +1255,151 @@ describe('phase complete command', () => { assert.ok(result.success, `Command should succeed even without REQUIREMENTS.md: ${result.error}`); }); + test('returns requirements_updated field in result', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Auth + +### Phase 1: Auth +**Goal:** User authentication +**Requirements:** AUTH-01 +**Plans:** 1 plans +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements + +## v1 Requirements + +- [ ] **AUTH-01**: User can sign up + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| AUTH-01 | Phase 1 | Pending | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Auth\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + const parsed = JSON.parse(result.output); + assert.strictEqual(parsed.requirements_updated, true, 'requirements_updated should be true'); + }); + + test('handles In Progress status in traceability table', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Auth + +### Phase 1: Auth +**Goal:** User authentication +**Requirements:** AUTH-01, AUTH-02 +**Plans:** 1 plans +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements + +## v1 Requirements + +- [ ] **AUTH-01**: User can sign up +- [ ] **AUTH-02**: User can log in + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| AUTH-01 | Phase 1 | In Progress | +| AUTH-02 | Phase 1 | Pending | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Auth\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-auth'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); + assert.ok(req.includes('| AUTH-01 | Phase 1 | Complete |'), 'In Progress should become Complete'); + assert.ok(req.includes('| AUTH-02 | Phase 1 | Complete |'), 'Pending should become Complete'); + }); + + test('scoped regex does not cross phase boundaries', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Setup +- [ ] Phase 2: Auth + +### Phase 1: Setup +**Goal:** Project setup +**Plans:** 1 plans + +### Phase 2: Auth +**Goal:** User authentication +**Requirements:** AUTH-01 +**Plans:** 0 plans +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements + +## v1 Requirements + +- [ ] **AUTH-01**: User can sign up + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| AUTH-01 | Phase 2 | Pending | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\n**Current Phase Name:** Setup\n**Status:** In progress\n**Current Plan:** 01-01\n**Last Activity:** 2025-01-01\n**Last Activity Description:** Working\n` + ); + + const p1 = path.join(tmpDir, '.planning', 'phases', '01-setup'); + fs.mkdirSync(p1, { recursive: true }); + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Summary'); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true }); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // Phase 1 has no Requirements field, so Phase 2's AUTH-01 should NOT be updated + const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); + assert.ok(req.includes('- [ ] **AUTH-01**'), 'AUTH-01 should remain unchecked (belongs to Phase 2)'); + assert.ok(req.includes('| AUTH-01 | Phase 2 | Pending |'), 'AUTH-01 should remain Pending (belongs to Phase 2)'); + }); + test('handles multi-level decimal phase without regex crash', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'),