codecritique 2.0.5 → 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 +48 -28
- 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,7 +29,7 @@ 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,
|
|
32
|
+
import { ensureBranchExists, findParentBranch } from './utils/git.js';
|
|
33
33
|
import { diagnosticLog, isDebugEnabled, isVerboseEnabled, verboseLog } from './utils/logging.js';
|
|
34
34
|
import { collectPRLevelFindings, getFileLevelIssueCount, hasPRLevelFindings } from './utils/review-results.js';
|
|
35
35
|
import { configureCleanStdoutForDataOutput, isBrokenStdoutPipeError, writeStdout } from './utils/stdout.js';
|
|
@@ -46,7 +46,11 @@ program.name('codecritique').description('CLI tool for AI-powered code review us
|
|
|
46
46
|
program
|
|
47
47
|
.command('analyze')
|
|
48
48
|
.description('Analyze code using dynamic context (RAG approach)')
|
|
49
|
-
.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)')
|
|
50
54
|
.option('-f, --files <files...>', 'Specific files or glob patterns to review')
|
|
51
55
|
.option('--file <file>', 'Analyze a single file')
|
|
52
56
|
.option('-d, --directory <dir>', 'Working directory for git operations (use with --diff-with)')
|
|
@@ -163,12 +167,12 @@ Examples:
|
|
|
163
167
|
$ codecritique analyze --directory src/components
|
|
164
168
|
$ codecritique analyze --file src/utils/validation.ts
|
|
165
169
|
$ codecritique analyze --files "src/**/*.tsx" "lib/*.js"
|
|
166
|
-
$ codecritique analyze -b
|
|
170
|
+
$ codecritique analyze -b feature-branch
|
|
167
171
|
$ codecritique analyze --doc "Our Eng Guidelines:./ENGINEERING_GUIDELINES.md" --file src/utils/validation.ts
|
|
168
172
|
$ codecritique analyze --diff-with feature-branch -d /path/to/repo
|
|
169
173
|
$ codecritique analyze --output json > review-results.json
|
|
170
174
|
$ codecritique analyze --track-feedback --feedback-path ./feedback-artifacts --file src/utils/validation.ts
|
|
171
|
-
$ codecritique analyze --track-feedback --feedback-threshold 0.8 -b
|
|
175
|
+
$ codecritique analyze --track-feedback --feedback-threshold 0.8 -b feature-branch
|
|
172
176
|
$ codecritique embeddings:generate --directory src
|
|
173
177
|
$ codecritique embeddings:generate --exclude "**/*.test.js" "**/*.spec.js"
|
|
174
178
|
$ codecritique embeddings:generate --exclude-file .embedignore
|
|
@@ -357,23 +361,26 @@ async function runCodeReview(options) {
|
|
|
357
361
|
diagnosticLog(chalk.bold.blue('AI Code Review (RAG Approach) - Starting analysis...'));
|
|
358
362
|
|
|
359
363
|
// Determine the review mode based on options
|
|
360
|
-
// Only support: single file, specific files, or diff
|
|
364
|
+
// Only support: single file, specific files, or target-branch diff review
|
|
361
365
|
if (options.diffWith) {
|
|
366
|
+
const targetBranch = options.diffWith;
|
|
362
367
|
// Use directory option as working directory for git commands if specified
|
|
363
368
|
const gitWorkingDir = options.directory ? path.resolve(options.directory) : process.cwd();
|
|
364
|
-
const changedFiles = getChangedFiles(
|
|
369
|
+
const { changedFiles, baseBranch } = getChangedFiles(targetBranch, gitWorkingDir, options.base);
|
|
365
370
|
if (changedFiles.length === 0) {
|
|
366
|
-
const message = `No changed files found
|
|
371
|
+
const message = `No changed files found in target branch '${targetBranch}' compared to its detected parent branch. Exiting.`;
|
|
367
372
|
diagnosticLog(chalk.yellow(message));
|
|
368
373
|
await emitEmptyReviewOutputIfNeeded(options, { success: true, results: [], message });
|
|
369
374
|
return;
|
|
370
375
|
}
|
|
371
|
-
operationDescription = `${changedFiles.length} files changed
|
|
372
|
-
// 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.
|
|
373
379
|
const enhancedReviewOptions = {
|
|
374
380
|
...reviewOptions,
|
|
375
|
-
actualBranch:
|
|
376
|
-
diffWith:
|
|
381
|
+
actualBranch: targetBranch,
|
|
382
|
+
diffWith: targetBranch,
|
|
383
|
+
baseBranch,
|
|
377
384
|
};
|
|
378
385
|
reviewTask = reviewPullRequest(changedFiles, enhancedReviewOptions);
|
|
379
386
|
}
|
|
@@ -400,7 +407,7 @@ async function runCodeReview(options) {
|
|
|
400
407
|
console.error(chalk.red('Error: You must specify one of the following:'));
|
|
401
408
|
console.error(chalk.yellow(' --file <file> Analyze a single file'));
|
|
402
409
|
console.error(chalk.yellow(' --files <files...> Analyze specific files or glob patterns'));
|
|
403
|
-
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'));
|
|
404
411
|
console.error(chalk.gray('\nOptional:'));
|
|
405
412
|
console.error(chalk.gray(' -d, --directory <dir> Working directory (for git operations with --diff-with)'));
|
|
406
413
|
console.error(chalk.gray('\nExamples:'));
|
|
@@ -973,24 +980,36 @@ async function expandFilePatterns(patterns, baseDir = process.cwd()) {
|
|
|
973
980
|
}
|
|
974
981
|
|
|
975
982
|
/**
|
|
976
|
-
* Get list of files changed in
|
|
977
|
-
*
|
|
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.
|
|
978
990
|
*
|
|
979
|
-
* @param {string}
|
|
991
|
+
* @param {string} targetBranch - Branch to analyze (the feature/target branch)
|
|
980
992
|
* @param {string} workingDir - Directory to run git commands in (optional, defaults to cwd)
|
|
981
|
-
* @
|
|
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).
|
|
982
996
|
*/
|
|
983
|
-
function getChangedFiles(
|
|
997
|
+
function getChangedFiles(targetBranch, workingDir = process.cwd(), explicitBase = undefined) {
|
|
984
998
|
try {
|
|
985
999
|
// Get git root directory
|
|
986
1000
|
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd: workingDir }).toString().trim();
|
|
987
1001
|
diagnosticLog(chalk.gray(`Git repository: ${gitRoot}`));
|
|
988
1002
|
|
|
989
|
-
// Ensure the branch exists locally (fetch if needed)
|
|
990
|
-
ensureBranchExists(
|
|
1003
|
+
// Ensure the target branch exists locally (fetch if needed)
|
|
1004
|
+
ensureBranchExists(targetBranch, workingDir);
|
|
991
1005
|
|
|
992
|
-
//
|
|
993
|
-
|
|
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
|
+
}
|
|
994
1013
|
|
|
995
1014
|
// Ensure the base branch exists locally as well (crucial for diff operations)
|
|
996
1015
|
try {
|
|
@@ -1001,12 +1020,13 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1001
1020
|
// Continue with the original baseBranch name, it might work with remote refs
|
|
1002
1021
|
}
|
|
1003
1022
|
|
|
1004
|
-
diagnosticLog(chalk.gray(`
|
|
1023
|
+
diagnosticLog(chalk.gray(`Reviewing target branch '${targetBranch}' against base branch '${baseBranch}'...`));
|
|
1005
1024
|
|
|
1006
|
-
// Use three-dot notation to get changes in branch compared to base
|
|
1007
|
-
// 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.
|
|
1008
1026
|
// By adding --diff-filter=d, we exclude deleted files from the list.
|
|
1009
|
-
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();
|
|
1010
1030
|
|
|
1011
1031
|
// Split, filter empty lines, resolve paths, and check existence
|
|
1012
1032
|
const changedFiles = gitOutput
|
|
@@ -1015,14 +1035,14 @@ function getChangedFiles(branch, workingDir = process.cwd()) {
|
|
|
1015
1035
|
.map((file) => path.resolve(gitRoot, file)); // Get absolute path
|
|
1016
1036
|
|
|
1017
1037
|
if (changedFiles.length > 0) {
|
|
1018
|
-
diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in ${
|
|
1038
|
+
diagnosticLog(chalk.gray(`Found ${changedFiles.length} changed files in target branch '${targetBranch}' vs base '${baseBranch}'`));
|
|
1019
1039
|
}
|
|
1020
1040
|
|
|
1021
|
-
return changedFiles;
|
|
1041
|
+
return { changedFiles, baseBranch };
|
|
1022
1042
|
}
|
|
1023
1043
|
catch (err) {
|
|
1024
1044
|
console.error(chalk.red('Error getting git diff:'), err.message);
|
|
1025
|
-
return [];
|
|
1045
|
+
return { changedFiles: [], baseBranch: null };
|
|
1026
1046
|
}
|
|
1027
1047
|
}
|
|
1028
1048
|
|
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
|
|