From 69e49d8822497616a81aacff666dcc5ebe043108 Mon Sep 17 00:00:00 2001 From: bohdansolovie Date: Wed, 1 Jul 2026 05:00:26 +0200 Subject: [PATCH 1/2] feat(scoring): explain issue-discovery validity floor in score breakdown Surface the upstream MIN_VALID_SOLVED_ISSUES / MIN_ISSUE_CREDIBILITY gate in explainScoreBreakdown with the same neutral/full/blocked pattern used for merged-PR history, so linked-issue previews show an actionable lever. Co-authored-by: Cursor --- src/services/score-breakdown.ts | 31 ++++++++++++++++++ test/unit/score-breakdown.test.ts | 52 +++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 69e2a7f95f..503afe7328 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -131,6 +131,36 @@ function mergedHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBre }; } +// Sibling of mergedHistoryBreakdown for the issue-discovery validity floor (upstream +// MIN_VALID_SOLVED_ISSUES and MIN_ISSUE_CREDIBILITY): when linked-issue scoring is active and +// contributor history is observed, falling below either floor zeroes the preview. +function issueDiscoveryHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { + const { issueDiscoveryHistoryMultiplier } = preview.scoreEstimate; + const { validSolvedIssues, validSolvedIssuesFloor, issueCredibility, issueCredibilityFloor } = preview.gates; + if (validSolvedIssues === undefined || issueCredibility === undefined) { + return { + component: "issueDiscoveryHistoryMultiplier", + band: "neutral", + summary: `Issue-discovery validity floor is not enforced for this preview (contributor issue-history is unobserved; upstream floors are ${validSolvedIssuesFloor} valid solved and ${issueCredibilityFloor} credibility).`, + lever: "No action needed for this preview; the validity floor applies once issue-discovery history is observed.", + leverageScore: 0, + }; + } + const band = bandForMultiplier(issueDiscoveryHistoryMultiplier); + const meetsFloor = validSolvedIssues >= validSolvedIssuesFloor && issueCredibility >= issueCredibilityFloor; + return { + component: "issueDiscoveryHistoryMultiplier", + band, + summary: meetsFloor + ? `Issue-discovery history (${validSolvedIssues} valid solved, credibility ${roundBand(issueCredibility)}) meets upstream floors (${validSolvedIssuesFloor} valid solved, ${issueCredibilityFloor} credibility).` + : `Issue-discovery history (${validSolvedIssues} valid solved, credibility ${roundBand(issueCredibility)}) is below upstream floors (${validSolvedIssuesFloor} valid solved, ${issueCredibilityFloor} credibility), so this preview is zeroed.`, + lever: meetsFloor + ? "Keep building valid solved-issue history with strong issue credibility." + : "Close more valid solved issues and improve issue credibility before relying on issue-discovery scoring.", + leverageScore: meetsFloor ? 8 : 100, + }; +} + // Upstream time-decay (#703), env-gated by SCORING_TIME_DECAY_ENABLED (default OFF) and opted into per-preview // via input.applyTimeDecay. When the flag is off (the common case) or the PR is fresh, the multiplier is 1 and // the breakdown reads as "not enabled" / "fresh" — surfacing the value is a no-op for those previews but @@ -318,6 +348,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo openPrBreakdown(preview), openIssueBreakdown(preview), mergedHistoryBreakdown(preview), + issueDiscoveryHistoryBreakdown(preview), timeDecayBreakdown(preview), ].map((entry) => ({ ...entry, diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 4a6038612d..29cbff40a5 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -85,6 +85,7 @@ describe("explainScoreBreakdown", () => { "openPrMultiplier", "openIssueMultiplier", "mergedHistoryMultiplier", + "issueDiscoveryHistoryMultiplier", "timeDecayMultiplier", ]), ); @@ -122,6 +123,57 @@ describe("explainScoreBreakdown", () => { expect(JSON.stringify(blocked)).not.toMatch(FORBIDDEN); }); + it("explains the issue-discovery validity floor as neutral (unobserved), full (meets floor), and blocked (below floor)", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.25 }, + }; + const baseInput = { + repoFullName: issueDiscoveryRepo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 5, + linkedIssueMode: "standard" as const, + }; + + const unobserved = explainScoreBreakdown(buildScorePreview({ repo: issueDiscoveryRepo, snapshot, input: baseInput })); + expect(unobserved.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "neutral", + leverageScore: 0, + }); + + const meets = explainScoreBreakdown( + buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { ...baseInput, validSolvedIssues: 4, issueCredibility: 0.9 }, + }), + ); + expect(meets.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "full", + summary: expect.stringMatching(/4 valid solved, private context 0.9/i), + }); + + const blocked = explainScoreBreakdown( + buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { ...baseInput, validSolvedIssues: 1, issueCredibility: 0.5 }, + }), + ); + expect(blocked.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "blocked", + summary: expect.stringMatching(/private context.*zeroed/i), + leverageScore: 100, + }); + expect(blocked.highestLeverageLever.lever).toMatch(/valid solved issues|issue credibility/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, From 5ff14a8626c8c42a9c7ca1cb8dc34c0b8c11a463 Mon Sep 17 00:00:00 2001 From: bohdansolovie Date: Wed, 1 Jul 2026 07:01:29 +0200 Subject: [PATCH 2/2] fix(scoring): mirror preview invariants in issue-discovery breakdown Handle inactive linked-issue mode and partial history inputs explicitly so breakdown wording matches upstream gating in preview.ts. Co-authored-by: Cursor --- src/services/score-breakdown.ts | 35 +++++++++++++++-- test/unit/score-breakdown.test.ts | 62 +++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 503afe7328..e4168e7b18 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -132,20 +132,47 @@ function mergedHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBre } // Sibling of mergedHistoryBreakdown for the issue-discovery validity floor (upstream -// MIN_VALID_SOLVED_ISSUES and MIN_ISSUE_CREDIBILITY): when linked-issue scoring is active and -// contributor history is observed, falling below either floor zeroes the preview. +// MIN_VALID_SOLVED_ISSUES and MIN_ISSUE_CREDIBILITY). Mirrors preview.ts: the multiplier +// stays 1 unless linked-issue mode is active AND both history fields are observed. function issueDiscoveryHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { issueDiscoveryHistoryMultiplier } = preview.scoreEstimate; const { validSolvedIssues, validSolvedIssuesFloor, issueCredibility, issueCredibilityFloor } = preview.gates; - if (validSolvedIssues === undefined || issueCredibility === undefined) { + const linkedIssueMode = preview.linkedIssueMultiplier.mode; + + if (linkedIssueMode === "none") { + return { + component: "issueDiscoveryHistoryMultiplier", + band: "neutral", + summary: + "Issue-discovery validity floor is not enforced for this preview (linked-issue mode is inactive; upstream only gates previews that claim linked-issue scoring).", + lever: "Use a linked-issue preview when planning issue-discovery work so validity floors can be evaluated.", + leverageScore: 0, + }; + } + + const hasValidSolved = validSolvedIssues !== undefined; + const hasIssueCredibility = issueCredibility !== undefined; + if (!hasValidSolved && !hasIssueCredibility) { return { component: "issueDiscoveryHistoryMultiplier", band: "neutral", summary: `Issue-discovery validity floor is not enforced for this preview (contributor issue-history is unobserved; upstream floors are ${validSolvedIssuesFloor} valid solved and ${issueCredibilityFloor} credibility).`, - lever: "No action needed for this preview; the validity floor applies once issue-discovery history is observed.", + lever: "No action needed for this preview; the validity floor applies once both valid solved count and issue credibility are observed.", leverageScore: 0, }; } + if (!hasValidSolved || !hasIssueCredibility) { + return { + component: "issueDiscoveryHistoryMultiplier", + band: "neutral", + summary: hasValidSolved + ? "Issue-discovery validity floor is not enforced for this preview (valid solved count is observed but issue credibility is not; upstream requires both before gating)." + : "Issue-discovery validity floor is not enforced for this preview (issue credibility is observed but valid solved count is not; upstream requires both before gating).", + lever: "Supply both valid solved-issue count and issue credibility before relying on issue-discovery validity floors.", + leverageScore: 0, + }; + } + const band = bandForMultiplier(issueDiscoveryHistoryMultiplier); const meetsFloor = validSolvedIssues >= validSolvedIssuesFloor && issueCredibility >= issueCredibilityFloor; return { diff --git a/test/unit/score-breakdown.test.ts b/test/unit/score-breakdown.test.ts index 29cbff40a5..0063fdd1eb 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -174,6 +174,68 @@ describe("explainScoreBreakdown", () => { expect(JSON.stringify(blocked)).not.toMatch(FORBIDDEN); }); + it("keeps issue-discovery validity neutral when linked-issue mode is inactive even if history is supplied", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.25 }, + }; + const preview = buildScorePreview({ + repo: issueDiscoveryRepo, + snapshot, + input: { + repoFullName: issueDiscoveryRepo.fullName, + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 0, + credibility: 1, + linkedIssueMode: "none", + validSolvedIssues: 4, + issueCredibility: 0.95, + }, + }); + expect(preview.scoreEstimate.issueDiscoveryHistoryMultiplier).toBe(1); + const breakdown = explainScoreBreakdown(preview); + expect(breakdown.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "neutral", + summary: expect.stringMatching(/linked-issue mode is inactive/i), + leverageScore: 0, + }); + }); + + it("keeps issue-discovery validity neutral when only one history field is observed", () => { + const issueDiscoveryRepo: RepositoryRecord = { + ...repo, + registryConfig: { ...repo.registryConfig!, issueDiscoveryShare: 0.25 }, + }; + const baseInput = { + repoFullName: issueDiscoveryRepo.fullName, + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 0, + credibility: 1, + mergedPullRequests: 5, + linkedIssueMode: "standard" as const, + }; + + const validSolvedOnly = explainScoreBreakdown( + buildScorePreview({ repo: issueDiscoveryRepo, snapshot, input: { ...baseInput, validSolvedIssues: 4 } }), + ); + expect(validSolvedOnly.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "neutral", + summary: expect.stringMatching(/valid solved count is observed but issue private context is not/i), + }); + + const credibilityOnly = explainScoreBreakdown( + buildScorePreview({ repo: issueDiscoveryRepo, snapshot, input: { ...baseInput, issueCredibility: 0.9 } }), + ); + expect(credibilityOnly.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ + band: "neutral", + summary: expect.stringMatching(/issue private context is observed but valid solved count is not/i), + }); + }); + it("explains an over-threshold open-issue count as a blocked open-issue spam gate", () => { const preview = buildScorePreview({ repo,