UNPKG

contaigents

Version:

Modular AI Content Ecosystem with Audio Generation

392 lines (391 loc) 15.4 kB
import { BaseTool } from '../base/BaseTool.js'; import { IntelligentFileService } from '../../services/IntelligentFileService.js'; import { ClassifyFileTool } from '../file/ClassifyFileTool.js'; import fs from 'fs/promises'; import path from 'path'; export class DependencyTrackingTool extends BaseTool { constructor(context) { super('dependency_tracking', 'Analyze and track file dependencies, imports, and relationships across the project', context); this.intelligentFileService = new IntelligentFileService(context.workingDirectory); } async execute(params) { try { const { files, includeTransitive = true, maxDepth = 5, analysisType = 'all' } = params; console.log('🔍 Starting dependency tracking analysis...'); // Get files to analyze const filesToAnalyze = files || await this.findAllRelevantFiles(); if (filesToAnalyze.length === 0) { return this.createErrorResult('No files found to analyze'); } // Build dependency graph const dependencyGraph = await this.buildDependencyGraph(filesToAnalyze, analysisType, includeTransitive, maxDepth); // Analyze the graph const summary = await this.analyzeDependencyGraph(dependencyGraph); const metrics = this.calculateGraphMetrics(dependencyGraph); const result = { dependencyGraph, summary, metrics }; console.log(`✅ Dependency analysis complete: ${dependencyGraph.nodes.length} files, ${dependencyGraph.edges.length} dependencies`); return this.createSuccessResult(result, { analysisType, includeTransitive, maxDepth, analyzedFiles: filesToAnalyze.length }); } catch (error) { console.error('Dependency tracking failed:', error); return this.createErrorResult(`Dependency tracking failed: ${error.message}`); } } /** * Build comprehensive dependency graph */ async buildDependencyGraph(files, analysisType, includeTransitive, maxDepth) { const nodes = []; const edges = []; const processedFiles = new Set(); // Process files in batches to avoid overwhelming the system const batchSize = 10; for (let i = 0; i < files.length; i += batchSize) { const batch = files.slice(i, i + batchSize); await this.processBatch(batch, nodes, edges, processedFiles, analysisType); } // Add transitive dependencies if requested if (includeTransitive) { await this.addTransitiveDependencies(nodes, edges, maxDepth, analysisType); } return { nodes, edges, clusters: [], // Will be populated by ProjectContextService analyzedAt: Date.now() }; } /** * Process a batch of files for dependency analysis */ async processBatch(files, nodes, edges, processedFiles, analysisType) { const promises = files.map(async (filePath) => { if (processedFiles.has(filePath)) return; processedFiles.add(filePath); try { // Create node const node = await this.createDependencyNode(filePath); nodes.push(node); // Analyze relationships const relationships = await this.analyzeFileRelationships(filePath, analysisType); // Create edges for (const rel of relationships) { edges.push({ source: filePath, target: rel.targetFile, edgeType: rel.type, strength: rel.confidence, metadata: { description: rel.description, detectedBy: 'pattern-matching' } }); } } catch (error) { console.warn(`Failed to process ${filePath}:`, error); } }); await Promise.all(promises); } /** * Create a dependency node for a file */ async createDependencyNode(filePath) { try { const stats = await fs.stat(filePath); const ext = path.extname(filePath).toLowerCase(); return { filePath, nodeType: this.determineNodeType(filePath), importance: 0, // Will be calculated later based on connections metadata: { fileSize: stats.size, lastModified: stats.mtime, fileType: ext, directory: path.dirname(filePath) } }; } catch (error) { return { filePath, nodeType: 'file', importance: 0, metadata: { error: 'Failed to read file stats' } }; } } /** * Analyze file relationships using existing ClassifyFileTool */ async analyzeFileRelationships(filePath, analysisType) { try { const classifyTool = new ClassifyFileTool(this.context); const result = await classifyTool.execute({ filePath, includeRelationships: true }); if (!result.success || !result.data.relationships) { return []; } const relationships = result.data.relationships; // Filter by analysis type if (analysisType === 'imports') { return relationships.filter(rel => rel.type === 'imports'); } else if (analysisType === 'references') { return relationships.filter(rel => ['references', 'links-to'].includes(rel.type)); } return relationships; } catch (error) { console.warn(`Failed to analyze relationships for ${filePath}:`, error); return []; } } /** * Add transitive dependencies to the graph */ async addTransitiveDependencies(nodes, edges, maxDepth, analysisType) { const existingFiles = new Set(nodes.map(n => n.filePath)); const newFilesToProcess = []; // Find files referenced but not yet in the graph for (const edge of edges) { if (!existingFiles.has(edge.target) && await this.fileExists(edge.target)) { newFilesToProcess.push(edge.target); existingFiles.add(edge.target); } } // Process new files up to maxDepth let currentDepth = 1; let filesToProcess = [...newFilesToProcess]; while (filesToProcess.length > 0 && currentDepth < maxDepth) { const nextBatch = []; await this.processBatch(filesToProcess, nodes, edges, new Set(), analysisType); // Find next level of dependencies const newEdges = edges.filter(e => filesToProcess.includes(e.source)); for (const edge of newEdges) { if (!existingFiles.has(edge.target) && await this.fileExists(edge.target)) { nextBatch.push(edge.target); existingFiles.add(edge.target); } } filesToProcess = nextBatch; currentDepth++; } } /** * Analyze the dependency graph for insights */ async analyzeDependencyGraph(graph) { // Calculate node importance based on connections const connectionCounts = new Map(); for (const edge of graph.edges) { connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1); connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1); } // Update node importance for (const node of graph.nodes) { node.importance = (connectionCounts.get(node.filePath) || 0) / Math.max(graph.nodes.length, 1); } // Find circular dependencies const circularDependencies = this.findCircularDependencies(graph); // Find orphaned files (no connections) const orphanedFiles = graph.nodes .filter(node => (connectionCounts.get(node.filePath) || 0) === 0) .map(node => node.filePath); // Find highly connected files const highlyConnectedFiles = Array.from(connectionCounts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 10) .map(([file, connections]) => ({ file, connections })); return { totalFiles: graph.nodes.length, totalDependencies: graph.edges.length, circularDependencies, orphanedFiles, highlyConnectedFiles }; } /** * Calculate graph metrics */ calculateGraphMetrics(graph) { const totalNodes = graph.nodes.length; const totalEdges = graph.edges.length; if (totalNodes === 0) { return { averageConnections: 0, maxConnections: 0, graphDensity: 0, modularityScore: 0 }; } // Calculate connection statistics const connectionCounts = new Map(); for (const edge of graph.edges) { connectionCounts.set(edge.source, (connectionCounts.get(edge.source) || 0) + 1); connectionCounts.set(edge.target, (connectionCounts.get(edge.target) || 0) + 1); } const connections = Array.from(connectionCounts.values()); const averageConnections = connections.length > 0 ? connections.reduce((sum, count) => sum + count, 0) / connections.length : 0; const maxConnections = connections.length > 0 ? Math.max(...connections) : 0; // Graph density: actual edges / possible edges const maxPossibleEdges = totalNodes * (totalNodes - 1); const graphDensity = maxPossibleEdges > 0 ? totalEdges / maxPossibleEdges : 0; // Simple modularity score based on clustering const modularityScore = this.calculateModularity(graph); return { averageConnections, maxConnections, graphDensity, modularityScore }; } /** * Find circular dependencies in the graph */ findCircularDependencies(graph) { const cycles = []; const visited = new Set(); const recursionStack = new Set(); const dfs = (node, path) => { if (recursionStack.has(node)) { // Found a cycle const cycleStart = path.indexOf(node); if (cycleStart !== -1) { cycles.push(path.slice(cycleStart)); } return; } if (visited.has(node)) return; visited.add(node); recursionStack.add(node); path.push(node); // Find outgoing edges const outgoingEdges = graph.edges.filter(edge => edge.source === node); for (const edge of outgoingEdges) { dfs(edge.target, [...path]); } recursionStack.delete(node); }; // Start DFS from each node for (const node of graph.nodes) { if (!visited.has(node.filePath)) { dfs(node.filePath, []); } } return cycles; } /** * Calculate modularity score */ calculateModularity(graph) { // Simple modularity calculation based on directory structure const directoryGroups = new Map(); for (const node of graph.nodes) { const dir = path.dirname(node.filePath); if (!directoryGroups.has(dir)) { directoryGroups.set(dir, []); } directoryGroups.get(dir).push(node.filePath); } let internalEdges = 0; let totalEdges = graph.edges.length; for (const edge of graph.edges) { const sourceDir = path.dirname(edge.source); const targetDir = path.dirname(edge.target); if (sourceDir === targetDir) { internalEdges++; } } return totalEdges > 0 ? internalEdges / totalEdges : 0; } /** * Determine node type based on file characteristics */ determineNodeType(filePath) { const ext = path.extname(filePath).toLowerCase(); const basename = path.basename(filePath, ext).toLowerCase(); if (['.ts', '.js', '.tsx', '.jsx'].includes(ext)) { if (basename.includes('component') || basename.includes('page')) { return 'component'; } if (basename.includes('service') || basename.includes('api')) { return 'module'; } } if (['.md', '.txt', '.json', '.yaml', '.yml'].includes(ext)) { return 'resource'; } return 'file'; } /** * Find all relevant files in the project */ async findAllRelevantFiles() { const relevantExtensions = [ '.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.cpp', '.c', '.h', '.html', '.css', '.scss', '.sass', '.less', '.php', '.rb', '.go', '.rs', '.swift', '.kt', '.md', '.mdx', '.txt', '.json', '.yaml', '.yml' ]; const skipDirs = ['node_modules', 'dist', 'build', '.git', '.next', 'coverage']; const files = []; const scanDirectory = async (dirPath) => { try { const entries = await fs.readdir(dirPath, { withFileTypes: true }); for (const entry of entries) { if (entry.name.startsWith('.')) continue; const fullPath = path.join(dirPath, entry.name); const relativePath = path.relative(this.context.workingDirectory, fullPath); if (entry.isDirectory()) { if (!skipDirs.includes(entry.name)) { await scanDirectory(fullPath); } } else { const ext = path.extname(entry.name).toLowerCase(); if (relevantExtensions.includes(ext)) { files.push(relativePath); } } } } catch (error) { console.warn(`Failed to scan directory ${dirPath}:`, error); } }; await scanDirectory(this.context.workingDirectory); return files; } /** * Check if a file exists */ async fileExists(filePath) { try { const fullPath = path.resolve(this.context.workingDirectory, filePath); await fs.access(fullPath); return true; } catch { return false; } } }