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
17 changes: 10 additions & 7 deletions src/queue/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ import {
buildContributorProfile,
buildContributorScoringProfile,
buildContributorStrategy,
buildDuplicateWinnerRelatedWorkView,
buildContributorIntakeHealth,
buildIssueQualityReport,
buildLabelAudit,
Expand All @@ -260,7 +261,6 @@ import {
buildRoleContext,
detectGittensorContributor,
PR_PANEL_RETRIGGER_MARKER,
unionScopedOverlapClusters,
type ContributorProfile,
} from "../signals/engine";
import { isDuplicateClusterWinnerByClaim } from "../signals/duplicate-winner";
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -5383,6 +5385,7 @@ async function maybePublishPrPublicSurface(
preflight,
queueHealth,
...(reviewConfig !== undefined ? { review: reviewConfig } : {}),
duplicateWinnerEnabled,
}),
footerMarkdown: gittensoryFooter({
earnUrl: repo?.isRegistered
Expand Down
2 changes: 1 addition & 1 deletion src/rules/advisory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
14 changes: 7 additions & 7 deletions src/signals/duplicate-winner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
134 changes: 104 additions & 30 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand All @@ -4099,19 +4100,20 @@ 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)" : ""}`,
`- Public audience mode: ${args.settings.publicAudienceMode.replace(/_/g, " ")}`,
`- 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),
];
}

Expand Down Expand Up @@ -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 [
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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[] {
Expand Down
11 changes: 8 additions & 3 deletions test/unit/duplicate-winner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});

Expand Down
Loading
Loading