| "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 { |
| |
| reviewedCommits = new Set(); |
| strictMode = false; |
| |
| |
| |
| async hasReviewedCommit(pullNumber, commitId) { |
| if (this.reviewedCommits.has(commitId)) { |
| return true; |
| } |
| |
| 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; |
| } |
| |
| |
| |
| 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('<!--') ? tag : `<!-- ${tag} -->`; |
| 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) { |
| |
| |
| try { |
| |
| 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 = []; |
| |
| promptShownCount = 0; |
| fixGeneratedCount = 0; |
| |
| |
| |
| getAnalytics() { |
| return { |
| promptsShown: this.promptShownCount, |
| fixesGenerated: this.fixGeneratedCount |
| }; |
| } |
| async bufferReviewComment(path, startLine, endLine, message, astValid, remedy, validationConfidence, originalCode, aiImpact, parsedIssue) { |
| |
| if (validationConfidence === 'failed' && !remedy) { |
| info(`Skipping low-confidence comment for ${path}:${startLine}`); |
| return; |
| } |
| |
| 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 || ''; |
| |
| const descLower = description.toLowerCase(); |
| const titleLower = issueTitle.toLowerCase(); |
| |
| 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'; |
| } |
| } |
| |
| 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'; |
| } |
| } |
| 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'; |
| } |
| |
| const functionName = this.extractFunctionName(description, remedy); |
| const context = this.extractCodeContext(description, remedy, type); |
| |
| const cleanDescription = description; |
| |
| issueTitle = issueTitle.trim(); |
| issueTitle = issueTitle.charAt(0).toUpperCase() + issueTitle.slice(1); |
| |
| let shortDesc = description.trim(); |
| shortDesc = shortDesc.charAt(0).toUpperCase() + shortDesc.slice(1); |
| if (!shortDesc.endsWith('.')) |
| shortDesc += '.'; |
| |
| const confidence = this.mapConfidence(level, astValid, validationConfidence); |
| |
| const validationSection = this.formatValidationCompact(astValid, remedy, validationConfidence); |
| |
| 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); |
| |
| const sanitizedTitle = (0, exports.sanitizeText)(issueTitle); |
| const sanitizedDesc = (0, exports.sanitizeText)(shortDesc); |
| const sanitizedConfidence = (0, exports.sanitizeText)(confidence); |
| |
| |
| |
| 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 |
| ? `<details>
|
| <summary>🛠 Suggested Fix</summary>
|
|
|
| \`\`\`diff
|
| ${this.generateMinimalDiff(originalCode, remedy)}
|
| \`\`\`
|
|
|
| </details>
|
|
|
| ---
|
| ` |
| : ''}
|
| ${this.generateEliteAIPrompt(path, functionName, context, type, remedy, description) |
| ? `
|
| ### 🤖 AI Assistant
|
|
|
| ${this.generateEliteAIPrompt(path, functionName, context, type, remedy, description)}
|
|
|
| ---
|
| ` |
| : ''}${validationSection |
| ? `<details>
|
| <summary>⚙️ Validation</summary>
|
|
|
| ${validationSection}
|
|
|
| </details>
|
|
|
| ` |
| : ''}${templates_1.PRIX_FOOTER}
|
|
|
| ${templates_1.COMMENT_TAG}`; |
| |
| |
| |
| 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; |
| } |
| |
| 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 |
| }); |
| } |
| |
| |
| |
| generateInsight(type, level, context, description) { |
| |
| if (level !== 'critical' && level !== 'major') { |
| return ''; |
| } |
| const varRef = context.variableName |
| ? `\`${context.variableName}\`` |
| : 'this value'; |
| |
| 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] || ''; |
| } |
| |
| |
| |
| extractFunctionName(description, remedy) { |
| |
| const funcMatch = description.match(/function\s+(\w+)|(\w+)\s*\(|`(\w+)`/); |
| if (funcMatch) { |
| return funcMatch[1] || funcMatch[2] || funcMatch[3]; |
| } |
| |
| if (remedy) { |
| const remedyMatch = remedy.match(/function\s+(\w+)|(\w+)\s*\(/); |
| if (remedyMatch) { |
| return remedyMatch[1] || remedyMatch[2]; |
| } |
| } |
| return ''; |
| } |
| |
| |
| |
| generateSpecificExplanation(description, type, path, functionName) { |
| |
| let cleaned = (0, text_normalizer_1.normalizeReviewLanguage)(description); |
| |
| 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.`; |
| } |
| |
| cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); |
| return cleaned; |
| } |
| |
| |
| |
| generateWhyItMatters(type, level, context, rawDescription) { |
| const varRef = context.variableName |
| ? `\`${context.variableName}\`` |
| : 'this value'; |
| const funcRef = context.functionName |
| ? `\`${context.functionName}\`` |
| : 'this code'; |
| |
| 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); |
| |
| 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`; |
| } |
| |
| return `- This issue in ${funcRef} affects code reliability\n- Should be addressed to prevent unexpected behavior`; |
| } |
| |
| |
| |
| formatDiffView(remedy, type) { |
| |
| const cleanFix = remedy |
| .replace(/\\ No newline at end of file/g, '') |
| .replace(/\bded\b/g, '') |
| .replace(/\n{3,}/g, '\n\n') |
| .trim(); |
| |
| const lines = cleanFix.split('\n'); |
| const diffLines = []; |
| for (const line of lines) { |
| const trimmed = line.trim(); |
| if (!trimmed || trimmed.startsWith('//')) |
| continue; |
| |
| if (trimmed.startsWith('-') || trimmed.startsWith('+')) { |
| diffLines.push(trimmed); |
| } |
| else if (trimmed.startsWith('@@')) { |
| |
| diffLines.push(trimmed); |
| } |
| else if (!trimmed.startsWith('```')) { |
| |
| diffLines.push(' ' + trimmed); |
| } |
| } |
| |
| let diffOutput = diffLines.join('\n'); |
| |
| if (!diffOutput.includes('-') && !diffOutput.includes('+')) { |
| |
| diffOutput = cleanFix |
| .split('\n') |
| .filter(l => l.trim()) |
| .map(l => `+ ${l}`) |
| .join('\n'); |
| } |
| if (!diffOutput.trim()) { |
| return ''; |
| } |
| return `<details>
|
| <summary>🛠 View diff</summary>
|
|
|
| \`\`\`diff
|
| ${diffOutput}
|
| \`\`\`
|
|
|
| </details>`; |
| } |
| |
| |
| |
| generateAIPrompt(path, functionName, type, description, remedy) { |
| const fileName = path.split('/').pop() || path; |
| const funcContext = functionName |
| ? `the \`${functionName}\` function` |
| : 'this code'; |
| |
| 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'; |
| |
| 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 `<details>
|
| <summary>� Copy prompt</summary>
|
|
|
| \`\`\`
|
| ${prompt}
|
| \`\`\`
|
|
|
| </details>`; |
| } |
| |
| |
| |
| 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')}`; |
| } |
| |
| |
| |
| extractCodeContext(description, remedy, type) { |
| const context = {}; |
| |
| 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 (!context.variableName && remedy) { |
| const remedyVars = remedy.match(/(?:const|let|var)\s+(\w+)\s*=/) || |
| remedy.match(/(\w+)\s*[=:]/); |
| if (remedyVars) |
| context.variableName = remedyVars[1]; |
| } |
| |
| 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; |
| } |
| |
| |
| |
| generateEliteExplanation(description, type, path, functionName, context) { |
| |
| 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'; |
| |
| 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')) { |
| |
| cleaned = `In ${fileName}, ${funcRef} has an issue: ${cleaned.replace(/issue|problem/gi, '').trim() || 'code quality concern'}`; |
| } |
| |
| cleaned = cleaned.charAt(0).toUpperCase() + cleaned.slice(1); |
| return cleaned; |
| } |
| |
| |
| |
| 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']; |
| } |
| |
| 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.`; |
| } |
| |
| |
| |
| formatSurgicalDiff(remedy, type, startLine, endLine) { |
| |
| const cleanFix = remedy |
| .replace(/\\ No newline at end of file/g, '') |
| .replace(/\bded\b/g, '') |
| .replace(/\n{3,}/g, '\n\n') |
| .trim(); |
| |
| const lines = cleanFix.split('\n'); |
| const diffLines = []; |
| for (const line of lines) { |
| const trimmed = line.trim(); |
| if (!trimmed) |
| continue; |
| if (trimmed.startsWith('//')) |
| continue; |
| |
| if (trimmed.startsWith('-') || trimmed.startsWith('+')) { |
| diffLines.push(trimmed); |
| } |
| else if (trimmed.startsWith('@@')) { |
| diffLines.push(trimmed); |
| } |
| else if (!trimmed.startsWith('```')) { |
| |
| diffLines.push(' ' + trimmed); |
| } |
| } |
| |
| if (!diffLines.some(l => l.startsWith('-') || l.startsWith('+'))) { |
| |
| 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); |
| } |
| } |
| |
| const limitedDiff = diffLines.slice(0, 10); |
| if (diffLines.length > 10) { |
| limitedDiff.push('// ... (additional changes)'); |
| } |
| if (limitedDiff.length === 0) { |
| return ''; |
| } |
| return `<details>
|
| <summary>🛠 View diff</summary>
|
|
|
| \`\`\`diff
|
| ${limitedDiff.join('\n')}
|
| \`\`\`
|
|
|
| </details>`; |
| } |
| |
| |
| |
| 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'); |
| } |
| |
| |
| |
| 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; |
| |
| const specificIssue = this.extractSpecificIssue(rawDescription || '', context); |
| |
| 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 { |
| |
| 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 `<details>
|
| <summary>🤖 Prompt for AI</summary>
|
|
|
| \`\`\`txt
|
| ${prompt}
|
| \`\`\`
|
|
|
| </details>`; |
| } |
| |
| |
| |
| extractSpecificIssue(description, context) { |
| const result = {}; |
| |
| const undefinedMatch = description.match(/undefined\s+(?:function|variable|reference|identifier)?\s*['`]?(\w+)['`]?/i); |
| if (undefinedMatch) { |
| result.undefinedReference = undefinedMatch[1]; |
| } |
| |
| if (/syntax\s+error|invalid\s+token|unexpected\s+token|parse\s+error/i.test(description)) { |
| result.syntaxError = true; |
| } |
| |
| if (/disconnected|unused|dead\s+code|no\s+effect|statement\s+has\s+no\s+effect/i.test(description)) { |
| result.disconnectedLogic = true; |
| } |
| |
| 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; |
| } |
| |
| |
| |
| generateIssueTitle(description, type, functionName, path, confidence) { |
| const funcRef = functionName ? `\`${functionName}\`` : 'this code'; |
| const fileName = path.split('/').pop() || path; |
| |
| 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); |
| |
| const hasSpecificEntities = undefinedMatch || |
| syntaxMatch || |
| mutationMatch || |
| /\b\w+\b.*\b\w+\b/.test(description); |
| const isHighConfidence = confidence === 'High' || confidence === 'Medium'; |
| |
| if (isHighConfidence && hasSpecificEntities) { |
| |
| 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); |
| } |
| } |
| |
| 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}`; |
| } |
| |
| 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}\``; |
| } |
| |
| |
| |
| 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; |
| |
| const cleaned = (0, text_normalizer_1.normalizeReviewLanguage)(description); |
| |
| 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.`; |
| } |
| |
| 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; |
| } |
| |
| |
| |
| 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'; |
| } |
| |
| |
| |
| formatValidationCompact(astValid, remedy, validationConfidence) { |
| const items = []; |
| |
| if (astValid === true) { |
| items.push(`- **AST:** Passed`); |
| } |
| else if (astValid === false) { |
| items.push(`- **AST:** Failed`); |
| } |
| else { |
| items.push(`- **AST:** Not verified`); |
| } |
| |
| 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'); |
| } |
| |
| |
| |
| 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'; |
| } |
| |
| |
| |
| generateMinimalDiff(original, fixed) { |
| if (!fixed) |
| return ''; |
| |
| |
| 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; |
| |
| 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++; |
| } |
| |
| if (diffLines.length >= 8) |
| break; |
| } |
| } |
| |
| 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'); |
| } |
| |
| |
| |
| 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` |
| }; |
| |
| 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`; |
| } |
| |
| 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 `<details>
|
| <summary>🤖 Master AI Prompt</summary>
|
|
|
| \`\`\`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')}
|
| \`\`\`
|
|
|
| </details>`; |
| } |
| async submitReview(pullNumber, commitId, statusMsg) { |
| |
| if (await this.hasReviewedCommit(pullNumber, commitId)) { |
| info(`Skipping duplicate review for commit ${commitId.substring(0, 7)}`); |
| return; |
| } |
| |
| this.reviewedCommits.add(commitId); |
| const masterPrompt = this.generateMasterAIPrompt(); |
| if (this.reviewCommentsBuffer.length === 0) { |
| |
| 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; |
| } |
| |
| 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); |
| |
| const highConfidenceComments = this.reviewCommentsBuffer.filter(c => c.confidence === 'High'); |
| const lowMediumConfidenceComments = this.reviewCommentsBuffer.filter(c => c.confidence !== 'High'); |
| |
| highConfidenceComments.sort((a, b) => (a.priority || 3) - (b.priority || 3)); |
| lowMediumConfidenceComments.sort((a, b) => (a.priority || 3) - (b.priority || 3)); |
| |
| const pushableComments = highConfidenceComments.map(comment => this.generateCommentData(comment)); |
| |
| 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`); |
| } |
| |
| 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); |
| |
| let cleanMessage = c.message |
| .replace(templates_1.COMMENT_GREETING, '') |
| .replace(templates_1.PRIX_FOOTER, '') |
| .replace(templates_1.COMMENT_TAG, '') |
| .trim(); |
| return `<details>
|
| <summary>${severityEmoji} [${severityLabel}] <b>${c.cleanTitle}</b> — <code>${c.path}:${c.startLine}</code></summary>
|
|
|
| ${cleanMessage}
|
|
|
| </details>`; |
| }) |
| .join('\n\n'); |
| enrichedStatus += `
|
|
|
| <details>
|
| <summary>📋 Additional Low/Medium Confidence Issues (${lowMediumConfidenceComments.length} detected)</summary>
|
|
|
| ${lowConfidenceItems}
|
|
|
| </details>`; |
| 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`); |
| } |
| |
| 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 { |
| |
| 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)) { |
| |
| 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); |
| |
| 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) { |
| |
| 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 { |
| |
| const response = await octokit_1.octokit.issues.createComment({ |
| owner: exports.repo.owner, |
| repo: exports.repo.repo, |
| issue_number: target, |
| body |
| }); |
| |
| 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; |
| } |
| } |
| |
| |
| |
| 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); |
| |
| return ids |
| .split('<!--') |
| .map(id => id.replace('-->', '').trim()) |
| .filter(id => id !== ''); |
| } |
| |
| |
| 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); |
| } |
| |
| |
| 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<!-- ${commitId} -->\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}<!-- ${commitId} -->\n${commentBody.substring(end)}`; |
| } |
| |
| 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; |
| } |
| |
| addInProgressStatus(commentBody, statusMsg) { |
| const start = commentBody.indexOf(templates_1.IN_PROGRESS_START_TAG); |
| const end = commentBody.indexOf(templates_1.IN_PROGRESS_END_TAG); |
| |
| |
| 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; |
| } |
| |
| removeInProgressStatus(commentBody) { |
| const start = commentBody.indexOf(templates_1.IN_PROGRESS_START_TAG); |
| const end = commentBody.indexOf(templates_1.IN_PROGRESS_END_TAG); |
| |
| |
| 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; |
| |