* 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
This commit is contained in:
5
.changeset/agile-elks-glide.md
Normal file
5
.changeset/agile-elks-glide.md
Normal file
@@ -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)
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user