| "use strict"; |
| var __importDefault = (this && this.__importDefault) || function (mod) { |
| return (mod && mod.__esModule) ? mod : { "default": mod }; |
| }; |
| Object.defineProperty(exports, "__esModule", { value: true }); |
| exports.LocalContextEngine = void 0; |
| const fs_1 = require("fs"); |
| const child_process_1 = require("child_process"); |
| const inputs_1 = require("./inputs"); |
| const review_1 = require("./review"); |
| const confidence_1 = require("./confidence"); |
| const p_limit_1 = __importDefault(require("p-limit")); |
| const web_tree_sitter_1 = __importDefault(require("web-tree-sitter")); |
| class LocalContextEngine { |
| files; |
| project; |
| changes = new Map(); |
| constructor(files) { |
| this.files = files; |
| this.project = null; |
| } |
| async initialize() { |
| await this.loadChanges(); |
| this.initializeParser(); |
| } |
| async loadChanges() { |
| for (const file of this.files) { |
| if (!(0, fs_1.existsSync)(file)) |
| continue; |
| try { |
| const newContent = (0, fs_1.readFileSync)(file, 'utf8'); |
| let oldContent = ''; |
| let diff = ''; |
| try { |
| diff = (0, child_process_1.execSync)(`git diff HEAD -- "${file}"`, { |
| encoding: 'utf8', |
| cwd: process.cwd() |
| }); |
| const parentCommit = (0, child_process_1.execSync)('git rev-parse HEAD^', { |
| encoding: 'utf8', |
| cwd: process.cwd() |
| }).trim(); |
| try { |
| oldContent = (0, child_process_1.execSync)(`git show ${parentCommit}:${file}`, { |
| encoding: 'utf8', |
| cwd: process.cwd() |
| }); |
| } |
| catch { |
| oldContent = ''; |
| } |
| } |
| catch { |
| diff = ''; |
| oldContent = ''; |
| } |
| const hunks = this.parseDiff(diff); |
| this.changes.set(file, { |
| filename: file, |
| oldContent, |
| newContent, |
| diff, |
| hunks |
| }); |
| } |
| catch (e) { |
| console.warn(`Failed to load ${file}: ${e}`); |
| } |
| } |
| } |
| parseDiff(diff) { |
| const hunks = []; |
| const hunkRegex = /^@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/gm; |
| let match; |
| while ((match = hunkRegex.exec(diff)) !== null) { |
| const oldStart = parseInt(match[1], 10); |
| const oldLines = parseInt(match[2] || '1', 10); |
| const newStart = parseInt(match[3], 10); |
| const newLines = parseInt(match[4] || '1', 10); |
| const startIdx = match.index + match[0].length; |
| const nextHunkIdx = diff.indexOf('@@', startIdx); |
| const endIdx = nextHunkIdx === -1 ? diff.length : nextHunkIdx; |
| const content = diff.substring(startIdx, endIdx).trim(); |
| hunks.push({ |
| oldStart, |
| oldLines, |
| newStart, |
| newLines, |
| content |
| }); |
| } |
| return hunks; |
| } |
| initializeParser() { |
| try { |
| web_tree_sitter_1.default.init(); |
| } |
| catch (e) { |
| console.warn(`Failed to initialize tree-sitter: ${e}`); |
| } |
| } |
| async runReview(lightBot, heavyBot, options, prompts) { |
| const findings = []; |
| const concurrencyLimit = (0, p_limit_1.default)(options.concurrencyLimit); |
| const reviewPromises = Array.from(this.changes.entries()).map(([filename, change]) => concurrencyLimit(async () => { |
| return this.reviewFile(filename, change, lightBot, heavyBot, options, prompts); |
| })); |
| const fileResults = await Promise.all(reviewPromises); |
| for (const result of fileResults) { |
| findings.push(...result); |
| } |
| return findings; |
| } |
| async reviewFile(filename, change, lightBot, heavyBot, options, prompts) { |
| const findings = []; |
| const inputs = new inputs_1.Inputs(); |
| inputs.title = `Local review: ${filename}`; |
| inputs.description = ''; |
| inputs.systemMessage = options.systemMessage; |
| for (const hunk of change.hunks) { |
| const ins = inputs.clone(); |
| const patchContent = this.formatPatchForReview(filename, hunk, change.oldContent); |
| ins.fileDiff = patchContent; |
| ins.filename = filename; |
| ins.patches = hunk.content; |
| const prompt = prompts.renderReviewFileDiff(ins); |
| const [response] = await heavyBot.chat(prompt, {}); |
| if (response && response.trim()) { |
| const fileFindings = this.parseFindings(response, filename, hunk.newStart, hunk.newStart + hunk.newLines); |
| for (const finding of fileFindings) { |
| if (options.meetsMinimumSeverity(finding.severity)) { |
| if (options.meetsConfidenceThreshold(finding.confidence)) { |
| findings.push(finding); |
| } |
| } |
| } |
| } |
| } |
| return findings; |
| } |
| formatPatchForReview(filename, hunk, oldContent) { |
| const oldLines = oldContent.split('\n'); |
| const relevantOldLines = oldLines.slice(hunk.oldStart - 1, hunk.oldStart - 1 + hunk.oldLines); |
| const patchLines = hunk.content.split('\n'); |
| const newHunkLines = patchLines.filter(l => l.startsWith('+') && !l.startsWith('+++')); |
| const oldHunkLines = patchLines.filter(l => l.startsWith('-') && !l.startsWith('---')); |
| return `## File: ${filename} |
| @@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@ |
| |
| ---old_hunk--- |
| \`\`\` |
| ${oldHunkLines.join('\n') || '(no old lines)'} |
| \`\`\` |
| |
| ---new_hunk--- |
| \`\`\` |
| ${newHunkLines.join('\n') || '(no new lines)'} |
| \`\`\` |
| `; |
| } |
| parseFindings(response, filename, startLine, endLine) { |
| const findings = []; |
| const lines = response.split('\n'); |
| let currentFinding = null; |
| let currentMessage = ''; |
| const lineRangeRegex = /^(\d+)-(\d+):/; |
| const levelTypeRegex = /^(CRITICAL|MAJOR|MINOR|INFO)\s*\|\s*(\w+)/; |
| const confidenceRegex = /CONFIDENCE:\s*(\d+)%?$/i; |
| for (const line of lines) { |
| const lineRangeMatch = line.match(lineRangeRegex); |
| if (lineRangeMatch) { |
| if (currentFinding && currentFinding.message) { |
| const calibrated = confidence_1.confidenceCalibrator.calibrate(currentFinding.confidence || 50, currentFinding.message, currentFinding.message.length, 2000, currentFinding.severity, 'local', 'local', (0, review_1.extractPatternType)(currentFinding.message), filename); |
| findings.push({ |
| filename, |
| line: currentFinding.line || startLine, |
| endLine: currentFinding.endLine || endLine, |
| severity: currentFinding.severity || 'minor', |
| type: currentFinding.type || 'general', |
| message: currentFinding.message.trim(), |
| confidence: calibrated.score, |
| suggestion: currentFinding.suggestion, |
| patternType: (0, review_1.extractPatternType)(currentFinding.message) |
| }); |
| } |
| currentFinding = { |
| line: parseInt(lineRangeMatch[1], 10), |
| endLine: parseInt(lineRangeMatch[2], 10), |
| severity: 'minor', |
| type: 'general', |
| confidence: 50 |
| }; |
| currentMessage = ''; |
| continue; |
| } |
| const levelTypeMatch = line.match(levelTypeRegex); |
| if (levelTypeMatch && currentFinding) { |
| currentFinding.severity = |
| levelTypeMatch[1].toLowerCase(); |
| currentFinding.type = levelTypeMatch[2].toLowerCase(); |
| continue; |
| } |
| if (line.includes('```suggestion')) { |
| const suggestionMatch = response.match(/```suggestion\n([\s\S]*?)```/); |
| if (suggestionMatch && currentFinding) { |
| currentFinding.suggestion = suggestionMatch[1].trim(); |
| } |
| } |
| const confidenceMatch = line.match(confidenceRegex); |
| if (confidenceMatch && currentFinding) { |
| currentFinding.confidence = parseInt(confidenceMatch[1], 10); |
| } |
| if (currentFinding) { |
| currentMessage += `${line}\n`; |
| currentFinding.message = currentMessage; |
| } |
| } |
| if (currentFinding && currentFinding.message) { |
| const calibrated = confidence_1.confidenceCalibrator.calibrate(currentFinding.confidence || 50, currentFinding.message, currentFinding.message.length, 2000, currentFinding.severity, 'local', 'local', (0, review_1.extractPatternType)(currentFinding.message), filename); |
| findings.push({ |
| filename, |
| line: currentFinding.line || startLine, |
| endLine: currentFinding.endLine || endLine, |
| severity: currentFinding.severity || 'minor', |
| type: currentFinding.type || 'general', |
| message: currentFinding.message.trim(), |
| confidence: calibrated.score, |
| suggestion: currentFinding.suggestion, |
| patternType: (0, review_1.extractPatternType)(currentFinding.message) |
| }); |
| } |
| return findings; |
| } |
| } |
| exports.LocalContextEngine = LocalContextEngine; |
| |