From f4e136cae52fba89d51a4dcf62b5b5113db3182b Mon Sep 17 00:00:00 2001 From: galuis116 Date: Wed, 1 Jul 2026 14:33:11 +0400 Subject: [PATCH] fix(signals): rank issue-quality report before capping to 100 --- src/signals/engine.ts | 7 ++++--- test/unit/issue-quality.test.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/signals/engine.ts b/src/signals/engine.ts index 939f852b3e..024d72cbef 100644 --- a/src/signals/engine.ts +++ b/src/signals/engine.ts @@ -740,6 +740,7 @@ const MAX_COLLISION_PAIRWISE_ISSUES = 80; const MAX_COLLISION_PAIRWISE_PULL_REQUESTS = 120; const MAX_COLLISION_PAIRWISE_RECENT_MERGES = 40; const ISSUE_DISCOVERY_LIFECYCLE_REPORT_CAP = 300; +const ISSUE_QUALITY_REPORT_CAP = 100; const REPO_OUTCOME_STALE_OPEN_DAYS = 30; const REPO_OUTCOME_MIN_DECIDED_SAMPLE = 3; const REPO_OUTCOME_MERGE_WELL_RATE = 0.7; @@ -2925,7 +2926,7 @@ export function buildIssueQualityReport( const lane = buildLaneAdvice(repo, fullName); const collisions = prebuiltCollisions ?? buildCollisionReport(fullName, issues, pullRequests, recentMergedPullRequests); const bountyByIssue = indexBountiesByIssue(bounties); - // Build per-issue indexes ONCE: the loop below runs over up to 100 open issues, and each previously re-scanned + // Build per-issue indexes ONCE: the loop below runs over every open issue, and each previously re-scanned // the full PR list (up to 10k) twice plus every collision cluster. O(issues·PRs) → O(issues + PRs). const prsByLinkedIssue = indexPullRequestsByLinkedIssue(pullRequests); const prByNumber = new Map(pullRequests.map((pr) => [pr.number, pr] as const)); @@ -2935,7 +2936,6 @@ export function buildIssueQualityReport( const lifecycleByIssue = new Map(buildIssueDiscoveryLifecycleReport(repo, issues, pullRequests, fullName, recentMergedPullRequests).states.map((entry) => [entry.number, entry])); const reports = issues .filter((issue) => issue.state === "open") - .slice(0, 100) .map((issue) => { const linkedPrs = resolveLinkedPullRequests(issue, pullRequests, prsByLinkedIssue, prByNumber); const linkedMergedPrs = resolveLinkedPullRequests(issue, recentMergedPullRequests, mergedPrsByLinkedIssue, mergedPrByNumber); @@ -2991,7 +2991,8 @@ export function buildIssueQualityReport( : "ready"; return { number: issue.number, title: issue.title, lifecycle, linkage, bounty: bountyContext, status, score, reasons, warnings }; }) - .sort((left, right) => right.score - left.score || left.number - right.number); + .sort((left, right) => right.score - left.score || left.number - right.number) + .slice(0, ISSUE_QUALITY_REPORT_CAP); return { repoFullName: fullName, generatedAt: nowIso(), diff --git a/test/unit/issue-quality.test.ts b/test/unit/issue-quality.test.ts index dda7f7bee0..97933bb9e9 100644 --- a/test/unit/issue-quality.test.ts +++ b/test/unit/issue-quality.test.ts @@ -293,6 +293,23 @@ describe("issue quality reports", () => { const report = buildIssueQualityReport(repo, issues, [], repo.fullName); expect(report.issues.length).toBeLessThanOrEqual(100); }); + + it("ranks by score before capping to 100, so a strong issue beyond the first 100 in DB order is not dropped (regression)", () => { + const repo = issueDiscoveryRepo("owner/rank-before-cap"); + // 110 thin, stale filler issues (low score) followed by one detailed, fresh, labelled issue (highest score) — + // capping by DB-order position before scoring would drop the strong issue entirely. + const filler = Array.from({ length: 110 }, (_, index) => + issue(repo.fullName, index + 1, `bulk ${index}`, { body: "Short.", updatedAt: "2025-01-01T00:00:00.000Z" }), + ); + const strongIssue = issue(repo.fullName, 111, "Detailed, well-labelled issue", { + body: "x".repeat(220), + labels: ["bug"], + updatedAt: now(), + }); + const report = buildIssueQualityReport(repo, [...filler, strongIssue], [], repo.fullName); + expect(report.issues).toHaveLength(100); + expect(report.issues[0]).toMatchObject({ number: 111, score: 100 }); + }); }); describe("buildContributorOpportunities x issue quality", () => {