codecritique 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1145 -0
- package/package.json +98 -0
- package/src/content-retrieval.js +747 -0
- package/src/custom-documents.js +597 -0
- package/src/embeddings/cache-manager.js +364 -0
- package/src/embeddings/constants.js +40 -0
- package/src/embeddings/database.js +921 -0
- package/src/embeddings/errors.js +208 -0
- package/src/embeddings/factory.js +447 -0
- package/src/embeddings/file-processor.js +851 -0
- package/src/embeddings/model-manager.js +337 -0
- package/src/embeddings/similarity-calculator.js +97 -0
- package/src/embeddings/types.js +113 -0
- package/src/feedback-loader.js +384 -0
- package/src/index.js +1418 -0
- package/src/llm.js +123 -0
- package/src/pr-history/analyzer.js +579 -0
- package/src/pr-history/bot-detector.js +123 -0
- package/src/pr-history/cli-utils.js +204 -0
- package/src/pr-history/comment-processor.js +549 -0
- package/src/pr-history/database.js +819 -0
- package/src/pr-history/github-client.js +629 -0
- package/src/project-analyzer.js +955 -0
- package/src/rag-analyzer.js +2764 -0
- package/src/rag-review.js +566 -0
- package/src/technology-keywords.json +753 -0
- package/src/utils/command.js +48 -0
- package/src/utils/constants.js +263 -0
- package/src/utils/context-inference.js +364 -0
- package/src/utils/document-detection.js +105 -0
- package/src/utils/file-validation.js +271 -0
- package/src/utils/git.js +232 -0
- package/src/utils/language-detection.js +170 -0
- package/src/utils/logging.js +24 -0
- package/src/utils/markdown.js +132 -0
- package/src/utils/mobilebert-tokenizer.js +141 -0
- package/src/utils/pr-chunking.js +276 -0
- package/src/utils/string-utils.js +28 -0
- package/src/zero-shot-classifier-open.js +392 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RAG Review Module
|
|
3
|
+
*
|
|
4
|
+
* This module serves as the main entry point for the dynamic, context-augmented
|
|
5
|
+
* code review process. It coordinates file discovery and analysis,
|
|
6
|
+
* relying on dynamic context retrieval via embeddings.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
|
|
12
|
+
import { shouldProcessFile } from './utils/file-validation.js';
|
|
13
|
+
import { findBaseBranch, getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
|
|
14
|
+
import { detectFileType, detectLanguageFromExtension } from './utils/language-detection.js';
|
|
15
|
+
import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chunking.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Review a single file using RAG approach
|
|
19
|
+
*
|
|
20
|
+
* @param {string} filePath - Path to the file to review
|
|
21
|
+
* @param {object} options - Review options
|
|
22
|
+
* @returns {Promise<object>} Review result object
|
|
23
|
+
*/
|
|
24
|
+
async function reviewFile(filePath, options = {}) {
|
|
25
|
+
try {
|
|
26
|
+
console.log(chalk.blue(`Reviewing file: ${filePath}`));
|
|
27
|
+
|
|
28
|
+
// Analyze the file using the RAG analyzer
|
|
29
|
+
const analyzeResult = await runAnalysis(filePath, options);
|
|
30
|
+
|
|
31
|
+
// If analysis successful, return the result
|
|
32
|
+
if (analyzeResult.success) {
|
|
33
|
+
// Convert object results to array format expected by the output functions
|
|
34
|
+
if (analyzeResult.results && !Array.isArray(analyzeResult.results)) {
|
|
35
|
+
console.log(chalk.blue('Converting results object to array format'));
|
|
36
|
+
|
|
37
|
+
// Create a new array with one entry containing the object results
|
|
38
|
+
const resultArray = [
|
|
39
|
+
{
|
|
40
|
+
filePath: analyzeResult.filePath,
|
|
41
|
+
language: analyzeResult.language,
|
|
42
|
+
success: true,
|
|
43
|
+
results: analyzeResult.results,
|
|
44
|
+
},
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
results: resultArray,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return analyzeResult;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// If analysis failed, return the error
|
|
57
|
+
return analyzeResult;
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error(chalk.red(`Error reviewing file ${filePath}:`), error.message);
|
|
60
|
+
return {
|
|
61
|
+
success: false,
|
|
62
|
+
error: error.message,
|
|
63
|
+
filePath,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Review multiple files using dynamic context retrieval.
|
|
70
|
+
*
|
|
71
|
+
* @param {Array<string>} filePaths - Paths to the files to review
|
|
72
|
+
* @param {Object} options - Review options (passed to each reviewFile call)
|
|
73
|
+
* @returns {Promise<Object>} Aggregated review results { success: boolean, results: Array<Object>, message: string, error?: string }
|
|
74
|
+
*/
|
|
75
|
+
async function reviewFiles(filePaths, options = {}) {
|
|
76
|
+
try {
|
|
77
|
+
const verbose = options.verbose || false;
|
|
78
|
+
if (verbose) {
|
|
79
|
+
console.log(chalk.blue(`Reviewing ${filePaths.length} files...`));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Review files concurrently
|
|
83
|
+
const results = [];
|
|
84
|
+
const concurrency = options.concurrency || 3; // Limit concurrency for API calls/CPU usage
|
|
85
|
+
|
|
86
|
+
// Process files in batches to limit concurrency
|
|
87
|
+
for (let i = 0; i < filePaths.length; i += concurrency) {
|
|
88
|
+
const batch = filePaths.slice(i, i + concurrency);
|
|
89
|
+
|
|
90
|
+
if (verbose) {
|
|
91
|
+
console.log(
|
|
92
|
+
chalk.blue(
|
|
93
|
+
`Processing review batch ${Math.floor(i / concurrency) + 1}/${Math.ceil(filePaths.length / concurrency)} (${
|
|
94
|
+
batch.length
|
|
95
|
+
} files)`
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Pass options down to reviewFile
|
|
101
|
+
const batchPromises = batch.map((filePath) => reviewFile(filePath, options));
|
|
102
|
+
const batchResults = await Promise.all(batchPromises);
|
|
103
|
+
|
|
104
|
+
results.push(...batchResults);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Filter out potential null results if any step could return null/undefined (though analyzeFile should always return an object)
|
|
108
|
+
const validResults = results.filter((r) => r != null);
|
|
109
|
+
const successCount = validResults.filter((r) => r.success && !r.skipped).length;
|
|
110
|
+
const skippedCount = validResults.filter((r) => r.skipped).length;
|
|
111
|
+
const errorCount = validResults.filter((r) => !r.success).length;
|
|
112
|
+
|
|
113
|
+
let finalMessage = `Review completed for ${filePaths.length} files. `;
|
|
114
|
+
finalMessage += `Success: ${successCount}, Skipped: ${skippedCount}, Errors: ${errorCount}.`;
|
|
115
|
+
|
|
116
|
+
console.log(chalk.green(finalMessage));
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
success: errorCount === 0,
|
|
120
|
+
results: validResults, // Return array of individual file results
|
|
121
|
+
message: finalMessage,
|
|
122
|
+
};
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(chalk.red(`Error reviewing multiple files: ${error.message}`));
|
|
125
|
+
console.error(error.stack);
|
|
126
|
+
return {
|
|
127
|
+
success: false,
|
|
128
|
+
error: error.message,
|
|
129
|
+
results: [],
|
|
130
|
+
message: 'Failed to review files due to an unexpected error',
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Review files changed in a pull request (requires changed file paths).
|
|
137
|
+
*
|
|
138
|
+
* @param {Array<string>} changedFilePaths - Array of file paths changed in the PR.
|
|
139
|
+
* @param {Object} options - Review options (passed to reviewFiles).
|
|
140
|
+
* @returns {Promise<Object>} Aggregated review results.
|
|
141
|
+
*/
|
|
142
|
+
async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
143
|
+
try {
|
|
144
|
+
const verbose = options.verbose || false;
|
|
145
|
+
if (verbose) {
|
|
146
|
+
console.log(chalk.blue(`Reviewing ${changedFilePaths.length} changed files from PR...`));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// No longer filter files here, as new files in a different branch won't exist locally.
|
|
150
|
+
// The downstream functions are responsible for fetching content from git.
|
|
151
|
+
const filesToReview = changedFilePaths;
|
|
152
|
+
|
|
153
|
+
if (filesToReview.length === 0) {
|
|
154
|
+
const message = 'No processable files found among the changed files provided for PR review.';
|
|
155
|
+
console.log(chalk.yellow(message));
|
|
156
|
+
return {
|
|
157
|
+
success: true,
|
|
158
|
+
message: message,
|
|
159
|
+
results: [],
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (verbose) {
|
|
164
|
+
console.log(chalk.green(`Reviewing ${filesToReview.length} existing and processable changed files`));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Use enhanced PR review with cross-file context
|
|
168
|
+
return await reviewPullRequestWithCrossFileContext(filesToReview, options);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error(chalk.red(`Error reviewing pull request files: ${error.message}`));
|
|
171
|
+
console.error(error.stack);
|
|
172
|
+
return {
|
|
173
|
+
success: false,
|
|
174
|
+
error: error.message,
|
|
175
|
+
message: 'Failed to review pull request files',
|
|
176
|
+
results: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Enhanced PR review with cross-file context and de-duplicated resources
|
|
183
|
+
*
|
|
184
|
+
* @param {Array<string>} filesToReview - Array of file paths to review
|
|
185
|
+
* @param {Object} options - Review options
|
|
186
|
+
* @returns {Promise<Object>} Aggregated review results
|
|
187
|
+
*/
|
|
188
|
+
async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}) {
|
|
189
|
+
try {
|
|
190
|
+
const verbose = options.verbose || false;
|
|
191
|
+
if (verbose) {
|
|
192
|
+
console.log(chalk.blue(`Starting enhanced PR review with cross-file context for ${filesToReview.length} files...`));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step 1: Get the base branch and collect diff info for all files in the PR
|
|
196
|
+
const workingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
197
|
+
const baseBranch = findBaseBranch(workingDir);
|
|
198
|
+
const targetBranch = options.diffWith || 'HEAD'; // The feature branch being reviewed
|
|
199
|
+
|
|
200
|
+
// Get the actual branch name from options passed from index.js
|
|
201
|
+
const actualTargetBranch = options.actualBranch || targetBranch;
|
|
202
|
+
|
|
203
|
+
if (verbose) {
|
|
204
|
+
console.log(chalk.gray(`Base branch: ${baseBranch}, Target branch: ${targetBranch}`));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const prFiles = [];
|
|
208
|
+
for (const filePath of filesToReview) {
|
|
209
|
+
try {
|
|
210
|
+
// Check if the file should be processed before fetching its content from git
|
|
211
|
+
if (!shouldProcessFile(filePath, '', options)) {
|
|
212
|
+
if (verbose) {
|
|
213
|
+
console.log(chalk.yellow(`Skipping file due to exclusion rules: ${path.basename(filePath)}`));
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const content = getFileContentFromGit(filePath, actualTargetBranch, workingDir);
|
|
219
|
+
const language = detectLanguageFromExtension(path.extname(filePath));
|
|
220
|
+
const fileType = detectFileType(filePath, content);
|
|
221
|
+
|
|
222
|
+
// Get the git diff for this file
|
|
223
|
+
const diffInfo = getChangedLinesInfo(filePath, baseBranch, actualTargetBranch, workingDir);
|
|
224
|
+
|
|
225
|
+
if (!diffInfo.hasChanges) {
|
|
226
|
+
if (verbose) {
|
|
227
|
+
console.log(chalk.yellow(`No changes detected in ${path.basename(filePath)}, skipping`));
|
|
228
|
+
}
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Create a summary of changes for context
|
|
233
|
+
const changesSummary = `${diffInfo.addedLines.length} lines added, ${diffInfo.removedLines.length} lines removed`;
|
|
234
|
+
|
|
235
|
+
prFiles.push({
|
|
236
|
+
filePath,
|
|
237
|
+
content, // Keep full content for context gathering
|
|
238
|
+
diffContent: diffInfo.fullDiff, // The actual diff to review
|
|
239
|
+
diffInfo: diffInfo, // Parsed diff info
|
|
240
|
+
language,
|
|
241
|
+
fileType,
|
|
242
|
+
isTest: fileType.isTest,
|
|
243
|
+
isComponent: content.includes('export default') || content.includes('export const') || content.includes('export function'),
|
|
244
|
+
summary: `${language} ${fileType.isTest ? 'test' : 'source'} file: ${path.basename(filePath)} (${changesSummary})`,
|
|
245
|
+
baseBranch,
|
|
246
|
+
targetBranch,
|
|
247
|
+
});
|
|
248
|
+
} catch (error) {
|
|
249
|
+
console.warn(chalk.yellow(`Error processing file ${filePath}: ${error.message}`));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (prFiles.length === 0) {
|
|
254
|
+
console.log(chalk.yellow('No files with changes found for review'));
|
|
255
|
+
return {
|
|
256
|
+
success: true,
|
|
257
|
+
results: [],
|
|
258
|
+
prContext: { message: 'No changes to review' },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Check if PR should be chunked based on size and complexity (skip if this is already a chunk)
|
|
263
|
+
if (!options.skipChunking) {
|
|
264
|
+
const chunkingDecision = shouldChunkPR(prFiles);
|
|
265
|
+
if (verbose) {
|
|
266
|
+
console.log(chalk.blue(`PR size assessment: ${chunkingDecision.estimatedTokens} tokens, ${prFiles.length} files`));
|
|
267
|
+
if (chunkingDecision.shouldChunk) {
|
|
268
|
+
console.log(chalk.yellow(`Large PR detected - will chunk into ~${chunkingDecision.recommendedChunks} chunks`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// If PR is too large, use chunked processing
|
|
273
|
+
if (chunkingDecision.shouldChunk) {
|
|
274
|
+
console.log(chalk.blue(`🔄 Using chunked processing for large PR (${chunkingDecision.estimatedTokens} tokens)`));
|
|
275
|
+
return await reviewLargePRInChunks(prFiles, options);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Step 2: Gather unified context for the entire PR (for regular-sized PRs)
|
|
280
|
+
if (verbose) {
|
|
281
|
+
console.log(chalk.blue(`Performing unified context retrieval for ${prFiles.length} PR files...`));
|
|
282
|
+
}
|
|
283
|
+
const {
|
|
284
|
+
codeExamples: deduplicatedCodeExamples,
|
|
285
|
+
guidelines: deduplicatedGuidelines,
|
|
286
|
+
prComments: deduplicatedPRComments,
|
|
287
|
+
customDocChunks: deduplicatedCustomDocChunks,
|
|
288
|
+
} = await gatherUnifiedContextForPR(prFiles, options);
|
|
289
|
+
|
|
290
|
+
if (verbose) {
|
|
291
|
+
console.log(
|
|
292
|
+
chalk.green(
|
|
293
|
+
`De-duplicated context: ${deduplicatedCodeExamples.length} code examples, ${deduplicatedGuidelines.length} guidelines, ${deduplicatedPRComments.length} PR comments, ${deduplicatedCustomDocChunks.length} custom doc chunks`
|
|
294
|
+
)
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Step 3: Create PR context summary for LLM
|
|
299
|
+
const prContext = {
|
|
300
|
+
allFiles: prFiles.map((f) => ({
|
|
301
|
+
path: path.relative(process.cwd(), f.filePath),
|
|
302
|
+
language: f.language,
|
|
303
|
+
isTest: f.isTest,
|
|
304
|
+
isComponent: f.isComponent,
|
|
305
|
+
summary: f.summary,
|
|
306
|
+
})),
|
|
307
|
+
totalFiles: prFiles.length,
|
|
308
|
+
testFiles: prFiles.filter((f) => f.isTest).length,
|
|
309
|
+
sourceFiles: prFiles.filter((f) => !f.isTest).length,
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Step 4: Perform holistic PR review with all files and unified context
|
|
313
|
+
if (verbose) {
|
|
314
|
+
console.log(chalk.blue(`Performing holistic PR review for all ${prFiles.length} files...`));
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
// Create a comprehensive review context with all files and their diffs
|
|
319
|
+
const comprehensiveContext = {
|
|
320
|
+
prFiles: prFiles.map((file) => ({
|
|
321
|
+
path: path.relative(workingDir, file.filePath),
|
|
322
|
+
language: file.language,
|
|
323
|
+
isTest: file.isTest,
|
|
324
|
+
isComponent: file.isComponent,
|
|
325
|
+
summary: file.summary,
|
|
326
|
+
fullContent: file.content, // Add full file content for context
|
|
327
|
+
diff: file.diffContent,
|
|
328
|
+
baseBranch: file.baseBranch,
|
|
329
|
+
targetBranch: file.targetBranch,
|
|
330
|
+
})),
|
|
331
|
+
unifiedContext: {
|
|
332
|
+
codeExamples: deduplicatedCodeExamples,
|
|
333
|
+
guidelines: deduplicatedGuidelines,
|
|
334
|
+
prComments: deduplicatedPRComments,
|
|
335
|
+
customDocChunks: deduplicatedCustomDocChunks,
|
|
336
|
+
},
|
|
337
|
+
prContext: prContext,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
// Use the existing analyzeFile function with holistic PR context
|
|
341
|
+
const holisticOptions = {
|
|
342
|
+
...options,
|
|
343
|
+
isHolisticPRReview: true,
|
|
344
|
+
prFiles: comprehensiveContext.prFiles,
|
|
345
|
+
unifiedContext: comprehensiveContext.unifiedContext,
|
|
346
|
+
prContext: comprehensiveContext.prContext,
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Create a synthetic "file" path for holistic analysis
|
|
350
|
+
const holisticResult = await runAnalysis('PR_HOLISTIC_REVIEW', holisticOptions);
|
|
351
|
+
|
|
352
|
+
// Convert holistic result to individual file results format for compatibility
|
|
353
|
+
const results = prFiles.map((file) => {
|
|
354
|
+
const relativePath = path.relative(workingDir, file.filePath);
|
|
355
|
+
const baseName = path.basename(file.filePath);
|
|
356
|
+
|
|
357
|
+
// Try multiple path formats to find file-specific issues
|
|
358
|
+
let fileIssues = [];
|
|
359
|
+
const possibleKeys = [
|
|
360
|
+
relativePath, // Full relative path
|
|
361
|
+
baseName, // Just filename
|
|
362
|
+
file.filePath, // Absolute path
|
|
363
|
+
path.posix.normalize(relativePath), // Normalized posix path
|
|
364
|
+
];
|
|
365
|
+
|
|
366
|
+
// Find issues using any of the possible key formats
|
|
367
|
+
for (const key of possibleKeys) {
|
|
368
|
+
if (holisticResult?.results?.fileSpecificIssues?.[key]) {
|
|
369
|
+
fileIssues = holisticResult.results.fileSpecificIssues[key];
|
|
370
|
+
console.log(chalk.green(`✅ Found ${fileIssues.length} issues for ${baseName} using key: "${key}"`));
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
console.log(chalk.gray(`🔍 Mapping issues for ${file.filePath}:`));
|
|
376
|
+
console.log(chalk.gray(` - Relative path: "${relativePath}"`));
|
|
377
|
+
console.log(chalk.gray(` - Tried keys: ${possibleKeys.map((k) => `"${k}"`).join(', ')}`));
|
|
378
|
+
console.log(chalk.gray(` - Final issues: ${fileIssues.length}`));
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
success: true,
|
|
382
|
+
filePath: file.filePath,
|
|
383
|
+
language: file.language,
|
|
384
|
+
results: {
|
|
385
|
+
summary: `Part of holistic PR review covering ${prFiles.length} files`,
|
|
386
|
+
issues: fileIssues,
|
|
387
|
+
},
|
|
388
|
+
context: {
|
|
389
|
+
codeExamples: deduplicatedCodeExamples.length,
|
|
390
|
+
guidelines: deduplicatedGuidelines.length,
|
|
391
|
+
prComments: deduplicatedPRComments.length,
|
|
392
|
+
customDocChunks: deduplicatedCustomDocChunks.length,
|
|
393
|
+
},
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Add holistic analysis to the first result
|
|
398
|
+
if (results.length > 0 && holisticResult?.results) {
|
|
399
|
+
results[0].holisticAnalysis = {
|
|
400
|
+
crossFileIssues: holisticResult.results.crossFileIssues || [],
|
|
401
|
+
overallSummary: holisticResult.results.summary,
|
|
402
|
+
recommendations: holisticResult.results.recommendations || [],
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
success: true,
|
|
408
|
+
results: results,
|
|
409
|
+
prContext: {
|
|
410
|
+
...prContext,
|
|
411
|
+
holisticAnalysis: holisticResult,
|
|
412
|
+
contextSummary: {
|
|
413
|
+
codeExamples: deduplicatedCodeExamples.length,
|
|
414
|
+
guidelines: deduplicatedGuidelines.length,
|
|
415
|
+
prComments: deduplicatedPRComments.length,
|
|
416
|
+
customDocChunks: deduplicatedCustomDocChunks.length,
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
} catch (error) {
|
|
421
|
+
console.error(chalk.red(`Error in holistic PR review: ${error.message}`));
|
|
422
|
+
|
|
423
|
+
// Fallback to individual file review if holistic review fails
|
|
424
|
+
if (verbose) {
|
|
425
|
+
console.log(chalk.yellow(`Falling back to individual file reviews...`));
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const results = [];
|
|
429
|
+
const concurrency = options.concurrency || 3;
|
|
430
|
+
|
|
431
|
+
for (let i = 0; i < prFiles.length; i += concurrency) {
|
|
432
|
+
const batch = prFiles.slice(i, i + concurrency);
|
|
433
|
+
|
|
434
|
+
const batchPromises = batch.map(async (file) => {
|
|
435
|
+
try {
|
|
436
|
+
// Enhance options with shared context and diff-only analysis
|
|
437
|
+
const enhancedOptions = {
|
|
438
|
+
...options,
|
|
439
|
+
// Add PR context for cross-file awareness
|
|
440
|
+
prContext: prContext,
|
|
441
|
+
// Override context gathering to use shared/pre-gathered resources
|
|
442
|
+
preGatheredContext: {
|
|
443
|
+
codeExamples: deduplicatedCodeExamples,
|
|
444
|
+
guidelines: deduplicatedGuidelines,
|
|
445
|
+
prComments: deduplicatedPRComments,
|
|
446
|
+
},
|
|
447
|
+
// Flag to indicate this is part of a PR review
|
|
448
|
+
isPRReview: true,
|
|
449
|
+
// Add diff-specific options
|
|
450
|
+
diffOnly: true,
|
|
451
|
+
diffContent: file.diffContent,
|
|
452
|
+
fullFileContent: file.content, // Pass full file content for context awareness
|
|
453
|
+
diffInfo: file.diffInfo,
|
|
454
|
+
baseBranch: file.baseBranch,
|
|
455
|
+
targetBranch: file.targetBranch,
|
|
456
|
+
// Add context about all files in the PR
|
|
457
|
+
allPRFiles: prContext.allFiles,
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = await runAnalysis(file.filePath, enhancedOptions);
|
|
461
|
+
return result;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
console.error(chalk.red(`Error reviewing ${file.filePath}: ${error.message}`));
|
|
464
|
+
return {
|
|
465
|
+
filePath: file.filePath,
|
|
466
|
+
success: false,
|
|
467
|
+
error: error.message,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
const batchResults = await Promise.all(batchPromises);
|
|
473
|
+
results.push(...batchResults);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Return fallback results
|
|
477
|
+
return {
|
|
478
|
+
success: true,
|
|
479
|
+
results: results,
|
|
480
|
+
prContext: prContext,
|
|
481
|
+
sharedContextStats: {
|
|
482
|
+
codeExamples: deduplicatedCodeExamples.length,
|
|
483
|
+
guidelines: deduplicatedGuidelines.length,
|
|
484
|
+
prComments: deduplicatedPRComments.length,
|
|
485
|
+
customDocChunks: deduplicatedCustomDocChunks.length,
|
|
486
|
+
},
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
} catch (error) {
|
|
490
|
+
console.error(chalk.red(`Error in enhanced PR review: ${error.message}`));
|
|
491
|
+
return {
|
|
492
|
+
success: false,
|
|
493
|
+
error: error.message,
|
|
494
|
+
results: [],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Reviews a large PR by splitting it into manageable chunks and processing them in parallel
|
|
501
|
+
* @param {Array} prFiles - Array of PR files with diff content
|
|
502
|
+
* @param {Object} options - Review options
|
|
503
|
+
* @returns {Promise<Object>} Combined review results
|
|
504
|
+
*/
|
|
505
|
+
async function reviewLargePRInChunks(prFiles, options) {
|
|
506
|
+
console.log(chalk.blue(`🔄 Large PR detected: ${prFiles.length} files. Splitting into chunks...`));
|
|
507
|
+
|
|
508
|
+
// Step 1: Gather shared context once for all chunks
|
|
509
|
+
console.log(chalk.cyan('📚 Gathering shared context for entire PR...'));
|
|
510
|
+
const sharedContext = await gatherUnifiedContextForPR(prFiles, options);
|
|
511
|
+
|
|
512
|
+
// Step 2: Split PR into manageable chunks
|
|
513
|
+
// Each chunk includes both diff AND full file content, plus ~25k context overhead
|
|
514
|
+
const chunks = chunkPRFiles(prFiles, 35000); // Conservative limit accounting for context overhead
|
|
515
|
+
console.log(chalk.green(`✂️ Split PR into ${chunks.length} chunks`));
|
|
516
|
+
|
|
517
|
+
chunks.forEach((chunk, i) => {
|
|
518
|
+
console.log(chalk.gray(` Chunk ${i + 1}: ${chunk.files.length} files (~${chunk.totalTokens} tokens)`));
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Step 3: Process chunks in parallel
|
|
522
|
+
console.log(chalk.blue('🔄 Processing chunks in parallel...'));
|
|
523
|
+
const chunkResults = await Promise.all(
|
|
524
|
+
chunks.map((chunk, index) => reviewPRChunk(chunk, sharedContext, options, index + 1, chunks.length))
|
|
525
|
+
);
|
|
526
|
+
|
|
527
|
+
// Step 4: Combine results
|
|
528
|
+
console.log(chalk.blue('🔗 Combining chunk results...'));
|
|
529
|
+
return combineChunkResults(chunkResults, prFiles.length);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Reviews a single chunk of files from a large PR
|
|
534
|
+
* @param {Object} chunk - Chunk object with files array
|
|
535
|
+
* @param {Object} sharedContext - Pre-gathered shared context
|
|
536
|
+
* @param {Object} options - Review options
|
|
537
|
+
* @param {number} chunkNumber - Current chunk number
|
|
538
|
+
* @param {number} totalChunks - Total number of chunks
|
|
539
|
+
* @returns {Promise<Object>} Chunk review results
|
|
540
|
+
*/
|
|
541
|
+
async function reviewPRChunk(chunk, sharedContext, options, chunkNumber, totalChunks) {
|
|
542
|
+
console.log(chalk.cyan(`📝 Reviewing chunk ${chunkNumber}/${totalChunks} (${chunk.files.length} files)...`));
|
|
543
|
+
|
|
544
|
+
// Create chunk-specific options
|
|
545
|
+
const chunkOptions = {
|
|
546
|
+
...options,
|
|
547
|
+
isChunkedReview: true,
|
|
548
|
+
chunkNumber: chunkNumber,
|
|
549
|
+
totalChunks: totalChunks,
|
|
550
|
+
preGatheredContext: sharedContext, // Use shared context
|
|
551
|
+
// Reduce context per chunk since we have multiple parallel reviews
|
|
552
|
+
maxExamples: Math.max(3, Math.floor((options.maxExamples || 40) / totalChunks)),
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
// Review this chunk as a smaller PR - call the main function recursively but with chunked flag
|
|
556
|
+
// to prevent infinite recursion
|
|
557
|
+
const chunkFilePaths = chunk.files.map((f) => f.filePath);
|
|
558
|
+
|
|
559
|
+
// Skip chunking decision for chunk reviews to prevent infinite recursion
|
|
560
|
+
const skipChunkingOptions = { ...chunkOptions, skipChunking: true };
|
|
561
|
+
|
|
562
|
+
return await reviewPullRequestWithCrossFileContext(chunkFilePaths, skipChunkingOptions);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// Export the core review functions
|
|
566
|
+
export { reviewFile, reviewFiles, reviewPullRequest };
|