PRININIT / src /autofix /api-context-extractor.ts
Rachit-Tw's picture
Upload 50 files
7d9309d verified
Raw
History Blame Contribute Delete
8.98 kB
/**
* 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<ExtractedContext | null> {
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<ExtractedContext['containingFunction']> {
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')
}