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
14 changes: 11 additions & 3 deletions packages/loopover-miner/lib/discover-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,11 +186,19 @@ async function supplementWithDiscoveryIndex(
if (!isDiscoveryPlaneEnabled(env)) return fanOut;
const queryIndex = options.queryDiscoveryIndex ?? queryDiscoveryIndex;
const response = await queryIndex(queryScope, { env });
recordDiscoveryTelemetry("discover_query", response.candidates.length > 0 ? "supplemented" : "empty", { env });
if (response.candidates.length === 0) return fanOut;
// #9680: the hosted index preserves a repo's AI-contribution ban (normalizeDiscoveryIndexCandidate keeps
// aiPolicyAllowed:false), but nothing downstream re-checks it -- the `as RawCandidateIssue` cast below would
// launder a `false` through a type declared as the literal `true`, so an AI-banned repo's issue would be ranked
// and enqueued. Enforce the ban here exactly as the local fetchTargetIssues does (`if (!verdict.allowed) return
// []`): drop it, not down-rank or warn. A candidate that omits the field is kept (`!== false`, matching
// normalizeDiscoveryIndexCandidate's own default).
const aiAllowed = response.candidates.filter((candidate) => candidate.aiPolicyAllowed !== false);
const droppedAiBanned = response.candidates.length - aiAllowed.length;
recordDiscoveryTelemetry("discover_query", response.candidates.length > 0 ? "supplemented" : "empty", { env, droppedAiBanned });
if (aiAllowed.length === 0) return fanOut;

const seen = new Set(fanOut.issues.map((issue) => dedupeKey(issue.repoFullName, issue.issueNumber)));
const supplemented = response.candidates
const supplemented = aiAllowed
.filter((candidate) => !seen.has(dedupeKey(candidate.repoFullName, candidate.issueNumber)))
// DiscoveryIndexCandidate is a near-superset of RawCandidateIssue; copy the real assignees through when the
// hosted contract carried them (#7442), falling back to [] only when the served response genuinely omitted the
Expand Down
11 changes: 9 additions & 2 deletions packages/loopover-miner/lib/discovery-index-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,16 @@ export async function submitSoftClaim(
export function recordDiscoveryTelemetry(
event: string,
outcome: string,
options: { env?: Record<string, string | undefined> } = {},
options: { env?: Record<string, string | undefined>; droppedAiBanned?: number } = {},
): void {
const env = options.env ?? process.env;
if (!isDiscoveryPlaneEnabled(env) || !isDiscoveryTelemetryEnabled(env)) return;
getLogger().info("discovery_plane_telemetry", { event, outcome });
// #9680: low-cardinality count of index candidates dropped for an AI-contribution ban this query, when the
// caller supplies it. Spread conditionally so callers that don't pass it keep the exact { event, outcome }
// shape (and payload) they emitted before.
getLogger().info("discovery_plane_telemetry", {
event,
outcome,
...(options.droppedAiBanned !== undefined ? { droppedAiBanned: options.droppedAiBanned } : {}),
});
}
83 changes: 83 additions & 0 deletions test/unit/miner-discover-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2035,6 +2035,89 @@ describe("discovery-index supplementation (#7168)", () => {
]);
});

it("REGRESSION (#9680): drops an index candidate whose repo bans AI contributions, keeps the allowed one, and reports the dropped count", async () => {
const clientModule = await import("../../packages/loopover-miner/lib/discovery-index-client");
const telemetrySpy = vi.spyOn(clientModule, "recordDiscoveryTelemetry");
const portfolioQueue = tempQueueStore();
const fetchCandidateIssuesWithSummary = vi.fn(async () => ({
issues: [fanOutIssue({ issueNumber: 1, title: "Local candidate" })],
warnings: [],
rateLimitRemaining: 100,
rateLimitResetAt: null,
}));
const queryDiscoveryIndex = vi.fn(async () => ({
contractVersion: 1,
candidates: [
indexCandidate({ issueNumber: 2, title: "AI-banned repo issue", aiPolicyAllowed: false }),
indexCandidate({ issueNumber: 3, title: "Allowed index issue", aiPolicyAllowed: true }),
],
nextCursor: null,
}));
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);

const exitCode = await runDiscover(["acme/widgets", "--json"], {
nowMs: NOW,
env: { LOOPOVER_MINER_DISCOVERY_PLANE: "true" },
initPortfolioQueue: () => portfolioQueue,
initPolicyDocCache: () => tempPolicyDocCacheStore(),
initPolicyVerdictCache: () => tempPolicyVerdictCacheStore(),
initRankedCandidatesStore: () => tempRankedCandidatesStore(),
fetchCandidateIssuesWithSummary,
queryDiscoveryIndex: queryDiscoveryIndex as never,
});

expect(exitCode).toBe(0);
const payload = JSON.parse(String(log.mock.calls[0]?.[0]));
const issueNumbers = payload.ranked
.map((entry: { issueNumber: number }) => entry.issueNumber)
.sort((a: number, b: number) => a - b);
// #2 (aiPolicyAllowed:false) is dropped before ranking; the local #1 and the allowed index #3 remain.
// Before the fix #2 would also be ranked and enqueued into the miner's backlog.
expect(issueNumbers).toEqual([1, 3]);
expect(telemetrySpy).toHaveBeenCalledWith(
"discover_query",
"supplemented",
expect.objectContaining({ droppedAiBanned: 1 }),
);
});

it("REGRESSION (#9680): keeps an index candidate that OMITS aiPolicyAllowed (field-absent is not a ban)", async () => {
const portfolioQueue = tempQueueStore();
const fetchCandidateIssuesWithSummary = vi.fn(async () => ({
issues: [fanOutIssue({ issueNumber: 1, title: "Local candidate" })],
warnings: [],
rateLimitRemaining: 100,
rateLimitResetAt: null,
}));
const candidateNoField = indexCandidate({ issueNumber: 4, title: "Older-build candidate" });
delete (candidateNoField as { aiPolicyAllowed?: unknown }).aiPolicyAllowed;
const queryDiscoveryIndex = vi.fn(async () => ({
contractVersion: 1,
candidates: [candidateNoField],
nextCursor: null,
}));
const log = vi.spyOn(console, "log").mockImplementation(() => undefined);

const exitCode = await runDiscover(["acme/widgets", "--json"], {
nowMs: NOW,
env: { LOOPOVER_MINER_DISCOVERY_PLANE: "true" },
initPortfolioQueue: () => portfolioQueue,
initPolicyDocCache: () => tempPolicyDocCacheStore(),
initPolicyVerdictCache: () => tempPolicyVerdictCacheStore(),
initRankedCandidatesStore: () => tempRankedCandidatesStore(),
fetchCandidateIssuesWithSummary,
queryDiscoveryIndex: queryDiscoveryIndex as never,
});

expect(exitCode).toBe(0);
const payload = JSON.parse(String(log.mock.calls[0]?.[0]));
const issueNumbers = payload.ranked
.map((entry: { issueNumber: number }) => entry.issueNumber)
.sort((a: number, b: number) => a - b);
// A candidate that never carried aiPolicyAllowed must NOT become a fail-closed drop.
expect(issueNumbers).toEqual([1, 4]);
});

it("uses the --search term as the discovery-index scope in search mode", async () => {
const portfolioQueue = tempQueueStore();
const searchCandidateIssuesWithSummary = vi.fn(async () => ({
Expand Down
12 changes: 12 additions & 0 deletions test/unit/miner-discovery-index-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,18 @@ describe("recordDiscoveryTelemetry (#7168)", () => {
});
expect(logSpy.info).toHaveBeenCalledWith("discovery_plane_telemetry", { event: "discover_query", outcome: "supplemented" });
});

it("#9680: carries the droppedAiBanned count in the payload when the caller supplies it", () => {
recordDiscoveryTelemetry("discover_query", "supplemented", {
env: { [DISCOVERY_PLANE_FLAG]: "true", [DISCOVERY_TELEMETRY_FLAG]: "true" },
droppedAiBanned: 2,
});
expect(logSpy.info).toHaveBeenCalledWith("discovery_plane_telemetry", {
event: "discover_query",
outcome: "supplemented",
droppedAiBanned: 2,
});
});
});

describe("defaulted options (real process.env / real global fetch) (#7168)", () => {
Expand Down