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
23 changes: 23 additions & 0 deletions src/services/score-breakdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,28 @@ function openPrBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown
};
}

// Sibling of openPrBreakdown for the issue-discovery channel: the open-issue spam gate (#808) zeroes the
// score once a contributor's concurrent open-issue count exceeds their earned allowance. Explained here so a
// miner in the issue-discovery lane sees the same actionable breakdown the open-PR gate already provides.
function openIssueBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown {
const { openIssueMultiplier } = preview.scoreEstimate;
const { openIssueCount, openIssueThreshold } = preview.gates;
const band = bandForMultiplier(openIssueMultiplier);
return {
component: "openIssueMultiplier",
band,
summary:
openIssueMultiplier >= 1
? `Open issue count (${openIssueCount}) is within the current allowance (${openIssueThreshold}).`
: `Open issue count (${openIssueCount}) exceeds the current allowance (${openIssueThreshold}), so the open-issue spam gate blocks scoring.`,
lever:
openIssueMultiplier >= 1
? "Keep concurrent open issues within the allowance to stay clear of the open-issue spam gate."
: "Close or resolve excess open issues to drop back within the open-issue spam threshold.",
leverageScore: openIssueMultiplier >= 1 ? 5 : 100,
};
}

function credibilityBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown {
const { credibilityMultiplier } = preview.scoreEstimate;
const { credibilityObserved, credibilityFloor } = preview.gates;
Expand Down Expand Up @@ -216,6 +238,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo
credibilityBreakdown(preview),
reviewPenaltyBreakdown(preview),
openPrBreakdown(preview),
openIssueBreakdown(preview),
].map((entry) => ({
...entry,
summary: sanitizePublicComment(entry.summary),
Expand Down
26 changes: 26 additions & 0 deletions test/unit/score-breakdown.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe("explainScoreBreakdown", () => {
"credibilityMultiplier",
"reviewPenaltyMultiplier",
"openPrMultiplier",
"openIssueMultiplier",
]),
);
for (const component of breakdown.components) {
Expand All @@ -92,6 +93,31 @@ describe("explainScoreBreakdown", () => {
expect(breakdown.highestLeverageLever.component).toBeTruthy();
expect(breakdown.highestLeverageLever.lever).toMatch(/merge|close|credibility|open PR|linked issue|density|review/i);
expect(JSON.stringify(breakdown)).not.toMatch(FORBIDDEN);
// No open issues → within the allowance → full band on the open-issue gate.
expect(breakdown.components.find((entry) => entry.component === "openIssueMultiplier")).toMatchObject({ band: "full" });
});

it("explains an over-threshold open-issue count as a blocked open-issue spam gate", () => {
const preview = buildScorePreview({
repo,
snapshot,
input: {
repoFullName: repo.fullName,
contributorLogin: "miner",
sourceTokenScore: 40,
totalTokenScore: 60,
sourceLines: 80,
openIssueCount: 50,
linkedIssueMode: "standard",
},
});

const breakdown = explainScoreBreakdown(preview);
const openIssue = breakdown.components.find((entry) => entry.component === "openIssueMultiplier");
expect(openIssue).toMatchObject({ band: "blocked" });
expect(openIssue?.summary).toMatch(/exceeds the current allowance/i);
expect(openIssue?.lever).toMatch(/close or resolve/i);
expect(JSON.stringify(breakdown)).not.toMatch(FORBIDDEN);
});

it("prioritizes open PR blocking as the highest leverage lever", () => {
Expand Down
Loading