fix(tools): update REQUIREMENTS.md traceability when phase completes

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 <noreply@anthropic.com>
This commit is contained in:
Lex Christopherson
2026-02-15 09:51:37 -06:00
parent dcdb31cdf5
commit a142002dfe
2 changed files with 174 additions and 0 deletions

View File

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

View File

@@ -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}`);
});
});
// ─────────────────────────────────────────────────────────────────────────────