fix(review): a code-review finding against a brief Non-Goal may drop to MINOR, never a BLOCKER

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-10-12 18:04:36 +02:00
commit ce7700b71b
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q
5 changed files with 533 additions and 4 deletions

View file

@ -80,6 +80,79 @@ export const UNVERIFIED_REASONS = Object.freeze([
'unknown-rule_key',
'file-existence:indeterminate',
]);
// The brief-conflict mark (`contradicts_brief`) is in neither set on purpose:
// it is a field on a KEPT finding, never a `suppressed_reason`. As a reason it
// would fail closed through `classifySuppression`, land the finding in
// `unverified` and forbid ALLOW — the opposite of what the mark is for.
// ---- The brief-conflict rule ---------------------------------------------------
//
// The one place that decides when a finding marked as contradicting the brief
// (`contradicts_brief: "<the Non-Goal or stated choice, verbatim>"`) may drop
// to MINOR. Only an engine finding (`engine` `code-review` or `security-review`)
// is ever marked; only a `/code-review` finding at the MAJOR tier ever drops,
// to MINOR, writing `brief_lowered_from: 'MAJOR'`. A BLOCKER and any
// `/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.
const ENGINE_NAMES = Object.freeze(['code-review', 'security-review']);
function carries(f, field) {
return f?.[field] !== undefined && f?.[field] !== null;
}
function isMark(v) {
return typeof v === 'string' && v.trim().length > 0;
}
/**
* True when a finding, before the lowering, may drop to MINOR: a non-empty
* string mark, `engine: 'code-review'`, a known `rule_key` whose catalogue tier
* is MAJOR, and severity MAJOR. Anything else (missing engine, unknown key,
* non-string mark, a BLOCKER-tier or MINOR-tier key) → false.
* @param {object} f
* @returns {boolean}
*/
export function lowerableBriefConflict(f) {
return isMark(f?.contradicts_brief) &&
f.engine === 'code-review' &&
getRule(f.rule_key)?.severity === 'MAJOR' &&
f.severity === 'MAJOR';
}
function isLegalBriefLowering(f) {
return isMark(f?.contradicts_brief) &&
f.engine === 'code-review' &&
getRule(f.rule_key)?.severity === 'MAJOR' &&
f.severity === 'MINOR' &&
f.brief_lowered_from === 'MAJOR';
}
/**
* True when what the coordinator wrote is not the one legal shape and (a)
* carries `brief_lowered_from`, or (b) carries a mark without an engine, or
* (c) carries a mark below its catalogue tier, or (d) carries a mark with a
* missing or unknown `rule_key` (fail-closed), or (e) is an engine finding
* below its catalogue tier, marked or not. An unmarked reviewer finding (no
* `engine`) without `brief_lowered_from` is never judged by this rule.
* @param {object} f
* @returns {boolean}
*/
export function illegalBriefLowering(f) {
if (f === null || typeof f !== 'object') return false;
if (isLegalBriefLowering(f)) return false;
const marked = carries(f, 'contradicts_brief');
const engine = ENGINE_NAMES.includes(f.engine);
const rule = getRule(f.rule_key);
const belowTier = rule !== null && severityRank(f.severity) > severityRank(rule.severity);
if (carries(f, 'brief_lowered_from')) return true; // (a)
if (marked && !engine) return true; // (b)
if (marked && belowTier) return true; // (c)
if (marked && rule === null) return true; // (d)
if (engine && belowTier) return true; // (e)
return false;
}
/**
* Classify a suppression reason. Anything not declared refuting is
@ -165,6 +238,10 @@ export function dedupByTriplet(findings) {
}
const out = [];
for (const group of groups.values()) {
// A brief-conflict mark is not carried over from a group member to the
// survivor: when the higher-severity survivor is unmarked, the group stays
// unmarked and is judged at its full severity. Conservative on purpose —
// a mark can only lower, so dropping it never makes a verdict greener.
let survivor = group[0];
for (const f of group.slice(1)) {
const higher = severityRank(f.severity) < severityRank(survivor.severity);
@ -202,11 +279,13 @@ export const TRIM_MARKER = ' […trimmed]';
* The tier a finding will be judged at: the catalogue tier when the rule_key is
* known (Pass 3 resets to it anyway), else the reviewer-asserted severity. The
* trim decision reads this, so a reviewer cannot get a MAJOR trimmed by
* mislabelling it MINOR.
* mislabelling it MINOR. A finding the brief rule will lower (Pass 3) is
* judged at MINOR, the tier it will be counted at.
* @param {object} f
* @returns {string}
*/
function effectiveSeverity(f) {
if (lowerableBriefConflict(f)) return 'MINOR';
return getRule(f.rule_key)?.severity ?? f.severity;
}
@ -287,6 +366,10 @@ export function judgeFilter(findings) {
* checkable claim) and is dropped. `unknown-rule_key` does not: an ad-hoc key
* is a real defect wearing the wrong label — v5.1.1 high-effort mode already
* KEEPS these, normalised to PLAN_EXECUTE_DRIFT — so it routes to `unverified`.
*
* The brief rule runs here too: a finding `lowerableBriefConflict` accepts is
* set to MINOR with `brief_lowered_from: 'MAJOR'`; every other finding gets
* the catalogue reset above.
* @param {object[]} findings
* @returns {{ kept: object[], dropped: object[], unverified: object[] }}
*/
@ -294,7 +377,17 @@ export function reasonablenessFilter(findings) {
const kept = [];
const dropped = [];
const unverified = [];
for (const f of findings) {
for (const incoming of findings) {
// The brief rule, first: only this pass writes `brief_lowered_from`, so an
// 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;
let f = rest;
if (!ENGINE_NAMES.includes(f.engine) && 'contradicts_brief' in f) {
const { contradicts_brief: _mark, ...unmarked } = f;
f = unmarked;
}
if (typeof f.file !== 'string' || f.file.length === 0 ||
(typeof f.line === 'number' && f.line < 0)) {
suppress(f, 'no-citation', dropped, unverified);
@ -305,7 +398,13 @@ export function reasonablenessFilter(findings) {
continue;
}
const rule = getRule(f.rule_key);
if (rule && f.severity !== rule.severity) {
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) {
// 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 });
} else {
kept.push(f);

View file

@ -8,7 +8,9 @@
// 0 = ALLOW or WARN
// 1 = BLOCK
// 2 = could not decide: missing/unreadable file, not a review, no verdict,
// an unknown verdict, a file that contradicts its own verdict, or a
// an unknown verdict, a file that contradicts its own verdict, a
// finding lowered or marked against the brief outside the one legal
// shape (coordinator-contract `illegalBriefLowering`), or a
// WARN because a reviewer or engine did not run (`missing-reviewer:` /
// `unattributable-payload` in allow_blocked_by — operator decision
// 2026-10-06: a review not performed in full stops a merge).
@ -29,6 +31,7 @@
import { readFileSync, existsSync } from 'node:fs';
import { validateReviewContent, VERDICT_VALUES } from '../validators/review-validator.mjs';
import { fencedBlocks, isJsonBlock } from './findings-schema.mjs';
import { illegalBriefLowering } from './coordinator-contract.mjs';
import { isMain } from '../util/is-main.mjs';
export const EXIT = Object.freeze({ PASS: 0, BLOCK: 1, UNDECIDED: 2 });
@ -100,6 +103,13 @@ export function decideVerdict(text) {
if (blockerFindings > 0 || blockerCount > 0 || blockerSection > 0) {
return undecided(`verdict ${verdict} over BLOCKER finding(s) (json: ${blockerFindings}, counts: ${blockerCount}, ## Findings (BLOCKER): ${blockerSection})`, verdict);
}
// The brief-conflict rule (coordinator-contract `illegalBriefLowering`, the
// one place it lives): a finding lowered or marked outside the one legal
// shape means the verdict may be greener than the findings — never green.
const illegal = payload.findings.filter((f) => illegalBriefLowering(f)).map((f) => f.id);
if (illegal.length > 0) {
return undecided(`verdict ${verdict} over a brief-conflict lowering outside the one legal shape (finding ${illegal.join(', ')})`, verdict);
}
const fmIds = new Set(Array.isArray(fm.findings) ? fm.findings : []);
const jsonIds = new Set(payload.findings.map((f) => f.id));
if (fmIds.size !== jsonIds.size || [...fmIds].some((id) => !jsonIds.has(id))) {