@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.
Files changed (217) hide show
  1. package/dist/src/core/analyzer.d.ts +42 -0
  2. package/dist/src/core/analyzer.js +431 -0
  3. package/dist/src/core/analyzer.js.map +1 -0
  4. package/dist/src/core/analyzers/forecast.d.ts +84 -0
  5. package/dist/src/core/analyzers/forecast.js +338 -0
  6. package/dist/src/core/analyzers/forecast.js.map +1 -0
  7. package/dist/src/core/analyzers/index.d.ts +9 -0
  8. package/dist/src/core/analyzers/index.js +7 -0
  9. package/dist/src/core/analyzers/index.js.map +1 -0
  10. package/dist/src/core/analyzers/temporal-scorer.d.ts +71 -0
  11. package/dist/src/core/analyzers/temporal-scorer.js +141 -0
  12. package/dist/src/core/analyzers/temporal-scorer.js.map +1 -0
  13. package/dist/src/core/anti-patterns.d.ts +28 -0
  14. package/dist/src/core/anti-patterns.js +264 -0
  15. package/dist/src/core/anti-patterns.js.map +1 -0
  16. package/dist/src/core/ast/ast-parser.interface.d.ts +20 -0
  17. package/dist/src/core/ast/ast-parser.interface.js +2 -0
  18. package/dist/src/core/ast/ast-parser.interface.js.map +1 -0
  19. package/dist/src/core/ast/path-resolver.d.ts +13 -0
  20. package/dist/src/core/ast/path-resolver.js +54 -0
  21. package/dist/src/core/ast/path-resolver.js.map +1 -0
  22. package/dist/src/core/ast/tree-sitter-parser.d.ts +10 -0
  23. package/dist/src/core/ast/tree-sitter-parser.js +142 -0
  24. package/dist/src/core/ast/tree-sitter-parser.js.map +1 -0
  25. package/dist/src/core/config.d.ts +11 -0
  26. package/dist/src/core/config.js +112 -0
  27. package/dist/src/core/config.js.map +1 -0
  28. package/dist/src/core/diagram.d.ts +9 -0
  29. package/dist/src/core/diagram.js +101 -0
  30. package/dist/src/core/diagram.js.map +1 -0
  31. package/dist/src/core/i18n.d.ts +14 -0
  32. package/dist/src/core/i18n.js +54 -0
  33. package/dist/src/core/i18n.js.map +1 -0
  34. package/dist/src/core/locales/en.d.ts +2 -0
  35. package/dist/src/core/locales/en.js +337 -0
  36. package/dist/src/core/locales/en.js.map +1 -0
  37. package/dist/src/core/locales/pt-BR.d.ts +172 -0
  38. package/dist/src/core/locales/pt-BR.js +337 -0
  39. package/dist/src/core/locales/pt-BR.js.map +1 -0
  40. package/dist/src/core/locales/types.d.ts +86 -0
  41. package/dist/src/core/locales/types.js +2 -0
  42. package/dist/src/core/locales/types.js.map +1 -0
  43. package/dist/src/core/plugin-loader.d.ts +11 -0
  44. package/dist/src/core/plugin-loader.js +67 -0
  45. package/dist/src/core/plugin-loader.js.map +1 -0
  46. package/dist/src/core/project-summarizer.d.ts +16 -0
  47. package/dist/src/core/project-summarizer.js +37 -0
  48. package/dist/src/core/project-summarizer.js.map +1 -0
  49. package/dist/src/core/refactor-engine.d.ts +18 -0
  50. package/dist/src/core/refactor-engine.js +87 -0
  51. package/dist/src/core/refactor-engine.js.map +1 -0
  52. package/dist/src/core/rules/barrel-optimizer.d.ts +13 -0
  53. package/dist/src/core/rules/barrel-optimizer.js +76 -0
  54. package/dist/src/core/rules/barrel-optimizer.js.map +1 -0
  55. package/dist/src/core/rules/dead-code-detector.d.ts +21 -0
  56. package/dist/src/core/rules/dead-code-detector.js +116 -0
  57. package/dist/src/core/rules/dead-code-detector.js.map +1 -0
  58. package/dist/src/core/rules/hub-splitter.d.ts +13 -0
  59. package/dist/src/core/rules/hub-splitter.js +117 -0
  60. package/dist/src/core/rules/hub-splitter.js.map +1 -0
  61. package/dist/src/core/rules/import-organizer.d.ts +13 -0
  62. package/dist/src/core/rules/import-organizer.js +84 -0
  63. package/dist/src/core/rules/import-organizer.js.map +1 -0
  64. package/dist/src/core/rules/module-grouper.d.ts +13 -0
  65. package/dist/src/core/rules/module-grouper.js +116 -0
  66. package/dist/src/core/rules/module-grouper.js.map +1 -0
  67. package/dist/src/core/rules-engine.d.ts +7 -0
  68. package/dist/src/core/rules-engine.js +89 -0
  69. package/dist/src/core/rules-engine.js.map +1 -0
  70. package/dist/src/core/scorer.d.ts +15 -0
  71. package/dist/src/core/scorer.js +165 -0
  72. package/dist/src/core/scorer.js.map +1 -0
  73. package/dist/src/core/summarizer/keyword-extractor.d.ts +6 -0
  74. package/dist/src/core/summarizer/keyword-extractor.js +38 -0
  75. package/dist/src/core/summarizer/keyword-extractor.js.map +1 -0
  76. package/dist/src/core/summarizer/module-inferrer.d.ts +11 -0
  77. package/dist/src/core/summarizer/module-inferrer.js +171 -0
  78. package/dist/src/core/summarizer/module-inferrer.js.map +1 -0
  79. package/dist/src/core/summarizer/package-reader.d.ts +3 -0
  80. package/dist/src/core/summarizer/package-reader.js +33 -0
  81. package/dist/src/core/summarizer/package-reader.js.map +1 -0
  82. package/dist/src/core/summarizer/purpose-inferrer.d.ts +8 -0
  83. package/dist/src/core/summarizer/purpose-inferrer.js +179 -0
  84. package/dist/src/core/summarizer/purpose-inferrer.js.map +1 -0
  85. package/dist/src/core/summarizer/readme-reader.d.ts +3 -0
  86. package/dist/src/core/summarizer/readme-reader.js +24 -0
  87. package/dist/src/core/summarizer/readme-reader.js.map +1 -0
  88. package/dist/src/core/types/architect-rules.d.ts +27 -0
  89. package/dist/src/core/types/architect-rules.js +2 -0
  90. package/dist/src/core/types/architect-rules.js.map +1 -0
  91. package/dist/src/core/types/core.d.ts +87 -0
  92. package/dist/src/core/types/core.js +2 -0
  93. package/dist/src/core/types/core.js.map +1 -0
  94. package/dist/src/core/types/infrastructure.d.ts +38 -0
  95. package/dist/src/core/types/infrastructure.js +2 -0
  96. package/dist/src/core/types/infrastructure.js.map +1 -0
  97. package/dist/src/core/types/plugin.d.ts +12 -0
  98. package/dist/src/core/types/plugin.js +2 -0
  99. package/dist/src/core/types/plugin.js.map +1 -0
  100. package/dist/src/core/types/rules.d.ts +53 -0
  101. package/dist/src/core/types/rules.js +2 -0
  102. package/dist/src/core/types/rules.js.map +1 -0
  103. package/dist/src/core/types/summarizer.d.ts +12 -0
  104. package/dist/src/core/types/summarizer.js +2 -0
  105. package/dist/src/core/types/summarizer.js.map +1 -0
  106. package/dist/src/infrastructure/git-cache.d.ts +6 -0
  107. package/dist/src/infrastructure/git-cache.js +41 -0
  108. package/dist/src/infrastructure/git-cache.js.map +1 -0
  109. package/dist/src/infrastructure/git-history.d.ts +112 -0
  110. package/dist/src/infrastructure/git-history.js +340 -0
  111. package/dist/src/infrastructure/git-history.js.map +1 -0
  112. package/dist/src/infrastructure/logger.d.ts +20 -0
  113. package/dist/src/infrastructure/logger.js +57 -0
  114. package/dist/src/infrastructure/logger.js.map +1 -0
  115. package/dist/src/infrastructure/scanner.d.ts +31 -0
  116. package/dist/src/infrastructure/scanner.js +334 -0
  117. package/dist/src/infrastructure/scanner.js.map +1 -0
  118. package/dist/tests/analyzers-integration.test.d.ts +7 -0
  119. package/dist/tests/analyzers-integration.test.js +140 -0
  120. package/dist/tests/analyzers-integration.test.js.map +1 -0
  121. package/dist/tests/anti-patterns.test.d.ts +1 -0
  122. package/dist/tests/anti-patterns.test.js +81 -0
  123. package/dist/tests/anti-patterns.test.js.map +1 -0
  124. package/dist/tests/ast-parser.test.d.ts +1 -0
  125. package/dist/tests/ast-parser.test.js +94 -0
  126. package/dist/tests/ast-parser.test.js.map +1 -0
  127. package/dist/tests/fixtures/monorepo/packages/app/src/index.d.ts +1 -0
  128. package/dist/tests/fixtures/monorepo/packages/app/src/index.js +9 -0
  129. package/dist/tests/fixtures/monorepo/packages/app/src/index.js.map +1 -0
  130. package/dist/tests/fixtures/monorepo/packages/core/src/index.d.ts +2 -0
  131. package/dist/tests/fixtures/monorepo/packages/core/src/index.js +11 -0
  132. package/dist/tests/fixtures/monorepo/packages/core/src/index.js.map +1 -0
  133. package/dist/tests/forecast.test.d.ts +7 -0
  134. package/dist/tests/forecast.test.js +380 -0
  135. package/dist/tests/forecast.test.js.map +1 -0
  136. package/dist/tests/git-history.test.d.ts +7 -0
  137. package/dist/tests/git-history.test.js +193 -0
  138. package/dist/tests/git-history.test.js.map +1 -0
  139. package/dist/tests/i18n.test.d.ts +1 -0
  140. package/dist/tests/i18n.test.js +39 -0
  141. package/dist/tests/i18n.test.js.map +1 -0
  142. package/dist/tests/monorepo-scan.test.d.ts +11 -0
  143. package/dist/tests/monorepo-scan.test.js +143 -0
  144. package/dist/tests/monorepo-scan.test.js.map +1 -0
  145. package/dist/tests/plugin-loader.test.d.ts +1 -0
  146. package/dist/tests/plugin-loader.test.js +31 -0
  147. package/dist/tests/plugin-loader.test.js.map +1 -0
  148. package/dist/tests/rules-engine.test.d.ts +1 -0
  149. package/dist/tests/rules-engine.test.js +112 -0
  150. package/dist/tests/rules-engine.test.js.map +1 -0
  151. package/dist/tests/scanner.test.d.ts +1 -0
  152. package/dist/tests/scanner.test.js +44 -0
  153. package/dist/tests/scanner.test.js.map +1 -0
  154. package/dist/tests/scorer.test.d.ts +1 -0
  155. package/dist/tests/scorer.test.js +610 -0
  156. package/dist/tests/scorer.test.js.map +1 -0
  157. package/dist/tests/temporal-scorer.test.d.ts +7 -0
  158. package/dist/tests/temporal-scorer.test.js +239 -0
  159. package/dist/tests/temporal-scorer.test.js.map +1 -0
  160. package/package.json +29 -0
  161. package/src/core/analyzer.ts +499 -0
  162. package/src/core/analyzers/forecast.ts +497 -0
  163. package/src/core/analyzers/index.ts +33 -0
  164. package/src/core/analyzers/temporal-scorer.ts +227 -0
  165. package/src/core/anti-patterns.ts +324 -0
  166. package/src/core/ast/ast-parser.interface.ts +21 -0
  167. package/src/core/ast/path-resolver.ts +61 -0
  168. package/src/core/ast/tree-sitter-parser.ts +158 -0
  169. package/src/core/config.ts +125 -0
  170. package/src/core/diagram.ts +129 -0
  171. package/src/core/i18n.ts +64 -0
  172. package/src/core/locales/en.ts +340 -0
  173. package/src/core/locales/pt-BR.ts +341 -0
  174. package/src/core/locales/types.ts +95 -0
  175. package/src/core/plugin-loader.ts +80 -0
  176. package/src/core/project-summarizer.ts +42 -0
  177. package/src/core/refactor-engine.ts +112 -0
  178. package/src/core/rules/barrel-optimizer.ts +99 -0
  179. package/src/core/rules/dead-code-detector.ts +134 -0
  180. package/src/core/rules/hub-splitter.ts +135 -0
  181. package/src/core/rules/import-organizer.ts +100 -0
  182. package/src/core/rules/module-grouper.ts +133 -0
  183. package/src/core/rules-engine.ts +100 -0
  184. package/src/core/scorer.ts +181 -0
  185. package/src/core/summarizer/keyword-extractor.ts +53 -0
  186. package/src/core/summarizer/module-inferrer.ts +194 -0
  187. package/src/core/summarizer/package-reader.ts +34 -0
  188. package/src/core/summarizer/purpose-inferrer.ts +197 -0
  189. package/src/core/summarizer/readme-reader.ts +24 -0
  190. package/src/core/types/architect-rules.ts +29 -0
  191. package/src/core/types/core.ts +94 -0
  192. package/src/core/types/infrastructure.ts +41 -0
  193. package/src/core/types/plugin.ts +19 -0
  194. package/src/core/types/rules.ts +51 -0
  195. package/src/core/types/summarizer.ts +8 -0
  196. package/src/infrastructure/git-cache.ts +52 -0
  197. package/src/infrastructure/git-history.ts +496 -0
  198. package/src/infrastructure/logger.ts +68 -0
  199. package/src/infrastructure/scanner.ts +349 -0
  200. package/tests/analyzers-integration.test.ts +174 -0
  201. package/tests/anti-patterns.test.ts +95 -0
  202. package/tests/ast-parser.test.ts +102 -0
  203. package/tests/fixtures/monorepo/package.json +6 -0
  204. package/tests/fixtures/monorepo/packages/app/package.json +12 -0
  205. package/tests/fixtures/monorepo/packages/app/src/index.ts +6 -0
  206. package/tests/fixtures/monorepo/packages/core/package.json +7 -0
  207. package/tests/fixtures/monorepo/packages/core/src/index.ts +7 -0
  208. package/tests/forecast.test.ts +504 -0
  209. package/tests/git-history.test.ts +254 -0
  210. package/tests/i18n.test.ts +47 -0
  211. package/tests/monorepo-scan.test.ts +170 -0
  212. package/tests/plugin-loader.test.ts +40 -0
  213. package/tests/rules-engine.test.ts +131 -0
  214. package/tests/scanner.test.ts +54 -0
  215. package/tests/scorer.test.ts +675 -0
  216. package/tests/temporal-scorer.test.ts +306 -0
  217. 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
+ });