codex-review-mcp 2.7.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -73,6 +73,7 @@ server.registerTool('perform_code_review', {
73
73
  // REVIEW OPTIONS
74
74
  focus: z.string().optional().describe('Specific areas to focus on (e.g., "security and performance")'),
75
75
  maxTokens: z.number().optional().describe('Maximum tokens for review response'),
76
+ force: z.boolean().optional().describe('Force review even if diff is large (>500 lines). Use this to bypass chunking suggestions.'),
76
77
  },
77
78
  }, async (input, extra) => {
78
79
  try {
@@ -84,6 +85,7 @@ server.registerTool('perform_code_review', {
84
85
  skipContextGathering: input.skipContextGathering,
85
86
  focus: input.focus,
86
87
  maxTokens: input.maxTokens,
88
+ force: input.force,
87
89
  };
88
90
  const onProgress = async (message, progress, total) => {
89
91
  // Only send progress notifications if we have a valid string token (like toolu_xxx)
@@ -44,6 +44,7 @@ export function buildPrompt({ diffText, context, focus, version, isStaticReview
44
44
  '- setTimeout/setInterval without clear cleanup or justification',
45
45
  '- Direct DOM manipulation (document.querySelector, etc.) instead of React refs',
46
46
  '- Inline styles with hardcoded colors/sizes instead of theme values',
47
+ '- ⚠️ Z-INDEX CHANGES (HIGH RISK): Treat all z-index modifications with extreme caution. MUST analyze the entire z-index hierarchy, verify no breaking changes in stacking context, and check for existing z-index patterns in codebase before recommending changes.',
47
48
  '',
48
49
  `\n---\n${reviewType}${isStaticReview ? ' - Code Files' : ' (unified=0)'}:\n`,
49
50
  diffText,
@@ -83,140 +83,6 @@ async function scanDirectory(dirPath, extensions, visited = new Set()) {
83
83
  }
84
84
  return files;
85
85
  }
86
- /**
87
- * Parse diff to extract changed files
88
- */
89
- function parseChangedFiles(diff) {
90
- const files = [];
91
- const lines = diff.split('\n');
92
- for (const line of lines) {
93
- // Match "diff --git a/path/to/file b/path/to/file"
94
- if (line.startsWith('diff --git')) {
95
- const match = line.match(/b\/(.+)$/);
96
- if (match && match[1]) {
97
- // Unescape Git-escaped paths (e.g., "My\ Button.tsx" → "My Button.tsx")
98
- const filePath = match[1].replace(/\\(.)/g, '$1');
99
- files.push(filePath);
100
- }
101
- }
102
- // Also match "+++ b/path/to/file"
103
- else if (line.startsWith('+++') && line.includes('b/')) {
104
- const match = line.match(/\+\+\+ b\/(.+)$/);
105
- if (match && match[1]) {
106
- // Unescape Git-escaped paths
107
- const filePath = match[1].replace(/\\(.)/g, '$1');
108
- files.push(filePath);
109
- }
110
- }
111
- }
112
- return [...new Set(files)]; // Dedupe
113
- }
114
- /**
115
- * Detect new exported components/functions per file
116
- */
117
- function detectNewExports(diff) {
118
- const exportsByFile = {};
119
- let currentFile = null;
120
- const lines = diff.split('\n');
121
- for (const line of lines) {
122
- // Track which file we're in
123
- if (line.startsWith('+++ b/')) {
124
- const match = line.match(/\+\+\+ b\/(.+)$/);
125
- // Unescape Git-escaped paths to match parseChangedFiles behavior
126
- currentFile = match && match[1] ? match[1].replace(/\\(.)/g, '$1') : null;
127
- continue;
128
- }
129
- // Only look at added lines in a known file
130
- if (!line.startsWith('+') || !currentFile)
131
- continue;
132
- // Match: export function ComponentName
133
- // Match: export default function Component
134
- // Match: export async function getData
135
- // Match: export const ComponentName
136
- // Match: export class ComponentName
137
- const match = line.match(/^\+\s*export\s+(?:default\s+)?(?:async\s+)?(?:function|const|class)\s+([A-Za-z_]\w*)/);
138
- if (match && match[1]) {
139
- if (!exportsByFile[currentFile]) {
140
- exportsByFile[currentFile] = [];
141
- }
142
- if (!exportsByFile[currentFile].includes(match[1])) {
143
- exportsByFile[currentFile].push(match[1]);
144
- }
145
- }
146
- }
147
- return exportsByFile;
148
- }
149
- /**
150
- * Check if test file exists for given source file
151
- */
152
- async function checkForTestFile(sourcePath, baseDir) {
153
- const testPatterns = [
154
- // Same directory patterns
155
- sourcePath.replace(/\.tsx?$/, '.test.ts'),
156
- sourcePath.replace(/\.tsx?$/, '.test.tsx'),
157
- sourcePath.replace(/\.tsx?$/, '.spec.ts'),
158
- sourcePath.replace(/\.tsx?$/, '.spec.tsx'),
159
- // __tests__ directory patterns
160
- sourcePath.replace(/^src\//, 'src/__tests__/').replace(/\.tsx?$/, '.test.ts'),
161
- sourcePath.replace(/^src\//, 'src/__tests__/').replace(/\.tsx?$/, '.test.tsx'),
162
- sourcePath.replace(/^src\//, 'src/__tests__/').replace(/\.tsx?$/, '.spec.ts'),
163
- sourcePath.replace(/^src\//, 'src/__tests__/').replace(/\.tsx?$/, '.spec.tsx'),
164
- ];
165
- for (const pattern of testPatterns) {
166
- const testPath = join(baseDir, pattern);
167
- const exists = await readIfExists(testPath);
168
- if (exists !== null) {
169
- return pattern;
170
- }
171
- }
172
- return null;
173
- }
174
- /**
175
- * Gather smart context based on the diff
176
- */
177
- export async function gatherSmartContext(diff, baseDir) {
178
- const cwd = baseDir || process.cwd();
179
- const chunks = [];
180
- let totalSize = 0;
181
- const MAX_SMART_CONTEXT_SIZE = 20_000; // 20KB budget for smart context
182
- // 1. Extract changed files from diff
183
- const changedFiles = parseChangedFiles(diff);
184
- // 2. Read full content of changed files (up to 3 files to stay within budget)
185
- for (const file of changedFiles.slice(0, 3)) {
186
- if (totalSize >= MAX_SMART_CONTEXT_SIZE)
187
- break;
188
- const fullPath = join(cwd, file);
189
- const content = await readIfExists(fullPath);
190
- if (content) {
191
- const capped = content.slice(0, 8000); // Cap each file at 8KB
192
- chunks.push(`\n<!-- FULL CONTEXT: ${file} -->\n${capped}`);
193
- totalSize += capped.length;
194
- }
195
- }
196
- // 3. Check for test coverage when new exports are detected
197
- const exportsByFile = detectNewExports(diff);
198
- const filesWithExports = Object.keys(exportsByFile);
199
- if (filesWithExports.length > 0) {
200
- for (const file of filesWithExports) {
201
- // Only check TypeScript/React files
202
- if (!/\.(ts|tsx)$/.test(file))
203
- continue;
204
- const exportsForFile = exportsByFile[file];
205
- const testFile = await checkForTestFile(file, cwd);
206
- if (!testFile) {
207
- // Match test file extension to source extension
208
- const suggestedTest = file.endsWith('.tsx')
209
- ? file.replace(/\.tsx$/, '.test.tsx')
210
- : file.replace(/\.ts$/, '.test.ts');
211
- chunks.push(`\n<!-- TEST COVERAGE: ${file} -->\n⚠️ No test file found for ${file} which exports: ${exportsForFile.join(', ')}\nConsider adding: ${suggestedTest}`);
212
- }
213
- else {
214
- chunks.push(`\n<!-- TEST COVERAGE: ${file} -->\n✓ Test file exists: ${testFile}`);
215
- }
216
- }
217
- }
218
- return chunks.join('\n');
219
- }
220
86
  export async function gatherContext(baseDir) {
221
87
  const chunks = [];
222
88
  const processedPaths = new Set();
@@ -1,7 +1,6 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { gatherContext, gatherSmartContext } from './gatherContext.js';
2
+ import { gatherContext } from './gatherContext.js';
3
3
  import { promises as fs } from 'node:fs';
4
- import { join } from 'node:path';
5
4
  vi.mock('node:fs', () => ({
6
5
  promises: {
7
6
  readFile: vi.fn(),
@@ -354,357 +353,3 @@ describe('gatherContext', () => {
354
353
  });
355
354
  });
356
355
  });
357
- describe('gatherSmartContext', () => {
358
- const mockCwd = '/test/workspace';
359
- beforeEach(() => {
360
- vi.clearAllMocks();
361
- vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
362
- });
363
- afterEach(() => {
364
- vi.restoreAllMocks();
365
- });
366
- describe('File Parsing', () => {
367
- it('should extract changed files from diff', async () => {
368
- const mockReadFile = vi.mocked(fs.readFile);
369
- const diff = `diff --git a/src/components/UserCard.tsx b/src/components/UserCard.tsx
370
- index 1234567..abcdefg 100644
371
- --- a/src/components/UserCard.tsx
372
- +++ b/src/components/UserCard.tsx
373
- @@ -1,3 +1,4 @@
374
- +import { useState } from 'react';
375
- export function UserCard() {`;
376
- mockReadFile.mockResolvedValue('// Full file content\nexport function UserCard() {}');
377
- const result = await gatherSmartContext(diff, mockCwd);
378
- // Should read the full file
379
- expect(mockReadFile).toHaveBeenCalledWith(join(mockCwd, 'src/components/UserCard.tsx'), 'utf8');
380
- expect(result).toContain('FULL CONTEXT: src/components/UserCard.tsx');
381
- expect(result).toContain('export function UserCard()');
382
- });
383
- it('should handle multiple changed files', async () => {
384
- const mockReadFile = vi.mocked(fs.readFile);
385
- const diff = `diff --git a/src/UserCard.tsx b/src/UserCard.tsx
386
- +++ b/src/UserCard.tsx
387
- diff --git a/src/UserList.tsx b/src/UserList.tsx
388
- +++ b/src/UserList.tsx
389
- diff --git a/src/utils.ts b/src/utils.ts
390
- +++ b/src/utils.ts`;
391
- mockReadFile.mockImplementation(async (path) => {
392
- if (path.includes('UserCard'))
393
- return 'UserCard content';
394
- if (path.includes('UserList'))
395
- return 'UserList content';
396
- if (path.includes('utils'))
397
- return 'utils content';
398
- return Promise.reject(new Error('ENOENT'));
399
- });
400
- const result = await gatherSmartContext(diff, mockCwd);
401
- // Should read up to 3 files
402
- expect(result).toContain('UserCard content');
403
- expect(result).toContain('UserList content');
404
- expect(result).toContain('utils content');
405
- });
406
- it('should cap at 3 files to stay within budget', async () => {
407
- const mockReadFile = vi.mocked(fs.readFile);
408
- const diff = `diff --git a/file1.tsx b/file1.tsx
409
- +++ b/file1.tsx
410
- diff --git a/file2.tsx b/file2.tsx
411
- +++ b/file2.tsx
412
- diff --git a/file3.tsx b/file3.tsx
413
- +++ b/file3.tsx
414
- diff --git a/file4.tsx b/file4.tsx
415
- +++ b/file4.tsx`;
416
- mockReadFile.mockResolvedValue('file content');
417
- const result = await gatherSmartContext(diff, mockCwd);
418
- // Should only read first 3 files
419
- expect(mockReadFile).toHaveBeenCalledTimes(3);
420
- });
421
- it('should cap each file at 8KB', async () => {
422
- const mockReadFile = vi.mocked(fs.readFile);
423
- const diff = `diff --git a/large.tsx b/large.tsx
424
- +++ b/large.tsx`;
425
- const largeContent = 'x'.repeat(20000); // 20KB
426
- mockReadFile.mockResolvedValue(largeContent);
427
- const result = await gatherSmartContext(diff, mockCwd);
428
- // Should be capped at 8000 chars
429
- const contentMatch = result.match(/FULL CONTEXT:.*?\n(.*)/s);
430
- if (contentMatch) {
431
- expect(contentMatch[1].length).toBeLessThanOrEqual(8000);
432
- }
433
- });
434
- });
435
- describe('New Exports Detection', () => {
436
- it('should detect new exported components per file', async () => {
437
- const mockReadFile = vi.mocked(fs.readFile);
438
- const diff = `diff --git a/src/UserCard.tsx b/src/UserCard.tsx
439
- index abc123..def456 100644
440
- --- a/src/UserCard.tsx
441
- +++ b/src/UserCard.tsx
442
- @@ -1,3 +1,4 @@
443
- +export function UserCard() {}
444
- +export const UserList = () => {}`;
445
- mockReadFile.mockImplementation(async (path) => {
446
- // Only return content for the source file, not test files
447
- if (path.includes('UserCard.tsx') && !path.includes('test') && !path.includes('spec') && !path.includes('__tests__')) {
448
- return 'export function UserCard() {}';
449
- }
450
- // All test file patterns should fail
451
- return Promise.reject(new Error('ENOENT'));
452
- });
453
- const result = await gatherSmartContext(diff, mockCwd);
454
- // Should warn about missing tests for THIS file's exports
455
- expect(result).toContain('TEST COVERAGE: src/UserCard.tsx');
456
- expect(result).toContain('No test file found');
457
- expect(result).toContain('UserCard');
458
- expect(result).toContain('UserList');
459
- });
460
- it('should detect test files when they exist', async () => {
461
- const mockReadFile = vi.mocked(fs.readFile);
462
- const diff = `diff --git a/src/UserCard.tsx b/src/UserCard.tsx
463
- index abc123..def456 100644
464
- --- a/src/UserCard.tsx
465
- +++ b/src/UserCard.tsx
466
- @@ -1,3 +1,4 @@
467
- +export function UserCard() {}`;
468
- mockReadFile.mockImplementation(async (path) => {
469
- if (path.includes('UserCard.tsx') && !path.includes('test'))
470
- return 'export function UserCard() {}';
471
- if (path.includes('UserCard.test.tsx'))
472
- return 'test content';
473
- return Promise.reject(new Error('ENOENT'));
474
- });
475
- const result = await gatherSmartContext(diff, mockCwd);
476
- // Should confirm test exists
477
- expect(result).toContain('TEST COVERAGE');
478
- expect(result).toContain('Test file exists');
479
- expect(result).toContain('UserCard.test.tsx');
480
- });
481
- it('should find test file in spec.ts pattern', async () => {
482
- const mockReadFile = vi.mocked(fs.readFile);
483
- const diff = `diff --git a/src/UserService.ts b/src/UserService.ts
484
- +++ b/src/UserService.ts
485
- @@ -1,3 +1,4 @@
486
- +export class UserService {}`;
487
- mockReadFile.mockImplementation(async (path) => {
488
- const pathStr = String(path);
489
- // Source file
490
- if (pathStr.endsWith('src/UserService.ts')) {
491
- return 'export class UserService {}';
492
- }
493
- // Test file found at .spec.ts pattern
494
- if (pathStr.includes('UserService.spec.ts')) {
495
- return 'test content';
496
- }
497
- return Promise.reject(new Error('ENOENT'));
498
- });
499
- const result = await gatherSmartContext(diff, mockCwd);
500
- // Should have found the test file and reported it
501
- expect(result).toContain('TEST COVERAGE');
502
- expect(result).toContain('Test file exists');
503
- expect(result).toContain('UserService.spec.ts');
504
- });
505
- it('should only check TypeScript/React files for tests', async () => {
506
- const mockReadFile = vi.mocked(fs.readFile);
507
- const diff = `diff --git a/README.md b/README.md
508
- index abc..def 100644
509
- --- a/README.md
510
- +++ b/README.md
511
- @@ -1,1 +1,2 @@
512
- +export function Something() {}
513
- diff --git a/src/Component.tsx b/src/Component.tsx
514
- index ghi..jkl 100644
515
- --- a/src/Component.tsx
516
- +++ b/src/Component.tsx
517
- @@ -1,1 +1,2 @@
518
- +export function Component() {}`;
519
- mockReadFile.mockImplementation(async (path) => {
520
- if (path.includes('README.md'))
521
- return 'readme content';
522
- if (path.includes('Component.tsx') && !path.includes('test'))
523
- return 'component content';
524
- return Promise.reject(new Error('ENOENT'));
525
- });
526
- const result = await gatherSmartContext(diff, mockCwd);
527
- // Should only check Component.tsx, not README.md
528
- expect(result).toContain('TEST COVERAGE: src/Component.tsx');
529
- expect(result).not.toContain('TEST COVERAGE: README.md');
530
- });
531
- it('should detect lowercase and async function exports', async () => {
532
- const mockReadFile = vi.mocked(fs.readFile);
533
- const diff = `diff --git a/src/api.ts b/src/api.ts
534
- index abc..def 100644
535
- --- a/src/api.ts
536
- +++ b/src/api.ts
537
- @@ -1,1 +1,3 @@
538
- +export async function fetchUserData() {}
539
- +export function parseResponse() {}`;
540
- mockReadFile.mockImplementation(async (path) => {
541
- if (path.includes('api.ts') && !path.includes('test')) {
542
- return 'export async function fetchUserData() {}';
543
- }
544
- return Promise.reject(new Error('ENOENT'));
545
- });
546
- const result = await gatherSmartContext(diff, mockCwd);
547
- // Should detect both async and regular lowercase functions
548
- expect(result).toContain('TEST COVERAGE: src/api.ts');
549
- expect(result).toContain('fetchUserData');
550
- expect(result).toContain('parseResponse');
551
- // Should suggest .test.ts for .ts files
552
- expect(result).toContain('api.test.ts');
553
- });
554
- it('should detect default exports', async () => {
555
- const mockReadFile = vi.mocked(fs.readFile);
556
- const diff = `diff --git a/src/MyComponent.tsx b/src/MyComponent.tsx
557
- index abc..def 100644
558
- --- a/src/MyComponent.tsx
559
- +++ b/src/MyComponent.tsx
560
- @@ -1,1 +1,2 @@
561
- +export default function MyComponent() {}`;
562
- mockReadFile.mockImplementation(async (path) => {
563
- if (path.includes('MyComponent.tsx') && !path.includes('test')) {
564
- return 'export default function MyComponent() {}';
565
- }
566
- return Promise.reject(new Error('ENOENT'));
567
- });
568
- const result = await gatherSmartContext(diff, mockCwd);
569
- // Should detect default export
570
- expect(result).toContain('TEST COVERAGE: src/MyComponent.tsx');
571
- expect(result).toContain('MyComponent');
572
- // Should suggest .test.tsx for .tsx files
573
- expect(result).toContain('MyComponent.test.tsx');
574
- });
575
- it('should find tests in __tests__ directory', async () => {
576
- const mockReadFile = vi.mocked(fs.readFile);
577
- const diff = `diff --git a/src/components/Button.tsx b/src/components/Button.tsx
578
- index abc..def 100644
579
- --- a/src/components/Button.tsx
580
- +++ b/src/components/Button.tsx
581
- @@ -1,1 +1,2 @@
582
- +export function Button() {}`;
583
- mockReadFile.mockImplementation(async (path) => {
584
- const pathStr = String(path);
585
- if (pathStr.endsWith('src/components/Button.tsx')) {
586
- return 'export function Button() {}';
587
- }
588
- // Test file in __tests__ directory
589
- if (pathStr.includes('src/__tests__/components/Button.test.tsx')) {
590
- return 'test content';
591
- }
592
- return Promise.reject(new Error('ENOENT'));
593
- });
594
- const result = await gatherSmartContext(diff, mockCwd);
595
- // Should find test in __tests__ directory
596
- expect(result).toContain('TEST COVERAGE');
597
- expect(result).toContain('Test file exists');
598
- expect(result).toContain('__tests__/components/Button.test.tsx');
599
- });
600
- it('should detect empty test files correctly', async () => {
601
- const mockReadFile = vi.mocked(fs.readFile);
602
- const diff = `diff --git a/src/UserCard.tsx b/src/UserCard.tsx
603
- index abc..def 100644
604
- --- a/src/UserCard.tsx
605
- +++ b/src/UserCard.tsx
606
- @@ -1,1 +1,2 @@
607
- +export function UserCard() {}`;
608
- mockReadFile.mockImplementation(async (path) => {
609
- if (path.includes('UserCard.tsx') && !path.includes('test')) {
610
- return 'export function UserCard() {}';
611
- }
612
- // Empty test file (should still be detected!)
613
- if (path.includes('UserCard.test.tsx')) {
614
- return ''; // Empty string
615
- }
616
- return Promise.reject(new Error('ENOENT'));
617
- });
618
- const result = await gatherSmartContext(diff, mockCwd);
619
- // Should recognize empty test file as existing
620
- expect(result).toContain('TEST COVERAGE');
621
- expect(result).toContain('Test file exists');
622
- expect(result).not.toContain('No test file found');
623
- });
624
- it('should handle files without exports gracefully', async () => {
625
- const mockReadFile = vi.mocked(fs.readFile);
626
- const diff = `diff --git a/src/utils.ts b/src/utils.ts
627
- index abc..def 100644
628
- --- a/src/utils.ts
629
- +++ b/src/utils.ts
630
- @@ -1,1 +1,2 @@
631
- +// Just a comment, no exports`;
632
- mockReadFile.mockResolvedValue('// Just a comment');
633
- const result = await gatherSmartContext(diff, mockCwd);
634
- // Should not complain about missing tests if no exports
635
- expect(result).not.toContain('No test file found');
636
- // Should still include full file context
637
- expect(result).toContain('FULL CONTEXT: src/utils.ts');
638
- });
639
- });
640
- describe('Budget Management', () => {
641
- it('should respect 20KB budget for smart context', async () => {
642
- const mockReadFile = vi.mocked(fs.readFile);
643
- const diff = `diff --git a/file1.tsx b/file1.tsx
644
- +++ b/file1.tsx
645
- diff --git a/file2.tsx b/file2.tsx
646
- +++ b/file2.tsx
647
- diff --git a/file3.tsx b/file3.tsx
648
- +++ b/file3.tsx`;
649
- // Each file is 8KB (within per-file limit)
650
- mockReadFile.mockResolvedValue('x'.repeat(8000));
651
- const result = await gatherSmartContext(diff, mockCwd);
652
- // Should not exceed 20KB total (3 files × 8KB = 24KB, but budget is 20KB)
653
- expect(result.length).toBeLessThanOrEqual(25000); // Small buffer for markers
654
- });
655
- it('should handle missing files gracefully', async () => {
656
- const mockReadFile = vi.mocked(fs.readFile);
657
- const diff = `diff --git a/missing.tsx b/missing.tsx
658
- +++ b/missing.tsx`;
659
- mockReadFile.mockRejectedValue(new Error('ENOENT'));
660
- const result = await gatherSmartContext(diff, mockCwd);
661
- // Should not throw, just skip the file
662
- expect(result).toBeDefined();
663
- });
664
- });
665
- describe('Edge Cases', () => {
666
- it('should handle empty diff', async () => {
667
- const result = await gatherSmartContext('', mockCwd);
668
- expect(result).toBe('');
669
- });
670
- it('should handle diff with no file changes', async () => {
671
- const diff = `Some random text
672
- without any file markers`;
673
- const result = await gatherSmartContext(diff, mockCwd);
674
- expect(result).toBe('');
675
- });
676
- it('should handle malformed diff gracefully', async () => {
677
- const mockReadFile = vi.mocked(fs.readFile);
678
- mockReadFile.mockRejectedValue(new Error('ENOENT'));
679
- const diff = `diff --git a/
680
- +++ b/
681
- ---malformed---`;
682
- const result = await gatherSmartContext(diff, mockCwd);
683
- expect(result).toBeDefined();
684
- });
685
- it('should unescape Git-escaped file paths', async () => {
686
- const mockReadFile = vi.mocked(fs.readFile);
687
- const diff = `diff --git a/src/components/My\\ Button.tsx b/src/components/My\\ Button.tsx
688
- index abc..def 100644
689
- --- a/src/components/My\\ Button.tsx
690
- +++ b/src/components/My\\ Button.tsx
691
- @@ -1,1 +1,2 @@
692
- +export function MyButton() {}`;
693
- mockReadFile.mockImplementation(async (path) => {
694
- const pathStr = String(path);
695
- // Should be called with unescaped path
696
- if (pathStr.includes('src/components/My Button.tsx')) {
697
- return 'export function MyButton() {}';
698
- }
699
- return Promise.reject(new Error('ENOENT'));
700
- });
701
- const result = await gatherSmartContext(diff, mockCwd);
702
- // Should have read the file with unescaped path
703
- expect(mockReadFile).toHaveBeenCalledWith(join(mockCwd, 'src/components/My Button.tsx'), 'utf8');
704
- expect(result).toContain('FULL CONTEXT: src/components/My Button.tsx');
705
- // Should also detect exports with unescaped path
706
- expect(result).toContain('TEST COVERAGE: src/components/My Button.tsx');
707
- expect(result).toContain('MyButton');
708
- });
709
- });
710
- });
@@ -1,4 +1,4 @@
1
- import { gatherContext, gatherSmartContext } from '../review/gatherContext.js';
1
+ import { gatherContext } from '../review/gatherContext.js';
2
2
  import { buildPrompt } from '../review/buildPrompt.js';
3
3
  import { invokeAgent } from '../review/invokeAgent.js';
4
4
  import { formatOutput } from '../review/formatOutput.js';
@@ -11,6 +11,30 @@ const __filename = fileURLToPath(import.meta.url);
11
11
  const __dirname = dirname(__filename);
12
12
  const packageJson = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
13
13
  const VERSION = packageJson.version;
14
+ /**
15
+ * Parse file list from a diff
16
+ */
17
+ function parseFilesFromDiff(diff) {
18
+ const files = [];
19
+ const lines = diff.split('\n');
20
+ for (const line of lines) {
21
+ // Match "diff --git a/path/to/file b/path/to/file"
22
+ if (line.startsWith('diff --git')) {
23
+ const match = line.match(/b\/(.+)$/);
24
+ if (match && match[1]) {
25
+ files.push(match[1]);
26
+ }
27
+ }
28
+ // Also match "+++ b/path/to/file"
29
+ else if (line.startsWith('+++') && line.includes('b/')) {
30
+ const match = line.match(/\+\+\+ b\/(.+)$/);
31
+ if (match && match[1] && match[1] !== '/dev/null') {
32
+ files.push(match[1]);
33
+ }
34
+ }
35
+ }
36
+ return [...new Set(files)]; // Dedupe
37
+ }
14
38
  /**
15
39
  * Perform code review with GPT-5 Codex
16
40
  *
@@ -23,6 +47,46 @@ export async function performCodeReview(input, onProgress) {
23
47
  throw new Error('content is required and cannot be empty. ' +
24
48
  'The calling agent should provide the diff or code to review.');
25
49
  }
50
+ // Check diff size and suggest chunking for large diffs
51
+ const lineCount = input.content.split('\n').length;
52
+ const LARGE_DIFF_THRESHOLD = 500;
53
+ if (lineCount > LARGE_DIFF_THRESHOLD && !input.force && input.contentType !== 'code') {
54
+ const files = parseFilesFromDiff(input.content);
55
+ const filesList = files.length > 0
56
+ ? files.map(f => ` • ${f}`).join('\n')
57
+ : ' (Unable to parse file list from diff)';
58
+ return `⚠️ **Large Diff Detected** (~${lineCount} lines)
59
+
60
+ For optimal review quality, consider chunking this review into smaller pieces.
61
+
62
+ **Why chunk?**
63
+ - More thorough analysis per file/feature
64
+ - Faster feedback on each chunk
65
+ - Better context retention by the AI reviewer
66
+ - Easier to track and address issues
67
+
68
+ **Files in this diff:**
69
+ ${filesList}
70
+
71
+ **Suggested approach:**
72
+ 1. Review by individual file or logical feature
73
+ 2. Aim for <${LARGE_DIFF_THRESHOLD} lines per review
74
+ 3. Use focused reviews for critical changes
75
+
76
+ **Want to proceed anyway?**
77
+ Pass \`force: true\` to review the full ${lineCount}-line diff in one go.
78
+
79
+ **Example chunked review:**
80
+ \`\`\`
81
+ # Review file by file:
82
+ await perform_code_review({
83
+ content: await getFileDiff("src/components/Button.tsx"),
84
+ workspaceDir: "/path/to/project",
85
+ focus: "React best practices"
86
+ });
87
+ \`\`\`
88
+ `;
89
+ }
26
90
  await onProgress?.('Preparing review…', 10, 100);
27
91
  // Gather context with smart precedence
28
92
  const shouldSkip = input.skipContextGathering === true;
@@ -37,22 +101,14 @@ export async function performCodeReview(input, onProgress) {
37
101
  : shouldSkip
38
102
  ? ''
39
103
  : await gatherContext(input.workspaceDir);
40
- // Gather smart context based on the diff (full files + test coverage)
41
- let smartContext = '';
42
- if (!shouldSkip && !usingCustom && input.workspaceDir) {
43
- smartContext = await gatherSmartContext(input.content, input.workspaceDir);
44
- }
45
- const combinedContext = [context, smartContext].filter(Boolean).join('\n\n');
46
104
  // Log context gathering results (informational, not errors!)
47
- const hasCursorRules = combinedContext.includes('.cursor/rules');
105
+ const hasCursorRules = context.includes('.cursor/rules');
48
106
  if (hasCursorRules) {
49
- const cursorFiles = (combinedContext.match(/<!-- \.cursor\/rules\/[^>]+ -->/g) || []);
50
- const smartInfo = smartContext ? ` + ${smartContext.length} smart context` : '';
51
- console.error(`[codex-review-mcp] Context: ${context.length} chars with .cursor/rules → ${cursorFiles.map(f => f.replace('<!-- ', '').replace(' -->', '')).join(', ')}${smartInfo}`);
107
+ const cursorFiles = (context.match(/<!-- \.cursor\/rules\/[^>]+ -->/g) || []);
108
+ console.error(`[codex-review-mcp] Context: ${context.length} chars with .cursor/rules ${cursorFiles.map(f => f.replace('<!-- ', '').replace(' -->', '')).join(', ')}`);
52
109
  }
53
- else if (combinedContext.length > 0) {
54
- const smartInfo = smartContext ? ` + ${smartContext.length} smart context` : '';
55
- console.error(`[codex-review-mcp] Context: ${context.length} chars from package.json, tsconfig.json, etc. (no .cursor/rules in repo)${smartInfo}`);
110
+ else if (context.length > 0) {
111
+ console.error(`[codex-review-mcp] Context: ${context.length} chars from package.json, tsconfig.json, etc. (no .cursor/rules in repo)`);
56
112
  }
57
113
  else {
58
114
  console.error(`[codex-review-mcp] No project context found (generic review mode)`);
@@ -64,13 +120,12 @@ export async function performCodeReview(input, onProgress) {
64
120
  const isStaticReview = input.contentType === 'code';
65
121
  const prompt = buildPrompt({
66
122
  diffText: input.content,
67
- context: combinedContext,
123
+ context,
68
124
  focus: input.focus,
69
125
  version: VERSION,
70
126
  isStaticReview
71
127
  });
72
128
  // Invoke GPT-5 Codex
73
- const lineCount = input.content.split('\n').length;
74
129
  const reviewType = isStaticReview ? 'code' : 'diff';
75
130
  await onProgress?.(`Calling GPT-5 Codex with ${reviewType} (~${lineCount} lines)…`, 70, 100);
76
131
  const agentMd = await invokeAgent({
@@ -249,4 +249,74 @@ describe('performCodeReview', () => {
249
249
  expect(progressCalls).toContain('Gathering project context…');
250
250
  });
251
251
  });
252
+ describe('Large Diff Detection', () => {
253
+ it('should return chunking guidance for large diffs (>500 lines)', async () => {
254
+ // Create a large diff with 600 lines
255
+ const largeDiff = Array(600)
256
+ .fill(0)
257
+ .map((_, i) => `+line ${i}`)
258
+ .join('\n');
259
+ const fullDiff = `diff --git a/file1.ts b/file1.ts
260
+ +++ b/file1.ts
261
+ ${largeDiff}
262
+ diff --git a/file2.ts b/file2.ts
263
+ +++ b/file2.ts
264
+ +more changes`;
265
+ const result = await performCodeReview({
266
+ content: fullDiff,
267
+ contentType: 'diff'
268
+ });
269
+ const lineCount = fullDiff.split('\n').length;
270
+ expect(result).toContain('⚠️ **Large Diff Detected**');
271
+ expect(result).toContain(`~${lineCount} lines`);
272
+ expect(result).toContain('file1.ts');
273
+ expect(result).toContain('file2.ts');
274
+ expect(result).toContain('force: true');
275
+ expect(result).toContain('Why chunk?');
276
+ });
277
+ it('should allow force review of large diffs', async () => {
278
+ const largeDiff = Array(600)
279
+ .fill(0)
280
+ .map((_, i) => `+line ${i}`)
281
+ .join('\n');
282
+ vi.spyOn(gatherContextModule, 'gatherContext').mockResolvedValue('');
283
+ vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review\n\nLooks good!');
284
+ const result = await performCodeReview({
285
+ content: largeDiff,
286
+ contentType: 'diff',
287
+ force: true
288
+ });
289
+ expect(result).toContain('# Review');
290
+ expect(result).toContain('Looks good!');
291
+ expect(result).not.toContain('Large Diff Detected');
292
+ });
293
+ it('should not trigger chunking guidance for code reviews', async () => {
294
+ const largeCode = Array(600)
295
+ .fill(0)
296
+ .map((_, i) => `const x${i} = ${i};`)
297
+ .join('\n');
298
+ vi.spyOn(gatherContextModule, 'gatherContext').mockResolvedValue('');
299
+ vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review\n\nLooks good!');
300
+ const result = await performCodeReview({
301
+ content: largeCode,
302
+ contentType: 'code'
303
+ });
304
+ expect(result).not.toContain('Large Diff Detected');
305
+ expect(result).toContain('# Review');
306
+ });
307
+ it('should not trigger chunking guidance for small diffs (<500 lines)', async () => {
308
+ const smallDiff = Array(400)
309
+ .fill(0)
310
+ .map((_, i) => `+line ${i}`)
311
+ .join('\n');
312
+ vi.spyOn(gatherContextModule, 'gatherContext').mockResolvedValue('');
313
+ vi.spyOn(invokeAgentModule, 'invokeAgent').mockResolvedValue('# Review\n\nLooks good!');
314
+ const result = await performCodeReview({
315
+ content: smallDiff,
316
+ contentType: 'diff'
317
+ });
318
+ expect(result).not.toContain('Large Diff Detected');
319
+ expect(result).toContain('# Review');
320
+ });
321
+ });
252
322
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-review-mcp",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "main": "index.js",
5
5
  "scripts": {
6
6
  "build": "tsc",