mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Scan REQUIREMENTS.md body for all **REQ-ID** patterns during phase complete and emit a warning for any IDs absent from the Traceability table, regardless of whether the roadmap has a Requirements: line. Closes #2526
This commit is contained in:
@@ -870,9 +870,10 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
const sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
||||
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
||||
|
||||
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||
|
||||
if (reqMatch) {
|
||||
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
|
||||
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||
|
||||
for (const reqId of reqIds) {
|
||||
const reqEscaped = escapeRegex(reqId);
|
||||
@@ -887,10 +888,40 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
||||
'$1 Complete $2'
|
||||
);
|
||||
}
|
||||
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
requirementsUpdated = true;
|
||||
}
|
||||
|
||||
// Scan body for all **REQ-ID** patterns, warn about any missing from the Traceability table.
|
||||
// Always runs regardless of whether the roadmap has a Requirements: line.
|
||||
const bodyReqIds = [];
|
||||
const bodyReqPattern = /\*\*([A-Z][A-Z0-9]*-\d+)\*\*/g;
|
||||
let bodyMatch;
|
||||
while ((bodyMatch = bodyReqPattern.exec(reqContent)) !== null) {
|
||||
const id = bodyMatch[1];
|
||||
if (!bodyReqIds.includes(id)) bodyReqIds.push(id);
|
||||
}
|
||||
|
||||
// Collect REQ-IDs present in the Traceability section only, to avoid
|
||||
// picking up IDs from other tables in the document.
|
||||
const traceabilityHeadingMatch = reqContent.match(/^#{1,6}\s+Traceability\b/im);
|
||||
const traceabilitySection = traceabilityHeadingMatch
|
||||
? reqContent.slice(traceabilityHeadingMatch.index)
|
||||
: '';
|
||||
const tableReqIds = new Set();
|
||||
const tableRowPattern = /^\|\s*([A-Z][A-Z0-9]*-\d+)\s*\|/gm;
|
||||
let tableMatch;
|
||||
while ((tableMatch = tableRowPattern.exec(traceabilitySection)) !== null) {
|
||||
tableReqIds.add(tableMatch[1]);
|
||||
}
|
||||
|
||||
const unregistered = bodyReqIds.filter(id => !tableReqIds.has(id));
|
||||
if (unregistered.length > 0) {
|
||||
warnings.push(
|
||||
`REQUIREMENTS.md: ${unregistered.length} REQ-ID(s) found in body but missing from Traceability table: ${unregistered.join(', ')} — add them manually to keep traceability in sync`
|
||||
);
|
||||
}
|
||||
|
||||
atomicWriteFileSync(reqPath, reqContent);
|
||||
requirementsUpdated = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
271
tests/bug-2526-phase-complete-req-discovery.test.cjs
Normal file
271
tests/bug-2526-phase-complete-req-discovery.test.cjs
Normal file
@@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Regression tests for bug #2526
|
||||
*
|
||||
* phase complete must warn about REQ-IDs that appear in the REQUIREMENTS.md
|
||||
* body but are missing from the Traceability table.
|
||||
*
|
||||
* Root cause: cmdPhaseComplete() only flips status for REQ-IDs already in
|
||||
* the Traceability table (from the roadmap **Requirements:** line). REQ-IDs
|
||||
* added to the REQUIREMENTS.md body after roadmap creation are never
|
||||
* discovered or reflected in the table.
|
||||
*
|
||||
* Fix (Option A — warning only): scan the REQUIREMENTS.md body for all
|
||||
* REQ-IDs, check which are absent from the Traceability table, and emit
|
||||
* a warning listing the missing IDs.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test, beforeEach, afterEach } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const os = require('node:os');
|
||||
const { execFileSync } = require('node:child_process');
|
||||
|
||||
const gsdTools = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs');
|
||||
|
||||
describe('bug #2526: phase complete warns about unregistered REQ-IDs', () => {
|
||||
let tmpDir;
|
||||
let planningDir;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2526-'));
|
||||
planningDir = path.join(tmpDir, '.planning');
|
||||
fs.mkdirSync(planningDir, { recursive: true });
|
||||
|
||||
// Minimal config
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'config.json'),
|
||||
JSON.stringify({ project_code: '' })
|
||||
);
|
||||
|
||||
// Minimal STATE.md
|
||||
fs.writeFileSync(
|
||||
path.join(planningDir, 'STATE.md'),
|
||||
'---\ncurrent_phase: 1\nstatus: executing\n---\n# State\n'
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('emits warning for REQ-IDs in body but missing from Traceability table', () => {
|
||||
// Set up phase directory with a plan and summary
|
||||
const phasesDir = path.join(planningDir, 'phases', '01-foundation');
|
||||
fs.mkdirSync(phasesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-PLAN.md'),
|
||||
'---\nphase: 1\nplan: 1\n---\n# Plan 1\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-SUMMARY.md'),
|
||||
'---\nstatus: complete\n---\n# Summary\nDone.'
|
||||
);
|
||||
|
||||
// ROADMAP.md — phase 1 lists only REQ-001 in its Requirements line
|
||||
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
||||
fs.writeFileSync(roadmapPath, [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'### Phase 1: Foundation',
|
||||
'',
|
||||
'**Goal:** Build core',
|
||||
'**Requirements:** REQ-001',
|
||||
'**Plans:** 1 plans',
|
||||
'',
|
||||
'Plans:',
|
||||
'- [x] 01-1-PLAN.md',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'|-------|-------|--------|-----------|',
|
||||
'| 1. Foundation | 0/1 | Pending | - |',
|
||||
].join('\n'));
|
||||
|
||||
// REQUIREMENTS.md — body has REQ-001 (in table) and REQ-002, REQ-003 (missing from table)
|
||||
const reqPath = path.join(planningDir, 'REQUIREMENTS.md');
|
||||
fs.writeFileSync(reqPath, [
|
||||
'# Requirements',
|
||||
'',
|
||||
'## Functional Requirements',
|
||||
'',
|
||||
'- [x] **REQ-001**: Core data model',
|
||||
'- [ ] **REQ-002**: User authentication',
|
||||
'- [ ] **REQ-003**: API endpoints',
|
||||
'',
|
||||
'## Traceability',
|
||||
'',
|
||||
'| REQ-ID | Phase | Status |',
|
||||
'|--------|-------|--------|',
|
||||
'| REQ-001 | 1 | Pending |',
|
||||
].join('\n'));
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
const result = execFileSync('node', [gsdTools, 'phase', 'complete', '1'], {
|
||||
cwd: tmpDir,
|
||||
timeout: 10000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
stdout = result;
|
||||
} catch (err) {
|
||||
stdout = err.stdout || '';
|
||||
stderr = err.stderr || '';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const combined = stdout + stderr;
|
||||
assert.match(
|
||||
combined,
|
||||
/REQ-002/,
|
||||
'output should mention REQ-002 as missing from Traceability table'
|
||||
);
|
||||
assert.match(
|
||||
combined,
|
||||
/REQ-003/,
|
||||
'output should mention REQ-003 as missing from Traceability table'
|
||||
);
|
||||
});
|
||||
|
||||
test('no warning when all body REQ-IDs are present in Traceability table', () => {
|
||||
// Set up phase directory
|
||||
const phasesDir = path.join(planningDir, 'phases', '01-foundation');
|
||||
fs.mkdirSync(phasesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-PLAN.md'),
|
||||
'---\nphase: 1\nplan: 1\n---\n# Plan 1\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-SUMMARY.md'),
|
||||
'---\nstatus: complete\n---\n# Summary\nDone.'
|
||||
);
|
||||
|
||||
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
||||
fs.writeFileSync(roadmapPath, [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'### Phase 1: Foundation',
|
||||
'',
|
||||
'**Goal:** Build core',
|
||||
'**Requirements:** REQ-001, REQ-002',
|
||||
'**Plans:** 1 plans',
|
||||
'',
|
||||
'Plans:',
|
||||
'- [x] 01-1-PLAN.md',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'|-------|-------|--------|-----------|',
|
||||
'| 1. Foundation | 0/1 | Pending | - |',
|
||||
].join('\n'));
|
||||
|
||||
// All body REQ-IDs are present in the Traceability table
|
||||
const reqPath = path.join(planningDir, 'REQUIREMENTS.md');
|
||||
fs.writeFileSync(reqPath, [
|
||||
'# Requirements',
|
||||
'',
|
||||
'## Functional Requirements',
|
||||
'',
|
||||
'- [x] **REQ-001**: Core data model',
|
||||
'- [x] **REQ-002**: User authentication',
|
||||
'',
|
||||
'## Traceability',
|
||||
'',
|
||||
'| REQ-ID | Phase | Status |',
|
||||
'|--------|-------|--------|',
|
||||
'| REQ-001 | 1 | Pending |',
|
||||
'| REQ-002 | 1 | Pending |',
|
||||
].join('\n'));
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
const result = execFileSync('node', [gsdTools, 'phase', 'complete', '1'], {
|
||||
cwd: tmpDir,
|
||||
timeout: 10000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
stdout = result;
|
||||
} catch (err) {
|
||||
stdout = err.stdout || '';
|
||||
stderr = err.stderr || '';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const combined = stdout + stderr;
|
||||
assert.doesNotMatch(
|
||||
combined,
|
||||
/unregistered|missing.*traceability|not in.*traceability/i,
|
||||
'no warning should appear when all REQ-IDs are in the table'
|
||||
);
|
||||
});
|
||||
|
||||
test('warning includes all missing REQ-IDs, not just the first', () => {
|
||||
const phasesDir = path.join(planningDir, 'phases', '01-foundation');
|
||||
fs.mkdirSync(phasesDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-PLAN.md'),
|
||||
'---\nphase: 1\nplan: 1\n---\n# Plan 1\n'
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, '01-1-SUMMARY.md'),
|
||||
'---\nstatus: complete\n---\n# Summary\nDone.'
|
||||
);
|
||||
|
||||
const roadmapPath = path.join(planningDir, 'ROADMAP.md');
|
||||
fs.writeFileSync(roadmapPath, [
|
||||
'# Roadmap',
|
||||
'',
|
||||
'### Phase 1: Foundation',
|
||||
'',
|
||||
'**Goal:** Build core',
|
||||
'**Requirements:** REQ-001',
|
||||
'**Plans:** 1 plans',
|
||||
'',
|
||||
'Plans:',
|
||||
'- [x] 01-1-PLAN.md',
|
||||
'',
|
||||
'| Phase | Plans | Status | Completed |',
|
||||
'|-------|-------|--------|-----------|',
|
||||
'| 1. Foundation | 0/1 | Pending | - |',
|
||||
].join('\n'));
|
||||
|
||||
// Body has 4 REQ-IDs; table only has 1
|
||||
const reqPath = path.join(planningDir, 'REQUIREMENTS.md');
|
||||
fs.writeFileSync(reqPath, [
|
||||
'# Requirements',
|
||||
'',
|
||||
'- [x] **REQ-001**: Core data model',
|
||||
'- [ ] **REQ-002**: User auth',
|
||||
'- [ ] **REQ-003**: API',
|
||||
'- [ ] **REQ-004**: Reports',
|
||||
'',
|
||||
'## Traceability',
|
||||
'',
|
||||
'| REQ-ID | Phase | Status |',
|
||||
'|--------|-------|--------|',
|
||||
'| REQ-001 | 1 | Pending |',
|
||||
].join('\n'));
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
try {
|
||||
const result = execFileSync('node', [gsdTools, 'phase', 'complete', '1'], {
|
||||
cwd: tmpDir,
|
||||
timeout: 10000,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
stdout = result;
|
||||
} catch (err) {
|
||||
stdout = err.stdout || '';
|
||||
stderr = err.stderr || '';
|
||||
throw err;
|
||||
}
|
||||
|
||||
const combined = stdout + stderr;
|
||||
assert.match(combined, /REQ-002/, 'should warn about REQ-002');
|
||||
assert.match(combined, /REQ-003/, 'should warn about REQ-003');
|
||||
assert.match(combined, /REQ-004/, 'should warn about REQ-004');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user