⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.
Context
RankedCandidateIssue = RawCandidateIssue & {...score fields} in
packages/loopover-miner/lib/opportunity-ranker.ts (~line 9), and RawCandidateIssue
(opportunity-fanout.ts, ~lines 39-54) includes assignees: string[], populated by
normalizeIssue (#7040).
normalizeCandidate in opportunity-ranker.ts (~lines 42-76) explicitly re-copies every other
field from the input candidate onto its return object — owner, repo, repoFullName,
issueNumber, title, labels, commentsCount, createdAt, updatedAt, htmlUrl,
aiPolicyAllowed, aiPolicySource — but never copies assignees.
Today's exclusion logic isn't broken by this: discover-cli.ts runs filterCandidatesByProfiles
(the repo-owner exclusion filter) on raw fan-out candidates before ranking, so it still sees the
real assignees field on the pre-ranked candidates. But rankIssues(kept, ...)'s output becomes
result.ranked in discover --json's output and options.onResult's payload — and that ranked,
post-normalizeCandidate output silently omits assignees despite RankedCandidateIssue's own
type promising the field is there (via the RawCandidateIssue & intersection).
Requirements
normalizeCandidate must copy assignees from the input candidate onto its return object,
following the same normalization discipline as the existing labels field (an array field
already handled with a type-safe filter/map): validate it's an array of strings, defaulting to
[] if the field is missing or malformed — mirror labels's exact validation shape
(Array.isArray(candidate.assignees) ? candidate.assignees.filter(...).map(...) : [], adjusted
for assignees not needing a .trim() if the existing RawCandidateIssue/normalizeIssue
producer never puts untrimmed values there — check normalizeIssue in the relevant fan-out file
to confirm whether trimming is needed for consistency).
- Do not change any other field's normalization in this function.
- Do not change
RawCandidateIssue, RankedCandidateIssue, or normalizeIssue — only
normalizeCandidate's copying of the already-typed assignees field.
Deliverables
All three deliverables are required in this single PR.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted, on all changed lines/branches
(applies to packages/loopover-miner/lib/**). The new copying logic and both new test cases must
be covered.
Expected Outcome
discover --json's ranked output (and options.onResult's payload) now actually includes
assignees for every ranked candidate, matching what RankedCandidateIssue's type already
promises, instead of silently dropping a field consumers may reasonably expect to be present.
Links & Resources
Context
RankedCandidateIssue = RawCandidateIssue & {...score fields}inpackages/loopover-miner/lib/opportunity-ranker.ts(~line 9), andRawCandidateIssue(
opportunity-fanout.ts, ~lines 39-54) includesassignees: string[], populated bynormalizeIssue(#7040).normalizeCandidateinopportunity-ranker.ts(~lines 42-76) explicitly re-copies every otherfield from the input candidate onto its return object —
owner,repo,repoFullName,issueNumber,title,labels,commentsCount,createdAt,updatedAt,htmlUrl,aiPolicyAllowed,aiPolicySource— but never copiesassignees.Today's exclusion logic isn't broken by this:
discover-cli.tsrunsfilterCandidatesByProfiles(the repo-owner exclusion filter) on raw fan-out candidates before ranking, so it still sees the
real
assigneesfield on the pre-ranked candidates. ButrankIssues(kept, ...)'s output becomesresult.rankedindiscover --json's output andoptions.onResult's payload — and that ranked,post-
normalizeCandidateoutput silently omitsassigneesdespiteRankedCandidateIssue's owntype promising the field is there (via the
RawCandidateIssue &intersection).Requirements
normalizeCandidatemust copyassigneesfrom the input candidate onto its return object,following the same normalization discipline as the existing
labelsfield (an array fieldalready handled with a type-safe filter/map): validate it's an array of strings, defaulting to
[]if the field is missing or malformed — mirrorlabels's exact validation shape(
Array.isArray(candidate.assignees) ? candidate.assignees.filter(...).map(...) : [], adjustedfor
assigneesnot needing a.trim()if the existingRawCandidateIssue/normalizeIssueproducer never puts untrimmed values there — check
normalizeIssuein the relevant fan-out fileto confirm whether trimming is needed for consistency).
RawCandidateIssue,RankedCandidateIssue, ornormalizeIssue— onlynormalizeCandidate's copying of the already-typedassigneesfield.Deliverables
normalizeCandidateinopportunity-ranker.tscopiesassigneesonto its return object,validated the same way
labelsalready is.assigneesarray survivesnormalizeCandidate/rankIssueswithassigneespresent and correct in the ranked output.assigneesfield normalizes to[]rather than throwing or propagating an invalid value.All three deliverables are required in this single PR.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted, on all changed lines/branches
(applies to
packages/loopover-miner/lib/**). The new copying logic and both new test cases mustbe covered.
Expected Outcome
discover --json's ranked output (andoptions.onResult's payload) now actually includesassigneesfor every ranked candidate, matching whatRankedCandidateIssue's type alreadypromises, instead of silently dropping a field consumers may reasonably expect to be present.
Links & Resources
packages/loopover-miner/lib/opportunity-ranker.ts(normalizeCandidate, ~lines 42-76;RankedCandidateIssuetype, ~line 9)packages/loopover-miner/lib/opportunity-fanout.ts(RawCandidateIssuetype, ~lines 39-54;normalizeIssue, AMS contribution-profile: exclude candidates assigned to the repo owner in discover #7040 — whereassigneesis originally populated)