From c950a49a4dcbd72dc7425b6348e61ddec17c5f5e Mon Sep 17 00:00:00 2001 From: garrytan Date: Mon, 2 Nov 2026 12:24:35 +0000 Subject: [PATCH] fix(review-ledger): Severity counts only in field position, locus-first and emoji-led labels lead, an inline field floors the next (GST-976 review) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- lib/review-ledger.ts | 37 +++++++++++++++++++++--------------- test/review-dispatch.test.ts | 8 ++++++++ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/lib/review-ledger.ts b/lib/review-ledger.ts index 6f2b24b9f..ca31b0527 100644 --- a/lib/review-ledger.ts +++ b/lib/review-ledger.ts @@ -67,10 +67,18 @@ const META = new RegExp(`^(?:[-*+]\\s+)?\\**\\s*(Severity|Blocks)\\**\\s*(?::|\\ /** The severity and Blocks a prose finding's own fields state (GST-766): recorded beside the collected label, never in its place. */ export interface Stated { severity?: string; blocks?: string } const STATED_SEVERITY = new RegExp(`(?|]\\s*|\\d+[.)]\\s*|#{1,6}\\s*|[*_\`[(]+\\s*)*(${LABELS})(?![A-Za-z0-9])`); +/** + * A finding's own `Severity` field: at the line's start or after a separator, with its colon ("1. Drops rows (q.ts:7). Severity: Low", + * "- Severity: P0"). With one present a severity word in the body never raises the label (GST-976); prose that mentions a + * severity ("pass 1 had Severity: Low", "filed at severity low") is not a field. + */ +const SEVERITY_FIELD = new RegExp(`(?:^(?:[-*+>]\\s+|\\d+[.)]\\s+)?|[.;,|(]\\s*|\\s[—–-]\\s*)\\**\\s*Severity\\**\\s*:\\s*\\**\\s*(${LABELS})(?![A-Za-z0-9])`, 'gi'); +/** + * A label leading its line past list, quote, heading, emphasis and emoji marks and an optional file:line, closed by a mark or a + * separator ("[P0] …", "1. **High**: …", "### CRITICAL: …", "1. api.ts:10 CRITICAL: …"): the reader's own label, which a Severity + * field never lowers. "P0 pilot …" is not one. + */ +const LEAD_LABEL = new RegExp(`^(?:[^\\p{L}\\p{N}\\s]+\\s*|\\d+[.)]\\s*)*(?:[\\w./-]+:\\d+[^\\p{L}\\p{N}\\s]*\\s+(?:[^\\p{L}\\p{N}\\s]+\\s*)*)?(${LABELS})(?=[*_\`\\])]|\\s*[:|—–]|\\s+-\\s|\\s*$)`, 'u'); const STATED_BLOCKS = /(? (role === 'adversarial-native' && l.toUpperCase() === 'FIXABLE' ? 'CRITICAL' : l); - const strongest = (t: string) => [...t.matchAll(ALL_LABELS)].map(m => named(m[1]!)).reduce((a, b) => (a === undefined ? b : stronger(a, b)), undefined); + const strongestOf = (labels: Array) => labels.reduce((a, b) => (a === undefined ? b : b === undefined ? a : stronger(a, b)), undefined); + const strongest = (t: string) => strongestOf([...t.matchAll(ALL_LABELS)].map(m => named(m[1]!))); + const fieldOf = (t: string) => strongestOf([...t.matchAll(SEVERITY_FIELD)].map(m => m[1]!)); const critical = (t: string) => BLOCKS_YES.test(t) || (role === 'adversarial-native' && /(? { const l = LEAD_LABEL.exec(t)?.[1]; return l === undefined ? undefined : named(l); }; const labelOf = (t: string) => { - const field = SEVERITY_FIELD.exec(t)?.[1]; + const field = fieldOf(t); if (field === undefined) return strongest(t); - if (critical(t)) return 'CRITICAL'; - const lead = leadOf(t); - return lead ? stronger(lead, field) : field; + return critical(t) ? 'CRITICAL' : strongestOf([leadOf(t), field])!; }; const push = (label: string, raw: string, fields: string[] = [], lead?: string): Finding => { const f = { label, raw, fields: new Set(fields), list: LIST_ITEM.test(raw), lead }; out.push(f); return f; }; for (const line of output.replace(/\r\n/g, '\n').split('\n')) { @@ -132,7 +140,7 @@ export function parseFindings(reader: string, output: string): Array<{ label: st if (current && !current.fields.has(field)) { current.fields.add(field); if (blocks) current.label = 'CRITICAL'; - else if (field === 'severity' && SEVERITY_FIELD.test(t) && !critical(current.raw)) current.label = current.lead ? stronger(current.lead, label!) : label!; + else if (field === 'severity' && fieldOf(t) !== undefined && !critical(current.raw)) current.label = strongestOf([current.lead, label])!; else if (label) current.label = stronger(current.label, label); current.raw += ` | ${t}`; } else if (blocks || label) current = push(blocks ? 'CRITICAL' : label!, head ? `${head} | ${t}` : t, [field]); @@ -155,8 +163,7 @@ export function parseFindings(reader: string, output: string): Array<{ label: st const inherited = section && listItem ? section : undefined; const found = label && inherited ? stronger(inherited, label) : label ?? inherited; if (found) { - const lead = leadOf(t); - current = push(blocks ? 'CRITICAL' : found, t, [], lead && inherited ? stronger(inherited, lead) : lead ?? inherited); + current = push(blocks ? 'CRITICAL' : found, t, [], strongestOf([leadOf(t), inherited, fieldOf(t)])); if (!indented) head = t; continue; } diff --git a/test/review-dispatch.test.ts b/test/review-dispatch.test.ts index 08d8d8625..06ce49120 100644 --- a/test/review-dispatch.test.ts +++ b/test/review-dispatch.test.ts @@ -673,6 +673,14 @@ describe('gstack-review-dispatch sent, collect, judge and verify: nothing change expect([output, got, got.map(blocking)]).toEqual([output, [label], [true]]); } expect(parseFindings('outside-voice', '1. The P0 pilot table drops a row (t.md:3)').map(f => f.label)).toEqual(['P0']); + // Review of this fix (gst976-p1): a locus-first or emoji-led label, an inline field the next field line follows, a second field on + // the line, and prose that only mentions a severity never lower a blocker the reader labeled. + const kept: Array<[string, string]> = [['1. api.ts:10 CRITICAL: missing auth. Severity: Low', 'CRITICAL'], ['1. api.ts:10 CRITICAL: missing auth\n Severity: Low', 'CRITICAL'], + ['1. 🔴 CRITICAL: missing auth. Severity: Low', 'CRITICAL'], ['1. Token leak in auth.ts:3 lets anyone read sessions. Severity: Critical\n\nSeverity: Low', 'Critical'], + ['- auth.ts:3 token leak. Severity: Low (pre-exploit), Severity: Critical (once exposed)', 'Critical'], + ['- Missing auth on DELETE /users lets any user wipe accounts (CRITICAL); the old ticket filed it at severity low.', 'CRITICAL'], + ['- api.ts:10 lets any user wipe accounts. Earlier triage said Severity: Low; it is CRITICAL.', 'CRITICAL']]; + for (const [output, label] of kept) expect([output, parseFindings('outside-voice', output).map(f => f.label)]).toEqual([output, [label]]); }); // Value: protects=a non-review the same dispatch later answers with a review is voided (counted, VERIFY clean), and a failed outside