/** * API-based Context Extractor for AI Autofix * * Uses GitHub Contents API instead of local filesystem. * No git clone required - faster, cheaper, more reliable. */ import {ApiCheckout, FileInfo} from '../api-checkout' import {Project, SyntaxKind} from 'ts-morph' export interface IssueLocation { filename: string startLine: number endLine: number } export interface ExtractedContext { filename: string targetLines: string surroundingContext: string containingFunction?: { name: string startLine: number endLine: number code: string } imports?: string[] language: string } // Configuration for context window sizes const CONTEXT_CONFIG = { // Lines before/after the issue SURROUNDING_LINES: 40, // Max lines for containing function (if we can identify it) MAX_FUNCTION_LINES: 200, // Max total context size (approximate token limit) MAX_CONTEXT_CHARS: 4000 } /** * Extract localized context for an issue using GitHub API * Returns minimal but sufficient context for LLM to generate a fix */ export async function extractIssueContextAPI( apiCheckout: ApiCheckout, location: IssueLocation ): Promise { try { // Get file content via API const file = await apiCheckout.getFile(location.filename) const lines = file.content.split('\n') // Detect language from file extension const language = location.filename.endsWith('.ts') ? 'typescript' : location.filename.endsWith('.js') ? 'javascript' : location.filename.endsWith('.py') ? 'python' : 'unknown' // Extract target lines const startIdx = Math.max(0, location.startLine - 1) const endIdx = Math.min(lines.length, location.endLine) const targetLines = lines.slice(startIdx, endIdx).join('\n') // Extract surrounding context const contextStart = Math.max( 0, location.startLine - CONTEXT_CONFIG.SURROUNDING_LINES - 1 ) const contextEnd = Math.min( lines.length, location.endLine + CONTEXT_CONFIG.SURROUNDING_LINES ) const surroundingLines = lines.slice(contextStart, contextEnd) const surroundingContext = surroundingLines.join('\n') // Extract imports (first 50 lines) const importLines: string[] = [] for (let i = 0; i < Math.min(50, lines.length); i++) { const line = lines[i].trim() if ( line.startsWith('import ') || line.startsWith('from ') || line.startsWith('require(') ) { importLines.push(lines[i]) } } // Try to find containing function for context-aware autofix let containingFunction: ExtractedContext['containingFunction'] = undefined if (language === 'typescript' || language === 'javascript') { containingFunction = await findContainingFunctionAPI( file.content, location.startLine ) } else if (language === 'python') { containingFunction = findContainingFunctionPython( lines, location.startLine ) } // Build context object const context: ExtractedContext = { filename: location.filename, targetLines, surroundingContext, containingFunction, imports: importLines, language } // Truncate if too large if (surroundingContext.length > CONTEXT_CONFIG.MAX_CONTEXT_CHARS) { console.log( `[CONTEXT] Truncating context from ${surroundingContext.length} to ${CONTEXT_CONFIG.MAX_CONTEXT_CHARS} chars` ) context.surroundingContext = surroundingContext.substring( 0, CONTEXT_CONFIG.MAX_CONTEXT_CHARS ) } return context } catch (error) { console.error( `[CONTEXT] Failed to extract context for ${location.filename}:`, error ) return null } } /** * Find containing function using ts-morph (API version) */ async function findContainingFunctionAPI( content: string, targetLine: number ): Promise { try { const project = new Project({useInMemoryFileSystem: true}) const sourceFile = project.createSourceFile('temp.ts', content) // Convert 1-based line to character position const lineStartPos = sourceFile.compilerNode.getPositionOfLineAndCharacter( targetLine - 1, 0 ) const node = sourceFile.getDescendantAtPos(lineStartPos) if (!node) return undefined // Find ancestor that is a function, method, class, or interface const parent = node.getFirstAncestor( a => a.getKind() === SyntaxKind.FunctionDeclaration || a.getKind() === SyntaxKind.MethodDeclaration || a.getKind() === SyntaxKind.ClassDeclaration || a.getKind() === SyntaxKind.InterfaceDeclaration || a.getKind() === SyntaxKind.ArrowFunction || a.getKind() === SyntaxKind.FunctionExpression ) if (parent) { let name = 'anonymous' try { if ((parent as any).getName) { name = (parent as any).getName() || 'anonymous' } } catch { // Some nodes don't have getName } const startLine = parent.getStartLineNumber() const endLine = parent.getEndLineNumber() const code = parent.getText() // Limit function size to avoid token overflow if (endLine - startLine > CONTEXT_CONFIG.MAX_FUNCTION_LINES) { return undefined } return { name, startLine, endLine, code } } else { console.log(`[CONTEXT] No function ancestor found at line ${targetLine}`) } } catch (e) { console.error( `[CONTEXT] ts-morph parsing failed for file (content length: ${content.length}): ${e}` ) } return undefined } /** * Find containing function in Python using regex + indentation * Ported from local context-extractor.ts for API-based extraction */ function findContainingFunctionPython( lines: string[], targetLine: number ): ExtractedContext['containingFunction'] { const pyRegex = /^\s*(?:async\s+)?def\s+(\w+)/ let functionStart = -1 let functionName = '' // Search backwards from target line to find function definition for (let i = targetLine - 1; i >= 0; i--) { const match = lines[i].match(pyRegex) if (match) { functionStart = i functionName = match[1] || 'anonymous' break } } if (functionStart === -1) return undefined // Find function end using indentation const baseIndent = lines[functionStart].match(/^(\s*)/)?.[1]?.length || 0 const maxLines = Math.min( lines.length, functionStart + CONTEXT_CONFIG.MAX_FUNCTION_LINES ) for (let j = functionStart + 1; j < maxLines; j++) { const checkLine = lines[j] // Skip empty lines and comments if (checkLine.trim() === '' || checkLine.trim().startsWith('#')) continue const lineIndent = checkLine.match(/^(\s*)/)?.[1]?.length || 0 // Function ends when we hit same or lower indentation (non-empty, non-comment) if (lineIndent <= baseIndent && checkLine.trim()) { const functionCode = lines.slice(functionStart, j).join('\n') return { name: functionName, startLine: functionStart + 1, endLine: j, code: functionCode } } } // If we hit end of file, return everything from function start const functionCode = lines.slice(functionStart).join('\n') return { name: functionName, startLine: functionStart + 1, endLine: lines.length, code: functionCode } } /** * Format context for LLM consumption */ export function formatContextForLLMAPI(context: ExtractedContext): string { const parts: string[] = [] parts.push(`FILE: ${context.filename}`) parts.push(`LANGUAGE: ${context.language}`) parts.push('') if (context.imports && context.imports.length > 0) { parts.push('IMPORTS:') context.imports.forEach(imp => parts.push(imp)) parts.push('') } if (context.containingFunction) { parts.push('CONTAINING FUNCTION:') parts.push(`Name: ${context.containingFunction.name}`) parts.push( `Lines: ${context.containingFunction.startLine}-${context.containingFunction.endLine}` ) parts.push('') parts.push('```' + context.language) parts.push(context.containingFunction.code) parts.push('```') parts.push('') } parts.push('TARGET CODE (lines with issue):') parts.push('```' + context.language) parts.push(context.targetLines) parts.push('```') parts.push('') parts.push('SURROUNDING CONTEXT:') parts.push('```' + context.language) parts.push(context.surroundingContext) parts.push('```') return parts.join('\n') }