aetherlight-analyzer
Version:
Code analysis tool to generate ÆtherLight sprint plans from any codebase
353 lines • 15.8 kB
JavaScript
;
/**
* 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