codecritique 2.0.4 → 2.0.6
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 +13 -6
- package/package.json +1 -1
- package/src/index.js +168 -73
- package/src/llm.js +1 -1
- package/src/rag-review.js +22 -4
- package/src/rag-review.test.js +35 -2
package/README.md
CHANGED
|
@@ -263,8 +263,13 @@ npx codecritique analyze --file src/components/Button.tsx
|
|
|
263
263
|
# Analyze files matching patterns
|
|
264
264
|
npx codecritique analyze --files "src/**/*.ts" "lib/*.js"
|
|
265
265
|
|
|
266
|
-
#
|
|
266
|
+
# Review changes in a target branch against its auto-detected parent branch
|
|
267
267
|
npx codecritique analyze --diff-with feature-branch
|
|
268
|
+
|
|
269
|
+
# Review against an explicit base branch (overrides auto-detection).
|
|
270
|
+
# Useful for stacked PRs where you know the real base, or in CI where the PR
|
|
271
|
+
# base ref is authoritative.
|
|
272
|
+
npx codecritique analyze --diff-with feature-B --base feature-A
|
|
268
273
|
```
|
|
269
274
|
|
|
270
275
|
#### Using with Custom Guidelines
|
|
@@ -383,9 +388,6 @@ jobs:
|
|
|
383
388
|
- name: ⬇️ Checkout repo
|
|
384
389
|
uses: actions/checkout@v4
|
|
385
390
|
|
|
386
|
-
- name: Setup master branch for diff analysis
|
|
387
|
-
run: git fetch --no-tags --prune origin main:main
|
|
388
|
-
|
|
389
391
|
- name: Code Review
|
|
390
392
|
uses: cosmocoder/CodeCritique/.github/actions/pr-review@main
|
|
391
393
|
with:
|
|
@@ -393,11 +395,16 @@ jobs:
|
|
|
393
395
|
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
394
396
|
```
|
|
395
397
|
|
|
398
|
+
> No manual `git fetch` or `fetch-depth: 0` is needed. The action fetches the PR base
|
|
399
|
+
> branch (the parent — which for a stacked PR is another feature branch) and deepens
|
|
400
|
+
> the shallow checkout just enough to compute the diff, so parent auto-detection works
|
|
401
|
+
> without the cost of a full-history clone.
|
|
402
|
+
|
|
396
403
|
#### Required Setup
|
|
397
404
|
|
|
398
405
|
1. **Anthropic API Key**: Store your Anthropic API key as a repository secret named `ANTHROPIC_API_KEY`
|
|
399
406
|
2. **Permissions**: The workflow must have `contents: write`, `actions: read`, and `pull-requests: write` permissions
|
|
400
|
-
3. **Git Setup**:
|
|
407
|
+
3. **Git Setup**: None required — the action fetches the PR base/parent branch and deepens the checkout only as much as the diff needs (no `fetch-depth: 0`)
|
|
401
408
|
|
|
402
409
|
#### Input Parameters
|
|
403
410
|
|
|
@@ -450,7 +457,7 @@ codecritique analyze --file src/components/Button.tsx
|
|
|
450
457
|
# Analyze files matching patterns
|
|
451
458
|
codecritique analyze --files "src/**/*.ts" "lib/*.js"
|
|
452
459
|
|
|
453
|
-
#
|
|
460
|
+
# Review target branch changes against its auto-detected parent branch
|
|
454
461
|
codecritique analyze --diff-with feature-branch
|
|
455
462
|
|
|
456
463
|
# Generate embeddings
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -29,9 +29,10 @@ import { cleanupClassifier, clearPRComments, getPRCommentsStats, hasPRComments }
|
|
|
29
29
|
import { ProjectAnalyzer } from './project-analyzer.js';
|
|
30
30
|
import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
|
|
31
31
|
import { execGitSafe } from './utils/command.js';
|
|
32
|
-
import { ensureBranchExists,
|
|
33
|
-
import { verboseLog } from './utils/logging.js';
|
|
32
|
+
import { ensureBranchExists, findParentBranch } from './utils/git.js';
|
|
33
|
+
import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './utils/logging.js';
|
|
34
34
|
import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
|
|
35
|
+
import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
|
|
35
36
|
|
|
36
37
|
// Create a default embeddings system instance
|
|
37
38
|
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
@@ -45,7 +46,11 @@ program.name('codecritique').description('CLI tool for AI-powered code review us
|
|
|
45
46
|
program
|
|
46
47
|
.command('analyze')
|
|
47
48
|
.description('Analyze code using dynamic context (RAG approach)')
|
|
48
|
-
.option(
|
|
49
|
+
.option(
|
|
50
|
+
'-b, --diff-with <branch>',
|
|
51
|
+
'Review the specified target branch against its auto-detected parent branch (triggers PR review mode)'
|
|
52
|
+
)
|
|
53
|
+
.option('--base <branch>', 'Base branch to diff the target against, overriding auto-detection (e.g. the authoritative PR base ref in CI)')
|
|
49
54
|
.option('-f, --files <files...>', 'Specific files or glob patterns to review')
|
|
50
55
|
.option('--file <file>', 'Analyze a single file')
|
|
51
56
|
.option('-d, --directory <dir>', 'Working directory for git operations (use with --diff-with)')
|
|
@@ -162,12 +167,12 @@ Examples:
|
|
|
162
167
|
$ codecritique analyze --directory src/components
|
|
163
168
|
$ codecritique analyze --file src/utils/validation.ts
|
|
164
169
|
$ codecritique analyze --files "src/**/*.tsx" "lib/*.js"
|
|
165
|
-
$ codecritique analyze -b
|
|
170
|
+
$ codecritique analyze -b feature-branch
|
|
166
171
|
$ codecritique analyze --doc "Our Eng Guidelines:./ENGINEERING_GUIDELINES.md" --file src/utils/validation.ts
|
|
167
172
|
$ codecritique analyze --diff-with feature-branch -d /path/to/repo
|
|
168
173
|
$ codecritique analyze --output json > review-results.json
|
|
169
174
|
$ codecritique analyze --track-feedback --feedback-path ./feedback-artifacts --file src/utils/validation.ts
|
|
170
|
-
$ codecritique analyze --track-feedback --feedback-threshold 0.8 -b
|
|
175
|
+
$ codecritique analyze --track-feedback --feedback-threshold 0.8 -b feature-branch
|
|
171
176
|
$ codecritique embeddings:generate --directory src
|
|
172
177
|
$ codecritique embeddings:generate --exclude "**/*.test.js" "**/*.spec.js"
|
|
173
178
|
$ codecritique embeddings:generate --exclude-file .embedignore
|
|
@@ -213,7 +218,7 @@ else {
|
|
|
213
218
|
|
|
214
219
|
// Register process event handlers for cleanup (embeddings cleanup primarily)
|
|
215
220
|
process.on('SIGINT', async () => {
|
|
216
|
-
|
|
221
|
+
diagnosticLog(chalk.yellow('\nReceived SIGINT. Attempting graceful shutdown...'));
|
|
217
222
|
// Set a timeout to force exit if cleanup hangs
|
|
218
223
|
const forceExitTimeout = setTimeout(() => {
|
|
219
224
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -221,23 +226,23 @@ process.on('SIGINT', async () => {
|
|
|
221
226
|
}, 10000); // 10 seconds timeout
|
|
222
227
|
|
|
223
228
|
try {
|
|
224
|
-
|
|
229
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Attempting embeddingsSystem.cleanup()...'));
|
|
225
230
|
await embeddingsSystem.cleanup();
|
|
226
|
-
|
|
231
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
227
232
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
228
|
-
|
|
233
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting normally (code 0).'));
|
|
229
234
|
process.exit(0); // Exit normally
|
|
230
235
|
}
|
|
231
236
|
catch (err) {
|
|
232
237
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
233
238
|
clearTimeout(forceExitTimeout);
|
|
234
|
-
|
|
239
|
+
diagnosticLog(chalk.cyan('SIGINT handler: Exiting after error (code 1).'));
|
|
235
240
|
process.exit(1); // Exit with error code
|
|
236
241
|
}
|
|
237
242
|
});
|
|
238
243
|
|
|
239
244
|
process.on('SIGTERM', async () => {
|
|
240
|
-
|
|
245
|
+
diagnosticLog(chalk.yellow('\nReceived SIGTERM. Attempting graceful shutdown...'));
|
|
241
246
|
// Set a timeout to force exit if cleanup hangs
|
|
242
247
|
const forceExitTimeout = setTimeout(() => {
|
|
243
248
|
console.error(chalk.red('Cleanup timed out after 10 seconds. Forcing exit...'));
|
|
@@ -245,17 +250,17 @@ process.on('SIGTERM', async () => {
|
|
|
245
250
|
}, 10000);
|
|
246
251
|
|
|
247
252
|
try {
|
|
248
|
-
|
|
253
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Attempting embeddingsSystem.cleanup()...'));
|
|
249
254
|
await embeddingsSystem.cleanup();
|
|
250
|
-
|
|
255
|
+
diagnosticLog(chalk.green('embeddingsSystem.cleanup() completed.'));
|
|
251
256
|
clearTimeout(forceExitTimeout); // Cleanup finished, clear the timeout
|
|
252
|
-
|
|
257
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting normally (code 0).'));
|
|
253
258
|
process.exit(0); // Exit normally
|
|
254
259
|
}
|
|
255
260
|
catch (err) {
|
|
256
261
|
console.error(chalk.red('Error during embeddingsSystem.cleanup():'), err.message);
|
|
257
262
|
clearTimeout(forceExitTimeout);
|
|
258
|
-
|
|
263
|
+
diagnosticLog(chalk.cyan('SIGTERM handler: Exiting after error (code 1).'));
|
|
259
264
|
process.exit(1); // Exit with error code
|
|
260
265
|
}
|
|
261
266
|
});
|
|
@@ -263,7 +268,7 @@ process.on('SIGTERM', async () => {
|
|
|
263
268
|
// Ensure cleanup on normal exit
|
|
264
269
|
process.on('exit', () => {
|
|
265
270
|
// Note: Async cleanup might not fully complete here
|
|
266
|
-
|
|
271
|
+
diagnosticLog(chalk.cyan('Exiting...'));
|
|
267
272
|
});
|
|
268
273
|
|
|
269
274
|
// REMOVED: Old options processing logic for ignore/severity
|
|
@@ -273,6 +278,7 @@ process.on('exit', () => {
|
|
|
273
278
|
|
|
274
279
|
// Main function to run the code review (Refactored to use cag-review.js)
|
|
275
280
|
async function runCodeReview(options) {
|
|
281
|
+
configureCleanStdoutForDataOutput(options);
|
|
276
282
|
let reviewTask = null;
|
|
277
283
|
let operationDescription = '';
|
|
278
284
|
const startTime = Date.now();
|
|
@@ -281,7 +287,7 @@ async function runCodeReview(options) {
|
|
|
281
287
|
// If --directory is specified, use that as the project directory
|
|
282
288
|
// Otherwise, use the current working directory
|
|
283
289
|
const projectPath = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
284
|
-
|
|
290
|
+
diagnosticLog(chalk.gray(`Using project path for analysis: ${projectPath}`));
|
|
285
291
|
|
|
286
292
|
// Parse custom documents
|
|
287
293
|
const customDocs = [];
|
|
@@ -322,12 +328,12 @@ async function runCodeReview(options) {
|
|
|
322
328
|
.trim(); // Remove leading/trailing whitespace
|
|
323
329
|
|
|
324
330
|
customDocs.push({ title, content });
|
|
325
|
-
|
|
331
|
+
diagnosticLog(chalk.gray(`Loaded custom document '${title}' from ${filePath} (${content.length} chars)`));
|
|
326
332
|
|
|
327
333
|
// Debug: Show a clean preview of the content
|
|
328
334
|
const previewLength = 200;
|
|
329
335
|
const preview = content.substring(0, previewLength).replace(/\n/g, '\\n');
|
|
330
|
-
|
|
336
|
+
diagnosticLog(chalk.gray(` Content preview: ${preview}${content.length > previewLength ? '...' : ''}`));
|
|
331
337
|
}
|
|
332
338
|
}
|
|
333
339
|
|
|
@@ -352,24 +358,29 @@ async function runCodeReview(options) {
|
|
|
352
358
|
};
|
|
353
359
|
|
|
354
360
|
try {
|
|
355
|
-
|
|
361
|
+
diagnosticLog(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
|
|
356
362
|
|
|
357
363
|
// Determine the review mode based on options
|
|
358
|
-
// Only support: single file, specific files, or diff
|
|
364
|
+
// Only support: single file, specific files, or target-branch diff review
|
|
359
365
|
if (options.diffWith) {
|
|
366
|
+
const targetBranch = options.diffWith;
|
|
360
367
|
// Use directory option as working directory for git commands if specified
|
|
361
368
|
const gitWorkingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
362
|
-
const changedFiles = getChangedFiles(
|
|
369
|
+
const { changedFiles, baseBranch } = getChangedFiles(targetBranch, gitWorkingDir, options.base);
|
|
363
370
|
if (changedFiles.length === 0) {
|
|
364
|
-
|
|
371
|
+
const message = `No changed files found in target branch '${targetBranch}' compared to its detected parent branch. Exiting.`;
|
|
372
|
+
diagnosticLog(chalk.yellow(message));
|
|
373
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
365
374
|
return;
|
|
366
375
|
}
|
|
367
|
-
operationDescription = `${changedFiles.length} files changed
|
|
368
|
-
// Add the actual branch name to reviewOptions
|
|
376
|
+
operationDescription = `${changedFiles.length} files changed in target branch ${targetBranch}`;
|
|
377
|
+
// Add the actual branch name to reviewOptions. Pass the already-detected
|
|
378
|
+
// parent branch so the review pipeline doesn't re-run parent detection.
|
|
369
379
|
const enhancedReviewOptions = {
|
|
370
380
|
...reviewOptions,
|
|
371
|
-
actualBranch:
|
|
372
|
-
diffWith:
|
|
381
|
+
actualBranch: targetBranch,
|
|
382
|
+
diffWith: targetBranch,
|
|
383
|
+
baseBranch,
|
|
373
384
|
};
|
|
374
385
|
reviewTask = reviewPullRequest(changedFiles, enhancedReviewOptions);
|
|
375
386
|
}
|
|
@@ -383,7 +394,9 @@ async function runCodeReview(options) {
|
|
|
383
394
|
else if (options.files && options.files.length > 0) {
|
|
384
395
|
const filesToAnalyze = await expandFilePatterns(options.files);
|
|
385
396
|
if (filesToAnalyze.length === 0) {
|
|
386
|
-
|
|
397
|
+
const message = 'No files found matching the specified patterns. Exiting.';
|
|
398
|
+
diagnosticLog(chalk.yellow(message));
|
|
399
|
+
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
387
400
|
return;
|
|
388
401
|
}
|
|
389
402
|
operationDescription = `${filesToAnalyze.length} specific files/patterns`;
|
|
@@ -394,7 +407,7 @@ async function runCodeReview(options) {
|
|
|
394
407
|
console.error(chalk.red('Error: You must specify one of the following:'));
|
|
395
408
|
console.error(chalk.yellow(' --file <file> Analyze a single file'));
|
|
396
409
|
console.error(chalk.yellow(' --files <files...> Analyze specific files or glob patterns'));
|
|
397
|
-
console.error(chalk.yellow(' -b, --diff-with <branch>
|
|
410
|
+
console.error(chalk.yellow(' -b, --diff-with <branch> Review a target branch against its auto-detected parent branch'));
|
|
398
411
|
console.error(chalk.gray('\nOptional:'));
|
|
399
412
|
console.error(chalk.gray(' -d, --directory <dir> Working directory (for git operations with --diff-with)'));
|
|
400
413
|
console.error(chalk.gray('\nExamples:'));
|
|
@@ -408,7 +421,7 @@ async function runCodeReview(options) {
|
|
|
408
421
|
process.exit(1);
|
|
409
422
|
}
|
|
410
423
|
|
|
411
|
-
|
|
424
|
+
diagnosticLog(chalk.cyan(`Starting review for ${operationDescription}...`));
|
|
412
425
|
|
|
413
426
|
// Execute the selected review task
|
|
414
427
|
const reviewResult = await reviewTask;
|
|
@@ -421,60 +434,77 @@ async function runCodeReview(options) {
|
|
|
421
434
|
// Process and output results
|
|
422
435
|
if (reviewResult && reviewResult.success) {
|
|
423
436
|
if (reviewResult.results && reviewResult.results.length > 0) {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// Pass the detailed results array to the output function
|
|
428
|
-
outputFn(reviewResult.results, options, reviewResult);
|
|
429
|
-
console.log(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
437
|
+
diagnosticLog(chalk.green(`Found ${reviewResult.results.length} result items to display`));
|
|
438
|
+
await outputReviewResults(reviewResult.results, options, reviewResult);
|
|
439
|
+
diagnosticLog(chalk.bold.green(`\nAnalysis complete for ${operationDescription}! (${duration}s)`));
|
|
430
440
|
}
|
|
431
441
|
else {
|
|
432
442
|
// No results to display (e.g., all files were excluded/skipped)
|
|
433
443
|
const message = reviewResult.message || 'All files were excluded from review (e.g., config files, lock files).';
|
|
434
|
-
|
|
444
|
+
diagnosticLog(chalk.yellow(message));
|
|
435
445
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
console.log(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
446
|
+
if (await emitEmptyReviewOutputIfNeeded(options, reviewResult)) {
|
|
447
|
+
if (options.outputFile) {
|
|
448
|
+
diagnosticLog(chalk.yellow(`Empty results written to: ${options.outputFile}`));
|
|
449
|
+
}
|
|
441
450
|
}
|
|
442
451
|
|
|
443
|
-
|
|
452
|
+
diagnosticLog(chalk.bold.yellow(`\nReview complete for ${operationDescription} - no reviewable files found (${duration}s)`));
|
|
444
453
|
}
|
|
445
454
|
}
|
|
446
455
|
else {
|
|
456
|
+
const error = reviewResult?.error || 'Code review process failed.';
|
|
447
457
|
console.error(chalk.red('\nCode review process failed.'));
|
|
448
|
-
|
|
449
|
-
|
|
458
|
+
console.error(chalk.red(`Error: ${error}`));
|
|
459
|
+
|
|
460
|
+
if (await emitErrorReviewOutputIfNeeded(options, error, reviewResult || {})) {
|
|
461
|
+
if (options.outputFile) {
|
|
462
|
+
diagnosticLog(chalk.yellow(`Error results written to: ${options.outputFile}`));
|
|
463
|
+
}
|
|
450
464
|
}
|
|
465
|
+
// AI review output is advisory; report review-task failures without failing CI.
|
|
451
466
|
}
|
|
452
467
|
|
|
453
468
|
// Clean up resources
|
|
454
|
-
|
|
469
|
+
diagnosticLog(chalk.cyan('Cleaning up resources...'));
|
|
455
470
|
try {
|
|
456
471
|
await embeddingsSystem.cleanup();
|
|
457
472
|
await cleanupClassifier();
|
|
458
|
-
|
|
473
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
459
474
|
}
|
|
460
475
|
catch (cleanupErr) {
|
|
461
476
|
console.error(chalk.yellow('Error during cleanup:'), cleanupErr.message);
|
|
462
|
-
process.
|
|
477
|
+
process.exitCode = 1;
|
|
478
|
+
return;
|
|
463
479
|
}
|
|
464
480
|
}
|
|
465
481
|
catch (err) {
|
|
466
|
-
|
|
467
|
-
|
|
482
|
+
if (!isBrokenStdoutPipeError(err)) {
|
|
483
|
+
console.error(chalk.red(`\nError during code review (${operationDescription}):`), err.message);
|
|
484
|
+
if (shouldPrintErrorStack(options, err)) {
|
|
485
|
+
console.error(err.stack);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
try {
|
|
490
|
+
await emitErrorReviewOutputIfNeeded(options, err.message, { success: false, error: err.message });
|
|
491
|
+
}
|
|
492
|
+
catch (outputErr) {
|
|
493
|
+
if (!isBrokenStdoutPipeError(outputErr)) {
|
|
494
|
+
console.error(chalk.red('Error writing review error output:'), outputErr.message);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
468
498
|
// Clean up resources even on error
|
|
469
499
|
try {
|
|
470
500
|
await embeddingsSystem.cleanup();
|
|
471
501
|
await cleanupClassifier();
|
|
472
|
-
|
|
502
|
+
diagnosticLog(chalk.green('All resources cleaned up successfully'));
|
|
473
503
|
}
|
|
474
504
|
catch (cleanupErr) {
|
|
475
505
|
console.error(chalk.red('Error during cleanup:'), cleanupErr.message);
|
|
476
506
|
}
|
|
477
|
-
process.
|
|
507
|
+
process.exitCode = 1;
|
|
478
508
|
}
|
|
479
509
|
}
|
|
480
510
|
|
|
@@ -950,24 +980,36 @@ async function expandFilePatterns(patterns, baseDir = process.cwd()) {
|
|
|
950
980
|
}
|
|
951
981
|
|
|
952
982
|
/**
|
|
953
|
-
* Get list of files changed in
|
|
954
|
-
*
|
|
983
|
+
* Get list of files changed in the target branch compared to its base branch.
|
|
984
|
+
* The supplied branch is the branch being reviewed, not the base branch.
|
|
985
|
+
*
|
|
986
|
+
* When `explicitBase` is given (e.g. the authoritative PR base ref in CI) it is
|
|
987
|
+
* used directly. Otherwise the parent is auto-detected (see findParentBranch),
|
|
988
|
+
* so stacked PRs are diffed against the branch they were forked from rather than
|
|
989
|
+
* always against main/master/develop.
|
|
955
990
|
*
|
|
956
|
-
* @param {string}
|
|
991
|
+
* @param {string} targetBranch - Branch to analyze (the feature/target branch)
|
|
957
992
|
* @param {string} workingDir - Directory to run git commands in (optional, defaults to cwd)
|
|
958
|
-
* @
|
|
993
|
+
* @param {string} [explicitBase] - Base branch to diff against, overriding auto-detection
|
|
994
|
+
* @returns {{ changedFiles: Array<string>, baseBranch: string|null }} Changed file paths
|
|
995
|
+
* (absolute) and the base branch they were diffed against (null on error).
|
|
959
996
|
*/
|
|
960
|
-
function getChangedFiles(
|
|
997
|
+
function getChangedFiles(targetBranch, workingDir = process.cwd(), explicitBase = undefined) {
|
|
961
998
|
try {
|
|
962
999
|
// Get git root directory
|
|
963
1000
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
|
|
964
|
-
|
|
1001
|
+
diagnosticLog(chalk.gray(`Git repository: ${gitRoot}`));
|
|
965
1002
|
|
|
966
|
-
// Ensure the branch exists locally (fetch if needed)
|
|
967
|
-
ensureBranchExists(
|
|
1003
|
+
// Ensure the target branch exists locally (fetch if needed)
|
|
1004
|
+
ensureBranchExists(targetBranch, workingDir);
|
|
968
1005
|
|
|
969
|
-
//
|
|
970
|
-
|
|
1006
|
+
// Use the explicitly provided base (authoritative, e.g. the PR base ref in CI)
|
|
1007
|
+
// when given; otherwise detect the parent branch the target was forked from
|
|
1008
|
+
// (main/master/develop, or another feature branch in stacked-PR workflows).
|
|
1009
|
+
const baseBranch = explicitBase || findParentBranch(targetBranch, workingDir);
|
|
1010
|
+
if (explicitBase) {
|
|
1011
|
+
diagnosticLog(chalk.gray(`Using explicit base branch '${baseBranch}' (auto-detection skipped)`));
|
|
1012
|
+
}
|
|
971
1013
|
|
|
972
1014
|
// Ensure the base branch exists locally as well (crucial for diff operations)
|
|
973
1015
|
try {
|
|
@@ -978,12 +1020,13 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
978
1020
|
// Continue with the original baseBranch name, it might work with remote refs
|
|
979
1021
|
}
|
|
980
1022
|
|
|
981
|
-
|
|
1023
|
+
diagnosticLog(chalk.gray(`Reviewing target branch '${targetBranch}' against base branch '${baseBranch}'...`));
|
|
982
1024
|
|
|
983
|
-
// Use three-dot notation to get changes in branch compared to base
|
|
984
|
-
// This shows commits that are in 'branch' but not in 'baseBranch'
|
|
1025
|
+
// Use three-dot notation to get changes in the target branch compared to the base.
|
|
985
1026
|
// By adding --diff-filter=d, we exclude deleted files from the list.
|
|
986
|
-
const gitOutput = execGitSafe('git diff', ['--name-only', '--diff-filter=d', `${baseBranch}...${
|
|
1027
|
+
const gitOutput = execGitSafe('git diff', ['--name-only', '--diff-filter=d', `${baseBranch}...${targetBranch}`], {
|
|
1028
|
+
cwd: gitRoot,
|
|
1029
|
+
}).toString();
|
|
987
1030
|
|
|
988
1031
|
// Split, filter empty lines, resolve paths, and check existence
|
|
989
1032
|
const changedFiles = gitOutput
|
|
@@ -992,14 +1035,14 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
992
1035
|
.map((file) => path.resolve(gitRoot, file)); // Get absolute path
|
|
993
1036
|
|
|
994
1037
|
if (changedFiles.length > 0) {
|
|
995
|
-
|
|
1038
|
+
diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in target branch '${targetBranch}' vs base '${baseBranch}'`));
|
|
996
1039
|
}
|
|
997
1040
|
|
|
998
|
-
return changedFiles;
|
|
1041
|
+
return { changedFiles, baseBranch };
|
|
999
1042
|
}
|
|
1000
1043
|
catch (err) {
|
|
1001
1044
|
console.error(chalk.red('Error getting git diff:'), err.message);
|
|
1002
|
-
return [];
|
|
1045
|
+
return { changedFiles: [], baseBranch: null };
|
|
1003
1046
|
}
|
|
1004
1047
|
}
|
|
1005
1048
|
|
|
@@ -1009,6 +1052,58 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1009
1052
|
// --- Output Formatting Functions --- //
|
|
1010
1053
|
// These consume the normalized results returned by the RAG review pipeline
|
|
1011
1054
|
|
|
1055
|
+
function getReviewOutputFunction(outputFormat) {
|
|
1056
|
+
if (outputFormat === 'json') {
|
|
1057
|
+
return outputJson;
|
|
1058
|
+
}
|
|
1059
|
+
if (outputFormat === 'markdown') {
|
|
1060
|
+
return outputMarkdown;
|
|
1061
|
+
}
|
|
1062
|
+
return outputText;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function shouldEmitReviewOutput(options = {}) {
|
|
1066
|
+
return Boolean(options.outputFile || options.output === 'json' || options.output === 'markdown');
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
function shouldPrintErrorStack(options, error) {
|
|
1070
|
+
return Boolean(error?.stack && (isVerboseEnabled(options) || isDebugEnabled()));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function outputReviewResults(reviewResults, options, aggregateResult = {}) {
|
|
1074
|
+
await getReviewOutputFunction(options.output)(reviewResults, options, aggregateResult);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
async function emitEmptyReviewOutputIfNeeded(options, aggregateResult = {}) {
|
|
1078
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
await outputReviewResults([], options, aggregateResult);
|
|
1083
|
+
return true;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
async function emitErrorReviewOutputIfNeeded(options, error, aggregateResult = {}) {
|
|
1087
|
+
if (!shouldEmitReviewOutput(options)) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
await outputReviewResults([createReviewErrorResult(error)], options, {
|
|
1092
|
+
...aggregateResult,
|
|
1093
|
+
success: false,
|
|
1094
|
+
error,
|
|
1095
|
+
});
|
|
1096
|
+
return true;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
function createReviewErrorResult(error) {
|
|
1100
|
+
return {
|
|
1101
|
+
filePath: 'review',
|
|
1102
|
+
success: false,
|
|
1103
|
+
error,
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1012
1107
|
/**
|
|
1013
1108
|
* Output results in JSON format
|
|
1014
1109
|
*
|
|
@@ -1016,7 +1111,7 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1016
1111
|
* @param {Object} options - Command line options
|
|
1017
1112
|
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1018
1113
|
*/
|
|
1019
|
-
function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
1114
|
+
async function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
1020
1115
|
const prLevelFindings = collectPRLevelFindings(reviewResults, aggregateResult);
|
|
1021
1116
|
const fileLevelIssues = getFileLevelIssueCount(reviewResults);
|
|
1022
1117
|
|
|
@@ -1069,7 +1164,7 @@ function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
|
1069
1164
|
}
|
|
1070
1165
|
else {
|
|
1071
1166
|
// Write JSON output to stdout (process.stdout is not buffered)
|
|
1072
|
-
|
|
1167
|
+
await writeStdout(jsonOutput);
|
|
1073
1168
|
}
|
|
1074
1169
|
}
|
|
1075
1170
|
|
|
@@ -1080,7 +1175,7 @@ function outputJson(reviewResults, options, aggregateResult = {}) {
|
|
|
1080
1175
|
* @param {Object} options - Command line options
|
|
1081
1176
|
* @param {Object} aggregateResult - Full review result with PR-level context
|
|
1082
1177
|
*/
|
|
1083
|
-
function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
1178
|
+
async function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
1084
1179
|
const totalFiles = reviewResults.length;
|
|
1085
1180
|
const filesWithIssues = reviewResults.filter((r) => r.success && !r.skipped && r.results?.issues?.length > 0).length;
|
|
1086
1181
|
const skippedFiles = reviewResults.filter((r) => r.skipped).length;
|
|
@@ -1164,7 +1259,7 @@ function outputMarkdown(reviewResults, options, aggregateResult = {}) {
|
|
|
1164
1259
|
return;
|
|
1165
1260
|
}
|
|
1166
1261
|
|
|
1167
|
-
|
|
1262
|
+
await writeStdout(markdownOutput);
|
|
1168
1263
|
}
|
|
1169
1264
|
|
|
1170
1265
|
/**
|
package/src/llm.js
CHANGED
package/src/rag-review.js
CHANGED
|
@@ -10,7 +10,7 @@ import path from 'path';
|
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
|
|
12
12
|
import { shouldProcessFile } from './utils/file-validation.js';
|
|
13
|
-
import {
|
|
13
|
+
import { ensureBranchExists, findParentBranch, getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
|
|
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';
|
|
@@ -209,8 +209,9 @@ async function reviewPullRequest(changedFilePaths, options = {}) {
|
|
|
209
209
|
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
210
210
|
* @param {number} [options.concurrency=3] - Maximum number of fallback per-file reviews to run in parallel
|
|
211
211
|
* @param {string} [options.directory] - Working directory for git operations
|
|
212
|
-
* @param {string} [options.diffWith='HEAD'] -
|
|
212
|
+
* @param {string} [options.diffWith='HEAD'] - Target branch or ref being reviewed against the detected base branch
|
|
213
213
|
* @param {string} [options.actualBranch] - Actual target branch used for content retrieval
|
|
214
|
+
* @param {string} [options.baseBranch] - Pre-detected parent/base branch to diff against; detected here if omitted
|
|
214
215
|
* @param {boolean} [options.skipChunking=false] - Skip PR chunking, used internally for chunk review recursion
|
|
215
216
|
* @returns {Promise<Object>} Aggregated review results
|
|
216
217
|
*/
|
|
@@ -221,12 +222,27 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
221
222
|
|
|
222
223
|
// Step 1: Get the base branch and collect diff info for all files in the PR
|
|
223
224
|
const workingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
224
|
-
const baseBranch = findBaseBranch(workingDir);
|
|
225
225
|
const targetBranch = options.diffWith || 'HEAD'; // The feature branch being reviewed
|
|
226
226
|
|
|
227
227
|
// Get the actual branch name from options passed from index.js
|
|
228
228
|
const actualTargetBranch = options.actualBranch || targetBranch;
|
|
229
229
|
|
|
230
|
+
// Prefer the parent branch already detected (and materialized locally) by the
|
|
231
|
+
// caller to avoid re-running the O(branches) detection. Only detect here when
|
|
232
|
+
// this function is invoked directly without a pre-detected base.
|
|
233
|
+
let baseBranch = options.baseBranch;
|
|
234
|
+
if (!baseBranch) {
|
|
235
|
+
baseBranch = findParentBranch(actualTargetBranch, workingDir);
|
|
236
|
+
// Detection may return a branch that exists only on the remote; ensure it
|
|
237
|
+
// resolves locally before it is used for diffing.
|
|
238
|
+
try {
|
|
239
|
+
ensureBranchExists(baseBranch, workingDir);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
verboseLog(verbose, chalk.yellow(`Could not ensure base branch '${baseBranch}' exists locally: ${error.message}`));
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
230
246
|
verboseLog(verbose, chalk.gray(`Base branch: ${baseBranch}, Target branch: ${targetBranch}`));
|
|
231
247
|
|
|
232
248
|
const prFiles = [];
|
|
@@ -319,7 +335,9 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
|
|
|
319
335
|
verboseLog(
|
|
320
336
|
verbose,
|
|
321
337
|
chalk.green(
|
|
322
|
-
`De-duplicated context: ${deduplicatedCodeExamples.length} code examples, ${deduplicatedGuidelines.length} guidelines, ${
|
|
338
|
+
`De-duplicated context: ${deduplicatedCodeExamples.length} code examples, ${deduplicatedGuidelines.length} guidelines, ${
|
|
339
|
+
deduplicatedPRComments.length
|
|
340
|
+
} PR comments, ${deduplicatedCustomDocChunks.length} custom doc chunks`
|
|
323
341
|
)
|
|
324
342
|
);
|
|
325
343
|
|
package/src/rag-review.test.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runAnalysis, gatherUnifiedContextForPR } from './rag-analyzer.js';
|
|
2
2
|
import { reviewFile, reviewFiles, reviewPullRequest } from './rag-review.js';
|
|
3
3
|
import { shouldProcessFile } from './utils/file-validation.js';
|
|
4
|
-
import { getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
|
|
4
|
+
import { ensureBranchExists, findParentBranch, getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
|
|
5
5
|
import { shouldChunkPR, chunkPRFiles, combineChunkResults } from './utils/pr-chunking.js';
|
|
6
6
|
|
|
7
7
|
vi.mock('./rag-analyzer.js', () => ({
|
|
@@ -19,7 +19,8 @@ vi.mock('./utils/file-validation.js', () => ({
|
|
|
19
19
|
}));
|
|
20
20
|
|
|
21
21
|
vi.mock('./utils/git.js', () => ({
|
|
22
|
-
|
|
22
|
+
ensureBranchExists: vi.fn(),
|
|
23
|
+
findParentBranch: vi.fn().mockReturnValue('main'),
|
|
23
24
|
getChangedLinesInfo: vi.fn().mockReturnValue({
|
|
24
25
|
hasChanges: true,
|
|
25
26
|
addedLines: [1, 2, 3],
|
|
@@ -210,6 +211,38 @@ describe('rag-review', () => {
|
|
|
210
211
|
expect(result.results).toEqual([]);
|
|
211
212
|
});
|
|
212
213
|
|
|
214
|
+
it('should use a pre-detected baseBranch without re-running parent detection', async () => {
|
|
215
|
+
findParentBranch.mockClear();
|
|
216
|
+
getChangedLinesInfo.mockClear();
|
|
217
|
+
runAnalysis.mockResolvedValue({
|
|
218
|
+
success: true,
|
|
219
|
+
results: { issues: [], crossFileIssues: [], summary: 'OK' },
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
await reviewPullRequest(['/src/file.js'], { actualBranch: 'feature-B', baseBranch: 'feature-A' });
|
|
223
|
+
|
|
224
|
+
// Detection is skipped when the caller supplies the base branch...
|
|
225
|
+
expect(findParentBranch).not.toHaveBeenCalled();
|
|
226
|
+
// ...and the supplied base is what the per-file diff is computed against.
|
|
227
|
+
expect(getChangedLinesInfo).toHaveBeenCalledWith('/src/file.js', 'feature-A', 'feature-B', expect.any(String));
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should detect and ensure the parent branch when none is supplied', async () => {
|
|
231
|
+
findParentBranch.mockClear();
|
|
232
|
+
ensureBranchExists.mockClear();
|
|
233
|
+
findParentBranch.mockReturnValue('main');
|
|
234
|
+
runAnalysis.mockResolvedValue({
|
|
235
|
+
success: true,
|
|
236
|
+
results: { issues: [], crossFileIssues: [], summary: 'OK' },
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
await reviewPullRequest(['/src/file.js'], { actualBranch: 'feature-X' });
|
|
240
|
+
|
|
241
|
+
expect(findParentBranch).toHaveBeenCalledWith('feature-X', expect.any(String));
|
|
242
|
+
// A detected (possibly remote-only) base must be materialized locally before diffing.
|
|
243
|
+
expect(ensureBranchExists).toHaveBeenCalledWith('main', expect.any(String));
|
|
244
|
+
});
|
|
245
|
+
|
|
213
246
|
it('should skip files based on exclusion rules', async () => {
|
|
214
247
|
shouldProcessFile.mockReturnValue(false);
|
|
215
248
|
|