Keep a numbered review finding's closing paragraph on that finding after a nested list (GST-2174) (#4237)

* Keep a numbered review finding's closing paragraph on that finding after a nested list (GST-2174)

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Keep the nested-list join from lowering a separate finding or a CRITICAL item

The join that keeps a numbered native finding's closing paragraph now ends after that first paragraph, skips bold-led margin paragraphs and verdict lines, applies only to the native voice's items, and never lowers an item that names CRITICAL or P0. Each of these let a separate finding's Severity and Blocks: no lower a FIXABLE blocker to Low (GST-2174).

Review-Finding: review/specialist-batched#1 reader=testing
Review-Finding: review/red-team#2 reader=red-team
Review-Finding: review/red-team#3 reader=red-team
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* Describe the numbered-finding nested-list rule and add its changelog entry

docs/review-policy.md now says how the collector keeps a native numbered finding's first closing paragraph after a nested list (GST-2174).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-11-09 21:43:04 +00:00
1 parent a58e6e27c6
commit dcebc44fa7
4 files changed
+111 -3

No files matched your search

+22 -2
View File
@@ -179,7 +179,11 @@ export interface MergedLine { id: string; reader: string; label: string; stated?
* under it, located or listed or bold, until its fields come (GST-1031), so its Severity and Blocks never split off as a second record;
* the line right under it that carries those fields, no other label and no file:line joins it at the stronger label, as a paragraph after
* a blank line does (a located line there is another finding), unless the heading itself names CRITICAL or P0, whose blocker another
* subject's Blocks: no must not lift. A prose line that only names a severity in passing is a summary (summaryLine).
* subject's Blocks: no must not lift. The native voice's numbered list item ("1. **FIXABLE. …**") with no fields of its own owns, after an
* unlabelled nested list, its indented lines and the first paragraph after a blank line (ALPH-93), until its fields come; their Severity
* and Blocks bind to it as a field line's do, except that an item naming CRITICAL or P0 is never lowered. A list item, heading, lead label,
* file:line, bold margin lead, verdict line or any later margin paragraph there starts the next finding.
* A prose line that only names a severity in passing is a summary (summaryLine).
* A native FIXABLE finding that states `Blocks: no` and one Severity field collects at that field on every class (GST-1031 ruling A);
* one with no Blocks: no, no Severity field or only a FIXABLE field stays CRITICAL. fixableRaises false (a code or docs review with no
* risk, error, history or floor reason, GST-987) also drops FIXABLE's raise from label headings and leads.
@@ -188,7 +192,7 @@ export interface MergedLine { id: string; reader: string; label: string; stated?
* nothing; elsewhere it collects as above. A line led by a bold, unlabelled lead ("**1. Coverage:** …") is read for a closing summary after it.
*/
function parseLabelled(reader: string, output: string, fixableRaises: boolean): Array<{ label: string; raw: string; shaped: boolean }> {
type Finding = { label: string; raw: string; fields: Set<string>; list: boolean; lead?: string; fixable?: boolean; heading?: boolean; section?: boolean; context?: string };
type Finding = { label: string; raw: string; fields: Set<string>; list: boolean; lead?: string; fixable?: boolean; heading?: boolean; item?: boolean; nested?: boolean; closed?: boolean; section?: boolean; context?: string };
const out: Finding[] = [];
let section: string | null = null;
let current: Finding | null = null;
@@ -269,6 +273,21 @@ function parseLabelled(reader: string, output: string, fixableRaises: boolean):
else if (label) current.label = stronger(current.label, label);
continue;
}
if (current?.item && !current.fields.size && !HEADING.test(t) && !NUMBERED_HEAD.test(t) && !t.startsWith('{')) {
if (indented && LIST_ITEM.test(t) && !label && !blocks) { current.nested = true; continue; }
if (current.nested && (indented || (afterGap && !t.startsWith('**'))) && !(afterGap && current.closed) && !LIST_ITEM.test(t) && !LEAD_LABEL.test(t)
&& !LOCUS.test(t) && !quietSummary(t)) {
const pinned = [...current.raw.split(' | ')[0]!.matchAll(ALL_LABELS)].some(m => rank(m[1]) === 4);
current.raw += ` | ${t}`;
if (afterGap) current.closed = true;
if (blocks) current.label = 'CRITICAL';
else if (field !== undefined && !critical(current.raw) && !pinned) current.label = strongestOf([current.lead, label])!;
else if (label) current.label = stronger(current.label, label);
if (field !== undefined) current.fields.add('severity');
if (t.match(BLOCKS_FIELD)) current.fields.add('blocks');
continue;
}
}
const fresh = (listItem && !detail) || (!indented && !detail && (t.startsWith('**') || afterGap)) || (!!current?.fields.size && locus);
if (fresh) { current = null; head = t; }
const inherited = section && listItem ? section : undefined;
@@ -279,6 +298,7 @@ function parseLabelled(reader: string, output: string, fixableRaises: boolean):
current.section = !!inherited;
if (continued) current.context = before;
current.heading = !indented && NUMBERED_HEAD.test(t);
current.item = role === 'adversarial-native' && !indented && /^\d+[.)]\s/.test(t) && field === undefined && !t.match(BLOCKS_FIELD);
if (!indented) head = t;
continue;
}