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: 12 additions & 2 deletions src/services/ai-review.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,22 @@ export function estimateNeurons(promptChars: number, maxOutputTokens: number, ca
return Math.max(1, Math.ceil((inputTokens + maxOutputTokens) * 0.035) * Math.max(1, calls));
}

/** Returns the text unchanged if it is public-safe, otherwise null (drop — never publish). */
function neutralizePublicMarkdown(text: string): string {
return text
.replace(/[\u0000-\u001f\u007f-\u009f]+/g, " ")
.replace(/\s+/g, " ")
.trim()
.replace(/@/g, "@\u200B")
.replace(/:\/\//g, ":\u200B//")
.replace(/([\\`*_{}\[\]()#+!|])/g, "\\$1");
}

/** Returns neutralized text if it is public-safe, otherwise null (drop — never publish). */
export function toPublicSafe(text: string | null | undefined): string | null {
const trimmed = (text ?? "").trim();
if (!trimmed) return null;
try {
return sanitizePublicComment(trimmed);
return neutralizePublicMarkdown(sanitizePublicComment(trimmed));
} catch {
return null;
}
Expand Down
6 changes: 3 additions & 3 deletions src/signals/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4088,9 +4088,9 @@ export function buildPublicPrIntelligenceComment(args: {
"",
"_Generated from public PR metadata and the diff. Advisory only; deterministic signals remain authoritative._",
"",
// Notes are already public-safe (built via toPublicSafe upstream). Escape angle brackets so a
// stray tag (e.g. </details> or an HTML comment marker) cannot break the panel structure, while
// preserving the markdown bullet/line layout that sanitizePanelText would otherwise flatten.
// Notes are already public-safe and markdown-neutralized (built via toPublicSafe upstream). Escape
// angle brackets as a final guard so a stray tag (e.g. </details> or an HTML comment marker) cannot
// break the panel structure while preserving the section/bullet layout we add ourselves.
args.aiReview.notes.replace(/[<>]/g, (char) => (char === "<" ? "&lt;" : "&gt;")).slice(0, 4000),
"",
"</details>",
Expand Down
5 changes: 4 additions & 1 deletion test/unit/ai-review.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,12 @@ describe("Workers AI fallback + degraded output", () => {
});

describe("pure helpers", () => {
it("toPublicSafe drops forbidden public text and keeps safe text", () => {
it("toPublicSafe drops forbidden public text and neutralizes markdown, mentions, links, and control characters", () => {
expect(toPublicSafe("This change is solid.")).toBe("This change is solid.");
expect(toPublicSafe("Boost your reward payout")).toBeNull();
expect(toPublicSafe("Ping @octo-team about [urgent update](https://evil.example/p) ![pixel](https://evil.example/i.png)\n- injected")).toBe(
"Ping @\u200Bocto-team about \\[urgent update\\]\\(https:\u200B//evil.example/p\\) \\!\\[pixel\\]\\(https:\u200B//evil.example/i.png\\) - injected",
);
expect(toPublicSafe("")).toBeNull();
expect(toPublicSafe(null)).toBeNull();
expect(toPublicSafe(undefined)).toBeNull();
Expand Down
Loading