fix(#2769): tolerate Requirements header with colon inside bold delimiters (#2782)

* fix(#2769): tolerate Requirements header with colon inside bold delimiters

extractReqIds in sdk/src/query/init.ts and the legacy init.cjs port only
matched `**Requirements**:` (colon outside bold), so phases declared with
the equally-valid markdown form `**Requirements:**` (colon inside bold,
which is what the project's own templates emit) returned phase_req_ids:
null for both `init plan-phase` and `init execute-phase`.

The mirror-image bug in `phase complete`'s REQUIREMENTS.md traceability
sweep at get-shit-done/bin/lib/phase.cjs:871 only matched the inside-bold
form, silently skipping the REQ-ID checkbox flips for any roadmap that
used the outside-bold form. Both parsers now share the same canonical
regex that accepts all three rendered-identical variants:

  **Requirements:**     (colon inside bold)
  **Requirements**:     (colon outside bold)
  **Requirements** :    (space before outside colon)

Tests:
- tests/init.test.cjs — parameterized over the three header variants for
  both init plan-phase and init execute-phase (6 new behavioral cases).
- sdk/src/query/init.test.ts — describe.each over the same variants
  exercising initPlanPhase through the SDK.
- tests/bug-2769-requirements-header-variants.test.cjs — phase complete
  flips REQ-001 in REQUIREMENTS.md across all three header variants.

Closes #2769

* refactor(#2769): centralize REQUIREMENTS_HEADER_RE constant per CodeRabbit
This commit is contained in:
Tom Boucher
2026-04-27 12:31:49 -04:00
committed by GitHub
parent 54e6da3126
commit 936cf26706
6 changed files with 227 additions and 4 deletions

View File

@@ -351,6 +351,40 @@ describe('initPlanPhase', () => {
const data = result.data as Record<string, unknown>;
expect(data.error).toBeDefined();
});
// #2769: extractReqIds must accept all bold/colon variants of the
// Requirements header. The forms render identically in markdown but differ
// textually; the previous regex only matched **Requirements**: (colon
// outside bold) and silently returned null for **Requirements:** (colon
// inside bold) and **Requirements** : (spaced).
describe.each([
{ name: 'colon inside bold', header: '**Requirements:** RV-01, RV-02' },
{ name: 'colon outside bold', header: '**Requirements**: RV-01, RV-02' },
{ name: 'space before colon', header: '**Requirements** : RV-01, RV-02' },
])('phase_req_ids extraction (#2769)', ({ name, header }) => {
it(`parses Requirements header with ${name}`, async () => {
// Overwrite ROADMAP.md so phase 9 carries the variant header.
await writeFile(join(tmpDir, '.planning', 'ROADMAP.md'), [
'# Roadmap',
'',
'## v3.0: SDK-First Migration',
'',
'### Phase 9: Foundation',
'',
'**Goal:** Build foundation',
header,
'',
'### Phase 10: Read-Only Queries',
'',
'**Goal:** Implement queries',
'',
].join('\n'));
const result = await initPlanPhase(['9'], tmpDir);
const data = result.data as Record<string, unknown>;
expect(data.phase_req_ids).toBe('RV-01, RV-02');
});
});
});
describe('initNewMilestone', () => {

View File

@@ -203,7 +203,12 @@ async function getPhaseInfoForVerifyWork(
*/
function extractReqIds(roadmapPhase: Record<string, unknown> | null): string | null {
const section = roadmapPhase?.section as string | undefined;
const reqMatch = section?.match(/^\*\*Requirements\*\*:[^\S\n]*([^\n]*)$/m);
// Accept all bold/colon variants of the Requirements header. The forms
// **Requirements:** (colon inside bold)
// **Requirements**: (colon outside bold)
// **Requirements** : (space before outside colon)
// render identically in markdown but differ textually. Issue #2769.
const reqMatch = section?.match(/^\*\*Requirements:?\*\*[^\S\n]*:?[^\S\n]*([^\n]*)$/m);
const reqExtracted = reqMatch
? reqMatch[1].replace(/[\[\]]/g, '').split(',').map((s: string) => s.trim()).filter(Boolean).join(', ')
: null;