import {SymbolGraph, Symbol, DependencyEdge} from './symbol-graph' export interface LayerDefinition { name: string patterns: string[] priority: number } export interface ArchitecturalRule { fromLayer: string toLayer: string allowed: boolean severity: 'critical' | 'major' | 'minor' description: string } export interface ArchitecturalViolation { from: string to: string fromLayer: string toLayer: string severity: 'critical' | 'major' | 'minor' description: string file?: string suggestedFix?: string } export interface ArchitectureAnalysis { violations: ArchitecturalViolation[] layerAssignments: Map unassignedFiles: string[] statistics: { totalViolations: number criticalCount: number majorCount: number minorCount: number } } const DEFAULT_LAYERS: LayerDefinition[] = [ { name: 'ui', patterns: [ '**/components/**', '**/pages/**', '**/views/**', '**/screens/**', '**/ui/**', '**/frontend/**' ], priority: 1 }, { name: 'api', patterns: [ '**/api/**', '**/controllers/**', '**/handlers/**', '**/routes/**', '**/endpoints/**' ], priority: 2 }, { name: 'services', patterns: [ '**/services/**', '**/business/**', '**/domain/**', '**/usecases/**' ], priority: 3 }, { name: 'data', patterns: [ '**/models/**', '**/schemas/**', '**/entities/**', '**/repositories/**', '**/dal/**' ], priority: 4 }, { name: 'infrastructure', patterns: [ '**/db/**', '**/database/**', '**/cache/**', '**/queue/**', '**/storage/**', '**/external/**' ], priority: 5 }, { name: 'shared', patterns: [ '**/shared/**', '**/common/**', '**/utils/**', '**/lib/**', '**/types/**' ], priority: 0 } ] const DEFAULT_RULES: ArchitecturalRule[] = [ { fromLayer: 'ui', toLayer: 'data', allowed: false, severity: 'critical', description: 'UI should not directly access data layer' }, { fromLayer: 'ui', toLayer: 'infrastructure', allowed: false, severity: 'critical', description: 'UI should not directly access infrastructure' }, { fromLayer: 'api', toLayer: 'ui', allowed: false, severity: 'major', description: 'API layer should not depend on UI' }, { fromLayer: 'api', toLayer: 'infrastructure', allowed: true, severity: 'minor', description: 'API may access infrastructure (database, cache)' }, { fromLayer: 'services', toLayer: 'ui', allowed: false, severity: 'major', description: 'Services should not depend on UI' }, { fromLayer: 'services', toLayer: 'infrastructure', allowed: true, severity: 'minor', description: 'Services may access infrastructure' }, { fromLayer: 'data', toLayer: 'api', allowed: false, severity: 'major', description: 'Data layer should not depend on API layer' }, { fromLayer: 'data', toLayer: 'infrastructure', allowed: false, severity: 'critical', description: 'Data layer should not depend on infrastructure' }, { fromLayer: 'shared', toLayer: 'ui', allowed: false, severity: 'minor', description: 'Shared code should not depend on UI' }, { fromLayer: 'shared', toLayer: 'api', allowed: false, severity: 'minor', description: 'Shared code should not depend on API' } ] export class ArchitecturalAnalyzer { private layers: LayerDefinition[] private rules: ArchitecturalRule[] private layerCache: Map constructor( layers: LayerDefinition[] = DEFAULT_LAYERS, rules: ArchitecturalRule[] = DEFAULT_RULES ) { this.layers = layers this.rules = rules this.layerCache = new Map() } analyze(symbolGraph: SymbolGraph): ArchitectureAnalysis { const violations: ArchitecturalViolation[] = [] const layerAssignments = new Map() const unassignedFiles: string[] = [] for (const [key, symbol] of symbolGraph.symbols) { const file = symbol.file if (!layerAssignments.has(file)) { const layer = this.assignLayer(file) layerAssignments.set(file, layer) if (layer === 'unassigned') { unassignedFiles.push(file) } } } for (const edge of symbolGraph.edges) { const fromLayer = layerAssignments.get(edge.from) || 'unassigned' const toLayer = this.resolveImportToLayer(edge.to, symbolGraph) if (fromLayer === 'unassigned' || toLayer === 'unassigned') continue const rule = this.findRule(fromLayer, toLayer) if (rule && !rule.allowed) { violations.push({ from: edge.from, to: edge.to, fromLayer, toLayer, severity: rule.severity, description: rule.description, file: edge.from }) } } const statistics = { totalViolations: violations.length, criticalCount: violations.filter(v => v.severity === 'critical').length, majorCount: violations.filter(v => v.severity === 'major').length, minorCount: violations.filter(v => v.severity === 'minor').length } return { violations, layerAssignments, unassignedFiles, statistics } } private assignLayer(filePath: string): string { const cached = this.layerCache.get(filePath) if (cached) return cached const normalizedPath = filePath.replace(/\\/g, '/').toLowerCase() for (const layer of this.layers) { for (const pattern of layer.patterns) { if (this.matchPattern(normalizedPath, pattern.toLowerCase())) { this.layerCache.set(filePath, layer.name) return layer.name } } } this.layerCache.set(filePath, 'unassigned') return 'unassigned' } private matchPattern(path: string, pattern: string): boolean { const regexPattern = pattern .replace(/\*\*/g, '<>') .replace(/\*/g, '[^/]*') .replace(/<>/g, '.*') .replace(/\?/g, '.') try { const regex = new RegExp(`^${regexPattern}$`) return regex.test(path) || this.matchGlob(path, pattern) } catch { return this.matchGlob(path, pattern) } } private matchGlob(path: string, pattern: string): boolean { const parts = path.split('/') const patternParts = pattern.split('/') let pi = 0 let psi = 0 while (pi < parts.length && psi < patternParts.length) { const patternPart = patternParts[psi] if (patternPart === '**') { if (psi === patternParts.length - 1) { return true } const nextPattern = patternParts[psi + 1] while (pi < parts.length) { if (this.matchSingle(parts[pi], nextPattern)) { if ( this.matchGlob( parts.slice(pi).join('/'), patternParts.slice(psi + 1).join('/') ) ) { return true } } pi++ } return false } if (!this.matchSingle(parts[pi], patternPart)) { return false } pi++ psi++ } return pi === parts.length && psi === patternParts.length } private matchSingle(part: string, pattern: string): boolean { if (pattern === '*') return part.length > 0 if (pattern === '**') return true const regexPattern = pattern .replace(/\./g, '\\.') .replace(/\*/g, '[^/]*') .replace(/\?/g, '.') try { const regex = new RegExp(`^${regexPattern}$`) return regex.test(part) } catch { return part === pattern } } private resolveImportToLayer( importPath: string, symbolGraph: SymbolGraph ): string { const normalizedImport = importPath.replace(/['"]/g, '').replace(/\\/g, '/') for (const [key, symbol] of symbolGraph.symbols) { const symbolPath = symbol.file if ( symbolPath.includes(normalizedImport) || normalizedImport.includes(symbolPath) ) { return this.assignLayer(symbolPath) } } if (normalizedImport.startsWith('.')) { return 'unassigned' } if (normalizedImport.includes('/node_modules/')) { return 'external' } if ( normalizedImport.includes('/shared/') || normalizedImport.includes('/common/') ) { return 'shared' } return 'unassigned' } private findRule( fromLayer: string, toLayer: string ): ArchitecturalRule | undefined { return this.rules.find( r => r.fromLayer === fromLayer && r.toLayer === toLayer ) } getDependencyGraph( layerAssignments: Map ): Map> { const graph = new Map>() for (const [file, layer] of layerAssignments) { if (!graph.has(layer)) { graph.set(layer, new Set()) } } return graph } generateReport(analysis: ArchitectureAnalysis): string { const lines: string[] = [] lines.push('# Architectural Violation Report') lines.push('') lines.push(`Total Violations: ${analysis.statistics.totalViolations}`) lines.push(` - Critical: ${analysis.statistics.criticalCount}`) lines.push(` - Major: ${analysis.statistics.majorCount}`) lines.push(` - Minor: ${analysis.statistics.minorCount}`) lines.push('') if (analysis.violations.length > 0) { lines.push('## Violations') lines.push('') const groupedBySeverity = { critical: analysis.violations.filter(v => v.severity === 'critical'), major: analysis.violations.filter(v => v.severity === 'major'), minor: analysis.violations.filter(v => v.severity === 'minor') } for (const severity of ['critical', 'major', 'minor'] as const) { const violations = groupedBySeverity[severity] if (violations.length === 0) continue lines.push(`### ${severity.toUpperCase()} (${violations.length})`) lines.push('') for (const v of violations) { lines.push( `- **${v.fromLayer}** → **${v.toLayer}**: ${v.description}` ) lines.push(` - File: \`${v.file || v.from}\``) lines.push(` - Import: \`${v.to}\``) } lines.push('') } } return lines.join('\n') } } export function analyzeArchitecture( symbolGraph: SymbolGraph ): ArchitectureAnalysis { const analyzer = new ArchitecturalAnalyzer() return analyzer.analyze(symbolGraph) }