From 6dce1de4a7f896c79b78acc6a994268554989646 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Thu, 30 Apr 2026 12:13:55 -0400 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + get-shit-done/bin/lib/gap-checker.cjs | 23 ++++++++--- tests/post-planning-gaps-2493.test.cjs | 53 ++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f31a37af..b2d492639 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:` instead of `/gsd-`** — 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 diff --git a/get-shit-done/bin/lib/gap-checker.cjs b/get-shit-done/bin/lib/gap-checker.cjs index 5e353e438..0869581af 100644 --- a/get-shit-done/bin/lib/gap-checker.cjs +++ b/get-shit-done/bin/lib/gap-checker.cjs @@ -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; diff --git a/tests/post-planning-gaps-2493.test.cjs b/tests/post-planning-gaps-2493.test.cjs index c671b17b5..812fb00ef 100644 --- a/tests/post-planning-gaps-2493.test.cjs +++ b/tests/post-planning-gaps-2493.test.cjs @@ -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');