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 +3 -3
- package/package.json +22 -20
- package/src/content-retrieval.js +109 -161
- package/src/content-retrieval.test.js +49 -9
- package/src/custom-documents.js +29 -14
- package/src/feedback-loader.js +19 -8
- package/src/index.js +97 -48
- package/src/llm.js +7 -3
- package/src/project-analyzer.js +92 -26
- package/src/rag-analyzer.js +70 -34
- package/src/rag-analyzer.test.js +12 -1
- package/src/rag-review.js +14 -7
- package/src/zero-shot-classifier-open.js +16 -7
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/codecritique)
|
|
4
4
|
[](https://www.npmjs.com/package/codecritique)
|
|
5
5
|
[](https://opensource.org/licenses/MIT)
|
|
6
|
-
[](https://nodejs.org/)
|
|
7
7
|
[](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**
|
|
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
|
|
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": "
|
|
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.
|
|
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
|
|
62
|
-
"fastembed": "2.
|
|
63
|
-
"glob": "13.0.
|
|
64
|
-
"linguist-languages": "9.
|
|
65
|
-
"lru-cache": "11.
|
|
66
|
-
"minimatch": "10.
|
|
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.
|
|
73
|
-
"@semantic-release/npm": "13.1.
|
|
74
|
-
"@semantic-release/release-notes-generator": "14.1.
|
|
75
|
-
"@types/node": "24.
|
|
76
|
-
"@vitest/coverage-v8": "4.
|
|
77
|
-
"@vitest/eslint-plugin": "1.
|
|
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.
|
|
83
|
+
"prettier": "3.8.3",
|
|
84
|
+
"prettier-plugin-brace-style": "0.10.1",
|
|
83
85
|
"typescript": "5.9.3",
|
|
84
|
-
"vitest": "4.
|
|
86
|
+
"vitest": "4.1.8"
|
|
85
87
|
},
|
|
86
88
|
"volta": {
|
|
87
|
-
"node": "
|
|
88
|
-
"npm": "
|
|
89
|
+
"node": "24.15.0",
|
|
90
|
+
"npm": "11.12.1"
|
|
89
91
|
},
|
|
90
92
|
"engines": {
|
|
91
|
-
"node": ">=
|
|
92
|
-
"npm": "
|
|
93
|
+
"node": ">=24.15.0",
|
|
94
|
+
"npm": ">=11.12.1 <12"
|
|
93
95
|
},
|
|
94
96
|
"engine-strict": true,
|
|
95
97
|
"publishConfig": {
|
package/src/content-retrieval.js
CHANGED
|
@@ -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
|
|
101
|
-
if (tableSchema
|
|
102
|
-
query = query.where(`project_path = '${resolvedProjectPath
|
|
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
|
-
}
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
}
|
|
180
|
+
}
|
|
181
|
+
else if (result._score !== undefined) {
|
|
178
182
|
similarity = Math.max(0, Math.min(1, result._score));
|
|
179
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
|
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
|
|
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
|
|
513
|
-
if (tableSchema
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
-
}
|
|
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
|
|
528
|
+
it('should include only project-scoped structure rows', async () => {
|
|
515
529
|
mockTable.toArray.mockResolvedValue([createMockCodeResult()]);
|
|
516
|
-
|
|
530
|
+
const queryChain = {
|
|
517
531
|
where: vi.fn().mockReturnThis(),
|
|
518
532
|
limit: vi.fn().mockReturnThis(),
|
|
519
|
-
toArray: vi
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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({
|