mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 03:48:06 +01:00
fix(review): lead judgment may lower a native finding to the Severity it stated beside Blocks: no (GST-1406) (#4017)
* fix(review): lead judgment may lower a native finding to the Severity it stated beside Blocks: no (GST-1406)
Collect keeps the FIXABLE raise on a native adversarial finding whose
Severity and Blocks: no follow prose, because it can't tell a finding's
own fields from another subject's. judge() and ledgerBlockers() now let
lead judgment lower such a finding to its one stated positive-rank
Severity, never below it, only with a reason, recorded as dismissed.
Carried from gst-1406-fixable-blocks-no (daf4fc2) without the GST-1409
fence pre-scan.
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* fix: pre-landing review fixes
verify checks each merged row's reader against the output it came from,
and ledgerBlockers takes the reader from the output event, so a row
relabelled adversarial-native can't take the native lowering and replay
clean. The JUDGMENT_REJECTED hint names the native route; the fragment
says the blocker count is unchanged rather than that it follows.
Review-Finding: v1/outside-voice#1 reader=outside-voice
Review-Finding: v1/adversarial-native#2 reader=adversarial-native
Review-Finding: v1/adversarial-native#4 reader=adversarial-native
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
* chore: name the GST-1406 fragment after its branch
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:
1 parent
796cceaeb0
commit
ab54cbb5e1
5 files changed
+68
-6
No files matched your search
+11
-4
@@ -86,6 +86,11 @@ const LEAD_LABEL = new RegExp(`^(?:[^\\p{L}\\p{N}\\s]\\s*|\\d+[.)]\\s*)*(?:\\w[\
|
||||
const STATED_BLOCKS = /(?<![A-Za-z0-9])Blocks\**\s*:?\s*\**\s*(yes|no)(?![A-Za-z0-9])/gi;
|
||||
/** A prose reader's `Blocks` field as it can lower a count (GST-1031): its colon required, so prose ("blocks no other writer") never does, and a JSON finding (no Blocks field in its schema) never has one. */
|
||||
const BLOCKS_FIELD = /(?<![A-Za-z0-9])Blocks\**\s*:\s*\**\s*(yes|no)(?![A-Za-z0-9-])/gi;
|
||||
/** A native voice's finding whose reader stated `Blocks: no` and one Severity (GST-1031 ruling A): lead judgment may lower it to that Severity, never below, on record with a reason (GST-1406). */
|
||||
const statedLowering = (reader: string, raw: string, to: number) => {
|
||||
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);
|
||||
};
|
||||
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'); };
|
||||
/**
|
||||
* A summary candidate (GST-1031): a prose finding whose every severity word is denied ("so nothing is CRITICAL", "No CRITICAL issues")
|
||||
@@ -339,7 +344,7 @@ export function judge(merged: MergedLine[], judged: JudgedLine[]): Judgment {
|
||||
const from = rank(m.label) ?? 0;
|
||||
const to = rank(target.severity);
|
||||
if (to === null) continue;
|
||||
if (to < from) (!j.duplicate_of && j.reason?.trim() && summaryCandidate(m.raw) ? res.dismissed : res.downgraded).push(m.id);
|
||||
if (to < from) (!j.duplicate_of && j.reason?.trim() && (summaryCandidate(m.raw) || statedLowering(m.reader, m.raw, to)) ? res.dismissed : res.downgraded).push(m.id);
|
||||
else if (j.duplicate_of) res.folded.push(m.id);
|
||||
else if (to > from) { res.promoted.push(m.id); if (!j.reason?.trim()) res.unexplained.push(m.id); }
|
||||
}
|
||||
@@ -408,6 +413,7 @@ export function ledgerBlockers(ledger: string): LedgerBlocker[] {
|
||||
const ranks = new Map<string, number>();
|
||||
const unblocking = new Map<string, number>();
|
||||
const raws = new Map<string, string>();
|
||||
const readers = new Map<string, string>();
|
||||
const loci = new Map<string, string | null>();
|
||||
for (const o of events.filter(e => e.event === 'output' && Array.isArray(e.ids) && e.ids.length)) {
|
||||
const lines = jsonLines(read(o.merged, `merged file of ${String(o.reader)}`), String(o.merged)) as MergedLine[];
|
||||
@@ -416,6 +422,7 @@ export function ledgerBlockers(ledger: string): LedgerBlocker[] {
|
||||
ranks.set(id, rank(line.label) ?? 0);
|
||||
if (statesNoBlocks(line.raw)) unblocking.set(id, rank(line.label) ?? 0);
|
||||
raws.set(id, line.raw);
|
||||
readers.set(id, String(o.reader));
|
||||
loci.set(id, lineLocus(line.raw));
|
||||
}
|
||||
}
|
||||
@@ -425,7 +432,7 @@ export function ledgerBlockers(ledger: string): LedgerBlocker[] {
|
||||
if (promptHash(text) !== verdict.judged_sha256) fail(`judged set ${String(verdict.judged)} changed since judge`);
|
||||
for (const j of jsonLines(text, String(verdict.judged)) as JudgedLine[]) {
|
||||
if (j.duplicate_of) { ranks.delete(j.id); continue; }
|
||||
const dismissed = !!j.reason?.trim() && summaryCandidate(raws.get(j.id) ?? '');
|
||||
const dismissed = !!j.reason?.trim() && (summaryCandidate(raws.get(j.id) ?? '') || statedLowering(readers.get(j.id) ?? '', raws.get(j.id) ?? '', rank(j.severity) ?? 0));
|
||||
ranks.set(j.id, dismissed ? rank(j.severity) ?? 0 : Math.max(ranks.get(j.id) ?? 0, rank(j.severity) ?? 0));
|
||||
if (!loci.has(j.id)) loci.set(j.id, lineLocus(j.reason ?? ''));
|
||||
}
|
||||
@@ -510,14 +517,14 @@ export function verify(ledger: string): { rows: VerifyRow[]; clean: boolean; bri
|
||||
try { lines = typeof o.merged === 'string' ? readJsonl<MergedLine>(o.merged).filter(l => ids.includes(l.id)) : []; } catch { notes.push('merged_unreadable'); status = 'MISMATCH'; }
|
||||
if (lines.length !== ids.length) { status = 'MISMATCH'; notes.push(`merged_lines=${lines.length}/${ids.length}`); }
|
||||
if (lines.some(l => l.output_sha256 !== o.sha256)) { status = 'MISMATCH'; notes.push('merged_line_from_other_output'); }
|
||||
// Each merged row must be the finding collect parses at that id's index from the unchanged output, label and raw text byte for byte.
|
||||
// Each merged row must be the finding collect parses at that id's index from the unchanged output, its reader, label and raw text byte for byte.
|
||||
if (text === undefined) {
|
||||
status = 'MISMATCH';
|
||||
notes.push(typeof o.file === 'string' ? 'output_file_missing' : 'output_file_unrecorded');
|
||||
differs.push(...ids);
|
||||
} else if (promptHash(text) === o.sha256) {
|
||||
const parsed = parseFindings(reader, text, o.fixable_raises !== false);
|
||||
const off = ids.filter((id, k) => { const m = lines.find(l => l.id === id); return !m || parsed[k]?.label !== m.label || parsed[k]?.raw !== m.raw; });
|
||||
const off = ids.filter((id, k) => { const m = lines.find(l => l.id === id); return !m || m.reader !== reader || parsed[k]?.label !== m.label || parsed[k]?.raw !== m.raw; });
|
||||
if (unavailableOutput(o) && parsed.length) { status = 'MISMATCH'; notes.push('unavailable_output_has_findings'); }
|
||||
else if (!unavailableOutput(o)) {
|
||||
if (off.length) { status = 'MISMATCH'; notes.push(`merged_line_differs=${JSON.stringify(off)}`); differs.push(...off); }
|
||||
|
||||
Reference in new issue
Block a user