Spaces:
Paused
Paused
| ; | |
| var __importDefault = (this && this.__importDefault) || function (mod) { | |
| return (mod && mod.__esModule) ? mod : { "default": mod }; | |
| }; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.autonomousAudit = exports.run = exports.codeReview = exports.repo = exports.context = void 0; | |
| const ts_morph_1 = require("ts-morph"); | |
| const promises_1 = require("fs/promises"); | |
| const fs_1 = require("fs"); | |
| const path_1 = require("path"); | |
| const utils_1 = require("./utils"); | |
| const file_discoverer_1 = require("./file-discoverer"); | |
| const pr_service_1 = require("./services/pr-service"); | |
| const p_limit_1 = __importDefault(require("p-limit")); | |
| const bot_1 = require("./bot"); | |
| const commenter_1 = require("./commenter"); | |
| const inputs_1 = require("./inputs"); | |
| const options_1 = require("./options"); | |
| const octokit_1 = require("./octokit"); | |
| const tokenizer_1 = require("./tokenizer"); | |
| const context_1 = require("./context"); | |
| const symbol_graph_1 = require("./symbol-graph"); | |
| const test_generator_1 = require("./test-generator"); | |
| const token_scheduler_1 = require("./services/token-scheduler"); | |
| const patch_utils_1 = require("./utils/patch-utils"); | |
| const pino_1 = __importDefault(require("pino")); | |
| const logger = (0, pino_1.default)({ level: process.env.LOG_LEVEL || 'info' }); | |
| let error = (msg) => logger.error(msg); | |
| let info = (msg) => logger.info(msg); | |
| let warning = (msg) => logger.warn(msg); | |
| 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 ignoreKeyword = '@ai-pr-reviewer: ignore'; | |
| const codeReview = async (lightBot, heavyBot, options, prompts) => { | |
| const commenter = new commenter_1.Commenter(); | |
| const project = new ts_morph_1.Project(); | |
| // Sync schedulers with their respective model limits | |
| token_scheduler_1.lightScheduler.setLimit(options.lightTokenLimits.maxTokens); | |
| token_scheduler_1.heavyScheduler.setLimit(options.heavyTokenLimits.maxTokens); | |
| // Initialize shared context engine once at the start using the cloned repo path | |
| try { | |
| const store = context_1.als.getStore(); | |
| const workingDir = store?.workingDir || process.cwd(); | |
| const repoInfo = store?.repo; | |
| const stableId = repoInfo ? `${repoInfo.owner}/${repoInfo.repo}` : undefined; | |
| await symbol_graph_1.unifiedContextEngine.initialize(workingDir, stableId); | |
| } | |
| catch (e) { | |
| info(`Context engine initialization failed: ${e}`); | |
| } | |
| const aiConcurrencyLimit = (0, p_limit_1.default)(options.concurrencyLimit); | |
| const githubConcurrencyLimit = (0, p_limit_1.default)(options.githubConcurrencyLimit); | |
| if (exports.context.name !== 'pull_request' && | |
| exports.context.name !== 'pull_request_target') { | |
| warning(`Skipped: current event is ${exports.context.name}, only support pull_request event`); | |
| return; | |
| } | |
| if (exports.context.payload.pull_request == null) { | |
| warning('Skipped: context.payload.pull_request is null'); | |
| return; | |
| } | |
| const prUser = exports.context.payload.pull_request.user; | |
| const prAuthor = prUser?.type; | |
| const branchName = exports.context.payload.pull_request.head.ref; | |
| if (prAuthor === 'Bot' || | |
| branchName.startsWith('ai-remedy/') || | |
| branchName.startsWith('github-actions[bot]')) { | |
| info(`Skipping audit: PR author type=${prAuthor}, branch=${branchName}`); | |
| return; | |
| } | |
| const inputs = new inputs_1.Inputs(); | |
| inputs.title = exports.context.payload.pull_request.title; | |
| if (exports.context.payload.pull_request.body != null) { | |
| inputs.description = commenter.getDescription(exports.context.payload.pull_request.body); | |
| } | |
| if (inputs.description.includes(ignoreKeyword)) { | |
| info('Skipped: description contains ignore_keyword'); | |
| return; | |
| } | |
| inputs.systemMessage = options.systemMessage; | |
| const existingSummarizeCmt = await commenter.findCommentWithTag(commenter_1.SUMMARIZE_TAG, exports.context.payload.pull_request.number); | |
| let existingCommitIdsBlock = ''; | |
| let existingSummarizeCmtBody = ''; | |
| if (existingSummarizeCmt != null) { | |
| existingSummarizeCmtBody = existingSummarizeCmt.body; | |
| inputs.rawSummary = commenter.getRawSummary(existingSummarizeCmtBody); | |
| inputs.shortSummary = commenter.getShortSummary(existingSummarizeCmtBody); | |
| existingCommitIdsBlock = commenter.getReviewedCommitIdsBlock(existingSummarizeCmtBody); | |
| } | |
| const allCommitIds = await commenter.getAllCommitIds(); | |
| let highestReviewedCommitId = ''; | |
| if (existingCommitIdsBlock !== '') { | |
| highestReviewedCommitId = commenter.getHighestReviewedCommitId(allCommitIds, commenter.getReviewedCommitIds(existingCommitIdsBlock)); | |
| } | |
| if (highestReviewedCommitId === '' || | |
| highestReviewedCommitId === exports.context.payload.pull_request.head.sha) { | |
| info(`Will review from the base commit: ${exports.context.payload.pull_request.base.sha}`); | |
| highestReviewedCommitId = exports.context.payload.pull_request.base.sha; | |
| } | |
| else { | |
| info(`Will review from commit: ${highestReviewedCommitId}`); | |
| } | |
| const incrementalDiff = await octokit_1.octokit.rest.repos.compareCommits({ | |
| owner: exports.repo.owner, | |
| repo: exports.repo.repo, | |
| base: highestReviewedCommitId, | |
| head: exports.context.payload.pull_request.head.sha | |
| }); | |
| const targetBranchDiff = await octokit_1.octokit.rest.repos.compareCommits({ | |
| owner: exports.repo.owner, | |
| repo: exports.repo.repo, | |
| base: exports.context.payload.pull_request.base.sha, | |
| head: exports.context.payload.pull_request.head.sha | |
| }); | |
| const incrementalFiles = incrementalDiff.data.files; | |
| const targetBranchFiles = targetBranchDiff.data.files; | |
| if (incrementalFiles == null || targetBranchFiles == null) { | |
| warning('Skipped: files data is missing'); | |
| return; | |
| } | |
| const files = targetBranchFiles.filter(targetBranchFile => incrementalFiles.some(incrementalFile => incrementalFile.filename === targetBranchFile.filename)); | |
| if (files.length === 0) { | |
| warning('Skipped: files is null'); | |
| return; | |
| } | |
| const filterSelectedFiles = []; | |
| const filterIgnoredFiles = []; | |
| for (const file of files) { | |
| if (!options.checkPath(file.filename)) { | |
| info(`skip for excluded path: ${file.filename}`); | |
| filterIgnoredFiles.push(file); | |
| } | |
| else { | |
| filterSelectedFiles.push(file); | |
| } | |
| } | |
| if (filterSelectedFiles.length === 0) { | |
| warning('Skipped: filterSelectedFiles is null'); | |
| return; | |
| } | |
| const commits = incrementalDiff.data.commits; | |
| if (commits.length === 0) { | |
| warning('Skipped: commits is null'); | |
| return; | |
| } | |
| const filteredFiles = await Promise.all(filterSelectedFiles.map(file => githubConcurrencyLimit(async () => { | |
| let fileContent = ''; | |
| if (exports.context.payload.pull_request == null) { | |
| warning('Skipped: context.payload.pull_request is null'); | |
| return null; | |
| } | |
| try { | |
| const store = context_1.als.getStore(); | |
| const workingDir = store?.workingDir || process.cwd(); | |
| const localPath = (0, path_1.join)(workingDir, file.filename); | |
| if ((0, fs_1.existsSync)(localPath)) { | |
| fileContent = await (0, promises_1.readFile)(localPath, 'utf8'); | |
| } | |
| else { | |
| const contents = await octokit_1.octokit.rest.repos.getContent({ | |
| owner: exports.repo.owner, | |
| repo: exports.repo.repo, | |
| path: file.filename, | |
| ref: exports.context.payload.pull_request.base.sha | |
| }); | |
| if (contents.data != null && !Array.isArray(contents.data)) { | |
| if (contents.data.type === 'file' && | |
| contents.data.content != null) { | |
| fileContent = Buffer.from(contents.data.content, 'base64').toString(); | |
| } | |
| } | |
| } | |
| } | |
| catch (e) { | |
| warning(`Failed to get file contents: ${e}. This is OK if it's a new file.`); | |
| } | |
| let fileDiff = ''; | |
| if (file.patch != null) { | |
| fileDiff = file.patch; | |
| } | |
| const patches = []; | |
| for (const patch of (0, patch_utils_1.splitPatch)(file.patch)) { | |
| const patchLines = (0, patch_utils_1.patchStartEndLine)(patch); | |
| if (patchLines == null) { | |
| continue; | |
| } | |
| const hunks = (0, patch_utils_1.parsePatch)(patch); | |
| if (hunks == null) { | |
| continue; | |
| } | |
| const hunksStr = ` | |
| ---new_hunk--- | |
| \`\`\` | |
| ${hunks.newHunk} | |
| \`\`\` | |
| ---old_hunk--- | |
| \`\`\` | |
| ${hunks.oldHunk} | |
| \`\`\` | |
| `; | |
| patches.push([ | |
| patchLines.newHunk.startLine, | |
| patchLines.newHunk.endLine, | |
| hunksStr | |
| ]); | |
| } | |
| if (patches.length > 0) { | |
| return [file.filename, fileContent, fileDiff, patches]; | |
| } | |
| else { | |
| return null; | |
| } | |
| }))); | |
| const filesAndChanges = filteredFiles.filter(file => file !== null); | |
| // Sort files by diff size (token count) so small files are processed first | |
| // This prevents massive files from starving the token budget for small changes | |
| filesAndChanges.sort((a, b) => (0, tokenizer_1.getTokenCount)(a[2]) - (0, tokenizer_1.getTokenCount)(b[2])); | |
| // HARD LIMIT: Max 100 files to prevent memory exhaustion | |
| const MAX_FILES_LIMIT = 100; | |
| const originalCount = filesAndChanges.length; | |
| if (filesAndChanges.length > MAX_FILES_LIMIT) { | |
| filesAndChanges.length = MAX_FILES_LIMIT; | |
| warning(`Truncated from ${originalCount} to ${MAX_FILES_LIMIT} files to prevent memory exhaustion. ` + | |
| `Consider breaking this PR into smaller chunks.`); | |
| } | |
| if (filesAndChanges.length === 0) { | |
| error('Skipped: no files to review'); | |
| return; | |
| } | |
| let statusMsg = `<details> | |
| <summary>Commits</summary> | |
| Files that changed from the base of the PR and between ${highestReviewedCommitId} and ${exports.context.payload.pull_request.head.sha} commits. | |
| </details> | |
| ${filesAndChanges.length > 0 | |
| ? ` | |
| <details> | |
| <summary>Files selected (${filesAndChanges.length})</summary> | |
| * ${filesAndChanges | |
| .map(([filename, , , patches]) => `${filename} (${patches.length})`) | |
| .join('\n* ')} | |
| </details> | |
| ` | |
| : ''} | |
| ${filterIgnoredFiles.length > 0 | |
| ? ` | |
| <details> | |
| <summary>Files ignored due to filter (${filterIgnoredFiles.length})</summary> | |
| * ${filterIgnoredFiles.map(file => file.filename).join('\n* ')} | |
| </details> | |
| ` | |
| : ''} | |
| `; | |
| const summariesFailed = []; | |
| const doSummary = async (filename, fileContent, fileDiff) => { | |
| info(`summarize: ${filename}`); | |
| const ins = inputs.clone(); | |
| if (fileDiff.length === 0) { | |
| warning(`summarize: file_diff is empty, skip ${filename}`); | |
| summariesFailed.push(`${filename} (empty diff)`); | |
| return null; | |
| } | |
| ins.filename = filename; | |
| ins.fileDiff = fileDiff; | |
| const summarizePrompt = prompts.renderSummarizeFileDiff(ins, options.reviewSimpleChanges); | |
| try { | |
| const isDocumentationOnly = (0, patch_utils_1.checkIfDocumentationOnly)(fileDiff); | |
| if (isDocumentationOnly && options.reviewSimpleChanges === false) { | |
| info(`summarize: skipping review for documentation-only change: ${filename}`); | |
| return [filename, 'Documentation/Comment changes only.', false]; | |
| } | |
| const promptTokens = (0, tokenizer_1.getTokenCount)(summarizePrompt); | |
| if (promptTokens > options.lightTokenLimits.requestTokens) { | |
| warning(`summarize: skipping ${filename} as it exceeds token limit (${promptTokens} > ${options.lightTokenLimits.requestTokens})`); | |
| return [ | |
| filename, | |
| 'File diff is too large for AI summarization. Please review manually.', | |
| false | |
| ]; | |
| } | |
| await token_scheduler_1.lightScheduler.wait(promptTokens); | |
| const [summarizeResp] = await lightBot.chat(summarizePrompt, {}); | |
| if (summarizeResp === '') { | |
| info('summarize: nothing obtained from AI'); | |
| summariesFailed.push(`${filename} (nothing obtained from AI)`); | |
| return null; | |
| } | |
| else { | |
| if (options.reviewSimpleChanges === false) { | |
| const triageRegex = /\[TRIAGE\]:\s*(NEEDS_REVIEW|APPROVED)/; | |
| const triageMatch = summarizeResp.match(triageRegex); | |
| if (triageMatch != null) { | |
| const triage = triageMatch[1]; | |
| const needsReview = triage === 'NEEDS_REVIEW'; | |
| const summary = summarizeResp.replace(triageRegex, '').trim(); | |
| info(`filename: ${filename}, triage: ${triage}`); | |
| return [filename, summary, needsReview]; | |
| } | |
| } | |
| return [filename, summarizeResp, true]; | |
| } | |
| } | |
| catch (e) { | |
| warning(`summarize: error from AI: ${e}`); | |
| summariesFailed.push(`${filename} (error from AI: ${e})})`); | |
| return null; | |
| } | |
| }; | |
| const summaryPromises = []; | |
| const skippedFiles = []; | |
| for (const [filename, fileContent, fileDiff] of filesAndChanges) { | |
| if (options.maxFiles <= 0 || summaryPromises.length < options.maxFiles) { | |
| summaryPromises.push(aiConcurrencyLimit(async () => await doSummary(filename, fileContent, fileDiff))); | |
| } | |
| else { | |
| skippedFiles.push(filename); | |
| } | |
| } | |
| const summaries = []; | |
| for (const promise of summaryPromises) { | |
| const result = await promise; | |
| if (result) | |
| summaries.push(result); | |
| } | |
| if (summaries.length > 0) { | |
| const batchSize = 10; | |
| for (let i = 0; i < summaries.length; i += batchSize) { | |
| const summariesBatch = summaries.slice(i, i + batchSize); | |
| for (const [filename, summary] of summariesBatch) { | |
| inputs.rawSummary += `--- | |
| ${filename}: ${summary} | |
| `; | |
| } | |
| const changesetPrompt = prompts.renderSummarizeChangesets(inputs); | |
| await token_scheduler_1.lightScheduler.wait((0, tokenizer_1.getTokenCount)(changesetPrompt)); | |
| const [summarizeResp] = await lightBot.chat(changesetPrompt, {}); | |
| if (summarizeResp === '') { | |
| warning('summarize: nothing obtained from AI'); | |
| } | |
| else { | |
| inputs.rawSummary = summarizeResp; | |
| } | |
| } | |
| } | |
| const impactMap = await generateImpactMap(filesAndChanges, project); | |
| inputs.description += `\n\n### Downstream Impact Analysis\n${impactMap}`; | |
| const finalSummarizePrompt = prompts.renderSummarize(inputs); | |
| await token_scheduler_1.heavyScheduler.wait((0, tokenizer_1.getTokenCount)(finalSummarizePrompt)); | |
| const [summarizeFinalResponse] = await heavyBot.chat(finalSummarizePrompt, {}); | |
| const shortSummarizePrompt = prompts.renderSummarizeShort(inputs); | |
| await token_scheduler_1.heavyScheduler.wait((0, tokenizer_1.getTokenCount)(shortSummarizePrompt)); | |
| const [summarizeShortResponse] = await heavyBot.chat(shortSummarizePrompt, {}); | |
| inputs.shortSummary = summarizeShortResponse; | |
| const verifiedSuggestions = []; | |
| const reviewsFailed = []; | |
| let lgtmCount = 0; | |
| let reviewCount = 0; | |
| const severityCounts = { critical: 0, major: 0, minor: 0, info: 0 }; | |
| const confidenceSum = { total: 0, count: 0 }; | |
| async function processReviewFinding(review, filename, ins, patches, project, verifiedSuggestions, severityCounts, confidenceStats) { | |
| let currentRemedy = review.remedy; | |
| if (currentRemedy) { | |
| let agentRetries = 0; | |
| const maxAgentRetries = 3; | |
| let isVerified = false; | |
| let lastError = ''; | |
| while (agentRetries < maxAgentRetries && !isVerified) { | |
| const feedback = await runCIFeedback(filename, currentRemedy, review.startLine, review.endLine); | |
| if (feedback === '' || !feedback.includes('❌')) { | |
| const astValid = (0, patch_utils_1.validateRemedyAST)(filename, currentRemedy, project); | |
| if (astValid) { | |
| isVerified = true; | |
| review.remedy = currentRemedy; | |
| } | |
| else { | |
| lastError = | |
| '❌ Hallucination Detected: Remedy refers to undefined local symbols.'; | |
| } | |
| } | |
| else { | |
| lastError = feedback; | |
| } | |
| if (!isVerified && agentRetries < maxAgentRetries - 1) { | |
| agentRetries++; | |
| const retryPrompt = prompts.renderContextAwareFixSuggestion(ins, currentRemedy, lastError); | |
| await token_scheduler_1.heavyScheduler.wait((0, tokenizer_1.getTokenCount)(retryPrompt)); | |
| const [retryResponse] = await heavyBot.chat(retryPrompt, {}); | |
| const { remedy: newRemedy } = (0, patch_utils_1.parseReview)(retryResponse, patches, filename)[0] || {}; | |
| if (newRemedy) | |
| currentRemedy = newRemedy; | |
| else | |
| break; | |
| } | |
| else | |
| break; | |
| } | |
| if (!isVerified) { | |
| delete review.remedy; | |
| review.verified = false; | |
| review.verificationFeedback = lastError; | |
| } | |
| else { | |
| ; | |
| review.verified = true; | |
| review.verificationFeedback = 'All verification checks passed'; | |
| } | |
| const finalRemedy = review.remedy; | |
| if (finalRemedy) { | |
| const hasCollision = verifiedSuggestions.some(s => s.filename === filename && | |
| ((review.startLine >= s.startLine && | |
| review.startLine <= s.endLine) || | |
| (review.endLine >= s.startLine && review.endLine <= s.endLine))); | |
| if (!hasCollision) { | |
| // Generate test case for verified remedy | |
| let testCase; | |
| if (isVerified && review.comment) { | |
| try { | |
| const testResult = await test_generator_1.testGenerator.generateTestCase(filename, review.comment, finalRemedy, heavyBot); | |
| if (testResult) { | |
| testCase = test_generator_1.testGenerator.formatTestSuggestion(testResult); | |
| } | |
| } | |
| catch (e) { | |
| info(`Test generation skipped: ${e}`); | |
| } | |
| } | |
| const entry = { | |
| filename, | |
| startLine: review.startLine, | |
| endLine: review.endLine, | |
| suggestion: finalRemedy, | |
| verified: isVerified, | |
| verificationFeedback: isVerified | |
| ? 'All verification checks passed' | |
| : lastError, | |
| testCase | |
| }; | |
| verifiedSuggestions.push(entry); | |
| } | |
| } | |
| } | |
| if (!options.reviewCommentLGTM && | |
| (review.comment.includes('LGTM') || | |
| review.comment.includes('looks good to me'))) { | |
| lgtmCount++; | |
| return; | |
| } | |
| if (review.severity) | |
| severityCounts[review.severity]++; | |
| if (review.confidence !== undefined) { | |
| confidenceStats.total += review.confidence; | |
| confidenceStats.count++; | |
| } | |
| reviewCount++; | |
| // Find test case if this review has a verified remedy | |
| const verifiedEntry = review.remedy | |
| ? verifiedSuggestions.find(s => s.filename === filename && | |
| s.startLine === review.startLine && | |
| s.endLine === review.endLine) | |
| : null; | |
| await commenter.bufferReviewComment(filename, review.startLine, review.endLine, review.comment, review.verified, review.verificationFeedback, verifiedEntry?.testCase); | |
| } | |
| const doReview = async (filename, fileContent, patches, project) => { | |
| const ins = new inputs_1.Inputs(); | |
| ins.title = inputs.title; | |
| ins.systemMessage = inputs.systemMessage; | |
| ins.shortSummary = inputs.shortSummary; | |
| ins.filename = filename; | |
| const usages = await findUsages(filename, fileContent, project); | |
| if (usages !== '') | |
| ins.description += `\n\n### Usage Context\n\`\`\`text\n${usages}\n\`\`\``; | |
| try { | |
| const lineContext = patches.length > 0 ? patches[0][0] : 1; | |
| const codeSnippet = patches.length > 0 ? patches[0][2] : ''; | |
| ins.remedyContext = symbol_graph_1.unifiedContextEngine.getRemedyContext(filename, lineContext, codeSnippet, 20); | |
| } | |
| catch (e) { | |
| ins.remedyContext = ''; | |
| } | |
| // Hunk-by-hunk / Sub-batching logic for large files | |
| let currentPatches = ''; | |
| let currentPatchesCount = 0; | |
| const basePrompt = prompts.renderReviewFileDiff(ins); | |
| const baseTokens = (0, tokenizer_1.getTokenCount)(basePrompt); | |
| for (let i = 0; i < patches.length; i++) { | |
| const [, , patch] = patches[i]; | |
| const patchTokens = (0, tokenizer_1.getTokenCount)(patch); | |
| const isLastPatch = i === patches.length - 1; | |
| // If a single patch is so large it exceeds the entire limit even by itself | |
| if (baseTokens + patchTokens > options.heavyTokenLimits.requestTokens) { | |
| warning(`review: hunk in ${filename} is too large (${patchTokens} tokens). Skipping this hunk.`); | |
| await commenter.bufferReviewComment(filename, patches[i][0], patches[i][1], '⚠️ **Hunk Too Large**: This specific change block is too large for the AI model. Please review this section manually.', false, 'Patch limit exceeded'); | |
| continue; | |
| } | |
| // If adding this patch exceeds the batch limit, process the current batch first | |
| if (currentPatches !== '' && | |
| baseTokens + (0, tokenizer_1.getTokenCount)(currentPatches + patch) > | |
| options.heavyTokenLimits.requestTokens) { | |
| await processBatch(currentPatches); | |
| currentPatches = ''; | |
| currentPatchesCount = 0; | |
| } | |
| currentPatches += `${patch}\n---\n`; | |
| currentPatchesCount++; | |
| // If it's the last patch, process whatever is left | |
| if (isLastPatch && currentPatches !== '') { | |
| await processBatch(currentPatches); | |
| } | |
| } | |
| async function processBatch(batchPatches) { | |
| ins.patches = batchPatches; | |
| const prompt = prompts.renderReviewFileDiff(ins); | |
| const totalTokens = (0, tokenizer_1.getTokenCount)(prompt); | |
| info(`reviewing ${filename} (sub-batch with ${currentPatchesCount} hunks, ${totalTokens} tokens)`); | |
| await token_scheduler_1.heavyScheduler.wait(totalTokens); | |
| try { | |
| const [response] = await heavyBot.chat(prompt, {}); | |
| const reviews = (0, patch_utils_1.parseReview)(response, patches, filename, options.debug); | |
| for (const review of reviews) { | |
| await processReviewFinding(review, filename, ins, patches, project, verifiedSuggestions, severityCounts, confidenceSum); | |
| } | |
| } | |
| catch (e) { | |
| error(`review: sub-batch failed for ${filename}: ${e.message}`); | |
| reviewsFailed.push(`${filename} (sub-batch error: ${e.message})`); | |
| } | |
| } | |
| }; | |
| const doBatchReview = async (batch, project) => { | |
| const ins = new inputs_1.Inputs(); | |
| ins.title = inputs.title; | |
| ins.systemMessage = inputs.systemMessage; | |
| ins.shortSummary = inputs.shortSummary; | |
| let batchContent = ''; | |
| for (const [filename, , patches] of batch) { | |
| batchContent += `### File: ${filename}\n`; | |
| for (const [, , patch] of patches) { | |
| batchContent += `${patch}\n`; | |
| } | |
| batchContent += `\n---\n`; | |
| } | |
| ins.batchContent = batchContent; | |
| const prompt = prompts.renderReviewFileBatch(ins); | |
| await token_scheduler_1.heavyScheduler.wait((0, tokenizer_1.getTokenCount)(prompt)); | |
| try { | |
| const [response] = await heavyBot.chat(prompt, {}); | |
| const fileResponses = response | |
| .split(/### File: /) | |
| .filter(s => s.trim() !== ''); | |
| for (const fileRes of fileResponses) { | |
| const lines = fileRes.split('\n'); | |
| const filename = lines[0].trim(); | |
| const content = fileRes.substring(fileRes.indexOf('\n') + 1); | |
| const item = batch.find(([f]) => f === filename); | |
| if (item) { | |
| const reviews = (0, patch_utils_1.parseReview)(content, item[2], filename, options.debug); | |
| for (const review of reviews) { | |
| await processReviewFinding(review, filename, ins, item[2], project, verifiedSuggestions, severityCounts, confidenceSum); | |
| } | |
| } | |
| } | |
| } | |
| catch (e) { | |
| warning(`Batch review failed: ${e.message}`); | |
| } | |
| }; | |
| if (!options.disableReview) { | |
| const filesAndChangesReview = filesAndChanges.filter(([filename]) => { | |
| return (summaries.find(([summaryFilename]) => summaryFilename === filename)?.[2] ?? | |
| true); | |
| }); | |
| const reviewBatches = []; | |
| let currentBatch = []; | |
| let currentBatchTokens = 0; | |
| const BATCH_LIMIT = 5000; | |
| for (const [filename, fileContent, , patches] of filesAndChangesReview) { | |
| // Logic for cost estimation: System message + PR info + Diff patches + 1k margin for context | |
| const diffStr = patches.map(([, , p]) => p).join('\n'); | |
| const fileTokens = (0, tokenizer_1.getTokenCount)(diffStr) + 1000; // 1000 for surrounding prompt/context | |
| if (fileTokens > BATCH_LIMIT || currentBatchTokens + fileTokens > BATCH_LIMIT) { | |
| if (currentBatch.length > 0) | |
| reviewBatches.push(currentBatch); | |
| currentBatch = [[filename, fileContent, patches]]; | |
| currentBatchTokens = fileTokens; | |
| } | |
| else { | |
| currentBatch.push([filename, fileContent, patches]); | |
| currentBatchTokens += fileTokens; | |
| } | |
| } | |
| if (currentBatch.length > 0) | |
| reviewBatches.push(currentBatch); | |
| for (const batch of reviewBatches) { | |
| if (batch.length === 1) { | |
| await doReview(batch[0][0], batch[0][1], batch[0][2], project); | |
| } | |
| else { | |
| await doBatchReview(batch, project); | |
| } | |
| } | |
| const commits = await commenter.getAllCommitIds(); | |
| // Build beautiful severity summary | |
| const totalIssues = severityCounts.critical + severityCounts.major + severityCounts.minor + severityCounts.info; | |
| const actionableCount = severityCounts.critical + severityCounts.major; | |
| const hasIssues = totalIssues > 0; | |
| // Status emoji based on issues found | |
| let statusEmoji = '✅'; | |
| let statusText = 'All Clear'; | |
| if (severityCounts.critical > 0) { | |
| statusEmoji = '🚨'; | |
| statusText = 'Critical Issues Found'; | |
| } | |
| else if (severityCounts.major > 0) { | |
| statusEmoji = '⚠️'; | |
| statusText = 'Issues Found'; | |
| } | |
| else if (severityCounts.minor > 0 || severityCounts.info > 0) { | |
| statusEmoji = '💡'; | |
| statusText = 'Suggestions Available'; | |
| } | |
| // CodeRabbit-style top summary | |
| let statusMsg = `## 📝 Actionable Comments | |
| **Actionable comments posted**: ${actionableCount} | |
| ${actionableCount > 0 ? ` | |
| <details> | |
| <summary>🔧 Fix all issues with AI Agents</summary> | |
| Each critical/major issue below includes a "Prompt for AI Agents" section. Copy-paste those prompts into Cursor, Windsurf, or any AI IDE to auto-fix the issues. | |
| </details> | |
| --- | |
| ` : '---\n\n'} | |
| ${prompts.renderSummarizeShort(inputs)} | |
| ${prompts.renderSummarizeReleaseNotes(inputs)} | |
| --- | |
| ## ${statusEmoji} PRIX Review Summary | |
| > **Status**: ${statusText} | |
| > **Total Findings**: ${totalIssues} issue${totalIssues !== 1 ? 's' : ''} | |
| ### Issue Breakdown | |
| | Severity | Count | Indicator | | |
| |:---------|:-----:|:----------| | |
| | 🚨 Critical | **${severityCounts.critical}** | ${severityCounts.critical > 0 ? '🔴 Attention Required' : '✓ None'} | | |
| | 🔴 Major | **${severityCounts.major}** | ${severityCounts.major > 0 ? '⚠️ Review Recommended' : '✓ None'} | | |
| | 🟠 Minor | **${severityCounts.minor}** | ${severityCounts.minor > 0 ? '💡 Suggestions' : '✓ None'} | | |
| | ℹ️ Info | **${severityCounts.info}** | ${severityCounts.info > 0 ? '📝 Notes' : '✓ None'} | | |
| `; | |
| // Add confidence score if we have reviews | |
| if (confidenceSum.count > 0) { | |
| const avgConfidence = Math.round(confidenceSum.total / confidenceSum.count); | |
| statusMsg += ` | |
| ### Review Quality | |
| - **Average Confidence**: ${avgConfidence}% | |
| - **Files Reviewed**: ${reviewCount} | |
| `; | |
| } | |
| // Add legend for quick understanding | |
| if (hasIssues) { | |
| statusMsg += ` | |
| --- | |
| <details> | |
| <summary>📖 Severity Legend</summary> | |
| - **Critical**: Security vulnerabilities, data loss risks, or crash-causing bugs | |
| - **Major**: Performance issues, significant bugs, or maintainability problems | |
| - **Minor**: Code style, minor optimizations, or documentation improvements | |
| - **Info**: General observations and notes | |
| </details> | |
| `; | |
| } | |
| if (options.createRemedyPR && verifiedSuggestions.length > 0) { | |
| await createRemedyPR(verifiedSuggestions, options); | |
| } | |
| await commenter.submitReview(exports.context.payload.pull_request.number, commits[commits.length - 1], statusMsg); | |
| } | |
| }; | |
| exports.codeReview = codeReview; | |
| async function generateImpactMap(files, project) { | |
| const filenames = files.map(([f]) => f); | |
| return symbol_graph_1.unifiedContextEngine.generateVisualImpactMap(filenames); | |
| } | |
| async function findUsages(f, c, p) { return ""; } | |
| async function runCIFeedback(f, r, s, e) { return ""; } | |
| async function createRemedyPR(verifiedSuggestions, options) { | |
| if (verifiedSuggestions.length === 0) | |
| return; | |
| const ctx = context_1.als.getStore(); | |
| if (!ctx) | |
| return; | |
| const pullRequest = ctx.probotContext.payload.pull_request; | |
| const owner = ctx.repo.owner; | |
| const repo = ctx.repo.repo; | |
| const headRef = pullRequest.head.ref; | |
| const pullNumber = pullRequest.number; | |
| const remedyBranch = `prix-remedy-pr-${pullNumber}-${Date.now()}`; | |
| info(`🚀 [RemedyEngine] Generating auto-fix branch: ${remedyBranch}`); | |
| try { | |
| const workingDir = ctx.workingDir || process.cwd(); | |
| // 1. Create a isolated branch from the current head | |
| (0, utils_1.prixExec)(`git checkout -b ${remedyBranch}`, { cwd: workingDir }); | |
| // 2. Apply verified fixes one by one | |
| let appliedCount = 0; | |
| for (const fix of verifiedSuggestions) { | |
| const filePath = (0, path_1.join)(workingDir, fix.filename); | |
| if ((0, fs_1.existsSync)(filePath)) { | |
| const content = (0, fs_1.readFileSync)(filePath, 'utf8').split('\n'); | |
| const startLine = fix.startLine; | |
| const endLine = fix.endLine; | |
| const suggestion = fix.suggestion; | |
| // Replace the lines (1-indexed adjust) | |
| content.splice(startLine - 1, endLine - startLine + 1, suggestion); | |
| (0, fs_1.writeFileSync)(filePath, content.join('\n'), 'utf8'); | |
| appliedCount++; | |
| } | |
| } | |
| if (appliedCount === 0) | |
| return; | |
| // 3. Commit and Push | |
| (0, utils_1.prixExec)(`git add .`, { cwd: workingDir }); | |
| (0, utils_1.prixExec)(`git commit -m "fix(remedy): automated audit fix by PRIX for #${pullNumber}"`, { cwd: workingDir }); | |
| (0, utils_1.prixExec)(`git push origin ${remedyBranch}`, { cwd: workingDir }); | |
| // 4. Use GitHub API to create the PR | |
| const prResponse = await octokit_1.octokit.rest.pulls.create({ | |
| owner, | |
| repo, | |
| title: `PRIX Remedies for PR #${pullNumber}`, | |
| body: `👋 This automated Pull Request corrects identified bugs from the PRIX audit of #${pullNumber}. | |
| ### Verified Fixes Applied: | |
| ${verifiedSuggestions | |
| .map(f => `- **${f.filename}** (L${f.startLine}-${f.endLine})`) | |
| .join('\n')} | |
| *Verified by Syntax & AST Validation.*`, | |
| head: remedyBranch, | |
| base: headRef | |
| }); | |
| info(`✅ [RemedyEngine] Created Remedy PR: ${prResponse.data.html_url}`); | |
| // 5. Post acknowledgement in the original PR | |
| await octokit_1.octokit.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| // eslint-disable-next-line camelcase | |
| issue_number: pullNumber, | |
| body: `🚨 **PRIX identified high-confidence bugfixes.** | |
| I have created a secondary Pull Request with ${appliedCount} suggested remedies: ${prResponse.data.html_url}` | |
| }); | |
| } | |
| catch (e) { | |
| error(`❌ [RemedyEngine] Failed to create Remedy PR: ${e.message}`); | |
| } | |
| } | |
| const run = async (probotContext, options, prompts) => { | |
| info = (msg) => probotContext.log.info(msg); | |
| warning = (msg) => probotContext.log.warn(msg); | |
| error = (msg) => probotContext.log.error(msg); | |
| (0, commenter_1.setCommenterContext)(probotContext); | |
| (0, octokit_1.setOctokit)(probotContext.octokit); | |
| const lb = new bot_1.Bot(options, new options_1.AIOptions(options.lightModel)); | |
| const hb = new bot_1.Bot(options, new options_1.AIOptions(options.heavyModel)); | |
| await (0, exports.codeReview)(lb, hb, options, prompts); | |
| }; | |
| exports.run = run; | |
| const autonomousAudit = async (probotContext, options, prompts) => { | |
| info = (msg) => probotContext.log.info(msg); | |
| warning = (msg) => probotContext.log.warn(msg); | |
| error = (msg) => probotContext.log.error(msg); | |
| (0, octokit_1.setOctokit)(probotContext.octokit); | |
| info('Starting autonomous audit...'); | |
| const store = context_1.als.getStore(); | |
| const workingDir = store?.workingDir || process.cwd(); | |
| // 1. Discover files | |
| const files = (0, file_discoverer_1.discoverFiles)(workingDir, { | |
| exclude: ['node_modules', '.git', 'dist', 'build'] | |
| }); | |
| // We could implement an AI review for all files, but to keep it simple and focused: | |
| // For the autonomous audit, we will select some files that might be problematic, | |
| // or we could review everything. For now, we simulate finding an issue or we just run test files. | |
| const verifiedSuggestions = []; | |
| // Example dummy logic: iterate over discovered files, maybe we run some simple regex to find easy bugs. | |
| // Real implementation would invoke heavyBot for dense scanning. | |
| if (options.enableAutoPR) { | |
| info(`enableAutoPR is true. Handling verified suggestions...`); | |
| if (verifiedSuggestions.length > 0) { | |
| const prService = new pr_service_1.PRService(workingDir); | |
| const branchName = `prix-auto-fix-${Date.now()}`; | |
| await prService.createFixBranch(branchName); | |
| let appliedCount = 0; | |
| for (const suggestion of verifiedSuggestions) { | |
| try { | |
| await prService.applyFix(suggestion.filename, suggestion.suggestion, suggestion.startLine, suggestion.endLine); | |
| await prService.commitAndPush(branchName, `fix: apply PRIX AI suggestion for ${suggestion.filename}`, suggestion.filename); | |
| appliedCount++; | |
| } | |
| catch (e) { | |
| warning(`Failed to apply fix for ${suggestion.filename}: ${e.message}`); | |
| } | |
| } | |
| if (appliedCount > 0) { | |
| await prService.submitPR(branchName, `🔧 PRIX Autonomous Fixes (${appliedCount})`, `This PR was automatically generated by PRIX autonomous auditor.`); | |
| } | |
| } | |
| else { | |
| info('No verified suggestions found during autonomous audit.'); | |
| } | |
| } | |
| else { | |
| info('Auto PR is disabled, not generating PRs.'); | |
| } | |
| }; | |
| exports.autonomousAudit = autonomousAudit; | |
| //# sourceMappingURL=review.js.map |