codecritique 2.3.0 → 2.3.2
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 +4 -4
- package/src/feedback-loader.js +70 -45
- package/src/feedback-loader.test.js +114 -0
- package/src/rag-analyzer.js +4 -37
- package/src/rag-analyzer.test.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecritique",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.2",
|
|
4
4
|
"description": "AI-powered code review tool for any programming language",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"fastembed": "2.1.0",
|
|
64
64
|
"glob": "13.0.6",
|
|
65
65
|
"linguist-languages": "9.5.0",
|
|
66
|
-
"lru-cache": "11.5.
|
|
66
|
+
"lru-cache": "11.5.2",
|
|
67
67
|
"minimatch": "10.2.5",
|
|
68
68
|
"stopwords-iso": "1.1.0"
|
|
69
69
|
},
|
|
@@ -78,8 +78,8 @@
|
|
|
78
78
|
"@vitest/eslint-plugin": "1.6.27",
|
|
79
79
|
"eslint": "9.39.1",
|
|
80
80
|
"eslint-plugin-import": "2.32.0",
|
|
81
|
-
"globals": "
|
|
82
|
-
"knip": "
|
|
81
|
+
"globals": "17.12.0",
|
|
82
|
+
"knip": "6.36.0",
|
|
83
83
|
"prettier": "3.9.6",
|
|
84
84
|
"prettier-plugin-brace-style": "0.10.3",
|
|
85
85
|
"typescript": "5.9.3",
|
package/src/feedback-loader.js
CHANGED
|
@@ -111,7 +111,6 @@ export async function initializeSemanticSimilarity() {
|
|
|
111
111
|
try {
|
|
112
112
|
embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
113
113
|
await embeddingsSystem.initialize();
|
|
114
|
-
semanticSimilarityInitialized = true;
|
|
115
114
|
semanticSimilarityAvailable = true;
|
|
116
115
|
verboseLog({}, chalk.green('[FeedbackLoader] Semantic similarity initialized using embeddings system'));
|
|
117
116
|
}
|
|
@@ -119,6 +118,11 @@ export async function initializeSemanticSimilarity() {
|
|
|
119
118
|
console.warn(chalk.yellow(`[FeedbackLoader] Semantic similarity initialization failed: ${error.message}`));
|
|
120
119
|
semanticSimilarityAvailable = false;
|
|
121
120
|
}
|
|
121
|
+
finally {
|
|
122
|
+
// Records the attempt, not the outcome. Callers initialize lazily, so a failure
|
|
123
|
+
// that left this unset would reload the model once per issue.
|
|
124
|
+
semanticSimilarityInitialized = true;
|
|
125
|
+
}
|
|
122
126
|
}
|
|
123
127
|
|
|
124
128
|
/**
|
|
@@ -168,74 +172,84 @@ async function calculateSemanticSimilarity(text1, text2) {
|
|
|
168
172
|
// ISSUE SIMILARITY CHECKING
|
|
169
173
|
// ============================================================================
|
|
170
174
|
|
|
175
|
+
/** Score above which an issue counts as a repeat of a dismissed one. */
|
|
176
|
+
export const DEFAULT_SIMILARITY_THRESHOLD = 0.7;
|
|
177
|
+
|
|
178
|
+
// A dismissal on the same line is evidence, not proof: a reworded repeat and a
|
|
179
|
+
// different finding on the same topic score too close together to tell apart. The
|
|
180
|
+
// lower bar catches the reworded repeat while a plainly unrelated finding still posts.
|
|
181
|
+
const SAME_LOCATION_SIMILARITY_THRESHOLD = 0.65;
|
|
182
|
+
|
|
183
|
+
// The word term is what separates a repost from a new finding: semantic scores
|
|
184
|
+
// alone rank unrelated findings above the threshold.
|
|
185
|
+
const SEMANTIC_WEIGHT = 0.7;
|
|
186
|
+
const WORD_WEIGHT = 0.3;
|
|
187
|
+
|
|
171
188
|
/**
|
|
172
|
-
* Check if an issue should be skipped based on previous feedback
|
|
173
|
-
*
|
|
189
|
+
* Check if an issue should be skipped based on previous feedback.
|
|
190
|
+
* Compares against dismissed issues with the combined semantic and word-based score,
|
|
191
|
+
* initializing the embeddings system on first use.
|
|
174
192
|
*
|
|
175
193
|
* @param {string} issueDescription - Description of the current issue
|
|
176
194
|
* @param {Object} feedbackData - Loaded feedback data
|
|
177
195
|
* @param {Object} options - Filtering options
|
|
178
|
-
* @param {number} [options.similarityThreshold=
|
|
196
|
+
* @param {number} [options.similarityThreshold=DEFAULT_SIMILARITY_THRESHOLD] - Threshold for considering issues similar
|
|
179
197
|
* @param {boolean} [options.verbose=false] - Enable verbose progress logging
|
|
180
198
|
* @param {boolean} [options.useSemanticSimilarity=true] - Use semantic similarity when available
|
|
199
|
+
* @param {string|null} [options.filePath=null] - Path of the file the current issue is on
|
|
200
|
+
* @param {number|null} [options.lineNumber=null] - Line the current issue is on
|
|
181
201
|
* @returns {Promise<boolean>} True if issue should be skipped
|
|
182
202
|
*/
|
|
183
203
|
export async function shouldSkipSimilarIssue(issueDescription, feedbackData, options = {}) {
|
|
184
|
-
const {
|
|
204
|
+
const {
|
|
205
|
+
similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD,
|
|
206
|
+
verbose = false,
|
|
207
|
+
useSemanticSimilarity = true,
|
|
208
|
+
filePath = null,
|
|
209
|
+
lineNumber = null,
|
|
210
|
+
} = options;
|
|
185
211
|
|
|
186
212
|
if (!feedbackData || Object.keys(feedbackData).length === 0) {
|
|
187
213
|
return false;
|
|
188
214
|
}
|
|
189
215
|
|
|
190
|
-
//
|
|
216
|
+
// Dismissals worth comparing against: one without its own text can never match.
|
|
191
217
|
const dismissedIssues = Object.values(feedbackData).filter(
|
|
192
218
|
(feedback) =>
|
|
193
|
-
feedback?.
|
|
194
|
-
feedback?.
|
|
195
|
-
(
|
|
196
|
-
reply
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
219
|
+
feedback?.originalIssue &&
|
|
220
|
+
(feedback?.overallSentiment === 'negative' ||
|
|
221
|
+
feedback?.userReplies?.some(
|
|
222
|
+
(reply) =>
|
|
223
|
+
reply.body.toLowerCase().includes('false positive') ||
|
|
224
|
+
reply.body.toLowerCase().includes('not relevant') ||
|
|
225
|
+
reply.body.toLowerCase().includes('ignore') ||
|
|
226
|
+
reply.body.toLowerCase().includes('resolved')
|
|
227
|
+
))
|
|
201
228
|
);
|
|
202
229
|
|
|
203
|
-
if (dismissedIssues.length === 0) {
|
|
230
|
+
if (!issueDescription || dismissedIssues.length === 0) {
|
|
204
231
|
return false;
|
|
205
232
|
}
|
|
206
233
|
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
234
|
+
// Loaded here rather than by the caller: this is the first point where a comparison
|
|
235
|
+
// is certain, and the embedding model is the expensive part of making one.
|
|
236
|
+
if (useSemanticSimilarity) {
|
|
237
|
+
await initializeSemanticSimilarity();
|
|
238
|
+
}
|
|
211
239
|
|
|
212
240
|
// Check similarity with dismissed issues
|
|
213
241
|
for (const dismissed of dismissedIssues) {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
let similarity;
|
|
219
|
-
let similarityMethod;
|
|
242
|
+
const threshold = isSameLocation(dismissed, filePath, lineNumber)
|
|
243
|
+
? Math.min(similarityThreshold, SAME_LOCATION_SIMILARITY_THRESHOLD)
|
|
244
|
+
: similarityThreshold;
|
|
220
245
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
// Fall back to word similarity if semantic calculation failed
|
|
227
|
-
if (similarity === null) {
|
|
228
|
-
similarity = calculateWordSimilarity(issueDescription, dismissed.originalIssue);
|
|
229
|
-
similarityMethod = 'word-based';
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
else {
|
|
233
|
-
// Use word-based similarity
|
|
234
|
-
similarity = calculateWordSimilarity(issueDescription, dismissed.originalIssue);
|
|
235
|
-
similarityMethod = 'word-based';
|
|
236
|
-
}
|
|
246
|
+
// The hybrid score, not the semantic score alone: a normalized semantic score ranks
|
|
247
|
+
// unrelated findings above the threshold, so one dismissal would suppress every issue.
|
|
248
|
+
const { similarity, method: similarityMethod } = await calculateIssueSimilarity(issueDescription, dismissed.originalIssue, {
|
|
249
|
+
useSemanticSimilarity,
|
|
250
|
+
});
|
|
237
251
|
|
|
238
|
-
if (similarity >
|
|
252
|
+
if (similarity > threshold) {
|
|
239
253
|
verboseLog(
|
|
240
254
|
verbose,
|
|
241
255
|
chalk.yellow(`⏭️ Skipping similar dismissed issue (${(similarity * 100).toFixed(1)}% ${similarityMethod} similarity)`)
|
|
@@ -249,6 +263,19 @@ export async function shouldSkipSimilarIssue(issueDescription, feedbackData, opt
|
|
|
249
263
|
return false;
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
/**
|
|
267
|
+
* Check whether a dismissed issue was recorded at the given file and line.
|
|
268
|
+
* A null on either side never matches, so an unknown location never narrows anything.
|
|
269
|
+
*
|
|
270
|
+
* @param {Object} dismissed - A dismissed feedback entry
|
|
271
|
+
* @param {string|null} filePath - Path of the current issue
|
|
272
|
+
* @param {number|null} lineNumber - Line of the current issue
|
|
273
|
+
* @returns {boolean} True when both locations are known and equal
|
|
274
|
+
*/
|
|
275
|
+
function isSameLocation(dismissed, filePath, lineNumber) {
|
|
276
|
+
return Boolean(filePath && lineNumber != null && dismissed.filePath === filePath && dismissed.lineNumber === lineNumber);
|
|
277
|
+
}
|
|
278
|
+
|
|
252
279
|
/**
|
|
253
280
|
* Calculate combined similarity between two issue descriptions.
|
|
254
281
|
* Uses both semantic and word-based similarity for robust comparison.
|
|
@@ -275,9 +302,7 @@ export async function calculateIssueSimilarity(text1, text2, options = {}) {
|
|
|
275
302
|
// Also calculate word similarity for a hybrid score
|
|
276
303
|
const wordSimilarity = calculateWordSimilarity(text1, text2);
|
|
277
304
|
|
|
278
|
-
|
|
279
|
-
// This helps catch both semantically similar and lexically similar issues
|
|
280
|
-
const combinedSimilarity = semanticSimilarity * 0.7 + wordSimilarity * 0.3;
|
|
305
|
+
const combinedSimilarity = semanticSimilarity * SEMANTIC_WEIGHT + wordSimilarity * WORD_WEIGHT;
|
|
281
306
|
|
|
282
307
|
return {
|
|
283
308
|
similarity: combinedSimilarity,
|
|
@@ -527,3 +527,117 @@ describe('loadFeedbackData edge cases', () => {
|
|
|
527
527
|
expect(result).toHaveProperty('item');
|
|
528
528
|
});
|
|
529
529
|
});
|
|
530
|
+
|
|
531
|
+
describe('shouldSkipSimilarIssue without a comparable pair', () => {
|
|
532
|
+
// Nothing to compare means nothing to load the embedding model for.
|
|
533
|
+
let loader;
|
|
534
|
+
let initialize;
|
|
535
|
+
|
|
536
|
+
beforeEach(async () => {
|
|
537
|
+
mockConsoleSelective('log', 'warn');
|
|
538
|
+
vi.resetModules();
|
|
539
|
+
initialize = vi.fn().mockResolvedValue(undefined);
|
|
540
|
+
|
|
541
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
542
|
+
const freshFactory = await import('./embeddings/factory.js');
|
|
543
|
+
freshFactory.getDefaultEmbeddingsSystem.mockReturnValue({ initialize, calculateEmbedding: vi.fn() });
|
|
544
|
+
|
|
545
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
546
|
+
loader = await import('./feedback-loader.js');
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
it.each([
|
|
550
|
+
['the only dismissal has no text of its own', 'a candidate description', { i1: { overallSentiment: 'negative', originalIssue: '' } }],
|
|
551
|
+
['the candidate has no description', '', { i1: { overallSentiment: 'negative', originalIssue: 'a dismissed finding' } }],
|
|
552
|
+
])('should not initialize the embeddings system when %s', async (_name, description, feedbackData) => {
|
|
553
|
+
await expect(loader.shouldSkipSimilarIssue(description, feedbackData)).resolves.toBe(false);
|
|
554
|
+
|
|
555
|
+
expect(initialize).not.toHaveBeenCalled();
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
describe('shouldSkipSimilarIssue location match', () => {
|
|
560
|
+
// Word similarity 0.667: above the same-location bar of 0.65, below the 0.9 passed here.
|
|
561
|
+
// Only the location can decide these cases.
|
|
562
|
+
const SHARED = 'alpha bravo charlie delta echo foxtrot golf hotel';
|
|
563
|
+
const DISMISSED = `${SHARED} india juliett`;
|
|
564
|
+
const REWORDED = `${SHARED} kilo lima`;
|
|
565
|
+
const UNRELATED = 'mike november oscar papa quebec';
|
|
566
|
+
const LOCATION = { filePath: '.github/workflows/pr-review.yml', lineNumber: 51 };
|
|
567
|
+
|
|
568
|
+
const feedbackData = {
|
|
569
|
+
issue1: {
|
|
570
|
+
overallSentiment: 'neutral',
|
|
571
|
+
originalIssue: DISMISSED,
|
|
572
|
+
userReplies: [{ body: 'False positive.' }],
|
|
573
|
+
...LOCATION,
|
|
574
|
+
},
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const check = (description, options) =>
|
|
578
|
+
shouldSkipSimilarIssue(description, feedbackData, { similarityThreshold: 0.9, useSemanticSimilarity: false, ...options });
|
|
579
|
+
|
|
580
|
+
it.each([
|
|
581
|
+
['a reworded repeat at the same location', REWORDED, LOCATION, true],
|
|
582
|
+
['a reworded repeat on a different line', REWORDED, { ...LOCATION, lineNumber: 52 }, false],
|
|
583
|
+
['an unrelated finding at the same location', UNRELATED, LOCATION, false],
|
|
584
|
+
['a reworded repeat with no known line', REWORDED, { ...LOCATION, lineNumber: null }, false],
|
|
585
|
+
])('should %s -> %s', async (_name, description, options, expected) => {
|
|
586
|
+
await expect(check(description, options)).resolves.toBe(expected);
|
|
587
|
+
});
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
describe('shouldSkipSimilarIssue with semantic similarity available', () => {
|
|
591
|
+
// Unit vectors whose cosine similarity is exact: [1, 0] against [cos, sin].
|
|
592
|
+
const vectorFor = (cosine) => [cosine, Math.sqrt(1 - cosine * cosine)];
|
|
593
|
+
const DISMISSED = 'Missing null check in the handler function';
|
|
594
|
+
const REPHRASED = 'The handler function does not guard against a null argument';
|
|
595
|
+
const UNRELATED = 'Prefer const over let for bindings that never change';
|
|
596
|
+
|
|
597
|
+
let loader;
|
|
598
|
+
|
|
599
|
+
beforeEach(async () => {
|
|
600
|
+
mockConsoleSelective('log', 'warn');
|
|
601
|
+
vi.resetModules();
|
|
602
|
+
|
|
603
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
604
|
+
const freshFactory = await import('./embeddings/factory.js');
|
|
605
|
+
freshFactory.getDefaultEmbeddingsSystem.mockReturnValue({
|
|
606
|
+
initialize: vi.fn().mockResolvedValue(undefined),
|
|
607
|
+
calculateEmbedding: vi.fn(async (text) => {
|
|
608
|
+
if (text === DISMISSED) {
|
|
609
|
+
return vectorFor(1);
|
|
610
|
+
}
|
|
611
|
+
return vectorFor(text === REPHRASED ? 0.98 : 0.8);
|
|
612
|
+
}),
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// eslint-disable-next-line no-restricted-syntax
|
|
616
|
+
loader = await import('./feedback-loader.js');
|
|
617
|
+
await loader.initializeSemanticSimilarity();
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should skip a dismissed issue that the model rephrased', async () => {
|
|
621
|
+
const feedbackData = {
|
|
622
|
+
issue1: {
|
|
623
|
+
overallSentiment: 'neutral',
|
|
624
|
+
originalIssue: DISMISSED,
|
|
625
|
+
userReplies: [{ body: 'False positive.' }],
|
|
626
|
+
},
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
await expect(loader.shouldSkipSimilarIssue(REPHRASED, feedbackData, { similarityThreshold: 0.7 })).resolves.toBe(true);
|
|
630
|
+
});
|
|
631
|
+
|
|
632
|
+
it('should not skip an unrelated issue once another issue was dismissed', async () => {
|
|
633
|
+
const feedbackData = {
|
|
634
|
+
issue1: {
|
|
635
|
+
overallSentiment: 'neutral',
|
|
636
|
+
originalIssue: DISMISSED,
|
|
637
|
+
userReplies: [{ body: 'False positive.' }],
|
|
638
|
+
},
|
|
639
|
+
};
|
|
640
|
+
|
|
641
|
+
await expect(loader.shouldSkipSimilarIssue(UNRELATED, feedbackData, { similarityThreshold: 0.7 })).resolves.toBe(false);
|
|
642
|
+
});
|
|
643
|
+
});
|
package/src/rag-analyzer.js
CHANGED
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
shouldSkipSimilarIssue,
|
|
17
17
|
extractDismissedPatterns,
|
|
18
18
|
generateFeedbackContext,
|
|
19
|
-
|
|
19
|
+
DEFAULT_SIMILARITY_THRESHOLD,
|
|
20
20
|
isSemanticSimilarityAvailable,
|
|
21
21
|
} from './feedback-loader.js';
|
|
22
22
|
import * as llm from './llm.js';
|
|
@@ -37,9 +37,6 @@ const DEFAULT_TRUNCATE_LINES = 300;
|
|
|
37
37
|
const GUIDELINE_TRUNCATE_LINES = 400;
|
|
38
38
|
const MAX_PR_COMMENTS_FOR_CONTEXT = 15;
|
|
39
39
|
|
|
40
|
-
// Track if semantic similarity has been initialized
|
|
41
|
-
let semanticSimilarityInitialized = false;
|
|
42
|
-
|
|
43
40
|
async function fileExists(filePath) {
|
|
44
41
|
try {
|
|
45
42
|
await fs.promises.access(filePath);
|
|
@@ -54,26 +51,6 @@ async function readTextFile(filePath) {
|
|
|
54
51
|
return await fs.promises.readFile(filePath, 'utf8');
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
/**
|
|
58
|
-
* Initialize semantic similarity for feedback filtering
|
|
59
|
-
* Uses the shared embeddings system from feedback-loader.js
|
|
60
|
-
*/
|
|
61
|
-
async function ensureSemanticSimilarityInitialized() {
|
|
62
|
-
if (semanticSimilarityInitialized) {
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
// Initialize semantic similarity using the shared embeddings system
|
|
68
|
-
await initializeSemanticSimilarity();
|
|
69
|
-
semanticSimilarityInitialized = true;
|
|
70
|
-
}
|
|
71
|
-
catch (error) {
|
|
72
|
-
console.warn(chalk.yellow(`⚠️ Could not initialize semantic similarity: ${error.message}`));
|
|
73
|
-
// Continue without semantic similarity - word-based fallback will be used
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
54
|
// ============================================================================
|
|
78
55
|
// PROMPT FORMATTING HELPERS
|
|
79
56
|
// Note: Static prompt content (critical rules, citation requirements, etc.) is
|
|
@@ -578,7 +555,7 @@ async function runAnalysis(filePath, options = {}) {
|
|
|
578
555
|
if (options.trackFeedback && feedbackData && Object.keys(feedbackData).length > 0) {
|
|
579
556
|
verboseLog(options, chalk.cyan('--- Filtering Results Based on Feedback ---'));
|
|
580
557
|
filteredResults = await filterAnalysisResults(lowSeverityFiltered, feedbackData, {
|
|
581
|
-
similarityThreshold: options.feedbackThreshold ||
|
|
558
|
+
similarityThreshold: options.feedbackThreshold || DEFAULT_SIMILARITY_THRESHOLD,
|
|
582
559
|
verbose: options.verbose,
|
|
583
560
|
});
|
|
584
561
|
}
|
|
@@ -2491,12 +2468,12 @@ function filterLowSeverityIssues(analysisResults, options = {}) {
|
|
|
2491
2468
|
* @param {Object} analysisResults - Raw analysis results from LLM
|
|
2492
2469
|
* @param {Object} feedbackData - Loaded feedback data
|
|
2493
2470
|
* @param {Object} options - Filtering options
|
|
2494
|
-
* @param {number} [options.similarityThreshold=
|
|
2471
|
+
* @param {number} [options.similarityThreshold=DEFAULT_SIMILARITY_THRESHOLD] - Threshold for considering issues similar to dismissed feedback
|
|
2495
2472
|
* @param {boolean} [options.verbose=false] - Enable verbose similarity and filtering logs
|
|
2496
2473
|
* @returns {Promise<Object>} Filtered analysis results
|
|
2497
2474
|
*/
|
|
2498
2475
|
async function filterAnalysisResults(analysisResults, feedbackData, options = {}) {
|
|
2499
|
-
const { similarityThreshold =
|
|
2476
|
+
const { similarityThreshold = DEFAULT_SIMILARITY_THRESHOLD, verbose = false } = options;
|
|
2500
2477
|
|
|
2501
2478
|
if (!analysisResults || !analysisResults.issues || !Array.isArray(analysisResults.issues)) {
|
|
2502
2479
|
return analysisResults;
|
|
@@ -2504,16 +2481,6 @@ async function filterAnalysisResults(analysisResults, feedbackData, options = {}
|
|
|
2504
2481
|
|
|
2505
2482
|
const originalCount = analysisResults.issues.length;
|
|
2506
2483
|
|
|
2507
|
-
// Ensure semantic similarity is initialized for better matching
|
|
2508
|
-
await ensureSemanticSimilarityInitialized();
|
|
2509
|
-
|
|
2510
|
-
// Log whether semantic similarity is available
|
|
2511
|
-
const usingSemanticSimilarity = isSemanticSimilarityAvailable();
|
|
2512
|
-
verboseLog(
|
|
2513
|
-
verbose,
|
|
2514
|
-
chalk.cyan(`🔍 Filtering issues using ${usingSemanticSimilarity ? 'semantic + word-based similarity' : 'word-based similarity only'}`)
|
|
2515
|
-
);
|
|
2516
|
-
|
|
2517
2484
|
// Filter issues based on feedback (now async due to semantic similarity)
|
|
2518
2485
|
const filterResults = await Promise.all(
|
|
2519
2486
|
analysisResults.issues.map(async (issue, index) => {
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -59,6 +59,7 @@ vi.mock('./feedback-loader.js', () => ({
|
|
|
59
59
|
generateFeedbackContext: vi.fn().mockReturnValue(''),
|
|
60
60
|
initializeSemanticSimilarity: vi.fn().mockResolvedValue(undefined),
|
|
61
61
|
isSemanticSimilarityAvailable: vi.fn().mockReturnValue(false),
|
|
62
|
+
DEFAULT_SIMILARITY_THRESHOLD: 0.7,
|
|
62
63
|
}));
|
|
63
64
|
|
|
64
65
|
vi.mock('./llm.js', () => ({
|