diff --git a/changelog.d/gst-1406-fixable-blocks-no.md b/changelog.d/gst-1406-fixable-blocks-no.md index 0c223f4e4..1e514bf95 100644 --- a/changelog.d/gst-1406-fixable-blocks-no.md +++ b/changelog.d/gst-1406-fixable-blocks-no.md @@ -1,3 +1,3 @@ ### Fixed -- **Lead judgment can lower a native adversarial finding to the Severity its reader stated beside `Blocks: no` (GST-1406).** When the native reader wrote its `Severity` and `Blocks: no` after prose (`… so the docs are wrong. Severity: Medium. Blocks: no.`), collect kept the heading's FIXABLE as CRITICAL. Judge refused any lowering, so the native record never resolved and /ship's Step 11.5 couldn't bind. Collect still can't tell those fields from another subject's written under the same heading, so it keeps the label. Now `gstack-review-dispatch judge` lets lead judgment lower such a finding to its stated Severity, never below, with a reason. It's recorded as `dismissed`, and the ledger's blocker count follows it. Other readers, a finding that states `Blocks: yes` or no `Blocks`, or a lowering below the stated Severity or with no reason are still refused as downgrades. See docs/review-policy.md. +- **Lead judgment can lower a native adversarial finding to the Severity its reader stated beside `Blocks: no` (GST-1406).** When the native reader wrote its `Severity` and `Blocks: no` after prose (`… so the docs are wrong. Severity: Medium. Blocks: no.`), collect kept the heading's FIXABLE as CRITICAL. Judge refused any lowering, so the native record never resolved and /ship's Step 11.5 couldn't bind. Collect still can't tell those fields from another subject's written under the same heading, so it keeps the label. Now `gstack-review-dispatch judge` lets lead judgment lower such a finding to its stated Severity, never below, with a reason. It's recorded as `dismissed`, and the ledger's blocker count follows it. When the native reader writes those fields after prose in the heading's own paragraph, collect now keeps them on the heading's record (at its label) instead of splitting them into a second record. Other readers, a finding that states `Blocks: yes`, no `Blocks` or a Severity that isn't a severity (such as `Severity: FIXABLE`), and a lowering below the stated Severity or with no reason are still refused as downgrades. See docs/review-policy.md. diff --git a/changelog.d/gst-1409-fenced-code-body.md b/changelog.d/gst-1409-fenced-code-body.md new file mode 100644 index 000000000..0d79832d4 --- /dev/null +++ b/changelog.d/gst-1409-fenced-code-body.md @@ -0,0 +1,3 @@ +### Fixed + +- **Code a reviewer quotes in a fence no longer turns into findings (GST-1409).** When a reader quoted a probe input or a checklist inside a ``` or ~~~ fence, `gstack-review-dispatch collect` parsed each quoted line. A quoted `### 1. FIXABLE: …` heading or a quoted `Blocks: yes` became a phantom CRITICAL that lead judgment couldn't lower, and the real finding lost its own Severity and Blocks fields. Now a fence that opens inside a finding is that finding's body: its lines collect nothing, and the record notes `(code block: lines)`. A JSON finding line inside a fence still collects, and a fence with no finding open parses as before. Checked against 334 stored reader outputs (668 comparisons): 664 collect the same, and the 4 that change each drop only fenced quotes. diff --git a/lib/review-ledger.ts b/lib/review-ledger.ts index ff3e5acaf..52c6f73a5 100644 --- a/lib/review-ledger.ts +++ b/lib/review-ledger.ts @@ -89,7 +89,7 @@ const BLOCKS_FIELD = /(? { const ls = [...raw.matchAll(SEVERITY_FIELD)].flatMap(m => (m[1] ?? m[2]!).match(FIELD_LABELS)!); - return reader.split('/')[0] === 'adversarial-native' && statesNoBlocks(raw) && ls.length === 1 && to >= (rank(ls[0]!) ?? Infinity); + return reader.split('/')[0] === 'adversarial-native' && statesNoBlocks(raw) && ls.length === 1 && to >= (rank(ls[0]!) || Infinity); }; export const statesNoBlocks = (raw: string) => { if (raw.startsWith('{') || BLOCKS_YES.test(raw)) return false; const said = [...raw.matchAll(BLOCKS_FIELD)].map(m => m[1]!.toLowerCase()); return said.length > 0 && !said.includes('yes'); }; /** @@ -243,6 +243,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 (role === 'adversarial-native' && current?.heading && !current.fields.size && !blocks && fieldOf(t) !== undefined && statesNoBlocks(t) && !LEAD_LABEL.test(t) && !LIST_ITEM.test(t) && !HEADING.test(t) && !NUMBERED_HEAD.test(t)) { + current.fields.add('severity'); + current.label = stronger(current.label, 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 df56940d2..c91c1d36a 100644 --- a/test/review-gst1031.test.ts +++ b/test/review-gst1031.test.ts @@ -116,6 +116,12 @@ describe('lead judgment may lower a native finding to the Severity its reader st expect(run('adversarial-native', OUT.replace('Blocks: no', 'Blocks: yes'), 'Medium', 'r')).toMatchObject({ ok: false, dismissed: 0 }); expect(run('adversarial-native', OUT.replace(' Blocks: no, because a reader still reads it.', ''), 'Medium', 'r')).toMatchObject({ ok: false, dismissed: 0 }); expect(run('red-team', '**1. Auth bypass lets any user read admin data (CRITICAL).**\n\nThe earlier triage filed it lower. Severity: Low. Blocks: no.\n', 'Low', 'r')).toMatchObject({ ok: false, dismissed: 0 }); + expect(run('adversarial-native', OUT.replace('Severity: Medium', 'Severity: FIXABLE'), 'Low', 'r')).toMatchObject({ ok: false, dismissed: 0 }); + }); + test('fields written in the heading paragraph after prose stay on the heading record, which keeps its label, so it can be lowered', () => { + const same = '**1. Title (FIXABLE).** body in lib/x.ts.\nMore prose. Severity: Medium. Blocks: no.\n'; + expect(run('adversarial-native', same, 'Medium', 'its reader stated Severity: Medium, Blocks: no')).toEqual({ labels: ['CRITICAL'], ok: true, dismissed: 1, blockers: 0 }); + expect(run('red-team', '**1. Title (CRITICAL).** body in lib/x.ts.\nMore prose. Severity: Low. Blocks: no.\n', 'CRITICAL')).toMatchObject({ labels: ['CRITICAL', 'Low'] }); }); });