@girardelli/architect 2.2.0 → 4.0.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 (296) hide show
  1. package/README.md +105 -116
  2. package/__test_agent_output__/INDEX.md +1 -0
  3. package/__test_agent_output__/agents/AGENT-ORCHESTRATOR.md +1 -0
  4. package/__test_agent_output__/agents/DATABASE-ENGINEER.md +174 -0
  5. package/__test_agent_output__/agents/QA-TEST-ENGINEER.md +138 -0
  6. package/__test_agent_output__/agents/SECURITY-AUDITOR.md +106 -0
  7. package/__test_agent_output__/agents/TECH-DEBT-CONTROLLER.md +104 -0
  8. package/__test_agent_output__/agents/TYPESCRIPT-BACKEND-DEVELOPER.md +135 -0
  9. package/__test_agent_output__/guards/CODE-REVIEW-CHECKLIST.md +95 -0
  10. package/__test_agent_output__/guards/PREFLIGHT.md +200 -0
  11. package/__test_agent_output__/guards/QUALITY-GATES.md +1 -0
  12. package/__test_agent_output__/rules/00-general.md +229 -0
  13. package/__test_agent_output__/rules/01-architecture.md +191 -0
  14. package/__test_agent_output__/rules/02-security.md +402 -0
  15. package/__test_agent_output__/rules/03-nestjs.md +124 -0
  16. package/__test_agent_output__/templates/ADR.md +95 -0
  17. package/__test_agent_output__/templates/BDD.md +58 -0
  18. package/__test_agent_output__/templates/C4.md +68 -0
  19. package/__test_agent_output__/templates/TDD.md +86 -0
  20. package/__test_agent_output__/templates/THREAT-MODEL.md +82 -0
  21. package/__test_agent_output__/workflows/fix-bug.md +228 -0
  22. package/__test_agent_output__/workflows/new-feature.md +311 -0
  23. package/__test_agent_output__/workflows/review.md +95 -0
  24. package/__test_context_7RvUrO/src/modules/empty/empty.ts +0 -0
  25. package/__test_context_Rf5fNJ/src/modules/mixed/mixed.ts +5 -0
  26. package/__test_context_WRCnYH/src/modules/test/test.ts +10 -0
  27. package/__test_context_YsnVS3/src/modules/test/test.ts +10 -0
  28. package/__test_context_w7XZeH/src/modules/mixed/mixed.ts +5 -0
  29. package/__test_context_y5noh6/src/modules/empty/empty.ts +0 -0
  30. package/__test_framework__24OjAu/package.json +1 -0
  31. package/__test_framework__3ZDZsx/pyproject.toml +8 -0
  32. package/__test_framework__4T54Jn/package.json +1 -0
  33. package/__test_framework__4tlXu9/pyproject.toml +8 -0
  34. package/__test_framework__6boWqQ/Pipfile +6 -0
  35. package/__test_framework__6gygMU/pom.xml +10 -0
  36. package/__test_framework__6kxj0N/go.mod +8 -0
  37. package/__test_framework__7CEoXw/pom.xml +10 -0
  38. package/__test_framework__85DDz0/Pipfile +6 -0
  39. package/__test_framework__9WrRIr/pom.xml +7 -0
  40. package/__test_framework__ANqGKl/Gemfile +5 -0
  41. package/__test_framework__BCXTEM/go.mod +3 -0
  42. package/__test_framework__BHiPNq/setup.py +2 -0
  43. package/__test_framework__BqkiKv/package.json +1 -0
  44. package/__test_framework__C5yd8X/Pipfile.lock +1 -0
  45. package/__test_framework__C5yd8X/requirements.txt +1 -0
  46. package/__test_framework__C87d3a/manage.py +1 -0
  47. package/__test_framework__C87d3a/requirements.txt +2 -0
  48. package/__test_framework__DXNwc5/build.gradle +7 -0
  49. package/__test_framework__GhHSt3/build.gradle.kts +4 -0
  50. package/__test_framework__GzklJP/Cargo.toml +7 -0
  51. package/__test_framework__H4hd13/go.mod +8 -0
  52. package/__test_framework__HKjOXO/composer.json +1 -0
  53. package/__test_framework__HaDN45/Gemfile +3 -0
  54. package/__test_framework__IBO7YG/pyproject.toml +9 -0
  55. package/__test_framework__JwSOyF/pyproject.toml +6 -0
  56. package/__test_framework__K6HrCr/build.gradle +2 -0
  57. package/__test_framework__KzRPlh/pubspec.yaml +9 -0
  58. package/__test_framework__L6uIym/pyproject.toml +6 -0
  59. package/__test_framework__LOdoGK/requirements.txt +4 -0
  60. package/__test_framework__LgHzss/package.json +1 -0
  61. package/__test_framework__M76M6q/Gemfile +5 -0
  62. package/__test_framework__Mr9vWW/composer.json +1 -0
  63. package/__test_framework__N03Gnv/package.json +1 -0
  64. package/__test_framework__Num4UE/requirements +1 -0
  65. package/__test_framework__OAGw3Y/build.gradle +7 -0
  66. package/__test_framework__OQc8yG/pubspec.yaml +9 -0
  67. package/__test_framework__OwKZcd/requirements.txt +3 -0
  68. package/__test_framework__P0gFv7/requirements +1 -0
  69. package/__test_framework__PN55Rq/package.json +1 -0
  70. package/__test_framework__PQiqX8/pubspec.yaml +3 -0
  71. package/__test_framework__RBHsg7/composer.json +1 -0
  72. package/__test_framework__RHxif4/Cargo.toml +7 -0
  73. package/__test_framework__T0v0p1/Cargo.toml +4 -0
  74. package/__test_framework__Tu0clt/Pipfile.lock +1 -0
  75. package/__test_framework__Tu0clt/requirements.txt +1 -0
  76. package/__test_framework__TwDj9P/Cargo.toml +4 -0
  77. package/__test_framework__VQJNC4/pom.xml +7 -0
  78. package/__test_framework__W6sm05/package.json +1 -0
  79. package/__test_framework__W7vBLy/pyproject.toml +4 -0
  80. package/__test_framework__WNJOWT/setup.py +2 -0
  81. package/__test_framework__WSJs7U/package.json +1 -0
  82. package/__test_framework__YQ5VpA/build.gradle.kts +4 -0
  83. package/__test_framework__ZNEUEs/package.json +1 -0
  84. package/__test_framework__Znt922/pom.xml +7 -0
  85. package/__test_framework__azyg0h/pom.xml +7 -0
  86. package/__test_framework__c6otLr/package.json +1 -0
  87. package/__test_framework__cl9S9G/build.gradle +2 -0
  88. package/__test_framework__eilvV4/composer.json +1 -0
  89. package/__test_framework__gQZxXO/manage.py +1 -0
  90. package/__test_framework__gQZxXO/requirements.txt +2 -0
  91. package/__test_framework__ghvl26/poetry.lock +1 -0
  92. package/__test_framework__ghvl26/pyproject.toml +2 -0
  93. package/__test_framework__hR7b9U/Makefile +11 -0
  94. package/__test_framework__iESVsi/composer.json +1 -0
  95. package/__test_framework__jm6TJy/package.json +1 -0
  96. package/__test_framework__kBUpjs/pyproject.toml +9 -0
  97. package/__test_framework__kqoZrw/requirements.txt +4 -0
  98. package/__test_framework__lWkoyO/pyproject.toml +4 -0
  99. package/__test_framework__mTKnUO/package.json +1 -0
  100. package/__test_framework__nCeZwe/Makefile +11 -0
  101. package/__test_framework__oljsU0/package.json +1 -0
  102. package/__test_framework__osRG4q/go.mod +3 -0
  103. package/__test_framework__pCHH4F/package.json +1 -0
  104. package/__test_framework__pExx6E/Gemfile +3 -0
  105. package/__test_framework__pyBoGd/pyproject.toml +5 -0
  106. package/__test_framework__qw16VQ/package.json +1 -0
  107. package/__test_framework__rRayrG/package.json +1 -0
  108. package/__test_framework__s82zO5/package.json +1 -0
  109. package/__test_framework__tp8MFK/pyproject.toml +5 -0
  110. package/__test_framework__w44k4w/composer.json +1 -0
  111. package/__test_framework__yefPZY/poetry.lock +1 -0
  112. package/__test_framework__yefPZY/pyproject.toml +2 -0
  113. package/__test_framework__zCiyDT/requirements.txt +3 -0
  114. package/__test_framework__zGZN3j/pubspec.yaml +3 -0
  115. package/__test_framework__zXpnxL/package.json +1 -0
  116. package/architect-run.sh +431 -0
  117. package/assets/banner-v3.html +561 -0
  118. package/dist/agent-generator/context-enricher.d.ts +58 -0
  119. package/dist/agent-generator/context-enricher.d.ts.map +1 -0
  120. package/dist/agent-generator/context-enricher.js +581 -0
  121. package/dist/agent-generator/context-enricher.js.map +1 -0
  122. package/dist/agent-generator/domain-inferrer.d.ts +52 -0
  123. package/dist/agent-generator/domain-inferrer.d.ts.map +1 -0
  124. package/dist/agent-generator/domain-inferrer.js +575 -0
  125. package/dist/agent-generator/domain-inferrer.js.map +1 -0
  126. package/dist/agent-generator/framework-detector.d.ts +40 -0
  127. package/dist/agent-generator/framework-detector.d.ts.map +1 -0
  128. package/dist/agent-generator/framework-detector.js +611 -0
  129. package/dist/agent-generator/framework-detector.js.map +1 -0
  130. package/dist/agent-generator/index.d.ts +33 -0
  131. package/dist/agent-generator/index.d.ts.map +1 -0
  132. package/dist/agent-generator/index.js +477 -0
  133. package/dist/agent-generator/index.js.map +1 -0
  134. package/dist/agent-generator/stack-detector.d.ts +12 -0
  135. package/dist/agent-generator/stack-detector.d.ts.map +1 -0
  136. package/dist/agent-generator/stack-detector.js +128 -0
  137. package/dist/agent-generator/stack-detector.js.map +1 -0
  138. package/dist/agent-generator/templates/core/agents.d.ts +17 -0
  139. package/dist/agent-generator/templates/core/agents.d.ts.map +1 -0
  140. package/dist/agent-generator/templates/core/agents.js +1252 -0
  141. package/dist/agent-generator/templates/core/agents.js.map +1 -0
  142. package/dist/agent-generator/templates/core/architecture-rules.d.ts +7 -0
  143. package/dist/agent-generator/templates/core/architecture-rules.d.ts.map +1 -0
  144. package/dist/agent-generator/templates/core/architecture-rules.js +274 -0
  145. package/dist/agent-generator/templates/core/architecture-rules.js.map +1 -0
  146. package/dist/agent-generator/templates/core/general-rules.d.ts +8 -0
  147. package/dist/agent-generator/templates/core/general-rules.d.ts.map +1 -0
  148. package/dist/agent-generator/templates/core/general-rules.js +301 -0
  149. package/dist/agent-generator/templates/core/general-rules.js.map +1 -0
  150. package/dist/agent-generator/templates/core/index-md.d.ts +7 -0
  151. package/dist/agent-generator/templates/core/index-md.d.ts.map +1 -0
  152. package/dist/agent-generator/templates/core/index-md.js +246 -0
  153. package/dist/agent-generator/templates/core/index-md.js.map +1 -0
  154. package/dist/agent-generator/templates/core/orchestrator.d.ts +8 -0
  155. package/dist/agent-generator/templates/core/orchestrator.d.ts.map +1 -0
  156. package/dist/agent-generator/templates/core/orchestrator.js +422 -0
  157. package/dist/agent-generator/templates/core/orchestrator.js.map +1 -0
  158. package/dist/agent-generator/templates/core/preflight.d.ts +8 -0
  159. package/dist/agent-generator/templates/core/preflight.d.ts.map +1 -0
  160. package/dist/agent-generator/templates/core/preflight.js +213 -0
  161. package/dist/agent-generator/templates/core/preflight.js.map +1 -0
  162. package/dist/agent-generator/templates/core/quality-gates.d.ts +11 -0
  163. package/dist/agent-generator/templates/core/quality-gates.d.ts.map +1 -0
  164. package/dist/agent-generator/templates/core/quality-gates.js +254 -0
  165. package/dist/agent-generator/templates/core/quality-gates.js.map +1 -0
  166. package/dist/agent-generator/templates/core/security-rules.d.ts +7 -0
  167. package/dist/agent-generator/templates/core/security-rules.d.ts.map +1 -0
  168. package/dist/agent-generator/templates/core/security-rules.js +528 -0
  169. package/dist/agent-generator/templates/core/security-rules.js.map +1 -0
  170. package/dist/agent-generator/templates/core/skills-generator.d.ts +6 -0
  171. package/dist/agent-generator/templates/core/skills-generator.d.ts.map +1 -0
  172. package/dist/agent-generator/templates/core/skills-generator.js +207 -0
  173. package/dist/agent-generator/templates/core/skills-generator.js.map +1 -0
  174. package/dist/agent-generator/templates/core/workflow-fix-bug.d.ts +7 -0
  175. package/dist/agent-generator/templates/core/workflow-fix-bug.d.ts.map +1 -0
  176. package/dist/agent-generator/templates/core/workflow-fix-bug.js +237 -0
  177. package/dist/agent-generator/templates/core/workflow-fix-bug.js.map +1 -0
  178. package/dist/agent-generator/templates/core/workflow-new-feature.d.ts +8 -0
  179. package/dist/agent-generator/templates/core/workflow-new-feature.d.ts.map +1 -0
  180. package/dist/agent-generator/templates/core/workflow-new-feature.js +321 -0
  181. package/dist/agent-generator/templates/core/workflow-new-feature.js.map +1 -0
  182. package/dist/agent-generator/templates/core/workflow-review.d.ts +7 -0
  183. package/dist/agent-generator/templates/core/workflow-review.d.ts.map +1 -0
  184. package/dist/agent-generator/templates/core/workflow-review.js +104 -0
  185. package/dist/agent-generator/templates/core/workflow-review.js.map +1 -0
  186. package/dist/agent-generator/templates/domain/index.d.ts +22 -0
  187. package/dist/agent-generator/templates/domain/index.d.ts.map +1 -0
  188. package/dist/agent-generator/templates/domain/index.js +1176 -0
  189. package/dist/agent-generator/templates/domain/index.js.map +1 -0
  190. package/dist/agent-generator/templates/stack/index.d.ts +8 -0
  191. package/dist/agent-generator/templates/stack/index.d.ts.map +1 -0
  192. package/dist/agent-generator/templates/stack/index.js +695 -0
  193. package/dist/agent-generator/templates/stack/index.js.map +1 -0
  194. package/dist/agent-generator/templates/template-helpers.d.ts +75 -0
  195. package/dist/agent-generator/templates/template-helpers.d.ts.map +1 -0
  196. package/dist/agent-generator/templates/template-helpers.js +726 -0
  197. package/dist/agent-generator/templates/template-helpers.js.map +1 -0
  198. package/dist/agent-generator/types.d.ts +196 -0
  199. package/dist/agent-generator/types.d.ts.map +1 -0
  200. package/dist/agent-generator/types.js +27 -0
  201. package/dist/agent-generator/types.js.map +1 -0
  202. package/dist/analyzer.d.ts +5 -0
  203. package/dist/analyzer.d.ts.map +1 -1
  204. package/dist/analyzer.js +35 -4
  205. package/dist/analyzer.js.map +1 -1
  206. package/dist/analyzers/forecast.d.ts +85 -0
  207. package/dist/analyzers/forecast.d.ts.map +1 -0
  208. package/dist/analyzers/forecast.js +337 -0
  209. package/dist/analyzers/forecast.js.map +1 -0
  210. package/dist/analyzers/git-cache.d.ts +7 -0
  211. package/dist/analyzers/git-cache.d.ts.map +1 -0
  212. package/dist/analyzers/git-cache.js +41 -0
  213. package/dist/analyzers/git-cache.js.map +1 -0
  214. package/dist/analyzers/git-history.d.ts +113 -0
  215. package/dist/analyzers/git-history.d.ts.map +1 -0
  216. package/dist/analyzers/git-history.js +333 -0
  217. package/dist/analyzers/git-history.js.map +1 -0
  218. package/dist/analyzers/index.d.ts +10 -0
  219. package/dist/analyzers/index.d.ts.map +1 -0
  220. package/dist/analyzers/index.js +7 -0
  221. package/dist/analyzers/index.js.map +1 -0
  222. package/dist/analyzers/temporal-scorer.d.ts +72 -0
  223. package/dist/analyzers/temporal-scorer.d.ts.map +1 -0
  224. package/dist/analyzers/temporal-scorer.js +140 -0
  225. package/dist/analyzers/temporal-scorer.js.map +1 -0
  226. package/dist/cli.d.ts +2 -3
  227. package/dist/cli.d.ts.map +1 -1
  228. package/dist/cli.js +275 -113
  229. package/dist/cli.js.map +1 -1
  230. package/dist/html-reporter.d.ts +3 -1
  231. package/dist/html-reporter.d.ts.map +1 -1
  232. package/dist/html-reporter.js +248 -12
  233. package/dist/html-reporter.js.map +1 -1
  234. package/dist/index.d.ts +16 -3
  235. package/dist/index.d.ts.map +1 -1
  236. package/dist/index.js +63 -4
  237. package/dist/index.js.map +1 -1
  238. package/dist/project-summarizer.d.ts +18 -0
  239. package/dist/project-summarizer.d.ts.map +1 -0
  240. package/dist/project-summarizer.js +306 -0
  241. package/dist/project-summarizer.js.map +1 -0
  242. package/dist/refactor-reporter.js +1 -1
  243. package/dist/types.d.ts +13 -0
  244. package/dist/types.d.ts.map +1 -1
  245. package/package.json +12 -3
  246. package/src/agent-generator/context-enricher.ts +643 -0
  247. package/src/agent-generator/domain-inferrer.ts +625 -0
  248. package/src/agent-generator/framework-detector.ts +669 -0
  249. package/src/agent-generator/index.ts +555 -0
  250. package/src/agent-generator/stack-detector.ts +103 -0
  251. package/src/agent-generator/templates/core/agents.ts +1293 -0
  252. package/src/agent-generator/templates/core/architecture-rules.ts +287 -0
  253. package/src/agent-generator/templates/core/general-rules.ts +306 -0
  254. package/src/agent-generator/templates/core/index-md.ts +260 -0
  255. package/src/agent-generator/templates/core/orchestrator.ts +459 -0
  256. package/src/agent-generator/templates/core/preflight.ts +215 -0
  257. package/src/agent-generator/templates/core/quality-gates.ts +256 -0
  258. package/src/agent-generator/templates/core/security-rules.ts +543 -0
  259. package/src/agent-generator/templates/core/skills-generator.ts +236 -0
  260. package/src/agent-generator/templates/core/workflow-fix-bug.ts +239 -0
  261. package/src/agent-generator/templates/core/workflow-new-feature.ts +323 -0
  262. package/src/agent-generator/templates/core/workflow-review.ts +106 -0
  263. package/src/agent-generator/templates/domain/index.ts +1201 -0
  264. package/src/agent-generator/templates/stack/index.ts +705 -0
  265. package/src/agent-generator/templates/template-helpers.ts +776 -0
  266. package/src/agent-generator/types.ts +232 -0
  267. package/src/analyzer.ts +38 -4
  268. package/src/analyzers/forecast.ts +496 -0
  269. package/src/analyzers/git-cache.ts +52 -0
  270. package/src/analyzers/git-history.ts +488 -0
  271. package/src/analyzers/index.ts +33 -0
  272. package/src/analyzers/temporal-scorer.ts +227 -0
  273. package/src/cli.ts +316 -117
  274. package/src/html-reporter.ts +263 -13
  275. package/src/index.ts +92 -9
  276. package/src/project-summarizer.ts +347 -0
  277. package/src/refactor-reporter.ts +1 -1
  278. package/src/types.ts +10 -0
  279. package/tests/agent-generator.test.ts +411 -0
  280. package/tests/analyzers-integration.test.ts +174 -0
  281. package/tests/architect-adapter-enrichment.test.ts +9 -0
  282. package/tests/context-enricher.test.ts +971 -0
  283. package/tests/forecast.test.ts +509 -0
  284. package/tests/framework-detector.test.ts +1172 -0
  285. package/tests/git-history.test.ts +254 -0
  286. package/tests/scanner.test.ts +7 -8
  287. package/tests/scorer.test.ts +588 -0
  288. package/tests/stack-detector.test.ts +241 -0
  289. package/tests/template-generation.test.ts +706 -0
  290. package/tests/template-helpers.test.ts +1152 -0
  291. package/tests/temporal-scorer.test.ts +307 -0
  292. package/dist/agent-generator.d.ts +0 -106
  293. package/dist/agent-generator.d.ts.map +0 -1
  294. package/dist/agent-generator.js +0 -1398
  295. package/dist/agent-generator.js.map +0 -1
  296. package/src/agent-generator.ts +0 -1526
@@ -77,4 +77,592 @@ describe('ArchitectureScorer', () => {
77
77
  );
78
78
  });
79
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
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(95);
514
+ });
515
+
516
+ it('should score 85 when there is 1 layering violation', () => {
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
+ const result = scorer.score([], antiPatterns, 10);
527
+ expect(result.breakdown.layering).toBe(85);
528
+ });
529
+
530
+ it('should score 70 when there are 2 violations', () => {
531
+ const antiPatterns: AntiPattern[] = [
532
+ {
533
+ name: 'Leaky Abstraction',
534
+ severity: 'HIGH',
535
+ location: 'src/api.ts',
536
+ description: 'Test',
537
+ suggestion: 'Test',
538
+ },
539
+ {
540
+ name: 'Shotgun Surgery',
541
+ severity: 'MEDIUM',
542
+ location: 'src/service.ts',
543
+ description: 'Test',
544
+ suggestion: 'Test',
545
+ },
546
+ ];
547
+ const result = scorer.score([], antiPatterns, 10);
548
+ expect(result.breakdown.layering).toBe(70);
549
+ });
550
+
551
+ it('should score 55 when there are 3 violations', () => {
552
+ const antiPatterns: AntiPattern[] = [
553
+ {
554
+ name: 'Leaky Abstraction',
555
+ severity: 'HIGH',
556
+ location: 'src/api.ts',
557
+ description: 'Test',
558
+ suggestion: 'Test',
559
+ },
560
+ {
561
+ name: 'Shotgun Surgery',
562
+ severity: 'MEDIUM',
563
+ location: 'src/service.ts',
564
+ description: 'Test',
565
+ suggestion: 'Test',
566
+ },
567
+ {
568
+ name: 'Circular Dependency',
569
+ severity: 'CRITICAL',
570
+ location: 'src/model.ts',
571
+ description: 'Test',
572
+ suggestion: 'Test',
573
+ },
574
+ ];
575
+ const result = scorer.score([], antiPatterns, 10);
576
+ expect(result.breakdown.layering).toBe(55);
577
+ });
578
+
579
+ it('should score 40 when there are 4-5 violations', () => {
580
+ const antiPatterns: AntiPattern[] = [
581
+ { name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/api.ts', description: 'Test', suggestion: 'Test' },
582
+ { name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/service.ts', description: 'Test', suggestion: 'Test' },
583
+ { name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/model.ts', description: 'Test', suggestion: 'Test' },
584
+ { name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/data.ts', description: 'Test', suggestion: 'Test' },
585
+ ];
586
+ const result = scorer.score([], antiPatterns, 10);
587
+ expect(result.breakdown.layering).toBe(40);
588
+ });
589
+
590
+ it('should score 25 when there are 6+ violations', () => {
591
+ const antiPatterns: AntiPattern[] = [
592
+ { name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/a.ts', description: 'Test', suggestion: 'Test' },
593
+ { name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/b.ts', description: 'Test', suggestion: 'Test' },
594
+ { name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/c.ts', description: 'Test', suggestion: 'Test' },
595
+ { name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/d.ts', description: 'Test', suggestion: 'Test' },
596
+ { name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/e.ts', description: 'Test', suggestion: 'Test' },
597
+ { name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/f.ts', description: 'Test', suggestion: 'Test' },
598
+ ];
599
+ const result = scorer.score([], antiPatterns, 10);
600
+ expect(result.breakdown.layering).toBe(25);
601
+ });
602
+
603
+ it('should only count specific violation types', () => {
604
+ // Only count: 'Leaky Abstraction', 'Shotgun Surgery', 'Circular Dependency'
605
+ const antiPatterns: AntiPattern[] = [
606
+ { name: 'God Class', severity: 'CRITICAL', location: 'src/a.ts', description: 'Test', suggestion: 'Test' },
607
+ { name: 'Leaky Abstraction', severity: 'HIGH', location: 'src/b.ts', description: 'Test', suggestion: 'Test' },
608
+ { name: 'Feature Envy', severity: 'MEDIUM', location: 'src/c.ts', description: 'Test', suggestion: 'Test' },
609
+ { name: 'Shotgun Surgery', severity: 'MEDIUM', location: 'src/d.ts', description: 'Test', suggestion: 'Test' },
610
+ { name: 'Long Method', severity: 'LOW', location: 'src/e.ts', description: 'Test', suggestion: 'Test' },
611
+ { name: 'Circular Dependency', severity: 'CRITICAL', location: 'src/f.ts', description: 'Test', suggestion: 'Test' },
612
+ ];
613
+ const result = scorer.score([], antiPatterns, 10);
614
+ // Only Leaky Abstraction, Shotgun Surgery, Circular Dependency count = 3
615
+ expect(result.breakdown.layering).toBe(55);
616
+ });
617
+ });
618
+
619
+ // ═══════════════════════════════════════════════════════════════════════
620
+ // OVERALL SCORE TESTS
621
+ // ═══════════════════════════════════════════════════════════════════════
622
+ describe('overall scoring and weighting', () => {
623
+ it('should compute weighted average correctly', () => {
624
+ // Create edges for known scores
625
+ const edges: DependencyEdge[] = [
626
+ { from: 'api/a.ts', to: 'api/b.ts', type: 'import', weight: 1 },
627
+ ];
628
+ const antiPatterns: AntiPattern[] = [];
629
+
630
+ const result = scorer.score(edges, antiPatterns, 1);
631
+
632
+ // With 1 file, 1 edge:
633
+ // modularity = 95 (avgEdgesPerFile = 1)
634
+ // coupling = 50 (1 totalFile)
635
+ // cohesion = 95 (internal dependency)
636
+ // layering = 95 (no violations)
637
+ // overall = 95*0.4 + 50*0.25 + 95*0.2 + 95*0.15 = 38 + 12.5 + 19 + 14.25 = 83.75 ≈ 84
638
+
639
+ expect(result.overall).toBeGreaterThanOrEqual(83);
640
+ expect(result.overall).toBeLessThanOrEqual(84);
641
+ });
642
+
643
+ it('should clamp overall score to [0, 100]', () => {
644
+ const result = scorer.score([], [], 0);
645
+ expect(result.overall).toBeGreaterThanOrEqual(0);
646
+ expect(result.overall).toBeLessThanOrEqual(100);
647
+ });
648
+
649
+ it('should round component scores', () => {
650
+ const edges: DependencyEdge[] = Array(3)
651
+ .fill(null)
652
+ .map((_, i) => ({
653
+ from: 'a.ts',
654
+ to: `b${i}.ts`,
655
+ type: 'import' as const,
656
+ weight: 1,
657
+ }));
658
+
659
+ const result = scorer.score(edges, [], 1);
660
+
661
+ // All component scores should be integers
662
+ expect(Number.isInteger(result.breakdown.modularity)).toBe(true);
663
+ expect(Number.isInteger(result.breakdown.coupling)).toBe(true);
664
+ expect(Number.isInteger(result.breakdown.cohesion)).toBe(true);
665
+ expect(Number.isInteger(result.breakdown.layering)).toBe(true);
666
+ });
667
+ });
80
668
  });