fix(review): the verdict gate refuses duplicate finding ids
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
f1f5ab5528
commit
fc252f8d56
3 changed files with 226 additions and 0 deletions
|
|
@ -339,6 +339,12 @@ export function decideAnswers(text, { records = [], reviewSubject, preExisting =
|
|||
if (!payload) {
|
||||
return { ok: false, unanswered: [], fixedPending: [], reason: 'review-not-measured: no readable trailing JSON block' };
|
||||
}
|
||||
// One id twice would let one bound answer clear two MAJORs (fail closed).
|
||||
const ids = (Array.isArray(payload.findings) ? payload.findings : []).filter((f) => f && typeof f === 'object').map((f) => f.id);
|
||||
const dup = ids.find((id, i) => ids.indexOf(id) !== i);
|
||||
if (dup !== undefined) {
|
||||
return { ok: false, unanswered: [], fixedPending: [], reason: `review-not-measured: duplicate finding id ${dup}` };
|
||||
}
|
||||
const majors = countingMajors(payload, { preExisting });
|
||||
if (majors.length === 0) return { ok: true };
|
||||
const { rows } = parseAnswers(bodyOf(text));
|
||||
|
|
@ -498,6 +504,9 @@ export function recordAnswer({ reviewPath, finding, answer, repo, round, store,
|
|||
try { text = readFileSync(reviewPath, 'utf8'); } catch (e) { return cannot(`cannot read ${reviewPath}: ${e.message}`); }
|
||||
const payload = payloadOf(text);
|
||||
if (!payload || !Array.isArray(payload.findings)) return cannot(`${reviewPath} has no readable trailing JSON block`);
|
||||
const ids = payload.findings.filter((x) => x && typeof x === 'object').map((x) => x.id);
|
||||
const dup = ids.find((x, i) => ids.indexOf(x) !== i);
|
||||
if (dup !== undefined) return cannot(`${reviewPath} holds a duplicate finding id ${dup}; nothing appended`);
|
||||
const f = payload.findings.find((x) => x && x.id === id);
|
||||
if (f && isBlocker(f)) return refuse(`${id} is a BLOCKER: a BLOCKER is never answered, it is fixed`);
|
||||
if (!f || !countingMajors(payload).some((x) => x.id === id)) return refuse(`${id} is not a counting MAJOR of this review`);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@
|
|||
// engine finding not in review: <severity> <rule_key> at
|
||||
// <file>:<line>[, …]`; `droppedEngineFindings`): finalize's put-back
|
||||
// did not run, or `finalized: true` was written by hand.
|
||||
// An ALLOW/WARN review that holds one finding id twice, in the
|
||||
// frontmatter `findings:` list or in the trailing JSON block, is exit
|
||||
// 2 (reason `duplicate finding id <id> in <where>`).
|
||||
// Exit 2 is NEVER green.
|
||||
//
|
||||
// The pure `decideVerdict` stays text-only: it never reads the answers. The
|
||||
|
|
@ -159,6 +162,13 @@ export function decideVerdict(text) {
|
|||
if (illegal.length > 0) {
|
||||
return undecided(`verdict ${verdict} over a brief-conflict lowering outside the one legal shape (finding ${illegal.join(', ')})`, verdict);
|
||||
}
|
||||
// One id twice would let one bound answer clear two MAJORs: not measured.
|
||||
const fmList = Array.isArray(fm.findings) ? fm.findings : [];
|
||||
const fmDup = fmList.find((id, i) => fmList.indexOf(id) !== i);
|
||||
if (fmDup !== undefined) return undecided(`duplicate finding id ${fmDup} in the frontmatter findings: list`, verdict);
|
||||
const jsonList = payload.findings.map((f) => f.id);
|
||||
const jsonDup = jsonList.find((id, i) => jsonList.indexOf(id) !== i);
|
||||
if (jsonDup !== undefined) return undecided(`duplicate finding id ${jsonDup} in the trailing JSON block`, 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