import {readFileSync, existsSync} from 'fs' import {execSync} from 'child_process' import {Options} from './options' import {Prompts} from './prompts' import {Bot} from './bot' import {Inputs} from './inputs' import {codeReview} from './review' import {extractPatternType} from './utils/patch-utils' import {confidenceCalibrator} from './confidence' import {als} from './context' import {feedbackTracker} from './feedback-tracker' import pLimit from 'p-limit' import Parser from 'web-tree-sitter' export interface LocalFinding { filename: string line: number endLine: number severity: 'critical' | 'major' | 'minor' | 'info' type: string message: string confidence: number suggestion?: string patternType: string } interface LocalFileChange { filename: string oldContent: string newContent: string diff: string hunks: Array<{ oldStart: number oldLines: number newStart: number newLines: number content: string }> } export class LocalContextEngine { private files: string[] private project: any private changes: Map = new Map() constructor(files: string[]) { this.files = files this.project = null } async initialize(): Promise { await this.loadChanges() this.initializeParser() } private async loadChanges(): Promise { for (const file of this.files) { if (!existsSync(file)) continue try { const newContent = readFileSync(file, 'utf8') let oldContent = '' let diff = '' try { diff = execSync(`git diff HEAD -- "${file}"`, { encoding: 'utf8', cwd: process.cwd() }) const parentCommit = execSync('git rev-parse HEAD^', { encoding: 'utf8', cwd: process.cwd() }).trim() try { oldContent = 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}`) } } } private parseDiff(diff: string): LocalFileChange['hunks'] { const hunks: LocalFileChange['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 } private initializeParser(): void { try { Parser.init() } catch (e) { console.warn(`Failed to initialize tree-sitter: ${e}`) } } async runReview( lightBot: Bot, heavyBot: Bot, options: Options, prompts: Prompts ): Promise { const findings: LocalFinding[] = [] const concurrencyLimit = pLimit(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 } private async reviewFile( filename: string, change: LocalFileChange, lightBot: Bot, heavyBot: Bot, options: Options, prompts: Prompts ): Promise { const findings: LocalFinding[] = [] const inputs = new 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 } private formatPatchForReview( filename: string, hunk: LocalFileChange['hunks'][0], oldContent: string ): string { 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)'} \`\`\` ` } private parseFindings( response: string, filename: string, startLine: number, endLine: number ): LocalFinding[] { const findings: LocalFinding[] = [] const lines = response.split('\n') let currentFinding: Partial | null = 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 = confidenceCalibrator.calibrate( currentFinding.confidence || 50, currentFinding.message, currentFinding.message.length, 2000, currentFinding.severity, 'local', 'local', 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: 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() as LocalFinding['severity'] 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 = confidenceCalibrator.calibrate( currentFinding.confidence || 50, currentFinding.message, currentFinding.message.length, 2000, currentFinding.severity, 'local', 'local', 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: extractPatternType(currentFinding.message) }) } return findings } }