fix(review): original_severity names what the kept finding asserted

Review finding 08deae68.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-14 11:09:01 +02:00
commit 9529df7665
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
2 changed files with 93 additions and 6 deletions

View file

@ -19,7 +19,11 @@
// What IS implemented, purely: Pass 1 (triplet dedup → survivor ranked by
// catalogue tier, then reviewer over engine, asserted severity, conformance,
// first seen; the survivor carries its group's highest asserted severity
// (review bc7f4e23) + detail concat + raised_by provenance),
// (review bc7f4e23) + detail concat + raised_by provenance; when that raises
// it, `pass1_raised_from` keeps what the survivor itself asserted, read by
// Pass 3 and never returned: `original_severity` is what the kept finding
// itself asserted, before Pass 1 raised it to its group's severity (review
// 08deae68)),
// Pass 2 succinctness (trims MINOR/SUGGESTION text, never removes) +
// actionability-presence, Pass 3 reasonableness
// (citation / unknown-rule_key suppression, severity-mismatch correction),
@ -98,6 +102,8 @@ export const UNVERIFIED_REASONS = Object.freeze([
// `/security-review` finding keep their severity and only carry the mark.
// The verdict gate (verdict-gate.mjs) imports `illegalBriefLowering`; the
// opt-in workflow review mirrors it. `original_severity` is never read here.
// `original_severity` is what the kept finding itself asserted, before Pass 1
// raised it to its group's severity (review 08deae68).
// The payload `reviewer` names lib/review/engine-findings.mjs writes, and the
// engine each one stands for.
@ -303,9 +309,13 @@ export function dedupByTriplet(findings) {
}
}
const id = computeFindingId(survivor.file, survivor.line, survivor.rule_key);
// When the group raised the survivor, Pass 3 still needs what the survivor
// itself asserted for `original_severity` (review 08deae68).
const raised = severityRank(severity) < severityRank(survivor.severity);
out.push({
...survivor, ...(severity !== undefined ? { severity } : {}), id,
...(detail !== undefined ? { detail } : {}), raised_by,
...(raised ? { pass1_raised_from: survivor.severity } : {}),
});
}
return out;
@ -428,7 +438,10 @@ export function reasonablenessFilter(findings) {
// incoming one is removed; only an engine finding is ever marked, so a mark
// on any other finding is removed. Every finding written here then makes
// `illegalBriefLowering` false.
const { brief_lowered_from: _lowered, ...rest } = incoming;
// `pass1_raised_from` (Pass 1) names what the finding itself asserted
// before its group raised it; it is read here and never written out.
const { brief_lowered_from: _lowered, pass1_raised_from: raisedFrom, ...rest } = incoming;
const own = raisedFrom ?? rest.severity;
let f = rest;
if (!isEngineFinding(f) && 'contradicts_brief' in f) {
const { contradicts_brief: _mark, ...unmarked } = f;
@ -447,11 +460,16 @@ export function reasonablenessFilter(findings) {
if (lowerableBriefConflict(f)) {
// The one legal lowering: a marked /code-review MAJOR on a MAJOR-tier key.
kept.push({ ...f, severity: 'MINOR', brief_lowered_from: 'MAJOR' });
} else if (rule && f.severity !== rule.severity) {
} else if (rule) {
// Today's catalogue reset. A marked engine finding arrives at its tier
// (engine-findings.mjs), so it lands here only when its severity was
// changed on the way, and the reset puts it back with its mark kept.
kept.push({ ...f, severity: rule.severity, original_severity: f.severity });
// `original_severity` names what the kept finding itself asserted
// (`own`), never the severity Pass 1 lent it (review 08deae68).
kept.push({
...f, severity: rule.severity,
...(own !== rule.severity ? { original_severity: own } : {}),
});
} else {
kept.push(f);
}
@ -529,6 +547,12 @@ export function runContract(reviewerPayloads, options = {}) {
const deduped = dedupByTriplet(ingested);
const judged = judgeFilter(deduped);
const reasoned = reasonablenessFilter(judged.kept);
// Pass 2's drops and unverified come straight from Pass 1: its
// `pass1_raised_from` bookkeeping is removed there too, so no returned
// finding carries it (Pass 3 removes it from everything it sees).
const withoutPass1 = ({ pass1_raised_from: _raised, ...f }) => f;
const judgedDropped = judged.dropped.map(withoutPass1);
const judgedUnverified = judged.unverified.map(withoutPass1);
// Engine findings on files outside the review's SHA range (engine-findings.mjs
// `out_of_scope`). /security-review diffs against origin's default branch,
// wider than this review: a real claim nobody here judged, so it forbids
@ -543,7 +567,7 @@ export function runContract(reviewerPayloads, options = {}) {
const minorOnly = (f) => f.engine === 'code-review' && (f.severity === 'MINOR' || f.severity === 'SUGGESTION');
const outOfScopeBlocking = outOfScope.filter((f) => !minorOnly(f));
const outOfScopeNoted = outOfScope.filter(minorOnly);
const unverified = [...judged.unverified, ...reasoned.unverified, ...outOfScopeBlocking];
const unverified = [...judgedUnverified, ...reasoned.unverified, ...outOfScopeBlocking];
// What the coordinator set aside without ruling on it, one line each, so a
// reader sees every finding that did not reach a verdict tier. Borrowed:
// "Declined to judge … nothing you set aside is dropped silently"
@ -591,7 +615,7 @@ export function runContract(reviewerPayloads, options = {}) {
verdict,
counts,
findings: reasoned.kept,
suppressed: [...judged.dropped, ...judged.unverified, ...reasoned.dropped, ...reasoned.unverified],
suppressed: [...judgedDropped, ...judgedUnverified, ...reasoned.dropped, ...reasoned.unverified],
unverified,
skipped,
missing_reviewers,

View file

@ -611,3 +611,66 @@ test('own property — toReviewerPayload refuses engine toString with the "engin
assert.equal(r.errors.length, 1);
assert.match(r.errors[0], /^engine must be one of code-review\|security-review, got "toString"$/);
});
// ---- original_severity names what the kept finding asserted (review 08deae68) --
//
// Pass 1 writes the survivor at its group's highest asserted severity. When
// Pass 3 then resets it to the catalogue tier, `original_severity` must name
// what the kept finding itself asserted, never the severity a duplicate lent
// it. The Pass-1 bookkeeping never leaves the contract.
// A reviewer copy at `reviewerSeverity` and an engine CODE_REVIEW_DEFECT at
// `engineSeverity` on one triplet, in `order`.
function reviewerAndEngine(order, reviewerSeverity, engineSeverity, reviewerExtra = {}) {
const reviewerPayload = { reviewer: 'code-correctness-reviewer', findings: [reviewerCopy({ severity: reviewerSeverity, ...reviewerExtra })] };
const enginePayload = { reviewer: 'claude-code-review', findings: [codeReviewDefect({ severity: engineSeverity })] };
return order === 'reviewer first' ? [reviewerPayload, enginePayload] : [enginePayload, reviewerPayload];
}
function assertNoPass1Field(result) {
for (const f of [...result.findings, ...result.suppressed, ...result.unverified]) {
assert.equal('pass1_raised_from' in f, false, `pass1_raised_from leaked: ${JSON.stringify(f)}`);
}
assert.equal(JSON.stringify(result).includes('pass1_raised_from'), false,
'no part of the result carries pass1_raised_from');
}
for (const order of ['reviewer first', 'engine first']) {
test(`08deae68 — a reviewer MAJOR raised by an engine BLOCKER ends at MAJOR with no original_severity (${order}) → WARN`, () => {
const result = m.runContract(reviewerAndEngine(order, 'MAJOR', 'BLOCKER'));
assert.equal(result.findings.length, 1);
const out = result.findings[0];
assert.equal('engine' in out, false, 'the survivor is the reviewer finding');
assert.equal(out.severity, 'MAJOR', 'the catalogue tier of CODE_REVIEW_DEFECT');
assert.equal('original_severity' in out, false,
'the kept finding asserted MAJOR, its tier; the BLOCKER was the engine duplicate\'s');
assert.equal(result.verdict, 'WARN');
assertAllOutputLegal(result);
assertNoPass1Field(result);
});
test(`08deae68 — a reviewer MINOR raised by an engine MAJOR ends at MAJOR with original_severity MINOR (${order})`, () => {
const result = m.runContract(reviewerAndEngine(order, 'MINOR', 'MAJOR'));
assert.equal(result.findings.length, 1);
const out = result.findings[0];
assert.equal('engine' in out, false, 'the survivor is the reviewer finding');
assert.equal(out.severity, 'MAJOR');
assert.equal(out.original_severity, 'MINOR', 'the kept finding itself asserted MINOR');
assert.equal(result.verdict, 'WARN');
assertAllOutputLegal(result);
assertNoPass1Field(result);
});
test(`08deae68 — a raised survivor set aside by Pass 2 (empty recommended_action) carries no pass1_raised_from (${order})`, () => {
const result = m.runContract(reviewerAndEngine(order, 'MAJOR', 'BLOCKER', { recommended_action: '' }));
assert.equal(result.findings.length, 0);
assert.equal(result.unverified.length, 1);
const u = result.unverified[0];
assert.equal(u.suppressed_reason, 'actionability:empty');
assert.equal(u.reviewer, 'code-correctness-reviewer', 'the raised survivor is the reviewer finding');
assert.equal(u.severity, 'BLOCKER', 'Pass 1 raised it to its group\'s severity');
assert.equal(result.suppressed.length, 1);
assert.equal(result.verdict, 'WARN');
assertNoPass1Field(result);
});
}