diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 07ffcb2af4..a329004377 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -246,6 +246,7 @@ import { buildContributorProfile, buildContributorScoringProfile, buildContributorStrategy, + buildDuplicateWinnerRelatedWorkView, buildContributorIntakeHealth, buildIssueQualityReport, buildLabelAudit, @@ -260,7 +261,6 @@ import { buildRoleContext, detectGittensorContributor, PR_PANEL_RETRIGGER_MARKER, - unionScopedOverlapClusters, type ContributorProfile, } from "../signals/engine"; import { isDuplicateClusterWinnerByClaim } from "../signals/duplicate-winner"; @@ -1399,7 +1399,7 @@ async function maybeRunAgentMaintenance( // reason ("duplicate of another open PR" via agent-actions when count > 0). When the flag is ON and this // PR is the cluster winner, force the count to 0 so the winner's close reason OMITS the duplicate cause // (it can still close on its own merits — CI/conflict/blockers). Flag-OFF short-circuits ⇒ the real - // count is used (byte-identical). Unknown claim time keeps the duplicate cause. + // count is used (byte-identical). Sparse legacy rows fall back to PR-number election. linkedDuplicateCount: dupWinnerLinkedDuplicateCount( linkedIssueDuplicatePullRequestRecordsForGate(pr, otherOpenPullRequests), pr.number, @@ -4533,16 +4533,18 @@ async function maybePublishPrPublicSurface( const isDupWinner = duplicateWinnerEnabled && isDuplicateClusterWinnerByClaim(pr, linkedDuplicatePrsForGate); + const relatedWork = buildDuplicateWinnerRelatedWorkView({ + pr, + collisions, + preflightCollisions: preflight.collisions, + duplicateWinnerEnabled, + }); const readiness = buildPublicReadinessScore({ pr, preflight, queueHealth, linkedDuplicatePrs: isDupWinner ? [] : linkedDuplicatePrsForGate.map((otherPr) => otherPr.number), - scopedOverlapCount: unionScopedOverlapClusters( - collisions, - pr, - preflight.collisions, - ).length, + scopedOverlapCount: relatedWork.scopedOverlapClusters.length, }); if (gateEnabled && author && !publicSurfaceSkipped && !official) { @@ -5383,6 +5385,7 @@ async function maybePublishPrPublicSurface( preflight, queueHealth, ...(reviewConfig !== undefined ? { review: reviewConfig } : {}), + duplicateWinnerEnabled, }), footerMarkdown: gittensoryFooter({ earnUrl: repo?.isRegistered diff --git a/src/rules/advisory.ts b/src/rules/advisory.ts index 73a123ba75..563f606c86 100644 --- a/src/rules/advisory.ts +++ b/src/rules/advisory.ts @@ -676,7 +676,7 @@ function addPullRequestFindings( ); // Duplicate-winner adjudication (#dup-winner): when the flag is ON and this PR is the earliest observed // linked-issue claimant, SKIP the duplicate finding — suppressing it suppresses the gate failure, so the - // winner survives while later claimants keep the finding. Unknown claim time keeps the blocker. + // winner survives while later claimants keep the finding. Sparse legacy rows fall back to PR-number election. // Flag-OFF (default) short-circuits ⇒ the finding is pushed exactly as before (byte-identical). if (overlappingPrs.length > 0 && !(duplicateWinnerEnabled && isDuplicateClusterWinnerByClaim(pr, overlappingPrs))) { findings.push({ diff --git a/src/signals/duplicate-winner.ts b/src/signals/duplicate-winner.ts index 7bf4540d54..0779256ecb 100644 --- a/src/signals/duplicate-winner.ts +++ b/src/signals/duplicate-winner.ts @@ -3,8 +3,9 @@ * * When several OPEN PRs link the same issue (a duplicate cluster), the legacy behavior gate-blocks + * auto-closes EVERY sibling as a duplicate — no winner survives. With the flag ON, exactly ONE winner is - * spared: the earliest observed linked-issue claimant. Only the LOSERS are blocked/closed; the winner still - * must pass CI / conflict / gate / linked-issue / slop on its OWN merits. + * spared: the earliest observed linked-issue claimant. Sparse legacy rows that do not yet have claim timing + * fall back to PR-number election so migrated clusters do not keep every sibling blocked. Only the LOSERS are + * blocked/closed; the winner still must pass CI / conflict / gate / linked-issue / slop on its OWN merits. * * This module is PURE — no IO, no Date, no random — so the same inputs always yield the same verdict and the * caller can compute the winner ONCE per review run and thread the result boolean consistently into every @@ -36,17 +37,16 @@ export function isDuplicateClusterWinner(prNumber: number, openSiblingNumbers: n } /** - * True iff `pr` is the earliest known linked-issue claimant in the open duplicate cluster. Unknown or invalid - * claim times fail closed: the caller should keep the duplicate finding/blocker instead of accidentally sparing - * the wrong PR. Ties fall back to PR number only after the claim time is known for every compared sibling. + * True iff `pr` is the earliest known linked-issue claimant in the open duplicate cluster. Sparse legacy rows + * fall back to the original PR-number election; ties between known claim times also use PR number. */ export function isDuplicateClusterWinnerByClaim(pr: DuplicateClaimMember, openSiblings: DuplicateClaimMember[]): boolean { if (openSiblings.length === 0) return true; const prClaim = claimTimeMs(pr.linkedIssueClaimedAt); - if (prClaim === null) return false; + if (prClaim === null) return isDuplicateClusterWinner(pr.number, openSiblings.map((sibling) => sibling.number)); for (const sibling of openSiblings) { const siblingClaim = claimTimeMs(sibling.linkedIssueClaimedAt); - if (siblingClaim === null) return false; + if (siblingClaim === null) return isDuplicateClusterWinner(pr.number, openSiblings.map((other) => other.number)); if (siblingClaim < prClaim) return false; if (siblingClaim === prClaim && sibling.number < pr.number) return false; } diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 4bbecc8647..120c0a1580 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -4075,6 +4075,7 @@ type PublicSafeCollapsibleArgs = { preflight: PreflightResult; queueHealth: QueueHealth; review?: FocusManifestReviewConfig | undefined; + duplicateWinnerEnabled?: boolean | undefined; }; /** "Signal definitions" body — a static legend for the readiness signals. No inputs. */ @@ -4099,8 +4100,12 @@ function reviewContextBody(args: PublicSafeCollapsibleArgs): string[] { profile: args.profile, }); const confirmedMiner = isOfficialContributorDetection(args.detection); - const prCollisionClusters = pullRequestSpecificCollisionClusters(args.collisions, args.pr); - const scopedOverlapClusters = unionScopedOverlapClusters(args.collisions, args.pr, args.preflight.collisions); + const relatedWork = buildDuplicateWinnerRelatedWorkView({ + pr: args.pr, + collisions: args.collisions, + preflightCollisions: args.preflight.collisions, + duplicateWinnerEnabled: args.duplicateWinnerEnabled, + }); return [ `- Author: \`${sanitizePanelText(args.pr.authorLogin ?? "unknown")}\``, `- Role context: ${sanitizePanelText(roleContext.role)}${roleContext.maintainerLane ? " (maintainer lane)" : ""}`, @@ -4108,10 +4113,7 @@ function reviewContextBody(args: PublicSafeCollapsibleArgs): string[] { `- Lane context: ${sanitizePanelText(buildLaneAdvice(args.repo, args.pr.repoFullName).summary)}`, `- Public profile languages: ${args.profile.github.topLanguages.length > 0 ? sanitizePanelText(args.profile.github.topLanguages.join(", ")) : "not available"}`, ...(confirmedMiner ? [`- Official Gittensor activity: ${args.detection.priorPullRequests} PR(s), ${args.detection.priorIssues} issue(s).`] : ["- Contributor context: Public profile only; not a blocker."]), - ...relatedWorkDetails(args.pr, scopedOverlapClusters), - // `prCollisionClusters` is referenced only to keep this body's derivation identical to the panel's - // (the panel computes both cluster sets); the overlap detail uses the scoped set. - ...(prCollisionClusters.length === 0 ? [] : []), + ...relatedWorkDetails(args.pr, relatedWork.scopedOverlapClusters), ]; } @@ -4144,12 +4146,18 @@ function publicSafeNextSteps(args: PublicSafeCollapsibleArgs): string[] { issues: [], profile: args.profile, }); + const relatedWork = buildDuplicateWinnerRelatedWorkView({ + pr: args.pr, + collisions: args.collisions, + preflightCollisions: args.preflight.collisions, + duplicateWinnerEnabled: args.duplicateWinnerEnabled, + }); const readiness = buildPublicReadinessScore({ pr: args.pr, preflight: args.preflight, queueHealth: args.queueHealth, - linkedDuplicatePrs: linkedIssueDuplicatePullRequests(args.pr, pullRequestSpecificCollisionClusters(args.collisions, args.pr)), - scopedOverlapCount: unionScopedOverlapClusters(args.collisions, args.pr, args.preflight.collisions).length, + linkedDuplicatePrs: relatedWork.visibleLinkedDuplicatePrs, + scopedOverlapCount: relatedWork.scopedOverlapClusters.length, }); const publicFindings = publicSafePreflightFindings(args.preflight, args.settings); return [ @@ -4193,15 +4201,20 @@ export function buildPublicPrIntelligenceComment(args: { duplicateWinnerEnabled?: boolean | undefined; }): string { const publicFindings = publicSafePreflightFindings(args.preflight, args.settings); - const prCollisionClusters = pullRequestSpecificCollisionClusters(args.collisions, args.pr); - const linkedDuplicatePrItems = linkedIssueDuplicatePullRequestItems(args.pr, prCollisionClusters); - const linkedDuplicatePrs = linkedDuplicatePrItems.map((item) => item.number); - const scopedOverlapClusters = unionScopedOverlapClusters(args.collisions, args.pr, args.preflight.collisions); + const relatedWork = buildDuplicateWinnerRelatedWorkView({ + pr: args.pr, + collisions: args.collisions, + preflightCollisions: args.preflight.collisions, + duplicateWinnerEnabled: args.duplicateWinnerEnabled, + }); + const linkedDuplicatePrs = relatedWork.linkedDuplicatePrItems.map((item) => item.number); + const visibleLinkedDuplicatePrs = relatedWork.visibleLinkedDuplicatePrs; + const scopedOverlapClusters = relatedWork.scopedOverlapClusters; const scopedOverlapCount = scopedOverlapClusters.length; - const hasRelatedWork = linkedDuplicatePrs.length > 0 || scopedOverlapCount > 0; - const readiness = buildPublicReadinessScore({ pr: args.pr, preflight: args.preflight, queueHealth: args.queueHealth, linkedDuplicatePrs, scopedOverlapCount }); + const hasRelatedWork = visibleLinkedDuplicatePrs.length > 0 || scopedOverlapCount > 0; + const readiness = buildPublicReadinessScore({ pr: args.pr, preflight: args.preflight, queueHealth: args.queueHealth, linkedDuplicatePrs: visibleLinkedDuplicatePrs, scopedOverlapCount }); const linkedIssueResult = linkedIssuePanelResult(args.pr); - const relatedWorkResult = relatedWorkPanelResult(linkedDuplicatePrs, scopedOverlapCount); + const relatedWorkResult = relatedWorkPanelResult(visibleLinkedDuplicatePrs, scopedOverlapCount); const roleContext = buildRoleContext({ login: args.pr.authorLogin ?? args.profile.login, repo: args.repo, @@ -4220,12 +4233,12 @@ export function buildPublicPrIntelligenceComment(args: { const hardLinkedIssueBlock = args.settings.linkedIssueGateMode === "block" && args.pr.linkedIssues.length === 0 && !hasClearNoIssueRationale(args.pr); // Duplicate-winner adjudication (#dup-winner): when the flag is ON and this PR is the earliest observed - // linked-issue claimant, do NOT hard-block it as a duplicate — only the losers block. `linkedDuplicatePrs` is - // open-only (collision clusters exclude closed PRs). Unknown claim time keeps the blocker. + // linked-issue claimant, do NOT hard-block it as a duplicate — only the losers block. Sparse legacy rows fall + // back to PR-number election so migrated clusters do not all stay blocked. const hardDuplicateBlock = args.settings.duplicatePrGateMode === "block" && linkedDuplicatePrs.length > 0 && - !(args.duplicateWinnerEnabled && isDuplicateClusterWinnerByClaim(args.pr, linkedDuplicatePrItems)); + visibleLinkedDuplicatePrs.length > 0; const fallbackGateConclusion = !gateEnabled ? "success" : !args.repo @@ -4275,8 +4288,8 @@ export function buildPublicPrIntelligenceComment(args: { ? args.gate?.summary ?? (gateConclusion === "action_required" ? "Gittensory cannot evaluate the repo state closely enough for the enabled gate." : "A repo-configured hard blocker was found.") : gateHeld ? args.gate?.summary ?? "Gittensory is holding this PR for maintainer review." - : linkedDuplicatePrs.length > 0 - ? `Same-issue duplicate risk found against ${formatPrRefs(linkedDuplicatePrs)}. Maintainers should resolve the overlap before review continues.` + : visibleLinkedDuplicatePrs.length > 0 + ? `Same-issue duplicate risk found against ${formatPrRefs(visibleLinkedDuplicatePrs)}. Maintainers should resolve the overlap before review continues.` : hasRelatedWork ? "Scoped related-work signals were found for this PR. They are advisory unless the gate reports a blocker." : genericOssMode @@ -4444,22 +4457,27 @@ export function buildPublicPrPanelSignalRows(args: { * to today. Matches `buildPublicPrIntelligenceComment` so both panels agree. */ duplicateWinnerEnabled?: boolean | undefined; }): { rows: PublicPrPanelSignalRow[]; readinessTotal: number } { - const prCollisionClusters = pullRequestSpecificCollisionClusters(args.collisions, args.pr); - const linkedDuplicatePrItems = linkedIssueDuplicatePullRequestItems(args.pr, prCollisionClusters); - const linkedDuplicatePrs = linkedDuplicatePrItems.map((item) => item.number); - const scopedOverlapClusters = unionScopedOverlapClusters(args.collisions, args.pr, args.preflight.collisions); + const relatedWork = buildDuplicateWinnerRelatedWorkView({ + pr: args.pr, + collisions: args.collisions, + preflightCollisions: args.preflight.collisions, + duplicateWinnerEnabled: args.duplicateWinnerEnabled, + }); + const linkedDuplicatePrs = relatedWork.linkedDuplicatePrItems.map((item) => item.number); + const visibleLinkedDuplicatePrs = relatedWork.visibleLinkedDuplicatePrs; + const scopedOverlapClusters = relatedWork.scopedOverlapClusters; const scopedOverlapCount = scopedOverlapClusters.length; - const readiness = buildPublicReadinessScore({ pr: args.pr, preflight: args.preflight, queueHealth: args.queueHealth, linkedDuplicatePrs, scopedOverlapCount }); + const readiness = buildPublicReadinessScore({ pr: args.pr, preflight: args.preflight, queueHealth: args.queueHealth, linkedDuplicatePrs: visibleLinkedDuplicatePrs, scopedOverlapCount }); const linkedIssueResult = linkedIssuePanelResult(args.pr); - const relatedWorkResult = relatedWorkPanelResult(linkedDuplicatePrs, scopedOverlapCount); + const relatedWorkResult = relatedWorkPanelResult(visibleLinkedDuplicatePrs, scopedOverlapCount); const gateEnabled = args.settings.gateCheckMode === "enabled"; const hardLinkedIssueBlock = args.settings.linkedIssueGateMode === "block" && args.pr.linkedIssues.length === 0 && !hasClearNoIssueRationale(args.pr); // Duplicate-winner adjudication (#dup-winner): suppress the earliest known claimant's hard-duplicate block - // (see the comment builder). Unknown claim time keeps the blocker; flag-OFF keeps legacy behavior. + // (see the comment builder). Sparse legacy rows fall back to PR-number election; flag-OFF keeps legacy behavior. const hardDuplicateBlock = args.settings.duplicatePrGateMode === "block" && linkedDuplicatePrs.length > 0 && - !(args.duplicateWinnerEnabled && isDuplicateClusterWinnerByClaim(args.pr, linkedDuplicatePrItems)); + visibleLinkedDuplicatePrs.length > 0; const fallbackGateConclusion = !gateEnabled ? "success" : !args.repo ? "neutral" : hardLinkedIssueBlock || hardDuplicateBlock ? "failure" : "success"; const gateConclusion = args.gate?.conclusion ?? fallbackGateConclusion; const confirmedMiner = isOfficialContributorDetection(args.detection); @@ -4498,8 +4516,64 @@ export function unionScopedOverlapClusters( return [...new Map([...prCollisionClusters, ...preflightCollisions].map((cluster) => [cluster.id, cluster])).values()]; } -function linkedIssueDuplicatePullRequests(pr: PullRequestRecord, clusters: CollisionCluster[]): number[] { - return linkedIssueDuplicatePullRequestItems(pr, clusters).map((item) => item.number); +export function buildDuplicateWinnerRelatedWorkView(args: { + pr: PullRequestRecord; + collisions: CollisionReport; + preflightCollisions: CollisionCluster[]; + duplicateWinnerEnabled?: boolean | undefined; +}): { + linkedDuplicatePrItems: CollisionItem[]; + visibleLinkedDuplicatePrs: number[]; + scopedOverlapClusters: CollisionCluster[]; + isDuplicateWinner: boolean; +} { + const prCollisionClusters = pullRequestSpecificCollisionClusters(args.collisions, args.pr); + const linkedDuplicatePrItems = linkedIssueDuplicatePullRequestItems(args.pr, prCollisionClusters); + const linkedDuplicatePrs = linkedDuplicatePrItems.map((item) => item.number); + const isDuplicateWinner = + Boolean(args.duplicateWinnerEnabled) && + isDuplicateClusterWinnerByClaim(args.pr, linkedDuplicatePrItems); + const scopedOverlapClusters = visibleScopedOverlapClustersForDuplicateWinner( + args.pr, + unionScopedOverlapClusters(args.collisions, args.pr, args.preflightCollisions), + isDuplicateWinner, + ); + return { + linkedDuplicatePrItems, + visibleLinkedDuplicatePrs: isDuplicateWinner ? [] : linkedDuplicatePrs, + scopedOverlapClusters, + isDuplicateWinner, + }; +} + +function visibleScopedOverlapClustersForDuplicateWinner( + pr: PullRequestRecord, + clusters: CollisionCluster[], + suppressSameIssueDuplicates: boolean, +): CollisionCluster[] { + if (!suppressSameIssueDuplicates) return clusters; + return clusters.flatMap((cluster) => { + if (!isSameLinkedIssueOnlyCluster(cluster)) return [cluster]; + const items = cluster.items.filter((item) => !isSameLinkedIssueDuplicateItem(pr, item)); + return hasVisibleRelatedWorkItem(pr, items) ? [{ ...cluster, items }] : []; + }); +} + +function isSameLinkedIssueOnlyCluster(cluster: CollisionCluster): boolean { + return /^Open PR work references issue #\d+\.$/.test(cluster.reason) || /^Items reference the same linked issue #\d+\.$/.test(cluster.reason); +} + +function isSameLinkedIssueDuplicateItem(pr: PullRequestRecord, item: CollisionItem): boolean { + if (item.type !== "pull_request" || item.number === pr.number) return false; + const linkedIssues = new Set(pr.linkedIssues); + return (item.linkedIssues ?? []).some((issue) => linkedIssues.has(issue)); +} + +function hasVisibleRelatedWorkItem(pr: PullRequestRecord, items: CollisionItem[]): boolean { + return items.some((item) => { + if (item.type === "issue") return false; + return !(item.type === "pull_request" && item.number === pr.number); + }); } function linkedIssueDuplicatePullRequestItems(pr: PullRequestRecord, clusters: CollisionCluster[]): CollisionItem[] { diff --git a/test/unit/duplicate-winner.test.ts b/test/unit/duplicate-winner.test.ts index 65c042cb30..85eb383834 100644 --- a/test/unit/duplicate-winner.test.ts +++ b/test/unit/duplicate-winner.test.ts @@ -51,9 +51,14 @@ describe("isDuplicateClusterWinnerByClaim (#dup-winner claim election)", () => { expect(isDuplicateClusterWinnerByClaim(claim(13, "2026-06-29T10:00:00.000Z"), [claim(12, "2026-06-29T10:00:00.000Z")])).toBe(false); }); - it("fails closed when any duplicate claimant has an unknown or invalid claim timestamp", () => { - expect(isDuplicateClusterWinnerByClaim(claim(12, null), [claim(13, "2026-06-29T10:00:00.000Z")])).toBe(false); - expect(isDuplicateClusterWinnerByClaim(claim(12, "2026-06-29T10:00:00.000Z"), [claim(13, "not-a-date")])).toBe(false); + it("falls back to PR-number election when sparse legacy rows lack claim timestamps", () => { + expect(isDuplicateClusterWinnerByClaim(claim(12, null), [claim(13, "2026-06-29T10:00:00.000Z")])).toBe(true); + expect(isDuplicateClusterWinnerByClaim(claim(13, "2026-06-29T10:00:00.000Z"), [claim(12, null)])).toBe(false); + }); + + it("treats invalid claim timestamps as sparse rows for the PR-number fallback", () => { + expect(isDuplicateClusterWinnerByClaim(claim(12, "not-a-date"), [claim(13, "2026-06-29T10:00:00.000Z")])).toBe(true); + expect(isDuplicateClusterWinnerByClaim(claim(13, "2026-06-29T10:00:00.000Z"), [claim(12, "not-a-date")])).toBe(false); }); }); diff --git a/test/unit/signals-coverage.test.ts b/test/unit/signals-coverage.test.ts index 0be56ee9da..cf295768dc 100644 --- a/test/unit/signals-coverage.test.ts +++ b/test/unit/signals-coverage.test.ts @@ -11,6 +11,7 @@ import { buildContributorScoringProfile, buildContributorStrategy, buildBurdenForecast, + buildDuplicateWinnerRelatedWorkView, buildIssueQualityReport, buildLabelAudit, buildLaneAdvice, @@ -821,12 +822,103 @@ describe("signal coverage edge cases", () => { const winnerComment = buildPublicPrIntelligenceComment({ ...baseFor(winnerPr), duplicateWinnerEnabled: true }); const loserComment = buildPublicPrIntelligenceComment({ ...baseFor(loserPr), duplicateWinnerEnabled: true }); expect(winnerComment).not.toContain("Gittensory Orb Review Agent is blocking merge"); + expect(winnerComment).not.toContain("#88"); + expect(buildPublicPrPanelSignalRows({ ...baseFor(winnerPr), duplicateWinnerEnabled: true }).rows.find((r) => r.key === "relatedWork")!.cells[1]).toContain("No active overlap"); expect(loserComment).toContain("Gittensory Orb Review Agent is blocking merge"); // Flag OFF on the winner is byte-identical to a blocking panel (today's behavior). const offWinnerComment = buildPublicPrIntelligenceComment(baseFor(winnerPr)); expect(offWinnerComment).toContain("Gittensory Orb Review Agent is blocking merge"); }); + it("#dup-winner: hides duplicate-only same-issue evidence while preserving mixed scoped overlap context", () => { + const directRepo = repo("owner/dupmixed"); + const duplicateIssue = issue(directRepo.fullName, 42, "Cache invalidation race"); + const winnerPr = pr(directRepo.fullName, 70, "Fix the cache race", { + authorLogin: "miner", + linkedIssues: [42], + linkedIssueClaimedAt: "2026-06-29T10:00:00.000Z", + body: "Fixes #42", + }); + const siblingPr = pr(directRepo.fullName, 88, "Also fixes the cache race", { + authorLogin: "other", + linkedIssues: [42], + linkedIssueClaimedAt: "2026-06-29T10:01:00.000Z", + body: "Fixes #42", + }); + const collisions: CollisionReport = { + repoFullName: directRepo.fullName, + generatedAt: "2026-06-29T10:02:00.000Z", + summary: { clusterCount: 2, highRiskCount: 1, itemsReviewed: 3 }, + clusters: [ + { + id: "issue-42", + risk: "high", + reason: "Open PR work references issue #42.", + items: [ + { type: "issue", number: duplicateIssue.number, title: duplicateIssue.title, linkedIssues: [42] }, + { type: "pull_request", number: winnerPr.number, title: winnerPr.title, linkedIssues: [42], linkedIssueClaimedAt: winnerPr.linkedIssueClaimedAt }, + { type: "pull_request", number: siblingPr.number, title: siblingPr.title, linkedIssues: [42], linkedIssueClaimedAt: siblingPr.linkedIssueClaimedAt }, + ], + }, + { + id: "mixed-scope", + risk: "medium", + reason: "Titles/paths share 3 meaningful terms.", + items: [ + { type: "pull_request", number: winnerPr.number, title: winnerPr.title, linkedIssues: [42], linkedIssueClaimedAt: winnerPr.linkedIssueClaimedAt }, + { type: "pull_request", number: siblingPr.number, title: siblingPr.title, linkedIssues: [42], linkedIssueClaimedAt: siblingPr.linkedIssueClaimedAt }, + ], + }, + ], + }; + const baseArgs = { + repo: directRepo, + pr: winnerPr, + profile: buildContributorProfile("miner", { login: "miner", topLanguages: ["TypeScript"], source: "github" }, [], []), + detection: { detected: true, source: "official_gittensor_api" as const, reason: "Confirmed.", priorPullRequests: 1, priorMergedPullRequests: 0, priorIssues: 0 }, + queueHealth: buildQueueHealth(directRepo, [duplicateIssue], [winnerPr, siblingPr], collisions), + collisions, + preflight: buildPreflightResult({ repoFullName: directRepo.fullName, title: winnerPr.title, body: winnerPr.body ?? undefined, linkedIssues: winnerPr.linkedIssues }, directRepo, [duplicateIssue], [winnerPr, siblingPr]), + settings: { ...repoSettings(directRepo.fullName), gateCheckMode: "enabled" as const, duplicatePrGateMode: "block" as const }, + duplicateWinnerEnabled: true, + }; + const retainedSameIssueView = buildDuplicateWinnerRelatedWorkView({ + pr: winnerPr, + collisions: { + ...collisions, + summary: { ...collisions.summary, clusterCount: collisions.summary.clusterCount + 1, itemsReviewed: collisions.summary.itemsReviewed + 1 }, + clusters: [ + ...collisions.clusters, + { + id: "issue-42-with-sparse-peer", + risk: "medium", + reason: "Items reference the same linked issue #42.", + items: [ + { type: "pull_request", number: winnerPr.number, title: winnerPr.title, linkedIssues: [42], linkedIssueClaimedAt: winnerPr.linkedIssueClaimedAt }, + { type: "pull_request", number: siblingPr.number, title: siblingPr.title, linkedIssues: [42], linkedIssueClaimedAt: siblingPr.linkedIssueClaimedAt }, + { type: "pull_request", number: 99, title: "Nearby cache cleanup" }, + ], + }, + ], + }, + preflightCollisions: [], + duplicateWinnerEnabled: true, + }); + const retainedSparseCluster = retainedSameIssueView.scopedOverlapClusters.find((cluster) => cluster.id === "issue-42-with-sparse-peer"); + + const relatedRow = buildPublicPrPanelSignalRows(baseArgs).rows.find((r) => r.key === "relatedWork")!; + const comment = buildPublicPrIntelligenceComment(baseArgs); + + expect(retainedSameIssueView.visibleLinkedDuplicatePrs).toEqual([]); + expect(retainedSparseCluster?.items.map((item) => (item.type === "pull_request" ? item.number : item.type))).toEqual([winnerPr.number, 99]); + expect(relatedRow.cells[1]).toContain("1 scoped overlap"); + expect(relatedRow.cells[1]).not.toContain("#88"); + expect(comment).toContain("Titles/paths share 3 meaningful terms"); + expect(comment).toContain("PR #88"); + expect(comment).not.toContain("Same-issue duplicate risk found against #88"); + expect(comment).not.toContain("Open PR work references issue #42."); + }); + it("renders opt-in gate panel states for collision and repo evaluation blockers", () => { const directRepo = repo("owner/gate"); const existingIssue = issue(directRepo.fullName, 7, "Cache refresh websocket reconnect failure");