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:
parent
3009c5eddb
commit
ce7700b71b
5 changed files with 533 additions and 4 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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))) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue