codecritique 2.0.7 → 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 +1 -1
- package/src/project-analyzer.js +26 -3
- package/src/rag-analyzer.js +35 -15
- package/src/rag-analyzer.test.js +64 -0
- package/src/rag-review.js +10 -3
- package/src/rag-review.test.js +3 -2
package/package.json
CHANGED
package/src/project-analyzer.js
CHANGED
|
@@ -405,8 +405,25 @@ export class ProjectAnalyzer {
|
|
|
405
405
|
return depthA - depthB;
|
|
406
406
|
});
|
|
407
407
|
|
|
408
|
-
//
|
|
409
|
-
|
|
408
|
+
// Dedupe by path before capping: transient duplicate rows can exist for
|
|
409
|
+
// the same file (different content_hash) when a partial run hasn't pruned
|
|
410
|
+
// stale embeddings, and they would otherwise consume candidate slots and
|
|
411
|
+
// bloat the content query. Keep the shallowest occurrence (allFiles is
|
|
412
|
+
// already depth-sorted), then take the first 500 distinct shallow files.
|
|
413
|
+
const seenPaths = new Set();
|
|
414
|
+
const sortedFiles = [];
|
|
415
|
+
for (const file of allFiles) {
|
|
416
|
+
if (file.path) {
|
|
417
|
+
if (seenPaths.has(file.path)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
seenPaths.add(file.path);
|
|
421
|
+
}
|
|
422
|
+
sortedFiles.push(file);
|
|
423
|
+
if (sortedFiles.length >= TERM_SEARCH_CONTENT_CANDIDATE_LIMIT) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
410
427
|
if (sortedFiles.length === 0) {
|
|
411
428
|
return [];
|
|
412
429
|
}
|
|
@@ -415,14 +432,20 @@ export class ProjectAnalyzer {
|
|
|
415
432
|
const contentByPath = new Map();
|
|
416
433
|
if (candidatePaths.length > 0) {
|
|
417
434
|
const pathList = candidatePaths.map((candidatePath) => `'${escapeSqlString(candidatePath)}'`).join(', ');
|
|
435
|
+
// No row limit here: the result set is already bounded by the IN list of
|
|
436
|
+
// distinct candidate paths. A limit could truncate rows when duplicate
|
|
437
|
+
// rows exist for a path, silently dropping content for matched files.
|
|
418
438
|
const contentResults = await table
|
|
419
439
|
.query()
|
|
420
440
|
.select(['path', 'content'])
|
|
421
441
|
.where(`${projectPathFilter} AND path IN (${pathList})`)
|
|
422
|
-
.limit(TERM_SEARCH_CONTENT_CANDIDATE_LIMIT)
|
|
423
442
|
.toArray();
|
|
424
443
|
|
|
425
444
|
for (const result of contentResults) {
|
|
445
|
+
// Prefer non-empty content when duplicate rows exist for a path.
|
|
446
|
+
if (contentByPath.get(result.path)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
426
449
|
contentByPath.set(result.path, result.content || '');
|
|
427
450
|
}
|
|
428
451
|
}
|
package/src/rag-analyzer.js
CHANGED
|
@@ -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
|
|
@@ -471,8 +472,10 @@ async function runAnalysis(filePath, options = {}) {
|
|
|
471
472
|
let fullFileContent;
|
|
472
473
|
if (options.diffOnly && options.diffContent) {
|
|
473
474
|
content = options.diffContent;
|
|
474
|
-
// For PR reviews, always read the full file content for context awareness
|
|
475
|
-
|
|
475
|
+
// For PR reviews, always read the full file content for context awareness.
|
|
476
|
+
// Tolerate a missing/unreadable file (e.g. deleted in the window after the
|
|
477
|
+
// existence check) by degrading to null; downstream falls back to file.content.
|
|
478
|
+
fullFileContent = await readTextFile(filePath).catch(() => null);
|
|
476
479
|
verboseLog(options, chalk.blue(`Analyzing diff only for ${path.basename(filePath)}`));
|
|
477
480
|
}
|
|
478
481
|
else {
|
|
@@ -873,6 +876,22 @@ ${projectArchitectureSection}`);
|
|
|
873
876
|
return blocks;
|
|
874
877
|
}
|
|
875
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Build the user-prompt sentence that points the model at the cached system blocks.
|
|
881
|
+
* Returns an empty string when no cached blocks were emitted, so the prompt never
|
|
882
|
+
* instructs the model to use context that was not sent.
|
|
883
|
+
*
|
|
884
|
+
* @param {string[]} cachedSystemBlocks - Cached blocks produced for this request
|
|
885
|
+
* @param {string} subject - What is being evaluated (e.g. 'this file')
|
|
886
|
+
* @returns {string} Instruction sentence, or '' when there is no cached context
|
|
887
|
+
*/
|
|
888
|
+
function buildCachedContextInstruction(cachedSystemBlocks, subject) {
|
|
889
|
+
if (!cachedSystemBlocks || cachedSystemBlocks.length === 0) {
|
|
890
|
+
return '';
|
|
891
|
+
}
|
|
892
|
+
return `Use the cached project context, project architecture, and custom instructions when evaluating ${subject}.`;
|
|
893
|
+
}
|
|
894
|
+
|
|
876
895
|
// LLM call function - sends prompt with cached system content for cost optimization
|
|
877
896
|
async function sendPromptToLLM(promptConfig, llmOptions) {
|
|
878
897
|
try {
|
|
@@ -1150,7 +1169,7 @@ ${reviewInstructions}
|
|
|
1150
1169
|
|
|
1151
1170
|
${fileSection}
|
|
1152
1171
|
|
|
1153
|
-
|
|
1172
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this file')}
|
|
1154
1173
|
|
|
1155
1174
|
CONTEXT A: EXPLICIT GUIDELINES FROM DOCUMENTATION
|
|
1156
1175
|
${formattedGuidelines}
|
|
@@ -1270,7 +1289,7 @@ ${reviewInstructions}
|
|
|
1270
1289
|
${fileSection}
|
|
1271
1290
|
|
|
1272
1291
|
## ANALYSIS CONTEXT
|
|
1273
|
-
|
|
1292
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this test file')}
|
|
1274
1293
|
|
|
1275
1294
|
CONTEXT A: TESTING GUIDELINES AND BEST PRACTICES
|
|
1276
1295
|
${formattedGuidelines}
|
|
@@ -1358,23 +1377,24 @@ ${g.content}
|
|
|
1358
1377
|
|
|
1359
1378
|
// Format PR files with their diffs
|
|
1360
1379
|
const prFiles = file.prFiles || [];
|
|
1380
|
+
const holisticContextPlans = mergeHolisticFileContextPlans(prFiles, context.options || {});
|
|
1361
1381
|
const formattedPRFiles = prFiles
|
|
1362
1382
|
.map((prFile, idx) => {
|
|
1383
|
+
const fileContextPlan = holisticContextPlans[idx];
|
|
1384
|
+
const fileContext = formatPlannedHolisticFileContext(prFile, fileContextPlan, context.options || {});
|
|
1363
1385
|
return `
|
|
1364
1386
|
## FILE ${idx + 1}: ${prFile.path}
|
|
1365
1387
|
**Language**: ${prFile.language}
|
|
1366
1388
|
**Type**: ${prFile.isTest ? 'Test' : 'Source'} file
|
|
1367
1389
|
**Summary**: ${prFile.summary}
|
|
1390
|
+
**Context Mode**: ${fileContextPlan.mode === 'full' ? 'Full file content' : 'Focused changed-line context'}
|
|
1368
1391
|
|
|
1369
1392
|
### Changes (Git Diff):
|
|
1370
1393
|
\`\`\`diff
|
|
1371
1394
|
${prFile.diff}
|
|
1372
1395
|
\`\`\`
|
|
1373
1396
|
|
|
1374
|
-
|
|
1375
|
-
\`\`\`${prFile.language}
|
|
1376
|
-
${addLineNumbers(prFile.fullContent)}
|
|
1377
|
-
\`\`\`
|
|
1397
|
+
${fileContext}
|
|
1378
1398
|
`;
|
|
1379
1399
|
})
|
|
1380
1400
|
.join('\n');
|
|
@@ -1388,17 +1408,17 @@ ${addLineNumbers(prFile.fullContent)}
|
|
|
1388
1408
|
**CRITICAL CONTEXT AWARENESS INSTRUCTIONS:**
|
|
1389
1409
|
|
|
1390
1410
|
For each file in this PR, you have access to:
|
|
1391
|
-
1. **
|
|
1411
|
+
1. **FILE CONTEXT** - Complete file content when it fits the holistic context budget, or focused changed-line windows for very large files/PRs
|
|
1392
1412
|
2. **GIT DIFF** - Only the changes to review
|
|
1393
1413
|
|
|
1394
1414
|
**Review Rules:**
|
|
1395
1415
|
- ONLY critique the CHANGED lines shown in each file's diff (lines with + or -)
|
|
1396
|
-
- USE the
|
|
1416
|
+
- USE the file context to understand dependencies, nearby behavior, and existing implementations
|
|
1397
1417
|
- DO NOT suggest adding code that already exists in the unchanged portions of any file
|
|
1398
|
-
- DO NOT flag issues about missing code
|
|
1399
|
-
- Before flagging cross-file issues, verify the code doesn't already exist in
|
|
1400
|
-
- Do NOT flag functions/variables as missing if they exist elsewhere in the
|
|
1401
|
-
- The unchanged
|
|
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`;
|
|
1402
1422
|
|
|
1403
1423
|
let roleDefinition = buildRoleDefinition(baseRole, customDocs, 'pr');
|
|
1404
1424
|
roleDefinition += '\nAnalyze ALL files together to identify cross-file issues, consistency problems, and overall code quality.';
|
|
@@ -1427,7 +1447,7 @@ For each file in this PR, you have access to:
|
|
|
1427
1447
|
- **Test Files**: ${prFiles.filter((f) => f.isTest).length}
|
|
1428
1448
|
|
|
1429
1449
|
## UNIFIED CONTEXT FROM PROJECT
|
|
1430
|
-
|
|
1450
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this pull request')}
|
|
1431
1451
|
|
|
1432
1452
|
### PROJECT CODE EXAMPLES
|
|
1433
1453
|
${formattedCodeExamples}
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -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) => {
|
package/src/rag-review.test.js
CHANGED
|
@@ -270,7 +270,8 @@ describe('rag-review', () => {
|
|
|
270
270
|
});
|
|
271
271
|
|
|
272
272
|
// Setup: shouldChunkPR returns true to trigger chunked processing
|
|
273
|
-
|
|
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).
|
|
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
|
});
|