fix(review): the workflow review fails closed when its coordinator returns nothing
Review finding edf2ec56; every reviewer that did not run is named in allow_blocked_by. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
9529df7665
commit
25856f2e9c
4 changed files with 166 additions and 8 deletions
|
|
@ -21,7 +21,9 @@
|
|||
// Ordre 5: the Claude Code engine payloads (Phase 4.7, run in prose before this
|
||||
// script) are merged with the reviewers' findings; a reviewer or engine that
|
||||
// returned nothing forbids ALLOW (fail-closed, as on the prose path); and no
|
||||
// finding is struck for length.
|
||||
// finding is struck for length. A coordinator that returns no verdict and
|
||||
// findings counts as a missing reviewer, and every reviewer, engine and
|
||||
// coordinator that did not deliver is named in `allow_blocked_by` (review edf2ec56).
|
||||
//
|
||||
// Run via the Workflow tool: Workflow({ scriptPath: this file, args }).
|
||||
// The structured result is logged as a single JSON line (prefix RESULT_JSON:)
|
||||
|
|
@ -217,9 +219,12 @@ const reviewerThunks = [
|
|||
agentType: 'voyage:code-correctness-reviewer', schema: FINDINGS_SCHEMA, label: 'correctness', phase: 'Review',
|
||||
}),
|
||||
];
|
||||
// The reviewer agent names in run order, so a reviewer that returned nothing is named.
|
||||
const reviewerNames = a.quick ? ['code-correctness-reviewer'] : ['brief-conformance-reviewer', 'code-correctness-reviewer'];
|
||||
const toRun = a.quick ? [reviewerThunks[1]] : reviewerThunks;
|
||||
const rawResults = await parallel(toRun);
|
||||
const reviewerResults = rawResults.filter(Boolean);
|
||||
const missingReviewerNames = reviewerNames.filter((_, i) => !rawResults[i]);
|
||||
// A reviewer that returned nothing is an absent one, not a clean one. The
|
||||
// engines that were due are counted from what actually arrived, never from
|
||||
// what the caller remembered to list as not run.
|
||||
|
|
@ -227,7 +232,7 @@ const enginePayloads = Array.isArray(a.enginePayloads) ? a.enginePayloads.filter
|
|||
const expectedEngines = a.quick ? ['claude-code-review'] : ['claude-code-review', 'claude-security-review'];
|
||||
const arrivedEngines = new Set(enginePayloads.map((p) => p.reviewer));
|
||||
const missingEngines = expectedEngines.filter((name) => !arrivedEngines.has(name));
|
||||
const missingReviewers = (rawResults.length - reviewerResults.length) + missingEngines.length;
|
||||
let missingReviewers = (rawResults.length - reviewerResults.length) + missingEngines.length;
|
||||
// Provenance, before dedup: "came from an engine" is read from the source,
|
||||
// never from an `engine` field. A reviewer finding loses any `engine` it
|
||||
// carries; an engine payload's findings get `engine` from its `reviewer`
|
||||
|
|
@ -260,10 +265,14 @@ if (merged.length === 0) {
|
|||
agentType: 'voyage:review-coordinator', schema: VERDICT_SCHEMA, label: 'coordinator', phase: 'Coordinate',
|
||||
});
|
||||
}
|
||||
// A coordinator that returns no verdict and findings is a missing reviewer, not
|
||||
// a clean one (the zero-findings skip above always sets a valid verdict).
|
||||
const coordinatorMissing = merged.length > 0 && !(coord && ['BLOCK', 'WARN', 'ALLOW'].includes(coord.verdict) && Array.isArray(coord.findings));
|
||||
if (coordinatorMissing) missingReviewers += 1;
|
||||
|
||||
// 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;
|
||||
const returned = coordinatorMissing ? merged : coord.findings;
|
||||
// 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
|
||||
|
|
@ -280,12 +289,23 @@ const returned = Array.isArray(coord?.findings) ? coord.findings : merged;
|
|||
// 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.
|
||||
// A coordinator that failed (no verdict and findings) is not trusted with a
|
||||
// partial list: `returned` is then every merged finding, each first raised to
|
||||
// its catalogue tier when the key is known (never lowered), as the default
|
||||
// path's Pass 3 would reset it; that raise stands after `raiseTo` is declared.
|
||||
const counterparts = new Map(merged
|
||||
.filter((m) => m && typeof m === 'object')
|
||||
.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);
|
||||
if (coordinatorMissing) {
|
||||
for (const f of returned) {
|
||||
if (!f || typeof f !== 'object') continue;
|
||||
const tier = tierOf(f.rule_key);
|
||||
if (tier !== null) raiseTo(f, tier);
|
||||
}
|
||||
}
|
||||
for (const f of returned) {
|
||||
if (!f || typeof f !== 'object') continue;
|
||||
const c = counterparts.get(tripletOf(f));
|
||||
|
|
@ -321,15 +341,23 @@ for (const f of putBack) {
|
|||
returned.push(copy);
|
||||
}
|
||||
const has = (sev) => returned.some((f) => f && f.severity === sev);
|
||||
let verdict = coord?.verdict ?? null;
|
||||
let verdict = coordinatorMissing ? (coord?.verdict === 'BLOCK' ? 'BLOCK' : 'WARN') : coord.verdict;
|
||||
if (has('BLOCKER')) verdict = 'BLOCK';
|
||||
else if (has('MAJOR') && verdict !== 'BLOCK') verdict = 'WARN';
|
||||
if (verdict === 'ALLOW' && missingReviewers > 0) verdict = 'WARN';
|
||||
|
||||
// Every reviewer, engine and coordinator that did not deliver, in the form the
|
||||
// default path writes and lib/review/verdict-gate.mjs counts as not run.
|
||||
const allowBlockedBy = [
|
||||
...missingReviewerNames.map((name) => `missing-reviewer:${name}`),
|
||||
...missingEngines.map((name) => `missing-reviewer:${name}`),
|
||||
...(coordinatorMissing ? ['missing-reviewer:review-coordinator'] : []),
|
||||
];
|
||||
const result = {
|
||||
verdict,
|
||||
missing_reviewers: missingReviewers,
|
||||
missing_engines: missingEngines,
|
||||
allow_blocked_by: allowBlockedBy,
|
||||
findings: returned,
|
||||
raw_reviewer_count: reviewerResults.length,
|
||||
raw_finding_count: allFindings.length,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue