From a142002dfe8392a8478379095b34b641276ac9d3 Mon Sep 17 00:00:00 2001 From: Lex Christopherson Date: Sun, 15 Feb 2026 09:51:37 -0600 Subject: [PATCH] fix(tools): update REQUIREMENTS.md traceability when phase completes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmdPhaseComplete now updates REQUIREMENTS.md when a phase finishes: - Checks off requirement checkboxes (- [ ] → - [x]) - Updates traceability table status (Pending → Complete) Parses Requirements line from ROADMAP.md phase section to find which REQ-IDs belong to the completing phase. Fixes #539 Co-Authored-By: Claude Opus 4.5 --- get-shit-done/bin/gsd-tools.js | 29 ++++++ get-shit-done/bin/gsd-tools.test.js | 145 ++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) diff --git a/get-shit-done/bin/gsd-tools.js b/get-shit-done/bin/gsd-tools.js index a7be1efbc..9db630719 100755 --- a/get-shit-done/bin/gsd-tools.js +++ b/get-shit-done/bin/gsd-tools.js @@ -3011,6 +3011,35 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { ); fs.writeFileSync(roadmapPath, roadmapContent, 'utf-8'); + + // 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+${phaseNum.replace('.', '\\.')}[\\s\\S]*?\\*\\*Requirements:\\*\\*\\s*([^\\n]+)`, 'i') + ); + + if (reqMatch) { + const reqIds = reqMatch[1].split(/[,\s]+/).map(r => r.trim()).filter(Boolean); + let reqContent = fs.readFileSync(reqPath, 'utf-8'); + + for (const reqId of reqIds) { + // Update checkbox: - [ ] **REQ-ID** → - [x] **REQ-ID** + reqContent = reqContent.replace( + new RegExp(`(-\\s*\\[)[ ](\\]\\s*\\*\\*${reqId}\\*\\*)`, 'gi'), + '$1x$2' + ); + // Update traceability table: | REQ-ID | Phase N | Pending | → | REQ-ID | Phase N | Complete | + reqContent = reqContent.replace( + new RegExp(`(\\|\\s*${reqId}\\s*\\|[^|]+\\|)\\s*Pending\\s*(\\|)`, 'gi'), + '$1 Complete $2' + ); + } + + fs.writeFileSync(reqPath, reqContent, 'utf-8'); + } + } } // Find next phase diff --git a/get-shit-done/bin/gsd-tools.test.js b/get-shit-done/bin/gsd-tools.test.js index ac358ad84..ebac13d1f 100644 --- a/get-shit-done/bin/gsd-tools.test.js +++ b/get-shit-done/bin/gsd-tools.test.js @@ -1700,6 +1700,151 @@ describe('phase complete command', () => { const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); assert.ok(state.includes('Milestone complete'), 'status should be milestone complete'); }); + + test('updates REQUIREMENTS.md traceability when phase completes', () => { + 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 + +### Phase 2: API +**Goal:** Build API +**Requirements:** API-01 +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements + +## v1 Requirements + +### Authentication + +- [ ] **AUTH-01**: User can sign up with email +- [ ] **AUTH-02**: User can log in +- [ ] **AUTH-03**: User can reset password + +### API + +- [ ] **API-01**: REST endpoints + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| AUTH-01 | Phase 1 | Pending | +| AUTH-02 | Phase 1 | Pending | +| AUTH-03 | Phase 2 | Pending | +| API-01 | Phase 2 | 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'); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-api'), { recursive: true }); + + 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'); + + // Checkboxes updated for phase 1 requirements + assert.ok(req.includes('- [x] **AUTH-01**'), 'AUTH-01 checkbox should be checked'); + assert.ok(req.includes('- [x] **AUTH-02**'), 'AUTH-02 checkbox should be checked'); + // Other requirements unchanged + assert.ok(req.includes('- [ ] **AUTH-03**'), 'AUTH-03 should remain unchecked'); + assert.ok(req.includes('- [ ] **API-01**'), 'API-01 should remain unchecked'); + + // Traceability table updated + assert.ok(req.includes('| AUTH-01 | Phase 1 | Complete |'), 'AUTH-01 status should be Complete'); + assert.ok(req.includes('| AUTH-02 | Phase 1 | Complete |'), 'AUTH-02 status should be Complete'); + assert.ok(req.includes('| AUTH-03 | Phase 2 | Pending |'), 'AUTH-03 should remain Pending'); + assert.ok(req.includes('| API-01 | Phase 2 | Pending |'), 'API-01 should remain Pending'); + }); + + test('handles phase with no requirements mapping', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Setup + +### Phase 1: Setup +**Goal:** Project setup (no requirements) +**Plans:** 1 plans +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), + `# Requirements + +## v1 Requirements + +- [ ] **REQ-01**: Some requirement + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| REQ-01 | Phase 2 | Pending | +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\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'); + + const result = runGsdTools('phase complete 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // REQUIREMENTS.md should be unchanged + const req = fs.readFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), 'utf-8'); + assert.ok(req.includes('- [ ] **REQ-01**'), 'REQ-01 should remain unchecked'); + assert.ok(req.includes('| REQ-01 | Phase 2 | Pending |'), 'REQ-01 should remain Pending'); + }); + + test('handles missing REQUIREMENTS.md gracefully', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +- [ ] Phase 1: Foundation +**Requirements:** REQ-01 + +### Phase 1: Foundation +**Goal:** Setup +` + ); + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + `# State\n\n**Current Phase:** 01\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-foundation'); + 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 should succeed even without REQUIREMENTS.md: ${result.error}`); + }); }); // ─────────────────────────────────────────────────────────────────────────────