codecritique 1.2.4 → 2.0.1
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 +38 -26
- package/src/custom-documents.js +29 -14
- package/src/feedback-loader.js +19 -8
- package/src/index.js +95 -45
- package/src/llm.js +7 -3
- package/src/project-analyzer.js +56 -31
- package/src/project-analyzer.test.js +8 -0
- package/src/rag-analyzer.js +70 -34
- package/src/rag-analyzer.test.js +3 -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.1",
|
|
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,6 +23,7 @@ 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';
|
|
27
28
|
import { isPathWithinProject } from './utils/path-utils.js';
|
|
28
29
|
import { escapeSqlString } from './utils/string-utils.js';
|
|
@@ -93,7 +94,8 @@ export class ContentRetriever {
|
|
|
93
94
|
try {
|
|
94
95
|
await fs.promises.access(absolutePath, fs.constants.F_OK);
|
|
95
96
|
return { index, exists: true };
|
|
96
|
-
}
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
97
99
|
debug(`Filtering out non-existent project file: ${resultPath}`);
|
|
98
100
|
return { index, exists: false };
|
|
99
101
|
}
|
|
@@ -151,12 +153,13 @@ export class ContentRetriever {
|
|
|
151
153
|
|
|
152
154
|
const resolvedProjectPath = path.resolve(projectPath);
|
|
153
155
|
try {
|
|
154
|
-
const tableSchema = await table
|
|
155
|
-
if (tableSchema
|
|
156
|
+
const tableSchema = await getTableSchema(table);
|
|
157
|
+
if (schemaHasField(tableSchema, 'project_path')) {
|
|
156
158
|
query = query.where(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
|
|
157
159
|
debug(`Filtering documentation by project_path: ${resolvedProjectPath}`);
|
|
158
160
|
}
|
|
159
|
-
}
|
|
161
|
+
}
|
|
162
|
+
catch (schemaError) {
|
|
160
163
|
debug(`Could not check schema for project_path field: ${schemaError.message}`);
|
|
161
164
|
}
|
|
162
165
|
|
|
@@ -174,9 +177,11 @@ export class ContentRetriever {
|
|
|
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')`
|
|
@@ -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 = '${escapeSqlString(resolvedProjectPath)}'`);
|
|
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
|
}
|
|
@@ -548,10 +555,12 @@ export class ContentRetriever {
|
|
|
548
555
|
// Vector search distance (0 = perfect match, higher = less similar)
|
|
549
556
|
// Apply more precise normalization to avoid all scores being 1.000
|
|
550
557
|
similarity = Math.max(0, Math.min(1, Math.exp(-result._distance * 2)));
|
|
551
|
-
}
|
|
558
|
+
}
|
|
559
|
+
else if (result._score !== undefined) {
|
|
552
560
|
// FTS or hybrid score - normalize to 0-1 range with better scaling
|
|
553
561
|
similarity = Math.max(0, Math.min(1, result._score / Math.max(result._score, 1)));
|
|
554
|
-
}
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
555
564
|
// Fallback
|
|
556
565
|
similarity = 0.5;
|
|
557
566
|
}
|
|
@@ -612,7 +621,8 @@ export class ContentRetriever {
|
|
|
612
621
|
}
|
|
613
622
|
}
|
|
614
623
|
}
|
|
615
|
-
}
|
|
624
|
+
}
|
|
625
|
+
catch (error) {
|
|
616
626
|
console.warn(chalk.yellow(`Project structure inclusion failed: ${error.message}`));
|
|
617
627
|
}
|
|
618
628
|
}
|
|
@@ -625,7 +635,8 @@ export class ContentRetriever {
|
|
|
625
635
|
|
|
626
636
|
verboseLog(options, chalk.green(`Returning ${finalResults.length} optimized hybrid search results`));
|
|
627
637
|
return finalResults;
|
|
628
|
-
}
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
629
640
|
console.error(chalk.red(`Error in optimized findSimilarCode: ${error.message}`), error);
|
|
630
641
|
return [];
|
|
631
642
|
}
|
|
@@ -680,7 +691,8 @@ export class ContentRetriever {
|
|
|
680
691
|
};
|
|
681
692
|
|
|
682
693
|
verboseLog({}, chalk.green('ContentRetriever cleanup complete'));
|
|
683
|
-
}
|
|
694
|
+
}
|
|
695
|
+
finally {
|
|
684
696
|
this.cleaningUp = false;
|
|
685
697
|
}
|
|
686
698
|
}
|
package/src/custom-documents.js
CHANGED
|
@@ -70,7 +70,8 @@ export class CustomDocumentProcessor {
|
|
|
70
70
|
const headerMatch = content.match(/^#\s+(.+)$/m);
|
|
71
71
|
if (headerMatch) {
|
|
72
72
|
documentTitle = headerMatch[1].trim();
|
|
73
|
-
}
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
74
75
|
// If no header found, try to extract filename from title like "instruction:./FILENAME.md"
|
|
75
76
|
const filePathMatch = title.match(/:\.\/([^/]+)\.([a-zA-Z]+)$/);
|
|
76
77
|
if (filePathMatch) {
|
|
@@ -88,7 +89,9 @@ export class CustomDocumentProcessor {
|
|
|
88
89
|
|
|
89
90
|
for (let i = 0; i < sections.length; i++) {
|
|
90
91
|
const section = sections[i].trim();
|
|
91
|
-
if (!section)
|
|
92
|
+
if (!section) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
92
95
|
|
|
93
96
|
// Check if adding this section would exceed max chunk size
|
|
94
97
|
if (currentChunk.length + section.length > maxChunkSize && currentChunk.length > minChunkSize) {
|
|
@@ -108,7 +111,8 @@ export class CustomDocumentProcessor {
|
|
|
108
111
|
|
|
109
112
|
chunkIndex++;
|
|
110
113
|
currentChunk = section;
|
|
111
|
-
}
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
112
116
|
// Add section to current chunk
|
|
113
117
|
currentChunk += (currentChunk ? '\n\n' : '') + section;
|
|
114
118
|
}
|
|
@@ -142,7 +146,8 @@ export class CustomDocumentProcessor {
|
|
|
142
146
|
|
|
143
147
|
verboseLog({}, chalk.gray(` Chunked document "${documentTitle}" into ${chunks.length} chunks`));
|
|
144
148
|
return chunks;
|
|
145
|
-
}
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
146
151
|
console.error(chalk.red(`Error chunking document: ${error.message}`));
|
|
147
152
|
throw new EmbeddingError(`Document chunking failed: ${error.message}`);
|
|
148
153
|
}
|
|
@@ -195,7 +200,8 @@ export class CustomDocumentProcessor {
|
|
|
195
200
|
project_path: path.resolve(projectPath),
|
|
196
201
|
created_at: new Date().toISOString(),
|
|
197
202
|
};
|
|
198
|
-
}
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
199
205
|
console.error(chalk.red(`Error generating embedding for chunk ${chunk.id}: batch processing failed`));
|
|
200
206
|
return null;
|
|
201
207
|
}
|
|
@@ -207,7 +213,8 @@ export class CustomDocumentProcessor {
|
|
|
207
213
|
|
|
208
214
|
verboseLog({}, chalk.gray(` Generated embeddings for ${validChunks.length}/${chunks.length} chunks`));
|
|
209
215
|
this.performanceMetrics.embeddingsCalculated += validChunks.length;
|
|
210
|
-
}
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
211
218
|
console.error(chalk.red(`Error in batch embedding generation for document ${doc.title}: ${error.message}`));
|
|
212
219
|
// Fallback to individual processing for this document
|
|
213
220
|
verboseLog({}, chalk.yellow(` Falling back to individual processing for ${doc.title}`));
|
|
@@ -225,7 +232,8 @@ export class CustomDocumentProcessor {
|
|
|
225
232
|
project_path: path.resolve(projectPath),
|
|
226
233
|
created_at: new Date().toISOString(),
|
|
227
234
|
};
|
|
228
|
-
}
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
229
237
|
console.error(chalk.red(`Error generating embedding for chunk ${chunk.id}: ${error.message}`));
|
|
230
238
|
return null;
|
|
231
239
|
}
|
|
@@ -254,7 +262,8 @@ export class CustomDocumentProcessor {
|
|
|
254
262
|
|
|
255
263
|
verboseLog({}, chalk.green(`Successfully processed ${allChunks.length} custom document chunks (${Date.now() - startTime}ms)`));
|
|
256
264
|
return allChunks;
|
|
257
|
-
}
|
|
265
|
+
}
|
|
266
|
+
catch (error) {
|
|
258
267
|
console.error(chalk.red(`Error processing custom documents: ${error.message}`));
|
|
259
268
|
throw new EmbeddingError(`Custom document processing failed: ${error.message}`);
|
|
260
269
|
}
|
|
@@ -327,7 +336,8 @@ export class CustomDocumentProcessor {
|
|
|
327
336
|
}
|
|
328
337
|
|
|
329
338
|
return filteredResults;
|
|
330
|
-
}
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
331
341
|
console.error(chalk.red(`Error searching custom document chunks: ${error.message}`));
|
|
332
342
|
throw new EmbeddingError(`Custom document search failed: ${error.message}`);
|
|
333
343
|
}
|
|
@@ -360,7 +370,8 @@ export class CustomDocumentProcessor {
|
|
|
360
370
|
|
|
361
371
|
debug(`[getExistingChunks] No existing chunks found for project: ${resolvedProjectPath}`);
|
|
362
372
|
return [];
|
|
363
|
-
}
|
|
373
|
+
}
|
|
374
|
+
catch (error) {
|
|
364
375
|
debug(`[getExistingChunks] Error checking existing chunks: ${error.message}`);
|
|
365
376
|
return [];
|
|
366
377
|
}
|
|
@@ -419,7 +430,8 @@ export class CustomDocumentProcessor {
|
|
|
419
430
|
contextMatchBonus += MODERATE_BOOST_TECH_MATCH;
|
|
420
431
|
}
|
|
421
432
|
}
|
|
422
|
-
}
|
|
433
|
+
}
|
|
434
|
+
else if (queryArea !== 'GeneralJS_TS') {
|
|
423
435
|
contextMatchBonus += HEAVY_PENALTY_AREA_MISMATCH;
|
|
424
436
|
}
|
|
425
437
|
}
|
|
@@ -505,7 +517,8 @@ export class CustomDocumentProcessor {
|
|
|
505
517
|
this.h1EmbeddingCache.set(docTitlesToCalculate[i], titleEmbeddings[i]);
|
|
506
518
|
}
|
|
507
519
|
}
|
|
508
|
-
}
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
509
522
|
debug(`[OPTIMIZATION] Error in batch title embedding calculation: ${error.message}`);
|
|
510
523
|
// Continue without title embeddings
|
|
511
524
|
}
|
|
@@ -522,7 +535,8 @@ export class CustomDocumentProcessor {
|
|
|
522
535
|
this.customDocumentChunks.delete(resolvedProjectPath);
|
|
523
536
|
this.cacheManager.customDocumentChunks.delete(resolvedProjectPath);
|
|
524
537
|
verboseLog({}, chalk.green(`Cleared custom document chunks for project: ${resolvedProjectPath}`));
|
|
525
|
-
}
|
|
538
|
+
}
|
|
539
|
+
catch (error) {
|
|
526
540
|
console.error(chalk.red(`Error clearing project chunks: ${error.message}`));
|
|
527
541
|
}
|
|
528
542
|
}
|
|
@@ -590,7 +604,8 @@ export class CustomDocumentProcessor {
|
|
|
590
604
|
};
|
|
591
605
|
|
|
592
606
|
verboseLog({}, chalk.green('CustomDocumentProcessor cleanup complete'));
|
|
593
|
-
}
|
|
607
|
+
}
|
|
608
|
+
finally {
|
|
594
609
|
this.cleaningUp = false;
|
|
595
610
|
}
|
|
596
611
|
}
|
package/src/feedback-loader.js
CHANGED
|
@@ -68,7 +68,8 @@ export async function loadFeedbackData(feedbackPath, options = {}) {
|
|
|
68
68
|
totalItems += itemCount;
|
|
69
69
|
verboseLog(verbose, chalk.cyan(`📋 Loaded feedback from ${file}: ${itemCount} items`));
|
|
70
70
|
}
|
|
71
|
-
}
|
|
71
|
+
}
|
|
72
|
+
catch (parseError) {
|
|
72
73
|
console.warn(chalk.yellow(`⚠️ Error parsing feedback file ${file}: ${parseError.message}`));
|
|
73
74
|
}
|
|
74
75
|
}
|
|
@@ -79,7 +80,8 @@ export async function loadFeedbackData(feedbackPath, options = {}) {
|
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
return {};
|
|
82
|
-
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
83
85
|
console.error(chalk.red(`❌ Error loading feedback data: ${error.message}`));
|
|
84
86
|
return {};
|
|
85
87
|
}
|
|
@@ -112,7 +114,8 @@ export async function initializeSemanticSimilarity() {
|
|
|
112
114
|
semanticSimilarityInitialized = true;
|
|
113
115
|
semanticSimilarityAvailable = true;
|
|
114
116
|
verboseLog({}, chalk.green('[FeedbackLoader] Semantic similarity initialized using embeddings system'));
|
|
115
|
-
}
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
116
119
|
console.warn(chalk.yellow(`[FeedbackLoader] Semantic similarity initialization failed: ${error.message}`));
|
|
117
120
|
semanticSimilarityAvailable = false;
|
|
118
121
|
}
|
|
@@ -154,7 +157,8 @@ async function calculateSemanticSimilarity(text1, text2) {
|
|
|
154
157
|
const similarity = calculateCosineSimilarity(embedding1, embedding2);
|
|
155
158
|
// Cosine similarity ranges from -1 to 1, normalize to 0-1
|
|
156
159
|
return (similarity + 1) / 2;
|
|
157
|
-
}
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
158
162
|
console.warn(chalk.yellow(`[FeedbackLoader] Semantic similarity calculation failed: ${error.message}`));
|
|
159
163
|
return null;
|
|
160
164
|
}
|
|
@@ -207,7 +211,9 @@ export async function shouldSkipSimilarIssue(issueDescription, feedbackData, opt
|
|
|
207
211
|
|
|
208
212
|
// Check similarity with dismissed issues
|
|
209
213
|
for (const dismissed of dismissedIssues) {
|
|
210
|
-
if (!dismissed.originalIssue)
|
|
214
|
+
if (!dismissed.originalIssue) {
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
211
217
|
|
|
212
218
|
let similarity;
|
|
213
219
|
let similarityMethod;
|
|
@@ -222,7 +228,8 @@ export async function shouldSkipSimilarIssue(issueDescription, feedbackData, opt
|
|
|
222
228
|
similarity = calculateWordSimilarity(issueDescription, dismissed.originalIssue);
|
|
223
229
|
similarityMethod = 'word-based';
|
|
224
230
|
}
|
|
225
|
-
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
226
233
|
// Use word-based similarity
|
|
227
234
|
similarity = calculateWordSimilarity(issueDescription, dismissed.originalIssue);
|
|
228
235
|
similarityMethod = 'word-based';
|
|
@@ -297,7 +304,9 @@ export async function calculateIssueSimilarity(text1, text2, options = {}) {
|
|
|
297
304
|
* @returns {number} Similarity score (0-1)
|
|
298
305
|
*/
|
|
299
306
|
export function calculateWordSimilarity(text1, text2) {
|
|
300
|
-
if (!text1 || !text2)
|
|
307
|
+
if (!text1 || !text2) {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
301
310
|
|
|
302
311
|
// Normalize and tokenize
|
|
303
312
|
const normalize = (text) =>
|
|
@@ -310,7 +319,9 @@ export function calculateWordSimilarity(text1, text2) {
|
|
|
310
319
|
const words1 = new Set(normalize(text1));
|
|
311
320
|
const words2 = new Set(normalize(text2));
|
|
312
321
|
|
|
313
|
-
if (words1.size === 0 || words2.size === 0)
|
|
322
|
+
if (words1.size === 0 || words2.size === 0) {
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
314
325
|
|
|
315
326
|
// Calculate Jaccard similarity (intersection over union)
|
|
316
327
|
const intersection = [...words1].filter((word) => words2.has(word)).length;
|