diff --git a/src/services/score-breakdown.ts b/src/services/score-breakdown.ts index 69e2a7f95f..221b6086bf 100644 --- a/src/services/score-breakdown.ts +++ b/src/services/score-breakdown.ts @@ -156,6 +156,62 @@ function timeDecayBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdo }; } +// Issue-discovery validity floor (upstream MIN_VALID_SOLVED_ISSUES + MIN_ISSUE_CREDIBILITY; constants.py +// mirror #808): a preview that engages the issue-discovery lane but has fewer solved-issues or lower +// credibility than the upstream floors has the multiplier zeroed. Mirrors mergedHistoryBreakdown / +// openPr / openIssue so the public projection explains the gate cleanly without collapsing the two +// evidence dimensions into a single phrase (the nit that closed the previous attempt at this surface). +function issueDiscoveryHistoryBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { + const { issueDiscoveryHistoryMultiplier } = preview.scoreEstimate; + const { validSolvedIssues, validSolvedIssuesFloor, issueCredibility, issueCredibilityFloor } = preview.gates; + const relevant = preview.linkedIssueMultiplier.mode !== "none"; + const known = validSolvedIssues !== undefined && issueCredibility !== undefined; + if (issueDiscoveryHistoryMultiplier >= 1) { + if (!relevant) { + return { + component: "issueDiscoveryHistoryMultiplier", + band: "neutral", + summary: "Issue-discovery lane is not engaged for this preview (linked-issue mode is none), so the validity floor multiplier is 1 by intent.", + lever: "Select a linked-issue mode (standard or maintainer) on subsequent previews to engage the issue-discovery lane.", + leverageScore: 0, + }; + } + if (!known) { + const dimClauses: string[] = []; + if (validSolvedIssues !== undefined) dimClauses.push(`${validSolvedIssues} valid solved-issue(s) observed`); + else dimClauses.push("valid solved-issue count still missing"); + if (issueCredibility !== undefined) dimClauses.push(`${(issueCredibility * 100).toFixed(0)}% credibility observed`); + else dimClauses.push("issue credibility still missing"); + return { + component: "issueDiscoveryHistoryMultiplier", + band: "neutral", + summary: `Issue-discovery validity floor is partially observed for this preview (${dimClauses.join("; ")}; upstream floors are ${validSolvedIssuesFloor} solved and ${(issueCredibilityFloor * 100).toFixed(0)}% credibility).`, + lever: "No action needed; the floor is not enforced until both dimensions are observed in a subsequent preview.", + leverageScore: 0, + }; + } + return { + component: "issueDiscoveryHistoryMultiplier", + band: "full", + summary: `Issue-discovery evidence meets both upstream floors (${validSolvedIssues} solved / ${(issueCredibility! * 100).toFixed(0)}% credibility vs floors ${validSolvedIssuesFloor} / ${(issueCredibilityFloor * 100).toFixed(0)}%).`, + lever: "Keep issue-discovery activity healthy: file solvable issues and resolve them with validated PRs.", + leverageScore: 4, + }; + } + const failSolved = validSolvedIssues !== undefined && validSolvedIssues < validSolvedIssuesFloor; + const failCredibility = issueCredibility !== undefined && issueCredibility < issueCredibilityFloor; + const causes: string[] = []; + if (failSolved) causes.push(`valid solved issues (${validSolvedIssues} < ${validSolvedIssuesFloor})`); + if (failCredibility) causes.push(`issue credibility (${(issueCredibility! * 100).toFixed(0)}% < ${(issueCredibilityFloor * 100).toFixed(0)}%)`); + return { + component: "issueDiscoveryHistoryMultiplier", + band: "blocked", + summary: `Issue-discovery validity floor is failing the preview: ${causes.join(" and ")}.`, + lever: "Land more solved issues (each with a solving-PR token-score ≥ upstream MIN_TOKEN_SCORE_FOR_VALID_ISSUE) and stabilize credibility at or above the floor before relying on this preview.", + leverageScore: 100, + }; +} + function credibilityBreakdown(preview: ScorePreviewResult): ScoreMultiplierBreakdown { const { credibilityMultiplier } = preview.scoreEstimate; const { credibilityObserved, credibilityFloor } = preview.gates; @@ -319,6 +375,7 @@ export function explainScoreBreakdown(preview: ScorePreviewResult): ScoreBreakdo openIssueBreakdown(preview), mergedHistoryBreakdown(preview), timeDecayBreakdown(preview), + issueDiscoveryHistoryBreakdown(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 4a6038612d..907a428f7c 100644 --- a/test/unit/score-breakdown.test.ts +++ b/test/unit/score-breakdown.test.ts @@ -86,6 +86,7 @@ describe("explainScoreBreakdown", () => { "openIssueMultiplier", "mergedHistoryMultiplier", "timeDecayMultiplier", + "issueDiscoveryHistoryMultiplier", ]), ); for (const component of breakdown.components) { @@ -189,6 +190,141 @@ describe("explainScoreBreakdown", () => { expect(JSON.stringify(agedBreakdown)).not.toMatch(FORBIDDEN); }); + it("explains the issue-discovery history floor: lane-off / unobserved neutral, meeting-floor full, below-floor blocked", () => { + // Lane not engaged (linkedIssueMode=none) => multiplier is 1 by intent, breakdown neutral + lane-off copy. + const laneOff = explainScoreBreakdown( + buildScorePreview({ repo, snapshot, input: { repoFullName: repo.fullName, contributorLogin: "miner", sourceTokenScore: 40, totalTokenScore: 60, sourceLines: 80, openPrCount: 1, credibility: 0.9, linkedIssueMode: "none" } }), + ); + expect(laneOff.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ band: "neutral" }); + + // Lane engaged + only one dimension observed => partial-evidence neutral that explicitly names the + // missing dim (avoids the collapse-copy nit from #1874). + const partial = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 1, + credibility: 0.9, + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [1513], solvedByPullRequests: [22] }, + validSolvedIssues: 5, + // issueCredibility intentionally omitted to trigger the partial branch. + }, + }), + ); + const partialEntry = partial.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")!; + expect(partialEntry).toMatchObject({ band: "neutral" }); + // After the sanitizer replaces "credibility" with "private context" (#1874 nit: don't collapse the + // missing dimension into a generic "no solved-issue observed" — the copy must still name it). + expect(partialEntry.summary).toMatch(/still missing.*private context|partial/i); + + // Mirror partial: issueCredibility observed, validSolvedIssues missing. Same neutral band, + // copy names the OTHER missing dimension — the two halves of the nit must both be covered. + const partialMirror = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 1, + credibility: 0.9, + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [1513], solvedByPullRequests: [22] }, + // validSolvedIssues intentionally omitted; issueCredibility observed. + issueCredibility: 0.9, + }, + }), + ); + const partialMirrorEntry = partialMirror.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")!; + expect(partialMirrorEntry).toMatchObject({ band: "neutral" }); + expect(partialMirrorEntry.summary).toMatch(/valid solved-issue count still missing/); + + // Lane engaged + both observed + both meet upstream floors => full band with concrete numbers. + const meeting = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 1, + credibility: 0.9, + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [1513], solvedByPullRequests: [22] }, + validSolvedIssues: 5, + issueCredibility: 0.9, + }, + }), + ); + expect(meeting.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")).toMatchObject({ band: "full" }); + + // Lane engaged + validSolvedIssues below upstream floor => blocked band that names the failing dim + // specifically (avoid the same collapse-copy nit). + const belowFloor = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 1, + credibility: 0.9, + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [1513], solvedByPullRequests: [22] }, + validSolvedIssues: 0, + issueCredibility: 0.9, + }, + }), + ); + const blockedEntry = belowFloor.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")!; + expect(blockedEntry).toMatchObject({ band: "blocked", leverageScore: 100 }); + expect(blockedEntry.summary).toMatch(/valid solved issues/i); + expect(blockedEntry.lever).toMatch(/solved issues|MIN_TOKEN_SCORE_FOR_VALID_ISSUE/i); + expect(JSON.stringify(belowFloor)).not.toMatch(FORBIDDEN); + + // Mirror blocked: issueCredibility is below the upstream floor (the OTHER half of the below-floor + // nit must both be covered: both dimensions can independently trigger the gate). + const belowFloorCredibility = explainScoreBreakdown( + buildScorePreview({ + repo, + snapshot, + input: { + repoFullName: repo.fullName, + contributorLogin: "miner", + sourceTokenScore: 40, + totalTokenScore: 60, + sourceLines: 80, + openPrCount: 1, + credibility: 0.9, + linkedIssueMode: "standard", + linkedIssueContext: { status: "validated", source: "official_mirror", issueNumbers: [1513], solvedByPullRequests: [22] }, + validSolvedIssues: 5, + issueCredibility: 0.5, + }, + }), + ); + const blockedCredEntry = belowFloorCredibility.components.find((entry) => entry.component === "issueDiscoveryHistoryMultiplier")!; + expect(blockedCredEntry).toMatchObject({ band: "blocked" }); + // "credibility" is sanitized to "private context" before reaching the public surface. + expect(blockedCredEntry.summary).toMatch(/issue (credibility|private context)/i); + }); + it("includes gate highlights without leaking forbidden language", () => { const preview = buildScorePreview({ repo,