diff --git a/src/services/ai-chat-qa.ts b/src/services/ai-chat-qa.ts index ad2def9d6b..9311b51a80 100644 --- a/src/services/ai-chat-qa.ts +++ b/src/services/ai-chat-qa.ts @@ -57,11 +57,13 @@ const PRIVATE_DECISION_BLOCKER_PATTERN = /\b(?:open_pr_pressure|closed_pr_credibility|low_credibility|maintainer_lane|inactive_or_unknown_lane|issue_discovery_only|merged_pr_history_floor|issue_discovery_validity_floor)\b/gi; const PRIVATE_BOUNDARY_TERM_PATTERN = /\b(?:wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|raw trust scores?|trust scores?|scoreability|reviewability|payouts?|rewards?|reward estimates?|farming|rankings?)\b/gi; +const PRIVATE_LANE_SIGNAL_PATTERN = + /\b(?:maintainer cut|direct PR lane share|direct PR share|direct PR|issue-discovery|split lane)(?:\s*[:(,;-]?\s*\d+(?:\.\d+)?%?)?/gi; // Public-safe forbidden-term guard on the MODEL's OWN output, mirroring ai-summaries' containsPublicForbiddenText: // near-miss phrasings the throwing word-list validator narrows (e.g. bare "estimated score") are also caught. const PUBLIC_FORBIDDEN_TEXT_PATTERN = - /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|raw trust scores?|trust scores?|estimated scores?|score estimates?|scoreability|score preview|public score estimates?|estimated rewards?|rewards?|reward estimates?|payouts?|farming|reviewability(?: internals?)?|private reviewability|private scoreability|private rankings?|rankings?|reward optimization)\b/i; + /\b(wallets?|hotkeys?|coldkeys?|seed phrases?|mnemonics?|raw trust scores?|trust scores?|estimated scores?|score estimates?|scoreability|score preview|public score estimates?|estimated rewards?|rewards?|reward estimates?|payouts?|farming|reviewability(?: internals?)?|private reviewability|private scoreability|private rankings?|rankings?|reward optimization|maintainer cut|direct PR lane share|direct PR share|issue-discovery|split lane)\b/i; type ChatGroundingAction = { actionType: string; @@ -192,6 +194,7 @@ function redactGroundingText(value: string): string { return value .replace(/\blikely_duplicate\b/gi, "possible overlap with existing work") .replace(PRIVATE_DECISION_BLOCKER_PATTERN, "private readiness context") + .replace(PRIVATE_LANE_SIGNAL_PATTERN, "private readiness context") .replace(PRIVATE_BOUNDARY_TERM_PATTERN, "private context") .trim(); } diff --git a/test/unit/ai-chat-qa.test.ts b/test/unit/ai-chat-qa.test.ts index 7a56b1cfe1..259236665a 100644 --- a/test/unit/ai-chat-qa.test.ts +++ b/test/unit/ai-chat-qa.test.ts @@ -256,6 +256,29 @@ describe("generateChatQaAnswer", () => { expect(userMessage).not.toMatch(/\blikely_duplicate\b/); }); + it("redacts private lane signals from cached rationale before prompting the provider", async () => { + const run = vi.fn(async () => ({ response: "Public-safe readiness answer." })); + const env = createTestEnv({ AI_ADVISORY: { run } as unknown as Ai, AI_DAILY_NEURON_BUDGET: "10000" }); + const result = await generateChatQaAnswer(env, { + bundle: bundleFixture(undefined, { + why: [ + "owner/repo: Maintainer cut: 1.", + "owner/repo: split lane (direct PR 1, issue-discovery 1); both lanes are useful here.", + "owner/repo: direct PR lane share 1 with no hard personal blocker.", + ], + }), + question: "what should I know?", + advisoryAiRouting: ADVISORY_ON, + repoFullName: "owner/repo", + issueNumber: 42, + }); + expect(result).toMatchObject({ status: "ok" }); + const call = run.mock.calls[0] as unknown as [string, { messages: Array<{ content: string }> }]; + const userMessage = call[1].messages[1]?.content ?? ""; + expect(userMessage).not.toMatch(/Maintainer cut|split lane|direct PR lane share|issue-discovery/i); + expect(userMessage).toContain("private readiness context"); + }); + it("honors a custom model override and clamps output tokens", async () => { const run = vi.fn(async () => ({ response: "Custom-model answer." })); const env = createTestEnv({ @@ -284,6 +307,13 @@ describe("generateChatQaAnswer", () => { expect(result).toMatchObject({ status: "unsafe" }); }); + it("withholds private lane signals if the provider repeats them", async () => { + const run = vi.fn(async () => ({ response: "This mentions direct PR lane share and issue-discovery details." })); + const env = createTestEnv({ AI_ADVISORY: { run } as unknown as Ai, AI_DAILY_NEURON_BUDGET: "10000" }); + const result = await generateChatQaAnswer(env, { bundle: bundleFixture(), question: "why?", advisoryAiRouting: ADVISORY_ON, repoFullName: "owner/repo", issueNumber: 1 }); + expect(result).toMatchObject({ status: "unsafe" }); + }); + it("reports an error status with the underlying message when the provider throws an Error", async () => { const run = vi.fn(async () => { throw new Error("provider_down"); @@ -317,6 +347,9 @@ describe("__chatQaInternals", () => { expect(redactGroundingText("blocked by open_pr_pressure")).toBe("blocked by private readiness context"); expect(redactGroundingText("do not mention a wallet or hotkey")).toBe("do not mention a private context or private context"); expect(redactGroundingText("likely_duplicate of #123")).toBe("possible overlap with existing work of #123"); + expect(redactGroundingText("Maintainer cut: 1; direct PR lane share 1; issue-discovery 1")).toBe( + "private readiness context; private readiness context; private readiness context", + ); expect(redactGroundingText("perfectly safe text")).toBe("perfectly safe text"); }); @@ -342,6 +375,7 @@ describe("__chatQaInternals", () => { it("flags forbidden public terms via the shared sanitizer and the local near-miss pattern", () => { expect(containsPublicForbiddenText("mentions a wallet")).toBe(true); + expect(containsPublicForbiddenText("mentions direct PR lane share details")).toBe(true); expect(containsPublicForbiddenText("perfectly safe prose")).toBe(false); });