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