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.
@@ -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
- console.log(chalk.blue(`Reviewing file: ${filePath}`));
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
- console.log(chalk.blue('Converting results object to array format'));
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
- if (verbose) {
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
- 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
- }
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
- results.push(...batchResults);
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
- console.log(chalk.green(finalMessage));
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
- if (verbose) {
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
- console.log(chalk.yellow(message));
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
- if (verbose) {
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
- if (verbose) {
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
- if (verbose) {
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
- if (verbose) {
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
- if (verbose) {
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
- console.log(chalk.yellow('No files with changes found for review'));
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
- 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
- }
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
- console.log(chalk.blue(`🔄 Using chunked processing for large PR (${chunkingDecision.estimatedTokens} tokens)`));
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
- if (verbose) {
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
- 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
- }
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
- if (verbose) {
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
- console.log(chalk.green(`✅ Found ${fileIssues.length} issues for ${baseName} using key: "${key}"`));
382
+ verboseLog(options, chalk.green(`✅ Found ${fileIssues.length} issues for ${baseName} using key: "${key}"`));
371
383
  break;
372
384
  }
373
385
  }
374
386
 
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}`));
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
- if (verbose) {
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
- console.log(chalk.blue(`🔄 Large PR detected: ${prFiles.length} files. Splitting into chunks...`));
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
- console.log(chalk.cyan('📚 Gathering shared context for entire PR...'));
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
- console.log(chalk.green(`✂️ Split PR into ${chunks.length} chunks`));
526
+ verboseLog(options, chalk.green(`✂️ Split PR into ${chunks.length} chunks`));
516
527
 
517
528
  chunks.forEach((chunk, i) => {
518
- console.log(chalk.gray(` Chunk ${i + 1}: ${chunk.files.length} files (~${chunk.totalTokens} tokens)`));
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
- console.log(chalk.blue('🔄 Processing chunks in parallel...'));
523
- const chunkResults = await Promise.all(
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
- console.log(chalk.blue('🔗 Combining chunk results...'));
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
- console.log(chalk.cyan(`📝 Reviewing chunk ${chunkNumber}/${totalChunks} (${chunk.files.length} files)...`));
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 = {
@@ -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.mockResolvedValue({ success: true, results: [] });
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({ success: true, results: [] })
120
- .mockResolvedValueOnce({ success: true, skipped: true, results: [] })
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
- console.log('Initializing open-ended zero-shot classifier...');
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
- console.log('✓ Open-ended zero-shot classifier initialized successfully');
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;