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 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
- # Analyze changes in feature-branch vs main branch (auto-detects base branch)
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**: Ensure the base branch is available for diff analysis (see example above)
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
- # Analyze branch diff
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.0.5",
3
+ "version": "2.0.6",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
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, findBaseBranch } from './utils/git.js';
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('-b, --diff-with <branch>', 'Analyze files changed compared to a branch (triggers PR review mode)')
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 main
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 main
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 with branch
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(options.diffWith, gitWorkingDir);
369
+ const { changedFiles, baseBranch } = getChangedFiles(targetBranch, gitWorkingDir, options.base);
365
370
  if (changedFiles.length === 0) {
366
- const message = `No changed files found compared to branch '${options.diffWith}'. Exiting.`;
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 vs ${options.diffWith}`;
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: options.diffWith,
376
- diffWith: options.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> Analyze files changed in a 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 a branch compared to the base branch (main/master).
977
- * This shows what changes the specified branch has compared to the base.
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} branch - Branch to analyze (the feature/target branch)
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
- * @returns {Array<string>} Array of changed file paths relative to git root
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(branch, workingDir = process.cwd()) {
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(branch, workingDir);
1003
+ // Ensure the target branch exists locally (fetch if needed)
1004
+ ensureBranchExists(targetBranch, workingDir);
991
1005
 
992
- // Find the base branch (main/master)
993
- const baseBranch = findBaseBranch(workingDir);
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(`Comparing ${branch} against ${baseBranch}...`));
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}...${branch}`], { cwd: gitRoot }).toString();
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 ${branch} vs ${baseBranch}`));
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 { findBaseBranch, getChangedLinesInfo, getFileContentFromGit } from './utils/git.js';
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'] - Branch or ref to compare against
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, ${deduplicatedPRComments.length} PR comments, ${deduplicatedCustomDocChunks.length} custom doc chunks`
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
 
@@ -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
- findBaseBranch: vi.fn().mockReturnValue('main'),
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