"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Commenter = exports.getReviewDuration = exports.startReviewTimer = exports.sanitizeText = exports.setCommenterContext = exports.repo = exports.context = void 0; const info = console.log; const warning = console.warn; const getInput = (name) => { try { return (process.env[name.toUpperCase()] || ''); } catch { return ''; } }; const octokit_1 = require("./octokit"); const text_normalizer_1 = require("./text-normalizer"); const context_1 = require("./context"); const templates_1 = require("./templates"); exports.context = new Proxy({}, { get(target, prop) { return context_1.als.getStore()?.probotContext?.[prop]; } }); exports.repo = new Proxy({}, { get(target, prop) { return context_1.als.getStore()?.repo?.[prop]; } }); const setCommenterContext = (_newContext) => { }; exports.setCommenterContext = setCommenterContext; const sanitizeText = (str) => { if (!str) return ''; return str .replace(/[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F-\x9F]/g, '') .replace(/\uFFFD/g, '⚠️'); }; exports.sanitizeText = sanitizeText; let reviewStartTime = null; const startReviewTimer = () => { reviewStartTime = Date.now(); }; exports.startReviewTimer = startReviewTimer; const getReviewDuration = () => { if (!reviewStartTime) return '0s'; const duration = Math.round((Date.now() - reviewStartTime) / 1000); return duration < 60 ? `${duration}s` : `${Math.floor(duration / 60)}m ${duration % 60}s`; }; exports.getReviewDuration = getReviewDuration; class Commenter { // Track commits we've reviewed to prevent duplicate comments reviewedCommits = new Set(); strictMode = false; /** * Check if we've already reviewed this commit to prevent duplicates */ async hasReviewedCommit(pullNumber, commitId) { if (this.reviewedCommits.has(commitId)) { return true; } // Check GitHub for existing bot reviews on this commit try { const reviews = await octokit_1.octokit.pulls.listReviews({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber }); const botReviewOnCommit = reviews.data.find((r) => r.user?.login?.includes('bot') && r.body?.includes(templates_1.COMMENT_TAG) && r.body?.includes(commitId.substring(0, 7))); if (botReviewOnCommit) { this.reviewedCommits.add(commitId); return true; } } catch (e) { warning(`Failed to check existing reviews: ${e}`); } return false; } /** * @param mode Can be "create", "replace". Default is "replace". */ async comment(message, tag, mode) { let target; if (exports.context.payload.pull_request != null) { target = exports.context.payload.pull_request.number; } else if (exports.context.payload.issue != null) { target = exports.context.payload.issue.number; } else { warning('Skipped: context.payload.pull_request and context.payload.issue are both null'); return; } if (!tag) { tag = templates_1.COMMENT_TAG; } const hiddenTag = tag.startsWith('`; const body = `${templates_1.COMMENT_GREETING} ${message} ${templates_1.PRIX_FOOTER} ${hiddenTag}`; if (mode === 'create') { await this.create(body, target); } else if (mode === 'replace') { await this.replace(body, tag, target); } else { warning(`Unknown mode: ${mode}, use "replace" instead`); await this.replace(body, tag, target); } } getContentWithinTags(content, startTag, endTag) { const start = content.indexOf(startTag); const end = content.indexOf(endTag); if (start >= 0 && end >= 0) { return content.slice(start + startTag.length, end); } return ''; } removeContentWithinTags(content, startTag, endTag) { const start = content.indexOf(startTag); const end = content.lastIndexOf(endTag); if (start >= 0 && end >= 0) { return content.slice(0, start) + content.slice(end + endTag.length); } return content; } getRawSummary(summary) { return this.getContentWithinTags(summary, templates_1.RAW_SUMMARY_START_TAG, templates_1.RAW_SUMMARY_END_TAG); } getShortSummary(summary) { return this.getContentWithinTags(summary, templates_1.SHORT_SUMMARY_START_TAG, templates_1.SHORT_SUMMARY_END_TAG); } getDescription(description) { return this.removeContentWithinTags(description, templates_1.DESCRIPTION_START_TAG, templates_1.DESCRIPTION_END_TAG); } getReleaseNotes(description) { const releaseNotes = this.getContentWithinTags(description, templates_1.DESCRIPTION_START_TAG, templates_1.DESCRIPTION_END_TAG); return releaseNotes.replace(/(^|\n)> .*/g, ''); } async updateDescription(pullNumber, message) { // add this response to the description field of the PR as release notes by looking // for the tag (marker) try { // get latest description from PR const pr = await octokit_1.octokit.pulls.get({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber }); let body = ''; if (pr.data.body) { body = pr.data.body; } const description = this.getDescription(body); const messageClean = this.removeContentWithinTags(message, templates_1.DESCRIPTION_START_TAG, templates_1.DESCRIPTION_END_TAG); const newDescription = `${description}\n${templates_1.DESCRIPTION_START_TAG}\n${messageClean}\n${templates_1.DESCRIPTION_END_TAG}`; await octokit_1.octokit.pulls.update({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, body: newDescription }); } catch (e) { warning(`Failed to get PR: ${e}, skipping adding release notes to description.`); } } reviewCommentsBuffer = []; // Track user action signals for analytics promptShownCount = 0; fixGeneratedCount = 0; /** * Get analytics for user actions (for future use) */ getAnalytics() { return { promptsShown: this.promptShownCount, fixesGenerated: this.fixGeneratedCount }; } async bufferReviewComment(path, startLine, endLine, message, astValid, remedy, validationConfidence, originalCode, aiImpact, parsedIssue) { // QUALITY FILTER: Skip low-confidence comments if (validationConfidence === 'failed' && !remedy) { info(`Skipping low-confidence comment for ${path}:${startLine}`); return; } // ARCHITECTURAL ENFORCEMENT: Only allow parsed issues from structured JSON if (!parsedIssue) { warning(`Commenter: Skipping unparsed comment for ${path}:${startLine} - JSON parsing failed or was bypassed`); return; } let level = parsedIssue.severity || 'minor'; const type = parsedIssue.type || 'issue'; const description = parsedIssue.description || ''; let issueTitle = parsedIssue.title || ''; // Refined Severity Adjustment: Ensure only major security issues or definite syntax/runtime blockers are Critical. const descLower = description.toLowerCase(); const titleLower = issueTitle.toLowerCase(); // Downgrade minor/info level issues that are overglorified if (level === 'critical' || level === 'major') { const isLoggingOrStyle = /log|logging|console|style|formatting|naming|comment|assertion|as keyword/i.test(descLower) || /log|logging|console|style|formatting|naming|comment|assertion|as keyword/i.test(titleLower); const isMinorBestPractice = /best practice|should be|consider|good practice|readability|redundant/i.test(descLower); if (isLoggingOrStyle || isMinorBestPractice) { level = 'minor'; } } // Force critical ONLY for true production blockers: definite syntax errors, severe crashes, or major security flaws const isMajorSecurity = /bypass|vulnerability|sql injection|xss|csrf|auth|unauthorized|logic inversion|privilege/i.test(descLower) || /bypass|vulnerability|sql injection|xss|csrf|auth|unauthorized|logic inversion|privilege/i.test(titleLower); const isRuntimeBlocker = /\b(?:syntax error|compilation error|import error|cannot find|nameerror|typeerror|referenceerror)\b/i.test(descLower) || /\b(?:memory leak|unclosed websocket|process crash|out of memory)\b/i.test(descLower); if (level === 'critical') { if (!isMajorSecurity && !isRuntimeBlocker) { level = 'major'; // Downgrade to major if it doesn't meet critical criteria } } let severityEmoji = '⚠️'; let severityLabel = 'Minor'; if (level === 'critical') { severityEmoji = 'πŸ”΄'; severityLabel = 'Critical Runtime Issue'; } else if (level === 'major') { severityEmoji = '🟠'; severityLabel = 'Major'; } else if (level === 'minor') { severityEmoji = '🟑'; severityLabel = 'Minor'; } else if (level === 'info') { severityEmoji = 'πŸ”΅'; severityLabel = 'Info'; } // Extract function name and variable names for context const functionName = this.extractFunctionName(description, remedy); const context = this.extractCodeContext(description, remedy, type); // Clean up description const cleanDescription = description; // Generate issue title - Capitalize it issueTitle = issueTitle.trim(); issueTitle = issueTitle.charAt(0).toUpperCase() + issueTitle.slice(1); // Generate short description - Basic cleanup for AI description let shortDesc = description.trim(); shortDesc = shortDesc.charAt(0).toUpperCase() + shortDesc.slice(1); if (!shortDesc.endsWith('.')) shortDesc += '.'; // Format confidence (High/Medium/Low) const confidence = this.mapConfidence(level, astValid, validationConfidence); // Build validation section const validationSection = this.formatValidationCompact(astValid, remedy, validationConfidence); // FIX 1: Get severity color and emoji const severityColor = (lvl) => { if (lvl === 'critical') return 'πŸ”΄ Critical'; if (lvl === 'major') return '🟠 Major'; if (lvl === 'minor') return '🟑 Minor'; return '🟒 Info'; }; const issueTypeLabel = this.getIssueTypeLabel(type, level); // PART 1: Sanitize all text content const sanitizedTitle = (0, exports.sanitizeText)(issueTitle); const sanitizedDesc = (0, exports.sanitizeText)(shortDesc); const sanitizedConfidence = (0, exports.sanitizeText)(confidence); // PART 2 & 3: Reordered structure - Always show Suggested Fix if remedy exists // PART 3: Only show Why this matters if impact is specific (not generic) // PRIORITIZE: AI-provided impact over hardcoded patterns const impactText = aiImpact ? aiImpact.startsWith('-') ? aiImpact : `- ${aiImpact}` : this.generateCleanImpact(type, level, { ...context, rawDescription: description }); const hasSpecificImpact = impactText && !impactText.includes('Code quality: can be improved'); const formattedMessage = `${templates_1.COMMENT_GREETING} ${severityEmoji} ${issueTypeLabel} | ${severityColor(level)} *${sanitizedTitle}* ${sanitizedDesc} **Confidence:** ${sanitizedConfidence} --- ${hasSpecificImpact ? ` ### πŸ’‘ Why this matters ${impactText} --- ` : ''} ${remedy && originalCode ? `
πŸ›  Suggested Fix \`\`\`diff ${this.generateMinimalDiff(originalCode, remedy)} \`\`\`
--- ` : ''} ${this.generateEliteAIPrompt(path, functionName, context, type, remedy, description) ? ` ### πŸ€– AI Assistant ${this.generateEliteAIPrompt(path, functionName, context, type, remedy, description)} --- ` : ''}${validationSection ? `
βš™οΈ Validation ${validationSection}
` : ''}${templates_1.PRIX_FOOTER} ${templates_1.COMMENT_TAG}`; // SEVERITY GATE: Skip low-signal issues to prevent "eslint bot syndrome" // Without strict mode: skip ALL noise-type issues (style/naming) regardless of severity // With strict mode: skip info-level issues and minor-level noise issues const noiseTypes = [ 'style', 'best-practice', 'refactor', 'maintainability', 'usability', 'accessibility' ]; const isNoiseType = noiseTypes.includes(type); if (!this.strictMode && isNoiseType) { info(`Strict mode off: skipping noise issue [${level}] ${type} - ${issueTitle}`); return; } if (level === 'info' || (level === 'minor' && isNoiseType)) { info(`Skipping low-signal issue: [${level}] ${type} - ${issueTitle}`); return; } // Map severity to priority number for sorting const priorityMap = { critical: 1, major: 2, minor: 3, info: 4 }; this.reviewCommentsBuffer.push({ path, startLine, endLine, rawDescription: description, cleanTitle: issueTitle, shortDescription: shortDesc, severity: level, type, confidence, message: formattedMessage, priority: priorityMap[level] || 3 }); } /** * Generate WOW factor insight for meaningful issues */ generateInsight(type, level, context, description) { // Only add insight for meaningful issues (critical/major) if (level !== 'critical' && level !== 'major') { return ''; } const varRef = context.variableName ? `\`${context.variableName}\`` : 'this value'; // Pattern-specific insights that create "wow" moments const insights = { mutation: `⚠️ This can silently corrupt shared state if ${varRef} is reused elsewhere. `, async: `⚠️ Race conditions here are hard to reproduce and debug in production. `, 'null-safety': `⚠️ This will crash at runtime when ${varRef} is undefined. `, security: `⚠️ Security issues like this are often exploited in supply chain attacks. `, performance: `⚠️ Performance bottlenecks here compound under load and affect all users. ` }; const pattern = context.pattern || type.toLowerCase(); return insights[pattern] || ''; } /** * Extract function name from description or remedy */ extractFunctionName(description, remedy) { // Try to find function name in description const funcMatch = description.match(/function\s+(\w+)|(\w+)\s*\(|`(\w+)`/); if (funcMatch) { return funcMatch[1] || funcMatch[2] || funcMatch[3]; } // Try remedy if (remedy) { const remedyMatch = remedy.match(/function\s+(\w+)|(\w+)\s*\(/); if (remedyMatch) { return remedyMatch[1] || remedyMatch[2]; } } return ''; } /** * Generate specific, actionable explanation */ generateSpecificExplanation(description, type, path, functionName) { // Clean up weak phrases let cleaned = (0, text_normalizer_1.normalizeReviewLanguage)(description); // Make specific enhancements based on patterns const contextPrefix = functionName ? `In \`${functionName}\`, ` : ''; if (/ded\b/i.test(cleaned)) { cleaned = `${contextPrefix}the token 'ded' is invalid and causes a syntax error. This is likely a typo.`; } else if (/mutat/i.test(cleaned) && /object/i.test(cleaned)) { cleaned = `${contextPrefix}mutating the original \`user\` object will affect all references to it, potentially causing bugs in other parts of the application.`; } else if (/mutat/i.test(cleaned)) { cleaned = `${contextPrefix}this code mutates the original value directly, which can lead to unintended side effects.`; } else if (/syntax error/i.test(cleaned)) { cleaned = `${contextPrefix}there's a syntax error that will prevent this code from executing.`; } else if (/undefined/i.test(cleaned) && /variable/i.test(cleaned)) { cleaned = `${contextPrefix}this references an undefined variable that will cause a runtime error.`; } // Ensure first letter is capitalized cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); return cleaned; } /** * Generate contextual "Why this matters" section based on actual issue details */ generateWhyItMatters(type, level, context, rawDescription) { const varRef = context.variableName ? `\`${context.variableName}\`` : 'this value'; const funcRef = context.functionName ? `\`${context.functionName}\`` : 'this code'; // Use raw description to extract specific context const desc = rawDescription || ''; const undefinedMatch = desc.match(/undefined\s+(?:function|variable|reference)?\s*['`]?(\w+)['`]?/i); const syntaxMatch = desc.match(/syntax\s+error|invalid\s+token/i); // Contextual explanations based on actual issue if (undefinedMatch) { return `- \`${undefinedMatch[1]}\` is referenced without definition, which raises NameError during execution\n- This prevents the code from running and blocks the execution flow`; } else if (syntaxMatch) { return `- Invalid syntax prevents the code from parsing and executing\n- This is a hard blocker that must be fixed before the code can run`; } else if (context.pattern === 'mutation' || /mutat/i.test(desc)) { return `- Mutating ${varRef} modifies the original object and can create shared-state bugs\n- Side effects propagate silently, making debugging difficult`; } else if (context.pattern === 'null-safety' || /null|undefined/i.test(desc)) { return `- Accessing ${varRef} without validation will throw TypeError at runtime\n- This causes crashes and breaks the application execution`; } else if (context.pattern === 'async' || /async|await/i.test(desc)) { return `- Incorrect async handling in ${funcRef} creates race conditions\n- Errors may be swallowed without proper handling, leading to silent failures`; } else if (context.pattern === 'security' || /security/i.test(desc)) { return `- Security vulnerabilities like this can be exploited in production\n- May expose sensitive data or allow unauthorized access`; } else if (context.pattern === 'performance' || /performance|slow/i.test(desc)) { return `- Performance issues compound under load and affect all users\n- Resource waste increases operational costs and degrades user experience`; } // Fallback with context only when absolutely necessary return `- This issue in ${funcRef} affects code reliability\n- Should be addressed to prevent unexpected behavior`; } /** * Format fix with proper diff view */ formatDiffView(remedy, type) { // Clean the remedy const cleanFix = remedy .replace(/\\ No newline at end of file/g, '') .replace(/\bded\b/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); // Convert to proper diff format if not already const lines = cleanFix.split('\n'); const diffLines = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('//')) continue; // If line starts with - or +, keep it if (trimmed.startsWith('-') || trimmed.startsWith('+')) { diffLines.push(trimmed); } else if (trimmed.startsWith('@@')) { // Keep diff markers diffLines.push(trimmed); } else if (!trimmed.startsWith('```')) { // Context lines (unchanged) diffLines.push(' ' + trimmed); } } // Build minimal diff let diffOutput = diffLines.join('\n'); // If no diff markers present, format as simple +/- if (!diffOutput.includes('-') && !diffOutput.includes('+')) { // Assume entire thing is the new code diffOutput = cleanFix .split('\n') .filter(l => l.trim()) .map(l => `+ ${l}`) .join('\n'); } if (!diffOutput.trim()) { return ''; } return `
πŸ›  View diff \`\`\`diff ${diffOutput} \`\`\`
`; } /** * Generate AI prompt for manual fix */ generateAIPrompt(path, functionName, type, description, remedy) { const fileName = path.split('/').pop() || path; const funcContext = functionName ? `the \`${functionName}\` function` : 'this code'; // Extract key issue let issue = type.toLowerCase(); if (/mutat/i.test(description)) issue = 'object mutation'; else if (/syntax/i.test(description)) issue = 'syntax error'; else if (/undefined/i.test(description)) issue = 'undefined reference'; else if (/security/i.test(description)) issue = 'security vulnerability'; // Build concise prompt let prompt = `In file \`${fileName}\`, update ${funcContext} to fix the ${issue}. `; if (/mutat/i.test(description)) { prompt += `Avoid mutating the original object. Create a shallow copy using object/array spreading before making changes.`; } else if (/syntax/i.test(description)) { prompt += `Fix the syntax error by checking for missing brackets, quotes, or semicolons.`; } else if (/undefined/i.test(description)) { prompt += `Initialize the variable before use or add proper null checks.`; } else { prompt += `Ensure the fix handles edge cases and maintains the original functionality.`; } return `
οΏ½ Copy prompt \`\`\` ${prompt} \`\`\`
`; } /** * Format status section with validation info */ formatStatusSection(astValid, remedy) { const status = []; if (astValid === false) { status.push('❌ AST validation failed'); status.push('πŸ›  Manual review required'); } else if (astValid === true) { status.push('βœ… AST validation passed'); if (remedy) { status.push('πŸ€– Auto-fix available'); } } else { status.push('⚠️ Validation status unknown'); } if (!remedy) { status.push('πŸ“ No automated fix available'); } return `### ⚠️ Status ${status.map(s => `- ${s}`).join('\n')}`; } /** * ELITE: Extract code context (variable names, patterns) for precise explanations */ extractCodeContext(description, remedy, type) { const context = {}; // Extract variable names (common patterns) const varPatterns = [ /variable\s+`?(\w+)`?/i, /`?(\w+)`?\s+is\s+undefined/i, /mutat(?:ing|e)\s+(?:the\s+)?`?(\w+)`?/i, /`?(\w+)`?\s+object/i, /original\s+`?(\w+)`?/i ]; for (const pattern of varPatterns) { const match = description.match(pattern); if (match) { context.variableName = match[1]; break; } } // If not found in description, try remedy if (!context.variableName && remedy) { const remedyVars = remedy.match(/(?:const|let|var)\s+(\w+)\s*=/) || remedy.match(/(\w+)\s*[=:]/); if (remedyVars) context.variableName = remedyVars[1]; } // Extract pattern type if (/mutat/i.test(description)) context.pattern = 'mutation'; else if (/async|await|promise/i.test(description)) context.pattern = 'async'; else if (/null|undefined/i.test(description)) context.pattern = 'null-safety'; else if (/memory|leak/i.test(description)) context.pattern = 'memory'; else if (/performance|slow|inefficient/i.test(description)) context.pattern = 'performance'; return context; } /** * ELITE: Generate context-aware, specific explanation without generic language */ generateEliteExplanation(description, type, path, functionName, context) { // Clean up weak phrases let cleaned = (0, text_normalizer_1.normalizeReviewLanguage)(description); const fileName = path.split('/').pop() || path; const funcRef = functionName ? `\`${functionName}\`` : 'this code'; const varRef = context.variableName ? `\`${context.variableName}\`` : 'the value'; // Generate specific explanations based on pattern if (context.pattern === 'mutation' || /mutat/i.test(cleaned)) { cleaned = `In ${funcRef}, ${varRef} is being mutated directly. If this object is reused elsewhere (e.g., in state, cache, or passed to other functions), this change will propagate unexpectedly and cause side effects that are difficult to trace.`; } else if (/syntax error/i.test(cleaned) || /ded\b/i.test(cleaned)) { cleaned = `There's a syntax error in ${funcRef}. The token \`ded\` is invalid JavaScript/TypeScriptβ€”this appears to be a typo or incomplete edit that will prevent the code from executing.`; } else if (context.pattern === 'null-safety' || /undefined/i.test(cleaned)) { cleaned = `In ${funcRef}, ${varRef} may be \`undefined\` at runtime. Accessing properties or calling methods on it will throw a TypeError. This needs a null check before use.`; } else if (context.pattern === 'async' || /async|await/i.test(cleaned)) { cleaned = `The async handling in ${funcRef} is incorrect. This will cause promises to resolve unexpectedly or errors to be swallowed, leading to race conditions or silent failures.`; } else if (cleaned.length < 40 || cleaned.includes('issue') || cleaned.includes('problem')) { // Too generic - regenerate with context cleaned = `In ${fileName}, ${funcRef} has an issue: ${cleaned.replace(/issue|problem/gi, '').trim() || 'code quality concern'}`; } // Capitalize first letter cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); return cleaned; } /** * Generate specific impact statement (clean, no emojis) */ generateEliteImpact(type, level, context) { const impacts = { mutation: { critical: `**Data corruption:** Mutated objects in shared state will corrupt application data.\n\n**Silent bugs:** Side effects may not surface immediately, making debugging difficult.`, major: `**Unexpected state changes:** Other parts of the app using this object will see unplanned modifications.\n\n**Testing difficulties:** Mutations make unit tests flaky and hard to write.`, default: `**Side effects:** Changes to ${context.variableName || 'the object'} will propagate to all references.` }, syntax: { critical: `**Runtime crash:** This code will throw a SyntaxError and prevent execution.\n\n**Build failure:** CI/CD pipelines will reject this commit.`, default: `**Execution blocked:** The code cannot run until this is fixed.` }, security: { critical: `**Vulnerability:** This could be exploited to access unauthorized data.\n\n**Compliance risk:** Security issues may violate SOC2/ISO requirements.`, default: `**Security exposure:** This pattern is known to create vulnerabilities.` }, 'null-safety': { major: `**Runtime crash:** Accessing properties on undefined will throw TypeError.\n\n**User-facing errors:** This will likely trigger error boundaries or crash the UI.`, default: `**Defensive coding needed:** Missing null check before property access.` } }; const patternImpacts = impacts[context.pattern || type.toLowerCase()]; if (patternImpacts) { return patternImpacts[level] || patternImpacts['default']; } // Default impact based on severity if (level === 'critical') { return `**System failure:** This will cause crashes or data loss.\n\n**Production risk:** This should be fixed before deployment.`; } else if (level === 'major') { return `**Bug introduction:** This pattern is known to cause runtime errors.\n\n**Maintainability:** This makes the code harder to understand and modify.`; } return `**Code quality:** This can be improved for better maintainability.\n\n**Best practice:** Following patterns prevents similar issues.`; } /** * ELITE: Format surgical diff (only changed lines, minimal context) */ formatSurgicalDiff(remedy, type, startLine, endLine) { // Clean the remedy const cleanFix = remedy .replace(/\\ No newline at end of file/g, '') .replace(/\bded\b/g, '') .replace(/\n{3,}/g, '\n\n') .trim(); // Parse lines and extract diff format const lines = cleanFix.split('\n'); const diffLines = []; for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; if (trimmed.startsWith('//')) continue; // Skip comment-only lines // Keep diff markers if (trimmed.startsWith('-') || trimmed.startsWith('+')) { diffLines.push(trimmed); } else if (trimmed.startsWith('@@')) { diffLines.push(trimmed); } else if (!trimmed.startsWith('```')) { // Context lines with space prefix diffLines.push(' ' + trimmed); } } // If no diff markers, create them if (!diffLines.some(l => l.startsWith('-') || l.startsWith('+'))) { // Split into removed and added const originalLines = diffLines .filter(l => l.includes('= ')) .map(l => '- ' + l.trim().replace(/^\s*/, '')); const newLines = diffLines.map(l => '+ ' + l.trim().replace(/^\s*/, '')); if (originalLines.length > 0 && newLines.length > 0) { diffLines.length = 0; diffLines.push(...originalLines, ...newLines); } else if (newLines.length > 0) { diffLines.length = 0; diffLines.push(...newLines); } } // Limit to 10 lines max for readability const limitedDiff = diffLines.slice(0, 10); if (diffLines.length > 10) { limitedDiff.push('// ... (additional changes)'); } if (limitedDiff.length === 0) { return ''; } return `
πŸ›  View diff \`\`\`diff ${limitedDiff.join('\n')} \`\`\`
`; } /** * Status section with validation confidence (neutral/informative tone) */ formatEliteStatusSection(astValid, remedy, validationConfidence) { const status = []; if (astValid === true) { if (validationConfidence === 'strong') { status.push('**Status:** Fully verified (full-file AST check)'); if (remedy) status.push('**Action:** Auto-fix ready β€” safe to apply'); } else if (validationConfidence === 'partial') { status.push('**Status:** Partially verified (limited context)'); if (remedy) status.push('**Action:** Review suggested β€” verify in your codebase'); } else { status.push('**Status:** Basic verification passed'); if (remedy) status.push('**Action:** Review suggested before applying'); } } else if (astValid === false) { status.push('**Status:** Requires manual review'); if (remedy) status.push('**Action:** Auto-fix not applied (could not be fully verified)'); } else { status.push('**Status:** Suggestion provided'); if (remedy) status.push('**Action:** Review recommended before applying'); } if (!remedy) { status.push('**Note:** No automated fix available for this issue'); } return status.join('\n\n'); } /** * ELITE: Generate context-aware AI prompt from actual issue details */ generateEliteAIPrompt(path, functionName, context, type, remedy, rawDescription) { const fileName = path.split('/').pop() || path; const funcRef = functionName ? `\`${functionName}\`` : 'this code'; const varRef = context.variableName ? `\`${context.variableName}\`` : null; // Extract specific issue details from raw description const specificIssue = this.extractSpecificIssue(rawDescription || '', context); // Build context-aware prompt based on actual issue let prompt = ''; if (specificIssue.undefinedReference) { const ref = specificIssue.undefinedReference; prompt = `In \`${fileName}\`, remove or correctly define the undefined \`${ref}\` reference. Requirements: - Ensure the file executes without runtime errors - Remove disconnected statements - Preserve existing arithmetic behavior - Keep main execution flow intact`; } else if (specificIssue.syntaxError) { prompt = `In \`${fileName}\`, fix the syntax error in ${funcRef}. Requirements: - Correct invalid tokens or incomplete statements - Verify all brackets, quotes, and semicolons are balanced - Ensure the code can parse and execute - Preserve the intended logic`; } else if (context.pattern === 'mutation' && varRef) { prompt = `In \`${fileName}\`, avoid mutating the original ${varRef} inside ${funcRef}. Requirements: - Return a new object instead of mutating input - Preserve nested settings structure - Maintain existing API behavior - Avoid shared reference mutation`; } else if (context.pattern === 'null-safety' && varRef) { prompt = `In \`${fileName}\`, add null safety checks for ${varRef} in ${funcRef}. Requirements: - Check if ${varRef} is defined before use - Use optional chaining (\`?.\`) where appropriate - Provide fallback values for undefined cases - Prevent runtime TypeError`; } else if (context.pattern === 'async') { prompt = `In \`${fileName}\`, fix async/await handling in ${funcRef}. Requirements: - Ensure all async operations are properly awaited - Add try/catch for error handling - Prevent race conditions - Handle promise rejections`; } else if (specificIssue.runtimeIssue) { prompt = `In \`${fileName}\`, fix the runtime issue in ${funcRef}: ${specificIssue.runtimeIssue}. Requirements: - Address the specific error condition - Ensure error handling is in place - Preserve the intended functionality - Test edge cases`; } else if (specificIssue.disconnectedLogic) { prompt = `In \`${fileName}\`, remove or connect the disconnected logic in ${funcRef}. Requirements: - Remove statements that don't contribute to the function - Ensure all code has a clear purpose - Maintain the main execution flow - Preserve intended behavior`; } else { // Fallback with raw description for specificity const issueDetail = rawDescription ? `: ${rawDescription}` : ''; prompt = `In \`${fileName}\`, fix the ${type} in ${funcRef}${issueDetail}. Requirements: - Address the specific issue described above - Preserve existing functionality - Ensure no regressions`; } return `
πŸ€– Prompt for AI \`\`\`txt ${prompt} \`\`\`
`; } /** * Extract specific issue details from raw description */ extractSpecificIssue(description, context) { const result = {}; // Detect undefined references (e.g., "undefined function 'ded'") const undefinedMatch = description.match(/undefined\s+(?:function|variable|reference|identifier)?\s*['`]?(\w+)['`]?/i); if (undefinedMatch) { result.undefinedReference = undefinedMatch[1]; } // Detect syntax errors if (/syntax\s+error|invalid\s+token|unexpected\s+token|parse\s+error/i.test(description)) { result.syntaxError = true; } // Detect disconnected logic if (/disconnected|unused|dead\s+code|no\s+effect|statement\s+has\s+no\s+effect/i.test(description)) { result.disconnectedLogic = true; } // Detect runtime issues if (/runtime\s+error|will\s+(?:throw|cause|raise)|crash|exception/i.test(description)) { result.runtimeIssue = description .replace(/runtime\s+error:?\s*/i, '') .trim(); } return result; } /** * Generate italic issue title (specific, preserve raw AI specificity) */ generateIssueTitle(description, type, functionName, path, confidence) { const funcRef = functionName ? `\`${functionName}\`` : 'this code'; const fileName = path.split('/').pop() || path; // Extract specific entities from description (variable names, function names) const undefinedMatch = description.match(/undefined\s+(?:function|variable|reference)?\s*['`]?(\w+)['`]?/i); const syntaxMatch = description.match(/syntax\s+error|invalid\s+token\s*['`]?(\w+)['`]?/i); const mutationMatch = description.match(/mutat(?:ing|e)?\s+(?:the\s+)?['`]?(\w+)['`]?/i); // Preserve raw specificity when confidence is high and description contains concrete entities const hasSpecificEntities = undefinedMatch || syntaxMatch || mutationMatch || /\b\w+\b.*\b\w+\b/.test(description); const isHighConfidence = confidence === 'High' || confidence === 'Medium'; // If high confidence and has specific entities, preserve the raw description if (isHighConfidence && hasSpecificEntities) { // Clean but preserve technical details const cleaned = description .replace(/confidence\s*:\s*(high|medium|low)/gi, '') .replace(/```[\s\S]*?```/g, '') .trim(); const firstSentence = cleaned.split(/[.!?]/)[0].trim(); if (firstSentence.length > 10 && firstSentence.length < 100) { return firstSentence.charAt(0).toUpperCase() + firstSentence.slice(1); } } // Pattern-specific titles with preserved entity names if (undefinedMatch) { return `Undefined \`${undefinedMatch[1]}\` referenced in ${funcRef}`; } else if (mutationMatch) { return `Unsafe mutation of \`${mutationMatch[1]}\` in ${funcRef}`; } else if (/mutat/i.test(description)) { return `Unsafe object mutation in ${funcRef}`; } else if (/null|undefined/i.test(description)) { return `Null safety issue in ${funcRef}`; } else if (/async|await/i.test(description)) { return `Async handling error in ${funcRef}`; } else if (/syntax/i.test(description)) { return `Syntax error in ${funcRef}`; } else if (/security/i.test(description)) { return `Security vulnerability in ${funcRef}`; } else if (/performance|slow/i.test(description)) { return `Performance issue in ${funcRef}`; } // Generic: extract key action or first key phrase (only when parsing fails) const keyPhrase = description .replace(/this code|the function|the method/gi, funcRef) .split(/[.!?]/)[0] .trim(); if (keyPhrase.length > 10 && keyPhrase.length < 80) { return keyPhrase.charAt(0).toUpperCase() + keyPhrase.slice(1); } return `${type} in \`${fileName}\``; } /** * Generate short description (1-2 lines, no may/might) */ generateShortDescriptionV2(description, type, path, functionName, context) { const funcRef = functionName ? `\`${functionName}\`` : 'this code'; const varRef = context.variableName ? `\`${context.variableName}\`` : 'this value'; const fileName = path.split('/').pop() || path; // Clean weak language using consolidated normalizer const cleaned = (0, text_normalizer_1.normalizeReviewLanguage)(description); // Pattern-specific descriptions if (context.pattern === 'mutation' || /mutat/i.test(cleaned)) { return `\`${fileName}\`: ${funcRef} mutates ${varRef} directly. This causes side effects when the object is reused.`; } else if (context.pattern === 'null-safety' || /null|undefined/i.test(cleaned)) { return `\`${fileName}\`: ${funcRef} accesses ${varRef} without validation. This throws a runtime error.`; } else if (context.pattern === 'async' || /async|await/i.test(cleaned)) { return `\`${fileName}\`: ${funcRef} handles promises incorrectly. This causes race conditions or swallowed errors.`; } else if (/syntax/i.test(cleaned)) { return `\`${fileName}\`: ${funcRef} contains invalid syntax that prevents execution.`; } // Generic: take first 1-2 sentences, limit to 120 chars const sentences = cleaned.split(/[.!?]+/).filter(s => s.trim().length > 5); if (sentences.length >= 2) { const combined = `${sentences[0].trim()}. ${sentences[1].trim()}.`; return combined.length > 120 ? combined.substring(0, 117) + '...' : combined; } const single = sentences[0] || cleaned; return single.length > 120 ? single.substring(0, 117) + '...' : single; } /** * Map internal confidence to High/Medium/Low */ mapConfidence(level, astValid, validationConfidence) { if (level === 'critical') return 'High'; if (level === 'major') return astValid ? 'High' : 'Medium'; if (level === 'minor') return validationConfidence === 'strong' ? 'Medium' : 'Low'; return 'Low'; } /** * Format compact validation for dropdown */ formatValidationCompact(astValid, remedy, validationConfidence) { const items = []; // AST validation status if (astValid === true) { items.push(`- **AST:** Passed`); } else if (astValid === false) { items.push(`- **AST:** Failed`); } else { items.push(`- **AST:** Not verified`); } // Auto-fix status if (remedy) { if (validationConfidence === 'strong') { items.push(`- **Auto-fix:** Available (verified)`); } else if (validationConfidence === 'partial') { items.push(`- **Auto-fix:** Available (review suggested)`); } else { items.push(`- **Auto-fix:** Available`); } } else { items.push(`- **Auto-fix:** Not available`); } return items.join('\n'); } /** * FIX 1: Get clean issue type label (no special chars, no generic placeholders) */ getIssueTypeLabel(type, level) { const typeLower = type.toLowerCase(); if (typeLower.includes('bug') || typeLower.includes('error')) return 'Bug'; if (typeLower.includes('security')) return 'Security issue'; if (typeLower.includes('performance')) return 'Performance issue'; if (typeLower.includes('mutation')) return 'Object mutation'; if (typeLower.includes('null')) return 'Null safety'; if (typeLower.includes('async')) return 'Async issue'; if (typeLower.includes('syntax')) return 'Syntax error'; if (typeLower.includes('undefined')) return 'Undefined reference'; if (typeLower.includes('runtime')) return 'Runtime error'; if (level === 'critical') return 'Critical issue'; if (level === 'major') return 'Major issue'; if (level === 'minor') return 'Code issue'; return 'Suggestion'; } /** * PART 1: Generate minimal diff - handles raw code, produces clean diff */ generateMinimalDiff(original, fixed) { if (!fixed) return ''; // If no original code, we'll show fixed as additions (handled below) // Sanitize inputs const cleanOriginal = (0, exports.sanitizeText)(original).trim(); const cleanFixed = (0, exports.sanitizeText)(fixed).trim(); const origLines = cleanOriginal.split('\n'); const fixedLines = cleanFixed.split('\n'); const diffLines = []; let changesFound = 0; // Find changed lines for (let i = 0; i < Math.max(origLines.length, fixedLines.length); i++) { const origLine = origLines[i] || ''; const fixedLine = fixedLines[i] || ''; if (origLine !== fixedLine) { if (origLine && origLine.trim()) { diffLines.push(`- ${origLine}`); changesFound++; } if (fixedLine && fixedLine.trim()) { diffLines.push(`+ ${fixedLine}`); changesFound++; } // Limit to 8 lines of changes if (diffLines.length >= 8) break; } } // If no changes detected, show fixed code as added if (changesFound === 0 && cleanFixed) { const fixedPreview = cleanFixed.split('\n').slice(0, 4); for (const line of fixedPreview) { if (line.trim()) diffLines.push(`+ ${line}`); } } if (diffLines.length === 0) { return ''; } return diffLines.join('\n'); } /** * FIX 5: Clean impact - 2 bullets max, no generic language */ generateCleanImpact(type, level, context) { const varRef = context.variableName ? `\`${context.variableName}\`` : 'this value'; const pattern = context.pattern || type.toLowerCase(); const rawDescription = context.rawDescription || ''; const impacts = { mutation: `- Runtime crash: accessing undefined throws TypeError\n- Unstable behavior: breaks execution flow`, 'null-safety': `- Runtime crash: accessing undefined throws TypeError\n- Unstable behavior: breaks execution flow`, async: `- Race conditions: hard to reproduce timing bugs\n- Silent failures: errors swallowed without handling`, syntax: `- Build failure: code cannot execute\n- CI/CD block: pipelines will reject this commit`, security: `- Data exposure: unauthorized access possible\n- Compliance risk: security requirements violated`, performance: `- Resource waste: unnecessary computation\n- Scaling issues: problems compound under load` }; // Generate contextual impact based on actual issue if (pattern === 'mutation' || /mutat/i.test(rawDescription)) { return `- **Data corruption:** Mutating ${varRef} affects all references to this object\n- **Hidden bugs:** Side effects propagate silently, making debugging difficult`; } else if (pattern === 'undefined' || /undefined|null/i.test(rawDescription)) { return `- **Runtime error:** This will likely trigger error at runtime or crash the UI\n- **Execution failure:** Code will not run as intended`; } else if (pattern === 'async' || /async|await/i.test(rawDescription)) { return `- **Timing bugs:** Race conditions in production are hard to reproduce\n- **Silent failures:** Errors may be swallowed without proper error handling`; } else if (pattern === 'syntax' || /syntax|invalid|unexpected token/i.test(rawDescription)) { return `- **Execution blocked:** Code cannot run\n- **Parse error:** Invalid syntax prevents compilation, making future changes harder`; } else if (pattern === 'security' || /security/i.test(rawDescription)) { return `- **Vulnerability:** This pattern may expose sensitive data\n- **Compliance risk:** Security issues may violate regulatory requirements`; } else if (pattern === 'performance' || /performance|slow/i.test(rawDescription)) { return `- **Resource waste:** Unnecessary computation consumes CPU/memory\n- **Scaling issues:** Performance problems compound under load`; } // Use predefined impact if pattern matches return impacts[pattern] || ''; } async deletePendingReview(pullNumber) { try { const reviews = await octokit_1.octokit.pulls.listReviews({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber }); const pendingReview = reviews.data.find((review) => review.state === 'PENDING'); if (pendingReview) { info(`Deleting pending review for PR #${pullNumber} id: ${pendingReview.id}`); try { await octokit_1.octokit.pulls.deletePendingReview({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, review_id: pendingReview.id }); } catch (e) { warning(`Failed to delete pending review: ${e}`); } } } catch (e) { warning(`Failed to list reviews: ${e}`); } } generateMasterAIPrompt() { if (this.reviewCommentsBuffer.length === 0) return ''; const files = [...new Set(this.reviewCommentsBuffer.map(c => c.path))]; const issues = this.reviewCommentsBuffer.map(c => ({ title: c.cleanTitle, severity: c.severity, file: c.path })); if (files.length === 0 || issues.length === 0) return ''; return `
πŸ€– Master AI Prompt \`\`\`txt Review and fix all issues identified in this PR. Requirements: - Preserve existing functionality - Maintain project coding style - Avoid modifying unrelated logic - Handle edge cases safely - Ensure tests continue passing Files requiring attention: ${files.map(f => `- ${f}`).join('\n')} Issues: ${issues.map((i, idx) => `${idx + 1}. [${i.severity.charAt(0).toUpperCase() + i.severity.slice(1)}] ${i.title} (${i.file.split('/').pop()})`).join('\n')} \`\`\`
`; } async submitReview(pullNumber, commitId, statusMsg) { // IDEMPOTENCY: Check if we've already reviewed this commit if (await this.hasReviewedCommit(pullNumber, commitId)) { info(`Skipping duplicate review for commit ${commitId.substring(0, 7)}`); return; } // Mark this commit as reviewed this.reviewedCommits.add(commitId); const masterPrompt = this.generateMasterAIPrompt(); if (this.reviewCommentsBuffer.length === 0) { // Submit empty review with statusMsg const bodyHeader = `${templates_1.COMMENT_GREETING} ${templates_1.COMMIT_ID_START_TAG}${commitId}${templates_1.COMMIT_ID_END_TAG} ${statusMsg} ${masterPrompt} ${templates_1.PRIX_FOOTER}`; info(`Submitting empty review for PR #${pullNumber}`); try { await octokit_1.octokit.pulls.createReview({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, commit_id: commitId, event: 'COMMENT', body: bodyHeader }); } catch (e) { warning(`Failed to submit empty review: ${e}`); } return; } // De-duplicate and clear existing bot comments in the same range for (const comment of this.reviewCommentsBuffer) { const comments = await this.getCommentsAtRange(pullNumber, comment.path, comment.startLine, comment.endLine); for (const c of comments) { if (c.body.includes(templates_1.COMMENT_TAG)) { try { await octokit_1.octokit.pulls.deleteReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, comment_id: c.id }); } catch (e) { warning(`Failed to delete review comment: ${e}`); } } } } await this.deletePendingReview(pullNumber); // Split into high confidence and low/medium confidence comments const highConfidenceComments = this.reviewCommentsBuffer.filter(c => c.confidence === 'High'); const lowMediumConfidenceComments = this.reviewCommentsBuffer.filter(c => c.confidence !== 'High'); // Sort comments by priority (Critical β†’ Major β†’ Minor β†’ Info) highConfidenceComments.sort((a, b) => (a.priority || 3) - (b.priority || 3)); lowMediumConfidenceComments.sort((a, b) => (a.priority || 3) - (b.priority || 3)); // Unified Review: High confidence comments are standalone threads to support suggestions const pushableComments = highConfidenceComments.map(comment => this.generateCommentData(comment)); // Filter out comments with unresolvable line numbers to prevent cascading failure const validComments = pushableComments.filter(c => typeof c.line === 'number' && c.line > 0); const skippedCount = pushableComments.length - validComments.length; if (skippedCount > 0) { warning(`Filtered out ${skippedCount}/${pushableComments.length} high confidence comments with invalid line numbers before review creation`); } // Build summary with low/medium confidence comments inside a premium collapsible dropdown let enrichedStatus = statusMsg; if (lowMediumConfidenceComments.length > 0) { const lowConfidenceItems = lowMediumConfidenceComments .map(c => { const severityEmoji = { critical: 'πŸ”΄', major: '🟠', minor: '🟑', info: 'πŸ”΅' }[c.severity] || 'βšͺ'; const severityLabel = { critical: 'Critical', major: 'Major', minor: 'Minor', info: 'Info' }[c.severity] || c.severity.charAt(0).toUpperCase() + c.severity.slice(1); // Strip greetings and footers to keep the dropdown content extremely clean let cleanMessage = c.message .replace(templates_1.COMMENT_GREETING, '') .replace(templates_1.PRIX_FOOTER, '') .replace(templates_1.COMMENT_TAG, '') .trim(); return `
${severityEmoji} [${severityLabel}] ${c.cleanTitle} β€” ${c.path}:${c.startLine} ${cleanMessage}
`; }) .join('\n\n'); enrichedStatus += `
πŸ“‹ Additional Low/Medium Confidence Issues (${lowMediumConfidenceComments.length} detected) ${lowConfidenceItems}
`; info(`Filtered ${lowMediumConfidenceComments.length} low/medium confidence issues into PR review summary dropdown`); } const bodyHeader = `${templates_1.COMMENT_GREETING} ${templates_1.COMMIT_ID_START_TAG}${commitId}${templates_1.COMMIT_ID_END_TAG} ${enrichedStatus} ${masterPrompt} ${templates_1.PRIX_FOOTER}`; try { const review = await octokit_1.octokit.pulls.createReview({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, commit_id: commitId, comments: validComments }); info(`Submitting review for PR #${pullNumber}, inline comments: ${validComments.length}, review id: ${review.data.id}`); await octokit_1.octokit.pulls.submitReview({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, review_id: review.data.id, event: 'COMMENT', body: bodyHeader }); } catch (e) { warning(`Failed to create review: ${e}. Falling back to individual comments.`); await this.deletePendingReview(pullNumber); let commentCounter = 0; for (const comment of validComments) { info(`Creating new review comment for ${comment.path}:${comment.line ?? '?'} start_line=${comment.start_line ?? '?'}`); const commentData = { owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, commit_id: commitId, ...comment }; try { await octokit_1.octokit.pulls.createReviewComment(commentData); } catch (ee) { warning(`Failed to create review comment: ${ee}`); } commentCounter++; info(`Comment ${commentCounter}/${validComments.length} posted`); } // Post the summary message separately since the review-level body was lost try { await octokit_1.octokit.pulls.createReview({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, commit_id: commitId, event: 'COMMENT', body: bodyHeader }); info(`Posted summary comment after fallback for PR #${pullNumber}`); } catch (ee) { warning(`Failed to post summary comment after fallback: ${ee}`); } } } generateCommentData(comment) { const commentData = { path: comment.path, body: comment.message, line: comment.endLine }; if (comment.startLine !== comment.endLine) { commentData.start_line = comment.startLine; commentData.start_side = 'RIGHT'; } return commentData; } async reviewCommentReply(pullNumber, topLevelComment, message) { const reply = `${templates_1.COMMENT_GREETING} ${message} ${templates_1.PRIX_FOOTER} ${templates_1.COMMENT_REPLY_TAG}`; try { // Post the reply to the user comment await octokit_1.octokit.pulls.createReplyForReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, body: reply, comment_id: topLevelComment.id }); } catch (error) { warning(`Failed to reply to the top-level comment ${error}`); try { await octokit_1.octokit.pulls.createReplyForReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: pullNumber, body: `Could not post the reply to the top-level comment due to the following error: ${error}`, comment_id: topLevelComment.id }); } catch (e) { warning(`Failed to reply to the top-level comment ${e}`); } } try { if (topLevelComment.body.includes(templates_1.COMMENT_TAG)) { // replace COMMENT_TAG with COMMENT_REPLY_TAG in topLevelComment const newBody = topLevelComment.body.replace(templates_1.COMMENT_TAG, templates_1.COMMENT_REPLY_TAG); await octokit_1.octokit.pulls.updateReviewComment({ owner: exports.repo.owner, repo: exports.repo.repo, comment_id: topLevelComment.id, body: newBody }); } } catch (error) { warning(`Failed to update the top-level comment ${error}`); } } async getCommentsWithinRange(pullNumber, path, startLine, endLine) { const comments = await this.listReviewComments(pullNumber); return comments.filter((comment) => comment.path === path && comment.body !== '' && ((comment.start_line !== undefined && comment.start_line >= startLine && comment.line <= endLine) || (startLine === endLine && comment.line === endLine))); } async getCommentsAtRange(pullNumber, path, startLine, endLine) { const comments = await this.listReviewComments(pullNumber); return comments.filter((comment) => comment.path === path && comment.body !== '' && ((comment.start_line !== undefined && comment.start_line === startLine && comment.line === endLine) || (startLine === endLine && comment.line === endLine))); } async getCommentChainsWithinRange(pullNumber, path, startLine, endLine, tag = '') { const existingComments = await this.getCommentsWithinRange(pullNumber, path, startLine, endLine); // find all top most comments const topLevelComments = []; for (const comment of existingComments) { if (!comment.in_reply_to_id) { topLevelComments.push(comment); } } let allChains = ''; let chainNum = 0; for (const topLevelComment of topLevelComments) { // get conversation chain const chain = await this.composeCommentChain(existingComments, topLevelComment); if (chain && chain.includes(tag)) { chainNum += 1; allChains += `Conversation Chain ${chainNum}: ${chain} --- `; } } return allChains; } async composeCommentChain(reviewComments, topLevelComment) { const conversationChain = reviewComments .filter((cmt) => cmt.in_reply_to_id === topLevelComment.id) .map((cmt) => `${cmt.user.login}: ${cmt.body}`); conversationChain.unshift(`${topLevelComment.user.login}: ${topLevelComment.body}`); return conversationChain.join('\n---\n'); } async getCommentChain(pullNumber, comment) { try { const reviewComments = await this.listReviewComments(pullNumber); const topLevelComment = await this.getTopLevelComment(reviewComments, comment); const chain = await this.composeCommentChain(reviewComments, topLevelComment); return { chain, topLevelComment }; } catch (e) { warning(`Failed to get conversation chain: ${e}`); return { chain: '', topLevelComment: null }; } } async getTopLevelComment(reviewComments, comment) { let topLevelComment = comment; while (topLevelComment.in_reply_to_id) { const parentComment = reviewComments.find((cmt) => cmt.id === topLevelComment.in_reply_to_id); if (parentComment) { topLevelComment = parentComment; } else { break; } } return topLevelComment; } reviewCommentsCache = {}; async listReviewComments(target) { if (this.reviewCommentsCache[target]) { return this.reviewCommentsCache[target]; } const allComments = []; let page = 1; try { for (;;) { const { data: comments } = await octokit_1.octokit.pulls.listReviewComments({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: target, page, per_page: 100 }); allComments.push(...comments); page++; if (!comments || comments.length < 100) { break; } } this.reviewCommentsCache[target] = allComments; return allComments; } catch (e) { warning(`Failed to list review comments: ${e}`); return allComments; } } async create(body, target) { try { // get comment ID from the response const response = await octokit_1.octokit.issues.createComment({ owner: exports.repo.owner, repo: exports.repo.repo, issue_number: target, body }); // add comment to issueCommentsCache if (this.issueCommentsCache[target]) { this.issueCommentsCache[target].push(response.data); } else { this.issueCommentsCache[target] = [response.data]; } } catch (e) { warning(`Failed to create comment: ${e}`); } } async replace(body, tag, target) { try { const cmt = await this.findCommentWithTag(tag, target); if (cmt) { await octokit_1.octokit.issues.updateComment({ owner: exports.repo.owner, repo: exports.repo.repo, comment_id: cmt.id, body }); } else { await this.create(body, target); } } catch (e) { warning(`Failed to replace comment: ${e}`); } } async findCommentWithTag(tag, target) { try { const comments = await this.listComments(target); for (const cmt of comments) { if (cmt.body && cmt.body.includes(tag)) { return cmt; } } return null; } catch (e) { warning(`Failed to find comment with tag: ${e}`); return null; } } issueCommentsCache = {}; async listComments(target) { if (this.issueCommentsCache[target]) { return this.issueCommentsCache[target]; } const allComments = []; let page = 1; try { for (;;) { const { data: comments } = await octokit_1.octokit.issues.listComments({ owner: exports.repo.owner, repo: exports.repo.repo, issue_number: target, page, per_page: 100 }); allComments.push(...comments); page++; if (!comments || comments.length < 100) { break; } } this.issueCommentsCache[target] = allComments; return allComments; } catch (e) { warning(`Failed to list comments: ${e}`); return allComments; } } // function that takes a comment body and returns the list of commit ids that have been reviewed // commit ids are comments between the commit_ids_reviewed_start and commit_ids_reviewed_end markers // getReviewedCommitIds(commentBody) { const start = commentBody.indexOf(templates_1.COMMIT_ID_START_TAG); const end = commentBody.indexOf(templates_1.COMMIT_ID_END_TAG); if (start === -1 || end === -1) { return []; } const ids = commentBody.substring(start + templates_1.COMMIT_ID_START_TAG.length, end); // remove the markers from each id and extract the id and remove empty strings return ids .split('', '').trim()) .filter(id => id !== ''); } // get review commit ids comment block from the body as a string // including markers getReviewedCommitIdsBlock(commentBody) { const start = commentBody.indexOf(templates_1.COMMIT_ID_START_TAG); const end = commentBody.indexOf(templates_1.COMMIT_ID_END_TAG); if (start === -1 || end === -1) { return ''; } return commentBody.substring(start, end + templates_1.COMMIT_ID_END_TAG.length); } // add a commit id to the list of reviewed commit ids // if the marker doesn't exist, add it addReviewedCommitId(commentBody, commitId) { const start = commentBody.indexOf(templates_1.COMMIT_ID_START_TAG); const end = commentBody.indexOf(templates_1.COMMIT_ID_END_TAG); if (start === -1 || end === -1) { return `${commentBody}\n${templates_1.COMMIT_ID_START_TAG}\n\n${templates_1.COMMIT_ID_END_TAG}`; } const ids = commentBody.substring(start + templates_1.COMMIT_ID_START_TAG.length, end); return `${commentBody.substring(0, start + templates_1.COMMIT_ID_START_TAG.length)}${ids}\n${commentBody.substring(end)}`; } // given a list of commit ids provide the highest commit id that has been reviewed getHighestReviewedCommitId(commitIds, reviewedCommitIds) { for (let i = commitIds.length - 1; i >= 0; i--) { if (reviewedCommitIds.includes(commitIds[i])) { return commitIds[i]; } } return ''; } async getAllCommitIds() { const allCommits = []; let page = 1; let commits; if (exports.context && exports.context.payload && exports.context.payload.pull_request != null) { do { commits = await octokit_1.octokit.pulls.listCommits({ owner: exports.repo.owner, repo: exports.repo.repo, pull_number: exports.context.payload.pull_request.number, per_page: 100, page }); allCommits.push(...commits.data.map((commit) => commit.sha)); page++; } while (commits.data.length > 0); } return allCommits; } // add in-progress status to the comment body addInProgressStatus(commentBody, statusMsg) { const start = commentBody.indexOf(templates_1.IN_PROGRESS_START_TAG); const end = commentBody.indexOf(templates_1.IN_PROGRESS_END_TAG); // add to the beginning of the comment body if the marker doesn't exist // otherwise do nothing if (start === -1 || end === -1) { return `${templates_1.IN_PROGRESS_START_TAG} Currently reviewing new changes in this PR... ${statusMsg} ${templates_1.IN_PROGRESS_END_TAG} --- ${commentBody}`; } return commentBody; } // remove in-progress status from the comment body removeInProgressStatus(commentBody) { const start = commentBody.indexOf(templates_1.IN_PROGRESS_START_TAG); const end = commentBody.indexOf(templates_1.IN_PROGRESS_END_TAG); // remove the in-progress status if the marker exists // otherwise do nothing if (start !== -1 && end !== -1) { return (commentBody.substring(0, start) + commentBody.substring(end + templates_1.IN_PROGRESS_END_TAG.length)); } return commentBody; } } exports.Commenter = Commenter; //# sourceMappingURL=commenter.js.map