Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/rules/predicted-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,13 @@ export function buildPredictedGateVerdict(args: {

// Linked-issue finding is surfaced when the repo's public policy treats it as anything but `off`, so the
// gate can evaluate it; evaluateGateCheck decides whether it actually blocks (block) or stays advisory.
const requireLinkedIssue = gate.linkedIssue !== null && gate.linkedIssue !== "off";
// The composite mergeReadiness gate forces the linked-issue sub-gate on (applyMergeReadinessGate), and the
// live path collects linked-issue evidence whenever merge-readiness is enabled (shouldCollectLinkedIssueEvidence,
// queue/processors.ts), so the predictor must surface the finding under mergeReadiness too — otherwise a
// `mergeReadiness:block` repo with linkedIssue unset predicts a false success while the live gate one-shot
// closes the PR on the missing-linked-issue blocker. (#merge-readiness-parity)
const requireLinkedIssue =
(gate.linkedIssue !== null && gate.linkedIssue !== "off") || (gate.mergeReadiness !== null && gate.mergeReadiness !== "off");
// `duplicateWinnerEnabled` is INTENTIONALLY omitted (#dup-winner): the prospective PR is synthetic #0, but a
// real new PR opened into an existing duplicate cluster gets the HIGHEST number ⇒ it is always a duplicate
// LOSER, never the winner. So the predictor must keep showing the duplicate finding (the honest pre-submit
Expand Down
15 changes: 15 additions & 0 deletions test/unit/predicted-gate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,21 @@ describe("buildPredictedGateVerdict", () => {
expect(result.blockers.some((b) => b.code === "duplicate_pr_risk")).toBe(true);
});

it("surfaces the missing-linked-issue blocker under composite mergeReadiness even when linkedIssue is unset (#merge-readiness-parity)", () => {
// mergeReadiness:block forces the composite linked-issue sub-gate to block; the live gate collects
// linked-issue evidence whenever merge-readiness is on (shouldCollectLinkedIssueEvidence), so the
// predictor must surface the finding here too — otherwise it shows a false success while the live gate
// one-shot auto-closes the PR. linkedIssue is left unset (null), so only the mergeReadiness term applies.
const blocked = verdict({ gate: { duplicates: "off", mergeReadiness: "block" }, input: { body: "no issue here", linkedIssues: [] }, issues: [] });
expect(blocked.conclusion).toBe("failure");
expect(blocked.blockers.some((b) => b.code === "missing_linked_issue")).toBe(true);

// With neither linkedIssue nor mergeReadiness set, no missing-linked-issue finding is created (the
// false/false arm of the new condition) — matching the live gate, which collects no linked-issue evidence.
const noGate = verdict({ gate: { duplicates: "off" }, input: { body: "no issue here", linkedIssues: [] }, issues: [] });
expect(noGate.blockers.some((b) => b.code === "missing_linked_issue")).toBe(false);
});

it("honors public gate.firstTimeContributorGrace with predicted author history", () => {
const newcomer = verdict({
gate: { duplicates: "block", firstTimeContributorGrace: true },
Expand Down
Loading