codecritique 1.2.3 → 2.0.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm version](https://img.shields.io/npm/v/codecritique.svg)](https://www.npmjs.com/package/codecritique)
4
4
  [![npm downloads](https://img.shields.io/npm/dm/codecritique.svg)](https://www.npmjs.com/package/codecritique)
5
5
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
- [![Node.js](https://img.shields.io/badge/node-%3E%3D22.14.0-brightgreen.svg)](https://nodejs.org/)
6
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D24.15.0-brightgreen.svg)](https://nodejs.org/)
7
7
  [![CI](https://github.com/cosmocoder/CodeCritique/actions/workflows/release.yml/badge.svg)](https://github.com/cosmocoder/CodeCritique/actions/workflows/release.yml)
8
8
 
9
9
  **AI-Powered Code Review. Context-Aware. Privacy-First.**
@@ -96,7 +96,7 @@ This RAG-based approach provides more accurate, project-specific code reviews co
96
96
 
97
97
  ### Prerequisites
98
98
 
99
- - **Node.js** v22.14.0 or higher
99
+ - **Node.js** v24.15.0 or higher
100
100
  - **Git** (for diff-based analysis)
101
101
  - **Anthropic API key** (for LLM analysis)
102
102
 
@@ -199,7 +199,7 @@ For easier integration with non-JavaScript projects, you can use the provided sh
199
199
 
200
200
  3. **Environment setup** (the script handles this automatically):
201
201
  - Creates/uses `.env` file in your project directory
202
- - Validates Node.js v22.14.0+ requirement
202
+ - Validates Node.js v24.15.0+ requirement
203
203
  - Provides helpful error messages for missing dependencies
204
204
 
205
205
  ## Quick Start
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "1.2.3",
3
+ "version": "2.0.0",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -53,43 +53,45 @@
53
53
  "dependencies": {
54
54
  "@anthropic-ai/sdk": "0.71.0",
55
55
  "@huggingface/transformers": "3.8.0",
56
- "@lancedb/lancedb": "0.22.3",
56
+ "@lancedb/lancedb": "0.30.0",
57
57
  "@octokit/rest": "22.0.1",
58
+ "apache-arrow": "18.1.0",
58
59
  "chalk": "5.6.2",
59
60
  "cli-spinner": "0.2.10",
60
61
  "commander": "14.0.1",
61
- "dotenv": "17.2.3",
62
- "fastembed": "2.0.0",
63
- "glob": "13.0.0",
64
- "linguist-languages": "9.1.0",
65
- "lru-cache": "11.2.2",
66
- "minimatch": "10.1.1",
62
+ "dotenv": "17.4.2",
63
+ "fastembed": "2.1.0",
64
+ "glob": "13.0.6",
65
+ "linguist-languages": "9.3.2",
66
+ "lru-cache": "11.5.1",
67
+ "minimatch": "10.2.5",
67
68
  "stopwords-iso": "1.1.0"
68
69
  },
69
70
  "devDependencies": {
70
71
  "@eslint/js": "9.39.1",
71
72
  "@semantic-release/commit-analyzer": "13.0.1",
72
- "@semantic-release/github": "12.0.3",
73
- "@semantic-release/npm": "13.1.3",
74
- "@semantic-release/release-notes-generator": "14.1.0",
75
- "@types/node": "24.9.1",
76
- "@vitest/coverage-v8": "4.0.16",
77
- "@vitest/eslint-plugin": "1.3.23",
73
+ "@semantic-release/github": "12.0.8",
74
+ "@semantic-release/npm": "13.1.5",
75
+ "@semantic-release/release-notes-generator": "14.1.1",
76
+ "@types/node": "24.12.4",
77
+ "@vitest/coverage-v8": "4.1.8",
78
+ "@vitest/eslint-plugin": "1.6.19",
78
79
  "eslint": "9.39.1",
79
80
  "eslint-plugin-import": "2.32.0",
80
81
  "globals": "16.5.0",
81
82
  "knip": "5.70.2",
82
- "prettier": "3.7.1",
83
+ "prettier": "3.8.3",
84
+ "prettier-plugin-brace-style": "0.10.1",
83
85
  "typescript": "5.9.3",
84
- "vitest": "4.0.16"
86
+ "vitest": "4.1.8"
85
87
  },
86
88
  "volta": {
87
- "node": "22.14.0",
88
- "npm": "10.9.2"
89
+ "node": "24.15.0",
90
+ "npm": "11.12.1"
89
91
  },
90
92
  "engines": {
91
- "node": ">=22.14.0",
92
- "npm": "10.x"
93
+ "node": ">=24.15.0",
94
+ "npm": ">=11.12.1 <12"
93
95
  },
94
96
  "engine-strict": true,
95
97
  "publishConfig": {
@@ -23,7 +23,10 @@ import { calculateCosineSimilarity, calculatePathSimilarity } from './embeddings
23
23
  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
+ import { getTableSchema, schemaHasField } from './utils/lancedb.js';
26
27
  import { debug, verboseLog } from './utils/logging.js';
28
+ import { isPathWithinProject } from './utils/path-utils.js';
29
+ import { escapeSqlString } from './utils/string-utils.js';
27
30
 
28
31
  const FILE_EMBEDDINGS_TABLE = TABLE_NAMES.FILE_EMBEDDINGS;
29
32
  const DOCUMENT_CHUNK_TABLE = TABLE_NAMES.DOCUMENT_CHUNK;
@@ -54,6 +57,59 @@ export class ContentRetriever {
54
57
  this.cleaningUp = false;
55
58
  }
56
59
 
60
+ resolveProjectResultPath(filePath, resolvedProjectPath) {
61
+ if (!filePath) {
62
+ return null;
63
+ }
64
+
65
+ const absolutePath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(resolvedProjectPath, filePath);
66
+ return isPathWithinProject(absolutePath, resolvedProjectPath) ? absolutePath : null;
67
+ }
68
+
69
+ async filterResultsForProject(results, resolvedProjectPath, getPath) {
70
+ const resultsToCheck = [];
71
+ const projectMatchMap = new Map();
72
+
73
+ for (let i = 0; i < results.length; i++) {
74
+ const result = results[i];
75
+ const resultPath = getPath(result);
76
+
77
+ if (result.project_path && result.project_path !== resolvedProjectPath) {
78
+ projectMatchMap.set(i, false);
79
+ continue;
80
+ }
81
+
82
+ const absolutePath = this.resolveProjectResultPath(resultPath, resolvedProjectPath);
83
+ if (!absolutePath) {
84
+ projectMatchMap.set(i, false);
85
+ continue;
86
+ }
87
+
88
+ resultsToCheck.push({ index: i, absolutePath, resultPath });
89
+ }
90
+
91
+ if (resultsToCheck.length > 0) {
92
+ const existenceResults = await Promise.all(
93
+ resultsToCheck.map(async ({ index, absolutePath, resultPath }) => {
94
+ try {
95
+ await fs.promises.access(absolutePath, fs.constants.F_OK);
96
+ return { index, exists: true };
97
+ }
98
+ catch {
99
+ debug(`Filtering out non-existent project file: ${resultPath}`);
100
+ return { index, exists: false };
101
+ }
102
+ })
103
+ );
104
+
105
+ for (const { index, exists } of existenceResults) {
106
+ projectMatchMap.set(index, exists);
107
+ }
108
+ }
109
+
110
+ return results.filter((result, index) => projectMatchMap.get(index) === true);
111
+ }
112
+
57
113
  /**
58
114
  * Find relevant documentation with sophisticated reranking
59
115
  * @param {string} queryText - The search query
@@ -97,86 +153,35 @@ export class ContentRetriever {
97
153
 
98
154
  const resolvedProjectPath = path.resolve(projectPath);
99
155
  try {
100
- const tableSchema = await table.schema;
101
- if (tableSchema?.fields?.some((field) => field.name === 'project_path')) {
102
- query = query.where(`project_path = '${resolvedProjectPath.replace(/'/g, "''")}'`);
156
+ const tableSchema = await getTableSchema(table);
157
+ if (schemaHasField(tableSchema, 'project_path')) {
158
+ query = query.where(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
103
159
  debug(`Filtering documentation by project_path: ${resolvedProjectPath}`);
104
160
  }
105
- } catch (schemaError) {
161
+ }
162
+ catch (schemaError) {
106
163
  debug(`Could not check schema for project_path field: ${schemaError.message}`);
107
164
  }
108
165
 
109
166
  const results = await query.limit(Math.max(limit * 3, 20)).toArray();
110
167
  verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} documentation results`));
111
168
 
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);
169
+ const projectFilteredResults = await this.filterResultsForProject(
170
+ results,
171
+ resolvedProjectPath,
172
+ (result) => result.original_document_path
173
+ );
171
174
 
172
175
  verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} documentation results from current project`));
173
176
  let finalResults = projectFilteredResults.map((result) => {
174
177
  let similarity;
175
178
  if (result._distance !== undefined) {
176
179
  similarity = Math.max(0, Math.min(1, 1 - result._distance));
177
- } else if (result._score !== undefined) {
180
+ }
181
+ else if (result._score !== undefined) {
178
182
  similarity = Math.max(0, Math.min(1, result._score));
179
- } else {
183
+ }
184
+ else {
180
185
  similarity = 0.5;
181
186
  }
182
187
 
@@ -286,7 +291,8 @@ export class ContentRetriever {
286
291
  // Use pre-computed context for generic documents (README, RUNBOOK, etc.)
287
292
  context = getGenericDocumentContext(originalPath, docH1);
288
293
  debug(`[FAST-PATH] Using pre-computed context for generic document: ${originalPath}`);
289
- } else {
294
+ }
295
+ else {
290
296
  // Use the expensive inference for non-generic documents
291
297
  context = await inferContextFromDocumentContent(
292
298
  originalPath,
@@ -297,7 +303,8 @@ export class ContentRetriever {
297
303
  }
298
304
 
299
305
  return context;
300
- } catch (error) {
306
+ }
307
+ catch (error) {
301
308
  debug(`[ERROR] Failed to get context for ${originalPath}: ${error.message}`);
302
309
  // Return a fallback context to avoid breaking the pipeline
303
310
  return {
@@ -367,7 +374,8 @@ export class ContentRetriever {
367
374
  contextMatchBonus += MODERATE_BOOST_TECH_MATCH;
368
375
  }
369
376
  }
370
- } else if (queryContextForReranking.area !== 'GeneralJS_TS') {
377
+ }
378
+ else if (queryContextForReranking.area !== 'GeneralJS_TS') {
371
379
  contextMatchBonus += HEAVY_PENALTY_AREA_MISMATCH;
372
380
  }
373
381
  }
@@ -420,7 +428,8 @@ export class ContentRetriever {
420
428
  verboseLog(options, chalk.green(`Returning ${finalResults.length} documentation results`));
421
429
 
422
430
  return finalResults;
423
- } catch (error) {
431
+ }
432
+ catch (error) {
424
433
  console.error(chalk.red(`Error in findRelevantDocs: ${error.message}`), error);
425
434
  throw new EmbeddingError(`Documentation search failed: ${error.message}`);
426
435
  }
@@ -477,7 +486,8 @@ export class ContentRetriever {
477
486
  // Only include test files
478
487
  conditions.push(`(path LIKE '%.test.%' OR path LIKE '%.spec.%' OR path LIKE '%_test.py' OR path LIKE 'test_%.py')`);
479
488
  verboseLog(options, chalk.blue(`Filtering to include only test files.`));
480
- } else {
489
+ }
490
+ else {
481
491
  // Exclude test files
482
492
  conditions.push(
483
493
  `(path NOT LIKE '%.test.%' AND path NOT LIKE '%.spec.%' AND path NOT LIKE '%_test.py' AND path NOT LIKE 'test_%.py')`
@@ -493,13 +503,13 @@ export class ContentRetriever {
493
503
  if (queryFilePath) {
494
504
  const normalizedQueryPath = path.resolve(resolvedProjectPath, queryFilePath);
495
505
  // Add condition to exclude the file being reviewed
496
- const escapedPath = normalizedQueryPath.replace(/'/g, "''");
506
+ const escapedPath = escapeSqlString(normalizedQueryPath);
497
507
  conditions.push(`path != '${escapedPath}'`);
498
508
 
499
509
  // Also check for relative path variants to be thorough
500
510
  const relativePath = path.relative(resolvedProjectPath, normalizedQueryPath);
501
511
  if (relativePath && !relativePath.startsWith('..')) {
502
- const escapedRelativePath = relativePath.replace(/'/g, "''");
512
+ const escapedRelativePath = escapeSqlString(relativePath);
503
513
  conditions.push(`path != '${escapedRelativePath}'`);
504
514
  }
505
515
 
@@ -509,17 +519,14 @@ export class ContentRetriever {
509
519
  // Add project path filtering if the field exists in the schema
510
520
  // Check if the table has project_path field
511
521
  try {
512
- const tableSchema = await table.schema;
513
- if (tableSchema && tableSchema.fields) {
514
- const hasProjectPathField = tableSchema.fields.some((field) => field.name === 'project_path');
515
-
516
- if (hasProjectPathField) {
517
- // Use exact match for project path
518
- conditions.push(`project_path = '${resolvedProjectPath.replace(/'/g, "''")}'`);
519
- debug(`Filtering by project_path: ${resolvedProjectPath}`);
520
- }
522
+ const tableSchema = await getTableSchema(table);
523
+ if (schemaHasField(tableSchema, 'project_path')) {
524
+ // Use exact match for project path
525
+ conditions.push(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
526
+ debug(`Filtering by project_path: ${resolvedProjectPath}`);
521
527
  }
522
- } catch (schemaError) {
528
+ }
529
+ catch (schemaError) {
523
530
  debug(`Could not check schema for project_path field: ${schemaError.message}`);
524
531
  // Continue without project_path filtering in query
525
532
  }
@@ -532,72 +539,11 @@ export class ContentRetriever {
532
539
 
533
540
  verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} results`));
534
541
 
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);
542
+ const projectFilteredResults = await this.filterResultsForProject(
543
+ results,
544
+ resolvedProjectPath,
545
+ (result) => result.original_document_path || result.path
546
+ );
601
547
 
602
548
  verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} results from current project`));
603
549
 
@@ -609,10 +555,12 @@ export class ContentRetriever {
609
555
  // Vector search distance (0 = perfect match, higher = less similar)
610
556
  // Apply more precise normalization to avoid all scores being 1.000
611
557
  similarity = Math.max(0, Math.min(1, Math.exp(-result._distance * 2)));
612
- } else if (result._score !== undefined) {
558
+ }
559
+ else if (result._score !== undefined) {
613
560
  // FTS or hybrid score - normalize to 0-1 range with better scaling
614
561
  similarity = Math.max(0, Math.min(1, result._score / Math.max(result._score, 1)));
615
- } else {
562
+ }
563
+ else {
616
564
  // Fallback
617
565
  similarity = 0.5;
618
566
  }
@@ -643,14 +591,11 @@ export class ContentRetriever {
643
591
  try {
644
592
  const fileTable = await this.database.getTable(FILE_EMBEDDINGS_TABLE);
645
593
  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
- }
594
+ const structureResults = await fileTable
595
+ .query()
596
+ .where(`project_path = '${escapeSqlString(resolvedProjectPath)}' AND type = 'directory-structure'`)
597
+ .limit(1)
598
+ .toArray();
654
599
 
655
600
  if (structureResults.length > 0) {
656
601
  const structureRecord = structureResults[0];
@@ -676,7 +621,8 @@ export class ContentRetriever {
676
621
  }
677
622
  }
678
623
  }
679
- } catch (error) {
624
+ }
625
+ catch (error) {
680
626
  console.warn(chalk.yellow(`Project structure inclusion failed: ${error.message}`));
681
627
  }
682
628
  }
@@ -689,7 +635,8 @@ export class ContentRetriever {
689
635
 
690
636
  verboseLog(options, chalk.green(`Returning ${finalResults.length} optimized hybrid search results`));
691
637
  return finalResults;
692
- } catch (error) {
638
+ }
639
+ catch (error) {
693
640
  console.error(chalk.red(`Error in optimized findSimilarCode: ${error.message}`), error);
694
641
  return [];
695
642
  }
@@ -744,7 +691,8 @@ export class ContentRetriever {
744
691
  };
745
692
 
746
693
  verboseLog({}, chalk.green('ContentRetriever cleanup complete'));
747
- } finally {
694
+ }
695
+ finally {
748
696
  this.cleaningUp = false;
749
697
  }
750
698
  }
@@ -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({