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 sectionText = phaseSectionMatch ? phaseSectionMatch[1] : '';
|
||||||
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
const reqMatch = sectionText.match(/\*\*Requirements:\*\*\s*([^\n]+)/i);
|
||||||
|
|
||||||
|
let reqContent = fs.readFileSync(reqPath, 'utf-8');
|
||||||
|
|
||||||
if (reqMatch) {
|
if (reqMatch) {
|
||||||
const reqIds = reqMatch[1].replace(/[\[\]]/g, '').split(/[,\s]+/).map(r => r.trim()).filter(Boolean);
|
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) {
|
for (const reqId of reqIds) {
|
||||||
const reqEscaped = escapeRegex(reqId);
|
const reqEscaped = escapeRegex(reqId);
|
||||||
@@ -887,10 +888,40 @@ function cmdPhaseComplete(cwd, phaseNum, raw) {
|
|||||||
'$1 Complete $2'
|
'$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