codecritique 1.2.3 → 1.2.4
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 +1 -1
- package/src/content-retrieval.js +73 -137
- package/src/content-retrieval.test.js +49 -9
- package/src/index.js +2 -3
- package/src/project-analyzer.js +44 -5
- package/src/rag-analyzer.test.js +9 -0
package/package.json
CHANGED
package/src/content-retrieval.js
CHANGED
|
@@ -24,6 +24,8 @@ 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
26
|
import { debug, verboseLog } from './utils/logging.js';
|
|
27
|
+
import { isPathWithinProject } from './utils/path-utils.js';
|
|
28
|
+
import { escapeSqlString } from './utils/string-utils.js';
|
|
27
29
|
|
|
28
30
|
const FILE_EMBEDDINGS_TABLE = TABLE_NAMES.FILE_EMBEDDINGS;
|
|
29
31
|
const DOCUMENT_CHUNK_TABLE = TABLE_NAMES.DOCUMENT_CHUNK;
|
|
@@ -54,6 +56,58 @@ export class ContentRetriever {
|
|
|
54
56
|
this.cleaningUp = false;
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
resolveProjectResultPath(filePath, resolvedProjectPath) {
|
|
60
|
+
if (!filePath) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const absolutePath = path.isAbsolute(filePath) ? path.resolve(filePath) : path.resolve(resolvedProjectPath, filePath);
|
|
65
|
+
return isPathWithinProject(absolutePath, resolvedProjectPath) ? absolutePath : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async filterResultsForProject(results, resolvedProjectPath, getPath) {
|
|
69
|
+
const resultsToCheck = [];
|
|
70
|
+
const projectMatchMap = new Map();
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < results.length; i++) {
|
|
73
|
+
const result = results[i];
|
|
74
|
+
const resultPath = getPath(result);
|
|
75
|
+
|
|
76
|
+
if (result.project_path && result.project_path !== resolvedProjectPath) {
|
|
77
|
+
projectMatchMap.set(i, false);
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const absolutePath = this.resolveProjectResultPath(resultPath, resolvedProjectPath);
|
|
82
|
+
if (!absolutePath) {
|
|
83
|
+
projectMatchMap.set(i, false);
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
resultsToCheck.push({ index: i, absolutePath, resultPath });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (resultsToCheck.length > 0) {
|
|
91
|
+
const existenceResults = await Promise.all(
|
|
92
|
+
resultsToCheck.map(async ({ index, absolutePath, resultPath }) => {
|
|
93
|
+
try {
|
|
94
|
+
await fs.promises.access(absolutePath, fs.constants.F_OK);
|
|
95
|
+
return { index, exists: true };
|
|
96
|
+
} catch {
|
|
97
|
+
debug(`Filtering out non-existent project file: ${resultPath}`);
|
|
98
|
+
return { index, exists: false };
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
for (const { index, exists } of existenceResults) {
|
|
104
|
+
projectMatchMap.set(index, exists);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return results.filter((result, index) => projectMatchMap.get(index) === true);
|
|
109
|
+
}
|
|
110
|
+
|
|
57
111
|
/**
|
|
58
112
|
* Find relevant documentation with sophisticated reranking
|
|
59
113
|
* @param {string} queryText - The search query
|
|
@@ -99,7 +153,7 @@ export class ContentRetriever {
|
|
|
99
153
|
try {
|
|
100
154
|
const tableSchema = await table.schema;
|
|
101
155
|
if (tableSchema?.fields?.some((field) => field.name === 'project_path')) {
|
|
102
|
-
query = query.where(`project_path = '${resolvedProjectPath
|
|
156
|
+
query = query.where(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
|
|
103
157
|
debug(`Filtering documentation by project_path: ${resolvedProjectPath}`);
|
|
104
158
|
}
|
|
105
159
|
} catch (schemaError) {
|
|
@@ -109,65 +163,11 @@ export class ContentRetriever {
|
|
|
109
163
|
const results = await query.limit(Math.max(limit * 3, 20)).toArray();
|
|
110
164
|
verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} documentation results`));
|
|
111
165
|
|
|
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);
|
|
166
|
+
const projectFilteredResults = await this.filterResultsForProject(
|
|
167
|
+
results,
|
|
168
|
+
resolvedProjectPath,
|
|
169
|
+
(result) => result.original_document_path
|
|
170
|
+
);
|
|
171
171
|
|
|
172
172
|
verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} documentation results from current project`));
|
|
173
173
|
let finalResults = projectFilteredResults.map((result) => {
|
|
@@ -493,13 +493,13 @@ export class ContentRetriever {
|
|
|
493
493
|
if (queryFilePath) {
|
|
494
494
|
const normalizedQueryPath = path.resolve(resolvedProjectPath, queryFilePath);
|
|
495
495
|
// Add condition to exclude the file being reviewed
|
|
496
|
-
const escapedPath = normalizedQueryPath
|
|
496
|
+
const escapedPath = escapeSqlString(normalizedQueryPath);
|
|
497
497
|
conditions.push(`path != '${escapedPath}'`);
|
|
498
498
|
|
|
499
499
|
// Also check for relative path variants to be thorough
|
|
500
500
|
const relativePath = path.relative(resolvedProjectPath, normalizedQueryPath);
|
|
501
501
|
if (relativePath && !relativePath.startsWith('..')) {
|
|
502
|
-
const escapedRelativePath = relativePath
|
|
502
|
+
const escapedRelativePath = escapeSqlString(relativePath);
|
|
503
503
|
conditions.push(`path != '${escapedRelativePath}'`);
|
|
504
504
|
}
|
|
505
505
|
|
|
@@ -515,7 +515,7 @@ export class ContentRetriever {
|
|
|
515
515
|
|
|
516
516
|
if (hasProjectPathField) {
|
|
517
517
|
// Use exact match for project path
|
|
518
|
-
conditions.push(`project_path = '${resolvedProjectPath
|
|
518
|
+
conditions.push(`project_path = '${escapeSqlString(resolvedProjectPath)}'`);
|
|
519
519
|
debug(`Filtering by project_path: ${resolvedProjectPath}`);
|
|
520
520
|
}
|
|
521
521
|
}
|
|
@@ -532,72 +532,11 @@ export class ContentRetriever {
|
|
|
532
532
|
|
|
533
533
|
verboseLog(options, chalk.green(`Native hybrid search returned ${results.length} results`));
|
|
534
534
|
|
|
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);
|
|
535
|
+
const projectFilteredResults = await this.filterResultsForProject(
|
|
536
|
+
results,
|
|
537
|
+
resolvedProjectPath,
|
|
538
|
+
(result) => result.original_document_path || result.path
|
|
539
|
+
);
|
|
601
540
|
|
|
602
541
|
verboseLog(options, chalk.blue(`Filtered to ${projectFilteredResults.length} results from current project`));
|
|
603
542
|
|
|
@@ -643,14 +582,11 @@ export class ContentRetriever {
|
|
|
643
582
|
try {
|
|
644
583
|
const fileTable = await this.database.getTable(FILE_EMBEDDINGS_TABLE);
|
|
645
584
|
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
|
-
}
|
|
585
|
+
const structureResults = await fileTable
|
|
586
|
+
.query()
|
|
587
|
+
.where(`project_path = '${escapeSqlString(resolvedProjectPath)}' AND type = 'directory-structure'`)
|
|
588
|
+
.limit(1)
|
|
589
|
+
.toArray();
|
|
654
590
|
|
|
655
591
|
if (structureResults.length > 0) {
|
|
656
592
|
const structureRecord = structureResults[0];
|
|
@@ -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({
|
package/src/index.js
CHANGED
|
@@ -519,6 +519,7 @@ async function generateEmbeddings(options) {
|
|
|
519
519
|
|
|
520
520
|
// Get files to process
|
|
521
521
|
let filesToProcess = [];
|
|
522
|
+
const runMode = options.files && options.files.length > 0 ? 'partial' : 'full';
|
|
522
523
|
|
|
523
524
|
if (options.files && options.files.length > 0) {
|
|
524
525
|
console.log(chalk.cyan('Processing specified files/patterns...'));
|
|
@@ -585,6 +586,7 @@ async function generateEmbeddings(options) {
|
|
|
585
586
|
baseDir: baseDir,
|
|
586
587
|
batchSize: 100, // Set a reasonable batch size
|
|
587
588
|
maxLines: parseInt(options.maxLines || '1000', 10),
|
|
589
|
+
runMode,
|
|
588
590
|
onProgress: (status) => {
|
|
589
591
|
// Update counters based on status
|
|
590
592
|
if (status === 'processed') {
|
|
@@ -625,9 +627,6 @@ async function generateEmbeddings(options) {
|
|
|
625
627
|
forceAnalysis: options.forceAnalysis,
|
|
626
628
|
});
|
|
627
629
|
|
|
628
|
-
// Store project summary in embeddings system for later use
|
|
629
|
-
await embeddingsSystem.storeProjectSummary(projectDir, projectSummary);
|
|
630
|
-
|
|
631
630
|
console.log(chalk.green('✅ Project analysis complete and stored'));
|
|
632
631
|
verboseLog(options, chalk.gray(` Project: ${projectSummary.projectName}`));
|
|
633
632
|
verboseLog(
|
package/src/project-analyzer.js
CHANGED
|
@@ -209,12 +209,17 @@ export class ProjectAnalyzer {
|
|
|
209
209
|
// Check for existing analysis
|
|
210
210
|
const existingSummary = forceAnalysis ? null : await this.loadExistingAnalysis(projectPath);
|
|
211
211
|
if (existingSummary && !forceAnalysis) {
|
|
212
|
-
const
|
|
213
|
-
if (existingSummary.
|
|
214
|
-
verboseLog(verbose, chalk.
|
|
215
|
-
|
|
212
|
+
const currentEmbeddingInventoryHash = await this.calculateEmbeddingInventoryHash(projectPath);
|
|
213
|
+
if (existingSummary.embeddingInventoryHash !== currentEmbeddingInventoryHash) {
|
|
214
|
+
verboseLog(verbose, chalk.yellow('🔄 Embedding inventory changed, regenerating analysis...'));
|
|
215
|
+
} else {
|
|
216
|
+
const currentHash = await this.calculateKeyFilesHash(existingSummary.keyFiles);
|
|
217
|
+
if (existingSummary.keyFilesHash === currentHash) {
|
|
218
|
+
verboseLog(verbose, chalk.green('✅ Project analysis up-to-date (no key file changes detected)'));
|
|
219
|
+
return existingSummary;
|
|
220
|
+
}
|
|
221
|
+
verboseLog(verbose, chalk.yellow('🔄 Key files changed, regenerating analysis...'));
|
|
216
222
|
}
|
|
217
|
-
verboseLog(verbose, chalk.yellow('🔄 Key files changed, regenerating analysis...'));
|
|
218
223
|
} else {
|
|
219
224
|
verboseLog(
|
|
220
225
|
verbose,
|
|
@@ -241,6 +246,7 @@ export class ProjectAnalyzer {
|
|
|
241
246
|
const currentHash = await this.calculateKeyFilesHash(keyFiles);
|
|
242
247
|
projectSummary.keyFiles = keyFiles;
|
|
243
248
|
projectSummary.keyFilesHash = currentHash;
|
|
249
|
+
projectSummary.embeddingInventoryHash = await this.calculateEmbeddingInventoryHash(projectPath);
|
|
244
250
|
|
|
245
251
|
await this.storeAnalysis(projectPath, projectSummary);
|
|
246
252
|
|
|
@@ -613,6 +619,39 @@ Select files following the criteria in the system instructions.`;
|
|
|
613
619
|
return hash.digest('hex');
|
|
614
620
|
}
|
|
615
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Calculate a project-scoped hash of the current embedding inventory.
|
|
624
|
+
*/
|
|
625
|
+
async calculateEmbeddingInventoryHash(projectPath) {
|
|
626
|
+
try {
|
|
627
|
+
const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
628
|
+
await embeddingsSystem.initialize();
|
|
629
|
+
const table = await embeddingsSystem.databaseManager.getTable(embeddingsSystem.databaseManager.fileEmbeddingsTable);
|
|
630
|
+
|
|
631
|
+
if (!table) {
|
|
632
|
+
return 'no-file-embeddings-table';
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const records = await table
|
|
636
|
+
.query()
|
|
637
|
+
.select(['type', 'path', 'content_hash', 'project_path'])
|
|
638
|
+
.where(`project_path = '${projectPath.replace(/'/g, "''")}'`)
|
|
639
|
+
.toArray();
|
|
640
|
+
|
|
641
|
+
const hash = crypto.createHash('sha256');
|
|
642
|
+
const normalizedRows = records.map((record) => `${record.type || 'file'}:${record.path || ''}:${record.content_hash || ''}`).sort();
|
|
643
|
+
|
|
644
|
+
for (const row of normalizedRows) {
|
|
645
|
+
hash.update(row);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return hash.digest('hex');
|
|
649
|
+
} catch (error) {
|
|
650
|
+
verboseLog({}, chalk.yellow(`Warning: Could not calculate embedding inventory hash: ${error.message}`));
|
|
651
|
+
return 'embedding-inventory-unavailable';
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
616
655
|
/**
|
|
617
656
|
* Generate comprehensive project summary using LLM analysis (SINGLE CALL)
|
|
618
657
|
*/
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -910,6 +910,15 @@ describe('rag-analyzer', () => {
|
|
|
910
910
|
expect(context).toHaveProperty('codeExamples');
|
|
911
911
|
});
|
|
912
912
|
|
|
913
|
+
it('should degrade gracefully when retrieval returns no context for active files', async () => {
|
|
914
|
+
mockEmbeddingsSystem.findSimilarCode.mockResolvedValue([]);
|
|
915
|
+
mockEmbeddingsSystem.findRelevantDocs.mockResolvedValue([]);
|
|
916
|
+
const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
|
|
917
|
+
const context = await gatherUnifiedContextForPR(prFiles, { projectPath: '/project' });
|
|
918
|
+
expect(context.codeExamples).toEqual([]);
|
|
919
|
+
expect(context.guidelines).toEqual([]);
|
|
920
|
+
});
|
|
921
|
+
|
|
913
922
|
it('should find custom document chunks', async () => {
|
|
914
923
|
mockEmbeddingsSystem.getExistingCustomDocumentChunks.mockResolvedValue([{ content: 'Custom doc', document_title: 'Guidelines' }]);
|
|
915
924
|
const prFiles = [{ filePath: '/src/file.js', content: 'code', language: 'javascript' }];
|