PRININIT / lib /src /autofix /api-context-extractor.js
yaminXwin's picture
feat: implement confidence-based comment routing, severity calibration, token budget truncation, and false-positive suppression rules
4d0586c
Raw
History Blame Contribute Delete
8.89 kB
"use strict";
/**
* API-based Context Extractor for AI Autofix
*
* Uses GitHub Contents API instead of local filesystem.
* No git clone required - faster, cheaper, more reliable.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.extractIssueContextAPI = extractIssueContextAPI;
exports.formatContextForLLMAPI = formatContextForLLMAPI;
const ts_morph_1 = require("ts-morph");
// 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
*/
async function extractIssueContextAPI(apiCheckout, location) {
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 = [];
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 = 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 = {
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, targetLine) {
try {
const project = new ts_morph_1.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() === ts_morph_1.SyntaxKind.FunctionDeclaration ||
a.getKind() === ts_morph_1.SyntaxKind.MethodDeclaration ||
a.getKind() === ts_morph_1.SyntaxKind.ClassDeclaration ||
a.getKind() === ts_morph_1.SyntaxKind.InterfaceDeclaration ||
a.getKind() === ts_morph_1.SyntaxKind.ArrowFunction ||
a.getKind() === ts_morph_1.SyntaxKind.FunctionExpression);
if (parent) {
let name = 'anonymous';
try {
if (parent.getName) {
name = parent.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, targetLine) {
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
*/
function formatContextForLLMAPI(context) {
const parts = [];
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');
}
//# sourceMappingURL=api-context-extractor.js.map