@girardelli/architect-core 8.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/core/analyzer.d.ts +42 -0
- package/dist/src/core/analyzer.js +431 -0
- package/dist/src/core/analyzer.js.map +1 -0
- package/dist/src/core/analyzers/forecast.d.ts +84 -0
- package/dist/src/core/analyzers/forecast.js +338 -0
- package/dist/src/core/analyzers/forecast.js.map +1 -0
- package/dist/src/core/analyzers/index.d.ts +9 -0
- package/dist/src/core/analyzers/index.js +7 -0
- package/dist/src/core/analyzers/index.js.map +1 -0
- package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
- package/dist/src/core/analyzers/temporal-scorer.js +141 -0
- package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
- package/dist/src/core/anti-patterns.d.ts +28 -0
- package/dist/src/core/anti-patterns.js +264 -0
- package/dist/src/core/anti-patterns.js.map +1 -0
- package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
- package/dist/src/core/ast/ast-parser.interface.js +2 -0
- package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
- package/dist/src/core/ast/path-resolver.d.ts +13 -0
- package/dist/src/core/ast/path-resolver.js +54 -0
- package/dist/src/core/ast/path-resolver.js.map +1 -0
- package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
- package/dist/src/core/ast/tree-sitter-parser.js +142 -0
- package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
- package/dist/src/core/config.d.ts +11 -0
- package/dist/src/core/config.js +112 -0
- package/dist/src/core/config.js.map +1 -0
- package/dist/src/core/diagram.d.ts +9 -0
- package/dist/src/core/diagram.js +101 -0
- package/dist/src/core/diagram.js.map +1 -0
- package/dist/src/core/i18n.d.ts +14 -0
- package/dist/src/core/i18n.js +54 -0
- package/dist/src/core/i18n.js.map +1 -0
- package/dist/src/core/locales/en.d.ts +2 -0
- package/dist/src/core/locales/en.js +337 -0
- package/dist/src/core/locales/en.js.map +1 -0
- package/dist/src/core/locales/pt-BR.d.ts +172 -0
- package/dist/src/core/locales/pt-BR.js +337 -0
- package/dist/src/core/locales/pt-BR.js.map +1 -0
- package/dist/src/core/locales/types.d.ts +86 -0
- package/dist/src/core/locales/types.js +2 -0
- package/dist/src/core/locales/types.js.map +1 -0
- package/dist/src/core/plugin-loader.d.ts +11 -0
- package/dist/src/core/plugin-loader.js +67 -0
- package/dist/src/core/plugin-loader.js.map +1 -0
- package/dist/src/core/project-summarizer.d.ts +16 -0
- package/dist/src/core/project-summarizer.js +37 -0
- package/dist/src/core/project-summarizer.js.map +1 -0
- package/dist/src/core/refactor-engine.d.ts +18 -0
- package/dist/src/core/refactor-engine.js +87 -0
- package/dist/src/core/refactor-engine.js.map +1 -0
- package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
- package/dist/src/core/rules/barrel-optimizer.js +76 -0
- package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
- package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
- package/dist/src/core/rules/dead-code-detector.js +116 -0
- package/dist/src/core/rules/dead-code-detector.js.map +1 -0
- package/dist/src/core/rules/hub-splitter.d.ts +13 -0
- package/dist/src/core/rules/hub-splitter.js +117 -0
- package/dist/src/core/rules/hub-splitter.js.map +1 -0
- package/dist/src/core/rules/import-organizer.d.ts +13 -0
- package/dist/src/core/rules/import-organizer.js +84 -0
- package/dist/src/core/rules/import-organizer.js.map +1 -0
- package/dist/src/core/rules/module-grouper.d.ts +13 -0
- package/dist/src/core/rules/module-grouper.js +116 -0
- package/dist/src/core/rules/module-grouper.js.map +1 -0
- package/dist/src/core/rules-engine.d.ts +7 -0
- package/dist/src/core/rules-engine.js +89 -0
- package/dist/src/core/rules-engine.js.map +1 -0
- package/dist/src/core/scorer.d.ts +15 -0
- package/dist/src/core/scorer.js +165 -0
- package/dist/src/core/scorer.js.map +1 -0
- package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
- package/dist/src/core/summarizer/keyword-extractor.js +38 -0
- package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
- package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
- package/dist/src/core/summarizer/module-inferrer.js +171 -0
- package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/package-reader.d.ts +3 -0
- package/dist/src/core/summarizer/package-reader.js +33 -0
- package/dist/src/core/summarizer/package-reader.js.map +1 -0
- package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
- package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
- package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
- package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
- package/dist/src/core/summarizer/readme-reader.js +24 -0
- package/dist/src/core/summarizer/readme-reader.js.map +1 -0
- package/dist/src/core/types/architect-rules.d.ts +27 -0
- package/dist/src/core/types/architect-rules.js +2 -0
- package/dist/src/core/types/architect-rules.js.map +1 -0
- package/dist/src/core/types/core.d.ts +87 -0
- package/dist/src/core/types/core.js +2 -0
- package/dist/src/core/types/core.js.map +1 -0
- package/dist/src/core/types/infrastructure.d.ts +38 -0
- package/dist/src/core/types/infrastructure.js +2 -0
- package/dist/src/core/types/infrastructure.js.map +1 -0
- package/dist/src/core/types/plugin.d.ts +12 -0
- package/dist/src/core/types/plugin.js +2 -0
- package/dist/src/core/types/plugin.js.map +1 -0
- package/dist/src/core/types/rules.d.ts +53 -0
- package/dist/src/core/types/rules.js +2 -0
- package/dist/src/core/types/rules.js.map +1 -0
- package/dist/src/core/types/summarizer.d.ts +12 -0
- package/dist/src/core/types/summarizer.js +2 -0
- package/dist/src/core/types/summarizer.js.map +1 -0
- package/dist/src/infrastructure/git-cache.d.ts +6 -0
- package/dist/src/infrastructure/git-cache.js +41 -0
- package/dist/src/infrastructure/git-cache.js.map +1 -0
- package/dist/src/infrastructure/git-history.d.ts +112 -0
- package/dist/src/infrastructure/git-history.js +340 -0
- package/dist/src/infrastructure/git-history.js.map +1 -0
- package/dist/src/infrastructure/logger.d.ts +20 -0
- package/dist/src/infrastructure/logger.js +57 -0
- package/dist/src/infrastructure/logger.js.map +1 -0
- package/dist/src/infrastructure/scanner.d.ts +31 -0
- package/dist/src/infrastructure/scanner.js +334 -0
- package/dist/src/infrastructure/scanner.js.map +1 -0
- package/dist/tests/analyzers-integration.test.d.ts +7 -0
- package/dist/tests/analyzers-integration.test.js +140 -0
- package/dist/tests/analyzers-integration.test.js.map +1 -0
- package/dist/tests/anti-patterns.test.d.ts +1 -0
- package/dist/tests/anti-patterns.test.js +81 -0
- package/dist/tests/anti-patterns.test.js.map +1 -0
- package/dist/tests/ast-parser.test.d.ts +1 -0
- package/dist/tests/ast-parser.test.js +94 -0
- package/dist/tests/ast-parser.test.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
- package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
- package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
- package/dist/tests/forecast.test.d.ts +7 -0
- package/dist/tests/forecast.test.js +380 -0
- package/dist/tests/forecast.test.js.map +1 -0
- package/dist/tests/git-history.test.d.ts +7 -0
- package/dist/tests/git-history.test.js +193 -0
- package/dist/tests/git-history.test.js.map +1 -0
- package/dist/tests/i18n.test.d.ts +1 -0
- package/dist/tests/i18n.test.js +39 -0
- package/dist/tests/i18n.test.js.map +1 -0
- package/dist/tests/monorepo-scan.test.d.ts +11 -0
- package/dist/tests/monorepo-scan.test.js +143 -0
- package/dist/tests/monorepo-scan.test.js.map +1 -0
- package/dist/tests/plugin-loader.test.d.ts +1 -0
- package/dist/tests/plugin-loader.test.js +31 -0
- package/dist/tests/plugin-loader.test.js.map +1 -0
- package/dist/tests/rules-engine.test.d.ts +1 -0
- package/dist/tests/rules-engine.test.js +112 -0
- package/dist/tests/rules-engine.test.js.map +1 -0
- package/dist/tests/scanner.test.d.ts +1 -0
- package/dist/tests/scanner.test.js +44 -0
- package/dist/tests/scanner.test.js.map +1 -0
- package/dist/tests/scorer.test.d.ts +1 -0
- package/dist/tests/scorer.test.js +610 -0
- package/dist/tests/scorer.test.js.map +1 -0
- package/dist/tests/temporal-scorer.test.d.ts +7 -0
- package/dist/tests/temporal-scorer.test.js +239 -0
- package/dist/tests/temporal-scorer.test.js.map +1 -0
- package/package.json +29 -0
- package/src/core/analyzer.ts +499 -0
- package/src/core/analyzers/forecast.ts +497 -0
- package/src/core/analyzers/index.ts +33 -0
- package/src/core/analyzers/temporal-scorer.ts +227 -0
- package/src/core/anti-patterns.ts +324 -0
- package/src/core/ast/ast-parser.interface.ts +21 -0
- package/src/core/ast/path-resolver.ts +61 -0
- package/src/core/ast/tree-sitter-parser.ts +158 -0
- package/src/core/config.ts +125 -0
- package/src/core/diagram.ts +129 -0
- package/src/core/i18n.ts +64 -0
- package/src/core/locales/en.ts +340 -0
- package/src/core/locales/pt-BR.ts +341 -0
- package/src/core/locales/types.ts +95 -0
- package/src/core/plugin-loader.ts +80 -0
- package/src/core/project-summarizer.ts +42 -0
- package/src/core/refactor-engine.ts +112 -0
- package/src/core/rules/barrel-optimizer.ts +99 -0
- package/src/core/rules/dead-code-detector.ts +134 -0
- package/src/core/rules/hub-splitter.ts +135 -0
- package/src/core/rules/import-organizer.ts +100 -0
- package/src/core/rules/module-grouper.ts +133 -0
- package/src/core/rules-engine.ts +100 -0
- package/src/core/scorer.ts +181 -0
- package/src/core/summarizer/keyword-extractor.ts +53 -0
- package/src/core/summarizer/module-inferrer.ts +194 -0
- package/src/core/summarizer/package-reader.ts +34 -0
- package/src/core/summarizer/purpose-inferrer.ts +197 -0
- package/src/core/summarizer/readme-reader.ts +24 -0
- package/src/core/types/architect-rules.ts +29 -0
- package/src/core/types/core.ts +94 -0
- package/src/core/types/infrastructure.ts +41 -0
- package/src/core/types/plugin.ts +19 -0
- package/src/core/types/rules.ts +51 -0
- package/src/core/types/summarizer.ts +8 -0
- package/src/infrastructure/git-cache.ts +52 -0
- package/src/infrastructure/git-history.ts +496 -0
- package/src/infrastructure/logger.ts +68 -0
- package/src/infrastructure/scanner.ts +349 -0
- package/tests/analyzers-integration.test.ts +174 -0
- package/tests/anti-patterns.test.ts +95 -0
- package/tests/ast-parser.test.ts +102 -0
- package/tests/fixtures/monorepo/package.json +6 -0
- package/tests/fixtures/monorepo/packages/app/package.json +12 -0
- package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
- package/tests/fixtures/monorepo/packages/core/package.json +7 -0
- package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
- package/tests/forecast.test.ts +504 -0
- package/tests/git-history.test.ts +254 -0
- package/tests/i18n.test.ts +47 -0
- package/tests/monorepo-scan.test.ts +170 -0
- package/tests/plugin-loader.test.ts +40 -0
- package/tests/rules-engine.test.ts +131 -0
- package/tests/scanner.test.ts +54 -0
- package/tests/scorer.test.ts +675 -0
- package/tests/temporal-scorer.test.ts +306 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { basename, dirname} from 'path';
|
|
2
|
+
import { AnalysisReport } from '../types/core.js';
|
|
3
|
+
import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hub Splitter Rule (Tier 1)
|
|
7
|
+
* Detects files with many connections and generates split plans.
|
|
8
|
+
* A "hub" is a file that many other files depend on, creating tight coupling.
|
|
9
|
+
*/
|
|
10
|
+
export class HubSplitterRule implements RefactorRule {
|
|
11
|
+
name = 'hub-splitter';
|
|
12
|
+
tier = 1 as const;
|
|
13
|
+
|
|
14
|
+
analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
|
|
15
|
+
const steps: RefactorStep[] = [];
|
|
16
|
+
|
|
17
|
+
// Count connections per node
|
|
18
|
+
const connectionCount: Record<string, { incoming: string[]; outgoing: string[] }> = {};
|
|
19
|
+
|
|
20
|
+
for (const edge of report.dependencyGraph.edges) {
|
|
21
|
+
if (!connectionCount[edge.from]) connectionCount[edge.from] = { incoming: [], outgoing: [] };
|
|
22
|
+
if (!connectionCount[edge.to]) connectionCount[edge.to] = { incoming: [], outgoing: [] };
|
|
23
|
+
connectionCount[edge.from].outgoing.push(edge.to);
|
|
24
|
+
connectionCount[edge.to].incoming.push(edge.from);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Find hubs (5+ incoming connections, not barrel files)
|
|
28
|
+
const barrelFiles = new Set(['__init__.py', 'index.ts', 'index.js', 'index.tsx', 'mod.rs']);
|
|
29
|
+
|
|
30
|
+
for (const [file, connections] of Object.entries(connectionCount)) {
|
|
31
|
+
const fileName = basename(file);
|
|
32
|
+
if (barrelFiles.has(fileName)) continue;
|
|
33
|
+
|
|
34
|
+
// Ignora Tipos base/DTOs - Alto acoplamento em tipos é sinal de maturidade, não gargalo
|
|
35
|
+
const lowerFile = file.toLowerCase();
|
|
36
|
+
if (lowerFile.includes('types') || lowerFile.includes('interface') || lowerFile.includes('/types/')) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Tolerância Arquitetural: Eleva de 5 pra 8 dependentes pra engatilhar quebra (Módulos Coesos maduros)
|
|
41
|
+
if (connections.incoming.length < 8) continue;
|
|
42
|
+
|
|
43
|
+
const operations: FileOperation[] = [];
|
|
44
|
+
|
|
45
|
+
// Determine if this is a dot-notation module or a real file
|
|
46
|
+
const isDotNotation = !file.includes('/') && !file.includes('\\');
|
|
47
|
+
const moduleName = isDotNotation
|
|
48
|
+
? file.split('.').pop() || file
|
|
49
|
+
: fileName.replace(/\.[^.]+$/, '');
|
|
50
|
+
const moduleDir = isDotNotation
|
|
51
|
+
? file.split('.').slice(0, -1).join('/')
|
|
52
|
+
: dirname(file);
|
|
53
|
+
const ext = isDotNotation ? 'py' : (fileName.split('.').pop() || 'py');
|
|
54
|
+
|
|
55
|
+
// Analyze what dependents import to suggest groupings
|
|
56
|
+
const dependentGroups = this.groupDependents(connections.incoming);
|
|
57
|
+
|
|
58
|
+
// Suggest splitting into domain modules
|
|
59
|
+
if (dependentGroups.length >= 2) {
|
|
60
|
+
for (const group of dependentGroups) {
|
|
61
|
+
const newFileName = `${moduleName}_${group.name}.${ext}`;
|
|
62
|
+
const newPath = moduleDir ? `${moduleDir}/${newFileName}` : newFileName;
|
|
63
|
+
|
|
64
|
+
operations.push({
|
|
65
|
+
type: 'CREATE',
|
|
66
|
+
path: newPath,
|
|
67
|
+
description: `Create \`${newFileName}\` with functionality used by: ${group.dependents.join(', ')}`,
|
|
68
|
+
content: ext === 'py'
|
|
69
|
+
? `"""${moduleName}_${group.name} — extracted from ${moduleName}."""\n# Used by: ${group.dependents.join(', ')}\n`
|
|
70
|
+
: `// ${moduleName}_${group.name} — extracted from ${moduleName}\n// Used by: ${group.dependents.join(', ')}\n`,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Update imports in all dependents
|
|
75
|
+
for (const dependent of connections.incoming) {
|
|
76
|
+
operations.push({
|
|
77
|
+
type: 'MODIFY',
|
|
78
|
+
path: dependent,
|
|
79
|
+
description: `Update imports in \`${basename(dependent)}\` to use new split modules`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Mark original for refactoring
|
|
84
|
+
operations.push({
|
|
85
|
+
type: 'MODIFY',
|
|
86
|
+
path: isDotNotation ? `${moduleDir}/${moduleName}.${ext}` : file,
|
|
87
|
+
description: `Refactor \`${moduleName}.${ext}\` — extract grouped functionality to new modules`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (operations.length > 0) {
|
|
92
|
+
steps.push({
|
|
93
|
+
id: 0,
|
|
94
|
+
tier: 1,
|
|
95
|
+
rule: this.name,
|
|
96
|
+
priority: connections.incoming.length >= 8 ? 'CRITICAL' : 'HIGH',
|
|
97
|
+
title: `Split hub file: ${moduleName}.${ext}`,
|
|
98
|
+
description: `\`${file}\` has ${connections.incoming.length} incoming connections. ` +
|
|
99
|
+
`Split into ${dependentGroups.length} focused modules to reduce coupling.`,
|
|
100
|
+
rationale: `High fan-in (${connections.incoming.length} files depend on this) creates a bottleneck. ` +
|
|
101
|
+
`Changes to this file ripple to ${connections.incoming.length} other files. ` +
|
|
102
|
+
`Splitting by usage pattern reduces blast radius.`,
|
|
103
|
+
operations,
|
|
104
|
+
scoreImpact: [
|
|
105
|
+
{ metric: 'coupling', before: report.score.breakdown.coupling, after: Math.min(95, report.score.breakdown.coupling + 15) },
|
|
106
|
+
],
|
|
107
|
+
aiPrompt: `Analyze the file \`${file}\`. Based on its incoming connections, it acts as a coupling bottleneck. Please split this file into the following smaller modules:\n` +
|
|
108
|
+
dependentGroups.map(g => `- \`${moduleName}_${g.name}.${ext}\`: Extract functionality specific to these dependents: ${g.dependents.join(', ')}`).join('\n') +
|
|
109
|
+
`\n\nAfter splitting, securely and automatically update all ${connections.incoming.length} dependent files to import from the new specific modules instead of the monolithic \`${moduleName}.${ext}\`. DO NOT remove any functionality, only move it.`,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return steps;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private groupDependents(
|
|
118
|
+
dependents: string[]
|
|
119
|
+
): Array<{ name: string; dependents: string[] }> {
|
|
120
|
+
// Group by top-level directory
|
|
121
|
+
const groups: Record<string, string[]> = {};
|
|
122
|
+
|
|
123
|
+
for (const dep of dependents) {
|
|
124
|
+
const parts = dep.includes('/') ? dep.split('/') : dep.split('.');
|
|
125
|
+
const groupName = parts.length >= 2 ? parts[parts.length - 2] : 'core';
|
|
126
|
+
if (!groups[groupName]) groups[groupName] = [];
|
|
127
|
+
groups[groupName].push(basename(dep));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return Object.entries(groups).map(([name, deps]) => ({
|
|
131
|
+
name,
|
|
132
|
+
dependents: deps,
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { basename, dirname } from 'path';
|
|
2
|
+
import { AnalysisReport } from '../types/core.js';
|
|
3
|
+
import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Import Organizer Rule (Tier 1)
|
|
7
|
+
* Detects files that import from too many different modules (cross-boundary).
|
|
8
|
+
* Suggests dependency injection or facade patterns.
|
|
9
|
+
*/
|
|
10
|
+
export class ImportOrganizerRule implements RefactorRule {
|
|
11
|
+
name = 'import-organizer';
|
|
12
|
+
tier = 1 as const;
|
|
13
|
+
|
|
14
|
+
analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
|
|
15
|
+
const steps: RefactorStep[] = [];
|
|
16
|
+
|
|
17
|
+
// Find files that import from many different directories
|
|
18
|
+
const crossBoundary: Record<string, { targets: Set<string>; dirs: Set<string> }> = {};
|
|
19
|
+
|
|
20
|
+
for (const edge of report.dependencyGraph.edges) {
|
|
21
|
+
const fromDir = dirname(edge.from);
|
|
22
|
+
const toDir = dirname(edge.to);
|
|
23
|
+
|
|
24
|
+
if (!crossBoundary[edge.from]) {
|
|
25
|
+
crossBoundary[edge.from] = { targets: new Set(), dirs: new Set() };
|
|
26
|
+
}
|
|
27
|
+
crossBoundary[edge.from].targets.add(edge.to);
|
|
28
|
+
|
|
29
|
+
if (fromDir !== toDir) {
|
|
30
|
+
crossBoundary[edge.from].dirs.add(toDir);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Outliers reais espalhados consumindo +5 diretórios. Testa bypass em unit-tests (.test.) naturais de altíssima injeção (mock).
|
|
35
|
+
const violators = Object.entries(crossBoundary)
|
|
36
|
+
.filter(([fileName, data]) => data.dirs.size >= 5 && !fileName.includes('.test.'))
|
|
37
|
+
.sort((a, b) => b[1].dirs.size - a[1].dirs.size);
|
|
38
|
+
|
|
39
|
+
for (const [file, data] of violators) {
|
|
40
|
+
const operations: FileOperation[] = [];
|
|
41
|
+
const fileName = basename(file);
|
|
42
|
+
const fileDir = dirname(file);
|
|
43
|
+
|
|
44
|
+
// Suggest creating a facade/service layer
|
|
45
|
+
const ext = fileName.split('.').pop() || 'py';
|
|
46
|
+
const nameBase = fileName.replace(/\.[^.]+$/, '');
|
|
47
|
+
const facadePath = `${fileDir}/${nameBase}_deps.${ext}`;
|
|
48
|
+
|
|
49
|
+
operations.push({
|
|
50
|
+
type: 'CREATE',
|
|
51
|
+
path: facadePath,
|
|
52
|
+
description: `Create dependency facade \`${basename(facadePath)}\` — centralizes ${data.dirs.size} cross-module imports`,
|
|
53
|
+
content: this.generateFacadeContent(ext, Array.from(data.targets), Array.from(data.dirs)),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
operations.push({
|
|
57
|
+
type: 'MODIFY',
|
|
58
|
+
path: file,
|
|
59
|
+
description: `Refactor \`${fileName}\` to import from local facade instead of ${data.dirs.size} different modules`,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
steps.push({
|
|
63
|
+
id: 0,
|
|
64
|
+
tier: 1,
|
|
65
|
+
rule: this.name,
|
|
66
|
+
priority: data.dirs.size >= 5 ? 'HIGH' : 'MEDIUM',
|
|
67
|
+
title: `Reduce cross-boundary imports: ${fileName}`,
|
|
68
|
+
description: `\`${file}\` imports from ${data.dirs.size} different modules: ` +
|
|
69
|
+
`${Array.from(data.dirs).map((d) => `\`${d}\``).join(', ')}. ` +
|
|
70
|
+
`Consider using a facade or dependency injection.`,
|
|
71
|
+
rationale: `Files with imports scattered across many modules have high afferent coupling. ` +
|
|
72
|
+
`A facade centralizes these dependencies, making the file easier to test (mock one facade) ` +
|
|
73
|
+
`and reducing the impact of changes in dependent modules.`,
|
|
74
|
+
operations,
|
|
75
|
+
scoreImpact: [
|
|
76
|
+
{ metric: 'cohesion', before: report.score.breakdown.cohesion, after: Math.min(95, report.score.breakdown.cohesion + 5) },
|
|
77
|
+
{ metric: 'coupling', before: report.score.breakdown.coupling, after: Math.min(95, report.score.breakdown.coupling + 5) },
|
|
78
|
+
],
|
|
79
|
+
aiPrompt: `Analyze the file \`${file}\` which currently imports from ${data.dirs.size} cross-boundary distinct directories/modules (${Array.from(data.dirs).join(', ')}).\nPlease refactor this to use the Dependency Injection (DI) or Facade pattern:\n1. Extract these imports into a dedicated file \`${facadePath}\`.\n2. Re-export or bundle them appropriately.\n3. Update \`${file}\` to import strictly from the new \`${facadePath}\` rather than directly reaching into multiple disjointed modules, lowering its afferent coupling.`,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return steps;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private generateFacadeContent(ext: string, targets: string[], dirs: string[]): string {
|
|
87
|
+
if (ext === 'py') {
|
|
88
|
+
const imports = targets
|
|
89
|
+
.map((t) => `# from ${t.replace(/\//g, '.')} import ...`)
|
|
90
|
+
.join('\n');
|
|
91
|
+
return `"""Dependency facade — centralizes cross-module imports."""\n\n${imports}\n\n# Re-export what ${dirs.length} modules need\n`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// JS/TS
|
|
95
|
+
const imports = targets
|
|
96
|
+
.map((t) => `// export { ... } from '${t}';`)
|
|
97
|
+
.join('\n');
|
|
98
|
+
return `/**\n * Dependency facade — centralizes cross-module imports.\n */\n\n${imports}\n`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { basename, dirname, join } from 'path';
|
|
2
|
+
import { AnalysisReport } from '../types/core.js';
|
|
3
|
+
import { RefactorRule, RefactorStep, FileOperation } from '../types/rules.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Module Grouper Rule (Tier 1)
|
|
7
|
+
* Analyzes which files are frequently imported together and suggests
|
|
8
|
+
* grouping them into cohesive modules/packages.
|
|
9
|
+
*/
|
|
10
|
+
export class ModuleGrouperRule implements RefactorRule {
|
|
11
|
+
name = 'module-grouper';
|
|
12
|
+
tier = 1 as const;
|
|
13
|
+
|
|
14
|
+
analyze(report: AnalysisReport, _projectPath: string): RefactorStep[] {
|
|
15
|
+
const steps: RefactorStep[] = [];
|
|
16
|
+
|
|
17
|
+
// Build co-import matrix: which files are imported together?
|
|
18
|
+
const coImportCount: Record<string, Record<string, number>> = {};
|
|
19
|
+
|
|
20
|
+
// For each source file, see what it imports
|
|
21
|
+
const importsBySource: Record<string, string[]> = {};
|
|
22
|
+
for (const edge of report.dependencyGraph.edges) {
|
|
23
|
+
if (!importsBySource[edge.from]) importsBySource[edge.from] = [];
|
|
24
|
+
importsBySource[edge.from].push(edge.to);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Count co-imports
|
|
28
|
+
for (const [_source, targets] of Object.entries(importsBySource)) {
|
|
29
|
+
for (let i = 0; i < targets.length; i++) {
|
|
30
|
+
for (let j = i + 1; j < targets.length; j++) {
|
|
31
|
+
const a = targets[i];
|
|
32
|
+
const b = targets[j];
|
|
33
|
+
if (!coImportCount[a]) coImportCount[a] = {};
|
|
34
|
+
if (!coImportCount[b]) coImportCount[b] = {};
|
|
35
|
+
coImportCount[a][b] = (coImportCount[a][b] || 0) + 1;
|
|
36
|
+
coImportCount[b][a] = (coImportCount[b][a] || 0) + 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Find clusters: files that are always imported together
|
|
42
|
+
const clusters: Array<{ files: string[]; coImportScore: number }> = [];
|
|
43
|
+
const visited = new Set<string>();
|
|
44
|
+
|
|
45
|
+
for (const [fileA, partners] of Object.entries(coImportCount)) {
|
|
46
|
+
if (visited.has(fileA)) continue;
|
|
47
|
+
|
|
48
|
+
const strongPartners = Object.entries(partners)
|
|
49
|
+
.filter(([_, count]) => count >= 3)
|
|
50
|
+
.sort((a, b) => b[1] - a[1]);
|
|
51
|
+
|
|
52
|
+
if (strongPartners.length >= 2) {
|
|
53
|
+
const cluster = [fileA, ...strongPartners.map(([f]) => f)];
|
|
54
|
+
const inSameDir = cluster.every(
|
|
55
|
+
(f) => dirname(f) === dirname(cluster[0])
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// Only suggest if NOT already in the same directory
|
|
59
|
+
if (!inSameDir) {
|
|
60
|
+
// Arquitetura Limpa: Impede arrastar Módulos Transversais (DTOs/Types) para se tornarem fechados em Pastas de Domínios
|
|
61
|
+
const hasTypes = cluster.some(f => {
|
|
62
|
+
const lowerF = f.toLowerCase();
|
|
63
|
+
return lowerF.includes('types') || lowerF.includes('interface') || lowerF.includes('/types/');
|
|
64
|
+
});
|
|
65
|
+
if (hasTypes) continue;
|
|
66
|
+
|
|
67
|
+
const score = strongPartners.reduce((sum, [_, c]) => sum + c, 0);
|
|
68
|
+
clusters.push({ files: cluster, coImportScore: score });
|
|
69
|
+
cluster.forEach((f) => visited.add(f));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Generate steps for each cluster
|
|
75
|
+
for (const cluster of clusters.slice(0, 3)) {
|
|
76
|
+
const operations: FileOperation[] = [];
|
|
77
|
+
const clusterName = this.suggestModuleName(cluster.files);
|
|
78
|
+
const targetDir = `${dirname(cluster.files[0])}/${clusterName}`;
|
|
79
|
+
|
|
80
|
+
// Create new module directory
|
|
81
|
+
operations.push({
|
|
82
|
+
type: 'CREATE',
|
|
83
|
+
path: `${targetDir}/__init__.py`,
|
|
84
|
+
description: `Create new module \`${clusterName}/\` to group ${cluster.files.length} co-dependent files`,
|
|
85
|
+
content: `"""Module ${clusterName} — grouped by co-import pattern."""\n`,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Move files
|
|
89
|
+
for (const file of cluster.files) {
|
|
90
|
+
const newPath = join(targetDir, basename(file));
|
|
91
|
+
operations.push({
|
|
92
|
+
type: 'MOVE',
|
|
93
|
+
path: file,
|
|
94
|
+
newPath,
|
|
95
|
+
description: `Move \`${basename(file)}\` → \`${clusterName}/${basename(file)}\``,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
steps.push({
|
|
100
|
+
id: 0,
|
|
101
|
+
tier: 1,
|
|
102
|
+
rule: this.name,
|
|
103
|
+
priority: 'MEDIUM',
|
|
104
|
+
title: `Group co-dependent files into \`${clusterName}/\``,
|
|
105
|
+
description: `Files ${cluster.files.map((f) => `\`${basename(f)}\``).join(', ')} ` +
|
|
106
|
+
`are frequently imported together (co-import score: ${cluster.coImportScore}). ` +
|
|
107
|
+
`Grouping them improves cohesion.`,
|
|
108
|
+
rationale: `Files that are frequently imported together belong in the same module. ` +
|
|
109
|
+
`This improves discoverability and reduces the cognitive load of understanding ` +
|
|
110
|
+
`which files work together.`,
|
|
111
|
+
operations,
|
|
112
|
+
scoreImpact: [
|
|
113
|
+
{ metric: 'cohesion', before: report.score.breakdown.cohesion, after: Math.min(95, report.score.breakdown.cohesion + 10) },
|
|
114
|
+
{ metric: 'modularity', before: report.score.breakdown.modularity, after: Math.min(95, report.score.breakdown.modularity + 5) },
|
|
115
|
+
],
|
|
116
|
+
aiPrompt: `Analyze the files: ${cluster.files.map(f => `\`${f}\``).join(', ')}.\nThese files are frequently imported together but are currently scattered across different directories.\nPlease refactor to improve cohesion:\n1. Create a new directory named \`${targetDir}\`.\n2. Move these files solidly into this new directory.\n3. Add an index/barrel file if appropriate.\n4. Securely scan the entire project to fix all broken imports resulting from this move.`,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return steps;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private suggestModuleName(files: string[]): string {
|
|
124
|
+
// Try to infer a common theme from filenames
|
|
125
|
+
const names = files.map((f) => basename(f).replace(/\.[^.]+$/, '').toLowerCase());
|
|
126
|
+
const commonParts = names[0].split(/[_-]/).filter((part) =>
|
|
127
|
+
names.every((n) => n.includes(part))
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
if (commonParts.length > 0) return commonParts[0];
|
|
131
|
+
return 'shared';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AnalysisReport } from './types/core.js';
|
|
2
|
+
import { ArchitectRules, RuleViolation, ValidationResult } from './types/architect-rules.js';
|
|
3
|
+
|
|
4
|
+
export class RulesEngine {
|
|
5
|
+
public validate(report: AnalysisReport, rules: ArchitectRules): ValidationResult {
|
|
6
|
+
const violations: RuleViolation[] = [];
|
|
7
|
+
|
|
8
|
+
this.checkQualityGates(report, rules, violations);
|
|
9
|
+
this.checkBoundaries(report, rules, violations);
|
|
10
|
+
|
|
11
|
+
return {
|
|
12
|
+
success: violations.every(v => v.level !== 'error'),
|
|
13
|
+
violations,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
private checkQualityGates(report: AnalysisReport, rules: ArchitectRules, violations: RuleViolation[]) {
|
|
18
|
+
if (!rules.quality_gates) return;
|
|
19
|
+
|
|
20
|
+
const { min_overall_score, max_critical_anti_patterns, max_high_anti_patterns } = rules.quality_gates;
|
|
21
|
+
|
|
22
|
+
// 1. Minimum Overall Score (Error)
|
|
23
|
+
if (min_overall_score !== undefined && report.score.overall < min_overall_score) {
|
|
24
|
+
violations.push({
|
|
25
|
+
level: 'error',
|
|
26
|
+
rule: 'quality_gates.min_overall_score',
|
|
27
|
+
message: `Overall score (${report.score.overall}) is below the minimum required (${min_overall_score}).`,
|
|
28
|
+
actual: report.score.overall,
|
|
29
|
+
expected: min_overall_score,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 2. Max Critical Anti-Patterns (Error)
|
|
34
|
+
if (max_critical_anti_patterns !== undefined) {
|
|
35
|
+
const criticalCount = report.antiPatterns.filter(p => p.severity === 'CRITICAL').length;
|
|
36
|
+
if (criticalCount > max_critical_anti_patterns) {
|
|
37
|
+
violations.push({
|
|
38
|
+
level: 'error',
|
|
39
|
+
rule: 'quality_gates.max_critical_anti_patterns',
|
|
40
|
+
message: `Too many CRITICAL anti-patterns detected (${criticalCount}). Maximum allowed is ${max_critical_anti_patterns}.`,
|
|
41
|
+
actual: criticalCount,
|
|
42
|
+
expected: max_critical_anti_patterns,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 3. Max High Anti-Patterns (Warning)
|
|
48
|
+
if (max_high_anti_patterns !== undefined) {
|
|
49
|
+
const highCount = report.antiPatterns.filter(p => p.severity === 'HIGH').length;
|
|
50
|
+
if (highCount > max_high_anti_patterns) {
|
|
51
|
+
violations.push({
|
|
52
|
+
level: 'warning',
|
|
53
|
+
rule: 'quality_gates.max_high_anti_patterns',
|
|
54
|
+
message: `High number of HIGH severity anti-patterns (${highCount}). Maximum recommended is ${max_high_anti_patterns}.`,
|
|
55
|
+
actual: highCount,
|
|
56
|
+
expected: max_high_anti_patterns,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private checkBoundaries(report: AnalysisReport, rules: ArchitectRules, violations: RuleViolation[]) {
|
|
63
|
+
if (!rules.boundaries) return;
|
|
64
|
+
|
|
65
|
+
const { allow_circular_dependencies, banned_imports } = rules.boundaries;
|
|
66
|
+
|
|
67
|
+
// 1. Allow Circular Dependencies
|
|
68
|
+
if (allow_circular_dependencies === false) {
|
|
69
|
+
const hasCycles = report.antiPatterns.filter(p => p.name === 'Circular Dependency');
|
|
70
|
+
if (hasCycles.length > 0) {
|
|
71
|
+
violations.push({
|
|
72
|
+
level: 'error',
|
|
73
|
+
rule: 'boundaries.allow_circular_dependencies',
|
|
74
|
+
message: `Circular dependencies are strictly forbidden, but ${hasCycles.length} were found.`,
|
|
75
|
+
actual: hasCycles.length,
|
|
76
|
+
expected: 0,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 2. Banned Imports (Checks the AST Dependency Graph edges)
|
|
82
|
+
if (banned_imports && banned_imports.length > 0) {
|
|
83
|
+
for (const edge of report.dependencyGraph.edges) {
|
|
84
|
+
// Look through 'to' targets
|
|
85
|
+
for (const banned of banned_imports) {
|
|
86
|
+
if (edge.to.includes(banned)) {
|
|
87
|
+
// Found a banned import!
|
|
88
|
+
violations.push({
|
|
89
|
+
level: 'error',
|
|
90
|
+
rule: 'boundaries.banned_imports',
|
|
91
|
+
message: `Banned import detected: File '${edge.from}' imports '${banned}'.`,
|
|
92
|
+
actual: edge.to,
|
|
93
|
+
expected: `Not to include ${banned}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { ArchitectureScore, DependencyEdge, AntiPattern} from './types/core.js';
|
|
2
|
+
import { basename } from 'path';
|
|
3
|
+
|
|
4
|
+
export class ArchitectureScorer {
|
|
5
|
+
/**
|
|
6
|
+
* Barrel/index files that naturally have many connections and should be
|
|
7
|
+
* excluded from coupling max-edge penalty calculations.
|
|
8
|
+
*/
|
|
9
|
+
private static readonly BARREL_FILES = new Set([
|
|
10
|
+
'__init__.py', 'index.ts', 'index.js', 'index.tsx', 'index.jsx',
|
|
11
|
+
'mod.rs', '__init__.pyi', 'types.ts', 'types.js', 'logger.ts', 'logger.js'
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
score(
|
|
15
|
+
edges: DependencyEdge[],
|
|
16
|
+
antiPatterns: AntiPattern[],
|
|
17
|
+
totalFiles: number
|
|
18
|
+
): ArchitectureScore {
|
|
19
|
+
const modularity = this.calculateModularity(edges, totalFiles);
|
|
20
|
+
const coupling = this.calculateCoupling(edges, totalFiles);
|
|
21
|
+
const cohesion = this.calculateCohesion(edges);
|
|
22
|
+
const layering = this.calculateLayering(antiPatterns, totalFiles);
|
|
23
|
+
|
|
24
|
+
const components = [
|
|
25
|
+
{
|
|
26
|
+
name: 'Modularity',
|
|
27
|
+
score: Math.round(modularity),
|
|
28
|
+
maxScore: 100,
|
|
29
|
+
weight: 0.4,
|
|
30
|
+
explanation: 'Measures appropriate module boundaries and single responsibility principle adherence',
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'Coupling',
|
|
34
|
+
score: Math.round(coupling),
|
|
35
|
+
maxScore: 100,
|
|
36
|
+
weight: 0.25,
|
|
37
|
+
explanation: 'Evaluates interdependencies between modules; lower coupling is better',
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
name: 'Cohesion',
|
|
41
|
+
score: Math.round(cohesion),
|
|
42
|
+
maxScore: 100,
|
|
43
|
+
weight: 0.2,
|
|
44
|
+
explanation: 'Assesses how closely related functionality is grouped together',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'Layering',
|
|
48
|
+
score: Math.round(layering),
|
|
49
|
+
maxScore: 100,
|
|
50
|
+
weight: 0.15,
|
|
51
|
+
explanation: 'Checks adherence to architectural layer separation',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const overall = Math.round(
|
|
56
|
+
components[0].score * components[0].weight +
|
|
57
|
+
components[1].score * components[1].weight +
|
|
58
|
+
components[2].score * components[2].weight +
|
|
59
|
+
components[3].score * components[3].weight
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
overall: Math.min(100, Math.max(0, overall)),
|
|
64
|
+
components,
|
|
65
|
+
breakdown: {
|
|
66
|
+
modularity: Math.round(modularity),
|
|
67
|
+
coupling: Math.round(coupling),
|
|
68
|
+
cohesion: Math.round(cohesion),
|
|
69
|
+
layering: Math.round(layering),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private calculateModularity(edges: DependencyEdge[], totalFiles: number): number {
|
|
75
|
+
if (totalFiles === 0) return 50;
|
|
76
|
+
|
|
77
|
+
const avgEdgesPerFile = edges.length / totalFiles;
|
|
78
|
+
|
|
79
|
+
if (avgEdgesPerFile < 2) return 100;
|
|
80
|
+
if (avgEdgesPerFile < 4) return 85;
|
|
81
|
+
if (avgEdgesPerFile < 6) return 70;
|
|
82
|
+
if (avgEdgesPerFile < 10) return 50;
|
|
83
|
+
return 30;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private calculateCoupling(edges: DependencyEdge[], totalFiles: number): number {
|
|
87
|
+
if (totalFiles === 0 || totalFiles === 1) return 50;
|
|
88
|
+
|
|
89
|
+
const nonBarrelEdges = edges.filter((e) => {
|
|
90
|
+
const fromFile = basename(e.from);
|
|
91
|
+
const toFile = basename(e.to);
|
|
92
|
+
return !ArchitectureScorer.BARREL_FILES.has(fromFile) &&
|
|
93
|
+
!ArchitectureScorer.BARREL_FILES.has(toFile);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const nodeWithMaxEdges = this.findNodeWithMaxEdges(nonBarrelEdges);
|
|
97
|
+
const maxEdgeCount = nodeWithMaxEdges ? nodeWithMaxEdges.count : 0;
|
|
98
|
+
|
|
99
|
+
const effectiveFiles = Math.max(totalFiles - 1, 1);
|
|
100
|
+
const couplingRatio = maxEdgeCount / effectiveFiles;
|
|
101
|
+
|
|
102
|
+
if (couplingRatio < 0.15) return 100;
|
|
103
|
+
if (couplingRatio < 0.25) return 85;
|
|
104
|
+
if (couplingRatio < 0.35) return 75;
|
|
105
|
+
if (couplingRatio < 0.5) return 65;
|
|
106
|
+
if (couplingRatio < 0.7) return 50;
|
|
107
|
+
if (couplingRatio < 0.85) return 35;
|
|
108
|
+
return 20;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private findNodeWithMaxEdges(edges: DependencyEdge[]): { node: string; count: number } | null {
|
|
112
|
+
const nodeEdgeCount: Record<string, number> = {};
|
|
113
|
+
|
|
114
|
+
for (const edge of edges) {
|
|
115
|
+
nodeEdgeCount[edge.from] = (nodeEdgeCount[edge.from] || 0) + 1;
|
|
116
|
+
nodeEdgeCount[edge.to] = (nodeEdgeCount[edge.to] || 0) + 1;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let maxNode: string | null = null;
|
|
120
|
+
let maxCount = 0;
|
|
121
|
+
|
|
122
|
+
for (const [node, count] of Object.entries(nodeEdgeCount)) {
|
|
123
|
+
if (count > maxCount) {
|
|
124
|
+
maxCount = count;
|
|
125
|
+
maxNode = node;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return maxNode ? { node: maxNode, count: maxCount } : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
private calculateCohesion(edges: DependencyEdge[]): number {
|
|
133
|
+
if (edges.length === 0) return 50;
|
|
134
|
+
|
|
135
|
+
const internalEdges = edges.filter((e) =>
|
|
136
|
+
this.isInternalDependency(e.from, e.to)
|
|
137
|
+
).length;
|
|
138
|
+
|
|
139
|
+
const cohesionRatio = internalEdges / edges.length;
|
|
140
|
+
|
|
141
|
+
if (cohesionRatio > 0.8) return 100;
|
|
142
|
+
if (cohesionRatio > 0.6) return 85;
|
|
143
|
+
if (cohesionRatio > 0.45) return 75;
|
|
144
|
+
if (cohesionRatio > 0.3) return 65;
|
|
145
|
+
if (cohesionRatio > 0.15) return 50;
|
|
146
|
+
return 30;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private isInternalDependency(from: string, to: string): boolean {
|
|
150
|
+
const fromParts = from.split('/');
|
|
151
|
+
const toParts = to.split('/');
|
|
152
|
+
|
|
153
|
+
if (fromParts.length <= 1 && toParts.length <= 1) return true;
|
|
154
|
+
|
|
155
|
+
const fromTopLevel = fromParts.length > 1 ? fromParts[0] : '';
|
|
156
|
+
const toTopLevel = toParts.length > 1 ? toParts[0] : '';
|
|
157
|
+
|
|
158
|
+
return fromTopLevel === toTopLevel;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private calculateLayering(antiPatterns: AntiPattern[], totalFiles?: number): number {
|
|
162
|
+
const layeringViolations = antiPatterns.filter(
|
|
163
|
+
(p) =>
|
|
164
|
+
p.name === 'Leaky Abstraction' ||
|
|
165
|
+
p.name === 'Shotgun Surgery' ||
|
|
166
|
+
p.name === 'Circular Dependency'
|
|
167
|
+
).length;
|
|
168
|
+
|
|
169
|
+
if (layeringViolations === 0) return 100;
|
|
170
|
+
|
|
171
|
+
const fileCount = Math.max(totalFiles || 50, 10);
|
|
172
|
+
const violationRatio = layeringViolations / fileCount;
|
|
173
|
+
|
|
174
|
+
if (violationRatio < 0.05) return 95;
|
|
175
|
+
if (violationRatio < 0.15) return 85;
|
|
176
|
+
if (violationRatio < 0.25) return 70;
|
|
177
|
+
if (violationRatio < 0.40) return 50;
|
|
178
|
+
if (violationRatio < 0.60) return 35;
|
|
179
|
+
return 20;
|
|
180
|
+
}
|
|
181
|
+
}
|