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
5 changes: 4 additions & 1 deletion src/services/ai-chat-qa.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
}
Expand Down
34 changes: 34 additions & 0 deletions test/unit/ai-chat-qa.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
});

Expand All @@ -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);
});

Expand Down