@kb-labs/review-llm 0.5.0
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 +23 -0
- package/dist/analyzers/architecture-analyzer.d.ts +22 -0
- package/dist/analyzers/architecture-analyzer.js +226 -0
- package/dist/analyzers/architecture-analyzer.js.map +1 -0
- package/dist/analyzers/naming-analyzer.d.ts +22 -0
- package/dist/analyzers/naming-analyzer.js +200 -0
- package/dist/analyzers/naming-analyzer.js.map +1 -0
- package/dist/analyzers/security-analyzer.d.ts +35 -0
- package/dist/analyzers/security-analyzer.js +261 -0
- package/dist/analyzers/security-analyzer.js.map +1 -0
- package/dist/index.d.ts +558 -0
- package/dist/index.js +2020 -0
- package/dist/index.js.map +1 -0
- package/package.json +58 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2020 @@
|
|
|
1
|
+
import { BaseLLMAnalyzer } from '@kb-labs/review-contracts';
|
|
2
|
+
export { BaseLLMAnalyzer } from '@kb-labs/review-contracts';
|
|
3
|
+
import { useCache, useAnalytics, useLLM, useLogger, useConfig } from '@kb-labs/sdk';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { readFile, access, readdir } from 'fs/promises';
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
|
|
9
|
+
// src/prompts/architecture.prompts.ts
|
|
10
|
+
function buildSystemPrompt(context) {
|
|
11
|
+
const sections = [
|
|
12
|
+
"You are an expert code reviewer specializing in software architecture.",
|
|
13
|
+
"",
|
|
14
|
+
"**Your role:**",
|
|
15
|
+
"- Identify architecture violations and anti-patterns",
|
|
16
|
+
"- Check adherence to project conventions",
|
|
17
|
+
"- Suggest improvements with clear rationale",
|
|
18
|
+
"",
|
|
19
|
+
"**CRITICAL RULES:**",
|
|
20
|
+
"1. ONLY report issues that exist in the provided code",
|
|
21
|
+
"2. Reference line numbers that actually exist in the file",
|
|
22
|
+
"3. Quote exact code snippets when referencing issues",
|
|
23
|
+
"4. Provide concrete, actionable suggestions",
|
|
24
|
+
"5. Reference project conventions and ADRs when applicable",
|
|
25
|
+
""
|
|
26
|
+
];
|
|
27
|
+
for (const [category, content] of Object.entries(context.conventions)) {
|
|
28
|
+
sections.push(`**Project Conventions (${category}):**`);
|
|
29
|
+
sections.push(content);
|
|
30
|
+
sections.push("");
|
|
31
|
+
}
|
|
32
|
+
if (context.taskContext) {
|
|
33
|
+
sections.push("**Current Task Context:**");
|
|
34
|
+
sections.push(context.taskContext);
|
|
35
|
+
sections.push("");
|
|
36
|
+
sections.push("IMPORTANT: Evaluate architecture decisions in the context of this specific task.");
|
|
37
|
+
sections.push("");
|
|
38
|
+
}
|
|
39
|
+
if (context.repoScope && context.repoScope.length > 0) {
|
|
40
|
+
sections.push("**Repositories in Scope:**");
|
|
41
|
+
sections.push(context.repoScope.map((r) => `- ${r}`).join("\n"));
|
|
42
|
+
sections.push("");
|
|
43
|
+
sections.push("NOTE: Changes may span multiple repositories as part of one logical task.");
|
|
44
|
+
sections.push("");
|
|
45
|
+
}
|
|
46
|
+
if (context.relatedADRs.length > 0) {
|
|
47
|
+
sections.push("**Related Architecture Decisions:**");
|
|
48
|
+
for (const adr of context.relatedADRs) {
|
|
49
|
+
sections.push(`- ${adr.id}: ${adr.title} - ${adr.summary}`);
|
|
50
|
+
}
|
|
51
|
+
sections.push("");
|
|
52
|
+
}
|
|
53
|
+
if (context.examples.length > 0) {
|
|
54
|
+
sections.push("**Examples from codebase (informative, not normative):**");
|
|
55
|
+
for (const ex of context.examples.slice(0, 3)) {
|
|
56
|
+
sections.push(`- ${ex.file}: ${ex.description}`);
|
|
57
|
+
}
|
|
58
|
+
sections.push("");
|
|
59
|
+
}
|
|
60
|
+
sections.push("Use the report_architecture_finding tool to report issues.");
|
|
61
|
+
return sections.join("\n");
|
|
62
|
+
}
|
|
63
|
+
function analyzeFile(file) {
|
|
64
|
+
const lines = file.content.split("\n");
|
|
65
|
+
const numbered = lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
66
|
+
return `Review this file for architecture issues:
|
|
67
|
+
|
|
68
|
+
File: ${file.path}
|
|
69
|
+
Language: ${file.language}
|
|
70
|
+
|
|
71
|
+
\`\`\`${file.language}
|
|
72
|
+
${numbered}
|
|
73
|
+
\`\`\`
|
|
74
|
+
|
|
75
|
+
Report any architecture violations, anti-patterns, or violations of project conventions.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// src/analyzers/architecture-analyzer.ts
|
|
79
|
+
var ARCHITECTURE_FINDING_TOOL = {
|
|
80
|
+
name: "report_architecture_finding",
|
|
81
|
+
description: "Report an architecture issue or suggestion",
|
|
82
|
+
inputSchema: {
|
|
83
|
+
type: "object",
|
|
84
|
+
properties: {
|
|
85
|
+
severity: {
|
|
86
|
+
type: "string",
|
|
87
|
+
enum: ["error", "warning", "info"],
|
|
88
|
+
description: "Severity level of the finding"
|
|
89
|
+
},
|
|
90
|
+
message: {
|
|
91
|
+
type: "string",
|
|
92
|
+
description: "Clear description of the architecture issue"
|
|
93
|
+
},
|
|
94
|
+
line: {
|
|
95
|
+
type: "number",
|
|
96
|
+
description: "Line number where issue occurs (must exist in file)"
|
|
97
|
+
},
|
|
98
|
+
suggestion: {
|
|
99
|
+
type: "string",
|
|
100
|
+
description: "Concrete fix suggestion with code example"
|
|
101
|
+
},
|
|
102
|
+
rationale: {
|
|
103
|
+
type: "string",
|
|
104
|
+
description: "Why this is an issue (reference conventions/ADRs if applicable)"
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
required: ["severity", "message", "line", "suggestion", "rationale"]
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
function mapSeverity(llmSeverity) {
|
|
111
|
+
switch (llmSeverity) {
|
|
112
|
+
case "error":
|
|
113
|
+
return "high";
|
|
114
|
+
case "warning":
|
|
115
|
+
return "medium";
|
|
116
|
+
case "info":
|
|
117
|
+
return "info";
|
|
118
|
+
default:
|
|
119
|
+
return "medium";
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
var ArchitectureAnalyzer = class extends BaseLLMAnalyzer {
|
|
123
|
+
id = "architecture";
|
|
124
|
+
name = "Architecture Analysis";
|
|
125
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex LLM-based analysis with caching, error handling, and result validation
|
|
126
|
+
async analyze(files, context) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
const cache = useCache();
|
|
129
|
+
const analytics = useAnalytics();
|
|
130
|
+
for (const file of files) {
|
|
131
|
+
const cacheKey = this.generateCacheKey(file, context.preset);
|
|
132
|
+
if (cache) {
|
|
133
|
+
const cached = await cache.get(cacheKey);
|
|
134
|
+
if (cached) {
|
|
135
|
+
findings.push(...cached);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const systemPrompt = buildSystemPrompt(context);
|
|
140
|
+
const userPrompt = analyzeFile(file);
|
|
141
|
+
const llm = useLLM({ tier: "medium" });
|
|
142
|
+
if (!llm?.chatWithTools) {
|
|
143
|
+
throw new Error("LLM not configured or does not support tool calling");
|
|
144
|
+
}
|
|
145
|
+
try {
|
|
146
|
+
const response = await llm.chatWithTools(
|
|
147
|
+
[
|
|
148
|
+
{ role: "system", content: systemPrompt },
|
|
149
|
+
{ role: "user", content: userPrompt }
|
|
150
|
+
],
|
|
151
|
+
{
|
|
152
|
+
tools: [ARCHITECTURE_FINDING_TOOL],
|
|
153
|
+
temperature: 0.2
|
|
154
|
+
// Low temp for consistency
|
|
155
|
+
}
|
|
156
|
+
);
|
|
157
|
+
if (!response || typeof response !== "object") {
|
|
158
|
+
useLogger()?.debug(`[ArchitectureAnalyzer] Invalid response from LLM for ${file.path}`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (analytics && response.usage) {
|
|
162
|
+
await analytics.track("review.architecture.complete", {
|
|
163
|
+
file: file.path,
|
|
164
|
+
tokensUsed: (response.usage.promptTokens ?? 0) + (response.usage.completionTokens ?? 0),
|
|
165
|
+
toolCalls: response.toolCalls?.length ?? 0
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
const fileFindings = this.processToolCalls(response.toolCalls || [], file);
|
|
169
|
+
findings.push(...fileFindings);
|
|
170
|
+
if (cache) {
|
|
171
|
+
await cache.set(cacheKey, fileFindings, 864e5);
|
|
172
|
+
}
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
175
|
+
const errorName = error instanceof Error ? error.name : "UnknownError";
|
|
176
|
+
useLogger()?.debug(`[ArchitectureAnalyzer] Failed to analyze ${file.path}:`, {
|
|
177
|
+
error,
|
|
178
|
+
errorName,
|
|
179
|
+
errorMessage
|
|
180
|
+
});
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return findings;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Process LLM tool calls into ReviewFindings
|
|
188
|
+
*/
|
|
189
|
+
processToolCalls(toolCalls, file) {
|
|
190
|
+
const findings = [];
|
|
191
|
+
for (const call of toolCalls) {
|
|
192
|
+
if (call.name === "report_architecture_finding") {
|
|
193
|
+
const args = call.input;
|
|
194
|
+
const lineCount = file.content.split("\n").length;
|
|
195
|
+
const line = typeof args.line === "number" ? args.line : parseInt(args.line, 10);
|
|
196
|
+
if (isNaN(line) || line < 1 || line > lineCount) {
|
|
197
|
+
useLogger()?.debug(`[ArchitectureAnalyzer] Invalid line number ${args.line} for ${file.path}`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const severity = mapSeverity(String(args.severity || "warning"));
|
|
201
|
+
findings.push({
|
|
202
|
+
id: this.buildFindingId(file, line, "arch"),
|
|
203
|
+
ruleId: "llm:architecture",
|
|
204
|
+
type: "architecture",
|
|
205
|
+
severity,
|
|
206
|
+
confidence: "likely",
|
|
207
|
+
// LLM findings are "likely", not "certain"
|
|
208
|
+
file: file.path,
|
|
209
|
+
line,
|
|
210
|
+
message: String(args.message || "").slice(0, 500),
|
|
211
|
+
suggestion: args.suggestion ? String(args.suggestion).slice(0, 1e3) : void 0,
|
|
212
|
+
rationale: args.rationale ? String(args.rationale).slice(0, 500) : void 0,
|
|
213
|
+
engine: "llm",
|
|
214
|
+
source: "llm-architecture",
|
|
215
|
+
// For agent mode gating
|
|
216
|
+
scope: "local",
|
|
217
|
+
// Architecture can be local or global
|
|
218
|
+
automated: false
|
|
219
|
+
// LLM suggestions need human review
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return findings;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
// src/prompts/security.prompts.ts
|
|
228
|
+
function buildSystemPrompt2(context) {
|
|
229
|
+
const sections = [
|
|
230
|
+
"You are an expert security auditor specializing in application security.",
|
|
231
|
+
"",
|
|
232
|
+
"**Your role:**",
|
|
233
|
+
"- Identify security vulnerabilities and weaknesses",
|
|
234
|
+
"- Check for common security anti-patterns (OWASP Top 10)",
|
|
235
|
+
"- Suggest secure coding practices with clear rationale",
|
|
236
|
+
"",
|
|
237
|
+
"**CRITICAL RULES:**",
|
|
238
|
+
"1. ONLY report security issues that exist in the provided code",
|
|
239
|
+
"2. Reference line numbers that actually exist in the file",
|
|
240
|
+
"3. Quote exact code snippets when referencing issues",
|
|
241
|
+
"4. Classify severity accurately (error = exploitable, warning = weakness)",
|
|
242
|
+
"5. Provide concrete, actionable mitigation steps",
|
|
243
|
+
"",
|
|
244
|
+
"**Focus areas:**",
|
|
245
|
+
"- SQL injection, XSS, CSRF",
|
|
246
|
+
"- Authentication/authorization bypass",
|
|
247
|
+
"- Insecure cryptography",
|
|
248
|
+
"- Secrets in code",
|
|
249
|
+
"- Command injection",
|
|
250
|
+
"- Path traversal",
|
|
251
|
+
"- Unsafe deserialization",
|
|
252
|
+
""
|
|
253
|
+
];
|
|
254
|
+
for (const [category, content] of Object.entries(context.conventions)) {
|
|
255
|
+
sections.push(`**Project Conventions (${category}):**`);
|
|
256
|
+
sections.push(content);
|
|
257
|
+
sections.push("");
|
|
258
|
+
}
|
|
259
|
+
if (context.taskContext) {
|
|
260
|
+
sections.push("**Current Task Context:**");
|
|
261
|
+
sections.push(context.taskContext);
|
|
262
|
+
sections.push("");
|
|
263
|
+
sections.push("IMPORTANT: Pay special attention to security implications in the context of this task.");
|
|
264
|
+
sections.push("");
|
|
265
|
+
}
|
|
266
|
+
if (context.repoScope && context.repoScope.length > 0) {
|
|
267
|
+
sections.push("**Repositories in Scope:**");
|
|
268
|
+
sections.push(context.repoScope.map((r) => `- ${r}`).join("\n"));
|
|
269
|
+
sections.push("");
|
|
270
|
+
sections.push("NOTE: Check for security issues that may arise from cross-repo interactions.");
|
|
271
|
+
sections.push("");
|
|
272
|
+
}
|
|
273
|
+
sections.push("Use the report_security_finding tool to report vulnerabilities.");
|
|
274
|
+
return sections.join("\n");
|
|
275
|
+
}
|
|
276
|
+
function analyzeFile2(file) {
|
|
277
|
+
const lines = file.content.split("\n");
|
|
278
|
+
const numbered = lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
279
|
+
return `Review this file for security vulnerabilities:
|
|
280
|
+
|
|
281
|
+
File: ${file.path}
|
|
282
|
+
Language: ${file.language}
|
|
283
|
+
|
|
284
|
+
\`\`\`${file.language}
|
|
285
|
+
${numbered}
|
|
286
|
+
\`\`\`
|
|
287
|
+
|
|
288
|
+
Report any security vulnerabilities or weaknesses. Focus on exploitable issues.`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// src/analyzers/security-analyzer.ts
|
|
292
|
+
var SECURITY_FINDING_TOOL = {
|
|
293
|
+
name: "report_security_finding",
|
|
294
|
+
description: "Report a security vulnerability or weakness",
|
|
295
|
+
inputSchema: {
|
|
296
|
+
type: "object",
|
|
297
|
+
properties: {
|
|
298
|
+
severity: {
|
|
299
|
+
type: "string",
|
|
300
|
+
enum: ["blocker", "high", "medium", "low"],
|
|
301
|
+
description: "Severity level (blocker=exploitable, high=serious weakness)"
|
|
302
|
+
},
|
|
303
|
+
vulnerabilityType: {
|
|
304
|
+
type: "string",
|
|
305
|
+
description: "Type of vulnerability (e.g., SQL injection, XSS, CSRF)"
|
|
306
|
+
},
|
|
307
|
+
message: {
|
|
308
|
+
type: "string",
|
|
309
|
+
description: "Clear description of the security issue"
|
|
310
|
+
},
|
|
311
|
+
line: {
|
|
312
|
+
type: "number",
|
|
313
|
+
description: "Line number where vulnerability occurs"
|
|
314
|
+
},
|
|
315
|
+
mitigation: {
|
|
316
|
+
type: "string",
|
|
317
|
+
description: "Concrete mitigation steps with code example"
|
|
318
|
+
},
|
|
319
|
+
impact: {
|
|
320
|
+
type: "string",
|
|
321
|
+
description: "Potential impact if exploited"
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
required: ["severity", "vulnerabilityType", "message", "line", "mitigation", "impact"]
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
var VALID_SEVERITIES = ["blocker", "high", "medium", "low"];
|
|
328
|
+
var MAX_RETRIES = 3;
|
|
329
|
+
var INITIAL_DELAY_MS = 1e3;
|
|
330
|
+
var BATCH_SIZE = 5;
|
|
331
|
+
function sleep(ms) {
|
|
332
|
+
return new Promise((resolve2) => {
|
|
333
|
+
setTimeout(resolve2, ms);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
var SecurityAnalyzer = class extends BaseLLMAnalyzer {
|
|
337
|
+
id = "security";
|
|
338
|
+
name = "Security Analysis";
|
|
339
|
+
/** Track failed files for reporting */
|
|
340
|
+
failedFiles = [];
|
|
341
|
+
async analyze(files, context) {
|
|
342
|
+
const findings = [];
|
|
343
|
+
const cache = useCache();
|
|
344
|
+
this.failedFiles = [];
|
|
345
|
+
const filesToAnalyze = [];
|
|
346
|
+
for (const file of files) {
|
|
347
|
+
const cacheKey = this.generateCacheKey(file, context.preset);
|
|
348
|
+
if (cache) {
|
|
349
|
+
const cached = await cache.get(cacheKey);
|
|
350
|
+
if (cached) {
|
|
351
|
+
findings.push(...cached);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
filesToAnalyze.push(file);
|
|
356
|
+
}
|
|
357
|
+
for (let i = 0; i < filesToAnalyze.length; i += BATCH_SIZE) {
|
|
358
|
+
const batch = filesToAnalyze.slice(i, i + BATCH_SIZE);
|
|
359
|
+
const batchResults = await Promise.allSettled(
|
|
360
|
+
batch.map((file) => this.analyzeFile(file, context, cache))
|
|
361
|
+
);
|
|
362
|
+
for (let j = 0; j < batchResults.length; j++) {
|
|
363
|
+
const result = batchResults[j];
|
|
364
|
+
const file = batch[j];
|
|
365
|
+
if (result.status === "fulfilled") {
|
|
366
|
+
findings.push(...result.value);
|
|
367
|
+
} else {
|
|
368
|
+
this.failedFiles.push({ file: file.path, error: result.reason?.message ?? "Unknown error" });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (this.failedFiles.length > 0) {
|
|
373
|
+
useLogger()?.debug(`[SecurityAnalyzer] Failed to analyze ${this.failedFiles.length} file(s):`, {
|
|
374
|
+
files: this.failedFiles
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return findings;
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Analyze a single file with retry logic
|
|
381
|
+
*/
|
|
382
|
+
async analyzeFile(file, context, cache) {
|
|
383
|
+
const analytics = useAnalytics();
|
|
384
|
+
const systemPrompt = buildSystemPrompt2(context);
|
|
385
|
+
const userPrompt = analyzeFile2(file);
|
|
386
|
+
const llm = useLLM({ tier: "medium" });
|
|
387
|
+
if (!llm?.chatWithTools) {
|
|
388
|
+
throw new Error("LLM not configured or does not support tool calling");
|
|
389
|
+
}
|
|
390
|
+
let lastError = null;
|
|
391
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
392
|
+
try {
|
|
393
|
+
const response = await llm.chatWithTools(
|
|
394
|
+
[
|
|
395
|
+
{ role: "system", content: systemPrompt },
|
|
396
|
+
{ role: "user", content: userPrompt }
|
|
397
|
+
],
|
|
398
|
+
{
|
|
399
|
+
tools: [SECURITY_FINDING_TOOL],
|
|
400
|
+
temperature: 0.1
|
|
401
|
+
}
|
|
402
|
+
);
|
|
403
|
+
if (analytics) {
|
|
404
|
+
await analytics.track("review.security.complete", {
|
|
405
|
+
file: file.path,
|
|
406
|
+
tokensUsed: response.usage.promptTokens + response.usage.completionTokens,
|
|
407
|
+
toolCalls: response.toolCalls?.length ?? 0,
|
|
408
|
+
attempts: attempt
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
const fileFindings = this.processToolCalls(response.toolCalls || [], file);
|
|
412
|
+
if (cache) {
|
|
413
|
+
const cacheKey = this.generateCacheKey(file, context.preset);
|
|
414
|
+
await cache.set(cacheKey, fileFindings, 864e5);
|
|
415
|
+
}
|
|
416
|
+
return fileFindings;
|
|
417
|
+
} catch (error) {
|
|
418
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
419
|
+
useLogger()?.debug(`[SecurityAnalyzer] Attempt ${attempt}/${MAX_RETRIES} failed for ${file.path}:`, { error });
|
|
420
|
+
if (attempt < MAX_RETRIES) {
|
|
421
|
+
const delay = INITIAL_DELAY_MS * Math.pow(2, attempt - 1);
|
|
422
|
+
await sleep(delay);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
throw lastError ?? new Error("Analysis failed");
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Get list of files that failed analysis
|
|
430
|
+
*/
|
|
431
|
+
getFailedFiles() {
|
|
432
|
+
return [...this.failedFiles];
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Process LLM tool calls into ReviewFindings
|
|
436
|
+
*/
|
|
437
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex validation and mapping of LLM tool call results to structured findings
|
|
438
|
+
processToolCalls(toolCalls, file) {
|
|
439
|
+
const findings = [];
|
|
440
|
+
for (const call of toolCalls) {
|
|
441
|
+
if (call.name === "report_security_finding") {
|
|
442
|
+
const args = call.input;
|
|
443
|
+
const lineCount = file.content.split("\n").length;
|
|
444
|
+
const line = typeof args.line === "number" ? args.line : parseInt(args.line, 10);
|
|
445
|
+
if (isNaN(line) || line < 1 || line > lineCount) {
|
|
446
|
+
useLogger()?.debug(`[SecurityAnalyzer] Invalid line number ${args.line} for ${file.path}`);
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
const severity = args.severity;
|
|
450
|
+
if (!VALID_SEVERITIES.includes(severity)) {
|
|
451
|
+
useLogger()?.debug(`[SecurityAnalyzer] Invalid severity "${severity}" for finding in ${file.path}:${line}`);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const vulnerabilityType = String(args.vulnerabilityType || "unknown").replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 50);
|
|
455
|
+
findings.push({
|
|
456
|
+
id: this.buildFindingId(file, line, "sec"),
|
|
457
|
+
ruleId: `llm:security:${vulnerabilityType}`,
|
|
458
|
+
type: "security",
|
|
459
|
+
severity,
|
|
460
|
+
confidence: "likely",
|
|
461
|
+
// LLM findings are "likely", not "certain"
|
|
462
|
+
file: file.path,
|
|
463
|
+
line,
|
|
464
|
+
message: `[${vulnerabilityType}] ${String(args.message || "").slice(0, 500)}`,
|
|
465
|
+
suggestion: args.mitigation ? String(args.mitigation).slice(0, 1e3) : void 0,
|
|
466
|
+
rationale: args.impact ? `Impact: ${String(args.impact).slice(0, 500)}` : void 0,
|
|
467
|
+
engine: "llm",
|
|
468
|
+
source: "llm-security",
|
|
469
|
+
// For agent mode gating
|
|
470
|
+
scope: "local",
|
|
471
|
+
automated: false
|
|
472
|
+
// Security fixes need human review
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return findings;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// src/prompts/naming.prompts.ts
|
|
481
|
+
function buildSystemPrompt3(context) {
|
|
482
|
+
const sections = [
|
|
483
|
+
"You are a code quality expert specializing in naming conventions and code clarity.",
|
|
484
|
+
"",
|
|
485
|
+
"**Your role:**",
|
|
486
|
+
"- Check adherence to naming conventions",
|
|
487
|
+
"- Identify unclear or misleading names",
|
|
488
|
+
"- Suggest improvements for readability",
|
|
489
|
+
"",
|
|
490
|
+
"**CRITICAL RULES:**",
|
|
491
|
+
"1. ONLY report names that violate project conventions",
|
|
492
|
+
"2. Reference line numbers that actually exist",
|
|
493
|
+
"3. Quote exact names when referencing issues",
|
|
494
|
+
"4. Provide concrete, better alternatives",
|
|
495
|
+
"5. Focus on clarity and consistency",
|
|
496
|
+
"",
|
|
497
|
+
"**Common issues:**",
|
|
498
|
+
"- Inconsistent casing (camelCase vs snake_case)",
|
|
499
|
+
"- Abbreviations without clear meaning",
|
|
500
|
+
"- Generic names (data, temp, obj)",
|
|
501
|
+
"- Misleading names (opposite of actual behavior)",
|
|
502
|
+
""
|
|
503
|
+
];
|
|
504
|
+
for (const [category, content] of Object.entries(context.conventions)) {
|
|
505
|
+
sections.push(`**Project Conventions (${category}):**`);
|
|
506
|
+
sections.push(content);
|
|
507
|
+
sections.push("");
|
|
508
|
+
}
|
|
509
|
+
if (context.taskContext) {
|
|
510
|
+
sections.push("**Current Task Context:**");
|
|
511
|
+
sections.push(context.taskContext);
|
|
512
|
+
sections.push("");
|
|
513
|
+
sections.push("IMPORTANT: Consider naming in the context of this specific task and its domain.");
|
|
514
|
+
sections.push("");
|
|
515
|
+
}
|
|
516
|
+
if (context.repoScope && context.repoScope.length > 0) {
|
|
517
|
+
sections.push("**Repositories in Scope:**");
|
|
518
|
+
sections.push(context.repoScope.map((r) => `- ${r}`).join("\n"));
|
|
519
|
+
sections.push("");
|
|
520
|
+
sections.push("NOTE: Ensure naming consistency across these repositories.");
|
|
521
|
+
sections.push("");
|
|
522
|
+
}
|
|
523
|
+
sections.push("Use the report_naming_finding tool to report naming issues.");
|
|
524
|
+
return sections.join("\n");
|
|
525
|
+
}
|
|
526
|
+
function analyzeFile3(file) {
|
|
527
|
+
const lines = file.content.split("\n");
|
|
528
|
+
const numbered = lines.map((line, idx) => `${idx + 1}: ${line}`).join("\n");
|
|
529
|
+
return `Review this file for naming convention issues:
|
|
530
|
+
|
|
531
|
+
File: ${file.path}
|
|
532
|
+
Language: ${file.language}
|
|
533
|
+
|
|
534
|
+
\`\`\`${file.language}
|
|
535
|
+
${numbered}
|
|
536
|
+
\`\`\`
|
|
537
|
+
|
|
538
|
+
Report naming issues that reduce code clarity or violate conventions.`;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/analyzers/naming-analyzer.ts
|
|
542
|
+
var NAMING_FINDING_TOOL = {
|
|
543
|
+
name: "report_naming_finding",
|
|
544
|
+
description: "Report a naming convention issue",
|
|
545
|
+
inputSchema: {
|
|
546
|
+
type: "object",
|
|
547
|
+
properties: {
|
|
548
|
+
severity: {
|
|
549
|
+
type: "string",
|
|
550
|
+
enum: ["warning", "info"],
|
|
551
|
+
description: "Severity level (naming issues are typically warning/info)"
|
|
552
|
+
},
|
|
553
|
+
currentName: {
|
|
554
|
+
type: "string",
|
|
555
|
+
description: "Current name that violates conventions"
|
|
556
|
+
},
|
|
557
|
+
suggestedName: {
|
|
558
|
+
type: "string",
|
|
559
|
+
description: "Suggested better name"
|
|
560
|
+
},
|
|
561
|
+
message: {
|
|
562
|
+
type: "string",
|
|
563
|
+
description: "Description of the naming issue"
|
|
564
|
+
},
|
|
565
|
+
line: {
|
|
566
|
+
type: "number",
|
|
567
|
+
description: "Line number where name appears"
|
|
568
|
+
},
|
|
569
|
+
rationale: {
|
|
570
|
+
type: "string",
|
|
571
|
+
description: "Why the current name is problematic"
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
required: ["severity", "currentName", "suggestedName", "message", "line", "rationale"]
|
|
575
|
+
}
|
|
576
|
+
};
|
|
577
|
+
var NamingAnalyzer = class extends BaseLLMAnalyzer {
|
|
578
|
+
id = "naming";
|
|
579
|
+
name = "Naming Conventions";
|
|
580
|
+
async analyze(files, context) {
|
|
581
|
+
const findings = [];
|
|
582
|
+
const cache = useCache();
|
|
583
|
+
const analytics = useAnalytics();
|
|
584
|
+
for (const file of files) {
|
|
585
|
+
const cacheKey = this.generateCacheKey(file, context.preset);
|
|
586
|
+
if (cache) {
|
|
587
|
+
const cached = await cache.get(cacheKey);
|
|
588
|
+
if (cached) {
|
|
589
|
+
findings.push(...cached);
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const systemPrompt = buildSystemPrompt3(context);
|
|
594
|
+
const userPrompt = analyzeFile3(file);
|
|
595
|
+
const llm = useLLM({ tier: "small" });
|
|
596
|
+
if (!llm?.chatWithTools) {
|
|
597
|
+
throw new Error("LLM not configured or does not support tool calling");
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const response = await llm.chatWithTools(
|
|
601
|
+
[
|
|
602
|
+
{ role: "system", content: systemPrompt },
|
|
603
|
+
{ role: "user", content: userPrompt }
|
|
604
|
+
],
|
|
605
|
+
{
|
|
606
|
+
tools: [NAMING_FINDING_TOOL],
|
|
607
|
+
temperature: 0.2
|
|
608
|
+
// Low temp for consistency
|
|
609
|
+
}
|
|
610
|
+
);
|
|
611
|
+
if (analytics) {
|
|
612
|
+
await analytics.track("review.naming.complete", {
|
|
613
|
+
file: file.path,
|
|
614
|
+
tokensUsed: response.usage.promptTokens + response.usage.completionTokens,
|
|
615
|
+
toolCalls: response.toolCalls?.length ?? 0
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
const fileFindings = this.processToolCalls(response.toolCalls || [], file);
|
|
619
|
+
findings.push(...fileFindings);
|
|
620
|
+
if (cache) {
|
|
621
|
+
await cache.set(cacheKey, fileFindings, 864e5);
|
|
622
|
+
}
|
|
623
|
+
} catch (error) {
|
|
624
|
+
useLogger()?.debug(`[NamingAnalyzer] Failed to analyze ${file.path}:`, { error });
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return findings;
|
|
629
|
+
}
|
|
630
|
+
/**
|
|
631
|
+
* Process LLM tool calls into ReviewFindings
|
|
632
|
+
*/
|
|
633
|
+
processToolCalls(toolCalls, file) {
|
|
634
|
+
const findings = [];
|
|
635
|
+
for (const call of toolCalls) {
|
|
636
|
+
if (call.name === "report_naming_finding") {
|
|
637
|
+
const args = call.input;
|
|
638
|
+
const lineCount = file.content.split("\n").length;
|
|
639
|
+
const line = typeof args.line === "number" ? args.line : parseInt(args.line, 10);
|
|
640
|
+
if (isNaN(line) || line < 1 || line > lineCount) {
|
|
641
|
+
useLogger()?.debug(`[NamingAnalyzer] Invalid line number ${args.line} for ${file.path}`);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
const severity = args.severity === "warning" ? "medium" : "info";
|
|
645
|
+
const currentName = String(args.currentName || "").slice(0, 100);
|
|
646
|
+
const suggestedName = String(args.suggestedName || "").slice(0, 100);
|
|
647
|
+
findings.push({
|
|
648
|
+
id: this.buildFindingId(file, line, "naming"),
|
|
649
|
+
ruleId: "llm:naming",
|
|
650
|
+
type: "style",
|
|
651
|
+
severity,
|
|
652
|
+
confidence: "heuristic",
|
|
653
|
+
// Naming is subjective, low confidence
|
|
654
|
+
file: file.path,
|
|
655
|
+
line,
|
|
656
|
+
message: String(args.message || "").slice(0, 500),
|
|
657
|
+
suggestion: `Rename '${currentName}' to '${suggestedName}'`,
|
|
658
|
+
rationale: args.rationale ? String(args.rationale).slice(0, 500) : void 0,
|
|
659
|
+
engine: "llm",
|
|
660
|
+
source: "llm-naming",
|
|
661
|
+
// For agent mode gating
|
|
662
|
+
scope: "local",
|
|
663
|
+
automated: false
|
|
664
|
+
// Naming suggestions are optional
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
return findings;
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
function buildToolDefinitions(validCategories, validRuleIds = []) {
|
|
672
|
+
const ruleIdDescription = validRuleIds.length > 0 ? `Rule ID if matches a project rule. Available rules: ${validRuleIds.join(", ")}. Use null if finding doesn't match any rule.` : 'Rule ID if matches project rule (e.g., "security/no-eval"), null if ad-hoc finding';
|
|
673
|
+
return [
|
|
674
|
+
{
|
|
675
|
+
name: "get_diffs",
|
|
676
|
+
description: `Get git diffs for multiple files at once.
|
|
677
|
+
Use this to see what changed in files that look suspicious.
|
|
678
|
+
Select files based on: file names, change size (+lines/-lines), whether new/modified.
|
|
679
|
+
Prefer files most likely to have issues (security-sensitive, complex logic, etc).`,
|
|
680
|
+
parameters: {
|
|
681
|
+
type: "object",
|
|
682
|
+
properties: {
|
|
683
|
+
files: {
|
|
684
|
+
type: "array",
|
|
685
|
+
items: { type: "string" },
|
|
686
|
+
maxItems: 15,
|
|
687
|
+
description: "File paths from the list (max 15 per call)"
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
required: ["files"]
|
|
691
|
+
}
|
|
692
|
+
},
|
|
693
|
+
{
|
|
694
|
+
name: "get_file_chunks",
|
|
695
|
+
description: `Get specific portions of files when diff alone isn't enough.
|
|
696
|
+
Use when you need to see:
|
|
697
|
+
- Code around a suspicious change (function context)
|
|
698
|
+
- Definition that a change references
|
|
699
|
+
- Import statements to understand dependencies
|
|
700
|
+
Use sparingly - prefer analyzing diffs first.`,
|
|
701
|
+
parameters: {
|
|
702
|
+
type: "object",
|
|
703
|
+
properties: {
|
|
704
|
+
requests: {
|
|
705
|
+
type: "array",
|
|
706
|
+
items: {
|
|
707
|
+
type: "object",
|
|
708
|
+
properties: {
|
|
709
|
+
file: { type: "string" },
|
|
710
|
+
startLine: { type: "integer", description: "Start line (1-indexed)" },
|
|
711
|
+
endLine: { type: "integer", description: "End line (max startLine + 100)" }
|
|
712
|
+
},
|
|
713
|
+
required: ["file"]
|
|
714
|
+
},
|
|
715
|
+
maxItems: 5,
|
|
716
|
+
description: "Chunk requests (max 5 total across all calls)"
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
required: ["requests"]
|
|
720
|
+
}
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
name: "report_findings",
|
|
724
|
+
description: `Report all code review findings. Call once when done analyzing.
|
|
725
|
+
Only report actual issues found in the code you reviewed - not hypotheticals.
|
|
726
|
+
Include specific line numbers from the diffs you analyzed.
|
|
727
|
+
Provide code snippets to help with verification.
|
|
728
|
+
IMPORTANT: If a finding matches a project rule, you MUST include the exact ruleId from the list.`,
|
|
729
|
+
parameters: {
|
|
730
|
+
type: "object",
|
|
731
|
+
properties: {
|
|
732
|
+
findings: {
|
|
733
|
+
type: "array",
|
|
734
|
+
items: {
|
|
735
|
+
type: "object",
|
|
736
|
+
properties: {
|
|
737
|
+
file: { type: "string", description: "File path" },
|
|
738
|
+
line: { type: "integer", description: "Line number (from diff)" },
|
|
739
|
+
endLine: { type: "integer", description: "End line for ranges (optional)" },
|
|
740
|
+
severity: {
|
|
741
|
+
type: "string",
|
|
742
|
+
enum: ["blocker", "high", "medium", "low", "info"]
|
|
743
|
+
},
|
|
744
|
+
category: {
|
|
745
|
+
type: "string",
|
|
746
|
+
enum: validCategories.length > 0 ? validCategories : void 0,
|
|
747
|
+
description: validCategories.length > 0 ? `Category must be one of: ${validCategories.join(", ")}` : "Category from project rules"
|
|
748
|
+
},
|
|
749
|
+
message: { type: "string", description: "Clear description of the issue" },
|
|
750
|
+
suggestion: { type: "string", description: "How to fix (optional)" },
|
|
751
|
+
codeSnippet: { type: "string", description: "Problematic code from diff" },
|
|
752
|
+
ruleId: {
|
|
753
|
+
type: ["string", "null"],
|
|
754
|
+
description: ruleIdDescription
|
|
755
|
+
}
|
|
756
|
+
},
|
|
757
|
+
required: ["file", "line", "severity", "category", "message"]
|
|
758
|
+
}
|
|
759
|
+
},
|
|
760
|
+
summary: {
|
|
761
|
+
type: "string",
|
|
762
|
+
description: "Brief summary of review findings"
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
required: ["findings"]
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
];
|
|
769
|
+
}
|
|
770
|
+
var DEFAULT_BUDGET = {
|
|
771
|
+
maxDiffCalls: 2,
|
|
772
|
+
maxFilesPerDiff: 15,
|
|
773
|
+
maxChunkCalls: 2,
|
|
774
|
+
maxTotalChunks: 5,
|
|
775
|
+
maxLinesPerChunk: 100,
|
|
776
|
+
usage: {
|
|
777
|
+
diffCalls: 0,
|
|
778
|
+
filesRequested: 0,
|
|
779
|
+
chunkCalls: 0,
|
|
780
|
+
totalChunks: 0
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
var ToolExecutor = class {
|
|
784
|
+
diffProvider;
|
|
785
|
+
cwd;
|
|
786
|
+
budget;
|
|
787
|
+
changedFiles;
|
|
788
|
+
fetchedDiffs = /* @__PURE__ */ new Map();
|
|
789
|
+
constructor(cwd, changedFiles, diffProvider, budget) {
|
|
790
|
+
this.cwd = cwd;
|
|
791
|
+
this.diffProvider = diffProvider;
|
|
792
|
+
this.changedFiles = new Set(changedFiles);
|
|
793
|
+
this.budget = {
|
|
794
|
+
...DEFAULT_BUDGET,
|
|
795
|
+
...budget,
|
|
796
|
+
usage: { ...DEFAULT_BUDGET.usage }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Execute a tool call
|
|
801
|
+
*/
|
|
802
|
+
async execute(toolCall) {
|
|
803
|
+
try {
|
|
804
|
+
switch (toolCall.name) {
|
|
805
|
+
case "get_diffs":
|
|
806
|
+
return await this.executeGetDiffs(toolCall.arguments);
|
|
807
|
+
case "get_file_chunks":
|
|
808
|
+
return await this.executeGetFileChunks(
|
|
809
|
+
toolCall.arguments
|
|
810
|
+
);
|
|
811
|
+
case "report_findings":
|
|
812
|
+
return this.executeReportFindings(
|
|
813
|
+
toolCall.arguments
|
|
814
|
+
);
|
|
815
|
+
default:
|
|
816
|
+
return {
|
|
817
|
+
name: toolCall.name,
|
|
818
|
+
result: null,
|
|
819
|
+
error: `Unknown tool: ${toolCall.name}`
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
} catch (error) {
|
|
823
|
+
return {
|
|
824
|
+
name: toolCall.name,
|
|
825
|
+
result: null,
|
|
826
|
+
error: error instanceof Error ? error.message : String(error)
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* Execute get_diffs tool
|
|
832
|
+
*/
|
|
833
|
+
async executeGetDiffs(args) {
|
|
834
|
+
if (this.budget.usage.diffCalls >= this.budget.maxDiffCalls) {
|
|
835
|
+
return {
|
|
836
|
+
name: "get_diffs",
|
|
837
|
+
result: null,
|
|
838
|
+
error: `Budget exceeded: max ${this.budget.maxDiffCalls} diff calls allowed`
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
const resolvedCwd = path.resolve(this.cwd);
|
|
842
|
+
const validFiles = args.files.slice(0, this.budget.maxFilesPerDiff).filter((f) => {
|
|
843
|
+
if (!this.changedFiles.has(f)) {
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
const fullPath = path.resolve(this.cwd, f);
|
|
847
|
+
const relative2 = path.relative(resolvedCwd, fullPath);
|
|
848
|
+
return relative2 && !relative2.startsWith("..") && !path.isAbsolute(relative2);
|
|
849
|
+
});
|
|
850
|
+
if (validFiles.length === 0) {
|
|
851
|
+
return {
|
|
852
|
+
name: "get_diffs",
|
|
853
|
+
result: { diffs: [], message: "No valid files requested" },
|
|
854
|
+
error: void 0
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
this.budget.usage.diffCalls++;
|
|
858
|
+
this.budget.usage.filesRequested += validFiles.length;
|
|
859
|
+
const result = await this.diffProvider.getDiffs({
|
|
860
|
+
cwd: this.cwd,
|
|
861
|
+
files: validFiles,
|
|
862
|
+
staged: true,
|
|
863
|
+
unstaged: true,
|
|
864
|
+
maxLinesPerFile: 500
|
|
865
|
+
});
|
|
866
|
+
for (const diff of result.diffs) {
|
|
867
|
+
this.fetchedDiffs.set(diff.file, diff);
|
|
868
|
+
}
|
|
869
|
+
const formattedDiffs = result.diffs.map((d) => ({
|
|
870
|
+
file: d.file,
|
|
871
|
+
additions: d.additions,
|
|
872
|
+
deletions: d.deletions,
|
|
873
|
+
isNewFile: d.isNewFile,
|
|
874
|
+
diff: d.diff
|
|
875
|
+
}));
|
|
876
|
+
return {
|
|
877
|
+
name: "get_diffs",
|
|
878
|
+
result: {
|
|
879
|
+
diffs: formattedDiffs,
|
|
880
|
+
errors: result.errors,
|
|
881
|
+
budgetRemaining: {
|
|
882
|
+
diffCalls: this.budget.maxDiffCalls - this.budget.usage.diffCalls,
|
|
883
|
+
chunkCalls: this.budget.maxChunkCalls - this.budget.usage.chunkCalls
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* Execute get_file_chunks tool
|
|
890
|
+
*/
|
|
891
|
+
async executeGetFileChunks(args) {
|
|
892
|
+
if (this.budget.usage.chunkCalls >= this.budget.maxChunkCalls) {
|
|
893
|
+
return {
|
|
894
|
+
name: "get_file_chunks",
|
|
895
|
+
result: null,
|
|
896
|
+
error: `Budget exceeded: max ${this.budget.maxChunkCalls} chunk calls allowed`
|
|
897
|
+
};
|
|
898
|
+
}
|
|
899
|
+
const remainingChunks = this.budget.maxTotalChunks - this.budget.usage.totalChunks;
|
|
900
|
+
if (remainingChunks <= 0) {
|
|
901
|
+
return {
|
|
902
|
+
name: "get_file_chunks",
|
|
903
|
+
result: null,
|
|
904
|
+
error: `Budget exceeded: max ${this.budget.maxTotalChunks} total chunks allowed`
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
const validRequests = args.requests.slice(0, remainingChunks).filter((r) => this.changedFiles.has(r.file));
|
|
908
|
+
if (validRequests.length === 0) {
|
|
909
|
+
return {
|
|
910
|
+
name: "get_file_chunks",
|
|
911
|
+
result: { chunks: [], message: "No valid files requested" }
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
this.budget.usage.chunkCalls++;
|
|
915
|
+
this.budget.usage.totalChunks += validRequests.length;
|
|
916
|
+
const chunks = [];
|
|
917
|
+
for (const request of validRequests) {
|
|
918
|
+
try {
|
|
919
|
+
const fullPath = path.resolve(this.cwd, request.file);
|
|
920
|
+
const resolvedCwd = path.resolve(this.cwd);
|
|
921
|
+
const relative2 = path.relative(resolvedCwd, fullPath);
|
|
922
|
+
if (!relative2 || relative2.startsWith("..") || path.isAbsolute(relative2)) {
|
|
923
|
+
useLogger()?.debug(`[ToolExecutor] Path traversal blocked: ${request.file}`);
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
const content = await readFile(fullPath, "utf-8");
|
|
927
|
+
const lines = content.split("\n");
|
|
928
|
+
const startLine = Math.max(1, request.startLine ?? 1);
|
|
929
|
+
const maxEndLine = Math.min(lines.length, startLine + this.budget.maxLinesPerChunk - 1);
|
|
930
|
+
const endLine = request.endLine ? Math.min(request.endLine, maxEndLine) : maxEndLine;
|
|
931
|
+
const chunkLines = lines.slice(startLine - 1, endLine);
|
|
932
|
+
chunks.push({
|
|
933
|
+
file: request.file,
|
|
934
|
+
startLine,
|
|
935
|
+
endLine,
|
|
936
|
+
content: chunkLines.join("\n")
|
|
937
|
+
});
|
|
938
|
+
} catch (error) {
|
|
939
|
+
useLogger()?.debug(`[ToolExecutor] Error reading ${request.file}:`, { error });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return {
|
|
943
|
+
name: "get_file_chunks",
|
|
944
|
+
result: {
|
|
945
|
+
chunks,
|
|
946
|
+
budgetRemaining: {
|
|
947
|
+
chunkCalls: this.budget.maxChunkCalls - this.budget.usage.chunkCalls,
|
|
948
|
+
totalChunks: this.budget.maxTotalChunks - this.budget.usage.totalChunks
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Execute report_findings tool (just passes through)
|
|
955
|
+
*/
|
|
956
|
+
executeReportFindings(args) {
|
|
957
|
+
return {
|
|
958
|
+
name: "report_findings",
|
|
959
|
+
result: {
|
|
960
|
+
findings: args.findings,
|
|
961
|
+
summary: args.summary,
|
|
962
|
+
accepted: true
|
|
963
|
+
}
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Get fetched diffs (for verification)
|
|
968
|
+
*/
|
|
969
|
+
getFetchedDiffs() {
|
|
970
|
+
return this.fetchedDiffs;
|
|
971
|
+
}
|
|
972
|
+
/**
|
|
973
|
+
* Get current budget usage
|
|
974
|
+
*/
|
|
975
|
+
getBudgetUsage() {
|
|
976
|
+
return { ...this.budget.usage };
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Check if a file was in the changed files list
|
|
980
|
+
*/
|
|
981
|
+
isValidFile(file) {
|
|
982
|
+
return this.changedFiles.has(file);
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
function createToolExecutor(cwd, changedFiles, diffProvider, budget) {
|
|
986
|
+
return new ToolExecutor(cwd, changedFiles, diffProvider, budget);
|
|
987
|
+
}
|
|
988
|
+
var VALID_SEVERITIES2 = ["blocker", "high", "medium", "low", "info"];
|
|
989
|
+
var SEVERITY_ALIASES = {
|
|
990
|
+
critical: "blocker",
|
|
991
|
+
error: "high",
|
|
992
|
+
warning: "medium",
|
|
993
|
+
warn: "medium",
|
|
994
|
+
minor: "low",
|
|
995
|
+
trivial: "info",
|
|
996
|
+
suggestion: "info",
|
|
997
|
+
hint: "info"
|
|
998
|
+
};
|
|
999
|
+
var SEVERITY_ORDER = ["blocker", "high", "medium", "low", "info"];
|
|
1000
|
+
var THRESHOLDS = {
|
|
1001
|
+
KEEP: 0.7,
|
|
1002
|
+
DOWNGRADE: 0.4
|
|
1003
|
+
};
|
|
1004
|
+
var SCORE_WEIGHTS = {
|
|
1005
|
+
severityValid: 0.075,
|
|
1006
|
+
categoryValid: 0.075,
|
|
1007
|
+
fileExists: 0.2,
|
|
1008
|
+
lineInBounds: 0.15,
|
|
1009
|
+
lineInDiff: 0.2,
|
|
1010
|
+
snippetMatch: 0.2,
|
|
1011
|
+
contextValid: 0.1,
|
|
1012
|
+
ruleIdValid: 0.05
|
|
1013
|
+
// Bonus for using valid project rule ID
|
|
1014
|
+
};
|
|
1015
|
+
var VerificationEngine = class {
|
|
1016
|
+
cwd;
|
|
1017
|
+
changedFiles;
|
|
1018
|
+
fetchedDiffs;
|
|
1019
|
+
validCategories;
|
|
1020
|
+
categoryAliases;
|
|
1021
|
+
validRuleIds;
|
|
1022
|
+
fileContents = /* @__PURE__ */ new Map();
|
|
1023
|
+
constructor(cwd, changedFiles, fetchedDiffs, validCategories, categoryAliases = {}, validRuleIds = /* @__PURE__ */ new Set()) {
|
|
1024
|
+
this.cwd = cwd;
|
|
1025
|
+
this.changedFiles = new Set(changedFiles);
|
|
1026
|
+
this.fetchedDiffs = fetchedDiffs;
|
|
1027
|
+
this.validCategories = validCategories;
|
|
1028
|
+
this.categoryAliases = categoryAliases;
|
|
1029
|
+
this.validRuleIds = validRuleIds;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Verify all findings from LLM
|
|
1033
|
+
*/
|
|
1034
|
+
async verify(findings) {
|
|
1035
|
+
const verified = [];
|
|
1036
|
+
const discarded = [];
|
|
1037
|
+
let keptCount = 0;
|
|
1038
|
+
let downgradedCount = 0;
|
|
1039
|
+
for (const finding of findings) {
|
|
1040
|
+
const result = await this.verifyFinding(finding);
|
|
1041
|
+
if (result.action === "discard") {
|
|
1042
|
+
discarded.push(result);
|
|
1043
|
+
} else {
|
|
1044
|
+
verified.push(result);
|
|
1045
|
+
if (result.action === "keep") {
|
|
1046
|
+
keptCount++;
|
|
1047
|
+
} else {
|
|
1048
|
+
downgradedCount++;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
const total = findings.length;
|
|
1053
|
+
const hallucinationRate = total > 0 ? discarded.length / total : 0;
|
|
1054
|
+
return {
|
|
1055
|
+
verified,
|
|
1056
|
+
discarded,
|
|
1057
|
+
stats: {
|
|
1058
|
+
total,
|
|
1059
|
+
kept: keptCount,
|
|
1060
|
+
downgraded: downgradedCount,
|
|
1061
|
+
discarded: discarded.length,
|
|
1062
|
+
hallucinationRate
|
|
1063
|
+
}
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
/**
|
|
1067
|
+
* Verify a single finding
|
|
1068
|
+
*/
|
|
1069
|
+
async verifyFinding(finding) {
|
|
1070
|
+
const severityNorm = this.normalizeSeverity(finding.severity);
|
|
1071
|
+
const categoryNorm = this.normalizeCategory(finding.category);
|
|
1072
|
+
const ruleIdValid = this.validateRuleId(finding.ruleId);
|
|
1073
|
+
const checks = {
|
|
1074
|
+
severityValid: severityNorm.valid,
|
|
1075
|
+
categoryValid: categoryNorm.valid,
|
|
1076
|
+
fileExists: this.changedFiles.has(finding.file),
|
|
1077
|
+
lineInBounds: await this.checkLineInBounds(finding.file, finding.line),
|
|
1078
|
+
lineInDiff: this.checkLineInDiff(finding.file, finding.line),
|
|
1079
|
+
snippetMatch: await this.matchSnippet(finding.file, finding.line, finding.codeSnippet),
|
|
1080
|
+
contextValid: this.checkContext(finding),
|
|
1081
|
+
ruleIdValid
|
|
1082
|
+
};
|
|
1083
|
+
const score = this.calculateScore(checks);
|
|
1084
|
+
let action;
|
|
1085
|
+
let adjustedSeverity;
|
|
1086
|
+
if (score >= THRESHOLDS.KEEP) {
|
|
1087
|
+
action = "keep";
|
|
1088
|
+
} else if (score >= THRESHOLDS.DOWNGRADE) {
|
|
1089
|
+
action = "downgrade";
|
|
1090
|
+
adjustedSeverity = this.downgradeSeverity(severityNorm.normalized);
|
|
1091
|
+
} else {
|
|
1092
|
+
action = "discard";
|
|
1093
|
+
}
|
|
1094
|
+
const normalizedFinding = {
|
|
1095
|
+
...finding,
|
|
1096
|
+
severity: adjustedSeverity ?? severityNorm.normalized,
|
|
1097
|
+
category: categoryNorm.normalized
|
|
1098
|
+
};
|
|
1099
|
+
return {
|
|
1100
|
+
finding: normalizedFinding,
|
|
1101
|
+
score,
|
|
1102
|
+
checks,
|
|
1103
|
+
action,
|
|
1104
|
+
adjustedSeverity,
|
|
1105
|
+
enumNormalization: {
|
|
1106
|
+
severityOriginal: finding.severity,
|
|
1107
|
+
severityNormalized: severityNorm.normalized,
|
|
1108
|
+
categoryOriginal: finding.category,
|
|
1109
|
+
categoryNormalized: categoryNorm.normalized
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
/**
|
|
1114
|
+
* Normalize severity value to RawFindingSeverity type.
|
|
1115
|
+
*/
|
|
1116
|
+
normalizeSeverity(value) {
|
|
1117
|
+
const lower = value.toLowerCase().trim();
|
|
1118
|
+
if (VALID_SEVERITIES2.includes(lower)) {
|
|
1119
|
+
return { valid: true, normalized: lower };
|
|
1120
|
+
}
|
|
1121
|
+
if (SEVERITY_ALIASES[lower]) {
|
|
1122
|
+
return { valid: true, normalized: SEVERITY_ALIASES[lower] };
|
|
1123
|
+
}
|
|
1124
|
+
return { valid: false, normalized: "medium" };
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* Normalize category value
|
|
1128
|
+
*/
|
|
1129
|
+
normalizeCategory(value) {
|
|
1130
|
+
const lower = value.toLowerCase().trim();
|
|
1131
|
+
if (this.validCategories.length === 0) {
|
|
1132
|
+
return { valid: true, normalized: value };
|
|
1133
|
+
}
|
|
1134
|
+
if (this.validCategories.includes(lower)) {
|
|
1135
|
+
return { valid: true, normalized: lower };
|
|
1136
|
+
}
|
|
1137
|
+
if (this.categoryAliases[lower]) {
|
|
1138
|
+
return { valid: true, normalized: this.categoryAliases[lower] };
|
|
1139
|
+
}
|
|
1140
|
+
return { valid: false, normalized: this.validCategories[0] ?? "other" };
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Check if line is within file bounds
|
|
1144
|
+
*/
|
|
1145
|
+
async checkLineInBounds(file, line) {
|
|
1146
|
+
if (line < 1) {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
try {
|
|
1150
|
+
const content = await this.getFileContent(file);
|
|
1151
|
+
if (!content) {
|
|
1152
|
+
return false;
|
|
1153
|
+
}
|
|
1154
|
+
const lineCount = content.split("\n").length;
|
|
1155
|
+
return line <= lineCount;
|
|
1156
|
+
} catch {
|
|
1157
|
+
return false;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Check if line is in the diff (actually changed)
|
|
1162
|
+
*/
|
|
1163
|
+
checkLineInDiff(file, line) {
|
|
1164
|
+
const diff = this.fetchedDiffs.get(file);
|
|
1165
|
+
if (!diff) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
return diff.changedLines.has(line);
|
|
1169
|
+
}
|
|
1170
|
+
/**
|
|
1171
|
+
* Match code snippet against actual file content
|
|
1172
|
+
*/
|
|
1173
|
+
async matchSnippet(file, targetLine, snippet) {
|
|
1174
|
+
if (!snippet) {
|
|
1175
|
+
return 0.5;
|
|
1176
|
+
}
|
|
1177
|
+
try {
|
|
1178
|
+
const content = await this.getFileContent(file);
|
|
1179
|
+
if (!content) {
|
|
1180
|
+
return 0;
|
|
1181
|
+
}
|
|
1182
|
+
const lines = content.split("\n");
|
|
1183
|
+
const windowSize = 5;
|
|
1184
|
+
const start = Math.max(0, targetLine - windowSize - 1);
|
|
1185
|
+
const end = Math.min(lines.length, targetLine + windowSize);
|
|
1186
|
+
const window = lines.slice(start, end).join("\n");
|
|
1187
|
+
if (window.includes(snippet)) {
|
|
1188
|
+
return 1;
|
|
1189
|
+
}
|
|
1190
|
+
const trimmedSnippet = snippet.split("\n").map((l) => l.trim()).join("\n");
|
|
1191
|
+
const trimmedWindow = window.split("\n").map((l) => l.trim()).join("\n");
|
|
1192
|
+
if (trimmedWindow.includes(trimmedSnippet)) {
|
|
1193
|
+
return 0.95;
|
|
1194
|
+
}
|
|
1195
|
+
if (!snippet.includes("\n")) {
|
|
1196
|
+
const normalizedSnippet = this.normalizeCodeLine(snippet);
|
|
1197
|
+
for (const line of lines.slice(start, end)) {
|
|
1198
|
+
if (this.normalizeCodeLine(line).includes(normalizedSnippet)) {
|
|
1199
|
+
return 0.9;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
const snippetTokens = this.extractIdentifiers(snippet);
|
|
1204
|
+
const windowTokens = new Set(this.extractIdentifiers(window));
|
|
1205
|
+
const matchedTokens = snippetTokens.filter((t) => windowTokens.has(t));
|
|
1206
|
+
const tokenScore = matchedTokens.length / Math.max(snippetTokens.length, 1);
|
|
1207
|
+
return tokenScore * 0.8;
|
|
1208
|
+
} catch {
|
|
1209
|
+
return 0;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
/**
|
|
1213
|
+
* Normalize a single line of code (preserves strings)
|
|
1214
|
+
*/
|
|
1215
|
+
normalizeCodeLine(line) {
|
|
1216
|
+
const strings = [];
|
|
1217
|
+
let normalized = line.replace(
|
|
1218
|
+
/(['"`])(?:(?!\1)[^\\]|\\.)*\1/g,
|
|
1219
|
+
(match) => {
|
|
1220
|
+
strings.push(match);
|
|
1221
|
+
return `__STR${strings.length - 1}__`;
|
|
1222
|
+
}
|
|
1223
|
+
);
|
|
1224
|
+
normalized = normalized.replace(/\s+/g, " ").trim();
|
|
1225
|
+
strings.forEach((str, i) => {
|
|
1226
|
+
normalized = normalized.replace(`__STR${i}__`, str);
|
|
1227
|
+
});
|
|
1228
|
+
return normalized;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Extract identifiers from code
|
|
1232
|
+
*/
|
|
1233
|
+
extractIdentifiers(code) {
|
|
1234
|
+
const matches = code.match(/[a-zA-Z_$][a-zA-Z0-9_$]*/g) || [];
|
|
1235
|
+
const keywords = /* @__PURE__ */ new Set([
|
|
1236
|
+
"const",
|
|
1237
|
+
"let",
|
|
1238
|
+
"var",
|
|
1239
|
+
"function",
|
|
1240
|
+
"class",
|
|
1241
|
+
"if",
|
|
1242
|
+
"else",
|
|
1243
|
+
"for",
|
|
1244
|
+
"while",
|
|
1245
|
+
"return",
|
|
1246
|
+
"import",
|
|
1247
|
+
"export",
|
|
1248
|
+
"from",
|
|
1249
|
+
"async",
|
|
1250
|
+
"await",
|
|
1251
|
+
"try",
|
|
1252
|
+
"catch",
|
|
1253
|
+
"throw",
|
|
1254
|
+
"new",
|
|
1255
|
+
"this",
|
|
1256
|
+
"true",
|
|
1257
|
+
"false",
|
|
1258
|
+
"null",
|
|
1259
|
+
"undefined",
|
|
1260
|
+
"typeof"
|
|
1261
|
+
]);
|
|
1262
|
+
return matches.filter((m) => !keywords.has(m) && m.length > 1);
|
|
1263
|
+
}
|
|
1264
|
+
/**
|
|
1265
|
+
* Validate ruleId - must be null or a valid rule ID from project rules
|
|
1266
|
+
*/
|
|
1267
|
+
validateRuleId(ruleId) {
|
|
1268
|
+
if (ruleId === null || ruleId === void 0) {
|
|
1269
|
+
return true;
|
|
1270
|
+
}
|
|
1271
|
+
if (this.validRuleIds.size === 0) {
|
|
1272
|
+
return true;
|
|
1273
|
+
}
|
|
1274
|
+
return this.validRuleIds.has(ruleId);
|
|
1275
|
+
}
|
|
1276
|
+
/**
|
|
1277
|
+
* Check if finding makes sense for file context
|
|
1278
|
+
*/
|
|
1279
|
+
checkContext(finding) {
|
|
1280
|
+
const fileLower = finding.file.toLowerCase();
|
|
1281
|
+
const messageLower = finding.message.toLowerCase();
|
|
1282
|
+
if (finding.category === "security" && messageLower.includes("sql") && !fileLower.includes("db") && !fileLower.includes("database") && !fileLower.includes("query") && !fileLower.includes("repository")) {
|
|
1283
|
+
return false;
|
|
1284
|
+
}
|
|
1285
|
+
if (messageLower.includes("xss") && !fileLower.includes("component") && !fileLower.includes("page") && !fileLower.includes(".tsx") && !fileLower.includes(".jsx")) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
if (fileLower.includes(".test.") || fileLower.includes(".spec.")) ;
|
|
1289
|
+
return true;
|
|
1290
|
+
}
|
|
1291
|
+
/**
|
|
1292
|
+
* Calculate verification score
|
|
1293
|
+
*/
|
|
1294
|
+
calculateScore(checks) {
|
|
1295
|
+
let score = 0;
|
|
1296
|
+
if (checks.severityValid) {
|
|
1297
|
+
score += SCORE_WEIGHTS.severityValid;
|
|
1298
|
+
}
|
|
1299
|
+
if (checks.categoryValid) {
|
|
1300
|
+
score += SCORE_WEIGHTS.categoryValid;
|
|
1301
|
+
}
|
|
1302
|
+
if (checks.fileExists) {
|
|
1303
|
+
score += SCORE_WEIGHTS.fileExists;
|
|
1304
|
+
}
|
|
1305
|
+
if (checks.lineInBounds) {
|
|
1306
|
+
score += SCORE_WEIGHTS.lineInBounds;
|
|
1307
|
+
}
|
|
1308
|
+
if (checks.lineInDiff) {
|
|
1309
|
+
score += SCORE_WEIGHTS.lineInDiff;
|
|
1310
|
+
}
|
|
1311
|
+
score += checks.snippetMatch * SCORE_WEIGHTS.snippetMatch;
|
|
1312
|
+
if (checks.contextValid) {
|
|
1313
|
+
score += SCORE_WEIGHTS.contextValid;
|
|
1314
|
+
}
|
|
1315
|
+
if (checks.ruleIdValid) {
|
|
1316
|
+
score += SCORE_WEIGHTS.ruleIdValid;
|
|
1317
|
+
}
|
|
1318
|
+
return score;
|
|
1319
|
+
}
|
|
1320
|
+
/**
|
|
1321
|
+
* Downgrade severity by one level.
|
|
1322
|
+
* Returns the next lower severity or 'info' if already at lowest.
|
|
1323
|
+
*/
|
|
1324
|
+
downgradeSeverity(severity) {
|
|
1325
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
1326
|
+
if (index === -1 || index >= SEVERITY_ORDER.length - 1) {
|
|
1327
|
+
return "info";
|
|
1328
|
+
}
|
|
1329
|
+
return SEVERITY_ORDER[index + 1] ?? "info";
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Get file content (with caching)
|
|
1333
|
+
*/
|
|
1334
|
+
async getFileContent(file) {
|
|
1335
|
+
if (this.fileContents.has(file)) {
|
|
1336
|
+
return this.fileContents.get(file);
|
|
1337
|
+
}
|
|
1338
|
+
try {
|
|
1339
|
+
const fullPath = path.join(this.cwd, file);
|
|
1340
|
+
const content = await readFile(fullPath, "utf-8");
|
|
1341
|
+
this.fileContents.set(file, content);
|
|
1342
|
+
return content;
|
|
1343
|
+
} catch {
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
};
|
|
1348
|
+
function createVerificationEngine(cwd, changedFiles, fetchedDiffs, validCategories, categoryAliases, validRuleIds) {
|
|
1349
|
+
return new VerificationEngine(cwd, changedFiles, fetchedDiffs, validCategories, categoryAliases, validRuleIds);
|
|
1350
|
+
}
|
|
1351
|
+
var COMMON_CATEGORY_MAPPINGS = {
|
|
1352
|
+
security: ["sec", "vulnerability", "vulnerabilities", "injection", "auth", "authentication", "authorization"],
|
|
1353
|
+
performance: ["perf", "speed", "memory", "optimization", "optimisation"],
|
|
1354
|
+
architecture: ["design", "structure", "pattern", "patterns", "arch"],
|
|
1355
|
+
naming: ["names", "conventions", "identifiers", "naming-convention"],
|
|
1356
|
+
consistency: ["style", "formatting", "code-style", "codestyle"],
|
|
1357
|
+
testing: ["test", "tests", "coverage", "unit-test", "unittest"],
|
|
1358
|
+
correctness: ["bug", "bugs", "logic", "error", "errors", "mistake"],
|
|
1359
|
+
maintainability: ["readability", "complexity", "code-quality", "quality"]
|
|
1360
|
+
};
|
|
1361
|
+
var CategoryValidator = class {
|
|
1362
|
+
validCategories = [];
|
|
1363
|
+
categoryAliases = {};
|
|
1364
|
+
defaultCategory = "other";
|
|
1365
|
+
initialized = false;
|
|
1366
|
+
/**
|
|
1367
|
+
* Initialize validator - discovers categories from config-defined rules directory
|
|
1368
|
+
*
|
|
1369
|
+
* @param cwd - Project root directory (ctx.cwd from command handler)
|
|
1370
|
+
*/
|
|
1371
|
+
async init(cwd) {
|
|
1372
|
+
if (this.initialized) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
this.validCategories = await discoverCategories(cwd);
|
|
1376
|
+
if (this.validCategories.length === 0) {
|
|
1377
|
+
this.initialized = true;
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this.categoryAliases = buildCategoryAliases(this.validCategories);
|
|
1381
|
+
this.defaultCategory = this.validCategories[0] ?? "general";
|
|
1382
|
+
this.initialized = true;
|
|
1383
|
+
}
|
|
1384
|
+
/**
|
|
1385
|
+
* Validate and normalize a category value
|
|
1386
|
+
*/
|
|
1387
|
+
validate(category) {
|
|
1388
|
+
if (this.validCategories.length === 0) {
|
|
1389
|
+
return { valid: false, normalized: category };
|
|
1390
|
+
}
|
|
1391
|
+
const lower = category.toLowerCase().trim();
|
|
1392
|
+
if (this.validCategories.includes(lower)) {
|
|
1393
|
+
return { valid: true, normalized: lower };
|
|
1394
|
+
}
|
|
1395
|
+
if (this.categoryAliases[lower]) {
|
|
1396
|
+
return { valid: true, normalized: this.categoryAliases[lower] };
|
|
1397
|
+
}
|
|
1398
|
+
return { valid: false, normalized: this.defaultCategory };
|
|
1399
|
+
}
|
|
1400
|
+
/**
|
|
1401
|
+
* Get list of valid categories
|
|
1402
|
+
*/
|
|
1403
|
+
getValidCategories() {
|
|
1404
|
+
return [...this.validCategories];
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* Get category aliases map
|
|
1408
|
+
*/
|
|
1409
|
+
getCategoryAliases() {
|
|
1410
|
+
return { ...this.categoryAliases };
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Check if validator has been initialized
|
|
1414
|
+
*/
|
|
1415
|
+
isInitialized() {
|
|
1416
|
+
return this.initialized;
|
|
1417
|
+
}
|
|
1418
|
+
};
|
|
1419
|
+
async function discoverCategories(cwd) {
|
|
1420
|
+
try {
|
|
1421
|
+
const config = await useConfig();
|
|
1422
|
+
const rulesDir = config?.rulesDir ?? "ai-review/rules";
|
|
1423
|
+
const kbDir = path.join(cwd, ".kb");
|
|
1424
|
+
const fullPath = path.join(kbDir, rulesDir);
|
|
1425
|
+
try {
|
|
1426
|
+
await access(fullPath);
|
|
1427
|
+
} catch {
|
|
1428
|
+
return [];
|
|
1429
|
+
}
|
|
1430
|
+
const entries = await readdir(fullPath, { withFileTypes: true });
|
|
1431
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name.toLowerCase());
|
|
1432
|
+
} catch {
|
|
1433
|
+
return [];
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
function buildCategoryAliases(validCategories) {
|
|
1437
|
+
const aliases = {};
|
|
1438
|
+
const categorySet = new Set(validCategories);
|
|
1439
|
+
for (const category of validCategories) {
|
|
1440
|
+
aliases[category] = category;
|
|
1441
|
+
const mappings = COMMON_CATEGORY_MAPPINGS[category];
|
|
1442
|
+
if (mappings) {
|
|
1443
|
+
for (const alias of mappings) {
|
|
1444
|
+
if (!categorySet.has(alias)) {
|
|
1445
|
+
aliases[alias] = category;
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
return aliases;
|
|
1451
|
+
}
|
|
1452
|
+
function createCategoryValidator() {
|
|
1453
|
+
return new CategoryValidator();
|
|
1454
|
+
}
|
|
1455
|
+
var globalValidator = null;
|
|
1456
|
+
async function getCategoryValidator(cwd) {
|
|
1457
|
+
if (!globalValidator) {
|
|
1458
|
+
globalValidator = new CategoryValidator();
|
|
1459
|
+
}
|
|
1460
|
+
if (!globalValidator.isInitialized()) {
|
|
1461
|
+
await globalValidator.init(cwd);
|
|
1462
|
+
}
|
|
1463
|
+
return globalValidator;
|
|
1464
|
+
}
|
|
1465
|
+
var DEFAULT_SYSTEM_PROMPT = `You are a code reviewer analyzing changes in a codebase.
|
|
1466
|
+
|
|
1467
|
+
Your goal is to find REAL issues in the code - not hypothetical problems.
|
|
1468
|
+
|
|
1469
|
+
## Instructions
|
|
1470
|
+
|
|
1471
|
+
1. First, use get_diffs() to fetch diffs for suspicious files
|
|
1472
|
+
2. Analyze the actual changes in the diffs
|
|
1473
|
+
3. If you need more context, use get_file_chunks() sparingly
|
|
1474
|
+
4. Report all findings using report_findings()
|
|
1475
|
+
|
|
1476
|
+
## Important Rules
|
|
1477
|
+
|
|
1478
|
+
- Only report issues you actually see in the code
|
|
1479
|
+
- Include specific line numbers FROM THE DIFFS
|
|
1480
|
+
- Include code snippets to prove the issue exists
|
|
1481
|
+
- Focus on: security, correctness, performance, maintainability
|
|
1482
|
+
- Don't report style issues unless they affect readability significantly
|
|
1483
|
+
|
|
1484
|
+
## What NOT to Report (False Positives)
|
|
1485
|
+
|
|
1486
|
+
- Constructor validation patterns - this is standard practice
|
|
1487
|
+
- "Make this configurable" suggestions unless explicitly broken
|
|
1488
|
+
- Async methods that don't await - may be intentional for interface compatibility
|
|
1489
|
+
- "Add error handling" for internal code paths that can't fail
|
|
1490
|
+
- Port/timeout validation for internal tools
|
|
1491
|
+
- Empty dispose() methods - placeholder for future cleanup
|
|
1492
|
+
- Optional logger parameters - dependency injection is valid
|
|
1493
|
+
|
|
1494
|
+
## Severity Calibration
|
|
1495
|
+
|
|
1496
|
+
Be CONSERVATIVE with severity. When in doubt, use lower severity.
|
|
1497
|
+
|
|
1498
|
+
### blocker (ONLY for these)
|
|
1499
|
+
- Exploitable security vulnerability (SQL injection, XSS, RCE)
|
|
1500
|
+
- Data loss or corruption risk
|
|
1501
|
+
- Application crash in production path
|
|
1502
|
+
- Authentication/authorization bypass
|
|
1503
|
+
|
|
1504
|
+
### high (Real bugs affecting behavior)
|
|
1505
|
+
- Logic error causing incorrect results
|
|
1506
|
+
- Race condition that can occur in practice
|
|
1507
|
+
- Resource leak (memory, file handles, connections)
|
|
1508
|
+
- Unhandled error that breaks functionality
|
|
1509
|
+
|
|
1510
|
+
### medium (Code smells, potential issues)
|
|
1511
|
+
- Missing null/undefined check where input CAN be null
|
|
1512
|
+
- Floating promise without explicit void
|
|
1513
|
+
- Type safety bypass (any, type assertion without validation)
|
|
1514
|
+
|
|
1515
|
+
### low (Suggestions)
|
|
1516
|
+
- Naming could be clearer
|
|
1517
|
+
- Code could be simplified
|
|
1518
|
+
- Missing documentation for public API
|
|
1519
|
+
|
|
1520
|
+
### info (Notes)
|
|
1521
|
+
- FYI about pattern usage
|
|
1522
|
+
- Style preference (not rule violation)`;
|
|
1523
|
+
var DEFAULT_TASK_PROMPT = `## Your Task
|
|
1524
|
+
|
|
1525
|
+
1. Review the file list and identify files most likely to have issues:
|
|
1526
|
+
- Security-sensitive files (auth, crypto, input handling)
|
|
1527
|
+
- Files with large changes (+50 lines)
|
|
1528
|
+
- New files (need thorough review)
|
|
1529
|
+
- Core business logic
|
|
1530
|
+
|
|
1531
|
+
2. Use get_diffs() to fetch diffs for suspicious files (max 15 per call)
|
|
1532
|
+
|
|
1533
|
+
3. Analyze the diffs for:
|
|
1534
|
+
- Security vulnerabilities (REAL, exploitable ones)
|
|
1535
|
+
- Logic errors and edge cases
|
|
1536
|
+
- Performance issues (measurable impact)
|
|
1537
|
+
- Code quality problems (actual bugs, not style)
|
|
1538
|
+
|
|
1539
|
+
4. Report all findings using report_findings()
|
|
1540
|
+
|
|
1541
|
+
## Quality Bar
|
|
1542
|
+
|
|
1543
|
+
Before reporting a finding, ask yourself:
|
|
1544
|
+
- Is this a REAL issue or just my opinion?
|
|
1545
|
+
- Would fixing this actually improve the code?
|
|
1546
|
+
- Is the severity I'm assigning accurate?
|
|
1547
|
+
|
|
1548
|
+
Focus on ACTUAL issues in the changed code, not hypothetical problems.`;
|
|
1549
|
+
async function loadPrompts(cwd) {
|
|
1550
|
+
const config = await useConfig("review");
|
|
1551
|
+
const kbDir = path.join(cwd, ".kb");
|
|
1552
|
+
const rulesDir = path.join(kbDir, config?.rulesDir ?? "ai-review/rules");
|
|
1553
|
+
const promptsDir = path.join(kbDir, config?.promptsDir ?? "ai-review/prompts");
|
|
1554
|
+
const system = await loadPromptFile(promptsDir, "system.md", DEFAULT_SYSTEM_PROMPT);
|
|
1555
|
+
const task = await loadPromptFile(promptsDir, "task.md", DEFAULT_TASK_PROMPT);
|
|
1556
|
+
const rules = await loadRules(rulesDir);
|
|
1557
|
+
const ruleIds = /* @__PURE__ */ new Set();
|
|
1558
|
+
for (const categoryRules of Object.values(rules)) {
|
|
1559
|
+
for (const rule of categoryRules) {
|
|
1560
|
+
if (rule.id) {
|
|
1561
|
+
ruleIds.add(rule.id);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
const rulesContext = formatRulesContext(rules);
|
|
1566
|
+
return { system, task, rules, rulesContext, ruleIds };
|
|
1567
|
+
}
|
|
1568
|
+
async function loadPromptFile(dir, filename, fallback) {
|
|
1569
|
+
try {
|
|
1570
|
+
const filePath = path.join(dir, filename);
|
|
1571
|
+
await access(filePath);
|
|
1572
|
+
const content = await readFile(filePath, "utf-8");
|
|
1573
|
+
return content.trim();
|
|
1574
|
+
} catch {
|
|
1575
|
+
return fallback;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
async function loadRules(rulesDir) {
|
|
1579
|
+
const rules = {};
|
|
1580
|
+
try {
|
|
1581
|
+
await access(rulesDir);
|
|
1582
|
+
} catch {
|
|
1583
|
+
return rules;
|
|
1584
|
+
}
|
|
1585
|
+
const entries = await readdir(rulesDir, { withFileTypes: true });
|
|
1586
|
+
const categories = entries.filter((e) => e.isDirectory() && !e.name.startsWith("."));
|
|
1587
|
+
for (const categoryDir of categories) {
|
|
1588
|
+
const category = categoryDir.name;
|
|
1589
|
+
if (category.includes("..") || category.includes(path.sep) || category.includes("/")) {
|
|
1590
|
+
continue;
|
|
1591
|
+
}
|
|
1592
|
+
const categoryPath = path.join(rulesDir, category);
|
|
1593
|
+
const ruleFiles = await readdir(categoryPath, { withFileTypes: true });
|
|
1594
|
+
const mdFiles = ruleFiles.filter((f) => f.isFile() && f.name.endsWith(".md"));
|
|
1595
|
+
rules[category] = [];
|
|
1596
|
+
for (const ruleFile of mdFiles) {
|
|
1597
|
+
if (ruleFile.name.includes("..") || ruleFile.name.includes(path.sep) || ruleFile.name.includes("/")) {
|
|
1598
|
+
continue;
|
|
1599
|
+
}
|
|
1600
|
+
const ruleName = ruleFile.name.replace(".md", "");
|
|
1601
|
+
const rulePath = path.join(categoryPath, ruleFile.name);
|
|
1602
|
+
try {
|
|
1603
|
+
const rawContent = await readFile(rulePath, "utf-8");
|
|
1604
|
+
const { frontmatter, body } = parseFrontmatter(rawContent);
|
|
1605
|
+
rules[category].push({
|
|
1606
|
+
category,
|
|
1607
|
+
name: ruleName,
|
|
1608
|
+
content: body.trim(),
|
|
1609
|
+
id: frontmatter?.id,
|
|
1610
|
+
severity: frontmatter?.severity,
|
|
1611
|
+
type: frontmatter?.type ?? "positive"
|
|
1612
|
+
// Default to positive rule
|
|
1613
|
+
});
|
|
1614
|
+
} catch {
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
return rules;
|
|
1619
|
+
}
|
|
1620
|
+
function formatRulesContext(rules) {
|
|
1621
|
+
const positiveSections = [];
|
|
1622
|
+
const negativeSections = [];
|
|
1623
|
+
for (const [category, categoryRules] of Object.entries(rules)) {
|
|
1624
|
+
if (categoryRules.length === 0) {
|
|
1625
|
+
continue;
|
|
1626
|
+
}
|
|
1627
|
+
const positiveRules = categoryRules.filter((r) => r.type !== "negative");
|
|
1628
|
+
const negativeRules = categoryRules.filter((r) => r.type === "negative");
|
|
1629
|
+
if (positiveRules.length > 0) {
|
|
1630
|
+
positiveSections.push(`## ${capitalize(category)} Rules
|
|
1631
|
+
`);
|
|
1632
|
+
for (const rule of positiveRules) {
|
|
1633
|
+
if (rule.id) {
|
|
1634
|
+
positiveSections.push(`### Rule: ${rule.id}
|
|
1635
|
+
`);
|
|
1636
|
+
}
|
|
1637
|
+
positiveSections.push(rule.content);
|
|
1638
|
+
positiveSections.push("");
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (negativeRules.length > 0) {
|
|
1642
|
+
for (const rule of negativeRules) {
|
|
1643
|
+
if (rule.id) {
|
|
1644
|
+
negativeSections.push(`### ${rule.id}
|
|
1645
|
+
`);
|
|
1646
|
+
}
|
|
1647
|
+
negativeSections.push(rule.content);
|
|
1648
|
+
negativeSections.push("");
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
}
|
|
1652
|
+
if (positiveSections.length === 0 && negativeSections.length === 0) {
|
|
1653
|
+
return "";
|
|
1654
|
+
}
|
|
1655
|
+
const allRuleIds = [];
|
|
1656
|
+
for (const categoryRules of Object.values(rules)) {
|
|
1657
|
+
for (const rule of categoryRules) {
|
|
1658
|
+
if (rule.id && rule.type !== "negative") {
|
|
1659
|
+
allRuleIds.push(rule.id);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1663
|
+
const ruleIdList = allRuleIds.length > 0 ? `
|
|
1664
|
+
|
|
1665
|
+
**Available Rule IDs:** ${allRuleIds.join(", ")}` : "";
|
|
1666
|
+
const falsePositiveSection = negativeSections.length > 0 ? `
|
|
1667
|
+
|
|
1668
|
+
## FALSE POSITIVE PREVENTION (Do NOT Report These)
|
|
1669
|
+
|
|
1670
|
+
**CRITICAL:** Before reporting any issue, check if it matches a pattern below. If it does, DO NOT REPORT IT.
|
|
1671
|
+
|
|
1672
|
+
${negativeSections.join("\n")}` : "";
|
|
1673
|
+
return `# Project-Specific Rules (MANDATORY)
|
|
1674
|
+
|
|
1675
|
+
## CRITICAL: You MUST Use Rule IDs
|
|
1676
|
+
|
|
1677
|
+
When reporting findings, you MUST check if the issue matches one of these project rules and use the exact ruleId:
|
|
1678
|
+
${ruleIdList}
|
|
1679
|
+
|
|
1680
|
+
### How to Report:
|
|
1681
|
+
|
|
1682
|
+
1. **If issue matches a rule** \u2192 Use the EXACT ruleId from the list above
|
|
1683
|
+
Example: \`"ruleId": "security/path-traversal"\` or \`"ruleId": "consistency/validation-logic"\`
|
|
1684
|
+
|
|
1685
|
+
2. **If issue does NOT match any rule** \u2192 Use \`"ruleId": null\`
|
|
1686
|
+
|
|
1687
|
+
### Examples:
|
|
1688
|
+
|
|
1689
|
+
- Path traversal issue \u2192 \`"ruleId": "security/path-traversal"\`
|
|
1690
|
+
- Input validation issue \u2192 \`"ruleId": "security/input-validation"\`
|
|
1691
|
+
- Inconsistent validation logic \u2192 \`"ruleId": "consistency/validation-logic"\`
|
|
1692
|
+
- Dead code \u2192 \`"ruleId": "architecture/dead-code-paths"\`
|
|
1693
|
+
- Some other issue not in rules \u2192 \`"ruleId": null\`
|
|
1694
|
+
|
|
1695
|
+
## Project Rules
|
|
1696
|
+
|
|
1697
|
+
${positiveSections.join("\n")}${falsePositiveSection}`;
|
|
1698
|
+
}
|
|
1699
|
+
function capitalize(str) {
|
|
1700
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
1701
|
+
}
|
|
1702
|
+
function parseFrontmatter(content) {
|
|
1703
|
+
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n*/;
|
|
1704
|
+
const match = content.match(frontmatterRegex);
|
|
1705
|
+
if (!match) {
|
|
1706
|
+
return { frontmatter: null, body: content };
|
|
1707
|
+
}
|
|
1708
|
+
const yamlContent = match[1] ?? "";
|
|
1709
|
+
const body = content.slice(match[0].length);
|
|
1710
|
+
const frontmatter = {};
|
|
1711
|
+
for (const line of yamlContent.split("\n")) {
|
|
1712
|
+
const colonIndex = line.indexOf(":");
|
|
1713
|
+
if (colonIndex === -1) {
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
const key = line.slice(0, colonIndex).trim();
|
|
1717
|
+
const value = line.slice(colonIndex + 1).trim();
|
|
1718
|
+
if (key === "id") {
|
|
1719
|
+
frontmatter.id = value;
|
|
1720
|
+
} else if (key === "severity") {
|
|
1721
|
+
const validSeverities = ["blocker", "high", "medium", "low", "info"];
|
|
1722
|
+
if (validSeverities.includes(value)) {
|
|
1723
|
+
frontmatter.severity = value;
|
|
1724
|
+
}
|
|
1725
|
+
} else if (key === "type" && (value === "positive" || value === "negative")) {
|
|
1726
|
+
frontmatter.type = value;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (!frontmatter.id) {
|
|
1730
|
+
return { frontmatter: null, body: content };
|
|
1731
|
+
}
|
|
1732
|
+
return { frontmatter, body };
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/llm-lite/llm-lite-analyzer.ts
|
|
1736
|
+
var DEFAULT_MIN_TURNS = 3;
|
|
1737
|
+
var DEFAULT_MAX_TURNS = 25;
|
|
1738
|
+
var DEFAULT_FILES_PER_TURN = 10;
|
|
1739
|
+
var DEFAULT_LINES_PER_TURN = 500;
|
|
1740
|
+
function calculateMaxTurns(fileCount, totalChangedLines, config) {
|
|
1741
|
+
const minTurns = config?.minTurns ?? DEFAULT_MIN_TURNS;
|
|
1742
|
+
const maxTurns = config?.maxTurns ?? DEFAULT_MAX_TURNS;
|
|
1743
|
+
const filesPerTurn = config?.filesPerTurn ?? DEFAULT_FILES_PER_TURN;
|
|
1744
|
+
const linesPerTurn = config?.linesPerTurn ?? DEFAULT_LINES_PER_TURN;
|
|
1745
|
+
const baseTurns = minTurns;
|
|
1746
|
+
const turnsForFiles = Math.ceil(fileCount / filesPerTurn);
|
|
1747
|
+
const turnsForLines = Math.ceil(totalChangedLines / linesPerTurn);
|
|
1748
|
+
return Math.min(maxTurns, baseTurns + turnsForFiles + turnsForLines);
|
|
1749
|
+
}
|
|
1750
|
+
var TOKEN_COSTS = {
|
|
1751
|
+
input: 3e-5,
|
|
1752
|
+
// $0.03 per 1K input tokens
|
|
1753
|
+
output: 6e-5
|
|
1754
|
+
// $0.06 per 1K output tokens
|
|
1755
|
+
};
|
|
1756
|
+
var LLMLiteAnalyzer = class {
|
|
1757
|
+
cwd;
|
|
1758
|
+
files;
|
|
1759
|
+
taskContext;
|
|
1760
|
+
repoScope;
|
|
1761
|
+
diffProvider;
|
|
1762
|
+
constructor(request) {
|
|
1763
|
+
this.cwd = request.cwd;
|
|
1764
|
+
this.files = request.files;
|
|
1765
|
+
this.taskContext = request.taskContext;
|
|
1766
|
+
this.repoScope = request.repoScope;
|
|
1767
|
+
this.diffProvider = request.diffProvider;
|
|
1768
|
+
}
|
|
1769
|
+
/**
|
|
1770
|
+
* Run LLM-Lite analysis
|
|
1771
|
+
*/
|
|
1772
|
+
// eslint-disable-next-line sonarjs/cognitive-complexity -- Complex multi-phase LLM analysis workflow with prompt loading, validation, and result processing
|
|
1773
|
+
async analyze() {
|
|
1774
|
+
const startTime = Date.now();
|
|
1775
|
+
const analytics = useAnalytics();
|
|
1776
|
+
analytics?.track("review:llm-lite:started", {
|
|
1777
|
+
fileCount: this.files.length,
|
|
1778
|
+
hasTaskContext: !!this.taskContext
|
|
1779
|
+
});
|
|
1780
|
+
const llm = useLLM({ tier: "medium" });
|
|
1781
|
+
if (!llm) {
|
|
1782
|
+
throw new Error("LLM not available for llm-lite mode");
|
|
1783
|
+
}
|
|
1784
|
+
const reviewConfig = await useConfig("review");
|
|
1785
|
+
const llmConfig = reviewConfig?.llm;
|
|
1786
|
+
const prompts = await loadPrompts(this.cwd);
|
|
1787
|
+
const categoryValidator = await getCategoryValidator(this.cwd);
|
|
1788
|
+
const validCategories = categoryValidator.getValidCategories();
|
|
1789
|
+
const categoryAliases = categoryValidator.getCategoryAliases();
|
|
1790
|
+
const fileSummaries = this.buildFileSummaries();
|
|
1791
|
+
const totalChangedLines = fileSummaries.reduce(
|
|
1792
|
+
(sum, f) => sum + f.additions + f.deletions,
|
|
1793
|
+
0
|
|
1794
|
+
);
|
|
1795
|
+
const maxTurns = calculateMaxTurns(this.files.length, totalChangedLines, llmConfig);
|
|
1796
|
+
const changedFiles = this.files.map((f) => f.path);
|
|
1797
|
+
const toolExecutor = createToolExecutor(this.cwd, changedFiles, this.diffProvider);
|
|
1798
|
+
const validRuleIds = Array.from(prompts.ruleIds);
|
|
1799
|
+
const tools = buildToolDefinitions(validCategories, validRuleIds);
|
|
1800
|
+
const systemPrompt = this.buildSystemPrompt(validCategories, prompts);
|
|
1801
|
+
const initialPrompt = this.buildInitialPrompt(fileSummaries, prompts);
|
|
1802
|
+
const messages = [
|
|
1803
|
+
{ role: "user", content: initialPrompt }
|
|
1804
|
+
];
|
|
1805
|
+
let llmCalls = 0;
|
|
1806
|
+
let totalInputTokens = 0;
|
|
1807
|
+
let totalOutputTokens = 0;
|
|
1808
|
+
let llmTimeMs = 0;
|
|
1809
|
+
let rawFindings = [];
|
|
1810
|
+
const toolCallCounts = {
|
|
1811
|
+
get_diffs: 0,
|
|
1812
|
+
get_file_chunks: 0,
|
|
1813
|
+
report_findings: 0
|
|
1814
|
+
};
|
|
1815
|
+
for (let turn = 0; turn < maxTurns; turn++) {
|
|
1816
|
+
const llmStart = Date.now();
|
|
1817
|
+
const isLastTurn = turn === maxTurns - 1;
|
|
1818
|
+
const availableTools = isLastTurn ? tools.filter((t) => t.name === "report_findings") : tools;
|
|
1819
|
+
const response = await llm.chatWithTools(
|
|
1820
|
+
[
|
|
1821
|
+
{ role: "system", content: systemPrompt },
|
|
1822
|
+
...messages
|
|
1823
|
+
],
|
|
1824
|
+
{
|
|
1825
|
+
tools: availableTools.map((t) => ({
|
|
1826
|
+
name: t.name,
|
|
1827
|
+
description: t.description,
|
|
1828
|
+
inputSchema: t.parameters
|
|
1829
|
+
}))
|
|
1830
|
+
}
|
|
1831
|
+
);
|
|
1832
|
+
llmCalls++;
|
|
1833
|
+
llmTimeMs += Date.now() - llmStart;
|
|
1834
|
+
if (response.usage) {
|
|
1835
|
+
totalInputTokens += response.usage.promptTokens ?? 0;
|
|
1836
|
+
totalOutputTokens += response.usage.completionTokens ?? 0;
|
|
1837
|
+
}
|
|
1838
|
+
const toolCalls = response.toolCalls ?? [];
|
|
1839
|
+
if (toolCalls.length === 0) {
|
|
1840
|
+
break;
|
|
1841
|
+
}
|
|
1842
|
+
const assistantContent = response.content ?? "";
|
|
1843
|
+
if (assistantContent.trim()) {
|
|
1844
|
+
messages.push({
|
|
1845
|
+
role: "assistant",
|
|
1846
|
+
content: assistantContent
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
for (const toolCall of toolCalls) {
|
|
1850
|
+
const parsed = {
|
|
1851
|
+
name: toolCall.name,
|
|
1852
|
+
arguments: toolCall.input
|
|
1853
|
+
};
|
|
1854
|
+
if (parsed.name in toolCallCounts) {
|
|
1855
|
+
toolCallCounts[parsed.name]++;
|
|
1856
|
+
}
|
|
1857
|
+
const result = await toolExecutor.execute(parsed);
|
|
1858
|
+
messages.push({
|
|
1859
|
+
role: "user",
|
|
1860
|
+
content: `Tool result (${parsed.name}):
|
|
1861
|
+
${JSON.stringify(result.result, null, 2)}`
|
|
1862
|
+
});
|
|
1863
|
+
if (parsed.name === "report_findings" && result.result) {
|
|
1864
|
+
const reportResult = result.result;
|
|
1865
|
+
if (reportResult.findings) {
|
|
1866
|
+
rawFindings = reportResult.findings;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
if (toolCallCounts.report_findings > 0) {
|
|
1871
|
+
break;
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
const verifyStart = Date.now();
|
|
1875
|
+
const verificationEngine = createVerificationEngine(
|
|
1876
|
+
this.cwd,
|
|
1877
|
+
changedFiles,
|
|
1878
|
+
toolExecutor.getFetchedDiffs(),
|
|
1879
|
+
validCategories,
|
|
1880
|
+
categoryAliases,
|
|
1881
|
+
prompts.ruleIds
|
|
1882
|
+
);
|
|
1883
|
+
const verificationResult = await verificationEngine.verify(rawFindings);
|
|
1884
|
+
const verifyTimeMs = Date.now() - verifyStart;
|
|
1885
|
+
const findings = this.convertToReviewFindings(verificationResult);
|
|
1886
|
+
const estimatedCost = totalInputTokens * TOKEN_COSTS.input + totalOutputTokens * TOKEN_COSTS.output;
|
|
1887
|
+
analytics?.track("review:llm-lite:completed", {
|
|
1888
|
+
fileCount: this.files.length,
|
|
1889
|
+
findingsCount: findings.length,
|
|
1890
|
+
llmCalls,
|
|
1891
|
+
hallucinationRate: verificationResult.stats.hallucinationRate,
|
|
1892
|
+
estimatedCost
|
|
1893
|
+
});
|
|
1894
|
+
return {
|
|
1895
|
+
findings,
|
|
1896
|
+
metadata: {
|
|
1897
|
+
llmCalls,
|
|
1898
|
+
toolCalls: toolCallCounts,
|
|
1899
|
+
tokens: {
|
|
1900
|
+
input: totalInputTokens,
|
|
1901
|
+
output: totalOutputTokens,
|
|
1902
|
+
total: totalInputTokens + totalOutputTokens
|
|
1903
|
+
},
|
|
1904
|
+
estimatedCost,
|
|
1905
|
+
verification: {
|
|
1906
|
+
rawFindings: verificationResult.stats.total,
|
|
1907
|
+
verified: verificationResult.verified.length,
|
|
1908
|
+
downgraded: verificationResult.stats.downgraded,
|
|
1909
|
+
discarded: verificationResult.stats.discarded,
|
|
1910
|
+
hallucinationRate: verificationResult.stats.hallucinationRate
|
|
1911
|
+
},
|
|
1912
|
+
timing: {
|
|
1913
|
+
totalMs: Date.now() - startTime,
|
|
1914
|
+
llmMs: llmTimeMs,
|
|
1915
|
+
verifyMs: verifyTimeMs
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* Build file summaries for initial prompt
|
|
1922
|
+
*/
|
|
1923
|
+
buildFileSummaries() {
|
|
1924
|
+
return this.files.map((f) => {
|
|
1925
|
+
const lines = f.content.split("\n");
|
|
1926
|
+
return {
|
|
1927
|
+
path: f.path,
|
|
1928
|
+
additions: lines.length,
|
|
1929
|
+
deletions: 0,
|
|
1930
|
+
isNewFile: false
|
|
1931
|
+
};
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Build system prompt from loaded prompts and rules
|
|
1936
|
+
*/
|
|
1937
|
+
buildSystemPrompt(validCategories, prompts) {
|
|
1938
|
+
const categoryList = validCategories.length > 0 ? `
|
|
1939
|
+
|
|
1940
|
+
## Valid Categories
|
|
1941
|
+
|
|
1942
|
+
You MUST use only these categories:
|
|
1943
|
+
${validCategories.map((c) => `- ${c}`).join("\n")}` : "";
|
|
1944
|
+
let fullPrompt = prompts.system;
|
|
1945
|
+
if (prompts.rulesContext) {
|
|
1946
|
+
fullPrompt += `
|
|
1947
|
+
|
|
1948
|
+
${prompts.rulesContext}`;
|
|
1949
|
+
}
|
|
1950
|
+
fullPrompt += categoryList;
|
|
1951
|
+
return fullPrompt;
|
|
1952
|
+
}
|
|
1953
|
+
/**
|
|
1954
|
+
* Build initial prompt with file list and task context
|
|
1955
|
+
*/
|
|
1956
|
+
buildInitialPrompt(files, prompts) {
|
|
1957
|
+
const fileList = files.map((f) => {
|
|
1958
|
+
const stats = `+${f.additions}/-${f.deletions}`;
|
|
1959
|
+
const status = f.isNewFile ? "new file" : "modified";
|
|
1960
|
+
return `- ${f.path} (${stats}, ${status})`;
|
|
1961
|
+
}).join("\n");
|
|
1962
|
+
let prompt = `## Files Changed (${files.length} files)
|
|
1963
|
+
|
|
1964
|
+
${fileList}`;
|
|
1965
|
+
if (this.taskContext) {
|
|
1966
|
+
prompt += `
|
|
1967
|
+
|
|
1968
|
+
## Task Context
|
|
1969
|
+
${this.taskContext}`;
|
|
1970
|
+
}
|
|
1971
|
+
if (this.repoScope?.length) {
|
|
1972
|
+
prompt += `
|
|
1973
|
+
|
|
1974
|
+
## Repository Scope
|
|
1975
|
+
${this.repoScope.join(", ")}`;
|
|
1976
|
+
}
|
|
1977
|
+
prompt += `
|
|
1978
|
+
|
|
1979
|
+
${prompts.task}`;
|
|
1980
|
+
return prompt;
|
|
1981
|
+
}
|
|
1982
|
+
/**
|
|
1983
|
+
* Convert verified findings to ReviewFinding format
|
|
1984
|
+
*/
|
|
1985
|
+
convertToReviewFindings(result) {
|
|
1986
|
+
return result.verified.map((v) => {
|
|
1987
|
+
const confidence = v.action === "keep" ? "certain" : v.action === "downgrade" ? "likely" : "heuristic";
|
|
1988
|
+
let projectRuleId = v.finding.ruleId;
|
|
1989
|
+
if (projectRuleId?.startsWith("rule:")) {
|
|
1990
|
+
projectRuleId = projectRuleId.slice(5);
|
|
1991
|
+
}
|
|
1992
|
+
const ruleId = projectRuleId ? `rule:${projectRuleId}` : `llm-lite:${v.finding.category}`;
|
|
1993
|
+
const source = projectRuleId ? "rule" : "llm";
|
|
1994
|
+
return {
|
|
1995
|
+
id: `llm-lite-${v.finding.file}-${v.finding.line}`,
|
|
1996
|
+
ruleId,
|
|
1997
|
+
file: v.finding.file,
|
|
1998
|
+
line: v.finding.line,
|
|
1999
|
+
endLine: v.finding.endLine,
|
|
2000
|
+
column: 1,
|
|
2001
|
+
message: v.finding.message,
|
|
2002
|
+
severity: v.finding.severity,
|
|
2003
|
+
confidence,
|
|
2004
|
+
type: v.finding.category,
|
|
2005
|
+
engine: "llm-lite",
|
|
2006
|
+
source,
|
|
2007
|
+
suggestion: v.finding.suggestion,
|
|
2008
|
+
snippet: v.finding.codeSnippet
|
|
2009
|
+
};
|
|
2010
|
+
});
|
|
2011
|
+
}
|
|
2012
|
+
};
|
|
2013
|
+
async function runLLMLiteAnalysis(request) {
|
|
2014
|
+
const analyzer = new LLMLiteAnalyzer(request);
|
|
2015
|
+
return analyzer.analyze();
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
export { ArchitectureAnalyzer, CategoryValidator, DEFAULT_BUDGET, LLMLiteAnalyzer, NamingAnalyzer, SecurityAnalyzer, ToolExecutor, VerificationEngine, buildCategoryAliases, buildToolDefinitions, createCategoryValidator, createToolExecutor, createVerificationEngine, discoverCategories, getCategoryValidator, runLLMLiteAnalysis };
|
|
2019
|
+
//# sourceMappingURL=index.js.map
|
|
2020
|
+
//# sourceMappingURL=index.js.map
|