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:
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user