Skip to content

[Bug]: Contributor opportunities cap each repo to its first 5 issues in raw DB order, discarding the issue-quality ranking — high-quality "ready" issues past index 5 are never surfaced #393

Description

@philluiz2323

Summary

buildContributorOpportunities (src/signals/engine.ts:1219-1309) selects, per registered repo, the issues to surface as contribution opportunities by taking the first 5 of an unsorted list:

// src/signals/engine.ts:1240-1249
const availableIssues = repoIssues.filter((issue) => issue.linkedPrs.length === 0 && !linkedIssueNumbers.has(issue.number));
// ...
const rankable = qualityByIssue
  ? availableIssues.filter((issue) => qualityByIssue.get(issue.number)?.status !== "do_not_use")
  : availableIssues;
for (const issue of rankable.slice(0, 5)) {           // <- first 5 in raw DB order, NOT ranked
  const quality = qualityByIssue?.get(issue.number);
  // ...computes a per-issue opportunity `score` (lane fit + label fit + qualityAdjustment ...)
  opportunities.push({ ..., score, ... });
}

availableIssues is repoIssues.filter(...), i.e. whatever order the issues arrive from the DB (effectively by ingestion / number, not by relevance). The variable is named rankable, but it is never ranked before .slice(0, 5). Only the ~5 issues that happen to sit first in DB order are ever scored and pushed; issues at index 5+ are silently dropped before the per-issue score is ever computed.

Why this is wrong

The issue-quality producer goes to real effort to rank issues. buildIssueQualityReport (engine.ts:2671-2744) computes a per-issue status/score and returns the issues sorted best-first:

// src/signals/engine.ts:2723, 2736
const score = clamp(100 - warnings.length * 18 + reasons.length * 5 - (age > 180 ? 15 : 0), 0, 100);
// ...
.sort((left, right) => right.score - left.score || left.number - right.number);   // ready/high-score issues first

The consumer throws that ordering away: it keys the report by issue number (engine.ts:1244) for per-issue lookups, but selects which issues to surface by raw DB order, then caps to 5. So the quality signal influences the score of whichever 5 issues were first, but not which 5 issues are chosen. A "ready" issue that the producer ranked #1 is invisible if it happens to be the 6th availableIssue in DB order.

The per-issue score the consumer itself computes (engine.ts:1265-1277, combining lane fit, label-history overlap, queue/bounty penalties, and the quality adjustment) is the value the whole pipeline ultimately ranks on — opportunities are globally sorted by it at the end (engine.ts:1308). But that score is only ever computed for the arbitrary first-5 per repo, so the global ranking is drawn from a biased sample: the genuinely best-fit issue for a contributor in a repo never enters the candidate set unless it is in the repo's first 5 by DB order.

Failure mode (concrete example)

A registered repo has 30 open, unlinked issues. By DB order the first 5 are all thin/stale (status: "hold"/"needs_proof"), while issue #742 at array index 12 is status: "ready" with strong label overlap to the contributor's history.

  • Current: the loop scores only the first 5; #742 is never turned into an opportunity. The contributor's opportunity list (and the decision pack built on top of it) surfaces 5 weak issues and omits the best match. reasons like "Issue quality report rates this issue as ready." can never appear for #742.
  • Correct: the repo's best-scoring issues (including the "ready", high-label-fit #742) are the ones surfaced and ranked.

This directly steers miners toward lower-fit issues and hides the highest-value work the system actually identified.

Steps to reproduce

  1. Build contributor opportunities (buildContributorOpportunities, reached via buildContributorFitbuildAndPersistContributorDecisionPack and queue/processors.ts:370) for a registered repo with more than 5 open unlinked issues whose DB order does not match quality/fit order.
  2. Put a high-quality (ready) issue beyond array index 5.
  3. Observe it is absent from the returned opportunities, while lower-scoring issues from the first 5 are present.

Expected behavior

The per-repo cap should select the best issues, not the first ones: rank rankable by the opportunity score (which already incorporates lane fit, label history, and the quality adjustment) and keep the top 5 per repo, consistent with the producer's score-descending ordering and with the global top-25 sort applied afterward.

Actual behavior

rankable.slice(0, 5) takes the first 5 issues in raw DB order, so only those are scored and considered; higher-scoring / quality-ready issues beyond index 5 are dropped before scoring and never surface.

Suggested fix

Score all rankable issues per repo, then keep that repo's top 5 by the computed opportunity score, instead of capping the raw list to 5 before scoring:

  • Build the opportunity objects (with their score) for every rankable issue (still skipping historical-bounty issues via the existing continue).
  • Sort that repo's opportunities by score descending (tie-break on issue number, mirroring the producer) and take the top 5.
  • Push those into the global list, which is then globally sorted and capped to 25 as today.

This makes the cap select the best-fit issues, restores the value of the issue-quality ranking, and also improves the no-quality-report path (which currently also samples the first 5 by DB order rather than by lane/label fit). Add fail-on-revert coverage: a repo whose highest-scoring / ready issue sits beyond index 5 must appear in the returned opportunities and outrank the weaker first-5 issues.

Metadata

Metadata

Assignees

No one assigned

    Labels

    slopAI slop and/or attempts to game additional points via manipulation or alt profiles.

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions