UNPKG

aetherlight-analyzer

Version:

Code analysis tool to generate ÆtherLight sprint plans from any codebase

353 lines 15.8 kB
"use strict"; /** * DESIGN DECISION: Heuristic-based architecture pattern detection * WHY: Most codebases follow common patterns (MVC, Clean, etc.) - can detect via naming and structure * * REASONING CHAIN: * 1. Parse codebase into AST (via parsers) * 2. Analyze file/directory naming conventions (controllers/, services/, models/) * 3. Analyze import patterns (which layers depend on which) * 4. Detect architectural patterns via heuristics (MVC, Clean, Layered) * 5. Generate Mermaid diagram showing layers and relationships * 6. Result: >90% accuracy on common architectures * * PATTERN: Pattern-ANALYZER-001 (AST-Based Code Analysis) * RELATED: TypeScript Parser, Rust Parser * PERFORMANCE: <2s for 100k LOC codebases */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ArchitectureAnalyzer = void 0; const types_1 = require("../parsers/types"); const types_2 = require("./types"); class ArchitectureAnalyzer { /** * Analyze architecture of parsed codebase * * DESIGN DECISION: Multi-phase analysis (naming → structure → relationships) * WHY: Incremental analysis builds confidence, enables fallback if heuristics fail */ analyze(parseResult) { const startTime = Date.now(); // Phase 1: Detect architecture pattern const pattern = this.detectArchitecturePattern(parseResult); // Phase 2: Identify layers const layers = this.identifyLayers(parseResult, pattern); // Phase 3: Extract components const components = this.extractComponents(parseResult, pattern); // Phase 4: Build relationships const relationships = this.buildRelationships(parseResult, components); // Phase 5: Generate diagram const diagram = this.generateMermaidDiagram(layers, components, relationships); // Calculate confidence score const confidence = this.calculateConfidence(pattern, layers, components); const analysis = { pattern, confidence, layers, components, relationships, diagram, }; return { name: 'architecture', version: '1.0.0', executionTimeMs: Date.now() - startTime, data: analysis, }; } /** * Detect architecture pattern via file/directory heuristics * * REASONING CHAIN: * 1. Check for /controllers, /services, /models → MVC * 2. Check for /domain, /application, /infrastructure → Clean Architecture * 3. Check for /adapters, /ports, /core → Hexagonal * 4. Check for layered structure (ui/, business/, data/) → Layered * 5. Default to Monolith or Unknown */ detectArchitecturePattern(parseResult) { const filePaths = parseResult.files.map((f) => f.filePath.toLowerCase()); // MVC detection const hasMvc = filePaths.some((p) => p.includes('controller')) && filePaths.some((p) => p.includes('model')) && filePaths.some((p) => p.includes('view')); if (hasMvc) { return types_2.ArchitecturePattern.MVC; } // Clean Architecture detection const hasClean = filePaths.some((p) => p.includes('domain')) && filePaths.some((p) => p.includes('application')) && filePaths.some((p) => p.includes('infrastructure')); if (hasClean) { return types_2.ArchitecturePattern.CLEAN; } // Hexagonal Architecture detection const hasHexagonal = (filePaths.some((p) => p.includes('adapter')) || filePaths.some((p) => p.includes('port'))) && filePaths.some((p) => p.includes('core')); if (hasHexagonal) { return types_2.ArchitecturePattern.HEXAGONAL; } // Layered Architecture detection const hasLayers = filePaths.some((p) => p.includes('ui') || p.includes('presentation')) && filePaths.some((p) => p.includes('business') || p.includes('logic')) && filePaths.some((p) => p.includes('data') || p.includes('repository')); if (hasLayers) { return types_2.ArchitecturePattern.LAYERED; } // Microservices detection (multiple separate services) const hasServices = filePaths.filter((p) => p.includes('service')).length > 5; if (hasServices) { return types_2.ArchitecturePattern.MICROSERVICES; } // Default: Monolith return parseResult.files.length > 50 ? types_2.ArchitecturePattern.MONOLITH : types_2.ArchitecturePattern.UNKNOWN; } /** * Identify architectural layers * * DESIGN DECISION: Pattern-specific layer extraction * WHY: Different patterns have different layer structures (MVC ≠ Clean) */ identifyLayers(parseResult, pattern) { const layers = []; switch (pattern) { case types_2.ArchitecturePattern.MVC: layers.push(this.extractLayer(parseResult, 'Controllers', ['controller'])); layers.push(this.extractLayer(parseResult, 'Models', ['model'])); layers.push(this.extractLayer(parseResult, 'Views', ['view', 'component', 'ui'])); layers.push(this.extractLayer(parseResult, 'Services', ['service'])); break; case types_2.ArchitecturePattern.CLEAN: layers.push(this.extractLayer(parseResult, 'Domain', ['domain', 'entity'])); layers.push(this.extractLayer(parseResult, 'Application', ['application', 'use-case'])); layers.push(this.extractLayer(parseResult, 'Infrastructure', ['infrastructure', 'repository'])); layers.push(this.extractLayer(parseResult, 'Presentation', ['presentation', 'controller'])); break; case types_2.ArchitecturePattern.HEXAGONAL: layers.push(this.extractLayer(parseResult, 'Core', ['core', 'domain'])); layers.push(this.extractLayer(parseResult, 'Ports', ['port'])); layers.push(this.extractLayer(parseResult, 'Adapters', ['adapter'])); break; case types_2.ArchitecturePattern.LAYERED: layers.push(this.extractLayer(parseResult, 'Presentation', ['ui', 'presentation'])); layers.push(this.extractLayer(parseResult, 'Business', ['business', 'logic', 'service'])); layers.push(this.extractLayer(parseResult, 'Data', ['data', 'repository', 'persistence'])); break; default: // Generic layers for Monolith/Unknown layers.push(this.extractLayer(parseResult, 'API', ['api', 'route', 'endpoint'])); layers.push(this.extractLayer(parseResult, 'Logic', ['service', 'business', 'logic'])); layers.push(this.extractLayer(parseResult, 'Data', ['model', 'entity', 'repository'])); layers.push(this.extractLayer(parseResult, 'UI', ['component', 'view', 'ui'])); break; } // Filter out empty layers return layers.filter((layer) => layer.files.length > 0); } /** * Extract layer from parsed files based on keywords */ extractLayer(parseResult, layerName, keywords) { const files = parseResult.files .filter((f) => { const lowerPath = f.filePath.toLowerCase(); return keywords.some((keyword) => lowerPath.includes(keyword)); }) .map((f) => f.filePath); const linesOfCode = parseResult.files .filter((f) => files.includes(f.filePath)) .reduce((sum, f) => sum + f.linesOfCode, 0); // Calculate average complexity of functions in this layer const complexityScores = []; parseResult.files .filter((f) => files.includes(f.filePath)) .forEach((file) => { file.elements.forEach((elem) => { if (elem.type === types_1.ElementType.FUNCTION || elem.type === types_1.ElementType.METHOD) { const funcElem = elem; if (funcElem.complexity) { complexityScores.push(funcElem.complexity); } } }); }); const avgComplexity = complexityScores.length > 0 ? complexityScores.reduce((a, b) => a + b, 0) / complexityScores.length : 0; const complexity = avgComplexity > 15 ? 'high' : avgComplexity > 8 ? 'medium' : 'low'; return { name: layerName, files, linesOfCode, complexity, dependencies: [], // Filled in later }; } /** * Extract components (individual modules/services) * * DESIGN DECISION: Component = logical unit (class, module, or related files) * WHY: Components are the building blocks of architecture, easier to reason about than raw files */ extractComponents(parseResult, pattern) { const components = []; parseResult.files.forEach((file) => { file.elements.forEach((elem) => { if (elem.type === types_1.ElementType.CLASS) { const componentType = this.inferComponentType(file.filePath, elem.name); components.push({ name: elem.name, type: componentType, files: [file.filePath], responsibilities: [elem.documentation || elem.name], dependencies: this.extractElementDependencies(file, elem), }); } }); }); return components; } /** * Infer component type from file path and name */ inferComponentType(filePath, elementName) { const lowerPath = filePath.toLowerCase(); const lowerName = elementName.toLowerCase(); if (lowerPath.includes('controller') || lowerName.includes('controller')) { return types_2.ComponentType.CONTROLLER; } if (lowerPath.includes('service') || lowerName.includes('service')) { return types_2.ComponentType.SERVICE; } if (lowerPath.includes('model') || lowerName.includes('model')) { return types_2.ComponentType.MODEL; } if (lowerPath.includes('repository') || lowerName.includes('repository')) { return types_2.ComponentType.REPOSITORY; } if (lowerPath.includes('view') || lowerPath.includes('component')) { return types_2.ComponentType.VIEW; } if (lowerPath.includes('middleware')) { return types_2.ComponentType.MIDDLEWARE; } if (lowerPath.includes('route') || lowerPath.includes('router')) { return types_2.ComponentType.ROUTER; } return types_2.ComponentType.UTILITY; } /** * Extract dependencies for a specific element */ extractElementDependencies(file, elem) { const dependencies = []; // Extract dependencies from file imports file.dependencies.forEach((dep) => { if (dep.importedSymbols) { dep.importedSymbols.forEach((symbol) => { dependencies.push(symbol); }); } }); return dependencies; } /** * Build relationships between components */ buildRelationships(parseResult, components) { const relationships = []; parseResult.files.forEach((file) => { file.dependencies.forEach((dep) => { // Find source component const fromComponent = components.find((c) => c.files.includes(file.filePath)); if (!fromComponent) return; // Find target component (match by imported symbols) const toComponents = components.filter((c) => dep.importedSymbols?.some((symbol) => c.name === symbol || c.dependencies.includes(symbol))); toComponents.forEach((toComponent) => { if (fromComponent.name !== toComponent.name) { relationships.push({ from: fromComponent.name, to: toComponent.name, type: types_2.RelationshipType.DEPENDS_ON, strength: 0.5, // Default strength }); } }); }); }); return relationships; } /** * Generate Mermaid diagram * * DESIGN DECISION: Mermaid format for architecture visualization * WHY: Markdown-compatible, renders in GitHub, VS Code, documentation sites */ generateMermaidDiagram(layers, components, relationships) { let diagram = 'graph TD\n'; // Add layers as subgraphs layers.forEach((layer) => { diagram += ` subgraph ${layer.name}\n`; const layerComponents = components.filter((c) => layer.files.some((f) => c.files.includes(f))); layerComponents.forEach((comp) => { diagram += ` ${this.sanitizeId(comp.name)}["${comp.name} (${comp.type})"]\n`; }); diagram += ' end\n'; }); // Add relationships relationships.forEach((rel) => { const arrow = rel.type === types_2.RelationshipType.EXTENDS ? '-->|extends|' : '-->'; diagram += ` ${this.sanitizeId(rel.from)} ${arrow} ${this.sanitizeId(rel.to)}\n`; }); return diagram; } /** * Sanitize ID for Mermaid (remove special characters) */ sanitizeId(id) { return id.replace(/[^a-zA-Z0-9]/g, '_'); } /** * Calculate confidence score for detected pattern * * DESIGN DECISION: Confidence based on evidence strength * WHY: Inform user if detection is uncertain, suggest manual review * * Confidence = (layer_coverage + component_coverage + relationship_density) / 3 */ calculateConfidence(pattern, layers, components) { if (pattern === types_2.ArchitecturePattern.UNKNOWN) { return 0.0; } // Layer coverage: How many expected layers were found? const expectedLayerCount = this.getExpectedLayerCount(pattern); const layerCoverage = Math.min(layers.length / expectedLayerCount, 1.0); // Component coverage: Do components exist in all layers? const componentCoverage = layers.filter((layer) => layer.files.length > 0).length / layers.length; // Relationship density: Are components connected? const relationshipDensity = components.length > 0 ? Math.min(components.length / 10, 1.0) : 0.0; return (layerCoverage + componentCoverage + relationshipDensity) / 3; } /** * Get expected layer count for pattern */ getExpectedLayerCount(pattern) { switch (pattern) { case types_2.ArchitecturePattern.MVC: return 4; // Controllers, Models, Views, Services case types_2.ArchitecturePattern.CLEAN: return 4; // Domain, Application, Infrastructure, Presentation case types_2.ArchitecturePattern.HEXAGONAL: return 3; // Core, Ports, Adapters case types_2.ArchitecturePattern.LAYERED: return 3; // Presentation, Business, Data default: return 4; // Generic layers } } } exports.ArchitectureAnalyzer = ArchitectureAnalyzer; //# sourceMappingURL=architecture-analyzer.js.map