PRIX / lib /src /architectural-analyzer.js
Rachit-Tw's picture
Upload 94 files
440cddf verified
Raw
History Blame Contribute Delete
11.1 kB
"use strict";
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