codecritique 2.0.8 → 2.1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.0.8",
3
+ "version": "2.1.0",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -27,6 +27,7 @@ import { isGenericDocument, getGenericDocumentContext } from './utils/document-d
27
27
  import { isTestFile, shouldProcessFile } from './utils/file-validation.js';
28
28
  import { detectFileType, detectLanguageFromExtension } from './utils/language-detection.js';
29
29
  import { debug, verboseLog } from './utils/logging.js';
30
+ import { formatPlannedHolisticFileContext, mergeHolisticFileContextPlans } from './utils/pr-file-context.js';
30
31
  import { addLineNumbers } from './utils/string-utils.js';
31
32
 
32
33
  // Constants for content processing
@@ -1376,23 +1377,24 @@ ${g.content}
1376
1377
 
1377
1378
  // Format PR files with their diffs
1378
1379
  const prFiles = file.prFiles || [];
1380
+ const holisticContextPlans = mergeHolisticFileContextPlans(prFiles, context.options || {});
1379
1381
  const formattedPRFiles = prFiles
1380
1382
  .map((prFile, idx) => {
1383
+ const fileContextPlan = holisticContextPlans[idx];
1384
+ const fileContext = formatPlannedHolisticFileContext(prFile, fileContextPlan, context.options || {});
1381
1385
  return `
1382
1386
  ## FILE ${idx + 1}: ${prFile.path}
1383
1387
  **Language**: ${prFile.language}
1384
1388
  **Type**: ${prFile.isTest ? 'Test' : 'Source'} file
1385
1389
  **Summary**: ${prFile.summary}
1390
+ **Context Mode**: ${fileContextPlan.mode === 'full' ? 'Full file content' : 'Focused changed-line context'}
1386
1391
 
1387
1392
  ### Changes (Git Diff):
1388
1393
  \`\`\`diff
1389
1394
  ${prFile.diff}
1390
1395
  \`\`\`
1391
1396
 
1392
- ### Full File Content (For Context - line numbers shown for reference):
1393
- \`\`\`${prFile.language}
1394
- ${addLineNumbers(prFile.fullContent)}
1395
- \`\`\`
1397
+ ${fileContext}
1396
1398
  `;
1397
1399
  })
1398
1400
  .join('\n');
@@ -1406,17 +1408,17 @@ ${addLineNumbers(prFile.fullContent)}
1406
1408
  **CRITICAL CONTEXT AWARENESS INSTRUCTIONS:**
1407
1409
 
1408
1410
  For each file in this PR, you have access to:
1409
- 1. **FULL FILE CONTENT** - The complete file for understanding context and existing code
1411
+ 1. **FILE CONTEXT** - Complete file content when it fits the holistic context budget, or focused changed-line windows for very large files/PRs
1410
1412
  2. **GIT DIFF** - Only the changes to review
1411
1413
 
1412
1414
  **Review Rules:**
1413
1415
  - ONLY critique the CHANGED lines shown in each file's diff (lines with + or -)
1414
- - USE the full file content to understand context, dependencies, and existing implementations
1416
+ - USE the file context to understand dependencies, nearby behavior, and existing implementations
1415
1417
  - DO NOT suggest adding code that already exists in the unchanged portions of any file
1416
- - DO NOT flag issues about missing code if it exists elsewhere in the full file
1417
- - Before flagging cross-file issues, verify the code doesn't already exist in unchanged portions
1418
- - Do NOT flag functions/variables as missing if they exist elsewhere in the full file
1419
- - The unchanged code is part of each file - always check it before making assumptions`;
1418
+ - DO NOT flag issues about missing code when the provided context already shows the code exists
1419
+ - Before flagging cross-file issues, verify the code doesn't already exist in the provided context
1420
+ - Do NOT flag functions/variables as missing if they exist elsewhere in the provided context
1421
+ - The unchanged context is part of each file - always check it before making assumptions`;
1420
1422
 
1421
1423
  let roleDefinition = buildRoleDefinition(baseRole, customDocs, 'pr');
1422
1424
  roleDefinition += '\nAnalyze ALL files together to identify cross-file issues, consistency problems, and overall code quality.';
@@ -578,6 +578,70 @@ describe('rag-analyzer', () => {
578
578
  );
579
579
  expect(result.success).toBe(true);
580
580
  });
581
+
582
+ it('should use focused context for oversized holistic PR files', async () => {
583
+ const hugeContent = Array.from({ length: 5000 }, (_, index) => `huge line ${index + 1}`).join('\n');
584
+ llm.sendPromptToClaude.mockResolvedValue(createMockHolisticReviewResponse());
585
+
586
+ const result = await runAnalysis(
587
+ 'PR_HOLISTIC_REVIEW',
588
+ setupHolisticReviewOptions({
589
+ prFiles: [
590
+ createMockPRFile({
591
+ path: 'src/small.js',
592
+ fullContent: 'const small = true;',
593
+ diff: '@@ -1,1 +1,1 @@\n-const small = false;\n+const small = true;',
594
+ }),
595
+ createMockPRFile({
596
+ path: 'src/huge.js',
597
+ fullContent: hugeContent,
598
+ diff: '@@ -2500,1 +2500,1 @@\n-huge line 2500\n+huge line 2500 changed',
599
+ }),
600
+ ],
601
+ prContext: { totalFiles: 2 },
602
+ maxTotalFullContentTokens: 1000,
603
+ })
604
+ );
605
+
606
+ const prompt = llm.sendPromptToClaude.mock.calls.at(-1)[0];
607
+ expect(result.success).toBe(true);
608
+ expect(prompt).toContain('**Context Mode**: Full file content');
609
+ expect(prompt).toContain('**Context Mode**: Focused changed-line context');
610
+ expect(prompt).toContain('const small = true;');
611
+ expect(prompt).toContain('Focused File Context');
612
+ expect(prompt).toContain('2500 | huge line 2500');
613
+ expect(prompt).not.toContain('5000 | huge line 5000');
614
+ });
615
+
616
+ it('should preserve carried holistic context plans for chunked reviews', async () => {
617
+ llm.sendPromptToClaude.mockResolvedValue(createMockHolisticReviewResponse());
618
+
619
+ await runAnalysis(
620
+ 'PR_HOLISTIC_REVIEW',
621
+ setupHolisticReviewOptions({
622
+ prFiles: [
623
+ createMockPRFile({
624
+ path: 'src/chunked.js',
625
+ fullContent: 'const wouldFit = true;',
626
+ diff: '@@ -1,1 +1,1 @@\n-const wouldFit = false;\n+const wouldFit = true;',
627
+ holisticContextPlan: {
628
+ mode: 'focused',
629
+ reason: 'global budget assigned focused context before chunking',
630
+ contextTokens: 5,
631
+ totalTokens: 20,
632
+ contextLineRadius: 0,
633
+ },
634
+ }),
635
+ ],
636
+ prContext: { totalFiles: 1 },
637
+ })
638
+ );
639
+
640
+ const prompt = llm.sendPromptToClaude.mock.calls.at(-1)[0];
641
+ expect(prompt).toContain('**Context Mode**: Focused changed-line context');
642
+ expect(prompt).toContain('global budget assigned focused context before chunking');
643
+ expect(prompt).not.toContain('### Full File Content');
644
+ });
581
645
  });
582
646
 
583
647
  // ==========================================================================
package/src/rag-review.js CHANGED
@@ -14,6 +14,7 @@ import { ensureBranchExists, findParentBranch, getChangedLinesInfo, getFileConte
14
14
  import { detectFileType, detectLanguageFromExtension } from './utils/language-detection.js';
15
15
  import { verboseLog } from './utils/logging.js';
16
16
  import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chunking.js';
17
+ import { mergeHolisticFileContextPlans } from './utils/pr-file-context.js';
17
18
 
18
19
  /**
19
20
  * Review a single file using RAG approach
@@ -308,8 +309,10 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
308
309
  }
309
310
 
310
311
  // Check if PR should be chunked based on size and complexity (skip if this is already a chunk)
312
+ let holisticContextPlans = options.holisticContextPlans;
311
313
  if (!options.skipChunking) {
312
314
  const chunkingDecision = shouldChunkPR(prFiles, options);
315
+ holisticContextPlans = chunkingDecision.holisticContextPlans;
313
316
  verboseLog(verbose, chalk.blue(`PR size assessment: ${chunkingDecision.estimatedTokens} tokens, ${prFiles.length} files`));
314
317
  verboseLog(
315
318
  verbose && chunkingDecision.shouldChunk,
@@ -319,10 +322,12 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
319
322
  // If PR is too large, use chunked processing
320
323
  if (chunkingDecision.shouldChunk) {
321
324
  verboseLog(options, chalk.blue(`🔄 Using chunked processing for large PR (${chunkingDecision.estimatedTokens} tokens)`));
322
- return await reviewLargePRInChunks(prFiles, options);
325
+ return await reviewLargePRInChunks(prFiles, { ...options, holisticContextPlans });
323
326
  }
324
327
  }
325
328
 
329
+ holisticContextPlans = mergeHolisticFileContextPlans(prFiles, options, holisticContextPlans);
330
+
326
331
  // Step 2: Gather unified context for the entire PR (for regular-sized PRs)
327
332
  verboseLog(verbose, chalk.blue(`Performing unified context retrieval for ${prFiles.length} PR files...`));
328
333
  const {
@@ -361,7 +366,7 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
361
366
  try {
362
367
  // Create a comprehensive review context with all files and their diffs
363
368
  const comprehensiveContext = {
364
- prFiles: prFiles.map((file) => ({
369
+ prFiles: prFiles.map((file, index) => ({
365
370
  path: path.relative(workingDir, file.filePath),
366
371
  language: file.language,
367
372
  isTest: file.isTest,
@@ -369,6 +374,8 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
369
374
  summary: file.summary,
370
375
  fullContent: file.content, // Add full file content for context
371
376
  diff: file.diffContent,
377
+ diffInfo: file.diffInfo,
378
+ holisticContextPlan: file.holisticContextPlan || holisticContextPlans[index],
372
379
  baseBranch: file.baseBranch,
373
380
  targetBranch: file.targetBranch,
374
381
  })),
@@ -557,7 +564,7 @@ async function reviewLargePRInChunks(prFiles, options) {
557
564
 
558
565
  // Step 2: Split PR into manageable chunks
559
566
  // Each chunk includes both diff AND full file content, plus ~25k context overhead
560
- const chunks = chunkPRFiles(prFiles, 35000); // Conservative limit accounting for context overhead
567
+ const chunks = chunkPRFiles(prFiles, 35000, options); // Conservative limit accounting for context overhead
561
568
  verboseLog(options, chalk.green(`✂️ Split PR into ${chunks.length} chunks`));
562
569
 
563
570
  chunks.forEach((chunk, i) => {
@@ -270,7 +270,8 @@ describe('rag-review', () => {
270
270
  });
271
271
 
272
272
  // Setup: shouldChunkPR returns true to trigger chunked processing
273
- shouldChunkPR.mockReturnValue({ shouldChunk: true, estimatedTokens: 100000, recommendedChunks: 2 });
273
+ const holisticContextPlans = [{ mode: 'focused', contextTokens: 10, totalTokens: 20 }];
274
+ shouldChunkPR.mockReturnValue({ shouldChunk: true, estimatedTokens: 100000, recommendedChunks: 2, holisticContextPlans });
274
275
 
275
276
  // Setup chunks that will be processed
276
277
  chunkPRFiles.mockReturnValue([
@@ -298,7 +299,7 @@ describe('rag-review', () => {
298
299
 
299
300
  // Verify chunking flow was triggered
300
301
  expect(shouldChunkPR).toHaveBeenCalled();
301
- expect(chunkPRFiles).toHaveBeenCalled();
302
+ expect(chunkPRFiles).toHaveBeenCalledWith(expect.any(Array), 35000, expect.objectContaining({ holisticContextPlans }));
302
303
  expect(combineChunkResults).toHaveBeenCalled();
303
304
  expect(result.success).toBe(true);
304
305
  });