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
- Build contributor opportunities (
buildContributorOpportunities, reached via buildContributorFit → buildAndPersistContributorDecisionPack 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.
- Put a high-quality (
ready) issue beyond array index 5.
- 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.
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:availableIssuesisrepoIssues.filter(...), i.e. whatever order the issues arrive from the DB (effectively by ingestion / number, not by relevance). The variable is namedrankable, 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-issuestatus/scoreand returns the issues sorted best-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 6thavailableIssuein DB order.The per-issue
scorethe 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#742at array index 12 isstatus: "ready"with strong label overlap to the contributor's history.#742is 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.reasonslike "Issue quality report rates this issue as ready." can never appear for#742.#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
buildContributorOpportunities, reached viabuildContributorFit→buildAndPersistContributorDecisionPackandqueue/processors.ts:370) for a registered repo with more than 5 open unlinked issues whose DB order does not match quality/fit order.ready) issue beyond array index 5.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
rankableby 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-readyissues beyond index 5 are dropped before scoring and never surface.Suggested fix
Score all
rankableissues per repo, then keep that repo's top 5 by the computed opportunity score, instead of capping the raw list to 5 before scoring:score) for everyrankableissue (still skipping historical-bounty issues via the existingcontinue).scoredescending (tie-break on issue number, mirroring the producer) and take the top 5.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 /
readyissue sits beyond index 5 must appear in the returned opportunities and outrank the weaker first-5 issues.