Spaces:
Paused
Paused
| ; | |
| Object.defineProperty(exports, "__esModule", { value: true }); | |
| exports.ArchitecturalAnalyzer = void 0; | |
| exports.analyzeArchitecture = analyzeArchitecture; | |
| const DEFAULT_LAYERS = [ | |
| { | |
| 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 = [ | |
| { | |
| 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' | |
| } | |
| ]; | |
| class ArchitecturalAnalyzer { | |
| layers; | |
| rules; | |
| layerCache; | |
| constructor(layers = DEFAULT_LAYERS, rules = DEFAULT_RULES) { | |
| this.layers = layers; | |
| this.rules = rules; | |
| this.layerCache = new Map(); | |
| } | |
| analyze(symbolGraph) { | |
| const violations = []; | |
| const layerAssignments = new Map(); | |
| const unassignedFiles = []; | |
| 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 | |
| }; | |
| } | |
| assignLayer(filePath) { | |
| 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'; | |
| } | |
| matchPattern(path, pattern) { | |
| const regexPattern = pattern | |
| .replace(/\*\*/g, '<<DOUBLE_STAR>>') | |
| .replace(/\*/g, '[^/]*') | |
| .replace(/<<DOUBLE_STAR>>/g, '.*') | |
| .replace(/\?/g, '.'); | |
| try { | |
| const regex = new RegExp(`^${regexPattern}$`); | |
| return regex.test(path) || this.matchGlob(path, pattern); | |
| } | |
| catch { | |
| return this.matchGlob(path, pattern); | |
| } | |
| } | |
| matchGlob(path, pattern) { | |
| 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; | |
| } | |
| matchSingle(part, pattern) { | |
| 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; | |
| } | |
| } | |
| resolveImportToLayer(importPath, symbolGraph) { | |
| 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'; | |
| } | |
| findRule(fromLayer, toLayer) { | |
| return this.rules.find(r => r.fromLayer === fromLayer && r.toLayer === toLayer); | |
| } | |
| getDependencyGraph(layerAssignments) { | |
| const graph = new Map(); | |
| for (const [file, layer] of layerAssignments) { | |
| if (!graph.has(layer)) { | |
| graph.set(layer, new Set()); | |
| } | |
| } | |
| return graph; | |
| } | |
| generateReport(analysis) { | |
| const lines = []; | |
| 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']) { | |
| 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'); | |
| } | |
| } | |
| exports.ArchitecturalAnalyzer = ArchitecturalAnalyzer; | |
| function analyzeArchitecture(symbolGraph) { | |
| const analyzer = new ArchitecturalAnalyzer(); | |
| return analyzer.analyze(symbolGraph); | |
| } | |
| //# sourceMappingURL=architectural-analyzer.js.map |