From fbcb697704adf2ab5be8610841d88f2ffcd2a79f Mon Sep 17 00:00:00 2001 From: galuis116 Date: Wed, 1 Jul 2026 03:01:32 +0400 Subject: [PATCH 1/2] fix(signals): sample collision pairwise PRs linked-first with deterministic updatedAt rank buildCollisionReport capped pairwise pull requests with a raw slice(0, 120) while issues already use boundedCollisionIssues. Mirror that pattern: keep linked PRs, then fill by newest updatedAt so title/path overlap is not dropped on large queues due to arbitrary DB order. --- src/signals/engine.ts | 21 ++++++++++++++++++++- test/unit/signals-v2.test.ts | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 939f852b3e..036830ff4f 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -835,7 +835,7 @@ export function buildCollisionReport( } const pairwiseIssues = boundedCollisionIssues(openIssues, openPullRequests); - const pairwisePullRequests = openPullRequests.slice(0, MAX_COLLISION_PAIRWISE_PULL_REQUESTS); + const pairwisePullRequests = boundedCollisionPullRequests(openPullRequests); const pairwiseRecentMergedPullRequests = recentMergedPullRequests.slice(0, MAX_COLLISION_PAIRWISE_RECENT_MERGES); const items = [...pairwiseIssues.map(issueItem), ...pairwisePullRequests.map(prItem), ...pairwiseRecentMergedPullRequests.map(recentMergedItem)]; const itemTerms = new Map(); @@ -5187,6 +5187,25 @@ function boundedCollisionIssues(openIssues: IssueRecord[], openPullRequests: Pul /* v8 ignore stop */ } +function boundedCollisionPullRequests(openPullRequests: PullRequestRecord[]): PullRequestRecord[] { + /* v8 ignore start -- Large-queue PR sampling mirrors boundedCollisionIssues; linked and pairwise collision paths are covered above. */ + if (openPullRequests.length <= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) return openPullRequests; + const selected = new Map(); + for (const pullRequest of openPullRequests) { + if (pullRequest.linkedIssues.length > 0) selected.set(pullRequest.number, pullRequest); + if (selected.size >= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) return [...selected.values()]; + } + const ranked = [...openPullRequests].sort( + (left, right) => (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "") || left.number - right.number, + ); + for (const pullRequest of ranked) { + selected.set(pullRequest.number, pullRequest); + if (selected.size >= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) break; + } + return [...selected.values()]; + /* v8 ignore stop */ +} + function itemKey(item: CollisionItem): string { return `${item.type}-${item.number}`; } diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index de337a19ff..4fc96332c3 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -166,6 +166,40 @@ describe("v2 signal builders", () => { expect(health.signals.openPullRequests).toBe(1); }); + it("detects pairwise PR title overlap when the queue exceeds 120 and overlapping PRs are newest (regression for bounded PR sampling)", () => { + const filler = Array.from({ length: 119 }, (_, index) => ({ + ...pullRequests[0]!, + number: index + 1, + title: `Unrelated maintenance task ${index + 1} for widgets module`, + linkedIssues: [] as number[], + updatedAt: isoDaysAgo(200), + })); + const overlapA: PullRequestRecord = { + ...pullRequests[0]!, + number: 5000, + title: "Fix authentication retry backoff handler", + linkedIssues: [], + updatedAt: isoDaysAgo(1), + }; + const overlapB: PullRequestRecord = { + ...pullRequests[0]!, + number: 5001, + title: "Fix authentication retry backoff logic", + linkedIssues: [], + updatedAt: isoDaysAgo(1), + }; + const manyPullRequests = [...filler, overlapA, overlapB]; + const report = buildCollisionReport(repo.fullName, issues, manyPullRequests, []); + expect( + report.clusters.some( + (cluster) => + cluster.items.some((item) => item.type === "pull_request" && item.number === 5000) && + cluster.items.some((item) => item.type === "pull_request" && item.number === 5001) && + /meaningful terms/i.test(cluster.reason), + ), + ).toBe(true); + }); + it("uses authoritative queue counts when signal inputs are sampled", () => { const sampledIssues = issues.slice(0, 1); const sampledPullRequests = pullRequests.slice(0, 1); From 94673aab55761bee1ad8dd16ade5893fe9ad4f5d Mon Sep 17 00:00:00 2001 From: galuis116 Date: Wed, 1 Jul 2026 16:50:07 +0400 Subject: [PATCH 2/2] fix(signals): rank PR sampling by linked-status and recency in one pass --- src/signals/engine.ts | 19 ++++++++---------- test/unit/signals-v2.test.ts | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 11 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 036830ff4f..5990d193d0 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -5190,19 +5190,16 @@ function boundedCollisionIssues(openIssues: IssueRecord[], openPullRequests: Pul function boundedCollisionPullRequests(openPullRequests: PullRequestRecord[]): PullRequestRecord[] { /* v8 ignore start -- Large-queue PR sampling mirrors boundedCollisionIssues; linked and pairwise collision paths are covered above. */ if (openPullRequests.length <= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) return openPullRequests; - const selected = new Map(); - for (const pullRequest of openPullRequests) { - if (pullRequest.linkedIssues.length > 0) selected.set(pullRequest.number, pullRequest); - if (selected.size >= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) return [...selected.values()]; - } + // Rank linked-issue PRs ahead of unlinked ones, then by recency within each group, so the cap keeps + // the most-relevant PRs even when linked PRs alone exceed the budget (not just whichever appear + // first in caller order). const ranked = [...openPullRequests].sort( - (left, right) => (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "") || left.number - right.number, + (left, right) => + Number(left.linkedIssues.length === 0) - Number(right.linkedIssues.length === 0) || + (right.updatedAt ?? "").localeCompare(left.updatedAt ?? "") || + left.number - right.number, ); - for (const pullRequest of ranked) { - selected.set(pullRequest.number, pullRequest); - if (selected.size >= MAX_COLLISION_PAIRWISE_PULL_REQUESTS) break; - } - return [...selected.values()]; + return ranked.slice(0, MAX_COLLISION_PAIRWISE_PULL_REQUESTS); /* v8 ignore stop */ } diff --git a/test/unit/signals-v2.test.ts b/test/unit/signals-v2.test.ts index 4fc96332c3..f08d3cd3af 100644 --- a/test/unit/signals-v2.test.ts +++ b/test/unit/signals-v2.test.ts @@ -200,6 +200,43 @@ describe("v2 signal builders", () => { ).toBe(true); }); + it("still ranks by recency when linked PRs alone exceed the pairwise cap (regression)", () => { + // 130 linked-issue filler PRs, all older than the two overlapping linked PRs below. Linked PRs alone + // (132) exceed the 120 cap, so a naive "take linked PRs in caller order" pass would still truncate + // before reaching the newest, colliding pair. + const linkedFiller = Array.from({ length: 130 }, (_, index) => ({ + ...pullRequests[0]!, + number: index + 1, + title: `Unrelated maintenance task ${index + 1} for widgets module`, + linkedIssues: [9000 + index], + updatedAt: isoDaysAgo(200), + })); + const overlapA: PullRequestRecord = { + ...pullRequests[0]!, + number: 6000, + title: "Fix authentication retry backoff handler", + linkedIssues: [7000], + updatedAt: isoDaysAgo(1), + }; + const overlapB: PullRequestRecord = { + ...pullRequests[0]!, + number: 6001, + title: "Fix authentication retry backoff logic", + linkedIssues: [7001], + updatedAt: isoDaysAgo(1), + }; + const manyPullRequests = [...linkedFiller, overlapA, overlapB]; + const report = buildCollisionReport(repo.fullName, issues, manyPullRequests, []); + expect( + report.clusters.some( + (cluster) => + cluster.items.some((item) => item.type === "pull_request" && item.number === 6000) && + cluster.items.some((item) => item.type === "pull_request" && item.number === 6001) && + /meaningful terms/i.test(cluster.reason), + ), + ).toBe(true); + }); + it("uses authoritative queue counts when signal inputs are sampled", () => { const sampledIssues = issues.slice(0, 1); const sampledPullRequests = pullRequests.slice(0, 1);