From 461c744c315d07a4f4a33ef5a036af1786ef5e7c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sat, 25 Jul 2026 00:36:05 -0400 Subject: [PATCH] fix(#2522): fold wrapped success-criteria lines into their criterion (#2637) * test(#2522): wrapped + blank-line success-criteria parse * fix(#2522): fold wrapped success-criteria lines into their criterion * chore(#2522): backfill changeset pr to 2637 --- .changeset/agile-elks-glide.md | 5 +++ src/roadmap.cts | 13 +++++-- tests/roadmap.test.cjs | 67 ++++++++++++++++++++++++++++++++++ 3 files changed, 82 insertions(+), 3 deletions(-) create mode 100644 .changeset/agile-elks-glide.md diff --git a/.changeset/agile-elks-glide.md b/.changeset/agile-elks-glide.md new file mode 100644 index 000000000..8c5d4ac37 --- /dev/null +++ b/.changeset/agile-elks-glide.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2637 +--- +**`roadmap get-phase` no longer drops success criteria that wrap onto a second line** — the parser broke the criteria run at any indented continuation line, truncating the wrapped criterion (losing its trailing `[REQ-ID]` tag) and silently dropping every criterion below it. `verify-work` and `plan-phase` consumed the shortened list, so a phase could be planned and certified complete against a strict subset of its own success criteria with nothing reporting the gap. Continuation lines now fold into their criterion; blank-line-separated criteria still parse. (#2522) diff --git a/src/roadmap.cts b/src/roadmap.cts index add1d861c..4482a0ddf 100644 --- a/src/roadmap.cts +++ b/src/roadmap.cts @@ -175,10 +175,17 @@ function searchPhaseInContent(content: string, escapedPhase: string, phaseNum: s const modeMatch = section.match(/\*\*Mode(?::\*\*|\*\*:)\s*([^\n]+)/i); const mode = modeMatch ? modeMatch[1].trim().toLowerCase() : null; - // Extract success criteria as structured array - const criteriaMatch = section.match(/\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\s*\d+\.\s*[^\n]+\n?)+)/i); + // Extract success criteria as structured array. A criterion may wrap onto extra + // indented lines (no `N.` prefix); those continuations must fold INTO their + // criterion, not end the run (#2522 — the old `(?:\s*\d+\.\s*[^\n]+)+` broke on a + // wrapped line, truncating it and silently dropping every criterion below it). + // `\n*` before each numbered line keeps blank-line-separated criteria working. + const criteriaMatch = section.match( + /\*\*Success Criteria\*\*[^\n]*:\s*\n((?:\n*[ \t]*\d+\.[^\n]*\n?(?:[ \t]+(?!\d+\.)[^\n]*\n?)*)+)/i); const success_criteria = criteriaMatch - ? criteriaMatch[1].trim().split('\n').map(line => line.replace(/^\s*\d+\.\s*/, '').trim()).filter(Boolean) + ? criteriaMatch[1].trim().split(/\n+(?=[ \t]*\d+\.)/) + .map(entry => entry.replace(/^\s*\d+\.\s*/, '').replace(/\s*\n\s*/g, ' ').trim()) + .filter(Boolean) : []; return { diff --git a/tests/roadmap.test.cjs b/tests/roadmap.test.cjs index d401b0c5a..5af46a12a 100644 --- a/tests/roadmap.test.cjs +++ b/tests/roadmap.test.cjs @@ -621,6 +621,73 @@ describe('roadmap get-phase success criteria', () => { assert.ok(output.success_criteria[2].includes('Third criterion'), 'third criterion matches'); }); + test('#2522: wrapped success-criteria lines fold into their criterion (not dropped/truncated)', () => { + // A criterion that wraps onto an indented continuation line must fold INTO its + // criterion (retaining its trailing [REQ-ID]), and every criterion below the wrap + // must still be parsed. The old regex broke the run at the wrap, truncating + // criterion 2 mid-sentence (losing [A-02]) and silently dropping criterion 3. + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '# Roadmap', + '', + '### Phase 9: T', + '**Goal**: g', + '**Success Criteria** (what must be TRUE):', + ' 1. alpha. [A-01]', + ' 2. beta which is long and', + ' wraps onto a second line. [A-02]', + ' 3. gamma. [A-03]', + '**Plans**: TBD', + '', + ].join('\n') + ); + + const result = runGsdTools('roadmap get-phase 9', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.found, true, 'phase should be found'); + assert.strictEqual(output.success_criteria.length, 3, 'all 3 criteria must parse — wrapped continuations must not drop/truncate (#2522)'); + assert.ok(output.success_criteria[1].includes('wraps onto a second line'), 'the wrapped continuation must fold into criterion 2'); + assert.ok(output.success_criteria[1].includes('[A-02]'), 'the wrapped criterion must retain its trailing [REQ-ID] tag (#2522)'); + assert.ok(output.success_criteria[2].includes('[A-03]'), 'criterion 3 (below the wrap) must not be dropped (#2522)'); + // Boundary: the parse must not bleed past the Success Criteria block. + assert.ok(!output.success_criteria.some(c => c.includes('TBD')), 'parse must not consume the **Plans** line'); + }); + + test('#2522: blank-line-separated success criteria still parse (the continuation fix must not regress blank lines)', () => { + // Guard against the naive-form trap the issue flags: narrowing the leading + // whitespace to [ \t]* and splitting on \n would trade wrapped-line truncation + // for blank-line truncation. Blank lines between criteria must keep working. + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + [ + '# Roadmap', + '', + '### Phase 1: T', + '**Goal**: g', + '**Success Criteria** (what must be TRUE):', + ' 1. alpha. [A-01]', + '', + ' 2. beta. [A-02]', + '', + ' 3. gamma. [A-03]', + '**Plans**: TBD', + '', + ].join('\n') + ); + + const result = runGsdTools('roadmap get-phase 1', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.success_criteria.length, 3, 'blank-line-separated criteria must all parse (#2522 guard)'); + assert.ok(output.success_criteria[0].includes('[A-01]')); + assert.ok(output.success_criteria[1].includes('[A-02]')); + assert.ok(output.success_criteria[2].includes('[A-03]')); + }); + test('returns empty array when no success criteria present', () => { fs.writeFileSync( path.join(tmpDir, '.planning', 'ROADMAP.md'),