Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

ai-review.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

ai-review.tsBlame156 lines · 1 contributor
e883329Claude1/**
2 * AI-powered code review using Claude.
3 *
4 * Generates inline review comments on pull request diffs.
5 * Reviews are posted as PR comments with isAiReview=true.
6 */
7
8import Anthropic from "@anthropic-ai/sdk";
9import { config } from "./config";
b2ff5c7Claude10import { buildReviewContext } from "./flywheel";
e883329Claude11
12interface ReviewComment {
13 filePath: string;
14 lineNumber: number | null;
15 body: string;
b2ff5c7Claude16 category?: string;
e883329Claude17}
18
19interface ReviewResult {
20 summary: string;
21 comments: ReviewComment[];
22 approved: boolean;
23}
24
25let _client: Anthropic | null = null;
26
27function getClient(): Anthropic {
28 if (!_client) {
29 if (!config.anthropicApiKey) {
30 throw new Error("ANTHROPIC_API_KEY is not set");
31 }
32 _client = new Anthropic({ apiKey: config.anthropicApiKey });
33 }
34 return _client;
35}
36
37/**
38 * Run AI code review on a PR diff.
39 */
40export async function reviewDiff(
41 repoFullName: string,
42 prTitle: string,
43 prBody: string | null,
44 baseBranch: string,
45 headBranch: string,
b2ff5c7Claude46 diffText: string,
47 opts?: { repositoryId?: string }
e883329Claude48): Promise<ReviewResult> {
49 const client = getClient();
50
b2ff5c7Claude51 // Flywheel: inject learned patterns from historical review data
52 const dominantLang = detectDominantLanguage(diffText);
53 const learnedContext = await buildReviewContext(
54 opts?.repositoryId ?? null,
55 dominantLang ?? undefined
56 ).catch(() => "");
57
e883329Claude58 const message = await client.messages.create({
59 model: "claude-sonnet-4-20250514",
60 max_tokens: 4096,
61 messages: [
62 {
63 role: "user",
64 content: `You are reviewing a pull request on the repository "${repoFullName}".
65
66**PR Title:** ${prTitle}
67**PR Description:** ${prBody || "(none)"}
68**Base branch:** ${baseBranch}
69**Head branch:** ${headBranch}
70
71Review the following diff. Look for:
72- Bugs, logic errors, or potential runtime failures
73- Security vulnerabilities (injection, XSS, auth bypasses, secrets in code)
74- Performance issues (N+1 queries, unnecessary allocations, blocking I/O)
75- Missing error handling at system boundaries
76- Breaking changes or API contract violations
77
b2ff5c7Claude78Do NOT comment on style, formatting, naming, missing docs, or minor nitpicks. Only flag issues that could cause real problems.${learnedContext}
79
80For each comment, classify it into one of these categories: bug, security, perf, logic, breaking.
e883329Claude81
82Respond in JSON format:
83{
84 "summary": "1-3 sentence overall assessment",
85 "approved": true/false,
86 "comments": [
87 {
88 "filePath": "path/to/file.ts",
89 "lineNumber": 42,
b2ff5c7Claude90 "body": "Explain the issue and suggest a fix",
91 "category": "bug"
e883329Claude92 }
93 ]
94}
95
96If the diff looks clean, return approved: true with an empty comments array.
97
98\`\`\`diff
99${diffText.slice(0, 100000)}
100\`\`\``,
101 },
102 ],
103 });
104
105 const text =
106 message.content[0].type === "text" ? message.content[0].text : "";
107
108 try {
109 // Extract JSON from response (may be wrapped in markdown code block)
110 const jsonMatch = text.match(/\{[\s\S]*\}/);
111 if (!jsonMatch) {
112 return {
113 summary: "AI review completed but could not parse structured output.",
114 comments: [],
115 approved: true,
116 };
117 }
118 const parsed = JSON.parse(jsonMatch[0]);
119 return {
120 summary: parsed.summary || "Review complete.",
121 comments: Array.isArray(parsed.comments) ? parsed.comments : [],
122 approved: parsed.approved !== false,
123 };
124 } catch {
125 return {
126 summary: text.slice(0, 500),
127 comments: [],
128 approved: true,
129 };
130 }
131}
132
133/**
134 * Check if AI review is available (API key configured).
135 */
136export function isAiReviewEnabled(): boolean {
137 return !!config.anthropicApiKey;
138}
b2ff5c7Claude139
140function detectDominantLanguage(diff: string): string | null {
141 const extCounts = new Map<string, number>();
142 const fileHeaders = diff.matchAll(/^(?:\+\+\+|---) [ab]\/(.+)$/gm);
143 for (const m of fileHeaders) {
144 const ext = m[1].split(".").pop()?.toLowerCase();
145 if (ext) extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
146 }
147 if (extCounts.size === 0) return null;
148 const sorted = [...extCounts.entries()].sort((a, b) => b[1] - a[1]);
149 const extMap: Record<string, string> = {
150 ts: "typescript", tsx: "typescript", js: "javascript", jsx: "javascript",
151 py: "python", rb: "ruby", go: "go", rs: "rust", java: "java",
152 kt: "kotlin", cs: "csharp", cpp: "cpp", c: "c", swift: "swift",
153 php: "php", sql: "sql",
154 };
155 return extMap[sorted[0][0]] ?? sorted[0][0];
156}