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