fix(review): a review with a control that did not run stops a merge
Operator decision 2026-10-06. Before, a WARN that existed only because a reviewer or engine never reported (e.g. /security-review without an origin remote) mapped to verdict-gate exit 0, so a review nobody performed in full was green. Now `missing-reviewer:` / `unattributable-payload` in the JSON block's allow_blocked_by is exit 2 (could not decide, never green); a WARN for a MAJOR or a set-aside finding still passes with its reasons. A 1.1 review must carry allow_blocked_by. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
40f6ae1177
commit
22e792ce19
7 changed files with 72 additions and 14 deletions
|
|
@ -8,7 +8,10 @@
|
|||
// 0 = ALLOW or WARN
|
||||
// 1 = BLOCK
|
||||
// 2 = could not decide: missing/unreadable file, not a review, no verdict,
|
||||
// an unknown verdict, or a file that contradicts its own verdict.
|
||||
// an unknown verdict, a file that contradicts its own verdict, 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).
|
||||
// Exit 2 is NEVER green.
|
||||
//
|
||||
// Fail-closed reading: a missing `verdict` is exit 2, never ALLOW. A verdict
|
||||
|
|
@ -96,10 +99,25 @@ export function decideVerdict(text) {
|
|||
if (fmIds.size !== jsonIds.size || [...fmIds].some((id) => !jsonIds.has(id))) {
|
||||
return undecided('frontmatter findings: list and the JSON block finding ids differ', verdict);
|
||||
}
|
||||
const warnReasons = Array.isArray(payload.allow_blocked_by) ? payload.allow_blocked_by : [];
|
||||
// A review in which a reviewer or engine never reported was not performed in
|
||||
// full: that is "could not decide", not a passable warning (operator
|
||||
// decision 2026-10-06, ordre 5). A 1.1 review must carry the list.
|
||||
if (atLeastVersion(fm.review_version, 1, 1) && !Array.isArray(payload.allow_blocked_by)) {
|
||||
return undecided('trailing JSON block lacks allow_blocked_by (required from review_version 1.1)', verdict);
|
||||
}
|
||||
const warnReasons = Array.isArray(payload.allow_blocked_by) ? payload.allow_blocked_by.map(String) : [];
|
||||
const notRun = warnReasons.filter((w) => w.startsWith('missing-reviewer:') || w.startsWith('unattributable-payload'));
|
||||
if (notRun.length > 0) {
|
||||
return undecided(`a control did not run: ${notRun.join(', ')}`, verdict);
|
||||
}
|
||||
return { exit: EXIT.PASS, verdict, reason: `verdict ${verdict}`, warn_reasons: warnReasons };
|
||||
}
|
||||
|
||||
function atLeastVersion(v, major, minor) {
|
||||
const m = typeof v === 'string' ? v.match(/^(\d+)\.(\d+)$/) : null;
|
||||
return Boolean(m) && (Number(m[1]) > major || (Number(m[1]) === major && Number(m[2]) >= minor));
|
||||
}
|
||||
|
||||
const FENCE_RE = /^```[ \t]*([A-Za-z0-9_+-]*)[^\n]*\n[\s\S]*?^```[ \t]*$/gm;
|
||||
|
||||
/** Language tag of the LAST fenced block in `body` (lower-cased), '' for none, null when no fence. */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue