codecritique 1.2.2 → 1.2.4
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/package.json +1 -1
- package/src/content-retrieval.js +93 -153
- package/src/content-retrieval.test.js +49 -9
- package/src/custom-documents.js +17 -17
- package/src/feedback-loader.js +31 -31
- package/src/index.js +71 -94
- package/src/llm.js +4 -3
- package/src/project-analyzer.js +73 -41
- package/src/project-analyzer.test.js +3 -5
- package/src/rag-analyzer.js +189 -169
- package/src/rag-analyzer.test.js +55 -0
- package/src/rag-review.js +105 -74
- package/src/rag-review.test.js +115 -3
- package/src/zero-shot-classifier-open.js +3 -2
package/src/rag-analyzer.test.js
CHANGED
|
@@ -77,6 +77,12 @@ vi.mock('./utils/language-detection.js', () => ({
|
|
|
77
77
|
|
|
78
78
|
vi.mock('./utils/logging.js', () => ({
|
|
79
79
|
debug: vi.fn(),
|
|
80
|
+
verboseLog: vi.fn((options, ...args) => {
|
|
81
|
+
if (typeof options === 'boolean' ? options : Boolean(options?.verbose)) {
|
|
82
|
+
console.log(...args);
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
isVerboseEnabled: vi.fn((options) => Boolean(options?.verbose)),
|
|
80
86
|
}));
|
|
81
87
|
|
|
82
88
|
vi.mock('./utils/context-inference.js', () => ({
|
|
@@ -326,6 +332,29 @@ describe('rag-analyzer', () => {
|
|
|
326
332
|
const result = await runAnalysis('/test/file.js');
|
|
327
333
|
expect(result.success).toBe(true);
|
|
328
334
|
});
|
|
335
|
+
|
|
336
|
+
it('should preserve successful context sources when one parallel retrieval fails', async () => {
|
|
337
|
+
mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
|
|
338
|
+
{
|
|
339
|
+
path: '/docs/api.md',
|
|
340
|
+
content: 'API docs',
|
|
341
|
+
similarity: 0.8,
|
|
342
|
+
type: 'documentation-chunk',
|
|
343
|
+
document_title: 'API Docs',
|
|
344
|
+
heading_text: 'Usage',
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/similar.js', content: 'similar code', similarity: 0.9 }]);
|
|
348
|
+
findRelevantPRComments.mockRejectedValue(new Error('PR comments failed'));
|
|
349
|
+
llm.sendPromptToClaude.mockResolvedValue({ json: { summary: 'Review', issues: [] } });
|
|
350
|
+
|
|
351
|
+
const result = await runAnalysis('/test/file.js');
|
|
352
|
+
|
|
353
|
+
expect(result.success).toBe(true);
|
|
354
|
+
expect(result.context.codeExamples).toBeGreaterThan(0);
|
|
355
|
+
expect(result.context.guidelines).toBeGreaterThanOrEqual(0);
|
|
356
|
+
expect(result.context.prComments).toBe(0);
|
|
357
|
+
});
|
|
329
358
|
});
|
|
330
359
|
|
|
331
360
|
// ==========================================================================
|
|
@@ -797,6 +826,23 @@ describe('rag-analyzer', () => {
|
|
|
797
826
|
// ==========================================================================
|
|
798
827
|
|
|
799
828
|
describe('verbose logging paths', () => {
|
|
829
|
+
it('should suppress context dump logs when not verbose', async () => {
|
|
830
|
+
mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/example.js', content: 'code', similarity: 0.9 }]);
|
|
831
|
+
mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
|
|
832
|
+
{ path: '/docs/api.md', content: 'docs', similarity: 0.8, type: 'documentation-chunk', document_title: 'API' },
|
|
833
|
+
]);
|
|
834
|
+
findRelevantPRComments.mockResolvedValue([createMockPRComment()]);
|
|
835
|
+
setupSuccessfulLLMResponse();
|
|
836
|
+
|
|
837
|
+
const result = await runAnalysis('/test/file.js');
|
|
838
|
+
|
|
839
|
+
expect(result.success).toBe(true);
|
|
840
|
+
const loggedMessages = console.log.mock.calls.flat().join('\n');
|
|
841
|
+
expect(loggedMessages).not.toContain('Guidelines Sent to LLM');
|
|
842
|
+
expect(loggedMessages).not.toContain('Checking for PR comments in prompt generation');
|
|
843
|
+
expect(loggedMessages).not.toContain('Received LLM response, attempting to parse');
|
|
844
|
+
});
|
|
845
|
+
|
|
800
846
|
it('should log context information when verbose', async () => {
|
|
801
847
|
mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([{ path: '/example.js', content: 'code', similarity: 0.9 }]);
|
|
802
848
|
mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([
|
|
@@ -864,6 +910,15 @@ describe('rag-analyzer', () => {
|
|
|
864
910
|
expect(context).toHaveProperty('codeExamples');
|
|
865
911
|
});
|
|
866
912
|
|
|
913
|
+
it('should degrade gracefully when retrieval returns no context for active files', async () => {
|
|
914
|
+
mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([]);
|
|
915
|
+
mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([]);
|
|
916
|
+
const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
|
|
917
|
+
const context = await gatherUnifiedContextForPR(prFiles, { projectPath: '/project' });
|
|
918
|
+
expect(context.codeExamples).toEqual([]);
|
|
919
|
+
expect(context.guidelines).toEqual([]);
|
|
920
|
+
});
|
|
921
|
+
|
|
867
922
|
it('should find custom document chunks', async () => {
|
|
868
923
|
mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([{ content: 'Custom doc', document_title: 'Guidelines' }]);
|
|
869
924
|
const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
|
package/src/rag-review.js
CHANGED
|
@@ -12,6 +12,7 @@ import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
|
|
|
12
12
|
import { shouldProcessFile } from './utils/file-validation.js';
|
|
13
13
|
import { findBaseBranch, getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
|
|
14
14
|
import { detectFileType, detectLanguageFromExtension } from './utils/language-detection.js';
|
|
15
|
+
import { verboseLog } from './utils/logging.js';
|
|
15
16
|
import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chunking.js';
|
|
16
17
|
|
|
17
18
|
/**
|
|
@@ -19,20 +20,37 @@ import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chu
|
|
|
19
20
|
*
|
|
20
21
|
* @param {string} filePath - Path to the file to review
|
|
21
22
|
* @param {object} options - Review options
|
|
23
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
22
24
|
* @returns {Promise<object>} Review result object
|
|
23
25
|
*/
|
|
24
26
|
async function reviewFile(filePath, options = {}) {
|
|
25
27
|
try {
|
|
26
|
-
|
|
28
|
+
verboseLog(options, chalk.blue(`Reviewing file: ${filePath}`));
|
|
27
29
|
|
|
28
30
|
// Analyze the file using the RAG analyzer
|
|
29
31
|
const analyzeResult = await runAnalysis(filePath, options);
|
|
30
32
|
|
|
31
33
|
// If analysis successful, return the result
|
|
32
34
|
if (analyzeResult.success) {
|
|
35
|
+
if (analyzeResult.skipped) {
|
|
36
|
+
return {
|
|
37
|
+
success: true,
|
|
38
|
+
results: [
|
|
39
|
+
{
|
|
40
|
+
filePath: analyzeResult.filePath || filePath,
|
|
41
|
+
language: analyzeResult.language,
|
|
42
|
+
success: true,
|
|
43
|
+
skipped: true,
|
|
44
|
+
message: analyzeResult.message,
|
|
45
|
+
results: analyzeResult.results,
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
33
51
|
// Convert object results to array format expected by the output functions
|
|
34
52
|
if (analyzeResult.results && !Array.isArray(analyzeResult.results)) {
|
|
35
|
-
|
|
53
|
+
verboseLog(options, chalk.blue('Converting results object to array format'));
|
|
36
54
|
|
|
37
55
|
// Create a new array with one entry containing the object results
|
|
38
56
|
const resultArray = [
|
|
@@ -70,14 +88,14 @@ async function reviewFile(filePath, options = {}) {
|
|
|
70
88
|
*
|
|
71
89
|
* @param {Array<string>} filePaths - Paths to the files to review
|
|
72
90
|
* @param {Object} options - Review options (passed to each reviewFile call)
|
|
91
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
92
|
+
* @param {number} [options.concurrency=3] - Maximum number of files to review in parallel
|
|
73
93
|
* @returns {Promise<Object>} Aggregated review results { success: boolean, results: Array<Object>, message: string, error?: string }
|
|
74
94
|
*/
|
|
75
95
|
async function reviewFiles(filePaths, options = {}) {
|
|
76
96
|
try {
|
|
77
97
|
const verbose = options.verbose || false;
|
|
78
|
-
|
|
79
|
-
console.log(chalk.blue(`Reviewing ${filePaths.length} files...`));
|
|
80
|
-
}
|
|
98
|
+
verboseLog(verbose, chalk.blue(`Reviewing ${filePaths.length} files...`));
|
|
81
99
|
|
|
82
100
|
// Review files concurrently
|
|
83
101
|
const results = [];
|
|
@@ -87,21 +105,26 @@ async function reviewFiles(filePaths, options = {}) {
|
|
|
87
105
|
for (let i = 0; i < filePaths.length; i += concurrency) {
|
|
88
106
|
const batch = filePaths.slice(i, i + concurrency);
|
|
89
107
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
)
|
|
97
|
-
);
|
|
98
|
-
}
|
|
108
|
+
verboseLog(
|
|
109
|
+
verbose,
|
|
110
|
+
chalk.blue(
|
|
111
|
+
`Processing review batch ${Math.floor(i / concurrency) + 1}/${Math.ceil(filePaths.length / concurrency)} (${batch.length} files)`
|
|
112
|
+
)
|
|
113
|
+
);
|
|
99
114
|
|
|
100
115
|
// Pass options down to reviewFile
|
|
101
116
|
const batchPromises = batch.map((filePath) => reviewFile(filePath, options));
|
|
102
117
|
const batchResults = await Promise.all(batchPromises);
|
|
103
118
|
|
|
104
|
-
|
|
119
|
+
const flattenedBatchResults = batchResults.flatMap((batchResult) => {
|
|
120
|
+
if (batchResult?.success && Array.isArray(batchResult.results)) {
|
|
121
|
+
return batchResult.results;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return batchResult ? [batchResult] : [];
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
results.push(...flattenedBatchResults);
|
|
105
128
|
}
|
|
106
129
|
|
|
107
130
|
// Filter out potential null results if any step could return null/undefined (though analyzeFile should always return an object)
|
|
@@ -113,7 +136,7 @@ async function reviewFiles(filePaths, options = {}) {
|
|
|
113
136
|
let finalMessage = `Review completed for ${filePaths.length} files. `;
|
|
114
137
|
finalMessage += `Success: ${successCount}, Skipped: ${skippedCount}, Errors: ${errorCount}.`;
|
|
115
138
|
|
|
116
|
-
|
|
139
|
+
verboseLog(options, chalk.green(finalMessage));
|
|
117
140
|
|
|
118
141
|
return {
|
|
119
142
|
success: errorCount === 0,
|
|
@@ -137,14 +160,13 @@ async function reviewFiles(filePaths, options = {}) {
|
|
|
137
160
|
*
|
|
138
161
|
* @param {Array<string>} changedFilePaths - Array of file paths changed in the PR.
|
|
139
162
|
* @param {Object} options - Review options (passed to reviewFiles).
|
|
163
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
140
164
|
* @returns {Promise<Object>} Aggregated review results.
|
|
141
165
|
*/
|
|
142
166
|
async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
143
167
|
try {
|
|
144
168
|
const verbose = options.verbose || false;
|
|
145
|
-
|
|
146
|
-
console.log(chalk.blue(`Reviewing ${changedFilePaths.length} changed files from PR...`));
|
|
147
|
-
}
|
|
169
|
+
verboseLog(verbose, chalk.blue(`Reviewing ${changedFilePaths.length} changed files from PR...`));
|
|
148
170
|
|
|
149
171
|
// No longer filter files here, as new files in a different branch won't exist locally.
|
|
150
172
|
// The downstream functions are responsible for fetching content from git.
|
|
@@ -152,7 +174,7 @@ async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
|
152
174
|
|
|
153
175
|
if (filesToReview.length === 0) {
|
|
154
176
|
const message = 'No processable files found among the changed files provided for PR review.';
|
|
155
|
-
|
|
177
|
+
verboseLog(options, chalk.yellow(message));
|
|
156
178
|
return {
|
|
157
179
|
success: true,
|
|
158
180
|
message: message,
|
|
@@ -160,9 +182,7 @@ async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
|
160
182
|
};
|
|
161
183
|
}
|
|
162
184
|
|
|
163
|
-
|
|
164
|
-
console.log(chalk.green(`Reviewing ${filesToReview.length} existing and processable changed files`));
|
|
165
|
-
}
|
|
185
|
+
verboseLog(verbose, chalk.green(`Reviewing ${filesToReview.length} existing and processable changed files`));
|
|
166
186
|
|
|
167
187
|
// Use enhanced PR review with cross-file context
|
|
168
188
|
return await reviewPullRequestWithCrossFileContext(filesToReview, options);
|
|
@@ -183,14 +203,18 @@ async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
|
183
203
|
*
|
|
184
204
|
* @param {Array<string>} filesToReview - Array of file paths to review
|
|
185
205
|
* @param {Object} options - Review options
|
|
206
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
207
|
+
* @param {number} [options.concurrency=3] - Maximum number of fallback per-file reviews to run in parallel
|
|
208
|
+
* @param {string} [options.directory] - Working directory for git operations
|
|
209
|
+
* @param {string} [options.diffWith='HEAD'] - Branch or ref to compare against
|
|
210
|
+
* @param {string} [options.actualBranch] - Actual target branch used for content retrieval
|
|
211
|
+
* @param {boolean} [options.skipChunking=false] - Skip PR chunking, used internally for chunk review recursion
|
|
186
212
|
* @returns {Promise<Object>} Aggregated review results
|
|
187
213
|
*/
|
|
188
214
|
async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}) {
|
|
189
215
|
try {
|
|
190
216
|
const verbose = options.verbose || false;
|
|
191
|
-
|
|
192
|
-
console.log(chalk.blue(`Starting enhanced PR review with cross-file context for ${filesToReview.length} files...`));
|
|
193
|
-
}
|
|
217
|
+
verboseLog(verbose, chalk.blue(`Starting enhanced PR review with cross-file context for ${filesToReview.length} files...`));
|
|
194
218
|
|
|
195
219
|
// Step 1: Get the base branch and collect diff info for all files in the PR
|
|
196
220
|
const workingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
@@ -200,18 +224,14 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
200
224
|
// Get the actual branch name from options passed from index.js
|
|
201
225
|
const actualTargetBranch = options.actualBranch || targetBranch;
|
|
202
226
|
|
|
203
|
-
|
|
204
|
-
console.log(chalk.gray(`Base branch: ${baseBranch}, Target branch: ${targetBranch}`));
|
|
205
|
-
}
|
|
227
|
+
verboseLog(verbose, chalk.gray(`Base branch: ${baseBranch}, Target branch: ${targetBranch}`));
|
|
206
228
|
|
|
207
229
|
const prFiles = [];
|
|
208
230
|
for (const filePath of filesToReview) {
|
|
209
231
|
try {
|
|
210
232
|
// Check if the file should be processed before fetching its content from git
|
|
211
233
|
if (!shouldProcessFile(filePath, '', options)) {
|
|
212
|
-
|
|
213
|
-
console.log(chalk.yellow(`Skipping file due to exclusion rules: ${path.basename(filePath)}`));
|
|
214
|
-
}
|
|
234
|
+
verboseLog(verbose, chalk.yellow(`Skipping file due to exclusion rules: ${path.basename(filePath)}`));
|
|
215
235
|
continue;
|
|
216
236
|
}
|
|
217
237
|
|
|
@@ -223,9 +243,7 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
223
243
|
const diffInfo = getChangedLinesInfo(filePath, baseBranch, actualTargetBranch, workingDir);
|
|
224
244
|
|
|
225
245
|
if (!diffInfo.hasChanges) {
|
|
226
|
-
|
|
227
|
-
console.log(chalk.yellow(`No changes detected in ${path.basename(filePath)}, skipping`));
|
|
228
|
-
}
|
|
246
|
+
verboseLog(verbose, chalk.yellow(`No changes detected in ${path.basename(filePath)}, skipping`));
|
|
229
247
|
continue;
|
|
230
248
|
}
|
|
231
249
|
|
|
@@ -251,7 +269,7 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
if (prFiles.length === 0) {
|
|
254
|
-
|
|
272
|
+
verboseLog(options, chalk.yellow('No files with changes found for review'));
|
|
255
273
|
return {
|
|
256
274
|
success: true,
|
|
257
275
|
results: [],
|
|
@@ -261,25 +279,22 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
261
279
|
|
|
262
280
|
// Check if PR should be chunked based on size and complexity (skip if this is already a chunk)
|
|
263
281
|
if (!options.skipChunking) {
|
|
264
|
-
const chunkingDecision = shouldChunkPR(prFiles);
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
282
|
+
const chunkingDecision = shouldChunkPR(prFiles, options);
|
|
283
|
+
verboseLog(verbose, chalk.blue(`PR size assessment: ${chunkingDecision.estimatedTokens} tokens, ${prFiles.length} files`));
|
|
284
|
+
verboseLog(
|
|
285
|
+
verbose && chunkingDecision.shouldChunk,
|
|
286
|
+
chalk.yellow(`Large PR detected - will chunk into ~${chunkingDecision.recommendedChunks} chunks`)
|
|
287
|
+
);
|
|
271
288
|
|
|
272
289
|
// If PR is too large, use chunked processing
|
|
273
290
|
if (chunkingDecision.shouldChunk) {
|
|
274
|
-
|
|
291
|
+
verboseLog(options, chalk.blue(`🔄 Using chunked processing for large PR (${chunkingDecision.estimatedTokens} tokens)`));
|
|
275
292
|
return await reviewLargePRInChunks(prFiles, options);
|
|
276
293
|
}
|
|
277
294
|
}
|
|
278
295
|
|
|
279
296
|
// Step 2: Gather unified context for the entire PR (for regular-sized PRs)
|
|
280
|
-
|
|
281
|
-
console.log(chalk.blue(`Performing unified context retrieval for ${prFiles.length} PR files...`));
|
|
282
|
-
}
|
|
297
|
+
verboseLog(verbose, chalk.blue(`Performing unified context retrieval for ${prFiles.length} PR files...`));
|
|
283
298
|
const {
|
|
284
299
|
codeExamples: deduplicatedCodeExamples,
|
|
285
300
|
guidelines: deduplicatedGuidelines,
|
|
@@ -287,13 +302,12 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
287
302
|
customDocChunks: deduplicatedCustomDocChunks,
|
|
288
303
|
} = await gatherUnifiedContextForPR(prFiles, options);
|
|
289
304
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
305
|
+
verboseLog(
|
|
306
|
+
verbose,
|
|
307
|
+
chalk.green(
|
|
308
|
+
`De-duplicated context: ${deduplicatedCodeExamples.length} code examples, ${deduplicatedGuidelines.length} guidelines, ${deduplicatedPRComments.length} PR comments, ${deduplicatedCustomDocChunks.length} custom doc chunks`
|
|
309
|
+
)
|
|
310
|
+
);
|
|
297
311
|
|
|
298
312
|
// Step 3: Create PR context summary for LLM
|
|
299
313
|
const prContext = {
|
|
@@ -310,9 +324,7 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
310
324
|
};
|
|
311
325
|
|
|
312
326
|
// Step 4: Perform holistic PR review with all files and unified context
|
|
313
|
-
|
|
314
|
-
console.log(chalk.blue(`Performing holistic PR review for all ${prFiles.length} files...`));
|
|
315
|
-
}
|
|
327
|
+
verboseLog(verbose, chalk.blue(`Performing holistic PR review for all ${prFiles.length} files...`));
|
|
316
328
|
|
|
317
329
|
try {
|
|
318
330
|
// Create a comprehensive review context with all files and their diffs
|
|
@@ -367,15 +379,15 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
367
379
|
for (const key of possibleKeys) {
|
|
368
380
|
if (holisticResult?.results?.fileSpecificIssues?.[key]) {
|
|
369
381
|
fileIssues = holisticResult.results.fileSpecificIssues[key];
|
|
370
|
-
|
|
382
|
+
verboseLog(options, chalk.green(`✅ Found ${fileIssues.length} issues for ${baseName} using key: "${key}"`));
|
|
371
383
|
break;
|
|
372
384
|
}
|
|
373
385
|
}
|
|
374
386
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
387
|
+
verboseLog(options, chalk.gray(`🔍 Mapping issues for ${file.filePath}:`));
|
|
388
|
+
verboseLog(options, chalk.gray(` - Relative path: "${relativePath}"`));
|
|
389
|
+
verboseLog(options, chalk.gray(` - Tried keys: ${possibleKeys.map((k) => `"${k}"`).join(', ')}`));
|
|
390
|
+
verboseLog(options, chalk.gray(` - Final issues: ${fileIssues.length}`));
|
|
379
391
|
|
|
380
392
|
return {
|
|
381
393
|
success: true,
|
|
@@ -421,9 +433,7 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
421
433
|
console.error(chalk.red(`Error in holistic PR review: ${error.message}`));
|
|
422
434
|
|
|
423
435
|
// Fallback to individual file review if holistic review fails
|
|
424
|
-
|
|
425
|
-
console.log(chalk.yellow(`Falling back to individual file reviews...`));
|
|
426
|
-
}
|
|
436
|
+
verboseLog(verbose, chalk.yellow(`Falling back to individual file reviews...`));
|
|
427
437
|
|
|
428
438
|
const results = [];
|
|
429
439
|
const concurrency = options.concurrency || 3;
|
|
@@ -500,33 +510,53 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
500
510
|
* Reviews a large PR by splitting it into manageable chunks and processing them in parallel
|
|
501
511
|
* @param {Array} prFiles - Array of PR files with diff content
|
|
502
512
|
* @param {Object} options - Review options
|
|
513
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
503
514
|
* @returns {Promise<Object>} Combined review results
|
|
504
515
|
*/
|
|
505
516
|
async function reviewLargePRInChunks(prFiles, options) {
|
|
506
|
-
|
|
517
|
+
verboseLog(options, chalk.blue(`🔄 Large PR detected: ${prFiles.length} files. Splitting into chunks...`));
|
|
507
518
|
|
|
508
519
|
// Step 1: Gather shared context once for all chunks
|
|
509
|
-
|
|
520
|
+
verboseLog(options, chalk.cyan('📚 Gathering shared context for entire PR...'));
|
|
510
521
|
const sharedContext = await gatherUnifiedContextForPR(prFiles, options);
|
|
511
522
|
|
|
512
523
|
// Step 2: Split PR into manageable chunks
|
|
513
524
|
// Each chunk includes both diff AND full file content, plus ~25k context overhead
|
|
514
525
|
const chunks = chunkPRFiles(prFiles, 35000); // Conservative limit accounting for context overhead
|
|
515
|
-
|
|
526
|
+
verboseLog(options, chalk.green(`✂️ Split PR into ${chunks.length} chunks`));
|
|
516
527
|
|
|
517
528
|
chunks.forEach((chunk, i) => {
|
|
518
|
-
|
|
529
|
+
verboseLog(options, chalk.gray(` Chunk ${i + 1}: ${chunk.files.length} files (~${chunk.totalTokens} tokens)`));
|
|
519
530
|
});
|
|
520
531
|
|
|
521
532
|
// Step 3: Process chunks in parallel
|
|
522
|
-
|
|
523
|
-
const
|
|
533
|
+
verboseLog(options, chalk.blue('🔄 Processing chunks in parallel...'));
|
|
534
|
+
const settledChunkResults = await Promise.allSettled(
|
|
524
535
|
chunks.map((chunk, index) => reviewPRChunk(chunk, sharedContext, options, index + 1, chunks.length))
|
|
525
536
|
);
|
|
526
537
|
|
|
538
|
+
const chunkResults = settledChunkResults.map((result, index) => {
|
|
539
|
+
if (result.status === 'fulfilled') {
|
|
540
|
+
return {
|
|
541
|
+
chunkId: index + 1,
|
|
542
|
+
...result.value,
|
|
543
|
+
};
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const errorMessage = result.reason instanceof Error ? result.reason.message : String(result.reason);
|
|
547
|
+
console.warn(chalk.yellow(`Chunk ${index + 1}/${chunks.length} failed: ${errorMessage}`));
|
|
548
|
+
|
|
549
|
+
return {
|
|
550
|
+
chunkId: index + 1,
|
|
551
|
+
success: false,
|
|
552
|
+
error: errorMessage,
|
|
553
|
+
results: [],
|
|
554
|
+
};
|
|
555
|
+
});
|
|
556
|
+
|
|
527
557
|
// Step 4: Combine results
|
|
528
|
-
|
|
529
|
-
return combineChunkResults(chunkResults, prFiles.length);
|
|
558
|
+
verboseLog(options, chalk.blue('🔗 Combining chunk results...'));
|
|
559
|
+
return combineChunkResults(chunkResults, prFiles.length, options);
|
|
530
560
|
}
|
|
531
561
|
|
|
532
562
|
/**
|
|
@@ -534,12 +564,13 @@ async function reviewLargePRInChunks(prFiles, options) {
|
|
|
534
564
|
* @param {Object} chunk - Chunk object with files array
|
|
535
565
|
* @param {Object} sharedContext - Pre-gathered shared context
|
|
536
566
|
* @param {Object} options - Review options
|
|
567
|
+
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
537
568
|
* @param {number} chunkNumber - Current chunk number
|
|
538
569
|
* @param {number} totalChunks - Total number of chunks
|
|
539
570
|
* @returns {Promise<Object>} Chunk review results
|
|
540
571
|
*/
|
|
541
572
|
async function reviewPRChunk(chunk, sharedContext, options, chunkNumber, totalChunks) {
|
|
542
|
-
|
|
573
|
+
verboseLog(options, chalk.cyan(`📝 Reviewing chunk ${chunkNumber}/${totalChunks} (${chunk.files.length} files)...`));
|
|
543
574
|
|
|
544
575
|
// Create chunk-specific options
|
|
545
576
|
const chunkOptions = {
|
package/src/rag-review.test.js
CHANGED
|
@@ -94,16 +94,62 @@ describe('rag-review', () => {
|
|
|
94
94
|
|
|
95
95
|
expect(runAnalysis).toHaveBeenCalledWith('/test/file.js', { verbose: true, maxExamples: 10 });
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
it('should wrap skipped files with file metadata', async () => {
|
|
99
|
+
runAnalysis.mockResolvedValue({
|
|
100
|
+
success: true,
|
|
101
|
+
skipped: true,
|
|
102
|
+
message: 'File skipped based on exclusion patterns',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await reviewFile('/test/skipped.js');
|
|
106
|
+
|
|
107
|
+
expect(result.success).toBe(true);
|
|
108
|
+
expect(result.results).toEqual([
|
|
109
|
+
expect.objectContaining({
|
|
110
|
+
filePath: '/test/skipped.js',
|
|
111
|
+
success: true,
|
|
112
|
+
skipped: true,
|
|
113
|
+
message: 'File skipped based on exclusion patterns',
|
|
114
|
+
}),
|
|
115
|
+
]);
|
|
116
|
+
});
|
|
97
117
|
});
|
|
98
118
|
|
|
99
119
|
describe('reviewFiles', () => {
|
|
100
120
|
it('should review multiple files', async () => {
|
|
101
|
-
runAnalysis
|
|
121
|
+
runAnalysis
|
|
122
|
+
.mockResolvedValueOnce({
|
|
123
|
+
success: true,
|
|
124
|
+
filePath: '/test/file1.js',
|
|
125
|
+
language: 'javascript',
|
|
126
|
+
results: { issues: [] },
|
|
127
|
+
})
|
|
128
|
+
.mockResolvedValueOnce({
|
|
129
|
+
success: true,
|
|
130
|
+
filePath: '/test/file2.js',
|
|
131
|
+
language: 'javascript',
|
|
132
|
+
results: { issues: [] },
|
|
133
|
+
});
|
|
102
134
|
|
|
103
135
|
const result = await reviewFiles(['/test/file1.js', '/test/file2.js']);
|
|
104
136
|
|
|
105
137
|
expect(result.success).toBe(true);
|
|
106
138
|
expect(result.results.length).toBe(2);
|
|
139
|
+
expect(result.results[0]).toEqual(
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
filePath: '/test/file1.js',
|
|
142
|
+
success: true,
|
|
143
|
+
results: { issues: [] },
|
|
144
|
+
})
|
|
145
|
+
);
|
|
146
|
+
expect(result.results[1]).toEqual(
|
|
147
|
+
expect.objectContaining({
|
|
148
|
+
filePath: '/test/file2.js',
|
|
149
|
+
success: true,
|
|
150
|
+
results: { issues: [] },
|
|
151
|
+
})
|
|
152
|
+
);
|
|
107
153
|
});
|
|
108
154
|
|
|
109
155
|
it('should process files in batches based on concurrency', async () => {
|
|
@@ -116,8 +162,13 @@ describe('rag-review', () => {
|
|
|
116
162
|
|
|
117
163
|
it('should count successes, skips, and errors', async () => {
|
|
118
164
|
runAnalysis
|
|
119
|
-
.mockResolvedValueOnce({
|
|
120
|
-
|
|
165
|
+
.mockResolvedValueOnce({
|
|
166
|
+
success: true,
|
|
167
|
+
filePath: '/file1.js',
|
|
168
|
+
language: 'javascript',
|
|
169
|
+
results: { issues: [] },
|
|
170
|
+
})
|
|
171
|
+
.mockResolvedValueOnce({ success: true, skipped: true, message: 'Skipped' })
|
|
121
172
|
.mockResolvedValueOnce({ success: false, error: 'Error' });
|
|
122
173
|
|
|
123
174
|
const result = await reviewFiles(['/file1.js', '/file2.js', '/file3.js']);
|
|
@@ -219,6 +270,67 @@ describe('rag-review', () => {
|
|
|
219
270
|
expect(result.success).toBe(true);
|
|
220
271
|
});
|
|
221
272
|
|
|
273
|
+
it('should preserve successful chunk results when one chunk fails', async () => {
|
|
274
|
+
shouldProcessFile.mockReturnValue(true);
|
|
275
|
+
getChangedLinesInfo.mockReturnValue({
|
|
276
|
+
hasChanges: true,
|
|
277
|
+
addedLines: [1, 2, 3],
|
|
278
|
+
removedLines: [],
|
|
279
|
+
fullDiff: '+ new code',
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
shouldChunkPR.mockReturnValue({ shouldChunk: true, estimatedTokens: 100000, recommendedChunks: 2 });
|
|
283
|
+
chunkPRFiles.mockReturnValue([
|
|
284
|
+
{ files: [{ filePath: '/file1.js' }], totalTokens: 30000 },
|
|
285
|
+
{ files: [{ filePath: '/file2.js' }], totalTokens: 30000 },
|
|
286
|
+
]);
|
|
287
|
+
|
|
288
|
+
const allSettledSpy = vi.spyOn(Promise, 'allSettled').mockResolvedValue([
|
|
289
|
+
{
|
|
290
|
+
status: 'fulfilled',
|
|
291
|
+
value: {
|
|
292
|
+
success: true,
|
|
293
|
+
results: [{ filePath: '/file1.js', success: true }],
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
status: 'rejected',
|
|
298
|
+
reason: new Error('Chunk LLM failed'),
|
|
299
|
+
},
|
|
300
|
+
]);
|
|
301
|
+
|
|
302
|
+
combineChunkResults.mockImplementation((chunkResults) => ({
|
|
303
|
+
success: true,
|
|
304
|
+
results: chunkResults.filter((chunk) => chunk.success).flatMap((chunk) => chunk.results || []),
|
|
305
|
+
chunkResults,
|
|
306
|
+
}));
|
|
307
|
+
|
|
308
|
+
runAnalysis.mockResolvedValueOnce({
|
|
309
|
+
success: true,
|
|
310
|
+
results: { fileSpecificIssues: {}, crossFileIssues: [], summary: 'OK' },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const result = await reviewPullRequest(['/file1.js', '/file2.js'], { verbose: true });
|
|
314
|
+
|
|
315
|
+
expect(combineChunkResults).toHaveBeenCalledWith(
|
|
316
|
+
expect.arrayContaining([
|
|
317
|
+
expect.objectContaining({ chunkId: 1, success: true }),
|
|
318
|
+
expect.objectContaining({
|
|
319
|
+
chunkId: 2,
|
|
320
|
+
success: false,
|
|
321
|
+
error: 'Chunk LLM failed',
|
|
322
|
+
results: [],
|
|
323
|
+
}),
|
|
324
|
+
]),
|
|
325
|
+
2,
|
|
326
|
+
expect.objectContaining({ verbose: true })
|
|
327
|
+
);
|
|
328
|
+
expect(allSettledSpy).toHaveBeenCalled();
|
|
329
|
+
expect(result.success).toBe(true);
|
|
330
|
+
expect(result.results).toHaveLength(1);
|
|
331
|
+
expect(result.chunkResults).toHaveLength(2);
|
|
332
|
+
});
|
|
333
|
+
|
|
222
334
|
it('should gather unified context for PR files', async () => {
|
|
223
335
|
// Setup analysis to return valid holistic result
|
|
224
336
|
runAnalysis.mockResolvedValue({
|
|
@@ -10,6 +10,7 @@ import * as linguistLanguages from 'linguist-languages';
|
|
|
10
10
|
import { LRUCache } from 'lru-cache';
|
|
11
11
|
import stopwords from 'stopwords-iso/stopwords-iso.json' with { type: 'json' };
|
|
12
12
|
import techKeywords from './technology-keywords.json' with { type: 'json' };
|
|
13
|
+
import { verboseLog } from './utils/logging.js';
|
|
13
14
|
import { truncateToTokenLimit } from './utils/mobilebert-tokenizer.js';
|
|
14
15
|
|
|
15
16
|
// Configure Transformers.js environment
|
|
@@ -124,14 +125,14 @@ class OpenZeroShotClassifier {
|
|
|
124
125
|
|
|
125
126
|
async _doInitialize() {
|
|
126
127
|
try {
|
|
127
|
-
|
|
128
|
+
verboseLog({}, 'Initializing open-ended zero-shot classifier...');
|
|
128
129
|
|
|
129
130
|
this.classifier = await pipeline('zero-shot-classification', 'Xenova/mobilebert-uncased-mnli', {
|
|
130
131
|
quantized: true,
|
|
131
132
|
});
|
|
132
133
|
|
|
133
134
|
this.isInitialized = true;
|
|
134
|
-
|
|
135
|
+
verboseLog({}, '✓ Open-ended zero-shot classifier initialized successfully');
|
|
135
136
|
} catch (error) {
|
|
136
137
|
console.error('Error initializing classifier:', error);
|
|
137
138
|
this.isInitialized = false;
|