contaigents
Version:
Modular AI Content Ecosystem with Audio Generation
392 lines (391 loc) • 15.4 kB
JavaScript
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;
}
}
}