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 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.4",
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.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,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
- } catch {
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.schema;
155
- if (tableSchema?.fields?.some((field) => field.name === 'project_path')) {
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
- } catch (schemaError) {
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
- } 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')`
@@ -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 = '${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
- } 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
  }
@@ -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
- } else if (result._score !== undefined) {
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
- } else {
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
- } catch (error) {
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
- } catch (error) {
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
- } finally {
694
+ }
695
+ finally {
684
696
  this.cleaningUp = false;
685
697
  }
686
698
  }
@@ -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
- } else {
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) continue;
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
- } else {
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
- } catch (error) {
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
- } else {
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
- } catch (error) {
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
- } catch (error) {
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
- } catch (error) {
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
- } catch (error) {
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
- } catch (error) {
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
- } else if (queryArea !== 'GeneralJS_TS') {
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
- } catch (error) {
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
- } catch (error) {
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
- } finally {
607
+ }
608
+ finally {
594
609
  this.cleaningUp = false;
595
610
  }
596
611
  }
@@ -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
- } catch (parseError) {
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
- } catch (error) {
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
- } catch (error) {
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
- } catch (error) {
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) continue;
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
- } else {
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) return 0;
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) return 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;