codecritique 2.0.6 → 2.0.7
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 +54 -18
- package/src/project-analyzer.test.js +73 -0
- package/src/rag-analyzer.js +66 -26
- 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) => {
|
|
@@ -400,24 +406,54 @@ export class ProjectAnalyzer {
|
|
|
400
406
|
});
|
|
401
407
|
|
|
402
408
|
// Take only the first 500 after sorting to ensure we have shallow files
|
|
403
|
-
const sortedFiles = allFiles.slice(0,
|
|
409
|
+
const sortedFiles = allFiles.slice(0, TERM_SEARCH_CONTENT_CANDIDATE_LIMIT);
|
|
410
|
+
if (sortedFiles.length === 0) {
|
|
411
|
+
return [];
|
|
412
|
+
}
|
|
404
413
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
|
|
414
|
+
const candidatePaths = sortedFiles.map((file) => file.path).filter(Boolean);
|
|
415
|
+
const contentByPath = new Map();
|
|
416
|
+
if (candidatePaths.length > 0) {
|
|
417
|
+
const pathList = candidatePaths.map((candidatePath) => `'${escapeSqlString(candidatePath)}'`).join(', ');
|
|
418
|
+
const contentResults = await table
|
|
419
|
+
.query()
|
|
420
|
+
.select(['path', 'content'])
|
|
421
|
+
.where(`${projectPathFilter} AND path IN (${pathList})`)
|
|
422
|
+
.limit(TERM_SEARCH_CONTENT_CANDIDATE_LIMIT)
|
|
423
|
+
.toArray();
|
|
424
|
+
|
|
425
|
+
for (const result of contentResults) {
|
|
426
|
+
contentByPath.set(result.path, result.content || '');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
412
429
|
|
|
413
|
-
|
|
414
|
-
|
|
430
|
+
return sortedFiles
|
|
431
|
+
.filter((result) => {
|
|
432
|
+
const content = (contentByPath.get(result.path) || '').toLowerCase();
|
|
433
|
+
const pathName = (result.path || '').toLowerCase();
|
|
434
|
+
const name = (result.name || '').toLowerCase();
|
|
435
|
+
const matches = config.terms.some(
|
|
436
|
+
(term) =>
|
|
437
|
+
content.includes(term.toLowerCase()) || pathName.includes(term.toLowerCase()) || name.includes(term.toLowerCase())
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
return matches;
|
|
441
|
+
})
|
|
442
|
+
.map((result) => {
|
|
443
|
+
return {
|
|
444
|
+
...result,
|
|
445
|
+
content: contentByPath.get(result.path) || '',
|
|
446
|
+
};
|
|
447
|
+
});
|
|
415
448
|
}
|
|
416
449
|
else {
|
|
417
|
-
|
|
450
|
+
return await table
|
|
451
|
+
.query()
|
|
452
|
+
.select(['path', 'name', 'content', 'type', 'language'])
|
|
453
|
+
.where(projectPathFilter)
|
|
454
|
+
.limit(config.limit || 30)
|
|
455
|
+
.toArray();
|
|
418
456
|
}
|
|
419
|
-
|
|
420
|
-
return await query.limit(config.limit || 30).toArray();
|
|
421
457
|
}
|
|
422
458
|
catch (error) {
|
|
423
459
|
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
|
|
|
@@ -458,11 +472,11 @@ async function runAnalysis(filePath, options = {}) {
|
|
|
458
472
|
if (options.diffOnly && options.diffContent) {
|
|
459
473
|
content = options.diffContent;
|
|
460
474
|
// For PR reviews, always read the full file content for context awareness
|
|
461
|
-
fullFileContent =
|
|
475
|
+
fullFileContent = await readTextFile(filePath);
|
|
462
476
|
verboseLog(options, chalk.blue(`Analyzing diff only for ${path.basename(filePath)}`));
|
|
463
477
|
}
|
|
464
478
|
else {
|
|
465
|
-
content =
|
|
479
|
+
content = await readTextFile(filePath);
|
|
466
480
|
fullFileContent = content;
|
|
467
481
|
verboseLog(options, chalk.blue(`Analyzing full file ${path.basename(filePath)}`));
|
|
468
482
|
}
|
|
@@ -835,6 +849,30 @@ MARKDOWN FORMATTING IN DESCRIPTIONS AND SUGGESTIONS:
|
|
|
835
849
|
Your response must start with { and end with } with no additional text.`;
|
|
836
850
|
}
|
|
837
851
|
|
|
852
|
+
function buildCachedProjectContextBlocks({ roleDefinition, customDocsSection, projectArchitectureSection }) {
|
|
853
|
+
const blocks = [];
|
|
854
|
+
|
|
855
|
+
if (roleDefinition?.trim()) {
|
|
856
|
+
blocks.push(`REVIEW ROLE AND PROJECT INSTRUCTIONS
|
|
857
|
+
|
|
858
|
+
${roleDefinition}`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (customDocsSection?.trim() && !customDocsSection.includes('No custom instructions provided.')) {
|
|
862
|
+
blocks.push(`CUSTOM PROJECT INSTRUCTIONS
|
|
863
|
+
|
|
864
|
+
${customDocsSection}`);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
if (projectArchitectureSection?.trim()) {
|
|
868
|
+
blocks.push(`PROJECT ARCHITECTURE CONTEXT
|
|
869
|
+
|
|
870
|
+
${projectArchitectureSection}`);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return blocks;
|
|
874
|
+
}
|
|
875
|
+
|
|
838
876
|
// LLM call function - sends prompt with cached system content for cost optimization
|
|
839
877
|
async function sendPromptToLLM(promptConfig, llmOptions) {
|
|
840
878
|
try {
|
|
@@ -946,12 +984,14 @@ async function sendPromptToLLM(promptConfig, llmOptions) {
|
|
|
946
984
|
verboseLog(llmOptions, chalk.gray(` Prompt config has systemPrompt: ${!!promptConfig.systemPrompt}`));
|
|
947
985
|
verboseLog(llmOptions, chalk.gray(` Prompt config has userPrompt: ${!!promptConfig.userPrompt}`));
|
|
948
986
|
verboseLog(llmOptions, chalk.gray(` System prompt length: ${promptConfig.systemPrompt?.length || 0} chars`));
|
|
987
|
+
verboseLog(llmOptions, chalk.gray(` Cached system blocks: ${promptConfig.cachedSystemBlocks?.length || 0}`));
|
|
949
988
|
verboseLog(llmOptions, chalk.gray(` User prompt length: ${promptConfig.userPrompt?.length || 0} chars`));
|
|
950
989
|
|
|
951
990
|
// Send prompt with system (cached) and user (dynamic) content
|
|
952
991
|
const response = await llm.sendPromptToClaude(promptConfig.userPrompt, {
|
|
953
992
|
...llmOptions,
|
|
954
993
|
system: promptConfig.systemPrompt,
|
|
994
|
+
cachedSystemBlocks: promptConfig.cachedSystemBlocks || [],
|
|
955
995
|
jsonSchema: codeReviewSchema,
|
|
956
996
|
});
|
|
957
997
|
|
|
@@ -1096,21 +1136,21 @@ ${addLineNumbers(file.content)}
|
|
|
1096
1136
|
|
|
1097
1137
|
// Build the cached system prompt (contains all static rules)
|
|
1098
1138
|
const systemPrompt = buildCachedSystemPrompt('code');
|
|
1139
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1140
|
+
roleDefinition,
|
|
1141
|
+
customDocsSection,
|
|
1142
|
+
projectArchitectureSection,
|
|
1143
|
+
});
|
|
1099
1144
|
|
|
1100
1145
|
// Build the dynamic user prompt (contains all variable content)
|
|
1101
1146
|
const userPrompt = finalizePrompt(`
|
|
1102
1147
|
## REVIEW TYPE: CODE FILE ANALYSIS
|
|
1103
1148
|
|
|
1104
|
-
${roleDefinition}
|
|
1105
|
-
|
|
1106
1149
|
${reviewInstructions}
|
|
1107
1150
|
|
|
1108
|
-
${customDocsSection}
|
|
1109
|
-
|
|
1110
1151
|
${fileSection}
|
|
1111
1152
|
|
|
1112
|
-
|
|
1113
|
-
${projectArchitectureSection}
|
|
1153
|
+
Use the cached project context, project architecture, and custom instructions when evaluating this file.
|
|
1114
1154
|
|
|
1115
1155
|
CONTEXT A: EXPLICIT GUIDELINES FROM DOCUMENTATION
|
|
1116
1156
|
${formattedGuidelines}
|
|
@@ -1136,7 +1176,7 @@ Analyze the FILE TO REVIEW above using the 4-stage methodology:
|
|
|
1136
1176
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1137
1177
|
`);
|
|
1138
1178
|
|
|
1139
|
-
return { systemPrompt, userPrompt };
|
|
1179
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1140
1180
|
}
|
|
1141
1181
|
|
|
1142
1182
|
/**
|
|
@@ -1215,22 +1255,22 @@ ${addLineNumbers(file.content)}
|
|
|
1215
1255
|
|
|
1216
1256
|
// Build the cached system prompt (contains all static rules)
|
|
1217
1257
|
const systemPrompt = buildCachedSystemPrompt('test');
|
|
1258
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1259
|
+
roleDefinition,
|
|
1260
|
+
customDocsSection,
|
|
1261
|
+
projectArchitectureSection,
|
|
1262
|
+
});
|
|
1218
1263
|
|
|
1219
1264
|
// Build the dynamic user prompt (contains all variable content)
|
|
1220
1265
|
const userPrompt = finalizePrompt(`
|
|
1221
1266
|
## REVIEW TYPE: TEST FILE ANALYSIS
|
|
1222
1267
|
|
|
1223
|
-
${roleDefinition}
|
|
1224
|
-
|
|
1225
1268
|
${reviewInstructions}
|
|
1226
1269
|
|
|
1227
1270
|
${fileSection}
|
|
1228
1271
|
|
|
1229
1272
|
## ANALYSIS CONTEXT
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
CONTEXT FROM PROJECT:
|
|
1233
|
-
${projectArchitectureSection}
|
|
1273
|
+
Use the cached project context, project architecture, and custom instructions when evaluating this test file.
|
|
1234
1274
|
|
|
1235
1275
|
CONTEXT A: TESTING GUIDELINES AND BEST PRACTICES
|
|
1236
1276
|
${formattedGuidelines}
|
|
@@ -1253,7 +1293,7 @@ Analyze the TEST FILE above using the 5-stage methodology:
|
|
|
1253
1293
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1254
1294
|
`);
|
|
1255
1295
|
|
|
1256
|
-
return { systemPrompt, userPrompt };
|
|
1296
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1257
1297
|
}
|
|
1258
1298
|
|
|
1259
1299
|
/**
|
|
@@ -1371,20 +1411,23 @@ For each file in this PR, you have access to:
|
|
|
1371
1411
|
|
|
1372
1412
|
// Build the cached system prompt (contains all static rules)
|
|
1373
1413
|
const systemPrompt = buildCachedSystemPrompt('pr');
|
|
1414
|
+
const cachedSystemBlocks = buildCachedProjectContextBlocks({
|
|
1415
|
+
roleDefinition,
|
|
1416
|
+
customDocsSection,
|
|
1417
|
+
projectArchitectureSection,
|
|
1418
|
+
});
|
|
1374
1419
|
|
|
1375
1420
|
// Build the dynamic user prompt (contains all variable content)
|
|
1376
1421
|
const userPrompt = finalizePrompt(`
|
|
1377
1422
|
## REVIEW TYPE: HOLISTIC PR ANALYSIS
|
|
1378
1423
|
|
|
1379
|
-
${roleDefinition}
|
|
1380
|
-
|
|
1381
1424
|
## PULL REQUEST OVERVIEW
|
|
1382
1425
|
- **Total Files**: ${prFiles.length}
|
|
1383
1426
|
- **Source Files**: ${prFiles.filter((f) => !f.isTest).length}
|
|
1384
1427
|
- **Test Files**: ${prFiles.filter((f) => f.isTest).length}
|
|
1385
1428
|
|
|
1386
1429
|
## UNIFIED CONTEXT FROM PROJECT
|
|
1387
|
-
|
|
1430
|
+
Use the cached project context, project architecture, and custom instructions when evaluating this pull request.
|
|
1388
1431
|
|
|
1389
1432
|
### PROJECT CODE EXAMPLES
|
|
1390
1433
|
${formattedCodeExamples}
|
|
@@ -1398,9 +1441,6 @@ ${formattedPRComments}
|
|
|
1398
1441
|
## PR FILES WITH CHANGES
|
|
1399
1442
|
${formattedPRFiles}
|
|
1400
1443
|
|
|
1401
|
-
## CUSTOM INSTRUCTIONS
|
|
1402
|
-
${customDocsSection}
|
|
1403
|
-
|
|
1404
1444
|
## YOUR TASK
|
|
1405
1445
|
|
|
1406
1446
|
Analyze ALL PR FILES above using the 5-stage methodology:
|
|
@@ -1418,7 +1458,7 @@ Analyze ALL PR FILES above using the 5-stage methodology:
|
|
|
1418
1458
|
- If you analyzed something and found it's fine, simply don't include it in the output
|
|
1419
1459
|
`);
|
|
1420
1460
|
|
|
1421
|
-
return { systemPrompt, userPrompt };
|
|
1461
|
+
return { systemPrompt, userPrompt, cachedSystemBlocks };
|
|
1422
1462
|
}
|
|
1423
1463
|
|
|
1424
1464
|
/**
|
|
@@ -1487,7 +1527,7 @@ async function getPRCommentContext(filePath, options = {}) {
|
|
|
1487
1527
|
verboseLog(options, chalk.blue(`🔍 Using pre-computed query embedding for PR comment search`));
|
|
1488
1528
|
// We still need the file content for the search function, but not for embedding
|
|
1489
1529
|
try {
|
|
1490
|
-
fileContent =
|
|
1530
|
+
fileContent = await readTextFile(filePath);
|
|
1491
1531
|
const maxEmbeddingLength = 8000; // Keep consistent with original truncation
|
|
1492
1532
|
contentForSearch = fileContent.length > maxEmbeddingLength ? fileContent.substring(0, maxEmbeddingLength) : fileContent;
|
|
1493
1533
|
}
|
|
@@ -1505,7 +1545,7 @@ async function getPRCommentContext(filePath, options = {}) {
|
|
|
1505
1545
|
else {
|
|
1506
1546
|
// Fallback to original behavior if no pre-computed embedding provided
|
|
1507
1547
|
try {
|
|
1508
|
-
fileContent =
|
|
1548
|
+
fileContent = await readTextFile(filePath);
|
|
1509
1549
|
}
|
|
1510
1550
|
catch (readError) {
|
|
1511
1551
|
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);
|