fix: gap-analysis parses mixed requirement prefixes and skips table headers (#2902)
* fix: parse non-REQ IDs in gap-analysis and ignore table headers * fix: parse requirement IDs from first traceability column only --------- Co-authored-by: Tom Boucher <thomas.boucher@sas.com>
This commit is contained in:
@@ -39,6 +39,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||
posting a comment that points to the contribution guide. (#2872)
|
||||
|
||||
### Fixed — 1.40.0-rc.1
|
||||
- **`gap-analysis` now parses non-`REQ-` requirement IDs and ignores traceability table headers** — `parseRequirements()` no longer hard-codes the `REQ-` prefix and now accepts uppercase prefixed IDs such as `TST-01`, `BACK-07`, and `INSP-04`; markdown table header rows (for example `| REQ-ID | ... |`) are excluded so header tokens are not reported as phantom uncovered requirements. Added regression coverage for mixed-prefix REQUIREMENTS files with traceability tables. (#2897)
|
||||
- **Gemini slash commands namespaced as `/gsd:<cmd>` instead of `/gsd-<cmd>`** —
|
||||
Gemini CLI namespaces commands under `gsd:`, so `/gsd-plan-phase` was unexecutable.
|
||||
Body-text references in commands, agents, banners, and patch-reapply hints are now
|
||||
|
||||
@@ -31,7 +31,10 @@ function parseRequirements(reqMd) {
|
||||
const out = [];
|
||||
const seen = new Set();
|
||||
|
||||
const checkboxRe = /^\s*-\s*\[[x ]\]\s*\*\*(REQ-[A-Za-z0-9_-]+)\*\*\s*(.*)$/gm;
|
||||
// Prefix-agnostic ID format: REQ-01, TST-01, BACK-07, INSP-04, etc.
|
||||
const ID_PATTERN = '[A-Z][A-Z0-9]*-[A-Za-z0-9_-]+';
|
||||
|
||||
const checkboxRe = new RegExp(`^\\s*-\\s*\\[[x ]\\]\\s*\\*\\*(${ID_PATTERN})\\*\\*\\s*(.*)$`, 'gm');
|
||||
let cm = checkboxRe.exec(reqMd);
|
||||
while (cm !== null) {
|
||||
const id = cm[1];
|
||||
@@ -42,15 +45,25 @@ function parseRequirements(reqMd) {
|
||||
cm = checkboxRe.exec(reqMd);
|
||||
}
|
||||
|
||||
const tableRe = /\|\s*(REQ-[A-Za-z0-9_-]+)\s*\|/g;
|
||||
let tm = tableRe.exec(reqMd);
|
||||
while (tm !== null) {
|
||||
const tableFirstCellRe = new RegExp(`^\\s*\\|\\s*(${ID_PATTERN})\\s*\\|`);
|
||||
const separatorRowRe = /^\s*\|[\s:|-]+\|\s*$/;
|
||||
const lines = reqMd.split(/\r?\n/);
|
||||
|
||||
for (let i = 0; i < lines.length; i += 1) {
|
||||
const line = lines[i];
|
||||
if (!line.includes('|')) continue;
|
||||
|
||||
// Skip markdown table separator rows and header rows immediately preceding them.
|
||||
if (separatorRowRe.test(line)) continue;
|
||||
if (i + 1 < lines.length && separatorRowRe.test(lines[i + 1])) continue;
|
||||
|
||||
const tm = tableFirstCellRe.exec(line);
|
||||
if (!tm) continue;
|
||||
const id = tm[1];
|
||||
if (!seen.has(id)) {
|
||||
seen.add(id);
|
||||
out.push({ id, text: '' });
|
||||
}
|
||||
tm = tableRe.exec(reqMd);
|
||||
}
|
||||
|
||||
return out;
|
||||
|
||||
@@ -240,6 +240,59 @@ describe('gap-analysis CLI (#2493)', () => {
|
||||
assert.deepStrictEqual(reqRows, ['REQ-01', 'REQ-02', 'REQ-10']);
|
||||
});
|
||||
|
||||
test('parses non-REQ prefixes and ignores traceability header tokens', () => {
|
||||
const requirementsMd = [
|
||||
'# Requirements',
|
||||
'',
|
||||
'| REQ-ID | Phase | Plan(s) |',
|
||||
'|--------|-------|---------|',
|
||||
'| TST-01 | Phase 01 | TBD |',
|
||||
'| BACK-07 | Phase 01 | TBD |',
|
||||
'',
|
||||
'- [ ] **INSP-04** Inspector requirement.',
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), `${requirementsMd}\n`);
|
||||
|
||||
writePlan('01', '# Plan\n\nCovers TST-01, BACK-07, and INSP-04.\n');
|
||||
|
||||
const r = runGsdTools(['gap-analysis', '--phase-dir', phaseDir], tmpDir);
|
||||
assert.ok(r.success, r.error);
|
||||
const out = JSON.parse(r.output);
|
||||
|
||||
const reqRows = out.rows.filter(x => x.source === 'REQUIREMENTS.md');
|
||||
const ids = reqRows.map(x => x.item);
|
||||
|
||||
assert.deepStrictEqual(ids, ['BACK-07', 'INSP-04', 'TST-01']);
|
||||
assert.ok(!ids.includes('REQ-ID'), 'traceability header token must not be parsed as a requirement ID');
|
||||
assert.ok(reqRows.every(x => x.status === 'Covered'));
|
||||
});
|
||||
|
||||
test('does not parse requirement-like IDs from non-first table columns', () => {
|
||||
const requirementsMd = [
|
||||
'# Requirements',
|
||||
'',
|
||||
'| REQ-ID | Phase | Plan(s) |',
|
||||
'|--------|-------|---------|',
|
||||
'| TST-01 | Phase 01 | PLAN-01 |',
|
||||
'| BACK-07 | Phase 01 | PLAN-02 |',
|
||||
].join('\n');
|
||||
fs.writeFileSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md'), `${requirementsMd}\n`);
|
||||
|
||||
writePlan('01', '# Plan\n\nCovers TST-01 and BACK-07 only.\n');
|
||||
|
||||
const r = runGsdTools(['gap-analysis', '--phase-dir', phaseDir], tmpDir);
|
||||
assert.ok(r.success, r.error);
|
||||
const out = JSON.parse(r.output);
|
||||
|
||||
const ids = out.rows
|
||||
.filter(x => x.source === 'REQUIREMENTS.md')
|
||||
.map(x => x.item);
|
||||
|
||||
assert.deepStrictEqual(ids, ['BACK-07', 'TST-01']);
|
||||
assert.ok(!ids.includes('PLAN-01'));
|
||||
assert.ok(!ids.includes('PLAN-02'));
|
||||
});
|
||||
|
||||
test('REQUIREMENTS.md missing → CONTEXT-only run still works', () => {
|
||||
writeContext([{ id: 'D-01', text: 'foo' }]);
|
||||
writePlan('01', '# Plan mentioning D-01\n');
|
||||
|
||||
Reference in New Issue
Block a user