UNPKG

@paulohenriquevn/m2js

Version:

Transform TypeScript/JavaScript code into LLM-friendly Markdown summaries + Smart Dead Code Detection + Graph-Deep Diff Analysis. Extract exported functions, classes, and JSDoc comments for better AI context with 60%+ token reduction. Intelligent dead cod

752 lines 30.5 kB
"use strict"; /** * Graph-Deep Diff Analyzer * Compare architectural states and detect problematic changes */ Object.defineProperty(exports, "__esModule", { value: true }); exports.analyzeGraphDiff = analyzeGraphDiff; const git_integrator_1 = require("./git-integrator"); const dependency_analyzer_1 = require("./dependency-analyzer"); const file_scanner_1 = require("./file-scanner"); /** * Analyze architectural differences between two states */ async function analyzeGraphDiff(projectPath, options) { const startTime = Date.now(); try { // Initialize git integrator const git = new git_integrator_1.GitIntegrator(projectPath); // Resolve references const baselineRef = git.resolveRef(options.baseline); const currentRef = options.current ? git.resolveRef(options.current) : 'working-directory'; console.log(`Comparing ${options.baseline} → ${currentRef || 'current'}`); // Create snapshots for both states const baselineSnapshot = await createGraphSnapshot(projectPath, git, baselineRef); const currentSnapshot = await createCurrentSnapshot(projectPath, git, currentRef); // Analyze differences const changes = detectArchitecturalChanges(baselineSnapshot, currentSnapshot); // Calculate impact const impact = calculateImpactSummary(baselineSnapshot, currentSnapshot, changes); // Generate recommendations const recommendations = generateRecommendations(changes, impact); // Calculate metrics diff const metricsDiff = calculateMetricsDiff(baselineSnapshot.metrics, currentSnapshot.metrics); return { projectPath, comparison: { baseline: options.baseline, current: currentRef || 'working-directory', timestamp: new Date(), }, options, changes, impact, recommendations, metrics: { baseline: baselineSnapshot.metrics, current: currentSnapshot.metrics, diff: metricsDiff, }, }; } catch (error) { throw new Error(`Graph diff analysis failed: ${error.message}`); } } /** * Create a graph snapshot for a specific git reference */ async function createGraphSnapshot(projectPath, git, ref) { let workspace = null; try { // Create temporary workspace with files from the reference workspace = await git.createTempWorkspace(ref); // Scan for TypeScript/JavaScript files const scanResult = await (0, file_scanner_1.scanDirectory)(workspace); if (scanResult.files.length === 0) { return { timestamp: new Date(), gitRef: ref, dependencyGraph: { projectPath: workspace, nodes: [], edges: [], metrics: createEmptyMetrics(), }, metrics: createEmptyMetrics(), files: [], }; } // Analyze dependencies const dependencyGraph = (0, dependency_analyzer_1.analyzeDependencies)(scanResult.files, { includeExternalDeps: true, detectCircular: true, }); return { timestamp: new Date(), gitRef: ref, dependencyGraph, metrics: adaptGraphMetrics(dependencyGraph.metrics), files: scanResult.files, }; } finally { // Cleanup workspace if (workspace) { git.cleanupWorkspace(workspace); } } } /** * Create snapshot for current state (working directory) */ async function createCurrentSnapshot(projectPath, git, ref) { // If ref is working-directory, analyze current files if (ref === 'working-directory') { const scanResult = await (0, file_scanner_1.scanDirectory)(projectPath); if (scanResult.files.length === 0) { return { timestamp: new Date(), gitRef: 'working-directory', dependencyGraph: { projectPath, nodes: [], edges: [], metrics: createEmptyMetrics(), }, metrics: createEmptyMetrics(), files: [], }; } const dependencyGraph = (0, dependency_analyzer_1.analyzeDependencies)(scanResult.files, { includeExternalDeps: true, detectCircular: true, }); return { timestamp: new Date(), gitRef: 'working-directory', dependencyGraph, metrics: adaptGraphMetrics(dependencyGraph.metrics), files: scanResult.files, }; } else { // Use git snapshot for specific ref return createGraphSnapshot(projectPath, git, ref); } } /** * Detect architectural changes between two snapshots */ function detectArchitecturalChanges(baseline, current) { const changes = []; let changeId = 1; // Detect circular dependency changes changes.push(...detectCircularDependencyChanges(baseline, current, changeId)); changeId += changes.length; // Detect coupling changes changes.push(...detectCouplingChanges(baseline, current, changeId)); changeId += changes.length; // Detect external dependency changes changes.push(...detectExternalDependencyChanges(baseline, current, changeId)); changeId += changes.length; // Detect layer violation changes changes.push(...detectLayerViolationChanges(baseline, current, changeId)); changeId += changes.length; // Detect hotspot changes changes.push(...detectHotspotChanges(baseline, current, changeId)); changeId += changes.length; // Detect architecture layer changes changes.push(...detectArchitectureLayerChanges(baseline, current, changeId)); return changes; } /** * Detect circular dependency changes */ function detectCircularDependencyChanges(baseline, current, startId) { const changes = []; const baseCircular = baseline.metrics.circularDependencies.length; const currentCircular = current.metrics.circularDependencies.length; if (currentCircular > baseCircular) { const increase = currentCircular - baseCircular; changes.push({ id: `change-${startId}`, type: 'circular-dependency-introduced', severity: increase > 2 ? 'critical' : increase > 1 ? 'high' : 'medium', category: 'dependencies', description: `${increase} new circular ${increase === 1 ? 'dependency' : 'dependencies'} introduced`, details: { before: baseCircular, after: currentCircular, modules: [], // TODO: Extract specific modules metrics: { increase }, }, affected: [], // TODO: Extract affected modules impact: { riskLevel: increase > 2 ? 'critical' : 'high', maintainability: -3 * increase, performance: -1 * increase, testability: -2 * increase, overallScore: -6 * increase, reasoning: 'Circular dependencies make code harder to understand, test, and maintain', affectedAreas: ['maintainability', 'testability', 'code organization'], }, }); } else if (currentCircular < baseCircular) { const decrease = baseCircular - currentCircular; changes.push({ id: `change-${startId}`, type: 'circular-dependency-resolved', severity: 'low', category: 'dependencies', description: `${decrease} circular ${decrease === 1 ? 'dependency' : 'dependencies'} resolved`, details: { before: baseCircular, after: currentCircular, modules: [], metrics: { decrease }, }, affected: [], impact: { riskLevel: 'low', maintainability: 3 * decrease, performance: 1 * decrease, testability: 2 * decrease, overallScore: 6 * decrease, reasoning: 'Resolving circular dependencies improves code organization and testability', affectedAreas: ['maintainability', 'testability', 'code organization'], }, }); } return changes; } /** * Detect coupling changes */ function detectCouplingChanges(baseline, current, startId) { const changes = []; const baseCoupling = baseline.metrics.averageCoupling || baseline.metrics.averageDependencies; const currentCoupling = current.metrics.averageCoupling || current.metrics.averageDependencies; const threshold = 0.5; // Significant change threshold const couplingDiff = currentCoupling - baseCoupling; if (Math.abs(couplingDiff) > threshold) { if (couplingDiff > 0) { changes.push({ id: `change-${startId}`, type: 'coupling-increased', severity: couplingDiff > 2 ? 'high' : couplingDiff > 1 ? 'medium' : 'low', category: 'coupling', description: `Average coupling increased by ${couplingDiff.toFixed(1)} dependencies per module`, details: { before: baseCoupling, after: currentCoupling, modules: [], metrics: { increase: couplingDiff }, }, affected: [], impact: { riskLevel: couplingDiff > 2 ? 'high' : 'medium', maintainability: -2 * Math.round(couplingDiff), performance: -1 * Math.round(couplingDiff), testability: -2 * Math.round(couplingDiff), overallScore: -5 * Math.round(couplingDiff), reasoning: 'Increased coupling makes modules more interdependent and harder to change', affectedAreas: ['maintainability', 'testability', 'modularity'], }, }); } else { changes.push({ id: `change-${startId}`, type: 'coupling-decreased', severity: 'low', category: 'coupling', description: `Average coupling decreased by ${Math.abs(couplingDiff).toFixed(1)} dependencies per module`, details: { before: baseCoupling, after: currentCoupling, modules: [], metrics: { decrease: Math.abs(couplingDiff) }, }, affected: [], impact: { riskLevel: 'low', maintainability: 2 * Math.round(Math.abs(couplingDiff)), performance: 1 * Math.round(Math.abs(couplingDiff)), testability: 2 * Math.round(Math.abs(couplingDiff)), overallScore: 5 * Math.round(Math.abs(couplingDiff)), reasoning: 'Decreased coupling improves modularity and makes code easier to maintain', affectedAreas: ['maintainability', 'testability', 'modularity'], }, }); } } return changes; } /** * Detect external dependency changes */ function detectExternalDependencyChanges(baseline, current, startId) { const changes = []; const baseExternal = baseline.metrics.externalDependencies; const currentExternal = current.metrics.externalDependencies; if (currentExternal > baseExternal) { const increase = currentExternal - baseExternal; changes.push({ id: `change-${startId}`, type: 'external-dependency-added', severity: increase > 5 ? 'medium' : 'low', category: 'external', description: `${increase} new external ${increase === 1 ? 'dependency' : 'dependencies'} added`, details: { before: baseExternal, after: currentExternal, modules: [], metrics: { increase }, }, affected: [], impact: { riskLevel: increase > 5 ? 'medium' : 'low', maintainability: -1 * increase, performance: 0, testability: -1 * increase, overallScore: -2 * increase, reasoning: 'New external dependencies increase maintenance burden and potential security risks', affectedAreas: ['maintainability', 'security', 'bundle size'], }, }); } else if (currentExternal < baseExternal) { const decrease = baseExternal - currentExternal; changes.push({ id: `change-${startId}`, type: 'external-dependency-removed', severity: 'low', category: 'external', description: `${decrease} external ${decrease === 1 ? 'dependency' : 'dependencies'} removed`, details: { before: baseExternal, after: currentExternal, modules: [], metrics: { decrease }, }, affected: [], impact: { riskLevel: 'low', maintainability: 1 * decrease, performance: 1 * decrease, testability: 0, overallScore: 2 * decrease, reasoning: 'Removing external dependencies reduces maintenance burden and improves performance', affectedAreas: ['maintainability', 'performance', 'bundle size'], }, }); } return changes; } /** * Detect layer violation changes (simplified heuristic) */ function detectLayerViolationChanges(baseline, current, startId) { const changes = []; // Simple heuristic: detect UI → DB direct connections (bypassing service layer) const baseViolations = detectLayerViolations(baseline.dependencyGraph); const currentViolations = detectLayerViolations(current.dependencyGraph); if (currentViolations > baseViolations) { const increase = currentViolations - baseViolations; changes.push({ id: `change-${startId}`, type: 'layer-violation-introduced', severity: increase > 2 ? 'high' : 'medium', category: 'architecture', description: `${increase} new layer ${increase === 1 ? 'violation' : 'violations'} introduced`, details: { before: baseViolations, after: currentViolations, modules: [], metrics: { increase }, }, affected: [], impact: { riskLevel: 'high', maintainability: -3 * increase, performance: 0, testability: -2 * increase, overallScore: -5 * increase, reasoning: 'Layer violations break architectural boundaries and make code harder to maintain', affectedAreas: [ 'architecture', 'maintainability', 'separation of concerns', ], }, }); } return changes; } /** * Detect hotspot changes (modules with high coupling) */ function detectHotspotChanges(baseline, current, startId) { const changes = []; const baseHotspots = baseline.metrics.hotspots?.length || 0; const currentHotspots = current.metrics.hotspots?.length || 0; if (currentHotspots > baseHotspots) { const increase = currentHotspots - baseHotspots; changes.push({ id: `change-${startId}`, type: 'hotspot-created', severity: increase > 2 ? 'high' : 'medium', category: 'complexity', description: `${increase} new complexity ${increase === 1 ? 'hotspot' : 'hotspots'} created`, details: { before: baseHotspots, after: currentHotspots, modules: current.metrics.hotspots || [], metrics: { increase }, }, affected: current.metrics.hotspots || [], impact: { riskLevel: 'medium', maintainability: -2 * increase, performance: -1 * increase, testability: -3 * increase, overallScore: -6 * increase, reasoning: 'Complexity hotspots are harder to understand, test, and maintain', affectedAreas: ['maintainability', 'testability', 'code complexity'], }, }); } return changes; } /** * Detect architecture layer changes */ function detectArchitectureLayerChanges(baseline, current, startId) { const changes = []; const baseLayers = new Set(baseline.metrics.layers || []); const currentLayers = new Set(current.metrics.layers || []); // New layers const newLayers = [...currentLayers].filter(layer => !baseLayers.has(layer)); if (newLayers.length > 0) { changes.push({ id: `change-${startId}`, type: 'architecture-layer-added', severity: 'low', category: 'architecture', description: `${newLayers.length} new architecture ${newLayers.length === 1 ? 'layer' : 'layers'} introduced: ${newLayers.join(', ')}`, details: { before: baseline.metrics.layers || [], after: current.metrics.layers || [], modules: newLayers, }, affected: [], impact: { riskLevel: 'low', maintainability: 1 * newLayers.length, performance: 0, testability: 1 * newLayers.length, overallScore: 2 * newLayers.length, reasoning: 'New layers can improve separation of concerns when used properly', affectedAreas: ['architecture', 'separation of concerns'], }, }); } return changes; } /** * Simple heuristic to detect layer violations */ function detectLayerViolations(graph) { let violations = 0; for (const edge of graph.edges) { const fromPath = edge.from; const toPath = edge.to; // Detect UI → DB violations (bypassing service layer) if ((fromPath.includes('/components/') || fromPath.includes('/ui/')) && (toPath.includes('/database/') || toPath.includes('/db/') || toPath.includes('/models/'))) { violations++; } } return violations; } /** * Calculate impact summary */ function calculateImpactSummary(baseline, current, changes) { const bySeverity = changes.reduce((acc, change) => { acc[change.severity] = (acc[change.severity] || 0) + 1; return acc; }, {}); const byCategory = changes.reduce((acc, change) => { acc[change.category] = (acc[change.category] || 0) + 1; return acc; }, {}); // Calculate health scores (0-100) const baselineHealth = calculateHealthScore(baseline.metrics); const currentHealth = calculateHealthScore(current.metrics); return { totalChanges: changes.length, bySeverity, byCategory, healthChange: { before: baselineHealth, after: currentHealth, delta: currentHealth - baselineHealth, }, keyMetrics: { circularDependencies: { before: baseline.metrics.circularDependencies.length, after: current.metrics.circularDependencies.length, delta: current.metrics.circularDependencies.length - baseline.metrics.circularDependencies.length, }, averageCoupling: { before: baseline.metrics.averageCoupling || baseline.metrics.averageDependencies, after: current.metrics.averageCoupling || current.metrics.averageDependencies, delta: (current.metrics.averageCoupling || current.metrics.averageDependencies) - (baseline.metrics.averageCoupling || baseline.metrics.averageDependencies), }, externalDependencies: { before: baseline.metrics.externalDependencies, after: current.metrics.externalDependencies, delta: current.metrics.externalDependencies - baseline.metrics.externalDependencies, }, moduleCount: { before: baseline.metrics.totalModules || baseline.metrics.totalNodes, after: current.metrics.totalModules || current.metrics.totalNodes, delta: (current.metrics.totalModules || current.metrics.totalNodes) - (baseline.metrics.totalModules || baseline.metrics.totalNodes), }, }, }; } /** * Calculate health score (0-100) based on metrics */ function calculateHealthScore(metrics) { let score = 100; // Penalize circular dependencies score -= metrics.circularDependencies.length * 10; // Penalize high coupling const avgCoupling = metrics.averageCoupling || metrics.averageDependencies; if (avgCoupling > 5) { score -= (avgCoupling - 5) * 5; } // Penalize too many external dependencies relative to modules const totalModules = metrics.totalModules || metrics.totalNodes; const externalRatio = totalModules > 0 ? metrics.externalDependencies / totalModules : 0; if (externalRatio > 2) { score -= (externalRatio - 2) * 10; } // Penalize hotspots score -= (metrics.hotspots?.length || 0) * 3; return Math.max(0, Math.min(100, score)); } /** * Generate recommendations based on changes */ function generateRecommendations(changes, impact) { const recommendations = []; let recId = 1; // Critical issues first const criticalChanges = changes.filter(c => c.severity === 'critical'); if (criticalChanges.length > 0) { recommendations.push({ id: `rec-${recId++}`, priority: 'critical', type: 'fix-issue', title: 'Address Critical Architectural Issues', description: 'Critical architectural problems detected that require immediate attention.', actions: criticalChanges.map(c => `Fix: ${c.description}`), addresses: criticalChanges.map(c => c.id), effort: 'high', expectedImpact: 'Prevent significant technical debt and maintainability issues', }); } // Circular dependencies const circularChanges = changes.filter(c => c.type === 'circular-dependency-introduced'); if (circularChanges.length > 0) { recommendations.push({ id: `rec-${recId++}`, priority: 'high', type: 'refactor', title: 'Resolve Circular Dependencies', description: 'New circular dependencies were introduced that should be resolved.', actions: [ 'Identify the circular dependency chain', 'Extract common functionality to a shared module', 'Use dependency injection or event-driven patterns', 'Consider architectural refactoring', ], addresses: circularChanges.map(c => c.id), effort: 'medium', expectedImpact: 'Improve code organization and testability', }); } // Coupling increases const couplingChanges = changes.filter(c => c.type === 'coupling-increased'); if (couplingChanges.length > 0) { recommendations.push({ id: `rec-${recId++}`, priority: 'medium', type: 'improve-architecture', title: 'Reduce Module Coupling', description: 'Average coupling has increased, which may impact maintainability.', actions: [ 'Review new dependencies and remove unnecessary ones', 'Use interfaces to decouple implementations', 'Consider using dependency injection', 'Extract common functionality to utilities', ], addresses: couplingChanges.map(c => c.id), effort: 'medium', expectedImpact: 'Improve modularity and make code easier to test', }); } // Layer violations const layerChanges = changes.filter(c => c.type === 'layer-violation-introduced'); if (layerChanges.length > 0) { recommendations.push({ id: `rec-${recId++}`, priority: 'high', type: 'fix-issue', title: 'Fix Layer Violations', description: 'New architectural layer violations detected.', actions: [ 'Review direct connections between UI and database layers', 'Ensure all data access goes through service layer', 'Implement proper abstractions and interfaces', 'Set up architectural linting rules', ], addresses: layerChanges.map(c => c.id), effort: 'medium', expectedImpact: 'Maintain proper separation of concerns', }); } // Positive changes const positiveChanges = changes.filter(c => c.type === 'circular-dependency-resolved' || c.type === 'coupling-decreased'); if (positiveChanges.length > 0) { recommendations.push({ id: `rec-${recId++}`, priority: 'low', type: 'monitor', title: 'Continue Good Practices', description: 'Positive architectural changes detected. Keep up the good work!', actions: [ 'Document the refactoring patterns used', 'Share knowledge with the team', 'Consider applying similar patterns elsewhere', ], addresses: positiveChanges.map(c => c.id), effort: 'low', expectedImpact: 'Maintain and spread good architectural practices', }); } return recommendations; } /** * Calculate metrics diff */ function calculateMetricsDiff(baseline, current) { const baseModules = baseline.totalModules || baseline.totalNodes; const currentModules = current.totalModules || current.totalNodes; const baseDeps = baseline.totalDependencies || baseline.totalEdges; const currentDeps = current.totalDependencies || current.totalEdges; const baseCoupling = baseline.averageCoupling || baseline.averageDependencies; const currentCoupling = current.averageCoupling || current.averageDependencies; return { totalModules: currentModules - baseModules, totalDependencies: currentDeps - baseDeps, internalDependencies: current.internalDependencies - baseline.internalDependencies, externalDependencies: current.externalDependencies - baseline.externalDependencies, circularDependencies: current.circularDependencies.length - baseline.circularDependencies.length, averageCoupling: currentCoupling - baseCoupling, newHotspots: (current.hotspots || []).filter(h => !(baseline.hotspots || []).includes(h)), resolvedHotspots: (baseline.hotspots || []).filter(h => !(current.hotspots || []).includes(h)), newLayers: (current.layers || []).filter(l => !(baseline.layers || []).includes(l)), removedLayers: (baseline.layers || []).filter(l => !(current.layers || []).includes(l)), layerViolations: 0, // Simplified for now }; } /** * Convert existing GraphMetrics to enhanced GraphMetrics for diff analysis */ function adaptGraphMetrics(existingMetrics) { const circularCount = existingMetrics.circularDependencies.length; const hotspots = detectHotspots(existingMetrics); const layers = detectArchitectureLayers(existingMetrics); return { totalNodes: existingMetrics.totalNodes, totalEdges: existingMetrics.totalEdges, internalDependencies: existingMetrics.internalDependencies, externalDependencies: existingMetrics.externalDependencies, circularDependencies: existingMetrics.circularDependencies, averageDependencies: existingMetrics.averageDependencies, mostConnectedModule: existingMetrics.mostConnectedModule, hotspots: hotspots, layers: layers, layerViolations: 0, // Will be calculated separately // Computed properties for backwards compatibility averageCoupling: existingMetrics.averageDependencies, totalModules: existingMetrics.totalNodes, totalDependencies: existingMetrics.totalEdges, }; } /** * Detect hotspots (modules with high coupling) */ function detectHotspots(metrics) { // Simple heuristic: modules with above-average dependencies are hotspots const threshold = metrics.averageDependencies * 1.5; const hotspots = []; // For now, just include the most connected module if it exists if (metrics.mostConnectedModule && metrics.averageDependencies > threshold) { hotspots.push(metrics.mostConnectedModule); } return hotspots; } /** * Detect architecture layers from file paths */ function detectArchitectureLayers(metrics) { // Simple heuristic based on common directory patterns const layers = new Set(); // This would normally analyze the actual file paths, but since we don't have them // in the metrics, we'll return common layer patterns const commonLayers = [ 'components', 'services', 'utils', 'types', 'api', 'database', ]; return commonLayers; } /** * Create empty metrics for cases with no files */ function createEmptyMetrics() { return adaptGraphMetrics({ totalNodes: 0, totalEdges: 0, internalDependencies: 0, externalDependencies: 0, circularDependencies: [], averageDependencies: 0, }); } //# sourceMappingURL=graph-diff-analyzer.js.map