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
7 changes: 4 additions & 3 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand All @@ -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);
Expand Down Expand Up @@ -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(),
Expand Down
17 changes: 17 additions & 0 deletions test/unit/issue-quality.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
Loading