@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/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