diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 66dfc49d7c..fad0709b87 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -99,6 +99,38 @@ function openIssueBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo }; } +// Sibling of openIssueBreakdown/openPrBreakdown for the merged-PR history floor (upstream MIN_VALID_MERGED_PRS): +// a contributor whose observed merged-PR count on this repo is below the floor has the entire preview zeroed. +// Explained here so a miner sees the same actionable breakdown the open-PR / open-issue gates already provide. +function mergedHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { + const { mergedHistoryMultiplier } = preview.scoreEstimate; + const { mergedPrFloor, mergedPullRequests } = preview.gates; + // mergedPullRequests is optional: when unobserved the floor is not enforced (the multiplier stays 1). + if (mergedPullRequests === undefined) { + return { + component: "mergedHistoryMultiplier", + band: "neutral", + summary: `Merged-PR history floor is not enforced for this preview (no contributor history observed; upstream floor is ${mergedPrFloor}).`, + lever: "No action needed for this preview; the upstream merged-PR floor applies once contributor history is observed.", + leverageScore: 0, + }; + } + const band = bandForMultiplier(mergedHistoryMultiplier); + return { + component: "mergedHistoryMultiplier", + band, + summary: + mergedPullRequests >= mergedPrFloor + ? `Merged PR history (${mergedPullRequests}) meets the upstream floor (${mergedPrFloor}).` + : `Merged PR history (${mergedPullRequests}) is below the upstream floor (${mergedPrFloor}), so this preview is zeroed.`, + lever: + mergedPullRequests >= mergedPrFloor + ? "Keep landing merged PRs in this repo to maintain contributor history." + : "Land more merged PRs in this repo to clear the contributor-history floor before relying on this preview.", + leverageScore: mergedPullRequests >= mergedPrFloor ? 5 : 100, + }; +} + function credibilityBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { credibilityMultiplier } = preview.scoreEstimate; const { credibilityObserved, credibilityFloor } = preview.gates; @@ -239,6 +271,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo reviewPenaltyBreakdown(preview), openPrBreakdown(preview), openIssueBreakdown(preview), + mergedHistoryBreakdown(preview), ].map((entry) => ({ ...entry, summary: sanitizePublicComment(entry.summary), diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 5f82c5740b..73b07c3cb0 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -83,6 +83,7 @@ describe("explainScoreBreakdown", () => { "reviewPenaltyMultiplier", "openPrMultiplier", "openIssueMultiplier", + "mergedHistoryMultiplier", ]), ); for (const component of breakdown.components) { @@ -97,6 +98,28 @@ describe("explainScoreBreakdown", () => { expect(breakdown.components.find((entry) => entry.component === "openIssueMultiplier")).toMatchObject({ band: "full" }); }); + it("explains the merged-PR history floor as neutral (unobserved), full (meets floor), and blocked (below floor)", () => { + // Unobserved history -> floor not enforced -> neutral. + const unobserved = explainScoreBreakdown( + buildScorePreview({ repo, snapshot, input: { repoFullName: repo.fullName, contributorLogin: "miner", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80, openPrCount: 1, credibility: 0.9, linkedIssueMode: "none" } }), + ); + expect(unobserved.components.find((entry) => entry.component === "mergedHistoryMultiplier")).toMatchObject({ band: "neutral" }); + + // Observed >= upstream floor (MIN_VALID_MERGED_PRS = 3) -> full. + const meets = explainScoreBreakdown( + buildScorePreview({ repo, snapshot, input: { repoFullName: repo.fullName, contributorLogin: "miner", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80, openPrCount: 1, credibility: 0.9, linkedIssueMode: "none", mergedPullRequests: 5 } }), + ); + expect(meets.components.find((entry) => entry.component === "mergedHistoryMultiplier")).toMatchObject({ band: "full" }); + + // Observed < floor -> blocked, and the merged-PR lever is the top-leverage one. + const blocked = explainScoreBreakdown( + buildScorePreview({ repo, snapshot, input: { repoFullName: repo.fullName, contributorLogin: "miner", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80, openPrCount: 1, credibility: 0.9, linkedIssueMode: "none", mergedPullRequests: 1 } }), + ); + expect(blocked.components.find((entry) => entry.component === "mergedHistoryMultiplier")).toMatchObject({ band: "blocked", leverageScore: 100 }); + expect(blocked.highestLeverageLever.lever).toMatch(/merge/i); + expect(JSON.stringify(blocked)).not.toMatch(FORBIDDEN); + }); + it("explains an over-threshold open-issue count as a blocked open-issue spam gate", () => { const preview = buildScorePreview({ repo,