codecritique 2.0.6 → 2.0.8
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/llm.js +49 -10
- package/src/llm.test.js +56 -0
- package/src/project-analyzer.js +78 -19
- package/src/project-analyzer.test.js +73 -0
- package/src/rag-analyzer.js +85 -27
- package/src/rag-analyzer.test.js +14 -6
package/package.json
CHANGED
package/src/llm.js
CHANGED
|
@@ -44,6 +44,44 @@ const DEFAULT_MODEL = 'claude-sonnet-4-5';
|
|
|
44
44
|
// Maximum tokens for response
|
|
45
45
|
const MAX_TOKENS = 4096;
|
|
46
46
|
|
|
47
|
+
const MAX_CACHE_CONTROL_BLOCKS = 4;
|
|
48
|
+
|
|
49
|
+
function normalizeSystemBlock(block, cacheControl) {
|
|
50
|
+
const normalizedBlock = typeof block === 'string' ? { type: 'text', text: block } : { type: 'text', ...block };
|
|
51
|
+
|
|
52
|
+
if (cacheControl) {
|
|
53
|
+
return {
|
|
54
|
+
...normalizedBlock,
|
|
55
|
+
cache_control: normalizedBlock.cache_control || cacheControl,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return normalizedBlock;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildSystemContent(system, cachedSystemBlocks, cacheControl) {
|
|
63
|
+
if (!system && cachedSystemBlocks.length === 0) {
|
|
64
|
+
return 'You are an expert code reviewer with deep knowledge of software engineering principles, design patterns, and best practices.';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const systemBlocks = [];
|
|
68
|
+
const appendBlock = (block) => {
|
|
69
|
+
const cacheable = systemBlocks.filter((existingBlock) => existingBlock.cache_control).length < MAX_CACHE_CONTROL_BLOCKS;
|
|
70
|
+
systemBlocks.push(normalizeSystemBlock(block, cacheable ? cacheControl : null));
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
if (Array.isArray(system)) {
|
|
74
|
+
system.forEach(appendBlock);
|
|
75
|
+
}
|
|
76
|
+
else if (system) {
|
|
77
|
+
appendBlock(system);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
cachedSystemBlocks.filter(Boolean).forEach(appendBlock);
|
|
81
|
+
|
|
82
|
+
return systemBlocks;
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
/**
|
|
48
86
|
* Send a prompt to Claude and get a structured JSON response using tool calling.
|
|
49
87
|
* Uses prompt caching for system prompts to reduce token costs.
|
|
@@ -51,12 +89,21 @@ const MAX_TOKENS = 4096;
|
|
|
51
89
|
* @param {string} prompt - The prompt to send to Claude
|
|
52
90
|
* @param {Object} options - Options for the request
|
|
53
91
|
* @param {string} options.system - System prompt (will be cached for cost optimization)
|
|
92
|
+
* @param {Array<string|Object>} options.cachedSystemBlocks - Additional stable system blocks to cache when possible
|
|
54
93
|
* @param {Object} options.jsonSchema - JSON schema for structured output
|
|
55
94
|
* @param {string} options.cacheTtl - Cache TTL: '5m' (default, no extra cost) or '1h' (extended, extra cost for writes)
|
|
56
95
|
* @returns {Promise<Object>} The response from Claude with structured data
|
|
57
96
|
*/
|
|
58
97
|
async function sendPromptToClaude(prompt, options = {}) {
|
|
59
|
-
const {
|
|
98
|
+
const {
|
|
99
|
+
model = DEFAULT_MODEL,
|
|
100
|
+
maxTokens = MAX_TOKENS,
|
|
101
|
+
temperature = 0.7,
|
|
102
|
+
system = '',
|
|
103
|
+
cachedSystemBlocks = [],
|
|
104
|
+
jsonSchema = null,
|
|
105
|
+
cacheTtl = '5m',
|
|
106
|
+
} = options;
|
|
60
107
|
|
|
61
108
|
try {
|
|
62
109
|
verboseLog(options, chalk.cyan('Sending prompt to Claude...'));
|
|
@@ -68,15 +115,7 @@ async function sendPromptToClaude(prompt, options = {}) {
|
|
|
68
115
|
// TTL options: '5m' (default, no extra cost) or '1h' (extended, extra cost for cache writes)
|
|
69
116
|
const cacheControl = cacheTtl === '1h' ? { type: 'ephemeral', ttl: '1h' } : { type: 'ephemeral' };
|
|
70
117
|
|
|
71
|
-
const systemContent = system
|
|
72
|
-
? [
|
|
73
|
-
{
|
|
74
|
-
type: 'text',
|
|
75
|
-
text: system,
|
|
76
|
-
cache_control: cacheControl,
|
|
77
|
-
},
|
|
78
|
-
]
|
|
79
|
-
: 'You are an expert code reviewer with deep knowledge of software engineering principles, design patterns, and best practices.';
|
|
118
|
+
const systemContent = buildSystemContent(system, cachedSystemBlocks, cacheControl);
|
|
80
119
|
|
|
81
120
|
// Build base request parameters
|
|
82
121
|
const requestParams = {
|
package/src/llm.test.js
CHANGED
|
@@ -241,6 +241,62 @@ describe('sendPromptToClaude', () => {
|
|
|
241
241
|
})
|
|
242
242
|
);
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
it('should append additional cached system blocks', async () => {
|
|
246
|
+
mockMessagesCreate.mockResolvedValue({
|
|
247
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
248
|
+
model: 'claude-sonnet-4-5',
|
|
249
|
+
usage: {},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
await sendPromptToClaude('Test', {
|
|
253
|
+
system: 'Base review rules',
|
|
254
|
+
cachedSystemBlocks: ['Custom project instructions', 'Project architecture context'],
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
expect(mockMessagesCreate).toHaveBeenCalledWith(
|
|
258
|
+
expect.objectContaining({
|
|
259
|
+
system: [
|
|
260
|
+
{
|
|
261
|
+
type: 'text',
|
|
262
|
+
text: 'Base review rules',
|
|
263
|
+
cache_control: { type: 'ephemeral' },
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
type: 'text',
|
|
267
|
+
text: 'Custom project instructions',
|
|
268
|
+
cache_control: { type: 'ephemeral' },
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: 'text',
|
|
272
|
+
text: 'Project architecture context',
|
|
273
|
+
cache_control: { type: 'ephemeral' },
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
})
|
|
277
|
+
);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should include extra system blocks without exceeding cache breakpoint limits', async () => {
|
|
281
|
+
mockMessagesCreate.mockResolvedValue({
|
|
282
|
+
content: [{ type: 'text', text: 'Response' }],
|
|
283
|
+
model: 'claude-sonnet-4-5',
|
|
284
|
+
usage: {},
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
await sendPromptToClaude('Test', {
|
|
288
|
+
system: 'Base review rules',
|
|
289
|
+
cachedSystemBlocks: ['Block 1', 'Block 2', 'Block 3', 'Block 4'],
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const request = mockMessagesCreate.mock.calls.at(-1)[0];
|
|
293
|
+
expect(request.system).toHaveLength(5);
|
|
294
|
+
expect(request.system.filter((block) => block.cache_control)).toHaveLength(4);
|
|
295
|
+
expect(request.system.at(-1)).toEqual({
|
|
296
|
+
type: 'text',
|
|
297
|
+
text: 'Block 4',
|
|
298
|
+
});
|
|
299
|
+
});
|
|
244
300
|
});
|
|
245
301
|
|
|
246
302
|
describe('message format', () => {
|
package/src/project-analyzer.js
CHANGED
|
@@ -16,6 +16,8 @@ import { isDocumentationFile, isTestFile } from './utils/file-validation.js';
|
|
|
16
16
|
import { verboseLog } from './utils/logging.js';
|
|
17
17
|
import { escapeSqlString } from './utils/string-utils.js';
|
|
18
18
|
|
|
19
|
+
const TERM_SEARCH_CONTENT_CANDIDATE_LIMIT = 500;
|
|
20
|
+
|
|
19
21
|
// Consolidated file classification configuration
|
|
20
22
|
const FILE_PATTERNS = {
|
|
21
23
|
config: {
|
|
@@ -383,14 +385,18 @@ export class ProjectAnalyzer {
|
|
|
383
385
|
// Unified query function
|
|
384
386
|
const queryFiles = async (config) => {
|
|
385
387
|
try {
|
|
386
|
-
let query = table.query().select(['path', 'name', 'content', 'type', 'language']);
|
|
387
|
-
|
|
388
388
|
if (config.whereClause) {
|
|
389
|
-
|
|
389
|
+
return await table
|
|
390
|
+
.query()
|
|
391
|
+
.select(['path', 'name', 'content', 'type', 'language'])
|
|
392
|
+
.where(`${projectPathFilter} AND (${config.whereClause})`)
|
|
393
|
+
.limit(config.limit || 30)
|
|
394
|
+
.toArray();
|
|
390
395
|
}
|
|
391
396
|
else if (config.terms) {
|
|
392
|
-
// For term-based searches, query
|
|
393
|
-
|
|
397
|
+
// For term-based searches, query metadata first and only fetch content for
|
|
398
|
+
// the shallow candidate set used for matching.
|
|
399
|
+
const allFiles = await table.query().select(['path', 'name', 'type', 'language']).where(projectPathFilter).toArray();
|
|
394
400
|
|
|
395
401
|
// Sort by path depth (shorter paths first) to prioritize config files
|
|
396
402
|
allFiles.sort((a, b) => {
|
|
@@ -399,25 +405,78 @@ export class ProjectAnalyzer {
|
|
|
399
405
|
return depthA - depthB;
|
|
400
406
|
});
|
|
401
407
|
|
|
402
|
-
//
|
|
403
|
-
|
|
408
|
+
// Dedupe by path before capping: transient duplicate rows can exist for
|
|
409
|
+
// the same file (different content_hash) when a partial run hasn't pruned
|
|
410
|
+
// stale embeddings, and they would otherwise consume candidate slots and
|
|
411
|
+
// bloat the content query. Keep the shallowest occurrence (allFiles is
|
|
412
|
+
// already depth-sorted), then take the first 500 distinct shallow files.
|
|
413
|
+
const seenPaths = new Set();
|
|
414
|
+
const sortedFiles = [];
|
|
415
|
+
for (const file of allFiles) {
|
|
416
|
+
if (file.path) {
|
|
417
|
+
if (seenPaths.has(file.path)) {
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
seenPaths.add(file.path);
|
|
421
|
+
}
|
|
422
|
+
sortedFiles.push(file);
|
|
423
|
+
if (sortedFiles.length >= TERM_SEARCH_CONTENT_CANDIDATE_LIMIT) {
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (sortedFiles.length === 0) {
|
|
428
|
+
return [];
|
|
429
|
+
}
|
|
404
430
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
431
|
+
const candidatePaths = sortedFiles.map((file) => file.path).filter(Boolean);
|
|
432
|
+
const contentByPath = new Map();
|
|
433
|
+
if (candidatePaths.length > 0) {
|
|
434
|
+
const pathList = candidatePaths.map((candidatePath) => `'${escapeSqlString(candidatePath)}'`).join(', ');
|
|
435
|
+
// No row limit here: the result set is already bounded by the IN list of
|
|
436
|
+
// distinct candidate paths. A limit could truncate rows when duplicate
|
|
437
|
+
// rows exist for a path, silently dropping content for matched files.
|
|
438
|
+
const contentResults = await table
|
|
439
|
+
.query()
|
|
440
|
+
.select(['path', 'content'])
|
|
441
|
+
.where(`${projectPathFilter} AND path IN (${pathList})`)
|
|
442
|
+
.toArray();
|
|
443
|
+
|
|
444
|
+
for (const result of contentResults) {
|
|
445
|
+
// Prefer non-empty content when duplicate rows exist for a path.
|
|
446
|
+
if (contentByPath.get(result.path)) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
contentByPath.set(result.path, result.content || '');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
412
452
|
|
|
413
|
-
|
|
414
|
-
|
|
453
|
+
return sortedFiles
|
|
454
|
+
.filter((result) => {
|
|
455
|
+
const content = (contentByPath.get(result.path) || '').toLowerCase();
|
|
456
|
+
const pathName = (result.path || '').toLowerCase();
|
|
457
|
+
const name = (result.name || '').toLowerCase();
|
|
458
|
+
const matches = config.terms.some(
|
|
459
|
+
(term) =>
|
|
460
|
+
content.includes(term.toLowerCase()) || pathName.includes(term.toLowerCase()) || name.includes(term.toLowerCase())
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
return matches;
|
|
464
|
+
})
|
|
465
|
+
.map((result) => {
|
|
466
|
+
return {
|
|
467
|
+
...result,
|
|
468
|
+
content: contentByPath.get(result.path) || '',
|
|
469
|
+
};
|
|
470
|
+
});
|
|
415
471
|
}
|
|
416
472
|
else {
|
|
417
|
-
|
|
473
|
+
return await table
|
|
474
|
+
.query()
|
|
475
|
+
.select(['path', 'name', 'content', 'type', 'language'])
|
|
476
|
+
.where(projectPathFilter)
|
|
477
|
+
.limit(config.limit || 30)
|
|
478
|
+
.toArray();
|
|
418
479
|
}
|
|
419
|
-
|
|
420
|
-
return await query.limit(config.limit || 30).toArray();
|
|
421
480
|
}
|
|
422
481
|
catch (error) {
|
|
423
482
|
verboseLog({}, chalk.yellow(` ⚠️ Query failed for ${config.category}: ${error.message}`));
|
|
@@ -410,6 +410,79 @@ describe('ProjectAnalyzer', () => {
|
|
|
410
410
|
expect(queryChain.where).toHaveBeenCalledWith(expect.stringContaining("/mock/we''re/project"));
|
|
411
411
|
});
|
|
412
412
|
|
|
413
|
+
it('should bound content reads for term-based key file searches', async () => {
|
|
414
|
+
const queryCalls = [];
|
|
415
|
+
mockTable.query.mockImplementation(() => {
|
|
416
|
+
const call = {
|
|
417
|
+
select: [],
|
|
418
|
+
where: [],
|
|
419
|
+
limit: [],
|
|
420
|
+
};
|
|
421
|
+
queryCalls.push(call);
|
|
422
|
+
|
|
423
|
+
const chain = {
|
|
424
|
+
select: vi.fn((columns) => {
|
|
425
|
+
call.select.push(columns);
|
|
426
|
+
return chain;
|
|
427
|
+
}),
|
|
428
|
+
where: vi.fn((clause) => {
|
|
429
|
+
call.where.push(clause);
|
|
430
|
+
return chain;
|
|
431
|
+
}),
|
|
432
|
+
limit: vi.fn((limit) => {
|
|
433
|
+
call.limit.push(limit);
|
|
434
|
+
return chain;
|
|
435
|
+
}),
|
|
436
|
+
toArray: vi.fn(async () => {
|
|
437
|
+
const selectedColumns = call.select[0] || [];
|
|
438
|
+
if (selectedColumns.includes('content')) {
|
|
439
|
+
return [
|
|
440
|
+
{
|
|
441
|
+
path: 'package.json',
|
|
442
|
+
name: 'package.json',
|
|
443
|
+
content: '{"name": "test-project"}',
|
|
444
|
+
type: 'json',
|
|
445
|
+
language: 'json',
|
|
446
|
+
},
|
|
447
|
+
];
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return [
|
|
451
|
+
{
|
|
452
|
+
path: 'package.json',
|
|
453
|
+
name: 'package.json',
|
|
454
|
+
type: 'json',
|
|
455
|
+
language: 'json',
|
|
456
|
+
},
|
|
457
|
+
];
|
|
458
|
+
}),
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
return chain;
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
const result = await analyzer.mineKeyFilesFromEmbeddings(mockProjectPath);
|
|
465
|
+
|
|
466
|
+
expect(result.some((file) => file.path === 'package.json')).toBe(true);
|
|
467
|
+
expect(
|
|
468
|
+
queryCalls.some((call) => {
|
|
469
|
+
const selectedColumns = call.select[0] || [];
|
|
470
|
+
return selectedColumns.includes('content') && call.where[0] === "project_path = '/mock/project'" && call.limit.length === 0;
|
|
471
|
+
})
|
|
472
|
+
).toBe(false);
|
|
473
|
+
expect(
|
|
474
|
+
queryCalls.some((call) => {
|
|
475
|
+
const selectedColumns = call.select[0] || [];
|
|
476
|
+
return (
|
|
477
|
+
selectedColumns.includes('path') &&
|
|
478
|
+
selectedColumns.includes('name') &&
|
|
479
|
+
!selectedColumns.includes('content') &&
|
|
480
|
+
call.where[0] === "project_path = '/mock/project'"
|
|
481
|
+
);
|
|
482
|
+
})
|
|
483
|
+
).toBe(true);
|
|
484
|
+
});
|
|
485
|
+
|
|
413
486
|
it('should handle table optimization errors gracefully', async () => {
|
|
414
487
|
mockTable.optimize.mockRejectedValue(new Error('legacy format'));
|
|
415
488
|
mockTable.query().toArray.mockResolvedValue([]);
|
package/src/rag-analyzer.js
CHANGED
|
@@ -42,6 +42,20 @@ const embeddingsSystem = getDefaultEmbeddingsSystem();
|
|
|
42
42
|
// Track if semantic similarity has been initialized
|
|
43
43
|
let semanticSimilarityInitialized = false;
|
|
44
44
|
|
|
45
|
+
async function fileExists(filePath) {
|
|
46
|
+
try {
|
|
47
|
+
await fs.promises.access(filePath);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function readTextFile(filePath) {
|
|
56
|
+
return await fs.promises.readFile(filePath, 'utf8');
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
/**
|
|
46
60
|
* Initialize semantic similarity for feedback filtering
|
|
47
61
|
* Uses the shared embeddings system from feedback-loader.js
|
|
@@ -448,7 +462,7 @@ async function runAnalysis(filePath, options = {}) {
|
|
|
448
462
|
}
|
|
449
463
|
|
|
450
464
|
// Check if file exists
|
|
451
|
-
if (!
|
|
465
|
+
if (!(await fileExists(filePath))) {
|
|
452
466
|
throw new Error(`File not found: ${filePath}`);
|
|
453
467
|
}
|
|
454
468
|
|
|
@@ -457,12 +471,14 @@ async function runAnalysis(filePath, options = {}) {
|
|
|
457
471
|
let fullFileContent;
|
|
458
472
|
if (options.diffOnly && options.diffContent) {
|
|
459
473
|
content = options.diffContent;
|
|
460
|
-
// For PR reviews, always read the full file content for context awareness
|
|
461
|
-
|
|
474
|
+
// For PR reviews, always read the full file content for context awareness.
|
|
475
|
+
// Tolerate a missing/unreadable file (e.g. deleted in the window after the
|
|
476
|
+
// existence check) by degrading to null; downstream falls back to file.content.
|
|
477
|
+
fullFileContent = await readTextFile(filePath).catch(() => null);
|
|
462
478
|
verboseLog(options, chalk.blue(`Analyzing diff only for ${path.basename(filePath)}`));
|
|
463
479
|
}
|
|
464
480
|
else {
|
|
465
|
-
content =
|
|
481
|
+
content = await readTextFile(filePath);
|
|
466
482
|
fullFileContent = content;
|
|
467
483
|
verboseLog(options, chalk.blue(`Analyzing full file ${path.basename(filePath)}`));
|
|
468
484
|
}
|
|
@@ -835,6 +851,46 @@ MARKDOWN FORMATTING IN DESCRIPTIONS AND SUGGESTIONS:
|
|
|
835
851
|
Your response must start with { and end with } with no additional text.`;
|
|
836
852
|
}
|
|
837
853
|
|
|
854
|
+
function buildCachedProjectContextBlocks({ roleDefinition, customDocsSection, projectArchitectureSection }) {
|
|
855
|
+
const blocks = [];
|
|
856
|
+
|
|
857
|
+
if (roleDefinition?.trim()) {
|
|
858
|
+
blocks.push(`REVIEW ROLE AND PROJECT INSTRUCTIONS
|
|
859
|
+
|
|
860
|
+
${roleDefinition}`);
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
if (customDocsSection?.trim() && !customDocsSection.includes('No custom instructions provided.')) {
|
|
864
|
+
blocks.push(`CUSTOM PROJECT INSTRUCTIONS
|
|
865
|
+
|
|
866
|
+
${customDocsSection}`);
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
if (projectArchitectureSection?.trim()) {
|
|
870
|
+
blocks.push(`PROJECT ARCHITECTURE CONTEXT
|
|
871
|
+
|
|
872
|
+
${projectArchitectureSection}`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
return blocks;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Build the user-prompt sentence that points the model at the cached system blocks.
|
|
880
|
+
* Returns an empty string when no cached blocks were emitted, so the prompt never
|
|
881
|
+
* instructs the model to use context that was not sent.
|
|
882
|
+
*
|
|
883
|
+
* @param {string[]} cachedSystemBlocks - Cached blocks produced for this request
|
|
884
|
+
* @param {string} subject - What is being evaluated (e.g. 'this file')
|
|
885
|
+
* @returns {string} Instruction sentence, or '' when there is no cached context
|
|
886
|
+
*/
|
|
887
|
+
function buildCachedContextInstruction(cachedSystemBlocks, subject) {
|
|
888
|
+
if (!cachedSystemBlocks || cachedSystemBlocks.length === 0) {
|
|
889
|
+
return '';
|
|
890
|
+
}
|
|
891
|
+
return `Use the cached project context, project architecture, and custom instructions when evaluating ${subject}.`;
|
|
892
|
+
}
|
|
893
|
+
|
|
838
894
|
// LLM call function - sends prompt with cached system content for cost optimization
|
|
839
895
|
async function sendPromptToLLM(promptConfig, llmOptions) {
|
|
840
896
|
try {
|
|
@@ -946,12 +1002,14 @@ async function sendPromptToLLM(promptConfig, llmOptions) {
|
|
|
946
1002
|
verboseLog(llmOptions, chalk.gray(` Prompt config has systemPrompt: ${!!promptConfig.systemPrompt}`));
|
|
947
1003
|
verboseLog(llmOptions, chalk.gray(` Prompt config has userPrompt: ${!!promptConfig.userPrompt}`));
|
|
948
1004
|
verboseLog(llmOptions, chalk.gray(` System prompt length: ${promptConfig.systemPrompt?.length || 0} chars`));
|
|
1005
|
+
verboseLog(llmOptions, chalk.gray(` Cached system blocks: ${promptConfig.cachedSystemBlocks?.length || 0}`));
|
|
949
1006
|
verboseLog(llmOptions, chalk.gray(` User prompt length: ${promptConfig.userPrompt?.length || 0} chars`));
|
|
950
1007
|
|
|
951
1008
|
// Send prompt with system (cached) and user (dynamic) content
|
|
952
1009
|
const response = await llm.sendPromptToClaude(promptConfig.userPrompt, {
|
|
953
1010
|
...llmOptions,
|
|
954
1011
|
system: promptConfig.systemPrompt,
|
|
1012
|
+
cachedSystemBlocks: promptConfig.cachedSystemBlocks || [],
|
|
955
1013
|
jsonSchema: codeReviewSchema,
|
|
956
1014
|
});
|
|
957
1015
|
|
|
@@ -1096,21 +1154,21 @@ ${addLineNumbers(file.content)}
|
|
|
1096
1154
|
|
|
1097
1155
|
// Build the cached system prompt (contains all static rules)
|
|
1098
1156
|
const systemPrompt = buildCachedSystemPrompt('code');
|
|
1157
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1158
|
+
roleDefinition,
|
|
1159
|
+
customDocsSection,
|
|
1160
|
+
projectArchitectureSection,
|
|
1161
|
+
});
|
|
1099
1162
|
|
|
1100
1163
|
// Build the dynamic user prompt (contains all variable content)
|
|
1101
1164
|
const userPrompt = finalizePrompt(`
|
|
1102
1165
|
## REVIEW TYPE: CODE FILE ANALYSIS
|
|
1103
1166
|
|
|
1104
|
-
${roleDefinition}
|
|
1105
|
-
|
|
1106
1167
|
${reviewInstructions}
|
|
1107
1168
|
|
|
1108
|
-
${customDocsSection}
|
|
1109
|
-
|
|
1110
1169
|
${fileSection}
|
|
1111
1170
|
|
|
1112
|
-
|
|
1113
|
-
${projectArchitectureSection}
|
|
1171
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this file')}
|
|
1114
1172
|
|
|
1115
1173
|
CONTEXT A: EXPLICIT GUIDELINES FROM DOCUMENTATION
|
|
1116
1174
|
${formattedGuidelines}
|
|
@@ -1136,7 +1194,7 @@ Analyze the FILE TO REVIEW above using the 4-stage methodology:
|
|
|
1136
1194
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1137
1195
|
`);
|
|
1138
1196
|
|
|
1139
|
-
return { systemPrompt, userPrompt };
|
|
1197
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1140
1198
|
}
|
|
1141
1199
|
|
|
1142
1200
|
/**
|
|
@@ -1215,22 +1273,22 @@ ${addLineNumbers(file.content)}
|
|
|
1215
1273
|
|
|
1216
1274
|
// Build the cached system prompt (contains all static rules)
|
|
1217
1275
|
const systemPrompt = buildCachedSystemPrompt('test');
|
|
1276
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1277
|
+
roleDefinition,
|
|
1278
|
+
customDocsSection,
|
|
1279
|
+
projectArchitectureSection,
|
|
1280
|
+
});
|
|
1218
1281
|
|
|
1219
1282
|
// Build the dynamic user prompt (contains all variable content)
|
|
1220
1283
|
const userPrompt = finalizePrompt(`
|
|
1221
1284
|
## REVIEW TYPE: TEST FILE ANALYSIS
|
|
1222
1285
|
|
|
1223
|
-
${roleDefinition}
|
|
1224
|
-
|
|
1225
1286
|
${reviewInstructions}
|
|
1226
1287
|
|
|
1227
1288
|
${fileSection}
|
|
1228
1289
|
|
|
1229
1290
|
## ANALYSIS CONTEXT
|
|
1230
|
-
${
|
|
1231
|
-
|
|
1232
|
-
CONTEXT FROM PROJECT:
|
|
1233
|
-
${projectArchitectureSection}
|
|
1291
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this test file')}
|
|
1234
1292
|
|
|
1235
1293
|
CONTEXT A: TESTING GUIDELINES AND BEST PRACTICES
|
|
1236
1294
|
${formattedGuidelines}
|
|
@@ -1253,7 +1311,7 @@ Analyze the TEST FILE above using the 5-stage methodology:
|
|
|
1253
1311
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1254
1312
|
`);
|
|
1255
1313
|
|
|
1256
|
-
return { systemPrompt, userPrompt };
|
|
1314
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1257
1315
|
}
|
|
1258
1316
|
|
|
1259
1317
|
/**
|
|
@@ -1371,20 +1429,23 @@ For each file in this PR, you have access to:
|
|
|
1371
1429
|
|
|
1372
1430
|
// Build the cached system prompt (contains all static rules)
|
|
1373
1431
|
const systemPrompt = buildCachedSystemPrompt('pr');
|
|
1432
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1433
|
+
roleDefinition,
|
|
1434
|
+
customDocsSection,
|
|
1435
|
+
projectArchitectureSection,
|
|
1436
|
+
});
|
|
1374
1437
|
|
|
1375
1438
|
// Build the dynamic user prompt (contains all variable content)
|
|
1376
1439
|
const userPrompt = finalizePrompt(`
|
|
1377
1440
|
## REVIEW TYPE: HOLISTIC PR ANALYSIS
|
|
1378
1441
|
|
|
1379
|
-
${roleDefinition}
|
|
1380
|
-
|
|
1381
1442
|
## PULL REQUEST OVERVIEW
|
|
1382
1443
|
- **Total Files**: ${prFiles.length}
|
|
1383
1444
|
- **Source Files**: ${prFiles.filter((f) => !f.isTest).length}
|
|
1384
1445
|
- **Test Files**: ${prFiles.filter((f) => f.isTest).length}
|
|
1385
1446
|
|
|
1386
1447
|
## UNIFIED CONTEXT FROM PROJECT
|
|
1387
|
-
${
|
|
1448
|
+
${buildCachedContextInstruction(cachedSystemBlocks, 'this pull request')}
|
|
1388
1449
|
|
|
1389
1450
|
### PROJECT CODE EXAMPLES
|
|
1390
1451
|
${formattedCodeExamples}
|
|
@@ -1398,9 +1459,6 @@ ${formattedPRComments}
|
|
|
1398
1459
|
## PR FILES WITH CHANGES
|
|
1399
1460
|
${formattedPRFiles}
|
|
1400
1461
|
|
|
1401
|
-
## CUSTOM INSTRUCTIONS
|
|
1402
|
-
${customDocsSection}
|
|
1403
|
-
|
|
1404
1462
|
## YOUR TASK
|
|
1405
1463
|
|
|
1406
1464
|
Analyze ALL PR FILES above using the 5-stage methodology:
|
|
@@ -1418,7 +1476,7 @@ Analyze ALL PR FILES above using the 5-stage methodology:
|
|
|
1418
1476
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1419
1477
|
`);
|
|
1420
1478
|
|
|
1421
|
-
return { systemPrompt, userPrompt };
|
|
1479
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1422
1480
|
}
|
|
1423
1481
|
|
|
1424
1482
|
/**
|
|
@@ -1487,7 +1545,7 @@ async function getPRCommentContext(filePath, options = {}) {
|
|
|
1487
1545
|
verboseLog(options, chalk.blue(`🔍 Using pre-computed query embedding for PR comment search`));
|
|
1488
1546
|
// We still need the file content for the search function, but not for embedding
|
|
1489
1547
|
try {
|
|
1490
|
-
fileContent =
|
|
1548
|
+
fileContent = await readTextFile(filePath);
|
|
1491
1549
|
const maxEmbeddingLength = 8000; // Keep consistent with original truncation
|
|
1492
1550
|
contentForSearch = fileContent.length > maxEmbeddingLength ? fileContent.substring(0, maxEmbeddingLength) : fileContent;
|
|
1493
1551
|
}
|
|
@@ -1505,7 +1563,7 @@ async function getPRCommentContext(filePath, options = {}) {
|
|
|
1505
1563
|
else {
|
|
1506
1564
|
// Fallback to original behavior if no pre-computed embedding provided
|
|
1507
1565
|
try {
|
|
1508
|
-
fileContent =
|
|
1566
|
+
fileContent = await readTextFile(filePath);
|
|
1509
1567
|
}
|
|
1510
1568
|
catch (readError) {
|
|
1511
1569
|
debug(`[getPRCommentContext] Could not read file ${filePath}: ${readError.message}`);
|
package/src/rag-analyzer.test.js
CHANGED
|
@@ -41,6 +41,10 @@ vi.mock('node:fs', () => ({
|
|
|
41
41
|
default: {
|
|
42
42
|
readFileSync: vi.fn(),
|
|
43
43
|
existsSync: vi.fn().mockReturnValue(true),
|
|
44
|
+
promises: {
|
|
45
|
+
access: vi.fn().mockResolvedValue(undefined),
|
|
46
|
+
readFile: vi.fn(),
|
|
47
|
+
},
|
|
44
48
|
},
|
|
45
49
|
}));
|
|
46
50
|
|
|
@@ -155,6 +159,8 @@ describe('rag-analyzer', () => {
|
|
|
155
159
|
mockEmbeddingsSystem.updatePRCommentsIndex.mockReset().mockResolvedValue(undefined);
|
|
156
160
|
fs.readFileSync.mockReturnValue('const x = 1;\nconsole.log(x);');
|
|
157
161
|
fs.existsSync.mockReturnValue(true);
|
|
162
|
+
fs.promises.access.mockReset().mockResolvedValue(undefined);
|
|
163
|
+
fs.promises.readFile.mockReset().mockResolvedValue('const x = 1;\nconsole.log(x);');
|
|
158
164
|
});
|
|
159
165
|
|
|
160
166
|
afterEach(() => {
|
|
@@ -197,7 +203,7 @@ describe('rag-analyzer', () => {
|
|
|
197
203
|
});
|
|
198
204
|
|
|
199
205
|
it('should return error when file does not exist', async () => {
|
|
200
|
-
fs.
|
|
206
|
+
fs.promises.access.mockRejectedValue(new Error('ENOENT'));
|
|
201
207
|
const result = await runAnalysis('/test/nonexistent.js');
|
|
202
208
|
expect(result.success).toBe(false);
|
|
203
209
|
expect(result.error).toContain('File not found');
|
|
@@ -369,13 +375,13 @@ describe('rag-analyzer', () => {
|
|
|
369
375
|
['whitespace only', ' \n\n '],
|
|
370
376
|
['normal content', 'const x = 1;\nfunction test() {}'],
|
|
371
377
|
])('should handle %s', async (_, content) => {
|
|
372
|
-
fs.
|
|
378
|
+
fs.promises.readFile.mockResolvedValue(content);
|
|
373
379
|
const result = await runAnalysis('/test/file.js');
|
|
374
380
|
expect(result).toBeDefined();
|
|
375
381
|
});
|
|
376
382
|
|
|
377
383
|
it('should handle very long files', async () => {
|
|
378
|
-
fs.
|
|
384
|
+
fs.promises.readFile.mockResolvedValue(createMockLongCode(1000));
|
|
379
385
|
const result = await runAnalysis('/test/long.js');
|
|
380
386
|
expect(result.success).toBe(true);
|
|
381
387
|
});
|
|
@@ -772,7 +778,9 @@ describe('rag-analyzer', () => {
|
|
|
772
778
|
customDocs: [{ content: 'Mandatory internal review rule', document_title: 'Internal Standards' }],
|
|
773
779
|
});
|
|
774
780
|
|
|
775
|
-
|
|
781
|
+
const [, options] = llm.sendPromptToClaude.mock.calls.at(-1);
|
|
782
|
+
expect(options.cachedSystemBlocks.join('\n')).toContain('Mandatory internal review rule');
|
|
783
|
+
expect(llm.sendPromptToClaude.mock.calls.at(-1)[0]).not.toContain('Mandatory internal review rule');
|
|
776
784
|
});
|
|
777
785
|
|
|
778
786
|
it('should log selected chunks when verbose', async () => {
|
|
@@ -954,7 +962,7 @@ describe('rag-analyzer', () => {
|
|
|
954
962
|
|
|
955
963
|
describe('gatherUnifiedContextForPR error handling', () => {
|
|
956
964
|
it('should handle file context gathering errors', async () => {
|
|
957
|
-
fs.
|
|
965
|
+
fs.promises.readFile.mockImplementation((path) => {
|
|
958
966
|
if (path.includes('error-file')) {
|
|
959
967
|
throw new Error('Read error');
|
|
960
968
|
}
|
|
@@ -1181,7 +1189,7 @@ describe('rag-analyzer', () => {
|
|
|
1181
1189
|
return <div>{data}</div>;
|
|
1182
1190
|
}
|
|
1183
1191
|
`;
|
|
1184
|
-
fs.
|
|
1192
|
+
fs.promises.readFile.mockResolvedValue(richCode);
|
|
1185
1193
|
setupSuccessfulLLMResponse();
|
|
1186
1194
|
const result = await runAnalysis('/test/Dashboard.jsx');
|
|
1187
1195
|
expect(result.success).toBe(true);
|