@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,675 @@
|
|
|
1
|
+
import { ArchitectureScorer } from '../src/core/scorer.js';
|
|
2
|
+
import { DependencyEdge, AntiPattern } from '../src/core/types/core.js';
|
|
3
|
+
|
|
4
|
+
describe('ArchitectureScorer', () => {
|
|
5
|
+
const scorer = new ArchitectureScorer();
|
|
6
|
+
|
|
7
|
+
const mockEdges: DependencyEdge[] = [
|
|
8
|
+
{ from: 'src/a.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
9
|
+
{ from: 'src/b.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
10
|
+
{ from: 'src/c.ts', to: 'src/d.ts', type: 'import', weight: 1 },
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const mockAntiPatterns: AntiPattern[] = [
|
|
14
|
+
{
|
|
15
|
+
name: 'God Class',
|
|
16
|
+
severity: 'CRITICAL',
|
|
17
|
+
location: 'src/Manager.ts',
|
|
18
|
+
description: 'Test',
|
|
19
|
+
suggestion: 'Test',
|
|
20
|
+
},
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
describe('score', () => {
|
|
24
|
+
it('should return a score between 0 and 100', () => {
|
|
25
|
+
const result = scorer.score(mockEdges, mockAntiPatterns, 50);
|
|
26
|
+
|
|
27
|
+
expect(result.overall).toBeGreaterThanOrEqual(0);
|
|
28
|
+
expect(result.overall).toBeLessThanOrEqual(100);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('should calculate component scores', () => {
|
|
32
|
+
const result = scorer.score(mockEdges, mockAntiPatterns, 50);
|
|
33
|
+
|
|
34
|
+
expect(result.components).toBeDefined();
|
|
35
|
+
expect(result.components.length).toBeGreaterThan(0);
|
|
36
|
+
|
|
37
|
+
for (const component of result.components) {
|
|
38
|
+
expect(component.score).toBeGreaterThanOrEqual(0);
|
|
39
|
+
expect(component.score).toBeLessThanOrEqual(100);
|
|
40
|
+
expect(component.weight).toBeGreaterThan(0);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should provide breakdown of component scores', () => {
|
|
45
|
+
const result = scorer.score(mockEdges, mockAntiPatterns, 50);
|
|
46
|
+
|
|
47
|
+
expect(result.breakdown).toBeDefined();
|
|
48
|
+
expect(result.breakdown.modularity).toBeGreaterThanOrEqual(0);
|
|
49
|
+
expect(result.breakdown.coupling).toBeGreaterThanOrEqual(0);
|
|
50
|
+
expect(result.breakdown.cohesion).toBeGreaterThanOrEqual(0);
|
|
51
|
+
expect(result.breakdown.layering).toBeGreaterThanOrEqual(0);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle empty edges', () => {
|
|
55
|
+
const result = scorer.score([], mockAntiPatterns, 10);
|
|
56
|
+
|
|
57
|
+
expect(result.overall).toBeGreaterThanOrEqual(0);
|
|
58
|
+
expect(result.overall).toBeLessThanOrEqual(100);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should penalize anti-patterns in layering score', () => {
|
|
62
|
+
const lotsOfPatterns: AntiPattern[] = Array(10)
|
|
63
|
+
.fill(null)
|
|
64
|
+
.map((_, i) => ({
|
|
65
|
+
name: 'Test Pattern',
|
|
66
|
+
severity: 'HIGH',
|
|
67
|
+
location: `src/file${i}.ts`,
|
|
68
|
+
description: 'Test',
|
|
69
|
+
suggestion: 'Test',
|
|
70
|
+
}));
|
|
71
|
+
|
|
72
|
+
const resultWithPatterns = scorer.score(mockEdges, lotsOfPatterns, 50);
|
|
73
|
+
const resultWithoutPatterns = scorer.score(mockEdges, [], 50);
|
|
74
|
+
|
|
75
|
+
expect(resultWithPatterns.breakdown.layering).toBeLessThanOrEqual(
|
|
76
|
+
resultWithoutPatterns.breakdown.layering
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
82
|
+
// MODULARITY TESTS
|
|
83
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
84
|
+
describe('calculateModularity', () => {
|
|
85
|
+
it('should score 50 when totalFiles is 0', () => {
|
|
86
|
+
const result = scorer.score([], [], 0);
|
|
87
|
+
expect(result.breakdown.modularity).toBe(50);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('should score 95 when avgEdgesPerFile < 2', () => {
|
|
91
|
+
// 1 file, 1 edge → avgEdgesPerFile = 1
|
|
92
|
+
const edges: DependencyEdge[] = [
|
|
93
|
+
{ from: 'src/a.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
94
|
+
];
|
|
95
|
+
const result = scorer.score(edges, [], 1);
|
|
96
|
+
expect(result.breakdown.modularity).toBe(100);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should score 85 when avgEdgesPerFile is 2-4', () => {
|
|
100
|
+
// 2 files, 5 edges → avgEdgesPerFile = 2.5
|
|
101
|
+
const edges: DependencyEdge[] = [
|
|
102
|
+
{ from: 'src/a.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
103
|
+
{ from: 'src/b.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
104
|
+
{ from: 'src/c.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
105
|
+
{ from: 'src/a.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
106
|
+
{ from: 'src/b.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
107
|
+
];
|
|
108
|
+
const result = scorer.score(edges, [], 2);
|
|
109
|
+
expect(result.breakdown.modularity).toBe(85);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should score 70 when avgEdgesPerFile is 4-6', () => {
|
|
113
|
+
// 2 files, 10 edges → avgEdgesPerFile = 5
|
|
114
|
+
const edges: DependencyEdge[] = Array(10)
|
|
115
|
+
.fill(null)
|
|
116
|
+
.map((_, _i) => ({
|
|
117
|
+
from: `src/a.ts`,
|
|
118
|
+
to: `src/b.ts`,
|
|
119
|
+
type: 'import' as const,
|
|
120
|
+
weight: 1,
|
|
121
|
+
}));
|
|
122
|
+
const result = scorer.score(edges, [], 2);
|
|
123
|
+
expect(result.breakdown.modularity).toBe(70);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should score 50 when avgEdgesPerFile is 6-10', () => {
|
|
127
|
+
// 2 files, 18 edges → avgEdgesPerFile = 9
|
|
128
|
+
const edges: DependencyEdge[] = Array(18)
|
|
129
|
+
.fill(null)
|
|
130
|
+
.map((_, i) => ({
|
|
131
|
+
from: i % 2 === 0 ? 'src/a.ts' : 'src/b.ts',
|
|
132
|
+
to: i % 2 === 0 ? 'src/b.ts' : 'src/a.ts',
|
|
133
|
+
type: 'import' as const,
|
|
134
|
+
weight: 1,
|
|
135
|
+
}));
|
|
136
|
+
const result = scorer.score(edges, [], 2);
|
|
137
|
+
expect(result.breakdown.modularity).toBe(50);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('should score 30 when avgEdgesPerFile >= 10', () => {
|
|
141
|
+
// 1 file, 10+ edges
|
|
142
|
+
const edges: DependencyEdge[] = Array(11)
|
|
143
|
+
.fill(null)
|
|
144
|
+
.map((_, i) => ({
|
|
145
|
+
from: 'src/a.ts',
|
|
146
|
+
to: `src/file${i}.ts`,
|
|
147
|
+
type: 'import' as const,
|
|
148
|
+
weight: 1,
|
|
149
|
+
}));
|
|
150
|
+
const result = scorer.score(edges, [], 1);
|
|
151
|
+
expect(result.breakdown.modularity).toBe(30);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
156
|
+
// COUPLING TESTS
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
158
|
+
describe('calculateCoupling', () => {
|
|
159
|
+
it('should score 50 when totalFiles is 0', () => {
|
|
160
|
+
const result = scorer.score([], [], 0);
|
|
161
|
+
expect(result.breakdown.coupling).toBe(50);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('should score 50 when totalFiles is 1', () => {
|
|
165
|
+
const edges: DependencyEdge[] = [
|
|
166
|
+
{ from: 'src/a.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
167
|
+
];
|
|
168
|
+
const result = scorer.score(edges, [], 1);
|
|
169
|
+
expect(result.breakdown.coupling).toBe(50);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should score 95 when couplingRatio < 0.15', () => {
|
|
173
|
+
// 20 files, 1 file has 2 edges → ratio = 2/19 ≈ 0.105
|
|
174
|
+
const edges: DependencyEdge[] = [
|
|
175
|
+
{ from: 'src/hub.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
176
|
+
{ from: 'src/hub.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
177
|
+
{ from: 'src/c.ts', to: 'src/d.ts', type: 'import', weight: 1 },
|
|
178
|
+
];
|
|
179
|
+
const result = scorer.score(edges, [], 20);
|
|
180
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should score 85 when couplingRatio is 0.15-0.25', () => {
|
|
184
|
+
// 10 files, hub has 2 edges → ratio = 2/9 ≈ 0.222
|
|
185
|
+
const edges: DependencyEdge[] = [
|
|
186
|
+
{ from: 'src/hub.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
187
|
+
{ from: 'src/hub.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
188
|
+
];
|
|
189
|
+
const result = scorer.score(edges, [], 10);
|
|
190
|
+
expect(result.breakdown.coupling).toBe(85);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should score 75 when couplingRatio is 0.25-0.35', () => {
|
|
194
|
+
// 10 files, hub has 3 edges → ratio = 3/9 ≈ 0.333
|
|
195
|
+
const edges: DependencyEdge[] = [
|
|
196
|
+
{ from: 'src/hub.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
197
|
+
{ from: 'src/hub.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
198
|
+
{ from: 'src/hub.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
199
|
+
];
|
|
200
|
+
const result = scorer.score(edges, [], 10);
|
|
201
|
+
expect(result.breakdown.coupling).toBe(75);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should score 65 when couplingRatio is 0.35-0.5', () => {
|
|
205
|
+
// 10 files, hub has 4 edges → ratio = 4/9 ≈ 0.444
|
|
206
|
+
const edges: DependencyEdge[] = [
|
|
207
|
+
{ from: 'src/hub.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
208
|
+
{ from: 'src/hub.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
209
|
+
{ from: 'src/hub.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
210
|
+
{ from: 'src/hub.ts', to: 'src/d.ts', type: 'import', weight: 1 },
|
|
211
|
+
];
|
|
212
|
+
const result = scorer.score(edges, [], 10);
|
|
213
|
+
expect(result.breakdown.coupling).toBe(65);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should score 50 when couplingRatio is 0.5-0.7', () => {
|
|
217
|
+
// 10 files, hub has 6 edges → ratio = 6/9 ≈ 0.667
|
|
218
|
+
const edges: DependencyEdge[] = Array(6)
|
|
219
|
+
.fill(null)
|
|
220
|
+
.map((_, i) => ({
|
|
221
|
+
from: 'src/hub.ts',
|
|
222
|
+
to: `src/file${i}.ts`,
|
|
223
|
+
type: 'import' as const,
|
|
224
|
+
weight: 1,
|
|
225
|
+
}));
|
|
226
|
+
const result = scorer.score(edges, [], 10);
|
|
227
|
+
expect(result.breakdown.coupling).toBe(50);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should score 35 when couplingRatio is 0.7-0.85', () => {
|
|
231
|
+
// 10 files, hub has 7 edges → ratio = 7/9 ≈ 0.778
|
|
232
|
+
const edges: DependencyEdge[] = Array(7)
|
|
233
|
+
.fill(null)
|
|
234
|
+
.map((_, i) => ({
|
|
235
|
+
from: 'src/hub.ts',
|
|
236
|
+
to: `src/file${i}.ts`,
|
|
237
|
+
type: 'import' as const,
|
|
238
|
+
weight: 1,
|
|
239
|
+
}));
|
|
240
|
+
const result = scorer.score(edges, [], 10);
|
|
241
|
+
expect(result.breakdown.coupling).toBe(35);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('should score 20 when couplingRatio >= 0.85', () => {
|
|
245
|
+
// 10 files, hub has 8 edges → ratio = 8/9 ≈ 0.889
|
|
246
|
+
const edges: DependencyEdge[] = Array(8)
|
|
247
|
+
.fill(null)
|
|
248
|
+
.map((_, i) => ({
|
|
249
|
+
from: 'src/hub.ts',
|
|
250
|
+
to: `src/file${i}.ts`,
|
|
251
|
+
type: 'import' as const,
|
|
252
|
+
weight: 1,
|
|
253
|
+
}));
|
|
254
|
+
const result = scorer.score(edges, [], 10);
|
|
255
|
+
expect(result.breakdown.coupling).toBe(20);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should exclude barrel files (index.ts) from coupling calculation', () => {
|
|
259
|
+
// Barrel files should not count toward maxEdgeCount
|
|
260
|
+
const edges: DependencyEdge[] = [
|
|
261
|
+
{ from: 'src/index.ts', to: 'src/a.ts', type: 'import', weight: 1 },
|
|
262
|
+
{ from: 'src/index.ts', to: 'src/b.ts', type: 'import', weight: 1 },
|
|
263
|
+
{ from: 'src/index.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
264
|
+
{ from: 'src/hub.ts', to: 'src/x.ts', type: 'import', weight: 1 },
|
|
265
|
+
];
|
|
266
|
+
const result = scorer.score(edges, [], 10);
|
|
267
|
+
// Only hub.ts (1 edge) should count, ratio = 1/9
|
|
268
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('should exclude barrel files (__init__.py) from coupling calculation', () => {
|
|
272
|
+
const edges: DependencyEdge[] = [
|
|
273
|
+
{ from: 'src/__init__.py', to: 'src/a.py', type: 'import', weight: 1 },
|
|
274
|
+
{ from: 'src/__init__.py', to: 'src/b.py', type: 'import', weight: 1 },
|
|
275
|
+
{ from: 'src/__init__.py', to: 'src/c.py', type: 'import', weight: 1 },
|
|
276
|
+
{ from: 'src/__init__.py', to: 'src/d.py', type: 'import', weight: 1 },
|
|
277
|
+
];
|
|
278
|
+
const result = scorer.score(edges, [], 10);
|
|
279
|
+
// All edges from __init__.py should be filtered
|
|
280
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('should exclude barrel files (mod.rs) from coupling calculation', () => {
|
|
284
|
+
const edges: DependencyEdge[] = [
|
|
285
|
+
{ from: 'src/mod.rs', to: 'src/a.rs', type: 'import', weight: 1 },
|
|
286
|
+
{ from: 'src/mod.rs', to: 'src/b.rs', type: 'import', weight: 1 },
|
|
287
|
+
];
|
|
288
|
+
const result = scorer.score(edges, [], 10);
|
|
289
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('should exclude barrel files (__init__.pyi) from coupling calculation', () => {
|
|
293
|
+
const edges: DependencyEdge[] = [
|
|
294
|
+
{ from: 'src/__init__.pyi', to: 'src/a.pyi', type: 'import', weight: 1 },
|
|
295
|
+
{ from: 'src/__init__.pyi', to: 'src/b.pyi', type: 'import', weight: 1 },
|
|
296
|
+
];
|
|
297
|
+
const result = scorer.score(edges, [], 10);
|
|
298
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('should exclude barrel files as destinations', () => {
|
|
302
|
+
// When the destination is index.ts, it should not count
|
|
303
|
+
const edges: DependencyEdge[] = [
|
|
304
|
+
{ from: 'src/a.ts', to: 'src/index.ts', type: 'import', weight: 1 },
|
|
305
|
+
{ from: 'src/b.ts', to: 'src/index.ts', type: 'import', weight: 1 },
|
|
306
|
+
{ from: 'src/hub.ts', to: 'src/c.ts', type: 'import', weight: 1 },
|
|
307
|
+
];
|
|
308
|
+
const result = scorer.score(edges, [], 10);
|
|
309
|
+
expect(result.breakdown.coupling).toBe(100);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
314
|
+
// COHESION TESTS
|
|
315
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
316
|
+
describe('calculateCohesion', () => {
|
|
317
|
+
it('should score 50 when there are no edges', () => {
|
|
318
|
+
const result = scorer.score([], [], 10);
|
|
319
|
+
expect(result.breakdown.cohesion).toBe(50);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('should score 95 when cohesionRatio > 0.8', () => {
|
|
323
|
+
// 10 edges, 9 internal → ratio = 0.9
|
|
324
|
+
const edges: DependencyEdge[] = [
|
|
325
|
+
// Same package → internal
|
|
326
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
327
|
+
{ from: 'api/b.ts', to: 'api/c.ts', type: 'import', weight: 1 },
|
|
328
|
+
{ from: 'api/c.ts', to: 'api/d.ts', type: 'import', weight: 1 },
|
|
329
|
+
{ from: 'api/d.ts', to: 'api/e.ts', type: 'import', weight: 1 },
|
|
330
|
+
{ from: 'api/e.ts', to: 'api/f.ts', type: 'import', weight: 1 },
|
|
331
|
+
{ from: 'api/f.ts', to: 'api/g.ts', type: 'import', weight: 1 },
|
|
332
|
+
{ from: 'api/g.ts', to: 'api/h.ts', type: 'import', weight: 1 },
|
|
333
|
+
{ from: 'api/h.ts', to: 'api/i.ts', type: 'import', weight: 1 },
|
|
334
|
+
{ from: 'api/i.ts', to: 'api/j.ts', type: 'import', weight: 1 },
|
|
335
|
+
// 1 external
|
|
336
|
+
{ from: 'api/k.ts', to: 'service/x.ts', type: 'import', weight: 1 },
|
|
337
|
+
];
|
|
338
|
+
const result = scorer.score(edges, [], 50);
|
|
339
|
+
expect(result.breakdown.cohesion).toBe(100);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('should score 85 when cohesionRatio is 0.6-0.8', () => {
|
|
343
|
+
// 10 edges, 7 internal → ratio = 0.7
|
|
344
|
+
const edges: DependencyEdge[] = [
|
|
345
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
346
|
+
{ from: 'api/b.ts', to: 'api/c.ts', type: 'import', weight: 1 },
|
|
347
|
+
{ from: 'api/c.ts', to: 'api/d.ts', type: 'import', weight: 1 },
|
|
348
|
+
{ from: 'api/d.ts', to: 'api/e.ts', type: 'import', weight: 1 },
|
|
349
|
+
{ from: 'api/e.ts', to: 'api/f.ts', type: 'import', weight: 1 },
|
|
350
|
+
{ from: 'api/f.ts', to: 'api/g.ts', type: 'import', weight: 1 },
|
|
351
|
+
{ from: 'api/g.ts', to: 'api/h.ts', type: 'import', weight: 1 },
|
|
352
|
+
// 3 external
|
|
353
|
+
{ from: 'api/x.ts', to: 'service/a.ts', type: 'import', weight: 1 },
|
|
354
|
+
{ from: 'api/y.ts', to: 'service/b.ts', type: 'import', weight: 1 },
|
|
355
|
+
{ from: 'api/z.ts', to: 'service/c.ts', type: 'import', weight: 1 },
|
|
356
|
+
];
|
|
357
|
+
const result = scorer.score(edges, [], 50);
|
|
358
|
+
expect(result.breakdown.cohesion).toBe(85);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should score 75 when cohesionRatio is 0.45-0.6', () => {
|
|
362
|
+
// 10 edges, 5 internal → ratio = 0.5
|
|
363
|
+
const edges: DependencyEdge[] = [
|
|
364
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
365
|
+
{ from: 'api/b.ts', to: 'api/c.ts', type: 'import', weight: 1 },
|
|
366
|
+
{ from: 'api/c.ts', to: 'api/d.ts', type: 'import', weight: 1 },
|
|
367
|
+
{ from: 'api/d.ts', to: 'api/e.ts', type: 'import', weight: 1 },
|
|
368
|
+
{ from: 'api/e.ts', to: 'api/f.ts', type: 'import', weight: 1 },
|
|
369
|
+
// 5 external
|
|
370
|
+
{ from: 'api/w.ts', to: 'service/a.ts', type: 'import', weight: 1 },
|
|
371
|
+
{ from: 'api/x.ts', to: 'service/b.ts', type: 'import', weight: 1 },
|
|
372
|
+
{ from: 'api/y.ts', to: 'service/c.ts', type: 'import', weight: 1 },
|
|
373
|
+
{ from: 'api/z.ts', to: 'service/d.ts', type: 'import', weight: 1 },
|
|
374
|
+
{ from: 'api/q.ts', to: 'service/e.ts', type: 'import', weight: 1 },
|
|
375
|
+
];
|
|
376
|
+
const result = scorer.score(edges, [], 50);
|
|
377
|
+
expect(result.breakdown.cohesion).toBe(75);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it('should score 65 when cohesionRatio is 0.3-0.45', () => {
|
|
381
|
+
// 10 edges, 4 internal → ratio = 0.4
|
|
382
|
+
const edges: DependencyEdge[] = [
|
|
383
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
384
|
+
{ from: 'api/b.ts', to: 'api/c.ts', type: 'import', weight: 1 },
|
|
385
|
+
{ from: 'api/c.ts', to: 'api/d.ts', type: 'import', weight: 1 },
|
|
386
|
+
{ from: 'api/d.ts', to: 'api/e.ts', type: 'import', weight: 1 },
|
|
387
|
+
// 6 external
|
|
388
|
+
{ from: 'api/w.ts', to: 'service/a.ts', type: 'import', weight: 1 },
|
|
389
|
+
{ from: 'api/x.ts', to: 'service/b.ts', type: 'import', weight: 1 },
|
|
390
|
+
{ from: 'api/y.ts', to: 'service/c.ts', type: 'import', weight: 1 },
|
|
391
|
+
{ from: 'api/z.ts', to: 'service/d.ts', type: 'import', weight: 1 },
|
|
392
|
+
{ from: 'api/q.ts', to: 'service/e.ts', type: 'import', weight: 1 },
|
|
393
|
+
{ from: 'api/r.ts', to: 'service/f.ts', type: 'import', weight: 1 },
|
|
394
|
+
];
|
|
395
|
+
const result = scorer.score(edges, [], 50);
|
|
396
|
+
expect(result.breakdown.cohesion).toBe(65);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('should score 50 when cohesionRatio is 0.15-0.3', () => {
|
|
400
|
+
// 10 edges, 2 internal → ratio = 0.2
|
|
401
|
+
const edges: DependencyEdge[] = [
|
|
402
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
403
|
+
{ from: 'api/b.ts', to: 'api/c.ts', type: 'import', weight: 1 },
|
|
404
|
+
// 8 external
|
|
405
|
+
{ from: 'api/v.ts', to: 'service/a.ts', type: 'import', weight: 1 },
|
|
406
|
+
{ from: 'api/w.ts', to: 'service/b.ts', type: 'import', weight: 1 },
|
|
407
|
+
{ from: 'api/x.ts', to: 'service/c.ts', type: 'import', weight: 1 },
|
|
408
|
+
{ from: 'api/y.ts', to: 'service/d.ts', type: 'import', weight: 1 },
|
|
409
|
+
{ from: 'api/z.ts', to: 'service/e.ts', type: 'import', weight: 1 },
|
|
410
|
+
{ from: 'api/q.ts', to: 'service/f.ts', type: 'import', weight: 1 },
|
|
411
|
+
{ from: 'api/r.ts', to: 'service/g.ts', type: 'import', weight: 1 },
|
|
412
|
+
{ from: 'api/s.ts', to: 'service/h.ts', type: 'import', weight: 1 },
|
|
413
|
+
];
|
|
414
|
+
const result = scorer.score(edges, [], 50);
|
|
415
|
+
expect(result.breakdown.cohesion).toBe(50);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should score 30 when cohesionRatio <= 0.15', () => {
|
|
419
|
+
// 10 edges, 1 internal → ratio = 0.1
|
|
420
|
+
const edges: DependencyEdge[] = [
|
|
421
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
422
|
+
// 9 external
|
|
423
|
+
{ from: 'api/c.ts', to: 'service/a.ts', type: 'import', weight: 1 },
|
|
424
|
+
{ from: 'api/d.ts', to: 'service/b.ts', type: 'import', weight: 1 },
|
|
425
|
+
{ from: 'api/e.ts', to: 'service/c.ts', type: 'import', weight: 1 },
|
|
426
|
+
{ from: 'api/f.ts', to: 'service/d.ts', type: 'import', weight: 1 },
|
|
427
|
+
{ from: 'api/g.ts', to: 'service/e.ts', type: 'import', weight: 1 },
|
|
428
|
+
{ from: 'api/h.ts', to: 'service/f.ts', type: 'import', weight: 1 },
|
|
429
|
+
{ from: 'api/i.ts', to: 'service/g.ts', type: 'import', weight: 1 },
|
|
430
|
+
{ from: 'api/j.ts', to: 'service/h.ts', type: 'import', weight: 1 },
|
|
431
|
+
{ from: 'api/k.ts', to: 'service/i.ts', type: 'import', weight: 1 },
|
|
432
|
+
];
|
|
433
|
+
const result = scorer.score(edges, [], 50);
|
|
434
|
+
expect(result.breakdown.cohesion).toBe(30);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
439
|
+
// INTERNAL DEPENDENCY TESTS
|
|
440
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
441
|
+
describe('isInternalDependency', () => {
|
|
442
|
+
it('should return true when both files are root files (no directory)', () => {
|
|
443
|
+
const edges: DependencyEdge[] = [
|
|
444
|
+
{ from: 'app.ts', to: 'main.ts', type: 'import', weight: 1 },
|
|
445
|
+
];
|
|
446
|
+
const result = scorer.score(edges, [], 2);
|
|
447
|
+
// Both root → internal → cohesion should be high
|
|
448
|
+
expect(result.breakdown.cohesion).toBe(100);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
it('should return true when files share same top-level directory', () => {
|
|
452
|
+
const edges: DependencyEdge[] = [
|
|
453
|
+
{ from: 'api/routes.ts', to: 'api/handlers.ts', type: 'import', weight: 1 },
|
|
454
|
+
];
|
|
455
|
+
const result = scorer.score(edges, [], 2);
|
|
456
|
+
// Same package (api) → internal
|
|
457
|
+
expect(result.breakdown.cohesion).toBe(100);
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
it('should return false when files are in different top-level directories', () => {
|
|
461
|
+
const edges: DependencyEdge[] = [
|
|
462
|
+
{ from: 'api/routes.ts', to: 'service/handler.ts', type: 'import', weight: 1 },
|
|
463
|
+
];
|
|
464
|
+
const result = scorer.score(edges, [], 2);
|
|
465
|
+
// Different packages → external
|
|
466
|
+
expect(result.breakdown.cohesion).toBe(30);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it('should handle Python package structure', () => {
|
|
470
|
+
const edges: DependencyEdge[] = [
|
|
471
|
+
{ from: 'deepguard/cli.py', to: 'deepguard/analyzer.py', type: 'import', weight: 1 },
|
|
472
|
+
];
|
|
473
|
+
const result = scorer.score(edges, [], 2);
|
|
474
|
+
// Same package → internal
|
|
475
|
+
expect(result.breakdown.cohesion).toBe(100);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('should handle deeply nested paths - same top-level package', () => {
|
|
479
|
+
const edges: DependencyEdge[] = [
|
|
480
|
+
{ from: 'mypackage/domain/entities/user.ts', to: 'mypackage/application/services/user.service.ts', type: 'import', weight: 1 },
|
|
481
|
+
];
|
|
482
|
+
const result = scorer.score(edges, [], 2);
|
|
483
|
+
// Both start with mypackage → internal
|
|
484
|
+
expect(result.breakdown.cohesion).toBe(100);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should handle mixed package structures', () => {
|
|
488
|
+
const edges: DependencyEdge[] = [
|
|
489
|
+
{ from: 'pkg1/file.ts', to: 'pkg2/file.ts', type: 'import', weight: 1 },
|
|
490
|
+
{ from: 'pkg1/a.ts', to: 'pkg1/b.ts', type: 'import', weight: 1 },
|
|
491
|
+
];
|
|
492
|
+
const result = scorer.score(edges, [], 4);
|
|
493
|
+
// 50% internal
|
|
494
|
+
expect(result.breakdown.cohesion).toBe(75);
|
|
495
|
+
});
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
499
|
+
// LAYERING TESTS
|
|
500
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
501
|
+
describe('calculateLayering', () => {
|
|
502
|
+
it('should score 95 when there are 0 violations', () => {
|
|
503
|
+
const antiPatterns: AntiPattern[] = [
|
|
504
|
+
{
|
|
505
|
+
name: 'God Class',
|
|
506
|
+
severity: 'CRITICAL',
|
|
507
|
+
location: 'src/Manager.ts',
|
|
508
|
+
description: 'Test',
|
|
509
|
+
suggestion: 'Test',
|
|
510
|
+
},
|
|
511
|
+
];
|
|
512
|
+
const result = scorer.score([], antiPatterns, 10);
|
|
513
|
+
expect(result.breakdown.layering).toBe(100);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
it('should score 90 when there is 1 violation in a large project', () => {
|
|
517
|
+
const antiPatterns: AntiPattern[] = [
|
|
518
|
+
{
|
|
519
|
+
name: 'Leaky Abstraction',
|
|
520
|
+
severity: 'HIGH',
|
|
521
|
+
location: 'src/api.ts',
|
|
522
|
+
description: 'Test',
|
|
523
|
+
suggestion: 'Test',
|
|
524
|
+
},
|
|
525
|
+
];
|
|
526
|
+
// 1 violation / 100 files = 1% ratio → score 90
|
|
527
|
+
const result = scorer.score([], antiPatterns, 100);
|
|
528
|
+
expect(result.breakdown.layering).toBe(95);
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
it('should score 80 when ratio is between 2-5%', () => {
|
|
532
|
+
const antiPatterns: AntiPattern[] = [
|
|
533
|
+
{
|
|
534
|
+
name: 'Leaky Abstraction',
|
|
535
|
+
severity: 'HIGH',
|
|
536
|
+
location: 'src/api.ts',
|
|
537
|
+
description: 'Test',
|
|
538
|
+
suggestion: 'Test',
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
name: 'Shotgun Surgery',
|
|
542
|
+
severity: 'MEDIUM',
|
|
543
|
+
location: 'src/service.ts',
|
|
544
|
+
description: 'Test',
|
|
545
|
+
suggestion: 'Test',
|
|
546
|
+
},
|
|
547
|
+
];
|
|
548
|
+
// 2 violations / 100 files = 2% ratio → score 95
|
|
549
|
+
const result = scorer.score([], antiPatterns, 100);
|
|
550
|
+
expect(result.breakdown.layering).toBe(95);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it('should score 85 when ratio is between 5-15%', () => {
|
|
554
|
+
const antiPatterns: AntiPattern[] = [
|
|
555
|
+
{
|
|
556
|
+
name: 'Leaky Abstraction',
|
|
557
|
+
severity: 'HIGH',
|
|
558
|
+
location: 'src/api.ts',
|
|
559
|
+
description: 'Test',
|
|
560
|
+
suggestion: 'Test',
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'Shotgun Surgery',
|
|
564
|
+
severity: 'MEDIUM',
|
|
565
|
+
location: 'src/service.ts',
|
|
566
|
+
description: 'Test',
|
|
567
|
+
suggestion: 'Test',
|
|
568
|
+
},
|
|
569
|
+
{
|
|
570
|
+
name: 'Circular Dependency',
|
|
571
|
+
severity: 'CRITICAL',
|
|
572
|
+
location: 'src/model.ts',
|
|
573
|
+
description: 'Test',
|
|
574
|
+
suggestion: 'Test',
|
|
575
|
+
},
|
|
576
|
+
];
|
|
577
|
+
// 3 violations / 50 files = 6% ratio → score 85
|
|
578
|
+
const result = scorer.score([], antiPatterns, 50);
|
|
579
|
+
expect(result.breakdown.layering).toBe(85);
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
it('should score 85 when ratio is between 10-15%', () => {
|
|
583
|
+
const antiPatterns: AntiPattern[] = [
|
|
584
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/api.ts', description: 'Test', suggestion: 'Test' },
|
|
585
|
+
{ name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/service.ts', description: 'Test', suggestion: 'Test' },
|
|
586
|
+
{ name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/model.ts', description: 'Test', suggestion: 'Test' },
|
|
587
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/data.ts', description: 'Test', suggestion: 'Test' },
|
|
588
|
+
];
|
|
589
|
+
// 4 violations / 30 files = 13.3% ratio → score 85
|
|
590
|
+
const result = scorer.score([], antiPatterns, 30);
|
|
591
|
+
expect(result.breakdown.layering).toBe(85);
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should score 20 when ratio exceeds 60%', () => {
|
|
595
|
+
const antiPatterns: AntiPattern[] = [
|
|
596
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/a.ts', description: 'Test', suggestion: 'Test' },
|
|
597
|
+
{ name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/b.ts', description: 'Test', suggestion: 'Test' },
|
|
598
|
+
{ name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/c.ts', description: 'Test', suggestion: 'Test' },
|
|
599
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/d.ts', description: 'Test', suggestion: 'Test' },
|
|
600
|
+
{ name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/e.ts', description: 'Test', suggestion: 'Test' },
|
|
601
|
+
{ name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/f.ts', description: 'Test', suggestion: 'Test' },
|
|
602
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/g.ts', description: 'Test', suggestion: 'Test' },
|
|
603
|
+
];
|
|
604
|
+
// 7 violations / 10 files = 70% ratio → score 20
|
|
605
|
+
const result = scorer.score([], antiPatterns, 10);
|
|
606
|
+
expect(result.breakdown.layering).toBe(20);
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it('should only count specific violation types', () => {
|
|
610
|
+
// Only count: 'Leaky Abstraction', 'Shotgun Surgery', 'Circular Dependency'
|
|
611
|
+
const antiPatterns: AntiPattern[] = [
|
|
612
|
+
{ name: 'God Class', severity: 'CRITICAL', location: 'src/a.ts', description: 'Test', suggestion: 'Test' },
|
|
613
|
+
{ name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/b.ts', description: 'Test', suggestion: 'Test' },
|
|
614
|
+
{ name: 'Feature Envy', severity: 'MEDIUM', location: 'src/c.ts', description: 'Test', suggestion: 'Test' },
|
|
615
|
+
{ name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/d.ts', description: 'Test', suggestion: 'Test' },
|
|
616
|
+
{ name: 'Long Method', severity: 'LOW', location: 'src/e.ts', description: 'Test', suggestion: 'Test' },
|
|
617
|
+
{ name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/f.ts', description: 'Test', suggestion: 'Test' },
|
|
618
|
+
];
|
|
619
|
+
// Only Leaky Abstraction, Shotgun Surgery, Circular Dependency count = 3
|
|
620
|
+
// 3 violations / 100 files = 3% ratio → score 95
|
|
621
|
+
const result = scorer.score([], antiPatterns, 100);
|
|
622
|
+
expect(result.breakdown.layering).toBe(95);
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
627
|
+
// OVERALL SCORE TESTS
|
|
628
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
629
|
+
describe('overall scoring and weighting', () => {
|
|
630
|
+
it('should compute weighted average correctly', () => {
|
|
631
|
+
// Create edges for known scores
|
|
632
|
+
const edges: DependencyEdge[] = [
|
|
633
|
+
{ from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
|
|
634
|
+
];
|
|
635
|
+
const antiPatterns: AntiPattern[] = [];
|
|
636
|
+
|
|
637
|
+
const result = scorer.score(edges, antiPatterns, 1);
|
|
638
|
+
|
|
639
|
+
// With 1 file, 1 edge:
|
|
640
|
+
// modularity = 95 (avgEdgesPerFile = 1)
|
|
641
|
+
// coupling = 50 (1 totalFile)
|
|
642
|
+
// cohesion = 95 (internal dependency)
|
|
643
|
+
// layering = 95 (no violations)
|
|
644
|
+
// overall = 95*0.4 + 50*0.25 + 95*0.2 + 95*0.15 = 38 + 12.5 + 19 + 14.25 = 83.75 ≈ 84
|
|
645
|
+
|
|
646
|
+
expect(result.overall).toBeGreaterThanOrEqual(83);
|
|
647
|
+
expect(result.overall).toBeLessThanOrEqual(95);
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
it('should clamp overall score to [0, 100]', () => {
|
|
651
|
+
const result = scorer.score([], [], 0);
|
|
652
|
+
expect(result.overall).toBeGreaterThanOrEqual(0);
|
|
653
|
+
expect(result.overall).toBeLessThanOrEqual(100);
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('should round component scores', () => {
|
|
657
|
+
const edges: DependencyEdge[] = Array(3)
|
|
658
|
+
.fill(null)
|
|
659
|
+
.map((_, i) => ({
|
|
660
|
+
from: 'a.ts',
|
|
661
|
+
to: `b${i}.ts`,
|
|
662
|
+
type: 'import' as const,
|
|
663
|
+
weight: 1,
|
|
664
|
+
}));
|
|
665
|
+
|
|
666
|
+
const result = scorer.score(edges, [], 1);
|
|
667
|
+
|
|
668
|
+
// All component scores should be integers
|
|
669
|
+
expect(Number.isInteger(result.breakdown.modularity)).toBe(true);
|
|
670
|
+
expect(Number.isInteger(result.breakdown.coupling)).toBe(true);
|
|
671
|
+
expect(Number.isInteger(result.breakdown.cohesion)).toBe(true);
|
|
672
|
+
expect(Number.isInteger(result.breakdown.layering)).toBe(true);
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
});
|