@juspay/yama 1.1.0 → 1.2.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/CHANGELOG.md +34 -1
- package/README.md +152 -120
- package/dist/cli/index.js +201 -200
- package/dist/core/ContextGatherer.d.ts +10 -5
- package/dist/core/ContextGatherer.js +176 -161
- package/dist/core/Guardian.d.ts +1 -1
- package/dist/core/Guardian.js +126 -122
- package/dist/core/providers/BitbucketProvider.d.ts +3 -3
- package/dist/core/providers/BitbucketProvider.js +129 -121
- package/dist/features/CodeReviewer.d.ts +7 -3
- package/dist/features/CodeReviewer.js +314 -222
- package/dist/features/DescriptionEnhancer.d.ts +3 -3
- package/dist/features/DescriptionEnhancer.js +115 -94
- package/dist/index.d.ts +11 -11
- package/dist/index.js +10 -48
- package/dist/types/index.d.ts +27 -21
- package/dist/types/index.js +13 -18
- package/dist/utils/Cache.d.ts +6 -1
- package/dist/utils/Cache.js +78 -68
- package/dist/utils/ConfigManager.d.ts +5 -1
- package/dist/utils/ConfigManager.js +301 -253
- package/dist/utils/Logger.d.ts +2 -2
- package/dist/utils/Logger.js +69 -67
- package/dist/utils/MemoryBankManager.d.ts +73 -0
- package/dist/utils/MemoryBankManager.js +310 -0
- package/dist/utils/ProviderLimits.d.ts +58 -0
- package/dist/utils/ProviderLimits.js +143 -0
- package/package.json +7 -6
- package/yama.config.example.yaml +37 -21
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
/**
|
|
3
2
|
* Enhanced Code Reviewer - Optimized to work with Unified Context
|
|
4
3
|
* Preserves all original functionality from pr-police.js but optimized
|
|
5
4
|
*/
|
|
6
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
exports.CodeReviewer = void 0;
|
|
8
|
-
exports.createCodeReviewer = createCodeReviewer;
|
|
9
5
|
// NeuroLink will be dynamically imported
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
import { ProviderError, } from "../types/index.js";
|
|
7
|
+
import { logger } from "../utils/Logger.js";
|
|
8
|
+
import { getProviderTokenLimit } from "../utils/ProviderLimits.js";
|
|
9
|
+
export class CodeReviewer {
|
|
10
|
+
neurolink;
|
|
11
|
+
bitbucketProvider;
|
|
12
|
+
aiConfig;
|
|
13
|
+
reviewConfig;
|
|
13
14
|
constructor(bitbucketProvider, aiConfig, reviewConfig) {
|
|
14
15
|
this.bitbucketProvider = bitbucketProvider;
|
|
15
16
|
this.aiConfig = aiConfig;
|
|
@@ -21,8 +22,8 @@ class CodeReviewer {
|
|
|
21
22
|
async reviewCodeWithContext(context, options) {
|
|
22
23
|
const startTime = Date.now();
|
|
23
24
|
try {
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
logger.phase("🧪 Conducting AI-powered code analysis...");
|
|
26
|
+
logger.info(`Analyzing ${context.diffStrategy.fileCount} files using ${context.diffStrategy.strategy} strategy`);
|
|
26
27
|
const analysisPrompt = this.buildAnalysisPrompt(context, options);
|
|
27
28
|
const violations = await this.analyzeWithAI(analysisPrompt, context);
|
|
28
29
|
const validatedViolations = this.validateViolations(violations, context);
|
|
@@ -31,12 +32,12 @@ class CodeReviewer {
|
|
|
31
32
|
}
|
|
32
33
|
const duration = Math.round((Date.now() - startTime) / 1000);
|
|
33
34
|
const result = this.generateReviewResult(validatedViolations, duration, context);
|
|
34
|
-
|
|
35
|
+
logger.success(`Code review completed in ${duration}s: ${validatedViolations.length} violations found`);
|
|
35
36
|
return result;
|
|
36
37
|
}
|
|
37
38
|
catch (error) {
|
|
38
|
-
|
|
39
|
-
throw new
|
|
39
|
+
logger.error(`Code review failed: ${error.message}`);
|
|
40
|
+
throw new ProviderError(`Code review failed: ${error.message}`);
|
|
40
41
|
}
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
@@ -46,7 +47,9 @@ class CodeReviewer {
|
|
|
46
47
|
const validatedViolations = [];
|
|
47
48
|
const diffContent = this.extractDiffContent(context);
|
|
48
49
|
for (const violation of violations) {
|
|
49
|
-
if (violation.type ===
|
|
50
|
+
if (violation.type === "inline" &&
|
|
51
|
+
violation.code_snippet &&
|
|
52
|
+
violation.file) {
|
|
50
53
|
// Check if the code snippet exists in the diff
|
|
51
54
|
if (diffContent.includes(violation.code_snippet)) {
|
|
52
55
|
validatedViolations.push(violation);
|
|
@@ -58,8 +61,8 @@ class CodeReviewer {
|
|
|
58
61
|
validatedViolations.push(fixedViolation);
|
|
59
62
|
}
|
|
60
63
|
else {
|
|
61
|
-
|
|
62
|
-
|
|
64
|
+
logger.debug(`⚠️ Skipping violation - snippet not found in diff: ${violation.file}`);
|
|
65
|
+
logger.debug(` Original snippet: "${violation.code_snippet}"`);
|
|
63
66
|
}
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -68,50 +71,56 @@ class CodeReviewer {
|
|
|
68
71
|
validatedViolations.push(violation);
|
|
69
72
|
}
|
|
70
73
|
}
|
|
71
|
-
|
|
74
|
+
logger.debug(`Validated ${validatedViolations.length} out of ${violations.length} violations`);
|
|
72
75
|
return validatedViolations;
|
|
73
76
|
}
|
|
74
77
|
/**
|
|
75
78
|
* Try to fix code snippet by finding it in the actual diff
|
|
76
79
|
*/
|
|
77
80
|
tryFixCodeSnippet(violation, context) {
|
|
78
|
-
if (!violation.file || !violation.code_snippet)
|
|
81
|
+
if (!violation.file || !violation.code_snippet) {
|
|
79
82
|
return null;
|
|
83
|
+
}
|
|
80
84
|
try {
|
|
81
85
|
// Get the diff for this specific file
|
|
82
86
|
let fileDiff;
|
|
83
|
-
if (context.diffStrategy.strategy ===
|
|
87
|
+
if (context.diffStrategy.strategy === "whole" && context.prDiff) {
|
|
84
88
|
// Extract file diff from whole diff - handle different path formats
|
|
85
|
-
const diffLines = context.prDiff.diff.split(
|
|
89
|
+
const diffLines = context.prDiff.diff.split("\n");
|
|
86
90
|
let fileStartIndex = -1;
|
|
87
91
|
// Generate all possible path variations
|
|
88
92
|
const pathVariations = this.generatePathVariations(violation.file);
|
|
89
93
|
// Try to find the file in the diff with various path formats
|
|
90
94
|
for (let i = 0; i < diffLines.length; i++) {
|
|
91
95
|
const line = diffLines[i];
|
|
92
|
-
if (line.startsWith(
|
|
96
|
+
if (line.startsWith("diff --git") || line.startsWith("Index:")) {
|
|
93
97
|
for (const pathVariation of pathVariations) {
|
|
94
98
|
if (line.includes(pathVariation)) {
|
|
95
99
|
fileStartIndex = i;
|
|
96
100
|
break;
|
|
97
101
|
}
|
|
98
102
|
}
|
|
99
|
-
if (fileStartIndex >= 0)
|
|
103
|
+
if (fileStartIndex >= 0) {
|
|
100
104
|
break;
|
|
105
|
+
}
|
|
101
106
|
}
|
|
102
107
|
}
|
|
103
108
|
if (fileStartIndex >= 0) {
|
|
104
|
-
const nextFileIndex = diffLines.findIndex((line, idx) => idx > fileStartIndex &&
|
|
105
|
-
|
|
109
|
+
const nextFileIndex = diffLines.findIndex((line, idx) => idx > fileStartIndex &&
|
|
110
|
+
(line.startsWith("diff --git") || line.startsWith("Index:")));
|
|
111
|
+
fileDiff = diffLines
|
|
112
|
+
.slice(fileStartIndex, nextFileIndex > 0 ? nextFileIndex : diffLines.length)
|
|
113
|
+
.join("\n");
|
|
106
114
|
}
|
|
107
115
|
}
|
|
108
|
-
else if (context.diffStrategy.strategy ===
|
|
116
|
+
else if (context.diffStrategy.strategy === "file-by-file" &&
|
|
117
|
+
context.fileDiffs) {
|
|
109
118
|
// Try all path variations
|
|
110
119
|
const pathVariations = this.generatePathVariations(violation.file);
|
|
111
120
|
for (const path of pathVariations) {
|
|
112
121
|
fileDiff = context.fileDiffs.get(path);
|
|
113
122
|
if (fileDiff) {
|
|
114
|
-
|
|
123
|
+
logger.debug(`Found diff for ${violation.file} using variation: ${path}`);
|
|
115
124
|
break;
|
|
116
125
|
}
|
|
117
126
|
}
|
|
@@ -120,14 +129,14 @@ class CodeReviewer {
|
|
|
120
129
|
for (const [key, value] of context.fileDiffs.entries()) {
|
|
121
130
|
if (key.endsWith(violation.file) || violation.file.endsWith(key)) {
|
|
122
131
|
fileDiff = value;
|
|
123
|
-
|
|
132
|
+
logger.debug(`Found diff for ${violation.file} using partial match: ${key}`);
|
|
124
133
|
break;
|
|
125
134
|
}
|
|
126
135
|
}
|
|
127
136
|
}
|
|
128
137
|
}
|
|
129
138
|
if (!fileDiff) {
|
|
130
|
-
|
|
139
|
+
logger.debug(`❌ Could not find diff for file: ${violation.file}`);
|
|
131
140
|
return null;
|
|
132
141
|
}
|
|
133
142
|
// First, try to find the exact line with line number extraction
|
|
@@ -136,27 +145,28 @@ class CodeReviewer {
|
|
|
136
145
|
const fixedViolation = { ...violation };
|
|
137
146
|
fixedViolation.line_type = lineInfo.lineType;
|
|
138
147
|
// Extract search context from the diff
|
|
139
|
-
const diffLines = fileDiff.split(
|
|
140
|
-
const snippetIndex = diffLines.findIndex(line => line === violation.code_snippet);
|
|
148
|
+
const diffLines = fileDiff.split("\n");
|
|
149
|
+
const snippetIndex = diffLines.findIndex((line) => line === violation.code_snippet);
|
|
141
150
|
if (snippetIndex > 0 && snippetIndex < diffLines.length - 1) {
|
|
142
151
|
fixedViolation.search_context = {
|
|
143
152
|
before: [diffLines[snippetIndex - 1]],
|
|
144
|
-
after: [diffLines[snippetIndex + 1]]
|
|
153
|
+
after: [diffLines[snippetIndex + 1]],
|
|
145
154
|
};
|
|
146
155
|
}
|
|
147
|
-
|
|
156
|
+
logger.debug(`✅ Found exact match with line number for ${violation.file}`);
|
|
148
157
|
return fixedViolation;
|
|
149
158
|
}
|
|
150
159
|
// Fallback: Clean the snippet and try fuzzy matching
|
|
151
160
|
const cleanSnippet = violation.code_snippet
|
|
152
161
|
.trim()
|
|
153
|
-
.replace(/^[+\-\s]/,
|
|
162
|
+
.replace(/^[+\-\s]/, ""); // Remove diff prefix for searching
|
|
154
163
|
// Look for the clean snippet in the diff
|
|
155
|
-
const diffLines = fileDiff.split(
|
|
164
|
+
const diffLines = fileDiff.split("\n");
|
|
156
165
|
for (let i = 0; i < diffLines.length; i++) {
|
|
157
166
|
const line = diffLines[i];
|
|
158
|
-
const cleanLine = line.replace(/^[+\-\s]/,
|
|
159
|
-
if (cleanLine.includes(cleanSnippet) ||
|
|
167
|
+
const cleanLine = line.replace(/^[+\-\s]/, "").trim();
|
|
168
|
+
if (cleanLine.includes(cleanSnippet) ||
|
|
169
|
+
cleanSnippet.includes(cleanLine)) {
|
|
160
170
|
// Found a match! Update the violation with the correct snippet
|
|
161
171
|
const fixedViolation = { ...violation };
|
|
162
172
|
fixedViolation.code_snippet = line; // Use the full line with diff prefix
|
|
@@ -164,18 +174,18 @@ class CodeReviewer {
|
|
|
164
174
|
if (i > 0 && i < diffLines.length - 1) {
|
|
165
175
|
fixedViolation.search_context = {
|
|
166
176
|
before: [diffLines[i - 1]],
|
|
167
|
-
after: [diffLines[i + 1]]
|
|
177
|
+
after: [diffLines[i + 1]],
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
|
-
|
|
180
|
+
logger.debug(`✅ Fixed code snippet for ${violation.file} using fuzzy match`);
|
|
171
181
|
return fixedViolation;
|
|
172
182
|
}
|
|
173
183
|
}
|
|
174
|
-
|
|
175
|
-
|
|
184
|
+
logger.debug(`❌ Could not find snippet in diff for ${violation.file}`);
|
|
185
|
+
logger.debug(` Looking for: "${violation.code_snippet}"`);
|
|
176
186
|
}
|
|
177
187
|
catch (error) {
|
|
178
|
-
|
|
188
|
+
logger.debug(`Error fixing code snippet: ${error.message}`);
|
|
179
189
|
}
|
|
180
190
|
return null;
|
|
181
191
|
}
|
|
@@ -183,7 +193,7 @@ class CodeReviewer {
|
|
|
183
193
|
* Get system prompt for security-focused code review
|
|
184
194
|
*/
|
|
185
195
|
getSecurityReviewSystemPrompt() {
|
|
186
|
-
return this.reviewConfig.systemPrompt ||
|
|
196
|
+
return (this.reviewConfig.systemPrompt ||
|
|
187
197
|
`You are an Expert Security Code Reviewer for enterprise applications. Your role is to:
|
|
188
198
|
|
|
189
199
|
🔒 SECURITY FIRST: Prioritize security vulnerabilities and data protection
|
|
@@ -194,14 +204,17 @@ class CodeReviewer {
|
|
|
194
204
|
You provide actionable, educational feedback with specific examples and solutions.
|
|
195
205
|
Focus on critical issues that could impact production systems.
|
|
196
206
|
|
|
197
|
-
CRITICAL INSTRUCTION: When identifying issues, you MUST copy the EXACT line from the diff, including the diff prefix (+, -, or space). Do not modify or clean the line in any way
|
|
207
|
+
CRITICAL INSTRUCTION: When identifying issues, you MUST copy the EXACT line from the diff, including the diff prefix (+, -, or space). Do not modify or clean the line in any way.`);
|
|
198
208
|
}
|
|
199
209
|
/**
|
|
200
210
|
* Get analysis requirements from config or defaults
|
|
201
211
|
*/
|
|
202
212
|
getAnalysisRequirements() {
|
|
203
|
-
if (this.reviewConfig.focusAreas &&
|
|
204
|
-
|
|
213
|
+
if (this.reviewConfig.focusAreas &&
|
|
214
|
+
this.reviewConfig.focusAreas.length > 0) {
|
|
215
|
+
return this.reviewConfig.focusAreas
|
|
216
|
+
.map((area) => `### ${area}`)
|
|
217
|
+
.join("\n\n");
|
|
205
218
|
}
|
|
206
219
|
// Default analysis requirements
|
|
207
220
|
return `### 🔒 Security Analysis (CRITICAL PRIORITY)
|
|
@@ -228,7 +241,7 @@ CRITICAL INSTRUCTION: When identifying issues, you MUST copy the EXACT line from
|
|
|
228
241
|
*/
|
|
229
242
|
buildCoreAnalysisPrompt(context) {
|
|
230
243
|
const diffContent = this.extractDiffContent(context);
|
|
231
|
-
return `Conduct a comprehensive security and quality analysis of this ${context.diffStrategy.strategy ===
|
|
244
|
+
return `Conduct a comprehensive security and quality analysis of this ${context.diffStrategy.strategy === "whole" ? "pull request" : "code changeset"}.
|
|
232
245
|
|
|
233
246
|
## COMPLETE PR CONTEXT:
|
|
234
247
|
**Title**: ${context.pr.title}
|
|
@@ -248,7 +261,7 @@ CRITICAL INSTRUCTION: When identifying issues, you MUST copy the EXACT line from
|
|
|
248
261
|
${context.projectContext.memoryBank.projectContext || context.projectContext.memoryBank.summary}
|
|
249
262
|
|
|
250
263
|
## PROJECT RULES & STANDARDS:
|
|
251
|
-
${context.projectContext.clinerules ||
|
|
264
|
+
${context.projectContext.clinerules || "No specific rules defined"}
|
|
252
265
|
|
|
253
266
|
## COMPLETE CODE CHANGES (NO TRUNCATION):
|
|
254
267
|
${diffContent}
|
|
@@ -310,17 +323,18 @@ Return ONLY valid JSON:
|
|
|
310
323
|
* Extract diff content based on strategy
|
|
311
324
|
*/
|
|
312
325
|
extractDiffContent(context) {
|
|
313
|
-
if (context.diffStrategy.strategy ===
|
|
326
|
+
if (context.diffStrategy.strategy === "whole" && context.prDiff) {
|
|
314
327
|
return context.prDiff.diff || JSON.stringify(context.prDiff, null, 2);
|
|
315
328
|
}
|
|
316
|
-
else if (context.diffStrategy.strategy ===
|
|
329
|
+
else if (context.diffStrategy.strategy === "file-by-file" &&
|
|
330
|
+
context.fileDiffs) {
|
|
317
331
|
const fileDiffArray = Array.from(context.fileDiffs.entries()).map(([file, diff]) => ({
|
|
318
332
|
file,
|
|
319
|
-
diff
|
|
333
|
+
diff,
|
|
320
334
|
}));
|
|
321
335
|
return JSON.stringify(fileDiffArray, null, 2);
|
|
322
336
|
}
|
|
323
|
-
return
|
|
337
|
+
return "No diff content available";
|
|
324
338
|
}
|
|
325
339
|
/**
|
|
326
340
|
* Detect project type for better context
|
|
@@ -329,42 +343,53 @@ Return ONLY valid JSON:
|
|
|
329
343
|
const fileExtensions = new Set();
|
|
330
344
|
// Extract file extensions from changes
|
|
331
345
|
if (context.pr.fileChanges) {
|
|
332
|
-
context.pr.fileChanges.forEach(file => {
|
|
333
|
-
const ext = file.split(
|
|
334
|
-
if (ext)
|
|
346
|
+
context.pr.fileChanges.forEach((file) => {
|
|
347
|
+
const ext = file.split(".").pop()?.toLowerCase();
|
|
348
|
+
if (ext) {
|
|
335
349
|
fileExtensions.add(ext);
|
|
350
|
+
}
|
|
336
351
|
});
|
|
337
352
|
}
|
|
338
|
-
if (fileExtensions.has(
|
|
339
|
-
return
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (fileExtensions.has(
|
|
345
|
-
return
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
if (fileExtensions.has(
|
|
351
|
-
return
|
|
352
|
-
|
|
353
|
+
if (fileExtensions.has("rs") || fileExtensions.has("res")) {
|
|
354
|
+
return "rescript";
|
|
355
|
+
}
|
|
356
|
+
if (fileExtensions.has("ts") || fileExtensions.has("tsx")) {
|
|
357
|
+
return "typescript";
|
|
358
|
+
}
|
|
359
|
+
if (fileExtensions.has("js") || fileExtensions.has("jsx")) {
|
|
360
|
+
return "javascript";
|
|
361
|
+
}
|
|
362
|
+
if (fileExtensions.has("py")) {
|
|
363
|
+
return "python";
|
|
364
|
+
}
|
|
365
|
+
if (fileExtensions.has("go")) {
|
|
366
|
+
return "golang";
|
|
367
|
+
}
|
|
368
|
+
if (fileExtensions.has("java")) {
|
|
369
|
+
return "java";
|
|
370
|
+
}
|
|
371
|
+
if (fileExtensions.has("cpp") || fileExtensions.has("c")) {
|
|
372
|
+
return "cpp";
|
|
373
|
+
}
|
|
374
|
+
return "mixed";
|
|
353
375
|
}
|
|
354
376
|
/**
|
|
355
377
|
* Assess complexity level for better AI context
|
|
356
378
|
*/
|
|
357
379
|
assessComplexity(context) {
|
|
358
380
|
const fileCount = context.diffStrategy.fileCount;
|
|
359
|
-
const hasLargeFiles = context.diffStrategy.estimatedSize.includes(
|
|
381
|
+
const hasLargeFiles = context.diffStrategy.estimatedSize.includes("Large");
|
|
360
382
|
const hasComments = (context.pr.comments?.length || 0) > 0;
|
|
361
|
-
if (fileCount > 50)
|
|
362
|
-
return
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
383
|
+
if (fileCount > 50) {
|
|
384
|
+
return "very-high";
|
|
385
|
+
}
|
|
386
|
+
if (fileCount > 20 || hasLargeFiles) {
|
|
387
|
+
return "high";
|
|
388
|
+
}
|
|
389
|
+
if (fileCount > 10 || hasComments) {
|
|
390
|
+
return "medium";
|
|
391
|
+
}
|
|
392
|
+
return "low";
|
|
368
393
|
}
|
|
369
394
|
/**
|
|
370
395
|
* Legacy method - kept for compatibility but simplified
|
|
@@ -373,21 +398,37 @@ Return ONLY valid JSON:
|
|
|
373
398
|
// Legacy method - now delegates to new structure
|
|
374
399
|
return this.buildCoreAnalysisPrompt(context);
|
|
375
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Get safe token limit based on AI provider using shared utility
|
|
403
|
+
*/
|
|
404
|
+
getSafeTokenLimit() {
|
|
405
|
+
const provider = this.aiConfig.provider || "auto";
|
|
406
|
+
const configuredTokens = this.aiConfig.maxTokens;
|
|
407
|
+
// Use conservative limits for CodeReviewer (safer for large diffs)
|
|
408
|
+
const providerLimit = getProviderTokenLimit(provider, true);
|
|
409
|
+
// Use the smaller of configured tokens or provider limit
|
|
410
|
+
if (configuredTokens && configuredTokens > 0) {
|
|
411
|
+
const safeLimit = Math.min(configuredTokens, providerLimit);
|
|
412
|
+
logger.debug(`Token limit: configured=${configuredTokens}, provider=${providerLimit}, using=${safeLimit}`);
|
|
413
|
+
return safeLimit;
|
|
414
|
+
}
|
|
415
|
+
logger.debug(`Token limit: using provider default=${providerLimit} for ${provider}`);
|
|
416
|
+
return providerLimit;
|
|
417
|
+
}
|
|
376
418
|
/**
|
|
377
419
|
* Analyze code with AI using the enhanced prompt
|
|
378
420
|
*/
|
|
379
421
|
async analyzeWithAI(prompt, context) {
|
|
380
422
|
try {
|
|
381
|
-
|
|
423
|
+
logger.debug("Starting AI analysis...");
|
|
382
424
|
// Initialize NeuroLink with eval-based dynamic import
|
|
383
425
|
if (!this.neurolink) {
|
|
384
|
-
const
|
|
385
|
-
const { NeuroLink } = await dynamicImport('@juspay/neurolink');
|
|
426
|
+
const { NeuroLink } = await import("@juspay/neurolink");
|
|
386
427
|
this.neurolink = new NeuroLink();
|
|
387
428
|
}
|
|
388
429
|
// Extract context from unified context for better AI understanding
|
|
389
430
|
const aiContext = {
|
|
390
|
-
operation:
|
|
431
|
+
operation: "code-review",
|
|
391
432
|
repository: `${context.identifier.workspace}/${context.identifier.repository}`,
|
|
392
433
|
branch: context.identifier.branch,
|
|
393
434
|
prId: context.identifier.pullRequestId,
|
|
@@ -395,52 +436,59 @@ Return ONLY valid JSON:
|
|
|
395
436
|
prAuthor: context.pr.author,
|
|
396
437
|
fileCount: context.diffStrategy.fileCount,
|
|
397
438
|
diffStrategy: context.diffStrategy.strategy,
|
|
398
|
-
analysisType: context.diffStrategy.strategy ===
|
|
439
|
+
analysisType: context.diffStrategy.strategy === "whole"
|
|
440
|
+
? "comprehensive"
|
|
441
|
+
: "file-by-file",
|
|
399
442
|
projectType: this.detectProjectType(context),
|
|
400
443
|
hasExistingComments: (context.pr.comments?.length || 0) > 0,
|
|
401
|
-
complexity: this.assessComplexity(context)
|
|
444
|
+
complexity: this.assessComplexity(context),
|
|
402
445
|
};
|
|
403
446
|
// Simplified, focused prompt without context pollution
|
|
404
447
|
const corePrompt = this.buildCoreAnalysisPrompt(context);
|
|
448
|
+
// Get safe token limit based on provider
|
|
449
|
+
const safeMaxTokens = this.getSafeTokenLimit();
|
|
450
|
+
logger.debug(`Using AI provider: ${this.aiConfig.provider || "auto"}`);
|
|
451
|
+
logger.debug(`Configured maxTokens: ${this.aiConfig.maxTokens}`);
|
|
452
|
+
logger.debug(`Safe maxTokens limit: ${safeMaxTokens}`);
|
|
405
453
|
const result = await this.neurolink.generate({
|
|
406
454
|
input: { text: corePrompt },
|
|
407
455
|
systemPrompt: this.getSecurityReviewSystemPrompt(),
|
|
408
|
-
provider: this.aiConfig.provider ||
|
|
409
|
-
model: this.aiConfig.model ||
|
|
456
|
+
provider: this.aiConfig.provider || "auto", // Auto-select best provider
|
|
457
|
+
model: this.aiConfig.model || "best", // Use most capable model
|
|
410
458
|
temperature: this.aiConfig.temperature || 0.3, // Lower for more focused analysis
|
|
411
|
-
maxTokens:
|
|
412
|
-
timeout:
|
|
459
|
+
maxTokens: safeMaxTokens, // Use provider-aware safe token limit
|
|
460
|
+
timeout: "15m", // Allow plenty of time for thorough analysis
|
|
413
461
|
context: aiContext,
|
|
414
462
|
enableAnalytics: this.aiConfig.enableAnalytics || true,
|
|
415
|
-
enableEvaluation: false // Disabled to prevent evaluation warnings
|
|
463
|
+
enableEvaluation: false, // Disabled to prevent evaluation warnings
|
|
416
464
|
});
|
|
417
465
|
// Log analytics if available
|
|
418
466
|
if (result.analytics) {
|
|
419
|
-
|
|
467
|
+
logger.debug(`AI Analytics - Provider: ${result.provider}, Response Time: ${result.responseTime}ms, Quality Score: ${result.evaluation?.overallScore}`);
|
|
420
468
|
}
|
|
421
|
-
|
|
469
|
+
logger.debug("AI analysis completed, parsing response...");
|
|
422
470
|
// Modern NeuroLink returns { content: string }
|
|
423
471
|
const analysisData = this.parseAIResponse(result);
|
|
424
472
|
// Display AI response for debugging
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
473
|
+
if (logger.getConfig().verbose) {
|
|
474
|
+
logger.debug("AI Analysis Response:");
|
|
475
|
+
logger.debug("═".repeat(80));
|
|
476
|
+
logger.debug(JSON.stringify(analysisData, null, 2));
|
|
477
|
+
logger.debug("═".repeat(80));
|
|
430
478
|
}
|
|
431
479
|
if (!analysisData.violations || !Array.isArray(analysisData.violations)) {
|
|
432
|
-
|
|
480
|
+
logger.debug("No violations array found in AI response");
|
|
433
481
|
return [];
|
|
434
482
|
}
|
|
435
|
-
|
|
483
|
+
logger.debug(`AI analysis found ${analysisData.violations.length} violations`);
|
|
436
484
|
return analysisData.violations;
|
|
437
485
|
}
|
|
438
486
|
catch (error) {
|
|
439
|
-
if (error.message?.includes(
|
|
440
|
-
|
|
441
|
-
throw new Error(
|
|
487
|
+
if (error.message?.includes("timeout")) {
|
|
488
|
+
logger.error("⏰ AI analysis timed out after 15 minutes");
|
|
489
|
+
throw new Error("Analysis timeout - try reducing diff size or adjusting timeout");
|
|
442
490
|
}
|
|
443
|
-
|
|
491
|
+
logger.error(`AI analysis failed: ${error.message}`);
|
|
444
492
|
throw error;
|
|
445
493
|
}
|
|
446
494
|
}
|
|
@@ -448,12 +496,12 @@ Return ONLY valid JSON:
|
|
|
448
496
|
* Post comments to PR using unified context - matching pr-police.js exactly
|
|
449
497
|
*/
|
|
450
498
|
async postComments(context, violations, _options) {
|
|
451
|
-
|
|
499
|
+
logger.phase("📝 Posting review comments...");
|
|
452
500
|
let commentsPosted = 0;
|
|
453
501
|
let commentsFailed = 0;
|
|
454
502
|
const failedComments = [];
|
|
455
503
|
// Post inline comments
|
|
456
|
-
const inlineViolations = violations.filter(v => v.type ===
|
|
504
|
+
const inlineViolations = violations.filter((v) => v.type === "inline" && v.file && v.code_snippet);
|
|
457
505
|
for (const violation of inlineViolations) {
|
|
458
506
|
try {
|
|
459
507
|
// Clean file path - remove protocol prefixes ONLY (keep a/ and b/ prefixes)
|
|
@@ -467,19 +515,19 @@ Return ONLY valid JSON:
|
|
|
467
515
|
// Clean code snippet and fix search context - EXACTLY like pr-police.js
|
|
468
516
|
const processedViolation = this.cleanCodeSnippet(violation);
|
|
469
517
|
if (!processedViolation) {
|
|
470
|
-
|
|
518
|
+
logger.debug(`⚠️ Skipping invalid violation for ${cleanFilePath}`);
|
|
471
519
|
continue;
|
|
472
520
|
}
|
|
473
521
|
const formattedComment = this.formatInlineComment(processedViolation);
|
|
474
522
|
// Debug logging
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
523
|
+
logger.debug(`🔍 Posting inline comment:`);
|
|
524
|
+
logger.debug(` File: ${cleanFilePath}`);
|
|
525
|
+
logger.debug(` Issue: ${processedViolation.issue}`);
|
|
526
|
+
logger.debug(` Original snippet: ${violation.code_snippet}`);
|
|
527
|
+
logger.debug(` Processed snippet: ${processedViolation.code_snippet}`);
|
|
480
528
|
if (processedViolation.search_context) {
|
|
481
|
-
|
|
482
|
-
|
|
529
|
+
logger.debug(` Search context before: ${JSON.stringify(processedViolation.search_context.before)}`);
|
|
530
|
+
logger.debug(` Search context after: ${JSON.stringify(processedViolation.search_context.after)}`);
|
|
483
531
|
}
|
|
484
532
|
// Use new code snippet approach - EXACTLY like pr-police.js
|
|
485
533
|
await this.bitbucketProvider.addComment(context.identifier, formattedComment, {
|
|
@@ -489,21 +537,21 @@ Return ONLY valid JSON:
|
|
|
489
537
|
codeSnippet: processedViolation.code_snippet,
|
|
490
538
|
searchContext: processedViolation.search_context,
|
|
491
539
|
matchStrategy: "best", // Use best match strategy instead of strict for flexibility
|
|
492
|
-
suggestion: processedViolation.suggestion // Pass the suggestion for inline code suggestions
|
|
540
|
+
suggestion: processedViolation.suggestion, // Pass the suggestion for inline code suggestions
|
|
493
541
|
});
|
|
494
542
|
commentsPosted++;
|
|
495
|
-
|
|
543
|
+
logger.debug(`✅ Posted inline comment: ${cleanFilePath} (${processedViolation.issue})`);
|
|
496
544
|
}
|
|
497
545
|
catch (error) {
|
|
498
546
|
commentsFailed++;
|
|
499
547
|
const errorMsg = error.message;
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
548
|
+
logger.debug(`❌ Failed to post inline comment: ${errorMsg}`);
|
|
549
|
+
logger.debug(` File: ${violation.file}, Issue: ${violation.issue}`);
|
|
550
|
+
logger.debug(` Code snippet: ${violation.code_snippet}`);
|
|
503
551
|
failedComments.push({
|
|
504
552
|
file: violation.file,
|
|
505
553
|
issue: violation.issue,
|
|
506
|
-
error: errorMsg
|
|
554
|
+
error: errorMsg,
|
|
507
555
|
});
|
|
508
556
|
}
|
|
509
557
|
}
|
|
@@ -513,15 +561,15 @@ Return ONLY valid JSON:
|
|
|
513
561
|
const summaryComment = this.generateSummaryComment(violations, context, failedComments);
|
|
514
562
|
await this.bitbucketProvider.addComment(context.identifier, summaryComment);
|
|
515
563
|
commentsPosted++;
|
|
516
|
-
|
|
564
|
+
logger.debug("✅ Posted summary comment");
|
|
517
565
|
}
|
|
518
566
|
catch (error) {
|
|
519
|
-
|
|
567
|
+
logger.debug(`❌ Failed to post summary comment: ${error.message}`);
|
|
520
568
|
}
|
|
521
569
|
}
|
|
522
|
-
|
|
570
|
+
logger.success(`✅ Posted ${commentsPosted} comments successfully`);
|
|
523
571
|
if (commentsFailed > 0) {
|
|
524
|
-
|
|
572
|
+
logger.warn(`⚠️ Failed to post ${commentsFailed} inline comments`);
|
|
525
573
|
}
|
|
526
574
|
}
|
|
527
575
|
/**
|
|
@@ -529,14 +577,23 @@ Return ONLY valid JSON:
|
|
|
529
577
|
*/
|
|
530
578
|
formatInlineComment(violation) {
|
|
531
579
|
const severityConfig = {
|
|
532
|
-
CRITICAL: {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
580
|
+
CRITICAL: {
|
|
581
|
+
emoji: "🚨",
|
|
582
|
+
badge: "**🚨 CRITICAL SECURITY ISSUE**",
|
|
583
|
+
color: "red",
|
|
584
|
+
},
|
|
585
|
+
MAJOR: { emoji: "⚠️", badge: "**⚠️ MAJOR ISSUE**", color: "orange" },
|
|
586
|
+
MINOR: { emoji: "📝", badge: "**📝 MINOR IMPROVEMENT**", color: "blue" },
|
|
587
|
+
SUGGESTION: { emoji: "💡", badge: "**💡 SUGGESTION**", color: "green" },
|
|
536
588
|
};
|
|
537
589
|
const categoryIcons = {
|
|
538
|
-
security:
|
|
539
|
-
|
|
590
|
+
security: "🔒",
|
|
591
|
+
performance: "⚡",
|
|
592
|
+
maintainability: "🏗️",
|
|
593
|
+
functionality: "⚙️",
|
|
594
|
+
error_handling: "🛡️",
|
|
595
|
+
testing: "🧪",
|
|
596
|
+
general: "📋",
|
|
540
597
|
};
|
|
541
598
|
const config = severityConfig[violation.severity] || severityConfig.MINOR;
|
|
542
599
|
const categoryIcon = categoryIcons[violation.category] || categoryIcons.general;
|
|
@@ -544,20 +601,30 @@ Return ONLY valid JSON:
|
|
|
544
601
|
|
|
545
602
|
**${categoryIcon} ${violation.issue}**
|
|
546
603
|
|
|
547
|
-
**Category**: ${violation.category.replace(/_/g,
|
|
604
|
+
**Category**: ${violation.category.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase())}
|
|
548
605
|
|
|
549
606
|
**Issue**: ${violation.message}`;
|
|
550
607
|
if (violation.impact) {
|
|
551
608
|
comment += `\n\n**Impact**: ${violation.impact}`;
|
|
552
609
|
}
|
|
553
610
|
if (violation.suggestion) {
|
|
554
|
-
const fileExt = violation.file?.split(
|
|
611
|
+
const fileExt = violation.file?.split(".").pop() || "text";
|
|
555
612
|
const langMap = {
|
|
556
|
-
js:
|
|
557
|
-
|
|
558
|
-
|
|
613
|
+
js: "javascript",
|
|
614
|
+
jsx: "javascript",
|
|
615
|
+
ts: "typescript",
|
|
616
|
+
tsx: "typescript",
|
|
617
|
+
res: "rescript",
|
|
618
|
+
resi: "rescript",
|
|
619
|
+
py: "python",
|
|
620
|
+
java: "java",
|
|
621
|
+
go: "go",
|
|
622
|
+
rb: "ruby",
|
|
623
|
+
php: "php",
|
|
624
|
+
sql: "sql",
|
|
625
|
+
json: "json",
|
|
559
626
|
};
|
|
560
|
-
const language = langMap[fileExt] ||
|
|
627
|
+
const language = langMap[fileExt] || "text";
|
|
561
628
|
// Use the escape method for code blocks
|
|
562
629
|
const escapedCodeBlock = this.escapeMarkdownCodeBlock(violation.suggestion, language);
|
|
563
630
|
comment += `\n\n**💡 Suggested Fix**:\n${escapedCodeBlock}`;
|
|
@@ -570,13 +637,20 @@ Return ONLY valid JSON:
|
|
|
570
637
|
*/
|
|
571
638
|
generateSummaryComment(violations, context, failedComments = []) {
|
|
572
639
|
const stats = this.calculateStats(violations);
|
|
573
|
-
const statusEmoji = stats.criticalCount > 0
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
640
|
+
const statusEmoji = stats.criticalCount > 0
|
|
641
|
+
? "🚨"
|
|
642
|
+
: stats.majorCount > 0
|
|
643
|
+
? "⚠️ "
|
|
644
|
+
: stats.minorCount > 0
|
|
645
|
+
? "📝"
|
|
646
|
+
: "✅";
|
|
647
|
+
const statusText = stats.criticalCount > 0
|
|
648
|
+
? "CRITICAL ISSUES FOUND"
|
|
649
|
+
: stats.majorCount > 0
|
|
650
|
+
? "ISSUES DETECTED"
|
|
651
|
+
: stats.minorCount > 0
|
|
652
|
+
? "IMPROVEMENTS SUGGESTED"
|
|
653
|
+
: "CODE QUALITY APPROVED";
|
|
580
654
|
let comment = `
|
|
581
655
|
╭─────────────────────────────────────────────────────────────╮
|
|
582
656
|
│ ⚔️ **YAMA REVIEW REPORT** ⚔️ │
|
|
@@ -587,10 +661,10 @@ Return ONLY valid JSON:
|
|
|
587
661
|
### 📊 **Security & Quality Analysis**
|
|
588
662
|
| **Severity** | **Count** | **Status** |
|
|
589
663
|
|--------------|-----------|------------|
|
|
590
|
-
| 🚨 Critical | ${stats.criticalCount} | ${stats.criticalCount > 0 ?
|
|
591
|
-
| ⚠️ Major | ${stats.majorCount} | ${stats.majorCount > 0 ?
|
|
592
|
-
| 📝 Minor | ${stats.minorCount} | ${stats.minorCount > 0 ?
|
|
593
|
-
| 💡 Suggestions | ${stats.suggestionCount} | ${stats.suggestionCount > 0 ?
|
|
664
|
+
| 🚨 Critical | ${stats.criticalCount} | ${stats.criticalCount > 0 ? "⛔ Must Fix" : "✅ Clear"} |
|
|
665
|
+
| ⚠️ Major | ${stats.majorCount} | ${stats.majorCount > 0 ? "⚠️ Should Fix" : "✅ Clear"} |
|
|
666
|
+
| 📝 Minor | ${stats.minorCount} | ${stats.minorCount > 0 ? "📝 Consider Fixing" : "✅ Clear"} |
|
|
667
|
+
| 💡 Suggestions | ${stats.suggestionCount} | ${stats.suggestionCount > 0 ? "💡 Optional" : "✅ Clear"} |
|
|
594
668
|
|
|
595
669
|
### 🔍 **Analysis Summary**
|
|
596
670
|
- **📁 Files Analyzed**: ${context.diffStrategy.fileCount}
|
|
@@ -603,12 +677,19 @@ Return ONLY valid JSON:
|
|
|
603
677
|
comment += `\n\n### 📍 **Issues by Category**\n`;
|
|
604
678
|
for (const [category, categoryViolations] of Object.entries(violationsByCategory)) {
|
|
605
679
|
const categoryIcons = {
|
|
606
|
-
security:
|
|
607
|
-
|
|
680
|
+
security: "🔒",
|
|
681
|
+
performance: "⚡",
|
|
682
|
+
maintainability: "🏗️",
|
|
683
|
+
functionality: "⚙️",
|
|
684
|
+
error_handling: "🛡️",
|
|
685
|
+
testing: "🧪",
|
|
686
|
+
general: "📋",
|
|
608
687
|
};
|
|
609
|
-
const icon = categoryIcons[category] ||
|
|
610
|
-
const name = category
|
|
611
|
-
|
|
688
|
+
const icon = categoryIcons[category] || "📋";
|
|
689
|
+
const name = category
|
|
690
|
+
.replace(/_/g, " ")
|
|
691
|
+
.replace(/\b\w/g, (l) => l.toUpperCase());
|
|
692
|
+
comment += `**${icon} ${name}**: ${categoryViolations.length} issue${categoryViolations.length !== 1 ? "s" : ""}\n`;
|
|
612
693
|
}
|
|
613
694
|
}
|
|
614
695
|
// Add failed comments section if any
|
|
@@ -617,17 +698,17 @@ Return ONLY valid JSON:
|
|
|
617
698
|
comment += `Some inline comments could not be posted due to code matching issues. `;
|
|
618
699
|
comment += `Please review the following issues manually:\n\n`;
|
|
619
700
|
for (const failed of failedComments) {
|
|
620
|
-
comment += `- **${failed.issue}** in \`${failed.file ||
|
|
701
|
+
comment += `- **${failed.issue}** in \`${failed.file || "unknown file"}\`\n`;
|
|
621
702
|
}
|
|
622
703
|
}
|
|
623
704
|
// Add recommendation
|
|
624
705
|
const recommendation = stats.criticalCount > 0
|
|
625
|
-
?
|
|
706
|
+
? "🚨 **URGENT**: Critical security issues must be resolved before merge"
|
|
626
707
|
: stats.majorCount > 0
|
|
627
|
-
?
|
|
708
|
+
? "⚠️ **RECOMMENDED**: Address major issues before merge"
|
|
628
709
|
: stats.minorCount > 0
|
|
629
|
-
?
|
|
630
|
-
:
|
|
710
|
+
? "📝 **OPTIONAL**: Consider addressing minor improvements"
|
|
711
|
+
: "✅ **APPROVED**: Code meets security and quality standards";
|
|
631
712
|
comment += `\n\n### 💡 **Recommendation**
|
|
632
713
|
${recommendation}
|
|
633
714
|
|
|
@@ -642,11 +723,10 @@ ${recommendation}
|
|
|
642
723
|
cleanFilePath(filePath) {
|
|
643
724
|
// Clean the file path but preserve the structure - EXACTLY like pr-police.js
|
|
644
725
|
// Only clean src:// and dst:// prefixes, keep a/ and b/ prefixes
|
|
645
|
-
const cleaned = filePath
|
|
646
|
-
.replace(/^(src|dst):\/\//, '');
|
|
726
|
+
const cleaned = filePath.replace(/^(src|dst):\/\//, "");
|
|
647
727
|
// Log the cleaning for debugging
|
|
648
728
|
if (cleaned !== filePath) {
|
|
649
|
-
|
|
729
|
+
logger.debug(`Cleaned file path: ${filePath} -> ${cleaned}`);
|
|
650
730
|
}
|
|
651
731
|
return cleaned;
|
|
652
732
|
}
|
|
@@ -654,12 +734,13 @@ ${recommendation}
|
|
|
654
734
|
* Extract exact file path from diff
|
|
655
735
|
*/
|
|
656
736
|
extractFilePathFromDiff(diff, fileName) {
|
|
657
|
-
const lines = diff.split(
|
|
737
|
+
const lines = diff.split("\n");
|
|
658
738
|
for (const line of lines) {
|
|
659
|
-
if (line.startsWith(
|
|
739
|
+
if (line.startsWith("diff --git")) {
|
|
660
740
|
// Extract both paths: a/path/to/file b/path/to/file
|
|
661
741
|
const match = line.match(/diff --git a\/(.*?) b\/(.*?)$/);
|
|
662
|
-
if (match &&
|
|
742
|
+
if (match &&
|
|
743
|
+
(match[1].includes(fileName) || match[2].includes(fileName))) {
|
|
663
744
|
return match[2]; // Return the 'b/' path (destination)
|
|
664
745
|
}
|
|
665
746
|
}
|
|
@@ -670,12 +751,12 @@ ${recommendation}
|
|
|
670
751
|
* Extract line number from diff for a specific code snippet
|
|
671
752
|
*/
|
|
672
753
|
extractLineNumberFromDiff(fileDiff, codeSnippet) {
|
|
673
|
-
const lines = fileDiff.split(
|
|
754
|
+
const lines = fileDiff.split("\n");
|
|
674
755
|
let currentNewLine = 0;
|
|
675
756
|
let currentOldLine = 0;
|
|
676
757
|
let inHunk = false;
|
|
677
758
|
// Debug logging
|
|
678
|
-
|
|
759
|
+
logger.debug(`Looking for snippet: "${codeSnippet}"`);
|
|
679
760
|
for (let i = 0; i < lines.length; i++) {
|
|
680
761
|
const line = lines[i];
|
|
681
762
|
// Parse hunk headers (e.g., @@ -10,6 +10,8 @@)
|
|
@@ -685,48 +766,51 @@ ${recommendation}
|
|
|
685
766
|
currentOldLine = parseInt(hunkMatch[1]);
|
|
686
767
|
currentNewLine = parseInt(hunkMatch[2]);
|
|
687
768
|
inHunk = true;
|
|
688
|
-
|
|
769
|
+
logger.debug(`Found hunk header: old=${currentOldLine}, new=${currentNewLine}`);
|
|
689
770
|
continue;
|
|
690
771
|
}
|
|
691
772
|
// Skip lines that aren't part of the diff content
|
|
692
|
-
if (!inHunk ||
|
|
773
|
+
if (!inHunk ||
|
|
774
|
+
(!line.startsWith("+") &&
|
|
775
|
+
!line.startsWith("-") &&
|
|
776
|
+
!line.startsWith(" "))) {
|
|
693
777
|
continue;
|
|
694
778
|
}
|
|
695
779
|
// Check if this line matches our snippet
|
|
696
780
|
if (line === codeSnippet) {
|
|
697
781
|
let resultLine;
|
|
698
782
|
let lineType;
|
|
699
|
-
if (line.startsWith(
|
|
783
|
+
if (line.startsWith("+")) {
|
|
700
784
|
resultLine = currentNewLine;
|
|
701
|
-
lineType =
|
|
785
|
+
lineType = "ADDED";
|
|
702
786
|
}
|
|
703
|
-
else if (line.startsWith(
|
|
787
|
+
else if (line.startsWith("-")) {
|
|
704
788
|
resultLine = currentOldLine;
|
|
705
|
-
lineType =
|
|
789
|
+
lineType = "REMOVED";
|
|
706
790
|
}
|
|
707
791
|
else {
|
|
708
792
|
resultLine = currentNewLine;
|
|
709
|
-
lineType =
|
|
793
|
+
lineType = "CONTEXT";
|
|
710
794
|
}
|
|
711
|
-
|
|
795
|
+
logger.debug(`Found match at line ${resultLine} (${lineType})`);
|
|
712
796
|
return { lineNumber: resultLine, lineType };
|
|
713
797
|
}
|
|
714
798
|
// Update line counters AFTER checking for match
|
|
715
799
|
// For added lines: only increment new line counter
|
|
716
800
|
// For removed lines: only increment old line counter
|
|
717
801
|
// For context lines: increment both counters
|
|
718
|
-
if (line.startsWith(
|
|
802
|
+
if (line.startsWith("+")) {
|
|
719
803
|
currentNewLine++;
|
|
720
804
|
}
|
|
721
|
-
else if (line.startsWith(
|
|
805
|
+
else if (line.startsWith("-")) {
|
|
722
806
|
currentOldLine++;
|
|
723
807
|
}
|
|
724
|
-
else if (line.startsWith(
|
|
808
|
+
else if (line.startsWith(" ")) {
|
|
725
809
|
currentNewLine++;
|
|
726
810
|
currentOldLine++;
|
|
727
811
|
}
|
|
728
812
|
}
|
|
729
|
-
|
|
813
|
+
logger.debug(`Snippet not found in diff`);
|
|
730
814
|
return null;
|
|
731
815
|
}
|
|
732
816
|
/**
|
|
@@ -734,7 +818,7 @@ ${recommendation}
|
|
|
734
818
|
*/
|
|
735
819
|
escapeMarkdownCodeBlock(code, language) {
|
|
736
820
|
// If code contains triple backticks, use quadruple backticks
|
|
737
|
-
if (code.includes(
|
|
821
|
+
if (code.includes("```")) {
|
|
738
822
|
return `\`\`\`\`${language}\n${code}\n\`\`\`\``;
|
|
739
823
|
}
|
|
740
824
|
return `\`\`\`${language}\n${code}\n\`\`\``;
|
|
@@ -745,49 +829,51 @@ ${recommendation}
|
|
|
745
829
|
const fixed = JSON.parse(JSON.stringify(violation));
|
|
746
830
|
// Fix search_context arrays if they contain embedded newlines
|
|
747
831
|
if (fixed.search_context) {
|
|
748
|
-
if (fixed.search_context.before &&
|
|
832
|
+
if (fixed.search_context.before &&
|
|
833
|
+
Array.isArray(fixed.search_context.before)) {
|
|
749
834
|
fixed.search_context.before = this.splitArrayLines(fixed.search_context.before);
|
|
750
835
|
}
|
|
751
|
-
if (fixed.search_context.after &&
|
|
836
|
+
if (fixed.search_context.after &&
|
|
837
|
+
Array.isArray(fixed.search_context.after)) {
|
|
752
838
|
fixed.search_context.after = this.splitArrayLines(fixed.search_context.after);
|
|
753
839
|
}
|
|
754
840
|
}
|
|
755
841
|
// Ensure line_type is set based on code snippet prefix BEFORE cleaning
|
|
756
842
|
if (!fixed.line_type && fixed.code_snippet) {
|
|
757
|
-
if (fixed.code_snippet.startsWith(
|
|
758
|
-
fixed.line_type =
|
|
843
|
+
if (fixed.code_snippet.startsWith("+")) {
|
|
844
|
+
fixed.line_type = "ADDED";
|
|
759
845
|
}
|
|
760
|
-
else if (fixed.code_snippet.startsWith(
|
|
761
|
-
fixed.line_type =
|
|
846
|
+
else if (fixed.code_snippet.startsWith("-")) {
|
|
847
|
+
fixed.line_type = "REMOVED";
|
|
762
848
|
}
|
|
763
849
|
else {
|
|
764
|
-
fixed.line_type =
|
|
850
|
+
fixed.line_type = "CONTEXT";
|
|
765
851
|
}
|
|
766
852
|
}
|
|
767
853
|
// Clean the code_snippet field to remove diff symbols - EXACTLY like pr-police.js
|
|
768
854
|
if (fixed.code_snippet) {
|
|
769
|
-
fixed.code_snippet = fixed.code_snippet.replace(/^[+\-\s]/,
|
|
855
|
+
fixed.code_snippet = fixed.code_snippet.replace(/^[+\-\s]/, "").trim();
|
|
770
856
|
}
|
|
771
857
|
// Clean the suggestion field to remove any diff symbols
|
|
772
858
|
if (fixed.suggestion) {
|
|
773
859
|
fixed.suggestion = fixed.suggestion
|
|
774
|
-
.split(
|
|
775
|
-
.map((line) => line.replace(/^[+\-\s]/,
|
|
776
|
-
.join(
|
|
860
|
+
.split("\n")
|
|
861
|
+
.map((line) => line.replace(/^[+\-\s]/, "")) // Remove diff symbols at start of each line
|
|
862
|
+
.join("\n")
|
|
777
863
|
.trim();
|
|
778
864
|
}
|
|
779
865
|
return fixed;
|
|
780
866
|
}
|
|
781
867
|
catch (error) {
|
|
782
|
-
|
|
868
|
+
logger.debug(`❌ Error cleaning code snippet: ${error.message}`);
|
|
783
869
|
return null;
|
|
784
870
|
}
|
|
785
871
|
}
|
|
786
872
|
splitArrayLines(arr) {
|
|
787
873
|
const result = [];
|
|
788
874
|
for (const item of arr) {
|
|
789
|
-
if (typeof item ===
|
|
790
|
-
result.push(...item.split(
|
|
875
|
+
if (typeof item === "string" && item.includes("\n")) {
|
|
876
|
+
result.push(...item.split("\n").filter((line) => line.length > 0));
|
|
791
877
|
}
|
|
792
878
|
else {
|
|
793
879
|
result.push(item);
|
|
@@ -797,22 +883,24 @@ ${recommendation}
|
|
|
797
883
|
}
|
|
798
884
|
groupViolationsByCategory(violations) {
|
|
799
885
|
const grouped = {};
|
|
800
|
-
violations.forEach(v => {
|
|
801
|
-
const category = v.category ||
|
|
802
|
-
if (!grouped[category])
|
|
886
|
+
violations.forEach((v) => {
|
|
887
|
+
const category = v.category || "general";
|
|
888
|
+
if (!grouped[category]) {
|
|
803
889
|
grouped[category] = [];
|
|
890
|
+
}
|
|
804
891
|
grouped[category].push(v);
|
|
805
892
|
});
|
|
806
893
|
return grouped;
|
|
807
894
|
}
|
|
808
895
|
calculateStats(violations) {
|
|
809
896
|
return {
|
|
810
|
-
criticalCount: violations.filter(v => v.severity ===
|
|
811
|
-
majorCount: violations.filter(v => v.severity ===
|
|
812
|
-
minorCount: violations.filter(v => v.severity ===
|
|
813
|
-
suggestionCount: violations.filter(v => v.severity ===
|
|
897
|
+
criticalCount: violations.filter((v) => v.severity === "CRITICAL").length,
|
|
898
|
+
majorCount: violations.filter((v) => v.severity === "MAJOR").length,
|
|
899
|
+
minorCount: violations.filter((v) => v.severity === "MINOR").length,
|
|
900
|
+
suggestionCount: violations.filter((v) => v.severity === "SUGGESTION")
|
|
901
|
+
.length,
|
|
814
902
|
totalIssues: violations.length,
|
|
815
|
-
filesReviewed: new Set(violations.filter(v => v.file).map(v => v.file)).size || 1
|
|
903
|
+
filesReviewed: new Set(violations.filter((v) => v.file).map((v) => v.file)).size || 1,
|
|
816
904
|
};
|
|
817
905
|
}
|
|
818
906
|
generateReviewResult(violations, _duration, _context) {
|
|
@@ -826,9 +914,9 @@ ${recommendation}
|
|
|
826
914
|
criticalCount: stats.criticalCount,
|
|
827
915
|
majorCount: stats.majorCount,
|
|
828
916
|
minorCount: stats.minorCount,
|
|
829
|
-
suggestionCount: stats.suggestionCount
|
|
917
|
+
suggestionCount: stats.suggestionCount,
|
|
830
918
|
},
|
|
831
|
-
positiveObservations: [] // Could be extracted from AI response
|
|
919
|
+
positiveObservations: [], // Could be extracted from AI response
|
|
832
920
|
};
|
|
833
921
|
}
|
|
834
922
|
/**
|
|
@@ -836,7 +924,7 @@ ${recommendation}
|
|
|
836
924
|
*/
|
|
837
925
|
parseAIResponse(result) {
|
|
838
926
|
try {
|
|
839
|
-
const responseText = result.content || result.text || result.response ||
|
|
927
|
+
const responseText = result.content || result.text || result.response || "";
|
|
840
928
|
if (!responseText) {
|
|
841
929
|
return { violations: [] };
|
|
842
930
|
}
|
|
@@ -848,7 +936,7 @@ ${recommendation}
|
|
|
848
936
|
return { violations: [] };
|
|
849
937
|
}
|
|
850
938
|
catch (error) {
|
|
851
|
-
|
|
939
|
+
logger.debug(`Failed to parse AI response: ${error.message}`);
|
|
852
940
|
return { violations: [] };
|
|
853
941
|
}
|
|
854
942
|
}
|
|
@@ -856,20 +944,21 @@ ${recommendation}
|
|
|
856
944
|
* Extract line information for comment from context
|
|
857
945
|
*/
|
|
858
946
|
extractLineInfoForComment(violation, context) {
|
|
859
|
-
if (!violation.file || !violation.code_snippet)
|
|
947
|
+
if (!violation.file || !violation.code_snippet) {
|
|
860
948
|
return null;
|
|
949
|
+
}
|
|
861
950
|
try {
|
|
862
951
|
// Get the diff for this specific file
|
|
863
952
|
let fileDiff;
|
|
864
|
-
if (context.diffStrategy.strategy ===
|
|
953
|
+
if (context.diffStrategy.strategy === "whole" && context.prDiff) {
|
|
865
954
|
// Extract file diff from whole diff
|
|
866
|
-
const diffLines = context.prDiff.diff.split(
|
|
955
|
+
const diffLines = context.prDiff.diff.split("\n");
|
|
867
956
|
let fileStartIndex = -1;
|
|
868
957
|
// Create all possible path variations for matching
|
|
869
958
|
const filePathVariations = this.generatePathVariations(violation.file);
|
|
870
959
|
for (let i = 0; i < diffLines.length; i++) {
|
|
871
960
|
const line = diffLines[i];
|
|
872
|
-
if (line.startsWith(
|
|
961
|
+
if (line.startsWith("diff --git")) {
|
|
873
962
|
// Check if any variation matches
|
|
874
963
|
for (const pathVariation of filePathVariations) {
|
|
875
964
|
if (line.includes(pathVariation)) {
|
|
@@ -877,22 +966,26 @@ ${recommendation}
|
|
|
877
966
|
break;
|
|
878
967
|
}
|
|
879
968
|
}
|
|
880
|
-
if (fileStartIndex >= 0)
|
|
969
|
+
if (fileStartIndex >= 0) {
|
|
881
970
|
break;
|
|
971
|
+
}
|
|
882
972
|
}
|
|
883
973
|
}
|
|
884
974
|
if (fileStartIndex >= 0) {
|
|
885
|
-
const nextFileIndex = diffLines.findIndex((line, idx) => idx > fileStartIndex && line.startsWith(
|
|
886
|
-
fileDiff = diffLines
|
|
975
|
+
const nextFileIndex = diffLines.findIndex((line, idx) => idx > fileStartIndex && line.startsWith("diff --git"));
|
|
976
|
+
fileDiff = diffLines
|
|
977
|
+
.slice(fileStartIndex, nextFileIndex > 0 ? nextFileIndex : diffLines.length)
|
|
978
|
+
.join("\n");
|
|
887
979
|
}
|
|
888
980
|
}
|
|
889
|
-
else if (context.diffStrategy.strategy ===
|
|
981
|
+
else if (context.diffStrategy.strategy === "file-by-file" &&
|
|
982
|
+
context.fileDiffs) {
|
|
890
983
|
// Try all possible path variations
|
|
891
984
|
const pathVariations = this.generatePathVariations(violation.file);
|
|
892
985
|
for (const path of pathVariations) {
|
|
893
986
|
fileDiff = context.fileDiffs.get(path);
|
|
894
987
|
if (fileDiff) {
|
|
895
|
-
|
|
988
|
+
logger.debug(`Found diff for ${violation.file} using variation: ${path}`);
|
|
896
989
|
break;
|
|
897
990
|
}
|
|
898
991
|
}
|
|
@@ -901,7 +994,7 @@ ${recommendation}
|
|
|
901
994
|
for (const [key, value] of context.fileDiffs.entries()) {
|
|
902
995
|
if (key.endsWith(violation.file) || violation.file.endsWith(key)) {
|
|
903
996
|
fileDiff = value;
|
|
904
|
-
|
|
997
|
+
logger.debug(`Found diff for ${violation.file} using partial match: ${key}`);
|
|
905
998
|
break;
|
|
906
999
|
}
|
|
907
1000
|
}
|
|
@@ -910,16 +1003,16 @@ ${recommendation}
|
|
|
910
1003
|
if (fileDiff) {
|
|
911
1004
|
const lineInfo = this.extractLineNumberFromDiff(fileDiff, violation.code_snippet);
|
|
912
1005
|
if (lineInfo) {
|
|
913
|
-
|
|
1006
|
+
logger.debug(`Extracted line info for ${violation.file}: line ${lineInfo.lineNumber}, type ${lineInfo.lineType}`);
|
|
914
1007
|
}
|
|
915
1008
|
return lineInfo;
|
|
916
1009
|
}
|
|
917
1010
|
else {
|
|
918
|
-
|
|
1011
|
+
logger.debug(`No diff found for file: ${violation.file}`);
|
|
919
1012
|
}
|
|
920
1013
|
}
|
|
921
1014
|
catch (error) {
|
|
922
|
-
|
|
1015
|
+
logger.debug(`Error extracting line info: ${error.message}`);
|
|
923
1016
|
}
|
|
924
1017
|
return null;
|
|
925
1018
|
}
|
|
@@ -933,29 +1026,29 @@ ${recommendation}
|
|
|
933
1026
|
// Add with a/ and b/ prefixes
|
|
934
1027
|
variations.add(`a/${filePath}`);
|
|
935
1028
|
variations.add(`b/${filePath}`);
|
|
936
|
-
// Handle nested paths
|
|
937
|
-
if (filePath.includes(
|
|
938
|
-
const parts = filePath.split(
|
|
1029
|
+
// Handle nested paths
|
|
1030
|
+
if (filePath.includes("/")) {
|
|
1031
|
+
const parts = filePath.split("/");
|
|
939
1032
|
// Try removing first directory
|
|
940
1033
|
if (parts.length > 1) {
|
|
941
|
-
variations.add(parts.slice(1).join(
|
|
1034
|
+
variations.add(parts.slice(1).join("/"));
|
|
942
1035
|
}
|
|
943
1036
|
// Try removing first two directories
|
|
944
1037
|
if (parts.length > 2) {
|
|
945
|
-
variations.add(parts.slice(2).join(
|
|
1038
|
+
variations.add(parts.slice(2).join("/"));
|
|
946
1039
|
}
|
|
947
1040
|
// Try with just the filename
|
|
948
1041
|
variations.add(parts[parts.length - 1]);
|
|
949
1042
|
}
|
|
950
1043
|
// Remove app/ prefix variations
|
|
951
|
-
if (filePath.startsWith(
|
|
1044
|
+
if (filePath.startsWith("app/")) {
|
|
952
1045
|
const withoutApp = filePath.substring(4);
|
|
953
1046
|
variations.add(withoutApp);
|
|
954
1047
|
variations.add(`a/${withoutApp}`);
|
|
955
1048
|
variations.add(`b/${withoutApp}`);
|
|
956
1049
|
}
|
|
957
1050
|
// Add app/ prefix variations
|
|
958
|
-
if (!filePath.startsWith(
|
|
1051
|
+
if (!filePath.startsWith("app/")) {
|
|
959
1052
|
variations.add(`app/${filePath}`);
|
|
960
1053
|
variations.add(`a/app/${filePath}`);
|
|
961
1054
|
variations.add(`b/app/${filePath}`);
|
|
@@ -963,8 +1056,7 @@ ${recommendation}
|
|
|
963
1056
|
return Array.from(variations);
|
|
964
1057
|
}
|
|
965
1058
|
}
|
|
966
|
-
|
|
967
|
-
function createCodeReviewer(bitbucketProvider, aiConfig, reviewConfig) {
|
|
1059
|
+
export function createCodeReviewer(bitbucketProvider, aiConfig, reviewConfig) {
|
|
968
1060
|
return new CodeReviewer(bitbucketProvider, aiConfig, reviewConfig);
|
|
969
1061
|
}
|
|
970
1062
|
//# sourceMappingURL=CodeReviewer.js.map
|