⚠️ 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
supplementWithDiscoveryIndex in packages/loopover-miner/lib/discover-cli.ts folds candidates served by the optional hosted discovery index into the local fan-out result:
const supplemented = response.candidates
.filter((candidate) => !seen.has(dedupeKey(candidate.repoFullName, candidate.issueNumber)))
.map((candidate) => ({ ...candidate, assignees: [...(candidate.assignees ?? [])], labels: [...candidate.labels] }) as RawCandidateIssue);
Dedupe is the only filter. Meanwhile:
RawCandidateIssue.aiPolicyAllowed is declared as the literal type true (packages/loopover-miner/lib/opportunity-fanout.ts:52), so the as RawCandidateIssue cast is laundering a possible false through a type that promises it can never be false.
- The index client deliberately preserves a
false: normalizeDiscoveryIndexCandidate writes aiPolicyAllowed: candidate.aiPolicyAllowed !== false.
- The local path enforces the ban hard:
fetchTargetIssues does if (!verdict.allowed) return []; (packages/loopover-miner/lib/opportunity-fanout.ts:522-523).
- Nothing downstream re-checks it.
opportunity-ranker.ts:77 only copies the field through and never filters on it; portfolio-discovery.ts's normalizeRankedIssue ignores it entirely.
So an index-served candidate flagged "this repo bans automated contributions" is ranked and enqueued into the local portfolio backlog, where the loop will later claim and attempt it. discover-cli.ts's own module contract describes the index as only partially trusted and already hardens against a misbehaving host elsewhere (page-size clamping), which is exactly the posture this filter is missing.
Requirements
supplementWithDiscoveryIndex must drop any index-served candidate whose aiPolicyAllowed is false, before the dedupe filter's output is cast to RawCandidateIssue.
- The check must be
candidate.aiPolicyAllowed !== false (so a candidate that omits the field entirely is still kept, matching normalizeDiscoveryIndexCandidate's own default).
- The dropped count must be surfaced: extend the existing
recordDiscoveryTelemetry("discover_query", ...) call in the same function to also carry the number of candidates dropped for this reason, using the same options-object shape that call already passes.
- No change to the local fan-out path, to the ranker, or to
RawCandidateIssue's declared type.
⚠️ Required pattern: mirror fetchTargetIssues's own ban enforcement at packages/loopover-miner/lib/opportunity-fanout.ts:522-523 — the candidate is dropped, not down-ranked and not warned about. It does NOT satisfy this issue to relax RawCandidateIssue.aiPolicyAllowed from true to boolean, to add the filter in opportunity-ranker.ts or portfolio-discovery.ts (the local path already guarantees the invariant before ranking; a second downstream filter would be a competing mechanism), or to add a config flag that makes the drop optional.
Deliverables
All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the filter but omitting the "field absent is still kept" case, which proves the filter did not become a fail-closed drop of every index candidate — does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/lib/**/*.ts IS inside Codecov's coverage.include in vitest.config.ts, so the 99%+ branch-counted codecov/patch gate applies exactly as for src/**. Both arms of the new aiPolicyAllowed !== false predicate need a test (dropped and kept), plus the field-absent arm, plus the telemetry count assertion. The fix needs a named regression test that fails against the current code.
Expected Outcome
A compromised, stale, or simply buggy discovery-index response can no longer put an AI-banned repo's issue into the miner's own portfolio backlog; the local fan-out's if (!verdict.allowed) return [] invariant holds for index-supplemented candidates too.
Links & Resources
packages/loopover-miner/lib/discover-cli.ts:180-201, packages/loopover-miner/lib/opportunity-fanout.ts:52, :515-523, packages/loopover-miner/lib/discovery-index-client.ts.
Context
supplementWithDiscoveryIndexinpackages/loopover-miner/lib/discover-cli.tsfolds candidates served by the optional hosted discovery index into the local fan-out result:Dedupe is the only filter. Meanwhile:
RawCandidateIssue.aiPolicyAllowedis declared as the literal typetrue(packages/loopover-miner/lib/opportunity-fanout.ts:52), so theas RawCandidateIssuecast is laundering a possiblefalsethrough a type that promises it can never befalse.false:normalizeDiscoveryIndexCandidatewritesaiPolicyAllowed: candidate.aiPolicyAllowed !== false.fetchTargetIssuesdoesif (!verdict.allowed) return [];(packages/loopover-miner/lib/opportunity-fanout.ts:522-523).opportunity-ranker.ts:77only copies the field through and never filters on it;portfolio-discovery.ts'snormalizeRankedIssueignores it entirely.So an index-served candidate flagged "this repo bans automated contributions" is ranked and enqueued into the local portfolio backlog, where the loop will later claim and attempt it.
discover-cli.ts's own module contract describes the index as only partially trusted and already hardens against a misbehaving host elsewhere (page-size clamping), which is exactly the posture this filter is missing.Requirements
supplementWithDiscoveryIndexmust drop any index-served candidate whoseaiPolicyAllowedisfalse, before the dedupe filter's output is cast toRawCandidateIssue.candidate.aiPolicyAllowed !== false(so a candidate that omits the field entirely is still kept, matchingnormalizeDiscoveryIndexCandidate's own default).recordDiscoveryTelemetry("discover_query", ...)call in the same function to also carry the number of candidates dropped for this reason, using the same options-object shape that call already passes.RawCandidateIssue's declared type.Deliverables
supplementWithDiscoveryIndexfilters out candidates withaiPolicyAllowed === falsebefore buildingsupplemented.recordDiscoveryTelemetry("discover_query", ...)call in that function reports the number of candidates dropped by the new filter.test/unit/miner-discover-cli.test.tsinjects aqueryDiscoveryIndexreturning one candidate withaiPolicyAllowed: falseand one withtrue, and asserts only thetrueone reaches the ranked/enqueued output.aiPolicyAllowedentirely is still kept.All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example adding the filter but omitting the "field absent is still kept" case, which proves the filter did not become a fail-closed drop of every index candidate — does not resolve this issue.
Test Coverage Requirements
packages/loopover-miner/lib/**/*.tsIS inside Codecov'scoverage.includeinvitest.config.ts, so the 99%+ branch-countedcodecov/patchgate applies exactly as forsrc/**. Both arms of the newaiPolicyAllowed !== falsepredicate need a test (dropped and kept), plus the field-absent arm, plus the telemetry count assertion. The fix needs a named regression test that fails against the current code.Expected Outcome
A compromised, stale, or simply buggy discovery-index response can no longer put an AI-banned repo's issue into the miner's own portfolio backlog; the local fan-out's
if (!verdict.allowed) return []invariant holds for index-supplemented candidates too.Links & Resources
packages/loopover-miner/lib/discover-cli.ts:180-201,packages/loopover-miner/lib/opportunity-fanout.ts:52,:515-523,packages/loopover-miner/lib/discovery-index-client.ts.