diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 5a3a677b9..173fc6f5c 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -20,6 +20,11 @@ export type ScoreBreakdownExplanation = { highestLeverageLever: { component: string; lever: string; + /** Other components that tie with the selected top component at the same leverageScore + * (excludes the selected top component itself). Ordered alphabetically (same tie-breaker + * used to pick the top component). Surfaced so a contributor can see the alphabetical + * tie-breaker isn't a strictly-dominant choice. Empty when no tie exists. */ + tiedLeverageComponents: string[]; reason: string; }; }; @@ -361,15 +366,23 @@ function gateHighlightsFor(preview: ScorePreviewResult): ScoreBreakdownExplanati function pickHighestLeverage(components: ScoreMultiplierBreakdown[]): ScoreBreakdownExplanation["highestLeverageLever"] { const ranked = [...components].sort((left, right) => right.leverageScore - left.leverageScore || left.component.localeCompare(right.component)); const top = ranked[0]!; + // Surface any components tied at the top leverageScore so the alphabetical tie-breaker + // (localeCompare above) is not silently dominant — a contributor fixing only the named top + // component would miss equally-impactful tied components. Empty when no tie exists. + const tied = ranked.filter((entry, idx) => idx > 0 && entry.leverageScore === top.leverageScore).map((entry) => entry.component); + const tiedClause = tied.length > 0 + ? ` (${top.component} ties with ${tied.join(", ")} at the same leverage score; any of these is the highest-leverage lever.)` + : ""; const reason = top.band === "blocked" - ? `${top.component} is fully blocking or zeroing part of the preview right now.` + ? `${top.component} is fully blocking or zeroing part of the preview right now.${tiedClause}` : top.band === "reduced" - ? `${top.component} is the largest remaining reducer in the multiplier stack.` - : `${top.component} is the best next optimization lever among non-blocking multipliers.`; + ? `${top.component} is the largest remaining reducer in the multiplier stack.${tiedClause}` + : `${top.component} is the best next optimization lever among non-blocking multipliers.${tiedClause}`; return { component: top.component, lever: top.lever, + tiedLeverageComponents: tied, reason: sanitizePublicComment(reason), }; } diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 69434388b..a116da068 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -677,4 +677,55 @@ describe("explainScoreBreakdown", () => { expect(isSaturated.summary).toMatch(/saturated near the score cap/); expect(JSON.stringify(explainScoreBreakdown(saturated))).not.toMatch(FORBIDDEN); }); + + it("surfaces tied-leverage components when multiple levers share the top leverageScore", () => { + // Both openPrMultiplier and openIssueMultiplier blocked at leverageScore 100. + // With existingContributorTokenScore = 0, openPrThreshold = 2 and openIssueThreshold = 2. + // openPrCount = 3 > 2 → blocked (leverageScore 100). + // openIssueCount = 3 > 2 → blocked (leverageScore 100). + // Alphabetical winner: "openIssueMultiplier" (I < P), tied component: "openPrMultiplier". + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 100, + totalTokenScore: 200, + sourceLines: 100, + openPrCount: 3, + openIssueCount: 3, + existingContributorTokenScore: 0, + credibility: 1, + }, + }); + const breakdown = explainScoreBreakdown(preview); + const top = breakdown.highestLeverageLever; + expect(top.component).toBe("openIssueMultiplier"); + expect(top.tiedLeverageComponents).toEqual(["openPrMultiplier"]); + expect(top.reason).toMatch(/openIssueMultiplier ties with openPrMultiplier at the same leverage score/); + expect(breakdown.components.find((c) => c.component === "openIssueMultiplier")?.leverageScore).toBe(100); + expect(breakdown.components.find((c) => c.component === "openPrMultiplier")?.leverageScore).toBe(100); + }); + + it("returns empty tiedLeverageComponents when no tie exists at the top leverageScore", () => { + // Single top lever (credibilityMultiplier at 85) — no tie. + const preview = buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + sourceTokenScore: 100, + totalTokenScore: 200, + sourceLines: 100, + openPrCount: 0, + openIssueCount: 0, + existingContributorTokenScore: 0, + credibility: 0.01, + }, + }); + const breakdown = explainScoreBreakdown(preview); + const top = breakdown.highestLeverageLever; + expect(top.tiedLeverageComponents).toEqual([]); + expect(top.reason).not.toMatch(/ties with/); + }); });