From 4be63de173ee301ca65bf467be90a1aed27f406d Mon Sep 17 00:00:00 2001 From: Ismail Pelaseyed Date: Mon, 25 May 2026 12:18:51 +0200 Subject: [PATCH] add support for ai-prompts in the comments --- src/services/__tests__/prScan.test.ts | 16 ++++++- src/services/prScan.ts | 63 ++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/src/services/__tests__/prScan.test.ts b/src/services/__tests__/prScan.test.ts index 5e0c101..9683f0b 100644 --- a/src/services/__tests__/prScan.test.ts +++ b/src/services/__tests__/prScan.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { CHECK_NAMES, DEFAULT_CONFIG } from "../../lib/types.js"; +import { CHECK_NAMES, DEFAULT_CONFIG, MARKERS } from "../../lib/types.js"; import { runPrScan } from "../prScan.js"; +import { fingerprintPrFindingCommentBody } from "../prFindings.js"; const isPrFindingFingerprintDismissedMock = vi.hoisted(() => vi.fn()); @@ -97,8 +98,21 @@ describe("runPrScan", () => { expect(body).toContain("A new postinstall hook executes a remote script."); expect(body).toContain("Remove the lifecycle hook"); expect(body).toContain("**P1:** Suspicious lifecycle hook"); + expect(body).toContain("AI prompt"); + expect(body).toContain("Check if this security scanner issue is valid."); + expect(body).toContain(''); + expect(body).toContain(''); + expect(body).toContain("Suspicious lifecycle hook"); expect(body).not.toContain("Fix:"); expect(body).not.toContain("Recommended fix"); + expect(fingerprintPrFindingCommentBody(body)).toBe( + fingerprintPrFindingCommentBody(`${MARKERS.PR_FINDING} +**P1:** Suspicious lifecycle hook + +A new postinstall hook executes a remote script. + +Remove the lifecycle hook or replace it with reviewed local setup code.`), + ); }); it("does not recreate comments for previously dismissed findings", async () => { diff --git a/src/services/prScan.ts b/src/services/prScan.ts index bf50a94..4e48a04 100644 --- a/src/services/prScan.ts +++ b/src/services/prScan.ts @@ -137,7 +137,7 @@ function isFindingDismissed( } function fingerprintPrFinding(finding: PrFinding): string { - return fingerprintPrFindingCommentBody(renderInlineFindingCommentBody(finding))!; + return fingerprintPrFindingCommentBody(renderInlineFindingFingerprintBody(finding))!; } function renderFindingsSummary(findings: PrFinding[]): string { @@ -209,10 +209,16 @@ async function deleteInlineFindingComments( function renderInlineFindingComment(finding: PrFinding): string { const body = renderInlineFindingCommentBody(finding); - return appendPrFindingFingerprint(body, fingerprintPrFindingCommentBody(body)!); + return appendPrFindingFingerprint(body, fingerprintPrFinding(finding)); } function renderInlineFindingCommentBody(finding: PrFinding): string { + return `${renderInlineFindingFingerprintBody(finding)} + +${renderAiPrompt(finding)}`; +} + +function renderInlineFindingFingerprintBody(finding: PrFinding): string { const evidence = compactSentence(finding.short_evidence ?? finding.evidence, 180); const recommendation = compactSentence( finding.short_recommendation ?? finding.recommendation, @@ -227,6 +233,59 @@ ${evidence} ${recommendation}`; } +function renderAiPrompt(finding: PrFinding): string { + return `
+AI prompt + +${markdownCodeFence(renderAiPromptText(finding))} + +
`; +} + +function renderAiPromptText(finding: PrFinding): string { + const file = finding.file ?? "unknown"; + const location = finding.file + ? `${finding.file}${finding.line ? `:${finding.line}` : ""}` + : finding.line + ? `line ${finding.line}` + : "unknown"; + + return `Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior. + + + +${escapeXmlText(formatFindingPriority(finding.severity))} +${escapeXmlText(finding.title)} +${escapeXmlText(finding.evidence)} +${escapeXmlText(finding.recommendation)} + +`; +} + +function markdownCodeFence(value: string): string { + const fenceLength = Math.max( + 3, + ...Array.from(value.matchAll(/`+/g), (match) => match[0].length + 1), + ); + const fence = "`".repeat(fenceLength); + return `${fence}text +${value} +${fence}`; +} + +function escapeXmlText(value: string): string { + return value + .replace(/&/g, "&") + .replace(//g, ">"); +} + +function escapeXmlAttribute(value: string): string { + return escapeXmlText(value) + .replace(/"/g, """) + .replace(/'/g, "'"); +} + function compactSentence(value: string, maxLength: number): string { const compact = value.replace(/\s+/g, " ").trim(); if (compact.length <= maxLength) return compact;