fix(review): the workflow floor and put-back match each finding to its own row
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
a5fcb5e8b4
commit
773aa44dc9
2 changed files with 194 additions and 30 deletions
|
|
@ -56,6 +56,10 @@ const sevRank = (s) => (SEVERITIES.includes(s) ? SEVERITIES.indexOf(s) : SEVERIT
|
|||
const tierOf = (key) => (typeof key === 'string' && Object.prototype.hasOwnProperty.call(RULE_TIERS, key) ? RULE_TIERS[key] : null);
|
||||
// The (file,line,rule_key) triplet a finding is deduped and matched on.
|
||||
const tripletOf = (f) => `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
|
||||
// The row a finding is matched to after the coordinator: its triplet plus its
|
||||
// title, normalized as `sameTitle` below reads it (trimmed, lower case). An
|
||||
// untitled finding gets a key no titled row has.
|
||||
const rowOf = (f) => `${tripletOf(f)}\u0000${f.title === undefined ? '\u0001' : String(f.title).trim().toLowerCase()}`;
|
||||
|
||||
// Findings schema — mirrors lib/review/findings-schema.mjs (NW1). The enum on
|
||||
// rule_key enforces the catalogue at the StructuredOutput layer (stronger than
|
||||
|
|
@ -144,7 +148,8 @@ const sameTitle = (a, b) => a.title === undefined || b.title === undefined ||
|
|||
// findings in canonical order, the untitled findings last), so a full tie on
|
||||
// the survivor never follows arrival order either; a triplet's groups keep the
|
||||
// order of their earliest input index. With `perSource: false` everything on a
|
||||
// triplet is one group, as before.
|
||||
// triplet is one group, as before. A `rows` Map, when given, is filled with
|
||||
// each row written -> the findings of its group (the put-back reads it).
|
||||
const sourceOf = new Map();
|
||||
const cmpStr = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
||||
function groupTriplet(entries) {
|
||||
|
|
@ -180,7 +185,7 @@ function groupTriplet(entries) {
|
|||
.sort((a, b) => a.first - b.first)
|
||||
.map((g) => g.members);
|
||||
}
|
||||
function dedupByTriplet(findings, { perSource = true } = {}) {
|
||||
function dedupByTriplet(findings, { perSource = true, rows = null } = {}) {
|
||||
const byTriplet = new Map();
|
||||
findings.forEach((f, i) => {
|
||||
const key = tripletOf(f);
|
||||
|
|
@ -213,7 +218,9 @@ function dedupByTriplet(findings, { perSource = true } = {}) {
|
|||
for (const f of group) {
|
||||
if (sevRank(f.severity) < sevRank(severity)) severity = f.severity;
|
||||
}
|
||||
out.push(severity === survivor.severity ? survivor : { ...survivor, severity });
|
||||
const row = severity === survivor.severity ? survivor : { ...survivor, severity };
|
||||
if (rows) rows.set(row, group);
|
||||
out.push(row);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
|
@ -315,7 +322,8 @@ const allFindings = [
|
|||
...enginePayloads.flatMap((p) => (Array.isArray(p?.findings) ? p.findings : [])
|
||||
.map((f) => tagged(withEngine(f, engineOf(p?.reviewer)), `engine:${p?.reviewer}`))),
|
||||
];
|
||||
const merged = dedupByTriplet(allFindings);
|
||||
const rowMembers = new Map();
|
||||
const merged = dedupByTriplet(allFindings, { rows: rowMembers });
|
||||
log(`Arm B: ${reviewerResults.length} reviewer(s); ${allFindings.length} raw → ${merged.length} after triplet-dedup`);
|
||||
|
||||
phase('Coordinate');
|
||||
|
|
@ -337,28 +345,51 @@ if (coordinatorMissing) missingReviewers += 1;
|
|||
// coordinator returned, so an LLM-written ALLOW over a BLOCKER cannot pass.
|
||||
const returned = coordinatorMissing ? merged : coord.findings;
|
||||
// The floor reads two inputs, never the labels the coordinator wrote:
|
||||
// - `merged` (the coordinator's input, same file/line/rule_key) for each
|
||||
// returned finding's counterpart: (a) an engine counterpart raises it to the
|
||||
// - `merged` (the coordinator's input, one finding per row) for each returned
|
||||
// finding's counterpart, its own row: the row with its `rowOf` key (triplet
|
||||
// plus normalized title); with no title match, the only row on its triplet,
|
||||
// else the strictest row there (highest tier, then highest severity; the
|
||||
// first in `merged` on a tie), never a lower one. `engine` is taken from
|
||||
// that row only. (a) an engine counterpart raises it to the
|
||||
// engine's severity, unless the one legal lowering; (b) a reviewer
|
||||
// counterpart, labelled or not, raises it to the lower of the counterpart's
|
||||
// asserted severity and its catalogue tier (the coordinator may reset a
|
||||
// finding to its tier, never below; for an unknown key the bound is the
|
||||
// counterpart's severity), and a labelled finding is raised as before.
|
||||
// - `allFindings` (before dedup, so an engine finding a reviewer finding
|
||||
// shadowed in its triplet counts too) for the put-back: (c) every engine
|
||||
// finding with no returned finding at its exact triplet is appended at its
|
||||
// own severity, one copy per triplet (the higher severity, else the first
|
||||
// seen) (operator decision 23, review ab1b5d9f). So the coordinator's drop,
|
||||
// move or relabel of an engine finding no longer lowers the verdict.
|
||||
// - the rows' members (before dedup, so an engine finding a reviewer finding
|
||||
// shadowed in its row counts too) for the put-back: (c) each engine finding
|
||||
// in a row whose `rowOf` key no returned finding carries is
|
||||
// put back at its own severity, one copy per row (the higher severity, else
|
||||
// the first seen) (operator decision 23, review ab1b5d9f). So the coordinator's drop,
|
||||
// move, rename or relabel of an engine finding no longer lowers the verdict
|
||||
// (a renamed row is put back beside the renamed finding: a duplicate, the
|
||||
// stricter side).
|
||||
// The raises and put-backs are written into `returned`, the marks kept, so the
|
||||
// rendered review.md carries them.
|
||||
// A coordinator that failed (no verdict and findings) is not trusted with a
|
||||
// partial list: `returned` is then every merged finding, each first raised to
|
||||
// its catalogue tier when the key is known (never lowered), as the default
|
||||
// path's Pass 3 would reset it; that raise stands after `raiseTo` is declared.
|
||||
const counterparts = new Map(merged
|
||||
.filter((m) => m && typeof m === 'object')
|
||||
.map((m) => [tripletOf(m), m]));
|
||||
const rowsOn = (map, key, m) => {
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key).push(m);
|
||||
};
|
||||
const counterparts = new Map();
|
||||
const rowsOnTriplet = new Map();
|
||||
for (const m of merged) {
|
||||
if (!m || typeof m !== 'object') continue;
|
||||
rowsOn(counterparts, rowOf(m), m);
|
||||
rowsOn(rowsOnTriplet, tripletOf(m), m);
|
||||
}
|
||||
const rowTier = (m) => sevRank(tierOf(m.rule_key) ?? m.severity);
|
||||
const strictest = (rows) => rows.reduce((best, m) => (rowTier(m) < rowTier(best) ||
|
||||
(rowTier(m) === rowTier(best) && sevRank(m.severity) < sevRank(best.severity)) ? m : best));
|
||||
const counterpartOf = (f) => {
|
||||
const own = counterparts.get(rowOf(f));
|
||||
if (own) return strictest(own);
|
||||
const onTriplet = rowsOnTriplet.get(tripletOf(f));
|
||||
return onTriplet ? strictest(onTriplet) : null;
|
||||
};
|
||||
const present = (f, field) => f[field] !== undefined && f[field] !== null;
|
||||
const raiseTo = (f, sev) => { if (sevRank(sev) < sevRank(f.severity)) f.severity = sev; };
|
||||
const lowerOf = (s1, s2) => (sevRank(s1) >= sevRank(s2) ? s1 : s2);
|
||||
|
|
@ -371,7 +402,7 @@ if (coordinatorMissing) {
|
|||
}
|
||||
for (const f of returned) {
|
||||
if (!f || typeof f !== 'object') continue;
|
||||
const c = counterparts.get(tripletOf(f));
|
||||
const c = counterpartOf(f);
|
||||
const labelled = present(f, 'engine') || present(f, 'contradicts_brief') || present(f, 'brief_lowered_from');
|
||||
if (c && c.engine) {
|
||||
// (a) an engine counterpart: raised to its severity, unless the one legal lowering.
|
||||
|
|
@ -395,11 +426,14 @@ for (const f of returned) {
|
|||
}
|
||||
}
|
||||
// (c) the put-back (decision 23).
|
||||
const returnedTriplets = new Set(returned.filter((f) => f && typeof f === 'object').map(tripletOf));
|
||||
// Engine findings only, so dedupByTriplet keeps the higher severity, else the
|
||||
// first seen: one copy per triplet (decision 23), so not split per source here.
|
||||
const putBack = dedupByTriplet(allFindings.filter((f) =>
|
||||
f && typeof f === 'object' && f.engine && !returnedTriplets.has(tripletOf(f))), { perSource: false });
|
||||
const returnedRows = new Set(returned.filter((f) => f && typeof f === 'object').map(rowOf));
|
||||
// Each row not returned: its engine findings only, so dedupByTriplet keeps the
|
||||
// higher severity, else the first seen: one copy per row, so not split per
|
||||
// source here.
|
||||
const putBack = merged
|
||||
.filter((m) => m && typeof m === 'object' && !returnedRows.has(rowOf(m)))
|
||||
.flatMap((m) => dedupByTriplet((rowMembers.get(m) ?? [m]).filter((f) =>
|
||||
f && typeof f === 'object' && f.engine), { perSource: false }));
|
||||
for (const f of putBack) {
|
||||
const { brief_lowered_from: _lowered, ...copy } = f;
|
||||
returned.push(copy);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue