bugger-mcp
Version:
MCP Server for managing bugs, feature requests, and improvements
1,082 lines (923 loc) • 37 kB
text/typescript
// Context collection engine that orchestrates pattern matching and dependency analysis
import { TreeSitterCodeAnalyzer, FunctionMatch, ClassMatch, PatternMatch } from './treesitter-code-analyzer.js';
import { DependencyAnalyzer, DependencyGraph, FileRelationship } from './dependency-analysis.js';
import { StackTraceParser, ParsedStackTrace, StackTraceContext } from './stack-trace-parser.js';
import * as fs from 'fs';
import * as path from 'path';
export type TaskType = 'bug' | 'improvement';
export interface CodeContext {
id: string;
taskId: string;
taskType: TaskType;
contextType: 'snippet' | 'file_reference' | 'dependency' | 'pattern';
source: 'ai_collected' | 'manual';
filePath: string;
startLine?: number;
endLine?: number;
content?: string;
description: string;
relevanceScore: number;
keywords: string[];
dateCollected: string;
dateLastChecked?: string;
isStale?: boolean;
}
export interface CodeSection {
filePath: string;
startLine: number;
endLine: number;
content: string;
relevanceScore: number;
contextType: 'function' | 'class' | 'import' | 'usage' | 'comment';
relatedEntities: string[];
}
export interface ContextCollectionConfig {
maxContextsPerTask: number;
relevanceThreshold: number;
maxFileSize: number;
excludePatterns: string[];
includeExtensions: string[];
cacheExpiryHours: number;
enableStalenessTracking: boolean;
enablePatternMatching: boolean;
enableDependencyAnalysis: boolean;
// Token optimization settings
maxTokensPerTask: number;
maxTokensPerContext: number;
enableIntelligentSummarization: boolean;
enableContentDeduplication: boolean;
compressionThreshold: number;
taskTypeTokenLimits: {
bug: number;
improvement: number;
};
contextScoringWeights: {
keywordMatch: number;
entityMatch: number;
intentMatch: number;
patternSimilarity: number;
dependencyStrength: number;
fileProximity: number;
};
}
export interface ContextCollectionResult {
contexts: CodeContext[];
stackTraces?: ParsedStackTrace[];
summary: {
totalContexts: number;
highRelevanceContexts: number;
mediumRelevanceContexts: number;
lowRelevanceContexts: number;
averageRelevanceScore: number;
processingTimeMs: number;
filesAnalyzed: number;
patternsFound: number;
dependenciesAnalyzed: number;
stackTracesFound: number;
};
recommendations: string[];
potentialIssues: string[];
}
export interface TaskAnalysisInput {
taskId: string;
taskType: TaskType;
title: string;
description: string;
currentState?: string;
desiredState?: string;
expectedBehavior?: string;
actualBehavior?: string;
filesLikelyInvolved?: string[];
keywords?: string[];
entities?: string[];
}
/**
* Main context collection engine that orchestrates all analysis components
*/
export class ContextCollectionEngine {
private codeAnalyzer: TreeSitterCodeAnalyzer;
private dependencyAnalyzer: DependencyAnalyzer;
private config: ContextCollectionConfig;
private contextCache: Map<string, CodeContext[]> = new Map();
private allowedRoot: string;
constructor(
rootPath: string = process.cwd(),
config: Partial<ContextCollectionConfig> = {}
) {
const resolvedRoot = path.resolve(process.env.CONTEXT_ROOT || rootPath || process.cwd());
this.allowedRoot = resolvedRoot;
this.codeAnalyzer = new TreeSitterCodeAnalyzer(resolvedRoot);
this.dependencyAnalyzer = new DependencyAnalyzer(resolvedRoot);
this.config = this.mergeWithDefaultConfig(config);
}
/**
* Collect contexts for a task using all available analysis methods
*/
public async collectContexts(input: TaskAnalysisInput): Promise<ContextCollectionResult> {
const startTime = Date.now();
try {
// Step 1: Analyze task description using text analysis
const textAnalysis = await this.analyzeTaskText(input);
// Step 2: Parse stack traces (especially useful for bug reports)
const stackTraces = input.taskType === 'bug' ?
await this.parseStackTraces(textAnalysis.combinedText) : [];
// Step 3: Find relevant code patterns
const patternMatches = this.config.enablePatternMatching ?
await this.findRelevantPatterns(textAnalysis) : [];
// Step 4: Analyze dependencies
const dependencyInfo = this.config.enableDependencyAnalysis ?
await this.analyzeDependencies(input.filesLikelyInvolved || []) : null;
// Step 5: Extract code sections (including stack trace contexts)
const codeSections = await this.extractCodeSections(input, textAnalysis, patternMatches, dependencyInfo, stackTraces);
// Step 6: Score and rank contexts
const scoredContexts = await this.scoreAndRankContexts(codeSections, textAnalysis, input);
// Step 7: Filter and limit contexts
const filteredContexts = this.filterContexts(scoredContexts);
// Step 8: Convert to CodeContext objects
let contexts = await this.convertToCodeContexts(filteredContexts, input);
// Step 9: Apply token optimizations
contexts = this.deduplicateContexts(contexts);
contexts = this.applyTokenFiltering(contexts, input.taskType);
// Step 10: Generate summary and recommendations
const summary = this.generateSummary(contexts, startTime, patternMatches, dependencyInfo, stackTraces);
const recommendations = this.generateRecommendations(contexts, textAnalysis, patternMatches, stackTraces);
const potentialIssues = this.identifyPotentialIssues(contexts, dependencyInfo);
// Step 11: Cache results
this.cacheResults(input.taskId, contexts);
const result: ContextCollectionResult = {
contexts,
summary,
recommendations,
potentialIssues
};
if (stackTraces.length > 0) {
result.stackTraces = stackTraces;
}
return result;
} catch (error) {
console.error('Error in context collection:', error);
throw error;
}
}
/**
* Get cached contexts for a task
*/
public getCachedContexts(taskId: string): CodeContext[] | null {
const cached = this.contextCache.get(taskId);
if (cached) {
// Check if cache is still valid
const cacheAge = Date.now() - new Date(cached[0]?.dateCollected || 0).getTime();
const maxAge = this.config.cacheExpiryHours * 60 * 60 * 1000;
if (cacheAge < maxAge) {
return cached;
} else {
// Remove expired cache
this.contextCache.delete(taskId);
}
}
return null;
}
/**
* Update configuration
*/
public updateConfig(newConfig: Partial<ContextCollectionConfig>): void {
this.config = { ...this.config, ...newConfig };
}
/**
* Clear all caches
*/
public clearCaches(): void {
this.contextCache.clear();
// Cache cleared
}
// Private methods
/**
* Parse stack traces from task description text
*/
private async parseStackTraces(combinedText: string): Promise<ParsedStackTrace[]> {
try {
if (!StackTraceParser.containsStackTrace(combinedText)) {
return [];
}
const stackTraces = StackTraceParser.extractStackTraces(combinedText);
// Filter for valid, high-confidence stack traces
return stackTraces.filter(trace => trace.isValid && trace.confidence > 0.5);
} catch (error) {
console.error('Error parsing stack traces:', error);
return [];
}
}
/**
* Extract stack trace contexts for code collection
*/
private async extractStackTraceContexts(stackTraces: ParsedStackTrace[]): Promise<CodeSection[]> {
const sections: CodeSection[] = [];
for (const stackTrace of stackTraces) {
const contexts = StackTraceParser.extractStackTraceContexts(stackTrace);
for (const context of contexts) {
// Check if file exists and is readable within allowed root
const resolvedStackPath = this.safeResolve(context.filePath);
if (resolvedStackPath && fs.existsSync(resolvedStackPath)) {
try {
const content = await this.readFileSection(
resolvedStackPath,
Math.max(1, context.lineNumber - context.contextLines),
context.lineNumber + context.contextLines
);
if (content) {
const relevanceScore = this.calculateStackTraceRelevance(context.priority, stackTrace.confidence);
sections.push({
filePath: resolvedStackPath,
startLine: Math.max(1, context.lineNumber - context.contextLines),
endLine: context.lineNumber + context.contextLines,
content,
relevanceScore,
contextType: 'function',
relatedEntities: context.functionName ? [context.functionName] : []
});
}
} catch (error) {
console.error(`Error reading stack trace context from ${resolvedStackPath}:`, error);
}
}
}
}
return sections;
}
/**
* Calculate relevance score for stack trace contexts
*/
private calculateStackTraceRelevance(priority: 'high' | 'medium' | 'low', confidence: number): number {
const priorityScores = {
high: 0.9,
medium: 0.7,
low: 0.5
};
return Math.min(1.0, priorityScores[priority] * confidence);
}
/**
* Estimate token count for text content
*/
private estimateTokenCount(text: string): number {
// Rough estimation: ~4 characters per token for English text
return Math.ceil(text.length / 4);
}
/**
* Intelligently summarize content to reduce token usage
*/
private summarizeContent(content: string, maxTokens: number): string {
const estimatedTokens = this.estimateTokenCount(content);
if (estimatedTokens <= maxTokens) {
return content;
}
// Target character count based on token limit
const targetChars = maxTokens * 4;
// If content is code, try to keep complete lines
const lines = content.split('\n');
let result = '';
let currentLength = 0;
for (const line of lines) {
if (currentLength + line.length + 1 > targetChars) {
// Try to break at a natural point
const remainingChars = targetChars - currentLength;
if (remainingChars > 20) {
result += line.substring(0, remainingChars - 5) + '...';
}
break;
}
result += line + '\n';
currentLength += line.length + 1;
}
return result.trim();
}
/**
* Deduplicate similar contexts based on content similarity
*/
private deduplicateContexts(contexts: CodeContext[]): CodeContext[] {
if (!this.config.enableContentDeduplication) {
return contexts;
}
const unique: CodeContext[] = [];
const seen = new Set<string>();
for (const context of contexts) {
// Create a hash of the content for deduplication
const contentHash = this.createContentHash(context.content || '');
if (!seen.has(contentHash)) {
seen.add(contentHash);
unique.push(context);
} else {
// If duplicate, merge keywords and update relevance score
const existing = unique.find(c => this.createContentHash(c.content || '') === contentHash);
if (existing) {
existing.keywords = [...new Set([...existing.keywords, ...context.keywords])];
existing.relevanceScore = Math.max(existing.relevanceScore, context.relevanceScore);
}
}
}
return unique;
}
/**
* Create a hash of content for deduplication
*/
private createContentHash(content: string): string {
// Simple hash based on normalized content
const normalized = content.replace(/\s+/g, ' ').trim().toLowerCase();
return normalized.substring(0, 200); // Use first 200 chars as hash
}
/**
* Apply token-aware filtering to contexts
*/
private applyTokenFiltering(contexts: CodeContext[], taskType: TaskType): CodeContext[] {
let totalTokens = 0;
const filtered: CodeContext[] = [];
// Get task-specific token limit
const taskTokenLimit = this.config.taskTypeTokenLimits[taskType] || this.config.maxTokensPerTask;
// Sort by relevance score (highest first)
const sorted = contexts.sort((a, b) => b.relevanceScore - a.relevanceScore);
for (const context of sorted) {
const contextTokens = this.estimateTokenCount(context.content || '');
if (totalTokens + contextTokens <= taskTokenLimit) {
// Apply summarization if context exceeds per-context limit
if (contextTokens > this.config.maxTokensPerContext) {
context.content = this.summarizeContent(context.content || '', this.config.maxTokensPerContext);
context.description += ' (summarized)';
}
filtered.push(context);
totalTokens += this.estimateTokenCount(context.content || '');
}
}
return filtered;
}
private async analyzeTaskText(input: TaskAnalysisInput): Promise<{
keywords: string[];
entities: string[];
combinedText: string;
}> {
// Combine all text fields for analysis
const textFields = [
input.title,
input.description,
input.currentState,
input.desiredState,
input.expectedBehavior,
input.actualBehavior
].filter(Boolean);
const combinedText = textFields.join(' ');
// Use provided keywords and entities, or extract simple ones from input
const keywords = input.keywords || this.extractSimpleKeywords(combinedText);
const entities = input.entities || this.extractSimpleEntities(combinedText);
return {
keywords,
entities,
combinedText
};
}
/**
* Simple keyword extraction (basic fallback when AI analysis isn't available)
*/
private extractSimpleKeywords(text: string): string[] {
if (!text || typeof text !== 'string') {
return [];
}
const words = text.toLowerCase()
.replace(/[^\w\s]/g, ' ')
.split(/\s+/)
.filter(word => word.length > 3)
.filter(word => !this.isCommonWord(word));
// Return unique words, limited to top 10
return Array.from(new Set(words)).slice(0, 10);
}
/**
* Simple entity extraction (basic fallback when AI analysis isn't available)
*/
private extractSimpleEntities(text: string): string[] {
if (!text || typeof text !== 'string') {
return [];
}
const entities: string[] = [];
// Extract file paths
const fileMatches = text.match(/[a-zA-Z0-9_\-]+\.(js|ts|jsx|tsx|py|java|rb|php|go|cs|html|css|json)/g);
if (fileMatches) entities.push(...fileMatches);
// Extract function-like patterns
const functionMatches = text.match(/[a-zA-Z_$][a-zA-Z0-9_$]*\s*\(/g);
if (functionMatches) {
entities.push(...functionMatches.map(m => m.replace(/\s*\($/, '')));
}
// Extract camelCase/PascalCase identifiers
const identifierMatches = text.match(/\b[a-z][a-zA-Z0-9]*[A-Z][a-zA-Z0-9]*\b/g);
if (identifierMatches) entities.push(...identifierMatches);
return Array.from(new Set(entities)).slice(0, 15);
}
/**
* Check if a word is a common English word
*/
private isCommonWord(word: string): boolean {
const commonWords = new Set([
'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by',
'from', 'up', 'about', 'into', 'over', 'after', 'this', 'that', 'these', 'those',
'they', 'them', 'their', 'there', 'then', 'than', 'when', 'where', 'why', 'how',
'what', 'which', 'who', 'will', 'would', 'could', 'should', 'might', 'must',
'have', 'has', 'had', 'been', 'being', 'are', 'was', 'were', 'is', 'am'
]);
return commonWords.has(word);
}
private async findRelevantPatterns(textAnalysis: {
keywords: string[];
entities: string[];
combinedText: string;
}): Promise<{
functions: FunctionMatch[];
classes: ClassMatch[];
patterns: PatternMatch[];
}> {
const functions: FunctionMatch[] = [];
const classes: ClassMatch[] = [];
const patterns: PatternMatch[] = [];
// Find functions mentioned in entities (entities are now just strings)
for (const entity of textAnalysis.entities) {
// Check if entity looks like a function (contains parentheses or common function patterns)
if (entity.includes('(') || /^[a-z][a-zA-Z0-9]*$/.test(entity)) {
const functionMatches = await this.codeAnalyzer.findFunctionDefinitions(entity.replace(/\s*\(.*$/, ''));
functions.push(...functionMatches);
}
// Check if entity looks like a class (starts with capital letter)
if (/^[A-Z][a-zA-Z0-9]*$/.test(entity)) {
const classMatches = await this.codeAnalyzer.findClassDefinitions(entity);
classes.push(...classMatches);
}
}
// Find similar patterns based on combined text
if (textAnalysis.combinedText.length > 50) {
const similarPatterns = await this.codeAnalyzer.findSimilarPatterns(textAnalysis.combinedText);
patterns.push(...similarPatterns);
}
return { functions, classes, patterns };
}
private async analyzeDependencies(filesLikelyInvolved: string[]): Promise<{
graph: DependencyGraph;
fileRelationships: Map<string, FileRelationship>;
} | null> {
try {
const graph = await this.dependencyAnalyzer.buildDependencyGraph();
const fileRelationships = new Map<string, FileRelationship>();
// Get relationships for files likely involved
for (const file of filesLikelyInvolved) {
const resolved = this.safeResolve(file);
if (resolved && fs.existsSync(resolved)) {
const relationship = await this.dependencyAnalyzer.mapFileRelationships(resolved);
fileRelationships.set(resolved, relationship);
}
}
return { graph, fileRelationships };
} catch (error) {
console.error('Error analyzing dependencies:', error);
return null;
}
}
private async extractCodeSections(
input: TaskAnalysisInput,
textAnalysis: any,
patternMatches: any,
dependencyInfo: any,
stackTraces: ParsedStackTrace[]
): Promise<CodeSection[]> {
const sections: CodeSection[] = [];
// Extract sections from stack traces (highest priority for bugs)
if (stackTraces.length > 0) {
const stackTraceSections = await this.extractStackTraceContexts(stackTraces);
sections.push(...stackTraceSections);
}
// Extract sections from pattern matches
for (const funcMatch of patternMatches.functions || []) {
const content = await this.readFileSection(funcMatch.filePath, funcMatch.startLine, funcMatch.endLine);
if (content) {
sections.push({
filePath: funcMatch.filePath,
startLine: funcMatch.startLine,
endLine: funcMatch.endLine,
content,
relevanceScore: funcMatch.relevanceScore,
contextType: 'function',
relatedEntities: [funcMatch.name]
});
}
}
for (const classMatch of patternMatches.classes || []) {
const content = await this.readFileSection(classMatch.filePath, classMatch.startLine, classMatch.endLine);
if (content) {
sections.push({
filePath: classMatch.filePath,
startLine: classMatch.startLine,
endLine: classMatch.endLine,
content,
relevanceScore: classMatch.relevanceScore,
contextType: 'class',
relatedEntities: [classMatch.name, ...classMatch.methods]
});
}
}
// Extract sections from files likely involved
for (const file of input.filesLikelyInvolved || []) {
const relevantSections = await this.extractRelevantSectionsFromFile(file, textAnalysis);
sections.push(...relevantSections);
}
// Extract sections from dependency analysis
if (dependencyInfo) {
const importSections = await this.extractImportSections(dependencyInfo);
sections.push(...importSections);
}
return sections;
}
private async readFileSection(filePath: string, startLine: number, endLine: number): Promise<string | null> {
try {
const resolved = this.safeResolve(filePath);
if (!resolved) return null;
const stat = fs.statSync(resolved);
if (stat.size > this.config.maxFileSize) return null;
const content = fs.readFileSync(resolved, 'utf8');
const lines = content.split('\n');
return lines.slice(startLine - 1, endLine).join('\n');
} catch (error) {
console.error(`Error reading file section ${filePath}:`, error);
return null;
}
}
private async extractRelevantSectionsFromFile(
filePath: string,
textAnalysis: any
): Promise<CodeSection[]> {
const sections: CodeSection[] = [];
try {
const resolved = this.safeResolve(filePath);
if (!resolved) return sections;
const stat = fs.statSync(resolved);
if (stat.size > this.config.maxFileSize) return sections;
const content = fs.readFileSync(resolved, 'utf8');
const lines = content.split('\n');
// Find lines that contain keywords or entities
const relevantLines: { line: number; content: string; score: number }[] = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
let score = 0;
// Score based on keyword matches
for (const keyword of textAnalysis.keywords || []) {
if (keyword && typeof keyword === 'string' && line.toLowerCase().includes(keyword.toLowerCase())) {
score += 1; // Simple scoring since we don't have keyword.score anymore
}
}
// Score based on entity matches
for (const entity of textAnalysis.entities || []) {
if (entity && typeof entity === 'string' && line.includes(entity)) {
score += 2; // Entities get higher score
}
}
if (score > 0) {
relevantLines.push({ line: i + 1, content: line, score });
}
}
// Group relevant lines into sections
const contextSize = 5; // Lines before and after
const groupedSections = this.groupRelevantLines(relevantLines, contextSize);
for (const group of groupedSections) {
const startLine = Math.max(1, group.startLine - contextSize);
const endLine = Math.min(lines.length, group.endLine + contextSize);
const sectionContent = lines.slice(startLine - 1, endLine).join('\n');
sections.push({
filePath: resolved,
startLine,
endLine,
content: sectionContent,
relevanceScore: group.averageScore,
contextType: 'usage',
relatedEntities: group.entities
});
}
} catch (error) {
console.error(`Error extracting sections from ${filePath}:`, error);
}
return sections;
}
private groupRelevantLines(
relevantLines: { line: number; content: string; score: number }[],
contextSize: number
): Array<{
startLine: number;
endLine: number;
averageScore: number;
entities: string[];
}> {
if (relevantLines.length === 0) return [];
const groups: Array<{
startLine: number;
endLine: number;
scores: number[];
entities: string[];
}> = [];
let currentGroup = {
startLine: relevantLines[0].line,
endLine: relevantLines[0].line,
scores: [relevantLines[0].score],
entities: []
};
for (let i = 1; i < relevantLines.length; i++) {
const line = relevantLines[i];
// If line is close to current group, extend the group
if (line.line - currentGroup.endLine <= contextSize * 2) {
currentGroup.endLine = line.line;
currentGroup.scores.push(line.score);
} else {
// Start new group
groups.push(currentGroup);
currentGroup = {
startLine: line.line,
endLine: line.line,
scores: [line.score],
entities: []
};
}
}
groups.push(currentGroup);
return groups.map(group => ({
startLine: group.startLine,
endLine: group.endLine,
averageScore: group.scores.reduce((sum, score) => sum + score, 0) / group.scores.length,
entities: group.entities
}));
}
private async extractImportSections(dependencyInfo: any): Promise<CodeSection[]> {
const sections: CodeSection[] = [];
if (!dependencyInfo || !dependencyInfo.fileRelationships) {
return sections;
}
for (const [filePath, relationship] of dependencyInfo.fileRelationships.entries()) {
for (const importStmt of relationship.imports) {
const content = await this.readFileSection(filePath, importStmt.line, importStmt.line + 1);
if (content) {
sections.push({
filePath,
startLine: importStmt.line,
endLine: importStmt.line + 1,
content,
relevanceScore: 0.6,
contextType: 'import',
relatedEntities: importStmt.imported
});
}
}
}
return sections;
}
private async scoreAndRankContexts(
sections: CodeSection[],
textAnalysis: any,
input: TaskAnalysisInput
): Promise<CodeSection[]> {
const scoredSections = sections.map(section => {
const score = this.calculateRelevanceScore(section, textAnalysis, input);
return { ...section, relevanceScore: score };
});
return scoredSections.sort((a, b) => b.relevanceScore - a.relevanceScore);
}
private calculateRelevanceScore(
section: CodeSection,
textAnalysis: any,
input: TaskAnalysisInput
): number {
const weights = this.config.contextScoringWeights;
let score = 0;
// Keyword match score
let keywordScore = 0;
for (const keyword of textAnalysis.keywords) {
if (section.content.toLowerCase().includes(keyword.toLowerCase())) {
keywordScore += 1; // Simple scoring since we don't have keyword.score anymore
}
}
score += keywordScore * weights.keywordMatch;
// Entity match score
let entityScore = 0;
for (const entity of textAnalysis.entities) {
if (section.content.includes(entity) || section.relatedEntities.includes(entity)) {
entityScore += 2; // Entities get higher score
}
}
score += entityScore * weights.entityMatch;
// Intent match score (simplified to use taskType instead of intent)
const intentScore = this.calculateIntentMatchScore(section, input.taskType);
score += intentScore * weights.intentMatch;
// File proximity score (if file is explicitly mentioned)
let proximityScore = 0;
if (input.filesLikelyInvolved?.includes(section.filePath)) {
proximityScore = 1.0;
}
score += proximityScore * weights.fileProximity;
// Base relevance score from section analysis
score += section.relevanceScore * 0.3;
return Math.min(1.0, score);
}
private calculateIntentMatchScore(section: CodeSection, taskType: TaskType): number {
// Simple intent matching based on context type and task type
const taskTypeMatches: Record<string, string[]> = {
'bug': ['function', 'usage', 'comment'],
'improvement': ['function', 'class', 'usage']
};
const contextTypeMatches = taskTypeMatches[taskType] || [];
return contextTypeMatches.includes(section.contextType) ? 0.8 : 0.3;
}
private filterContexts(sections: CodeSection[]): CodeSection[] {
return sections
.filter(section => section.relevanceScore >= this.config.relevanceThreshold)
.slice(0, this.config.maxContextsPerTask);
}
private async convertToCodeContexts(
sections: CodeSection[],
input: TaskAnalysisInput
): Promise<CodeContext[]> {
const contexts: CodeContext[] = [];
for (let i = 0; i < sections.length; i++) {
const section = sections[i];
contexts.push({
id: `${input.taskId}_context_${i}`,
taskId: input.taskId,
taskType: input.taskType,
contextType: this.mapContextType(section.contextType),
source: 'ai_collected',
filePath: section.filePath,
startLine: section.startLine,
endLine: section.endLine,
content: section.content,
description: this.generateContextDescription(section),
relevanceScore: section.relevanceScore,
keywords: section.relatedEntities,
dateCollected: new Date().toISOString(),
isStale: false
});
}
return contexts;
}
private mapContextType(sectionType: string): CodeContext['contextType'] {
const mapping: Record<string, CodeContext['contextType']> = {
'function': 'snippet',
'class': 'snippet',
'import': 'dependency',
'usage': 'snippet',
'comment': 'snippet'
};
return mapping[sectionType] || 'snippet';
}
private generateContextDescription(section: CodeSection): string {
const fileName = path.basename(section.filePath);
const lineRange = section.startLine === section.endLine ?
`line ${section.startLine}` :
`lines ${section.startLine}-${section.endLine}`;
return `${section.contextType} in ${fileName} (${lineRange})`;
}
private generateSummary(
contexts: CodeContext[],
startTime: number,
patternMatches: any,
dependencyInfo: any,
stackTraces: ParsedStackTrace[]
): ContextCollectionResult['summary'] {
const highRelevanceContexts = contexts.filter(c => c.relevanceScore > 0.7).length;
const mediumRelevanceContexts = contexts.filter(c => c.relevanceScore > 0.4 && c.relevanceScore <= 0.7).length;
const lowRelevanceContexts = contexts.filter(c => c.relevanceScore <= 0.4).length;
const averageRelevanceScore = contexts.length > 0 ?
contexts.reduce((sum, c) => sum + c.relevanceScore, 0) / contexts.length : 0;
const processingTimeMs = Date.now() - startTime;
const filesAnalyzed = new Set(contexts.map(c => c.filePath)).size;
const patternsFound = (patternMatches.functions?.length || 0) +
(patternMatches.classes?.length || 0) +
(patternMatches.patterns?.length || 0);
const dependenciesAnalyzed = dependencyInfo ? dependencyInfo.graph.edges.length : 0;
const stackTracesFound = stackTraces.length;
return {
totalContexts: contexts.length,
highRelevanceContexts,
mediumRelevanceContexts,
lowRelevanceContexts,
averageRelevanceScore,
processingTimeMs,
filesAnalyzed,
patternsFound,
dependenciesAnalyzed,
stackTracesFound
};
}
private generateRecommendations(
contexts: CodeContext[],
textAnalysis: any,
patternMatches: any,
stackTraces: ParsedStackTrace[]
): string[] {
const recommendations: string[] = [];
// Stack trace specific recommendations
if (stackTraces.length > 0) {
const highConfidenceTraces = stackTraces.filter(trace => trace.confidence > 0.8);
if (highConfidenceTraces.length > 0) {
recommendations.push(`Found ${highConfidenceTraces.length} high-confidence stack trace(s) - code contexts automatically collected from error locations`);
}
const languages = [...new Set(stackTraces.map(trace => trace.language))];
if (languages.length > 1) {
recommendations.push(`Multiple programming languages detected in stack traces: ${languages.join(', ')}`);
}
const errorTypes = [...new Set(stackTraces.map(trace => trace.errorType).filter(Boolean))];
if (errorTypes.length > 0) {
recommendations.push(`Error types identified: ${errorTypes.join(', ')} - consider adding error handling for these cases`);
}
}
// Check if we have enough high-quality contexts
const highQualityContexts = contexts.filter(c => c.relevanceScore > 0.7);
if (highQualityContexts.length < 3) {
recommendations.push('Consider adding more specific files or code references to improve context relevance');
}
// Check for missing function/class definitions
const functionEntities = textAnalysis.entities.filter((e: any) => e.type === 'function');
const foundFunctions = patternMatches.functions?.map((f: any) => f.name) || [];
const missingFunctions = functionEntities.filter((e: any) => !foundFunctions.includes(e.entity));
if (missingFunctions.length > 0) {
recommendations.push(`Could not find definitions for functions: ${missingFunctions.map((f: any) => f.entity).join(', ')}`);
}
// Check for architectural patterns
if (patternMatches.patterns?.length > 0) {
recommendations.push('Similar code patterns found - consider checking for consistent implementation');
}
return recommendations;
}
private identifyPotentialIssues(
contexts: CodeContext[],
dependencyInfo: any
): string[] {
const issues: string[] = [];
// Check for circular dependencies
if (dependencyInfo?.graph?.cyclicDependencies?.length > 0) {
issues.push(`Circular dependencies detected in ${dependencyInfo.graph.cyclicDependencies.length} cycles`);
}
// Check for large files
const largeFiles = contexts.filter(c => c.content && c.content.length > 5000);
if (largeFiles.length > 0) {
issues.push(`Large code sections found in ${largeFiles.length} contexts - consider breaking down`);
}
// Check for low relevance contexts
const lowRelevanceCount = contexts.filter(c => c.relevanceScore < 0.3).length;
if (lowRelevanceCount > contexts.length * 0.5) {
issues.push('Many contexts have low relevance scores - consider refining task description');
}
return issues;
}
private cacheResults(taskId: string, contexts: CodeContext[]): void {
this.contextCache.set(taskId, contexts);
// Clean up old cache entries
const maxCacheSize = 100;
if (this.contextCache.size > maxCacheSize) {
const oldestKey = this.contextCache.keys().next().value;
if (oldestKey) {
this.contextCache.delete(oldestKey);
}
}
}
private safeResolve(p: string): string | null {
try {
const resolved = path.resolve(p);
const root = this.allowedRoot;
if (resolved === root || resolved.startsWith(root + path.sep)) {
if (this.config.excludePatterns.some((pat) => resolved.includes(pat))) return null;
return resolved;
}
return null;
} catch {
return null;
}
}
private mergeWithDefaultConfig(config: Partial<ContextCollectionConfig>): ContextCollectionConfig {
return {
maxContextsPerTask: 20,
relevanceThreshold: 0.3,
maxFileSize: 100000,
excludePatterns: ['node_modules', '.git', 'dist', 'build', 'coverage'],
includeExtensions: ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.rb', '.php', '.go', '.cs'],
cacheExpiryHours: 24,
enableStalenessTracking: true,
enablePatternMatching: true,
enableDependencyAnalysis: true,
// Token optimization defaults
maxTokensPerTask: 2000,
maxTokensPerContext: 200,
enableIntelligentSummarization: true,
enableContentDeduplication: true,
compressionThreshold: 500,
taskTypeTokenLimits: {
bug: 1500,
improvement: 2000,
},
contextScoringWeights: {
keywordMatch: 0.3,
entityMatch: 0.3,
intentMatch: 0.2,
patternSimilarity: 0.1,
dependencyStrength: 0.05,
fileProximity: 0.05
},
...config
};
}
}