diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index fad0709b87..b4ef0c9621 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -190,6 +190,26 @@ function reviewPenaltyBreakdown(preview: ScorePreviewResult): ScoreMultiplierBre }; } +// Sibling of reviewPenaltyBreakdown: upstream models review churn twice — reviewPenaltyMultiplier shrinks the +// current preview while reviewCollateralMultiplier raises the open-PR collateral fraction +// (OPEN_PR_COLLATERAL_PERCENT × multiplier) reserved on concurrent PRs after CHANGES_REQUESTED reviews. +function reviewCollateralBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { + const { reviewCollateralMultiplier, collateralFraction } = preview.gates; + const elevated = reviewCollateralMultiplier > 1.01; + const band: ScoreMultiplierBand = elevated ? "reduced" : "neutral"; + return { + component: "reviewCollateralMultiplier", + band, + summary: elevated + ? `Open-PR review collateral is elevated (effective fraction ${roundBand(collateralFraction)}) because prior CHANGES_REQUESTED reviews on open PRs raised the collateral multiplier above baseline.` + : `Open-PR review collateral is at the baseline fraction (${roundBand(collateralFraction)}); no CHANGES_REQUESTED review churn is inflating concurrent-PR collateral.`, + lever: elevated + ? "Resolve outstanding change requests on open PRs before opening more concurrent work, or expect tighter collateral on the open-PR allowance." + : "Keep open PRs review-clean to avoid collateral inflation on concurrent work.", + leverageScore: elevated ? Math.min(55, Math.round((reviewCollateralMultiplier - 1) * 40 + 20)) : 6, + }; +} + function labelMultiplierBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { labelMultiplier } = preview.scoreEstimate; const band: ScoreMultiplierBand = labelMultiplier > 1 ? "full" : labelMultiplier < 1 ? "reduced" : "neutral"; @@ -269,6 +289,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo issueMultiplierBreakdown(preview), credibilityBreakdown(preview), reviewPenaltyBreakdown(preview), + reviewCollateralBreakdown(preview), openPrBreakdown(preview), openIssueBreakdown(preview), mergedHistoryBreakdown(preview), diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 73b07c3cb0..04219839cf 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -81,6 +81,7 @@ describe("explainScoreBreakdown", () => { "issueMultiplier", "credibilityMultiplier", "reviewPenaltyMultiplier", + "reviewCollateralMultiplier", "openPrMultiplier", "openIssueMultiplier", "mergedHistoryMultiplier", @@ -212,6 +213,101 @@ describe("explainScoreBreakdown", () => { expect(breakdown.components.find((entry) => entry.component === "openPrMultiplier")).toMatchObject({ band: "full" }); expect(breakdown.components.find((entry) => entry.component === "credibilityMultiplier")).toMatchObject({ band: "full" }); expect(breakdown.components.find((entry) => entry.component === "reviewPenaltyMultiplier")).toMatchObject({ band: "full" }); + expect(breakdown.components.find((entry) => entry.component === "reviewCollateralMultiplier")).toMatchObject({ band: "neutral" }); + }); + + it("explains elevated open-PR review collateral as reduced strength and baseline as neutral", () => { + const baseline = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 100, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + changesRequestedCount: 0, + }, + }), + ); + expect(baseline.components.find((entry) => entry.component === "reviewCollateralMultiplier")).toMatchObject({ + band: "neutral", + summary: expect.stringMatching(/baseline fraction/i), + }); + + const elevated = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot: { + ...snapshot, + constants: { ...snapshot.constants, MAX_OPEN_PR_REVIEW_COLLATERAL_MULTIPLIER: 2.0 }, + }, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 100, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + changesRequestedCount: 4, + }, + }), + ); + const collateral = elevated.components.find((entry) => entry.component === "reviewCollateralMultiplier"); + expect(collateral).toMatchObject({ band: "reduced" }); + expect(collateral?.summary).toMatch(/elevated|CHANGES_REQUESTED/i); + expect(collateral?.summary).toMatch(/0\.32/); + expect(collateral?.lever).toMatch(/change requests/i); + expect(JSON.stringify(elevated)).not.toMatch(FORBIDDEN); + + const capped = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot: { + ...snapshot, + constants: { ...snapshot.constants, MAX_OPEN_PR_REVIEW_COLLATERAL_MULTIPLIER: 2.0 }, + }, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 100, + sourceLines: 50, + openPrCount: 1, + credibility: 1, + changesRequestedCount: 10, + }, + }), + ); + expect(capped.components.find((entry) => entry.component === "reviewCollateralMultiplier")).toMatchObject({ + band: "reduced", + summary: expect.stringMatching(/0\.4/), + }); + }); + + it("prioritizes open PR blocking above elevated review collateral", () => { + const preview = buildScorePreview({ + repo, + snapshot: { + ...snapshot, + constants: { ...snapshot.constants, MAX_OPEN_PR_REVIEW_COLLATERAL_MULTIPLIER: 2.0 }, + }, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 80, + totalTokenScore: 100, + sourceLines: 50, + openPrCount: 8, + existingContributorTokenScore: 50, + credibility: 1, + changesRequestedCount: 4, + }, + }); + + const breakdown = explainScoreBreakdown(preview); + expect(breakdown.components.find((entry) => entry.component === "reviewCollateralMultiplier")).toMatchObject({ band: "reduced" }); + expect(breakdown.highestLeverageLever.component).toBe("openPrMultiplier"); }); it("marks penalty label multipliers as reduced strength (#994)", () => {