codecritique 1.2.3 → 1.2.4

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -24,6 +24,8 @@ import { inferContextFromDocumentContent } from './utils/context-inference.js';
24
24
  import { isGenericDocument, getGenericDocumentContext } from './utils/document-detection.js';
25
25
  import { isDocumentationFile } from './utils/file-validation.js';
26
26
  import { debug, verboseLog } from './utils/logging.js';
27
+ import { isPathWithinProject } from './utils/path-utils.js';
28
+ import { escapeSqlString } from './utils/string-utils.js';
27
29
 
28
30
  const FILE_EMBEDDINGS_TABLE = TABLE_NAMES.FILE_EMBEDDINGS;
29
31
  const DOCUMENT_CHUNK_TABLE = TABLE_NAMES.DOCUMENT_CHUNK;
@@ -54,6 +56,58 @@ export class ContentRetriever {
54
56
  this.cleaningUp = false;
55
57
  }
56
58
 
59
+ resolveProjectResultPath(filePath, resolvedProjectPath) {
60
+ if (!filePath) {
61
+ return null;
62
+ }
63
+
64
+ const absolutePath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(resolvedProjectPath, filePath);
65
+ return isPathWithinProject(absolutePath, resolvedProjectPath) ? absolutePath : null;
66
+ }
67
+
68
+ async filterResultsForProject(results, resolvedProjectPath, getPath) {
69
+ const resultsToCheck = [];
70
+ const projectMatchMap = new Map();
71
+
72
+ for (let i = 0; i < results.length; i++) {
73
+ const result = results[i];
74
+ const resultPath = getPath(result);
75
+
76
+ if (result.project_path && result.project_path !== resolvedProjectPath) {
77
+ projectMatchMap.set(i, false);
78
+ continue;
79
+ }
80
+
81
+ const absolutePath = this.resolveProjectResultPath(resultPath, resolvedProjectPath);
82
+ if (!absolutePath) {
83
+ projectMatchMap.set(i, false);
84
+ continue;
85
+ }
86
+
87
+ resultsToCheck.push({ index: i, absolutePath, resultPath });
88
+ }
89
+
90
+ if (resultsToCheck.length > 0) {
91
+ const existenceResults = await Promise.all(
92
+ resultsToCheck.map(async ({ index, absolutePath, resultPath }) => {
93
+ try {
94
+ await fs.promises.access(absolutePath, fs.constants.F_OK);
95
+ return { index, exists: true };
96
+ } catch {
97
+ debug(`Filtering out non-existent project file: ${resultPath}`);
98
+ return { index, exists: false };
99
+ }
100
+ })
101
+ );
102
+
103
+ for (const { index, exists } of existenceResults) {
104
+ projectMatchMap.set(index, exists);
105
+ }
106
+ }
107
+
108
+ return results.filter((result, index) => projectMatchMap.get(index) === true);
109
+ }
110
+
57
111
  /**
58
112
  * Find relevant documentation with sophisticated reranking
59
113
  * @param {string} queryText - The search query
@@ -99,7 +153,7 @@ export class ContentRetriever {
99
153
  try {
100
154
  const tableSchema = await table.schema;
101
155
  if (tableSchema?.fields?.some((field) => field.name === 'project_path')) {
102
- query = query.where(`project_path = '${resolvedProjectPath.replace(/'/g, "''")}'`);
156
+ query = query.where(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
103
157
  debug(`Filtering documentation by project_path: ${resolvedProjectPath}`);
104
158
  }
105
159
  } catch (schemaError) {
@@ -109,65 +163,11 @@ export class ContentRetriever {
109
163
  const results = await query.limit(Math.max(limit * 3, 20)).toArray();
110
164
  verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} documentation results`));
111
165
 
112
- // OPTIMIZATION: Enhanced batch file existence checks with parallel processing
113
- const docsToCheck = [];
114
- const docProjectMatchMap = new Map();
115
-
116
- // First pass: collect files that need existence checking
117
- for (let i = 0; i < results.length; i++) {
118
- const result = results[i];
119
-
120
- if (result.project_path) {
121
- docProjectMatchMap.set(i, result.project_path === resolvedProjectPath);
122
- continue;
123
- }
124
-
125
- if (!result.original_document_path) {
126
- docProjectMatchMap.set(i, false);
127
- continue;
128
- }
129
-
130
- const filePath = result.original_document_path;
131
- try {
132
- if (path.isAbsolute(filePath)) {
133
- docProjectMatchMap.set(i, filePath.startsWith(resolvedProjectPath));
134
- continue;
135
- }
136
-
137
- const absolutePath = path.resolve(resolvedProjectPath, filePath);
138
- if (absolutePath.startsWith(resolvedProjectPath)) {
139
- // Mark for batch existence check
140
- docsToCheck.push({ result, index: i, absolutePath, filePath });
141
- } else {
142
- docProjectMatchMap.set(i, false);
143
- }
144
- } catch (error) {
145
- debug(`Error filtering result for project: ${error.message}`);
146
- docProjectMatchMap.set(i, false);
147
- }
148
- }
149
-
150
- // Enhanced batch check file existence with improved error handling
151
- if (docsToCheck.length > 0) {
152
- debug(`[OPTIMIZATION] Batch checking existence of ${docsToCheck.length} documentation files`);
153
- const existencePromises = docsToCheck.map(async ({ index, absolutePath, filePath }) => {
154
- try {
155
- await fs.promises.access(absolutePath, fs.constants.F_OK);
156
- return { index, exists: true };
157
- } catch {
158
- debug(`Filtering out non-existent documentation file: ${filePath}`);
159
- return { index, exists: false };
160
- }
161
- });
162
-
163
- const existenceResults = await Promise.all(existencePromises);
164
- for (const { index, exists } of existenceResults) {
165
- docProjectMatchMap.set(index, exists);
166
- }
167
- }
168
-
169
- // Filter results based on project match using the map
170
- const projectFilteredResults = results.filter((result, index) => docProjectMatchMap.get(index) === true);
166
+ const projectFilteredResults = await this.filterResultsForProject(
167
+ results,
168
+ resolvedProjectPath,
169
+ (result) => result.original_document_path
170
+ );
171
171
 
172
172
  verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} documentation results from current project`));
173
173
  let finalResults = projectFilteredResults.map((result) => {
@@ -493,13 +493,13 @@ export class ContentRetriever {
493
493
  if (queryFilePath) {
494
494
  const normalizedQueryPath = path.resolve(resolvedProjectPath, queryFilePath);
495
495
  // Add condition to exclude the file being reviewed
496
- const escapedPath = normalizedQueryPath.replace(/'/g, "''");
496
+ const escapedPath = escapeSqlString(normalizedQueryPath);
497
497
  conditions.push(`path != '${escapedPath}'`);
498
498
 
499
499
  // Also check for relative path variants to be thorough
500
500
  const relativePath = path.relative(resolvedProjectPath, normalizedQueryPath);
501
501
  if (relativePath && !relativePath.startsWith('..')) {
502
- const escapedRelativePath = relativePath.replace(/'/g, "''");
502
+ const escapedRelativePath = escapeSqlString(relativePath);
503
503
  conditions.push(`path != '${escapedRelativePath}'`);
504
504
  }
505
505
 
@@ -515,7 +515,7 @@ export class ContentRetriever {
515
515
 
516
516
  if (hasProjectPathField) {
517
517
  // Use exact match for project path
518
- conditions.push(`project_path = '${resolvedProjectPath.replace(/'/g, "''")}'`);
518
+ conditions.push(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
519
519
  debug(`Filtering by project_path: ${resolvedProjectPath}`);
520
520
  }
521
521
  }
@@ -532,72 +532,11 @@ export class ContentRetriever {
532
532
 
533
533
  verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} results`));
534
534
 
535
- // OPTIMIZATION: Batch file existence checks for better performance
536
- const resultsToCheck = [];
537
- const projectMatchMap = new Map();
538
-
539
- // First pass: collect files that need existence checking
540
- for (let i = 0; i < results.length; i++) {
541
- const result = results[i];
542
-
543
- // Use project_path field if available (new schema)
544
- if (result.project_path) {
545
- projectMatchMap.set(i, result.project_path === resolvedProjectPath);
546
- continue;
547
- }
548
-
549
- // Fallback for old embeddings without project_path field
550
- if (!result.path && !result.original_document_path) {
551
- projectMatchMap.set(i, false);
552
- continue;
553
- }
554
-
555
- const filePath = result.original_document_path || result.path;
556
- try {
557
- // Check if this result belongs to the current project
558
- // First try as absolute path
559
- if (path.isAbsolute(filePath)) {
560
- projectMatchMap.set(i, filePath.startsWith(resolvedProjectPath));
561
- continue;
562
- }
563
-
564
- // For relative paths, check if the file actually exists in the project
565
- const absolutePath = path.resolve(resolvedProjectPath, filePath);
566
-
567
- // Verify the path is within project bounds
568
- if (absolutePath.startsWith(resolvedProjectPath)) {
569
- // Mark for batch existence check
570
- resultsToCheck.push({ result, index: i, absolutePath });
571
- } else {
572
- projectMatchMap.set(i, false);
573
- }
574
- } catch (error) {
575
- debug(`Error filtering result for project: ${error.message}`);
576
- projectMatchMap.set(i, false);
577
- }
578
- }
579
-
580
- // Batch check file existence for better performance
581
- if (resultsToCheck.length > 0) {
582
- debug(`[OPTIMIZATION] Batch checking existence of ${resultsToCheck.length} files`);
583
- const existencePromises = resultsToCheck.map(async ({ result, index, absolutePath }) => {
584
- try {
585
- await fs.promises.access(absolutePath, fs.constants.F_OK);
586
- return { index, exists: true };
587
- } catch {
588
- debug(`Filtering out non-existent file: ${result.original_document_path || result.path}`);
589
- return { index, exists: false };
590
- }
591
- });
592
-
593
- const existenceResults = await Promise.all(existencePromises);
594
- for (const { index, exists } of existenceResults) {
595
- projectMatchMap.set(index, exists);
596
- }
597
- }
598
-
599
- // Filter results based on project match using the map
600
- const projectFilteredResults = results.filter((result, index) => projectMatchMap.get(index) === true);
535
+ const projectFilteredResults = await this.filterResultsForProject(
536
+ results,
537
+ resolvedProjectPath,
538
+ (result) => result.original_document_path || result.path
539
+ );
601
540
 
602
541
  verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} results from current project`));
603
542
 
@@ -643,14 +582,11 @@ export class ContentRetriever {
643
582
  try {
644
583
  const fileTable = await this.database.getTable(FILE_EMBEDDINGS_TABLE);
645
584
  if (fileTable) {
646
- // Look for project-specific structure ID
647
- const projectStructureId = `__project_structure__${path.basename(resolvedProjectPath)}`;
648
- let structureResults = await fileTable.query().where(`id = '${projectStructureId}'`).limit(1).toArray();
649
-
650
- // Fall back to generic project structure if project-specific one doesn't exist
651
- if (structureResults.length === 0) {
652
- structureResults = await fileTable.query().where("id = '__project_structure__'").limit(1).toArray();
653
- }
585
+ const structureResults = await fileTable
586
+ .query()
587
+ .where(`project_path = '${escapeSqlString(resolvedProjectPath)}' AND type = 'directory-structure'`)
588
+ .limit(1)
589
+ .toArray();
654
590
 
655
591
  if (structureResults.length > 0) {
656
592
  const structureRecord = structureResults[0];
@@ -459,6 +459,14 @@ describe('ContentRetriever', () => {
459
459
  const results = await retriever.findRelevantDocs('query', { projectPath: '/project' });
460
460
  expect(results.length).toBe(0);
461
461
  });
462
+
463
+ it('should reject sibling project absolute paths for documentation', async () => {
464
+ mockTable.toArray.mockResolvedValue([
465
+ createMockDocResult({ project_path: null, original_document_path: '/project-old/docs/readme.md' }),
466
+ ]);
467
+ const results = await retriever.findRelevantDocs('query', { projectPath: '/project' });
468
+ expect(results).toHaveLength(0);
469
+ });
462
470
  });
463
471
 
464
472
  // ==========================================================================
@@ -498,6 +506,12 @@ describe('ContentRetriever', () => {
498
506
  expect(results.length).toBe(0);
499
507
  });
500
508
 
509
+ it('should reject sibling project absolute paths for code results', async () => {
510
+ mockTable.toArray.mockResolvedValue([createMockCodeResult({ project_path: null, path: '/project-old/src/file.js' })]);
511
+ const results = await retriever.findSimilarCode('query', { projectPath: '/project', similarityThreshold: 0 });
512
+ expect(results).toHaveLength(0);
513
+ });
514
+
501
515
  it('should handle schema check errors', async () => {
502
516
  mockTable.schema = null;
503
517
  mockTable.toArray.mockResolvedValue([createMockCodeResult()]);
@@ -511,20 +525,31 @@ describe('ContentRetriever', () => {
511
525
  // ==========================================================================
512
526
 
513
527
  describe('project structure inclusion', () => {
514
- it('should fall back to generic project structure', async () => {
528
+ it('should include only project-scoped structure rows', async () => {
515
529
  mockTable.toArray.mockResolvedValue([createMockCodeResult()]);
516
- mockTable.query.mockReturnValue({
530
+ const queryChain = {
517
531
  where: vi.fn().mockReturnThis(),
518
532
  limit: vi.fn().mockReturnThis(),
519
- toArray: vi
520
- .fn()
521
- .mockResolvedValueOnce([])
522
- .mockResolvedValueOnce([
523
- { id: '__project_structure__', content: 'Generic structure', path: '.', vector: new Float32Array(384).fill(0.1) },
524
- ]),
533
+ toArray: vi.fn().mockResolvedValue([
534
+ {
535
+ id: '__project_structure__#abc12345',
536
+ content: 'Project structure',
537
+ path: '.',
538
+ project_path: '/project',
539
+ type: 'directory-structure',
540
+ vector: new Float32Array(384).fill(0.1),
541
+ },
542
+ ]),
543
+ };
544
+ mockTable.query.mockReturnValue(queryChain);
545
+ const results = await retriever.findSimilarCode('query', {
546
+ includeProjectStructure: true,
547
+ similarityThreshold: 0,
548
+ projectPath: '/project',
525
549
  });
526
- const results = await retriever.findSimilarCode('query', { includeProjectStructure: true, similarityThreshold: 0 });
527
550
  expect(results.some((r) => r.type === 'project-structure')).toBe(true);
551
+ expect(queryChain.where).toHaveBeenCalledWith(expect.stringContaining("type = 'directory-structure'"));
552
+ expect(queryChain.where).toHaveBeenCalledWith(expect.stringContaining("project_path = '/project'"));
528
553
  });
529
554
 
530
555
  it('should handle project structure inclusion errors', async () => {
@@ -539,6 +564,21 @@ describe('ContentRetriever', () => {
539
564
  expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Project structure inclusion failed'));
540
565
  });
541
566
 
567
+ it('should skip project structure rows from another project', async () => {
568
+ mockTable.toArray.mockResolvedValue([createMockCodeResult()]);
569
+ mockTable.query.mockReturnValue({
570
+ where: vi.fn().mockReturnThis(),
571
+ limit: vi.fn().mockReturnThis(),
572
+ toArray: vi.fn().mockResolvedValue([]),
573
+ });
574
+ const results = await retriever.findSimilarCode('query', {
575
+ includeProjectStructure: true,
576
+ similarityThreshold: 0,
577
+ projectPath: '/project',
578
+ });
579
+ expect(results.some((r) => r.type === 'project-structure')).toBe(false);
580
+ });
581
+
542
582
  it('should skip structure when similarity is too low', async () => {
543
583
  mockTable.toArray.mockResolvedValue([createMockCodeResult()]);
544
584
  mockTable.query.mockReturnValue({
package/src/index.js CHANGED
@@ -519,6 +519,7 @@ async function generateEmbeddings(options) {
519
519
 
520
520
  // Get files to process
521
521
  let filesToProcess = [];
522
+ const runMode = options.files && options.files.length > 0 ? 'partial' : 'full';
522
523
 
523
524
  if (options.files && options.files.length > 0) {
524
525
  console.log(chalk.cyan('Processing specified files/patterns...'));
@@ -585,6 +586,7 @@ async function generateEmbeddings(options) {
585
586
  baseDir: baseDir,
586
587
  batchSize: 100, // Set a reasonable batch size
587
588
  maxLines: parseInt(options.maxLines || '1000', 10),
589
+ runMode,
588
590
  onProgress: (status) => {
589
591
  // Update counters based on status
590
592
  if (status === 'processed') {
@@ -625,9 +627,6 @@ async function generateEmbeddings(options) {
625
627
  forceAnalysis: options.forceAnalysis,
626
628
  });
627
629
 
628
- // Store project summary in embeddings system for later use
629
- await embeddingsSystem.storeProjectSummary(projectDir, projectSummary);
630
-
631
630
  console.log(chalk.green('✅ Project analysis complete and stored'));
632
631
  verboseLog(options, chalk.gray(` Project: ${projectSummary.projectName}`));
633
632
  verboseLog(
@@ -209,12 +209,17 @@ export class ProjectAnalyzer {
209
209
  // Check for existing analysis
210
210
  const existingSummary = forceAnalysis ? null : await this.loadExistingAnalysis(projectPath);
211
211
  if (existingSummary && !forceAnalysis) {
212
- const currentHash = await this.calculateKeyFilesHash(existingSummary.keyFiles);
213
- if (existingSummary.keyFilesHash === currentHash) {
214
- verboseLog(verbose, chalk.green(' Project analysis up-to-date (no key file changes detected)'));
215
- return existingSummary;
212
+ const currentEmbeddingInventoryHash = await this.calculateEmbeddingInventoryHash(projectPath);
213
+ if (existingSummary.embeddingInventoryHash !== currentEmbeddingInventoryHash) {
214
+ verboseLog(verbose, chalk.yellow('🔄 Embedding inventory changed, regenerating analysis...'));
215
+ } else {
216
+ const currentHash = await this.calculateKeyFilesHash(existingSummary.keyFiles);
217
+ if (existingSummary.keyFilesHash === currentHash) {
218
+ verboseLog(verbose, chalk.green('✅ Project analysis up-to-date (no key file changes detected)'));
219
+ return existingSummary;
220
+ }
221
+ verboseLog(verbose, chalk.yellow('🔄 Key files changed, regenerating analysis...'));
216
222
  }
217
- verboseLog(verbose, chalk.yellow('🔄 Key files changed, regenerating analysis...'));
218
223
  } else {
219
224
  verboseLog(
220
225
  verbose,
@@ -241,6 +246,7 @@ export class ProjectAnalyzer {
241
246
  const currentHash = await this.calculateKeyFilesHash(keyFiles);
242
247
  projectSummary.keyFiles = keyFiles;
243
248
  projectSummary.keyFilesHash = currentHash;
249
+ projectSummary.embeddingInventoryHash = await this.calculateEmbeddingInventoryHash(projectPath);
244
250
 
245
251
  await this.storeAnalysis(projectPath, projectSummary);
246
252
 
@@ -613,6 +619,39 @@ Select files following the criteria in the system instructions.`;
613
619
  return hash.digest('hex');
614
620
  }
615
621
 
622
+ /**
623
+ * Calculate a project-scoped hash of the current embedding inventory.
624
+ */
625
+ async calculateEmbeddingInventoryHash(projectPath) {
626
+ try {
627
+ const embeddingsSystem = getDefaultEmbeddingsSystem();
628
+ await embeddingsSystem.initialize();
629
+ const table = await embeddingsSystem.databaseManager.getTable(embeddingsSystem.databaseManager.fileEmbeddingsTable);
630
+
631
+ if (!table) {
632
+ return 'no-file-embeddings-table';
633
+ }
634
+
635
+ const records = await table
636
+ .query()
637
+ .select(['type', 'path', 'content_hash', 'project_path'])
638
+ .where(`project_path = '${projectPath.replace(/'/g, "''")}'`)
639
+ .toArray();
640
+
641
+ const hash = crypto.createHash('sha256');
642
+ const normalizedRows = records.map((record) => `${record.type || 'file'}:${record.path || ''}:${record.content_hash || ''}`).sort();
643
+
644
+ for (const row of normalizedRows) {
645
+ hash.update(row);
646
+ }
647
+
648
+ return hash.digest('hex');
649
+ } catch (error) {
650
+ verboseLog({}, chalk.yellow(`Warning: Could not calculate embedding inventory hash: ${error.message}`));
651
+ return 'embedding-inventory-unavailable';
652
+ }
653
+ }
654
+
616
655
  /**
617
656
  * Generate comprehensive project summary using LLM analysis (SINGLE CALL)
618
657
  */
@@ -910,6 +910,15 @@ describe('rag-analyzer', () => {
910
910
  expect(context).toHaveProperty('codeExamples');
911
911
  });
912
912
 
913
+ it('should degrade gracefully when retrieval returns no context for active files', async () => {
914
+ mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([]);
915
+ mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([]);
916
+ const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
917
+ const context = await gatherUnifiedContextForPR(prFiles, { projectPath: '/project' });
918
+ expect(context.codeExamples).toEqual([]);
919
+ expect(context.guidelines).toEqual([]);
920
+ });
921
+
913
922
  it('should find custom document chunks', async () => {
914
923
  mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([{ content: 'Custom doc', document_title: 'Guidelines' }]);
915
924
  const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];