PRIX / src /architectural-analyzer.ts
Rachit-Tw's picture
Upload 169 files
9284ad7 verified
Raw
History Blame Contribute Delete
11.2 kB
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<string, string>
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<string, string>
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<string, string>()
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, '<<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)
}
}
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<string, string>
): Map<string, Set<string>> {
const graph = new Map<string, Set<string>>()
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)
}