diff --git a/changelog.d/gst-1406-fixable-blocks-no.md b/changelog.d/gst-1406-fixable-blocks-no.md new file mode 100644 index 000000000..9f70775ae --- /dev/null +++ b/changelog.d/gst-1406-fixable-blocks-no.md @@ -0,0 +1,3 @@ +### Fixed + +- **A native adversarial finding that says `Blocks: no` after some prose is no longer collected as CRITICAL (GST-1406).** `gstack-review-dispatch collect` honored GST-1031's rule (a FIXABLE finding with its own `Severity` and `Blocks: no` collects at that severity) only when the fields started their own line. When the reader wrote them after prose (`… so the docs are wrong. Severity: Medium. Blocks: no.`), the heading's FIXABLE won. If the line was in the heading's paragraph, the fields split off as a second record. Lead judgment can't lower a CRITICAL, so the native record stayed unresolved and /ship's Step 11.5 couldn't bind. Now such a line counts as the finding's fields. A finding with no `Blocks: no`, or with `Blocks: yes`, still collects CRITICAL. diff --git a/lib/review-ledger.ts b/lib/review-ledger.ts index bea5628c0..e268bbcad 100644 --- a/lib/review-ledger.ts +++ b/lib/review-ledger.ts @@ -222,6 +222,12 @@ export function parseFindings(reader: string, output: string, fixableRaises = tr } const heading = LABEL_HEADING.exec(t); if (heading) { section = fixableRaises ? named(heading[1]!) : heading[1]!; current = null; head = ''; continue; } + if (current?.heading && !current.fields.size && fieldOf(t) !== undefined && !LEAD_LABEL.test(t) && !LIST_ITEM.test(t) && !HEADING.test(t) && !NUMBERED_HEAD.test(t) && !LOCUS.test(t)) { + current.fields.add('severity'); + current.label = blocks ? 'CRITICAL' : critical(current.raw) ? stronger(current.label, label!) : strongestOf([current.lead, label])!; + current.raw += ` | ${t}`; + continue; + } if (current?.heading && !current.fields.size && !label && !blocks && !/^#{1,6}\s/.test(t) && !NUMBERED_HEAD.test(t) && !t.startsWith('{')) { current.raw += ` | ${t}`; continue; } if (!indented && HEADING.test(t)) { section = null; current = null; } if (t.startsWith('{')) { diff --git a/test/review-gst1031.test.ts b/test/review-gst1031.test.ts index c8a4b76f0..f17fd239f 100644 --- a/test/review-gst1031.test.ts +++ b/test/review-gst1031.test.ts @@ -39,6 +39,19 @@ describe('one record per native finding at its stated severity (GST-1031 case 1, } expect(labels('adversarial-native', '## FIXABLE\n1. Leak in a.ts:3\n2. Race in b.ts:4. Blocks: no', true)).toEqual(['CRITICAL', 'CRITICAL']); }); + test('fields that follow body prose on their line still count as the fields (GST-1406)', () => { + // Value: protects=ruling A when the reader writes its Severity and Blocks: no after prose on the same line, in the heading's + // paragraph or after a blank line; fails_when=the heading's FIXABLE collects CRITICAL (an unlowerable blocker that stops Step + // 11.5) or the field line splits off as a second record; why_new=GSTA-275 GST-1276 v4 native pass, finding 1; seam=parseFindings + const head = '**1. Title (FIXABLE).** body in lib/x.ts.'; + for (const raises of [true, false]) { + expect(labels('adversarial-native', [head, '', 'More prose. Severity: Medium. Blocks: no.'].join('\n'), raises)).toEqual(['Medium']); + expect(labels('adversarial-native', [head, '- a', '- b', '', 'More prose. Severity: Medium. Blocks: no, because a reader still reads it.'].join('\n'), raises)).toEqual(['Medium']); + expect(labels('adversarial-native', [head, 'More prose. Severity: Medium. Blocks: no.'].join('\n'), raises)).toEqual(['Medium']); + expect(labels('adversarial-native', [head, '', 'More prose. Severity: Medium.'].join('\n'), raises)).toEqual(['CRITICAL']); + expect(labels('adversarial-native', [head, '', 'More prose. Severity: Medium. Blocks: yes.'].join('\n'), raises)).toEqual(['CRITICAL']); + } + }); test('collect records the stated severity beside the one record', () => { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gst1031-')); const c = collect(path.join(dir, 'l.jsonl'), 'adversarial-native', fixture('gst-1031-native.txt'), path.join(dir, 'm.jsonl'));