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