import type { GeminiAcpConfig } from "../types.ts";
import { providerError } from "./provider-result.ts";
import type { PromptDeps, PromptRunResult, PromptUpdateHandler } from "./run.ts";
import { runPrompt } from "./run.ts";
const DEFAULT_MAX_FINDINGS = 12;
export const CODE_REVIEW_SECTIONS = ["Blockers", "Important", "Optional", "Validation"] as const;
export const CODE_REVIEW_FOCUS_AREAS = [
"correctness",
"security",
"performance",
"maintainability",
"tests",
"api",
"documentation",
] as const;
export const CODE_REVIEW_SEVERITY_THRESHOLDS = ["all", "important", "blockers"] as const;
export type CodeReviewFocusArea = (typeof CODE_REVIEW_FOCUS_AREAS)[number];
export type CodeReviewSeverityThreshold = (typeof CODE_REVIEW_SEVERITY_THRESHOLDS)[number];
/** Inputs for Gemini-backed analysis-only code review over supplied text. */
export interface CodeReviewOptions {
diff?: string;
code?: string;
context?: string;
language?: string;
filename?: string;
focus?: CodeReviewFocusArea[];
severityThreshold?: CodeReviewSeverityThreshold;
maxFindings?: number;
config?: GeminiAcpConfig;
rootDir?: string;
cwd?: string;
}
/** Code review result with the exact review prompt retained for tests/debugging. */
export interface CodeReviewResult extends PromptRunResult {
prompt?: string;
sections: typeof CODE_REVIEW_SECTIONS;
}
/** Builds the deterministic, analysis-only prompt used by gemini_code_review. */
export function buildCodeReviewPrompt(options: CodeReviewOptions): string {
const diff = options.diff?.trim() ?? "";
const code = options.code?.trim() ?? "";
const context = options.context?.trim() ?? "";
const focus = normalizeFocus(options.focus);
const severityThreshold = options.severityThreshold ?? "all";
const maxFindings = normalizeMaxFindings(options.maxFindings);
const metadata = [
options.filename?.trim() ? `Filename: ${options.filename.trim()}` : undefined,
options.language?.trim() ? `Language: ${options.language.trim()}` : undefined,
]
.filter(Boolean)
.join("\n");
return [
"You are Gemini performing an analysis-only code review for Pi.",
"Do not edit files, produce patches, or claim that fixes were applied.",
"Review only the caller-provided diff/code/context below; do not assume filesystem access or hidden project state.",
"If evidence is insufficient, say so in the relevant section instead of inventing facts.",
"",
"Return Markdown with exactly these top-level headings, in this order:",
...CODE_REVIEW_SECTIONS.map((section) => `## ${section}`),
"",
"Finding format: - [severity] concise title — evidence; impact; recommendation.",
"Use 'None found.' under a section when there are no findings for it.",
`Severity threshold: ${severityThreshold}. Still include every required heading even when a threshold suppresses findings.`,
`Focus areas: ${focus.join(", ")}.`,
`Maximum findings: ${maxFindings}. Prioritize the highest-impact issues first.`,
"Validation should list concrete checks the caller can run or explain why no validation is inferable.",
metadata ? `\nMetadata:\n${metadata}` : "",
context ? `\nContext:\n\n${context}\n` : "",
diff ? `\nDiff:\n\n${diff}\n` : "",
code ? `\nCode:\n\n${code}\n` : "",
]
.filter((part) => part.length > 0)
.join("\n");
}
/** Runs an analysis-only Gemini code review through the shared prompt workflow. */
export async function runCodeReview(
options: CodeReviewOptions,
deps: PromptDeps = {},
signal?: AbortSignal,
onUpdate?: PromptUpdateHandler,
): Promise {
if (!hasReviewInput(options)) {
return codeReviewError(
"GEMINI_CODE_REVIEW_EMPTY_INPUT",
"input_validation",
"Provide diff or code text for gemini_code_review. File paths are not read by this tool.",
);
}
const prompt = buildCodeReviewPrompt(options);
const result = await runPrompt(
{
prompt,
config: options.config,
rootDir: options.rootDir,
cwd: options.cwd,
requestSummary: codeReviewRequestSummary(options),
},
deps,
signal,
onUpdate,
);
return { ...result, prompt, sections: CODE_REVIEW_SECTIONS };
}
function codeReviewRequestSummary(options: CodeReviewOptions) {
const focus = normalizeFocus(options.focus);
return {
toolName: "gemini_code_review" as const,
action: "Sending code review prompt",
subject: options.filename?.trim() ?? options.language?.trim() ?? "provided text",
arguments: {
language: options.language?.trim() ?? undefined,
filename: options.filename?.trim() ?? undefined,
focus: focus.join("/"),
severity: options.severityThreshold ?? "all",
maxFindings: normalizeMaxFindings(options.maxFindings),
diffLength: options.diff?.length,
codeLength: options.code?.length,
contextLength: options.context?.length,
},
};
}
function hasReviewInput(options: CodeReviewOptions): boolean {
return Boolean(options.diff?.trim() ?? options.code?.trim());
}
function normalizeFocus(focus: CodeReviewFocusArea[] | undefined): CodeReviewFocusArea[] {
return focus && focus.length > 0 ? [...new Set(focus)] : ["correctness"];
}
function normalizeMaxFindings(value: number | undefined): number {
if (typeof value !== "number" || !Number.isFinite(value)) {
return DEFAULT_MAX_FINDINGS;
}
return Math.min(50, Math.max(1, Math.trunc(value)));
}
function codeReviewError(code: string, phase: string, message: string): CodeReviewResult {
return {
provider: "gemini-acp",
text: "",
responseLength: 0,
truncated: false,
sections: CODE_REVIEW_SECTIONS,
error: providerError(code, phase, message),
};
}