From 10dd000e8e55af0cd9f77ea43be35d0a024cad85 Mon Sep 17 00:00:00 2001 From: galuis116 Date: Thu, 4 Jun 2026 13:44:14 -0700 Subject: [PATCH] Rank contributor opportunities before the per-repo cap so the best issues surface --- src/signals/engine.ts | 10 ++++++++-- test/unit/issue-quality.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 7aab610cd9..9c12801474 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -1246,7 +1246,11 @@ export function buildContributorOpportunities( const rankable = qualityByIssue ? availableIssues.filter((issue) => qualityByIssue.get(issue.number)?.status !== "do_not_use") : availableIssues; - for (const issue of rankable.slice(0, 5)) { + // Score every eligible issue, then keep this repo's best 5 by score -- the cap must select the + // strongest-fit issues (mirroring the issue-quality report's score-descending order), not the + // arbitrary first 5 in DB order. + const repoOpportunities: ContributorOpportunity[] = []; + for (const issue of rankable) { const quality = qualityByIssue?.get(issue.number); const bounty = bountyByIssue.get(bountyIssueKey(repo.fullName, issue.number)) ?? null; const bountyLifecycle = bounty ? classifyBountyLifecycle(bounty, issue) : null; @@ -1277,7 +1281,7 @@ export function buildContributorOpportunities( ); const baseFit = score >= 70 ? "good" : score >= 40 ? "caution" : "hold"; const downgradeToCaution = (bountyPenalty > 0 || quality?.status === "needs_proof") && baseFit === "good"; - opportunities.push({ + repoOpportunities.push({ repoFullName: repo.fullName, issueNumber: issue.number, title: issue.title, @@ -1302,6 +1306,8 @@ export function buildContributorOpportunities( ], }); } + repoOpportunities.sort((left, right) => right.score - left.score || (left.issueNumber ?? 0) - (right.issueNumber ?? 0)); + opportunities.push(...repoOpportunities.slice(0, 5)); } /* v8 ignore next -- Repo-name tie ordering is deterministic presentation fallback after scored opportunity ranking. */ diff --git a/test/unit/issue-quality.test.ts b/test/unit/issue-quality.test.ts index 384b88b7db..8edd9dff64 100644 --- a/test/unit/issue-quality.test.ts +++ b/test/unit/issue-quality.test.ts @@ -238,6 +238,28 @@ describe("buildContributorOpportunities x issue quality", () => { expect(opportunities[0]?.reasons).toEqual(expect.arrayContaining(["Issue quality report rates this issue as ready."])); }); + it("surfaces the highest-quality issue even when it sits beyond the first 5 in DB order", () => { + const repo = issueDiscoveryRepo("owner/deep-ready"); + // Six available issues in DB order; the only "ready" one is last (index 5), the rest are "hold". + const issues = [1, 2, 3, 4, 5, 6].map((n) => issue(repo.fullName, n, `Issue ${n}`, { body: "x".repeat(220), labels: ["bug"] })); + const quality: IssueQualityReport = { + repoFullName: repo.fullName, + generatedAt: now(), + lane: { repoFullName: repo.fullName, lane: "issue_discovery", issueDiscoveryShare: 1, directPrShare: 0, summary: "", contributorGuidance: "", maintainerGuidance: "" }, + issues: [ + ...[1, 2, 3, 4, 5].map((n) => ({ number: n, title: `Issue ${n}`, status: "hold" as const, score: 30, reasons: [], warnings: [] })), + { number: 6, title: "Issue 6", status: "ready" as const, score: 92, reasons: [], warnings: [] }, + ], + summary: "", + }; + const opportunities = buildContributorOpportunities(sampleProfile(), [repo], issues, [], [], new Map([[repo.fullName, quality]])); + // The "ready" issue #6 (highest score) must be surfaced and ranked first, even though it is the + // 6th available issue -- the per-repo cap selects the best, not the arbitrary first 5. + expect(opportunities.map((o) => o.issueNumber)).toContain(6); + expect(opportunities[0]?.issueNumber).toBe(6); + expect(opportunities[0]?.reasons).toEqual(expect.arrayContaining(["Issue quality report rates this issue as ready."])); + }); + it("downgrades needs_proof issues to caution and adds a warning", () => { const repo = issueDiscoveryRepo("owner/caution"); const issues = [issue(repo.fullName, 1, "Vague candidate", { body: "x".repeat(220), labels: ["bug"] })];