codecritique 2.0.8 → 2.1.1
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/README.md +4 -0
- package/package.json +1 -1
- package/src/codecritique.test.js +169 -0
- package/src/index.js +15 -7
- package/src/llm.js +3 -1
- package/src/rag-analyzer.js +15 -13
- package/src/rag-analyzer.test.js +64 -0
- package/src/rag-review.js +10 -3
- package/src/rag-review.test.js +3 -2
package/README.md
CHANGED
|
@@ -202,6 +202,8 @@ For easier integration with non-JavaScript projects, you can use the provided sh
|
|
|
202
202
|
- Validates Node.js 24+ requirement
|
|
203
203
|
- Provides helpful error messages for missing dependencies
|
|
204
204
|
|
|
205
|
+
The wrapper forwards only `ANTHROPIC_API_KEY`, `ANTHROPIC_BASE_URL`, `ANTHROPIC_LOG`, `GITHUB_TOKEN`, `GH_TOKEN`, `DEBUG`, `VERBOSE`, `CI`, and `GITHUB_WORKSPACE_PATH` from `.env`; other entries, including process-control variables such as `NODE_OPTIONS` and `PATH`, are ignored. An unreadable `.env` stops the wrapper with an error. Run it only in trusted project directories: `ANTHROPIC_BASE_URL` controls where API requests are sent, while `CI` and `GITHUB_WORKSPACE_PATH` control where embedding data is stored.
|
|
206
|
+
|
|
205
207
|
## Quick Start
|
|
206
208
|
|
|
207
209
|
Follow this three-step workflow for optimal code review results:
|
|
@@ -541,6 +543,8 @@ DEBUG=true
|
|
|
541
543
|
VERBOSE=true
|
|
542
544
|
```
|
|
543
545
|
|
|
546
|
+
Set `CODECRITIQUE_SKIP_DOTENV=1` to disable `.env` loading, as the shell wrapper does after forwarding its supported variables.
|
|
547
|
+
|
|
544
548
|
## Output Formats
|
|
545
549
|
|
|
546
550
|
CodeCritique supports three output formats:
|
package/package.json
CHANGED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { access, chmod, mkdir, mkdtemp, rm, symlink, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { describe, expect, it, onTestFinished } from 'vitest';
|
|
7
|
+
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
|
|
10
|
+
async function runWrapper(envFile, args = [], command = 'codecritique', extraEnv = {}, envIsDirectory = false, nodeMajorVersion = null) {
|
|
11
|
+
const directory = await mkdtemp(join(tmpdir(), 'codecritique-shell-'));
|
|
12
|
+
onTestFinished(() => rm(directory, { recursive: true, force: true }));
|
|
13
|
+
const fakeCommand = join(directory, command);
|
|
14
|
+
const envInspector = join(directory, 'inspect-entry-env.mjs');
|
|
15
|
+
|
|
16
|
+
if (envIsDirectory) {
|
|
17
|
+
await mkdir(join(directory, '.env'));
|
|
18
|
+
}
|
|
19
|
+
else if (envFile !== null) {
|
|
20
|
+
await writeFile(join(directory, '.env'), envFile);
|
|
21
|
+
}
|
|
22
|
+
await writeFile(
|
|
23
|
+
envInspector,
|
|
24
|
+
`process.argv = ['node'];
|
|
25
|
+
process.exit = code => { throw new Error(\`exit \${code}\`); };
|
|
26
|
+
try { await import(${JSON.stringify(new URL('./index.js', import.meta.url).href)}); } catch {}
|
|
27
|
+
console.log(\`inspected_node_options=\${process.env.NODE_OPTIONS || ''}\`);
|
|
28
|
+
`
|
|
29
|
+
);
|
|
30
|
+
await writeFile(
|
|
31
|
+
fakeCommand,
|
|
32
|
+
`#!/bin/bash
|
|
33
|
+
[ "$1" = "--terminate" ] && kill -TERM $$
|
|
34
|
+
[ "$1" = "--inspect-entry" ] && exec node "${envInspector}"
|
|
35
|
+
[ "$1" = "--inspect-entry-with-dotenv" ] && unset CODECRITIQUE_SKIP_DOTENV && exec node "${envInspector}"
|
|
36
|
+
/usr/bin/env
|
|
37
|
+
printf "arg=%s\\n" "$@"
|
|
38
|
+
`
|
|
39
|
+
);
|
|
40
|
+
await chmod(fakeCommand, 0o755);
|
|
41
|
+
if (nodeMajorVersion === null) {
|
|
42
|
+
await symlink(process.execPath, join(directory, 'node'));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await writeFile(
|
|
46
|
+
join(directory, 'node'),
|
|
47
|
+
`#!/bin/bash\nif [ "$1" = "-p" ]; then echo ${nodeMajorVersion}; exit; fi\nif [ "$1" = "--version" ]; then echo v${nodeMajorVersion}.0.0; exit; fi\nexec "${process.execPath}" "$@"\n`
|
|
48
|
+
);
|
|
49
|
+
await chmod(join(directory, 'node'), 0o755);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const result = await execFileAsync('/bin/bash', [join(import.meta.dirname, 'codecritique.sh'), ...args], {
|
|
53
|
+
cwd: directory,
|
|
54
|
+
env: { PATH: directory, ...extraEnv },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return { ...result, directory };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('codecritique.sh', () => {
|
|
61
|
+
it.each([
|
|
62
|
+
[20, 'Error: CodeCritique requires Node.js 24 or newer (found v20.0.0).\n'],
|
|
63
|
+
['not-a-version', 'Error: Unable to determine the Node.js major version (received: not-a-version).\n'],
|
|
64
|
+
])('rejects unsupported Node version output: %s', async (nodeVersion, stderr) => {
|
|
65
|
+
await expect(runWrapper(null, [], 'codecritique', {}, false, nodeVersion)).rejects.toMatchObject({
|
|
66
|
+
code: 1,
|
|
67
|
+
stderr,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it.each([
|
|
72
|
+
['unquoted spaces and globs', 'ANTHROPIC_API_KEY=value with spaces *\n', 'value with spaces *'],
|
|
73
|
+
['literal JSON quotes', 'ANTHROPIC_API_KEY={"token":"a*b c"}\n', '{"token":"a*b c"}'],
|
|
74
|
+
])('loads %s without corruption', async (_label, envFile, expected) => {
|
|
75
|
+
const { stdout } = await runWrapper(envFile, ['--flag', 'value with spaces']);
|
|
76
|
+
|
|
77
|
+
expect(stdout).toContain(`ANTHROPIC_API_KEY=${expected}`);
|
|
78
|
+
expect(stdout).toContain('arg=--flag');
|
|
79
|
+
expect(stdout).toContain('arg=value with spaces');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not execute .env contents', async () => {
|
|
83
|
+
const { directory, stdout } = await runWrapper(
|
|
84
|
+
'ANTHROPIC_API_KEY=test\nMALICIOUS=$(touch executed)\nNODE_OPTIONS=--require=./does-not-exist.cjs\n'
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(stdout).toContain('ANTHROPIC_API_KEY=test');
|
|
88
|
+
expect(stdout).not.toContain('NODE_OPTIONS=');
|
|
89
|
+
await expect(access(join(directory, 'executed'))).rejects.toThrow();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('prevents the real CLI entry point from reloading filtered .env values', async () => {
|
|
93
|
+
const { stdout } = await runWrapper('ANTHROPIC_API_KEY=test\nNODE_OPTIONS=--require=./does-not-exist.cjs\n', ['--inspect-entry']);
|
|
94
|
+
|
|
95
|
+
expect(stdout).toContain('inspected_node_options=\n');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('loads .env at the real CLI entry point when the wrapper sentinel is absent', async () => {
|
|
99
|
+
const { stdout } = await runWrapper('ANTHROPIC_API_KEY=test\nNODE_OPTIONS=loaded-by-dotenv\n', ['--inspect-entry-with-dotenv']);
|
|
100
|
+
|
|
101
|
+
expect(stdout).toContain('inspected_node_options=loaded-by-dotenv\n');
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('forwards every supported .env setting', async () => {
|
|
105
|
+
const supportedEnv = {
|
|
106
|
+
ANTHROPIC_API_KEY: 'test',
|
|
107
|
+
ANTHROPIC_BASE_URL: 'https://api.example.test',
|
|
108
|
+
ANTHROPIC_LOG: 'debug',
|
|
109
|
+
GITHUB_TOKEN: 'github-token',
|
|
110
|
+
GH_TOKEN: 'gh-token',
|
|
111
|
+
DEBUG: '1',
|
|
112
|
+
VERBOSE: 'true',
|
|
113
|
+
CI: 'true',
|
|
114
|
+
GITHUB_WORKSPACE_PATH: '/workspace',
|
|
115
|
+
};
|
|
116
|
+
const envFile = Object.entries(supportedEnv)
|
|
117
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
118
|
+
.join('\n');
|
|
119
|
+
const { stdout } = await runWrapper(envFile);
|
|
120
|
+
|
|
121
|
+
for (const [key, value] of Object.entries(supportedEnv)) {
|
|
122
|
+
expect(stdout).toContain(`${key}=${value}`);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('reports null bytes in supported values without an internal stack', async () => {
|
|
127
|
+
await expect(runWrapper('ANTHROPIC_API_KEY=test\0value\n')).rejects.toMatchObject({
|
|
128
|
+
stderr: 'Unable to load .env: ANTHROPIC_API_KEY contains a null byte\n',
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('preserves an inherited API key when .env is absent', async () => {
|
|
133
|
+
const { stdout } = await runWrapper(null, [], 'codecritique', { ANTHROPIC_API_KEY: 'inherited' });
|
|
134
|
+
|
|
135
|
+
expect(stdout).toContain('ANTHROPIC_API_KEY=inherited');
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('keeps the original .env-over-inherited-value precedence', async () => {
|
|
139
|
+
const { stdout } = await runWrapper('ANTHROPIC_API_KEY=from-file\n', [], 'codecritique', {
|
|
140
|
+
ANTHROPIC_API_KEY: 'from-parent',
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
expect(stdout).toContain('ANTHROPIC_API_KEY=from-file');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it.each([null, 'ANTHROPIC_AUTH_TOKEN=token\n'])('warns when no supported Anthropic credentials are configured', async (envFile) => {
|
|
147
|
+
const { stderr } = await runWrapper(envFile);
|
|
148
|
+
|
|
149
|
+
expect(stderr).toContain('Warning: ANTHROPIC_API_KEY is not set.');
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('reports unreadable .env paths without an internal stack', async () => {
|
|
153
|
+
await expect(runWrapper(null, [], 'codecritique', {}, true)).rejects.toMatchObject({
|
|
154
|
+
stderr: expect.stringMatching(/^Unable to load \.env: .+\n$/),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('falls back to npx when codecritique is not installed globally', async () => {
|
|
159
|
+
const { stdout } = await runWrapper('ANTHROPIC_API_KEY=test\n', ['--version'], 'npx');
|
|
160
|
+
|
|
161
|
+
expect(stdout).toContain('codecritique not found globally, trying with npx...');
|
|
162
|
+
expect(stdout).toContain('arg=codecritique');
|
|
163
|
+
expect(stdout).toContain('arg=--version');
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('preserves signal-derived child exit codes', async () => {
|
|
167
|
+
await expect(runWrapper('ANTHROPIC_API_KEY=test\n', ['--terminate'])).rejects.toMatchObject({ code: 143 });
|
|
168
|
+
});
|
|
169
|
+
});
|
package/src/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import chalk from 'chalk';
|
|
|
15
15
|
import { Spinner } from 'cli-spinner';
|
|
16
16
|
import { program } from 'commander';
|
|
17
17
|
import { glob } from 'glob';
|
|
18
|
-
import { getDefaultEmbeddingsSystem } from './embeddings/factory.js';
|
|
18
|
+
import { getDefaultEmbeddingsSystem, getDefaultEmbeddingsSystemIfInitialized } from './embeddings/factory.js';
|
|
19
19
|
import { PRHistoryAnalyzer } from './pr-history/analyzer.js';
|
|
20
20
|
import {
|
|
21
21
|
displayAnalysisResults,
|
|
@@ -34,8 +34,12 @@ import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './u
|
|
|
34
34
|
import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
|
|
35
35
|
import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
const embeddingsSystem =
|
|
37
|
+
async function cleanupEmbeddingsSystemIfInitialized() {
|
|
38
|
+
const embeddingsSystem = getDefaultEmbeddingsSystemIfInitialized();
|
|
39
|
+
if (embeddingsSystem) {
|
|
40
|
+
await embeddingsSystem.cleanup();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
const packageJson = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
41
45
|
|
|
@@ -103,6 +107,7 @@ program
|
|
|
103
107
|
.command('embeddings:clear-all')
|
|
104
108
|
.description('Clear ALL stored embeddings (affects all projects - use with caution)')
|
|
105
109
|
.action(async () => {
|
|
110
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
106
111
|
try {
|
|
107
112
|
console.log(chalk.red('WARNING: This will clear embeddings for ALL projects on this machine!'));
|
|
108
113
|
console.log(chalk.cyan('Clearing all embeddings...'));
|
|
@@ -227,7 +232,7 @@ process.on('SIGINT', async () => {
|
|
|
227
232
|
|
|
228
233
|
try {
|
|
229
234
|
diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
|
|
230
|
-
await
|
|
235
|
+
await cleanupEmbeddingsSystemIfInitialized();
|
|
231
236
|
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
232
237
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
233
238
|
diagnosticLog(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
|
|
@@ -251,7 +256,7 @@ process.on('SIGTERM', async () => {
|
|
|
251
256
|
|
|
252
257
|
try {
|
|
253
258
|
diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
|
|
254
|
-
await
|
|
259
|
+
await cleanupEmbeddingsSystemIfInitialized();
|
|
255
260
|
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
256
261
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
257
262
|
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
|
|
@@ -468,7 +473,7 @@ async function runCodeReview(options) {
|
|
|
468
473
|
// Clean up resources
|
|
469
474
|
diagnosticLog(chalk.cyan('Cleaning up resources...'));
|
|
470
475
|
try {
|
|
471
|
-
await
|
|
476
|
+
await cleanupEmbeddingsSystemIfInitialized();
|
|
472
477
|
await cleanupClassifier();
|
|
473
478
|
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
474
479
|
}
|
|
@@ -497,7 +502,7 @@ async function runCodeReview(options) {
|
|
|
497
502
|
|
|
498
503
|
// Clean up resources even on error
|
|
499
504
|
try {
|
|
500
|
-
await
|
|
505
|
+
await cleanupEmbeddingsSystemIfInitialized();
|
|
501
506
|
await cleanupClassifier();
|
|
502
507
|
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
503
508
|
}
|
|
@@ -516,6 +521,7 @@ async function runCodeReview(options) {
|
|
|
516
521
|
* @param {Object} options - Command options
|
|
517
522
|
*/
|
|
518
523
|
async function generateEmbeddings(options) {
|
|
524
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
519
525
|
try {
|
|
520
526
|
console.log(chalk.bold.blue('AI Code Review - Generating embeddings...'));
|
|
521
527
|
const startTime = Date.now();
|
|
@@ -717,6 +723,7 @@ async function generateEmbeddings(options) {
|
|
|
717
723
|
* Clear stored embeddings for the current project
|
|
718
724
|
*/
|
|
719
725
|
async function clearEmbeddings(options) {
|
|
726
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
720
727
|
try {
|
|
721
728
|
// Determine the working directory for project separation
|
|
722
729
|
// If --directory is specified, use that as the project directory
|
|
@@ -751,6 +758,7 @@ async function clearEmbeddings(options) {
|
|
|
751
758
|
* Show statistics about stored embeddings
|
|
752
759
|
*/
|
|
753
760
|
async function showEmbeddingStats(options) {
|
|
761
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
754
762
|
try {
|
|
755
763
|
// Determine the working directory for project separation
|
|
756
764
|
// If --directory is specified, use that as the project directory
|
package/src/llm.js
CHANGED
|
@@ -18,7 +18,9 @@ import dotenv from 'dotenv';
|
|
|
18
18
|
import { verboseLog } from './utils/logging.js';
|
|
19
19
|
|
|
20
20
|
// Load env variables if present; do not enforce key at import time
|
|
21
|
-
|
|
21
|
+
if (process.env.CODECRITIQUE_SKIP_DOTENV !== '1') {
|
|
22
|
+
dotenv.config({ quiet: true });
|
|
23
|
+
}
|
|
22
24
|
|
|
23
25
|
let anthropic = null;
|
|
24
26
|
|
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
|
|
@@ -36,9 +37,6 @@ const DEFAULT_TRUNCATE_LINES = 300;
|
|
|
36
37
|
const GUIDELINE_TRUNCATE_LINES = 400;
|
|
37
38
|
const MAX_PR_COMMENTS_FOR_CONTEXT = 15;
|
|
38
39
|
|
|
39
|
-
// Create embeddings system instance
|
|
40
|
-
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
41
|
-
|
|
42
40
|
// Track if semantic similarity has been initialized
|
|
43
41
|
let semanticSimilarityInitialized = false;
|
|
44
42
|
|
|
@@ -233,6 +231,7 @@ ${ex.content}
|
|
|
233
231
|
*/
|
|
234
232
|
async function getProjectSummary(projectPath, options = {}) {
|
|
235
233
|
const resolvedPath = path.resolve(projectPath);
|
|
234
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
236
235
|
|
|
237
236
|
try {
|
|
238
237
|
// Retrieve from database
|
|
@@ -1376,23 +1375,24 @@ ${g.content}
|
|
|
1376
1375
|
|
|
1377
1376
|
// Format PR files with their diffs
|
|
1378
1377
|
const prFiles = file.prFiles || [];
|
|
1378
|
+
const holisticContextPlans = mergeHolisticFileContextPlans(prFiles, context.options || {});
|
|
1379
1379
|
const formattedPRFiles = prFiles
|
|
1380
1380
|
.map((prFile, idx) => {
|
|
1381
|
+
const fileContextPlan = holisticContextPlans[idx];
|
|
1382
|
+
const fileContext = formatPlannedHolisticFileContext(prFile, fileContextPlan, context.options || {});
|
|
1381
1383
|
return `
|
|
1382
1384
|
## FILE ${idx + 1}: ${prFile.path}
|
|
1383
1385
|
**Language**: ${prFile.language}
|
|
1384
1386
|
**Type**: ${prFile.isTest ? 'Test' : 'Source'} file
|
|
1385
1387
|
**Summary**: ${prFile.summary}
|
|
1388
|
+
**Context Mode**: ${fileContextPlan.mode === 'full' ? 'Full file content' : 'Focused changed-line context'}
|
|
1386
1389
|
|
|
1387
1390
|
### Changes (Git Diff):
|
|
1388
1391
|
\`\`\`diff
|
|
1389
1392
|
${prFile.diff}
|
|
1390
1393
|
\`\`\`
|
|
1391
1394
|
|
|
1392
|
-
|
|
1393
|
-
\`\`\`${prFile.language}
|
|
1394
|
-
${addLineNumbers(prFile.fullContent)}
|
|
1395
|
-
\`\`\`
|
|
1395
|
+
${fileContext}
|
|
1396
1396
|
`;
|
|
1397
1397
|
})
|
|
1398
1398
|
.join('\n');
|
|
@@ -1406,17 +1406,17 @@ ${addLineNumbers(prFile.fullContent)}
|
|
|
1406
1406
|
**CRITICAL CONTEXT AWARENESS INSTRUCTIONS:**
|
|
1407
1407
|
|
|
1408
1408
|
For each file in this PR, you have access to:
|
|
1409
|
-
1. **
|
|
1409
|
+
1. **FILE CONTEXT** - Complete file content when it fits the holistic context budget, or focused changed-line windows for very large files/PRs
|
|
1410
1410
|
2. **GIT DIFF** - Only the changes to review
|
|
1411
1411
|
|
|
1412
1412
|
**Review Rules:**
|
|
1413
1413
|
- ONLY critique the CHANGED lines shown in each file's diff (lines with + or -)
|
|
1414
|
-
- USE the
|
|
1414
|
+
- USE the file context to understand dependencies, nearby behavior, and existing implementations
|
|
1415
1415
|
- DO NOT suggest adding code that already exists in the unchanged portions of any file
|
|
1416
|
-
- DO NOT flag issues about missing code
|
|
1417
|
-
- Before flagging cross-file issues, verify the code doesn't already exist in
|
|
1418
|
-
- Do NOT flag functions/variables as missing if they exist elsewhere in the
|
|
1419
|
-
- The unchanged
|
|
1416
|
+
- DO NOT flag issues about missing code when the provided context already shows the code exists
|
|
1417
|
+
- Before flagging cross-file issues, verify the code doesn't already exist in the provided context
|
|
1418
|
+
- Do NOT flag functions/variables as missing if they exist elsewhere in the provided context
|
|
1419
|
+
- The unchanged context is part of each file - always check it before making assumptions`;
|
|
1420
1420
|
|
|
1421
1421
|
let roleDefinition = buildRoleDefinition(baseRole, customDocs, 'pr');
|
|
1422
1422
|
roleDefinition += '\nAnalyze ALL files together to identify cross-file issues, consistency problems, and overall code quality.';
|
|
@@ -1927,6 +1927,7 @@ async function performHolisticPRAnalysis(options) {
|
|
|
1927
1927
|
* @returns {Promise<Object>} An object containing the gathered context.
|
|
1928
1928
|
*/
|
|
1929
1929
|
async function getContextForFile(filePath, content, options = {}) {
|
|
1930
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
1930
1931
|
const RELEVANT_CHUNK_THRESHOLD = 0.1;
|
|
1931
1932
|
const W_H1_SIM = 0.2;
|
|
1932
1933
|
const W_DOC_CONTEXT_MATCH = 0.6;
|
|
@@ -2281,6 +2282,7 @@ async function getContextForFile(filePath, content, options = {}) {
|
|
|
2281
2282
|
}
|
|
2282
2283
|
|
|
2283
2284
|
async function gatherUnifiedContextForPR(prFiles, options = {}) {
|
|
2285
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
2284
2286
|
const allProcessedContext = {
|
|
2285
2287
|
codeExamples: new Map(),
|
|
2286
2288
|
guidelines: new Map(),
|
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
|
});
|