fix(review): the workflow review ranks by tier and puts back every engine finding

Operator decision 23 (13.10); review findings ab1b5d9f and bc7f4e23 (workflow side).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-13 14:32:59 +02:00
commit 05f6b67be9
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 257 additions and 37 deletions

View file

@ -114,22 +114,47 @@ const VERDICT_SCHEMA = {
// Inlined dedup by (file,line,rule_key) — the JS-side replacement for the
// coordinator's pass-1 triplet dedup (T2 §5 / lib/review/plan-review-dedup.mjs
// pattern; scripts cannot import, so the triplet key is inlined). On a
// repeated triplet the higher severity stays; at equal severity a reviewer
// finding (no `engine`) stays over an engine finding, so an engine's mark never
// lowers what a reviewer agent asserted (decision 13, operator 12.10, as
// coordinator-contract Pass 1); otherwise the first-seen one stays.
// pattern; scripts cannot import, so the triplet key is inlined). The survivor
// order is coordinator-contract Pass 1's (review bc7f4e23): the catalogue tier
// first (for a catalogue key it always ties inside one triplet, since the
// rule_key is shared), then a reviewer finding (no `engine`) over an engine
// finding (decision 13, operator 12.10), then the higher asserted severity;
// the workflow has no conformance rule, so otherwise the first-seen one stays.
// The survivor carries the highest severity any finding in its group asserted,
// so an engine's mark never lowers what a reviewer agent asserted.
function dedupByTriplet(findings) {
const seen = new Map();
const groups = new Map();
for (const f of findings) {
const key = `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
if (!seen.has(key)) { seen.set(key, f); continue; }
const kept = seen.get(key);
const higher = sevRank(f.severity) < sevRank(kept.severity);
const tieToReviewer = sevRank(f.severity) === sevRank(kept.severity) && !f.engine && Boolean(kept.engine);
if (higher || tieToReviewer) seen.set(key, f);
if (!groups.has(key)) groups.set(key, []);
groups.get(key).push(f);
}
return [...seen.values()];
const tier = (f) => sevRank(tierOf(f.rule_key) ?? f.severity);
const out = [];
for (const group of groups.values()) {
let survivor = group[0];
for (const f of group.slice(1)) {
const tf = tier(f);
const ts = tier(survivor);
if (tf !== ts) {
if (tf < ts) survivor = f;
continue;
}
const ef = Boolean(f.engine);
const es = Boolean(survivor.engine);
if (ef !== es) {
if (!ef) survivor = f;
continue;
}
if (sevRank(f.severity) < sevRank(survivor.severity)) survivor = f;
}
let severity = survivor.severity;
for (const f of group) {
if (sevRank(f.severity) < sevRank(severity)) severity = f.severity;
}
out.push(severity === survivor.severity ? survivor : { ...survivor, severity });
}
return out;
}
function reviewerPrompt(kind, a) {
@ -237,33 +262,68 @@ if (merged.length === 0) {
// The verdict is mechanical: recompute its floor from the findings the
// coordinator returned, so an LLM-written ALLOW over a BLOCKER cannot pass.
const returned = Array.isArray(coord?.findings) ? coord.findings : merged;
// The brief-conflict floor: every returned finding is judged from its
// counterpart in the coordinator's input (`merged`, same file/line/rule_key),
// never from the labels the coordinator wrote. The raise is written into
// `returned`, the mark kept, so the rendered review.md carries it.
// 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
// 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 raises and put-backs are written into `returned`, the marks kept, so the
// rendered review.md carries them.
const tripletOf = (f) => `${f.file}\u0000${f.line}\u0000${f.rule_key}`;
const counterparts = new Map(merged
.filter((m) => m && typeof m === 'object')
.map((m) => [`${m.file}\u0000${m.line}\u0000${m.rule_key}`, m]));
.map((m) => [tripletOf(m), m]));
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);
for (const f of returned) {
if (!f || typeof f !== 'object') continue;
const c = counterparts.get(`${f.file}\u0000${f.line}\u0000${f.rule_key}`);
const c = counterparts.get(tripletOf(f));
const labelled = present(f, 'engine') || present(f, 'contradicts_brief') || present(f, 'brief_lowered_from');
if (c && c.engine) {
// (1) an engine counterpart: raised to its severity, unless the one legal lowering.
// (a) an engine counterpart: raised to its severity, unless the one legal lowering.
const legal = c.engine === 'code-review' && c.severity === 'MAJOR' && tierOf(c.rule_key) === 'MAJOR' &&
f.severity === 'MINOR' && typeof f.contradicts_brief === 'string' && f.contradicts_brief.trim().length > 0 &&
f.brief_lowered_from === 'MAJOR';
if (!legal) raiseTo(f, c.severity);
f.engine = c.engine;
} else if (labelled) {
// (2) a reviewer counterpart, (3) no counterpart: an unknown key raises to BLOCKER.
continue;
}
if (c) {
// (b) a reviewer counterpart: never below the lower of its asserted severity and the tier.
const tier = tierOf(c.rule_key);
raiseTo(f, tier === null ? c.severity : lowerOf(c.severity, tier));
}
if (labelled) {
// A labelled reviewer finding, or one with no counterpart: an unknown key raises to BLOCKER.
raiseTo(f, tierOf(f.rule_key) ?? 'BLOCKER');
if (c) raiseTo(f, c.severity);
delete f.engine;
}
}
// (c) the put-back (decision 23).
const returnedTriplets = new Set(returned.filter((f) => f && typeof f === 'object').map(tripletOf));
const putBack = new Map();
for (const f of allFindings) {
if (!f || typeof f !== 'object' || !f.engine) continue;
const key = tripletOf(f);
if (returnedTriplets.has(key)) continue;
const kept = putBack.get(key);
if (!kept || sevRank(f.severity) < sevRank(kept.severity)) putBack.set(key, f);
}
for (const f of putBack.values()) {
const { brief_lowered_from: _lowered, ...copy } = f;
returned.push(copy);
}
const has = (sev) => returned.some((f) => f && f.severity === sev);
let verdict = coord?.verdict ?? null;
if (has('BLOCKER')) verdict = 'BLOCK';