codecritique 2.0.0 → 2.0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecritique",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "AI-powered code review tool for any programming language",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -14,6 +14,7 @@ import * as llm from './llm.js';
14
14
  import { FILE_SELECTION_SYSTEM_PROMPT, PROJECT_SUMMARY_SYSTEM_PROMPT } from './prompt-cache.js';
15
15
  import { isDocumentationFile, isTestFile } from './utils/file-validation.js';
16
16
  import { verboseLog } from './utils/logging.js';
17
+ import { escapeSqlString } from './utils/string-utils.js';
17
18
 
18
19
  // Consolidated file classification configuration
19
20
  const FILE_PATTERNS = {
@@ -377,6 +378,7 @@ export class ProjectAnalyzer {
377
378
 
378
379
  try {
379
380
  verboseLog({}, chalk.gray(` 📊 Using LanceDB hybrid search for project: ${projectPath}`));
381
+ const projectPathFilter = `project_path = '${escapeSqlString(projectPath)}'`;
380
382
 
381
383
  // Unified query function
382
384
  const queryFiles = async (config) => {
@@ -384,15 +386,11 @@ export class ProjectAnalyzer {
384
386
  let query = table.query().select(['path', 'name', 'content', 'type', 'language']);
385
387
 
386
388
  if (config.whereClause) {
387
- query = query.where(`project_path = '${projectPath}' AND (${config.whereClause})`);
389
+ query = query.where(`${projectPathFilter} AND (${config.whereClause})`);
388
390
  }
389
391
  else if (config.terms) {
390
392
  // For term-based searches, query ALL files and sort by depth to prioritize shallow config files
391
- const allFiles = await table
392
- .query()
393
- .select(['path', 'name', 'content', 'type', 'language'])
394
- .where(`project_path = '${projectPath}'`)
395
- .toArray(); // NO LIMIT - get all files
393
+ const allFiles = await table.query().select(['path', 'name', 'content', 'type', 'language']).where(projectPathFilter).toArray(); // NO LIMIT - get all files
396
394
 
397
395
  // Sort by path depth (shorter paths first) to prioritize config files
398
396
  allFiles.sort((a, b) => {
@@ -416,7 +414,7 @@ export class ProjectAnalyzer {
416
414
  });
417
415
  }
418
416
  else {
419
- query = query.where(`project_path = '${projectPath}'`);
417
+ query = query.where(projectPathFilter);
420
418
  }
421
419
 
422
420
  return await query.limit(config.limit || 30).toArray();
@@ -655,7 +653,7 @@ Select files following the criteria in the system instructions.`;
655
653
  const records = await table
656
654
  .query()
657
655
  .select(['type', 'path', 'content_hash', 'project_path'])
658
- .where(`project_path = '${projectPath.replace(/'/g, "''")}'`)
656
+ .where(`project_path = '${escapeSqlString(projectPath)}'`)
659
657
  .toArray();
660
658
 
661
659
  const hash = crypto.createHash('sha256');
@@ -402,6 +402,14 @@ describe('ProjectAnalyzer', () => {
402
402
  expect(mockTable.query).toHaveBeenCalled();
403
403
  });
404
404
 
405
+ it('should escape project paths in key file queries', async () => {
406
+ const queryChain = mockTable.query();
407
+
408
+ await analyzer.mineKeyFilesFromEmbeddings("/mock/we're/project");
409
+
410
+ expect(queryChain.where).toHaveBeenCalledWith(expect.stringContaining("/mock/we''re/project"));
411
+ });
412
+
405
413
  it('should handle table optimization errors gracefully', async () => {
406
414
  mockTable.optimize.mockRejectedValue(new Error('legacy format'));
407
415
  mockTable.query().toArray.mockResolvedValue([]);
@@ -722,7 +722,7 @@ function prepareContextForLLM(filePath, content, language, finalCodeExamples, fi
722
722
  context: contextSections,
723
723
  codeExamples,
724
724
  guidelineSnippets,
725
- customDocs: relevantCustomDocChunks || customDocs, // Use relevant chunks if available, fallback to full docs
725
+ customDocs: relevantCustomDocChunks?.length > 0 ? relevantCustomDocChunks : customDocs, // Use relevant chunks if available, fallback to full docs
726
726
  feedbackContext: generateFeedbackContext(dismissedPatterns), // Add feedback context for LLM
727
727
  projectSummary: projectSummary, // Add project architecture summary
728
728
  metadata: {
@@ -1729,7 +1729,12 @@ async function performHolisticPRAnalysis(options) {
1729
1729
  items: unifiedContext.prComments.slice(0, 10),
1730
1730
  },
1731
1731
  ],
1732
- customDocs: unifiedContext.customDocChunks || options.relevantCustomDocChunks || customDocs, // Use unified chunks first, then relevant chunks, then full docs
1732
+ customDocs:
1733
+ unifiedContext.customDocChunks?.length > 0
1734
+ ? unifiedContext.customDocChunks
1735
+ : options.relevantCustomDocChunks?.length > 0
1736
+ ? options.relevantCustomDocChunks
1737
+ : customDocs, // Use chunks only when available, then fall back to full docs
1733
1738
  projectSummary: projectSummary, // Add project architecture summary
1734
1739
  metadata: {
1735
1740
  hasCodeExamples: unifiedContext.codeExamples.length > 0,
@@ -762,6 +762,19 @@ describe('rag-analyzer', () => {
762
762
  expect(result.success).toBe(true);
763
763
  });
764
764
 
765
+ it('should fall back to full custom docs when no relevant chunks are found', async () => {
766
+ mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([
767
+ { id: 'existing', content: 'Existing indexed chunk', document_title: 'Indexed Doc', chunk_index: 0 },
768
+ ]);
769
+ mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([]);
770
+
771
+ await runAnalysis('/test/file.js', {
772
+ customDocs: [{ content: 'Mandatory internal review rule', document_title: 'Internal Standards' }],
773
+ });
774
+
775
+ expect(llm.sendPromptToClaude).toHaveBeenCalledWith(expect.stringContaining('Mandatory internal review rule'), expect.any(Object));
776
+ });
777
+
765
778
  it('should log selected chunks when verbose', async () => {
766
779
  mockEmbeddingsSystem.findRelevantCustomDocChunks.mockResolvedValue([
767
780
  { id: 'chunk1', content: 'Guidelines', document_title: 'Guide', chunk_index: 0, similarity: 0.85 },
package/src/rag-review.js CHANGED
@@ -230,7 +230,17 @@ async function reviewPullRequestWithCrossFileContext(filesToReview, options = {}
230
230
  verboseLog(verbose, chalk.gray(`Base branch: ${baseBranch}, Target branch: ${targetBranch}`));
231
231
 
232
232
  const prFiles = [];
233
- for (const filePath of filesToReview) {
233
+ if (options.preloadedPRFiles?.length > 0) {
234
+ prFiles.push(
235
+ ...options.preloadedPRFiles.map((file) => ({
236
+ ...file,
237
+ baseBranch: file.baseBranch || baseBranch,
238
+ targetBranch: file.targetBranch || targetBranch,
239
+ }))
240
+ );
241
+ }
242
+
243
+ for (const filePath of options.preloadedPRFiles?.length > 0 ? [] : filesToReview) {
234
244
  try {
235
245
  // Check if the file should be processed before fetching its content from git
236
246
  if (!shouldProcessFile(filePath, '', options)) {
@@ -596,6 +606,7 @@ async function reviewPRChunk(chunk, sharedContext, options, chunkNumber, totalCh
596
606
 
597
607
  // Skip chunking decision for chunk reviews to prevent infinite recursion
598
608
  const skipChunkingOptions = { ...chunkOptions, skipChunking: true };
609
+ skipChunkingOptions.preloadedPRFiles = chunk.files;
599
610
 
600
611
  return await reviewPullRequestWithCrossFileContext(chunkFilePaths, skipChunkingOptions);
601
612
  }