♻️ refactor: update analysis.ts

swadhinbiswas committed 3 months ago
9219247
1 parent 1435dfa
commit 9219247
1 file changed
+344 -0
▼src/pages/api/repos/[owner]/[repo]/pulls/[number]/analysis.ts
+344-0
1
+/**
2
+ * PR Analysis API Endpoint
3
+ * Triggers and retrieves AI-powered code review analysis.
4
+ */
5
+
6
+import type { APIRoute } from "astro";
7
+import { eq, and, desc } from "drizzle-orm";
8
+import { getDatabase, schema } from "@/db";
9
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
10
+import { withErrorHandler } from "@/lib/errors";
11
+import { getUserFromRequest } from "@/lib/auth";
12
+import { generateId } from "@/lib/utils";
13
+import { runAnalysisPipeline } from "@/lib/review/pipeline";
14
+import { generateAISummaries, generateFallbackSummaries } from "@/lib/review/summarizer";
15
+import { buildFullReviewPrompt } from "@/lib/review/prompts";
16
+import { parseAIConfigFromStorage } from "@/lib/ai-config";
17
+import { getAIAdapter } from "@/lib/ai";
18
+import { logger } from "@/lib/logger";
19
+import { existsSync } from "fs";
20
+
21
+export const GET: APIRoute = withErrorHandler(async ({ params }) => {
22
+ const { owner, repo, number } = params!;
23
+ const prNumber = parseInt(number!);
24
+
25
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
26
+
27
+ const user = await db.query.users.findFirst({
28
+ where: eq(schema.users.username, owner!),
29
+ });
30
+
31
+ if (!user) {
32
+ return new Response(JSON.stringify({ error: { message: "User not found" } }), { status: 404 });
33
+ }
34
+
35
+ const repository = await db.query.repositories.findFirst({
36
+ where: and(
37
+ eq(schema.repositories.name, repo!),
38
+ eq(schema.repositories.ownerId, user.id),
39
+ ),
40
+ });
41
+
42
+ if (!repository) {
43
+ return new Response(JSON.stringify({ error: { message: "Repository not found" } }), { status: 404 });
44
+ }
45
+
46
+ const pr = await db.query.pullRequests.findFirst({
47
+ where: and(
48
+ eq(schema.pullRequests.repositoryId, repository.id),
49
+ eq(schema.pullRequests.number, prNumber),
50
+ ),
51
+ });
52
+
53
+ if (!pr) {
54
+ return new Response(JSON.stringify({ error: { message: "Pull request not found" } }), { status: 404 });
55
+ }
56
+
57
+ const analysis = await db.query.prReviewAnalyses.findFirst({
58
+ where: eq(schema.prReviewAnalyses.pullRequestId, pr.id),
59
+ orderBy: [desc(schema.prReviewAnalyses.createdAt)],
60
+ });
61
+
62
+ if (!analysis) {
63
+ return new Response(JSON.stringify({ data: null }), { status: 200 });
64
+ }
65
+
66
+ const inlineComments = await db.query.aiInlineComments.findMany({
67
+ where: eq(schema.aiInlineComments.analysisId, analysis.id),
68
+ orderBy: [schema.aiInlineComments.filePath, schema.aiInlineComments.line],
69
+ });
70
+
71
+ return new Response(JSON.stringify({
72
+ data: {
73
+ analysis: {
74
+ id: analysis.id,
75
+ status: analysis.status,
76
+ healthScore: analysis.healthScore,
77
+ healthGrade: analysis.healthGrade,
78
+ blastRadius: analysis.blastRadius,
79
+ architectureImpact: analysis.architectureImpact,
80
+ changeGroups: analysis.changeGroups,
81
+ complexityData: analysis.complexityData,
82
+ summaries: analysis.summaries,
83
+ filesAnalyzed: analysis.filesAnalyzed,
84
+ totalAdditions: analysis.totalAdditions,
85
+ totalDeletions: analysis.totalDeletions,
86
+ model: analysis.model,
87
+ provider: analysis.provider,
88
+ tokensUsed: analysis.tokensUsed,
89
+ errorMessage: analysis.errorMessage,
90
+ createdAt: analysis.createdAt,
91
+ completedAt: analysis.completedAt,
92
+ },
93
+ inlineComments: inlineComments.map((c: any) => ({
94
+ id: c.id,
95
+ filePath: c.filePath,
96
+ line: c.line,
97
+ endLine: c.endLine,
98
+ severity: c.severity,
99
+ type: c.type,
100
+ category: c.category,
101
+ title: c.title,
102
+ message: c.message,
103
+ suggestedFix: c.suggestedFix,
104
+ explanation: c.explanation,
105
+ confidence: c.confidence,
106
+ isResolved: c.isResolved,
107
+ isApplied: c.isApplied,
108
+ })),
109
+ },
110
+ }), { status: 200 });
111
+});
112
+
113
+export const POST: APIRoute = withErrorHandler(async ({ params, request }) => {
114
+ const { owner, repo, number } = params!;
115
+ const prNumber = parseInt(number!);
116
+
117
+ const tokenPayload = await getUserFromRequest(request);
118
+ if (!tokenPayload) {
119
+ return new Response(JSON.stringify({ error: { message: "Authentication required" } }), { status: 401 });
120
+ }
121
+
122
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
123
+
124
+ const user = await db.query.users.findFirst({
125
+ where: eq(schema.users.username, owner!),
126
+ });
127
+
128
+ if (!user) {
129
+ return new Response(JSON.stringify({ error: { message: "User not found" } }), { status: 404 });
130
+ }
131
+
132
+ const repository = await db.query.repositories.findFirst({
133
+ where: and(
134
+ eq(schema.repositories.name, repo!),
135
+ eq(schema.repositories.ownerId, user.id),
136
+ ),
137
+ });
138
+
139
+ if (!repository) {
140
+ return new Response(JSON.stringify({ error: { message: "Repository not found" } }), { status: 404 });
141
+ }
142
+
143
+ const pr = await db.query.pullRequests.findFirst({
144
+ where: and(
145
+ eq(schema.pullRequests.repositoryId, repository.id),
146
+ eq(schema.pullRequests.number, prNumber),
147
+ ),
148
+ });
149
+
150
+ if (!pr) {
151
+ return new Response(JSON.stringify({ error: { message: "Pull request not found" } }), { status: 404 });
152
+ }
153
+
154
+ // Check if analysis is already running
155
+ const existingRunning = await db.query.prReviewAnalyses.findFirst({
156
+ where: and(
157
+ eq(schema.prReviewAnalyses.pullRequestId, pr.id),
158
+ eq(schema.prReviewAnalyses.status, "running"),
159
+ ),
160
+ });
161
+
162
+ if (existingRunning) {
163
+ return new Response(JSON.stringify({
164
+ data: { id: existingRunning.id, status: "running" },
165
+ message: "Analysis already in progress",
166
+ }), { status: 200 });
167
+ }
168
+
169
+ // Create analysis record
170
+ const analysisId = generateId();
171
+ await db.insert(schema.prReviewAnalyses).values({
172
+ id: analysisId,
173
+ pullRequestId: pr.id,
174
+ repositoryId: repository.id,
175
+ status: "running",
176
+ baseSha: pr.baseSha,
177
+ headSha: pr.headSha,
178
+ triggeredById: tokenPayload.userId,
179
+ });
180
+
181
+ // Run analysis asynchronously
182
+ const repoPath = repository.diskPath;
183
+ runAnalysisAsync(analysisId, repoPath, pr.baseSha, pr.headSha, {
184
+ prTitle: pr.title,
185
+ prBody: pr.body || "",
186
+ repoName: repo!,
187
+ owner: owner!,
188
+ branch: pr.headBranch,
189
+ filesChanged: pr.changedFiles || 0,
190
+ additions: pr.additions || 0,
191
+ deletions: pr.deletions || 0,
192
+ }).catch((err: any) => {
193
+ logger.error({ err, analysisId }, "Analysis pipeline failed");
194
+ });
195
+
196
+ return new Response(JSON.stringify({
197
+ data: { id: analysisId, status: "running" },
198
+ message: "Analysis started",
199
+ }), { status: 202 });
200
+});
201
+
202
+async function runAnalysisAsync(
203
+ analysisId: string,
204
+ repoPath: string,
205
+ baseSha: string,
206
+ headSha: string,
207
+ options: {
208
+ prTitle: string;
209
+ prBody: string;
210
+ repoName: string;
211
+ owner: string;
212
+ branch: string;
213
+ filesChanged: number;
214
+ additions: number;
215
+ deletions: number;
216
+ },
217
+) {
218
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
219
+
220
+ try {
221
+ if (!existsSync(repoPath)) {
222
+ throw new Error(`Repository path not found: ${repoPath}`);
223
+ }
224
+
225
+ const analysis = await runAnalysisPipeline(repoPath, baseSha, headSha);
226
+
227
+ // Generate summaries
228
+ let summaries;
229
+ try {
230
+ const aiConfig = parseAIConfigFromStorage(undefined as any);
231
+ if (aiConfig?.apiKeys) {
232
+ summaries = await generateAISummaries(analysis, options, aiConfig);
233
+ } else {
234
+ summaries = generateFallbackSummaries(analysis, options);
235
+ }
236
+ } catch {
237
+ summaries = generateFallbackSummaries(analysis, options);
238
+ }
239
+
240
+ // Generate AI review suggestions
241
+ let aiSuggestions: any[] = [];
242
+ let rawAiResponse: any = null;
243
+ let tokensUsed = 0;
244
+
245
+ try {
246
+ const aiConfig = parseAIConfigFromStorage(undefined as any);
247
+ if (aiConfig?.apiKeys && aiConfig.provider) {
248
+ const apiKey = (aiConfig.apiKeys as any)[aiConfig.provider];
249
+ if (apiKey) {
250
+ const adapter = getAIAdapter(aiConfig.provider as any);
251
+ const diffContent = analysis.chunks.map((c: any) => c.content).join("\n\n").slice(0, 50000);
252
+ const prompt = buildFullReviewPrompt(analysis, options, diffContent);
253
+
254
+ const result = await adapter.complete(
255
+ { system: "You are an expert code reviewer. Always respond with valid JSON only, no markdown fences.", user: prompt },
256
+ { provider: aiConfig.provider as any, model: aiConfig.model, apiKey },
257
+ );
258
+
259
+ rawAiResponse = result;
260
+ tokensUsed = result.usage?.totalTokens || 0;
261
+
262
+ const parsed = parseJSONResponse(result.content);
263
+ if (parsed?.suggestions) {
264
+ aiSuggestions = parsed.suggestions;
265
+ }
266
+ }
267
+ }
268
+ } catch (err) {
269
+ logger.warn({ err, analysisId }, "AI review generation failed, continuing with analysis only");
270
+ }
271
+
272
+ // Update analysis record
273
+ await db.update(schema.prReviewAnalyses).set({
274
+ status: "completed",
275
+ complexityData: analysis.complexity.aggregate as any,
276
+ dependencyGraph: analysis.depGraph as any,
277
+ architectureImpact: analysis.archImpact as any,
278
+ changeGroups: analysis.groups as any,
279
+ blastRadius: analysis.blastRadius as any,
280
+ healthScore: analysis.healthScore.score,
281
+ healthGrade: analysis.healthScore.grade,
282
+ summaries: summaries as any,
283
+ filesAnalyzed: analysis.chunks.length,
284
+ chunksProcessed: analysis.chunks.length,
285
+ totalAdditions: analysis.stats.totalAdditions,
286
+ totalDeletions: analysis.stats.totalDeletions,
287
+ tokensUsed,
288
+ rawAiResponse,
289
+ completedAt: new Date(),
290
+ }).where(eq(schema.prReviewAnalyses.id, analysisId));
291
+
292
+ // Get the pull request ID from the analysis record
293
+ const analysisRecord = await db.query.prReviewAnalyses.findFirst({
294
+ where: eq(schema.prReviewAnalyses.id, analysisId),
295
+ });
296
+
297
+ // Insert inline comments
298
+ for (const suggestion of aiSuggestions) {
299
+ if (suggestion.path && suggestion.line) {
300
+ await db.insert(schema.aiInlineComments).values({
301
+ id: generateId(),
302
+ analysisId,
303
+ pullRequestId: analysisRecord?.pullRequestId || "",
304
+ filePath: suggestion.path,
305
+ line: suggestion.line,
306
+ severity: suggestion.severity || "info",
307
+ type: suggestion.type || "suggestion",
308
+ category: suggestion.category || "code-quality",
309
+ title: suggestion.title || "Review suggestion",
310
+ message: suggestion.message || "",
311
+ suggestedFix: suggestion.suggestedFix,
312
+ explanation: suggestion.explanation,
313
+ confidence: Math.round((suggestion.confidence || 0.5) * 100),
314
+ });
315
+ }
316
+ }
317
+
318
+ logger.info({ analysisId, filesAnalyzed: analysis.chunks.length }, "Analysis completed successfully");
319
+ } catch (err) {
320
+ logger.error({ err, analysisId }, "Analysis failed");
321
+
322
+ await db.update(schema.prReviewAnalyses).set({
323
+ status: "failed",
324
+ errorMessage: err instanceof Error ? err.message : "Unknown error",
325
+ completedAt: new Date(),
326
+ }).where(eq(schema.prReviewAnalyses.id, analysisId));
327
+ }
328
+}
329
+
330
+function parseJSONResponse(text: string): any {
331
+ try {
332
+ return JSON.parse(text);
333
+ } catch {
334
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
335
+ if (jsonMatch) {
336
+ try { return JSON.parse(jsonMatch[1]); } catch {}
337
+ }
338
+ const objMatch = text.match(/\{[\s\S]*\}/);
339
+ if (objMatch) {
340
+ try { return JSON.parse(objMatch[0]); } catch {}
341
+ }
342
+ return null;
343
+ }
344
+}
0
345