UNPKG

bugger-mcp

Version:

MCP Server for managing bugs, feature requests, and improvements

758 lines 33.2 kB
// Context collection engine that orchestrates pattern matching and dependency analysis import { TreeSitterCodeAnalyzer } from './treesitter-code-analyzer.js'; import { DependencyAnalyzer } from './dependency-analysis.js'; import { StackTraceParser } from './stack-trace-parser.js'; import * as fs from 'fs'; import * as path from 'path'; /** * Main context collection engine that orchestrates all analysis components */ export class ContextCollectionEngine { codeAnalyzer; dependencyAnalyzer; config; contextCache = new Map(); constructor(rootPath = process.cwd(), config = {}) { this.codeAnalyzer = new TreeSitterCodeAnalyzer(rootPath); this.dependencyAnalyzer = new DependencyAnalyzer(rootPath); this.config = this.mergeWithDefaultConfig(config); } /** * Collect contexts for a task using all available analysis methods */ async collectContexts(input) { 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 = { 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 */ getCachedContexts(taskId) { 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 */ updateConfig(newConfig) { this.config = { ...this.config, ...newConfig }; } /** * Clear all caches */ clearCaches() { this.contextCache.clear(); // Cache cleared } // Private methods /** * Parse stack traces from task description text */ async parseStackTraces(combinedText) { 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 */ async extractStackTraceContexts(stackTraces) { const sections = []; for (const stackTrace of stackTraces) { const contexts = StackTraceParser.extractStackTraceContexts(stackTrace); for (const context of contexts) { // Check if file exists and is readable if (fs.existsSync(context.filePath)) { try { const content = await this.readFileSection(context.filePath, Math.max(1, context.lineNumber - context.contextLines), context.lineNumber + context.contextLines); if (content) { const relevanceScore = this.calculateStackTraceRelevance(context.priority, stackTrace.confidence); sections.push({ filePath: context.filePath, 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 ${context.filePath}:`, error); } } } } return sections; } /** * Calculate relevance score for stack trace contexts */ calculateStackTraceRelevance(priority, confidence) { 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 */ estimateTokenCount(text) { // Rough estimation: ~4 characters per token for English text return Math.ceil(text.length / 4); } /** * Intelligently summarize content to reduce token usage */ summarizeContent(content, maxTokens) { 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 */ deduplicateContexts(contexts) { if (!this.config.enableContentDeduplication) { return contexts; } const unique = []; const seen = new Set(); 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 */ createContentHash(content) { // 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 */ applyTokenFiltering(contexts, taskType) { let totalTokens = 0; const filtered = []; // 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; } async analyzeTaskText(input) { // 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) */ extractSimpleKeywords(text) { 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) */ extractSimpleEntities(text) { if (!text || typeof text !== 'string') { return []; } const entities = []; // 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 */ isCommonWord(word) { 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); } async findRelevantPatterns(textAnalysis) { const functions = []; const classes = []; const patterns = []; // 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 }; } async analyzeDependencies(filesLikelyInvolved) { try { const graph = await this.dependencyAnalyzer.buildDependencyGraph(); const fileRelationships = new Map(); // Get relationships for files likely involved for (const file of filesLikelyInvolved) { if (fs.existsSync(file)) { const relationship = await this.dependencyAnalyzer.mapFileRelationships(file); fileRelationships.set(file, relationship); } } return { graph, fileRelationships }; } catch (error) { console.error('Error analyzing dependencies:', error); return null; } } async extractCodeSections(input, textAnalysis, patternMatches, dependencyInfo, stackTraces) { const sections = []; // 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 || []) { if (fs.existsSync(file)) { 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; } async readFileSection(filePath, startLine, endLine) { try { const content = fs.readFileSync(filePath, '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; } } async extractRelevantSectionsFromFile(filePath, textAnalysis) { const sections = []; try { const content = fs.readFileSync(filePath, 'utf8'); const lines = content.split('\n'); // Find lines that contain keywords or entities const relevantLines = []; 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, startLine, endLine, content: sectionContent, relevanceScore: group.averageScore, contextType: 'usage', relatedEntities: group.entities }); } } catch (error) { console.error(`Error extracting sections from ${filePath}:`, error); } return sections; } groupRelevantLines(relevantLines, contextSize) { if (relevantLines.length === 0) return []; const groups = []; 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 })); } async extractImportSections(dependencyInfo) { const sections = []; 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; } async scoreAndRankContexts(sections, textAnalysis, input) { 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); } calculateRelevanceScore(section, textAnalysis, input) { 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); } calculateIntentMatchScore(section, taskType) { // Simple intent matching based on context type and task type const taskTypeMatches = { 'bug': ['function', 'usage', 'comment'], 'feature': ['class', 'function', 'import'], 'improvement': ['function', 'class', 'usage'] }; const contextTypeMatches = taskTypeMatches[taskType] || []; return contextTypeMatches.includes(section.contextType) ? 0.8 : 0.3; } filterContexts(sections) { return sections .filter(section => section.relevanceScore >= this.config.relevanceThreshold) .slice(0, this.config.maxContextsPerTask); } async convertToCodeContexts(sections, input) { const contexts = []; 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; } mapContextType(sectionType) { const mapping = { 'function': 'snippet', 'class': 'snippet', 'import': 'dependency', 'usage': 'snippet', 'comment': 'snippet' }; return mapping[sectionType] || 'snippet'; } generateContextDescription(section) { 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})`; } generateSummary(contexts, startTime, patternMatches, dependencyInfo, stackTraces) { 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 }; } generateRecommendations(contexts, textAnalysis, patternMatches, stackTraces) { const recommendations = []; // 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) => e.type === 'function'); const foundFunctions = patternMatches.functions?.map((f) => f.name) || []; const missingFunctions = functionEntities.filter((e) => !foundFunctions.includes(e.entity)); if (missingFunctions.length > 0) { recommendations.push(`Could not find definitions for functions: ${missingFunctions.map((f) => 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; } identifyPotentialIssues(contexts, dependencyInfo) { const issues = []; // 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; } cacheResults(taskId, contexts) { 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); } } } mergeWithDefaultConfig(config) { 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, feature: 2500, improvement: 2000, }, contextScoringWeights: { keywordMatch: 0.3, entityMatch: 0.3, intentMatch: 0.2, patternSimilarity: 0.1, dependencyStrength: 0.05, fileProximity: 0.05 }, ...config }; } } //# sourceMappingURL=context-collection-engine.js.map