PRIX / lib /src /review.js
Rachit-Tw's picture
feat: implement autonomous Auto-PR and optimize Docker image
5cb7295
Raw
History Blame Contribute Delete
37.7 kB
"use strict";
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