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