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