octocode-cli 1.2.6 → 1.2.8

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 (303) hide show
  1. package/LICENSE +21 -63
  2. package/README.md +85 -142
  3. package/out/octocode-cli.js +7063 -6934
  4. package/package.json +8 -6
  5. package/skills/README.md +97 -120
  6. package/skills/octocode-code-engineer/.claude/settings.local.json +18 -0
  7. package/skills/octocode-code-engineer/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +255 -0
  8. package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +223 -0
  9. package/skills/octocode-code-engineer/README.md +178 -0
  10. package/skills/octocode-code-engineer/SKILL.md +418 -0
  11. package/skills/octocode-code-engineer/coverage/architecture.ts.html +7828 -0
  12. package/skills/octocode-code-engineer/coverage/ast-helpers.ts.html +211 -0
  13. package/skills/octocode-code-engineer/coverage/ast-search.ts.html +1795 -0
  14. package/skills/octocode-code-engineer/coverage/base.css +224 -0
  15. package/skills/octocode-code-engineer/coverage/block-navigation.js +87 -0
  16. package/skills/octocode-code-engineer/coverage/cache.ts.html +376 -0
  17. package/skills/octocode-code-engineer/coverage/cli.ts.html +982 -0
  18. package/skills/octocode-code-engineer/coverage/clover.xml +3217 -0
  19. package/skills/octocode-code-engineer/coverage/collect-effects.ts.html +664 -0
  20. package/skills/octocode-code-engineer/coverage/collect-input-sources.ts.html +577 -0
  21. package/skills/octocode-code-engineer/coverage/collect-performance.ts.html +331 -0
  22. package/skills/octocode-code-engineer/coverage/collect-prototype-pollution.ts.html +421 -0
  23. package/skills/octocode-code-engineer/coverage/collect-security.ts.html +604 -0
  24. package/skills/octocode-code-engineer/coverage/collect-test-profile.ts.html +589 -0
  25. package/skills/octocode-code-engineer/coverage/coverage-final.json +30 -0
  26. package/skills/octocode-code-engineer/coverage/dependencies.ts.html +997 -0
  27. package/skills/octocode-code-engineer/coverage/dependency-summary.ts.html +688 -0
  28. package/skills/octocode-code-engineer/coverage/discovery.ts.html +322 -0
  29. package/skills/octocode-code-engineer/coverage/favicon.png +0 -0
  30. package/skills/octocode-code-engineer/coverage/graph-analytics.ts.html +1510 -0
  31. package/skills/octocode-code-engineer/coverage/index.html +536 -0
  32. package/skills/octocode-code-engineer/coverage/index.ts.html +826 -0
  33. package/skills/octocode-code-engineer/coverage/metrics.ts.html +553 -0
  34. package/skills/octocode-code-engineer/coverage/pipeline.ts.html +2044 -0
  35. package/skills/octocode-code-engineer/coverage/prettify.css +1 -0
  36. package/skills/octocode-code-engineer/coverage/prettify.js +2 -0
  37. package/skills/octocode-code-engineer/coverage/report-analysis.ts.html +1570 -0
  38. package/skills/octocode-code-engineer/coverage/report-writer.ts.html +1102 -0
  39. package/skills/octocode-code-engineer/coverage/security-detectors.ts.html +1747 -0
  40. package/skills/octocode-code-engineer/coverage/semantic-detectors.ts.html +2152 -0
  41. package/skills/octocode-code-engineer/coverage/semantic.ts.html +1897 -0
  42. package/skills/octocode-code-engineer/coverage/sort-arrow-sprite.png +0 -0
  43. package/skills/octocode-code-engineer/coverage/sorter.js +210 -0
  44. package/skills/octocode-code-engineer/coverage/summary-md.ts.html +1222 -0
  45. package/skills/octocode-code-engineer/coverage/test-quality-detectors.ts.html +1039 -0
  46. package/skills/octocode-code-engineer/coverage/tree-sitter-analyzer.ts.html +955 -0
  47. package/skills/octocode-code-engineer/coverage/ts-analyzer.ts.html +1213 -0
  48. package/skills/octocode-code-engineer/coverage/types.ts.html +2473 -0
  49. package/skills/octocode-code-engineer/coverage/utils.ts.html +820 -0
  50. package/skills/octocode-code-engineer/eslint.config.mjs +54 -0
  51. package/skills/octocode-code-engineer/minify-scripts.mjs +32 -0
  52. package/skills/octocode-code-engineer/package.json +54 -0
  53. package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +95 -0
  54. package/skills/octocode-code-engineer/references/architecture-techniques.md +121 -0
  55. package/skills/octocode-code-engineer/references/ast-search.md +210 -0
  56. package/skills/octocode-code-engineer/references/ast-tree-search.md +151 -0
  57. package/skills/octocode-code-engineer/references/cli-reference.md +167 -0
  58. package/skills/octocode-code-engineer/references/concepts.md +107 -0
  59. package/skills/octocode-code-engineer/references/finding-categories.md +128 -0
  60. package/skills/octocode-code-engineer/references/improvement-roadmap.md +304 -0
  61. package/skills/octocode-code-engineer/references/output-files.md +144 -0
  62. package/skills/octocode-code-engineer/references/playbooks.md +204 -0
  63. package/skills/octocode-code-engineer/references/present-results.md +136 -0
  64. package/skills/octocode-code-engineer/references/tool-workflows.md +566 -0
  65. package/skills/octocode-code-engineer/references/validate-investigate.md +225 -0
  66. package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +1 -0
  67. package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +1 -0
  68. package/skills/octocode-code-engineer/scripts/analysis/discovery.js +1 -0
  69. package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +1 -0
  70. package/skills/octocode-code-engineer/scripts/analysis/semantic.js +1 -0
  71. package/skills/octocode-code-engineer/scripts/ast/helpers.js +1 -0
  72. package/skills/octocode-code-engineer/scripts/ast/metrics.js +1 -0
  73. package/skills/octocode-code-engineer/scripts/ast/search.js +2 -0
  74. package/skills/octocode-code-engineer/scripts/ast/tree-search.js +2 -0
  75. package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +1 -0
  76. package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +1 -0
  77. package/skills/octocode-code-engineer/scripts/collectors/chains.js +1 -0
  78. package/skills/octocode-code-engineer/scripts/collectors/effects.js +1 -0
  79. package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +1 -0
  80. package/skills/octocode-code-engineer/scripts/collectors/performance.js +1 -0
  81. package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +1 -0
  82. package/skills/octocode-code-engineer/scripts/collectors/security.js +1 -0
  83. package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +1 -0
  84. package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +1 -0
  85. package/skills/octocode-code-engineer/scripts/common/utils.js +1 -0
  86. package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +1 -0
  87. package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +1 -0
  88. package/skills/octocode-code-engineer/scripts/detectors/coupling.js +1 -0
  89. package/skills/octocode-code-engineer/scripts/detectors/cycle.js +1 -0
  90. package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +1 -0
  91. package/skills/octocode-code-engineer/scripts/detectors/import-style.js +1 -0
  92. package/skills/octocode-code-engineer/scripts/detectors/index.js +1 -0
  93. package/skills/octocode-code-engineer/scripts/detectors/security.js +1 -0
  94. package/skills/octocode-code-engineer/scripts/detectors/semantic.js +1 -0
  95. package/skills/octocode-code-engineer/scripts/detectors/shared.js +1 -0
  96. package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +1 -0
  97. package/skills/octocode-code-engineer/scripts/index.js +1 -0
  98. package/skills/octocode-code-engineer/scripts/pipeline/cache.js +1 -0
  99. package/skills/octocode-code-engineer/scripts/pipeline/cli.js +1 -0
  100. package/skills/octocode-code-engineer/scripts/pipeline/main.js +2 -0
  101. package/skills/octocode-code-engineer/scripts/reporting/analysis.js +1 -0
  102. package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +1 -0
  103. package/skills/octocode-code-engineer/scripts/reporting/writer.js +1 -0
  104. package/skills/octocode-code-engineer/scripts/types/constants.js +1 -0
  105. package/skills/octocode-code-engineer/scripts/types/index.js +1 -0
  106. package/skills/octocode-code-engineer/scripts/types/interfaces.js +1 -0
  107. package/skills/octocode-code-engineer/src/analysis/dependencies.test.ts +545 -0
  108. package/skills/octocode-code-engineer/src/analysis/dependencies.ts +406 -0
  109. package/skills/octocode-code-engineer/src/analysis/dependency-summary.test.ts +566 -0
  110. package/skills/octocode-code-engineer/src/analysis/dependency-summary.ts +257 -0
  111. package/skills/octocode-code-engineer/src/analysis/discovery.test.ts +420 -0
  112. package/skills/octocode-code-engineer/src/analysis/discovery.ts +87 -0
  113. package/skills/octocode-code-engineer/src/analysis/graph-analytics.test.ts +449 -0
  114. package/skills/octocode-code-engineer/src/analysis/graph-analytics.ts +534 -0
  115. package/skills/octocode-code-engineer/src/analysis/semantic.test.ts +1533 -0
  116. package/skills/octocode-code-engineer/src/analysis/semantic.ts +830 -0
  117. package/skills/octocode-code-engineer/src/ast/helpers.test.ts +185 -0
  118. package/skills/octocode-code-engineer/src/ast/helpers.ts +62 -0
  119. package/skills/octocode-code-engineer/src/ast/metrics.test.ts +304 -0
  120. package/skills/octocode-code-engineer/src/ast/metrics.ts +204 -0
  121. package/skills/octocode-code-engineer/src/ast/search.test.ts +647 -0
  122. package/skills/octocode-code-engineer/src/ast/search.ts +648 -0
  123. package/skills/octocode-code-engineer/src/ast/tree-search.test.ts +199 -0
  124. package/skills/octocode-code-engineer/src/ast/tree-search.ts +392 -0
  125. package/skills/octocode-code-engineer/src/ast/tree-sitter.test.ts +407 -0
  126. package/skills/octocode-code-engineer/src/ast/tree-sitter.ts +402 -0
  127. package/skills/octocode-code-engineer/src/ast/ts-analyzer.test.ts +1864 -0
  128. package/skills/octocode-code-engineer/src/ast/ts-analyzer.ts +509 -0
  129. package/skills/octocode-code-engineer/src/collectors/chains.ts +74 -0
  130. package/skills/octocode-code-engineer/src/collectors/effects.test.ts +490 -0
  131. package/skills/octocode-code-engineer/src/collectors/effects.ts +332 -0
  132. package/skills/octocode-code-engineer/src/collectors/input-sources.test.ts +144 -0
  133. package/skills/octocode-code-engineer/src/collectors/input-sources.ts +196 -0
  134. package/skills/octocode-code-engineer/src/collectors/performance.test.ts +82 -0
  135. package/skills/octocode-code-engineer/src/collectors/performance.ts +141 -0
  136. package/skills/octocode-code-engineer/src/collectors/prototype-pollution.test.ts +55 -0
  137. package/skills/octocode-code-engineer/src/collectors/prototype-pollution.ts +162 -0
  138. package/skills/octocode-code-engineer/src/collectors/security.test.ts +124 -0
  139. package/skills/octocode-code-engineer/src/collectors/security.ts +309 -0
  140. package/skills/octocode-code-engineer/src/collectors/test-profile.test.ts +97 -0
  141. package/skills/octocode-code-engineer/src/collectors/test-profile.ts +269 -0
  142. package/skills/octocode-code-engineer/src/common/is-direct-run.test.ts +32 -0
  143. package/skills/octocode-code-engineer/src/common/is-direct-run.ts +13 -0
  144. package/skills/octocode-code-engineer/src/common/utils.test.ts +463 -0
  145. package/skills/octocode-code-engineer/src/common/utils.ts +304 -0
  146. package/skills/octocode-code-engineer/src/detectors/code-quality.ts +966 -0
  147. package/skills/octocode-code-engineer/src/detectors/cohesion.ts +539 -0
  148. package/skills/octocode-code-engineer/src/detectors/coupling.ts +323 -0
  149. package/skills/octocode-code-engineer/src/detectors/cycle.ts +349 -0
  150. package/skills/octocode-code-engineer/src/detectors/dead-code.ts +320 -0
  151. package/skills/octocode-code-engineer/src/detectors/import-style.ts +376 -0
  152. package/skills/octocode-code-engineer/src/detectors/index.test.ts +3061 -0
  153. package/skills/octocode-code-engineer/src/detectors/index.ts +88 -0
  154. package/skills/octocode-code-engineer/src/detectors/security.test.ts +882 -0
  155. package/skills/octocode-code-engineer/src/detectors/security.ts +821 -0
  156. package/skills/octocode-code-engineer/src/detectors/semantic.ts +758 -0
  157. package/skills/octocode-code-engineer/src/detectors/shared.ts +49 -0
  158. package/skills/octocode-code-engineer/src/detectors/test-quality.test.ts +388 -0
  159. package/skills/octocode-code-engineer/src/detectors/test-quality.ts +367 -0
  160. package/skills/octocode-code-engineer/src/index.test.ts +4425 -0
  161. package/skills/octocode-code-engineer/src/index.ts +403 -0
  162. package/skills/octocode-code-engineer/src/pipeline/cache.test.ts +199 -0
  163. package/skills/octocode-code-engineer/src/pipeline/cache.ts +130 -0
  164. package/skills/octocode-code-engineer/src/pipeline/cli.test.ts +493 -0
  165. package/skills/octocode-code-engineer/src/pipeline/cli.ts +344 -0
  166. package/skills/octocode-code-engineer/src/pipeline/main.test.ts +174 -0
  167. package/skills/octocode-code-engineer/src/pipeline/main.ts +1074 -0
  168. package/skills/octocode-code-engineer/src/pipeline.test.ts +84 -0
  169. package/skills/octocode-code-engineer/src/reporting/analysis.test.ts +782 -0
  170. package/skills/octocode-code-engineer/src/reporting/analysis.ts +688 -0
  171. package/skills/octocode-code-engineer/src/reporting/output-contract.test.ts +463 -0
  172. package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +421 -0
  173. package/skills/octocode-code-engineer/src/reporting/summary-md.ts +714 -0
  174. package/skills/octocode-code-engineer/src/reporting/writer.ts +430 -0
  175. package/skills/octocode-code-engineer/src/sanity.test.ts +47 -0
  176. package/skills/octocode-code-engineer/src/types/constants.ts +248 -0
  177. package/skills/octocode-code-engineer/src/types/index.ts +80 -0
  178. package/skills/octocode-code-engineer/src/types/interfaces.ts +682 -0
  179. package/skills/octocode-code-engineer/tsconfig.json +17 -0
  180. package/skills/octocode-code-engineer/vitest.config.ts +8 -0
  181. package/skills/octocode-documentation-writer/README.md +113 -0
  182. package/skills/octocode-documentation-writer/SKILL.md +886 -0
  183. package/skills/octocode-documentation-writer/references/agent-discovery-analysis.md +453 -0
  184. package/skills/octocode-documentation-writer/references/agent-documentation-writer.md +255 -0
  185. package/skills/octocode-documentation-writer/references/agent-engineer-questions.md +247 -0
  186. package/skills/octocode-documentation-writer/references/agent-orchestrator.md +370 -0
  187. package/skills/octocode-documentation-writer/references/agent-qa-validator.md +227 -0
  188. package/skills/octocode-documentation-writer/references/agent-researcher.md +250 -0
  189. package/skills/octocode-documentation-writer/schemas/analysis-schema.json +886 -0
  190. package/skills/octocode-documentation-writer/schemas/discovery-tasks.json +96 -0
  191. package/skills/octocode-documentation-writer/schemas/documentation-structure.json +373 -0
  192. package/skills/octocode-documentation-writer/schemas/partial-discovery-schema.json +102 -0
  193. package/skills/octocode-documentation-writer/schemas/partial-research-schema.json +98 -0
  194. package/skills/octocode-documentation-writer/schemas/qa-results-schema.json +113 -0
  195. package/skills/octocode-documentation-writer/schemas/questions-schema.json +228 -0
  196. package/skills/octocode-documentation-writer/schemas/research-schema.json +104 -0
  197. package/skills/octocode-documentation-writer/schemas/state-schema.json +222 -0
  198. package/skills/octocode-documentation-writer/schemas/work-assignments-schema.json +74 -0
  199. package/skills/octocode-plan/SKILL.md +122 -116
  200. package/skills/octocode-prompt-optimizer/SKILL.md +617 -0
  201. package/skills/octocode-pull-request-reviewer/README.md +249 -0
  202. package/skills/octocode-pull-request-reviewer/SKILL.md +479 -0
  203. package/skills/octocode-pull-request-reviewer/references/dependency-check.md +74 -0
  204. package/skills/octocode-pull-request-reviewer/references/domain-reviewers.md +24 -0
  205. package/skills/octocode-pull-request-reviewer/references/execution-lifecycle.md +441 -0
  206. package/skills/octocode-pull-request-reviewer/references/flow-analysis-protocol.md +64 -0
  207. package/skills/octocode-pull-request-reviewer/references/output-template.md +174 -0
  208. package/skills/octocode-pull-request-reviewer/references/parallel-agent-protocol.md +182 -0
  209. package/skills/octocode-pull-request-reviewer/references/review-guidelines.md +26 -0
  210. package/skills/octocode-pull-request-reviewer/references/verification-checklist.md +40 -0
  211. package/skills/octocode-research/.claude/settings.local.json +46 -0
  212. package/skills/octocode-research/.octocode/plan/code-review-fixes/plan.md +312 -0
  213. package/skills/octocode-research/.octocode/plan/code-review-fixes/research.md +212 -0
  214. package/skills/octocode-research/.octocode/plans/NODE_SERVER_START_PLAN.md +755 -0
  215. package/skills/octocode-research/.octocode/research/code-review/research.md +371 -0
  216. package/skills/octocode-research/.octocode/review/IMPROVEMENTS.md +391 -0
  217. package/skills/octocode-research/.octocode/review/REVIEW_PLAN.md +289 -0
  218. package/skills/octocode-research/.octocode/review/REVIEW_REPORT.md +356 -0
  219. package/skills/octocode-research/AGENTS.md +349 -0
  220. package/skills/octocode-research/README.md +494 -0
  221. package/skills/octocode-research/SKILL.md +652 -274
  222. package/skills/octocode-research/docs/API_REFERENCE.md +562 -0
  223. package/skills/octocode-research/docs/ARCHITECTURE.md +554 -0
  224. package/skills/octocode-research/docs/FLOWS.md +577 -0
  225. package/skills/octocode-research/docs/OVERVIEW.md +564 -0
  226. package/skills/octocode-research/docs/SERVER_FLOWS.md +631 -0
  227. package/skills/octocode-research/ecosystem.config.cjs +88 -0
  228. package/skills/octocode-research/eslint.config.mjs +27 -0
  229. package/skills/octocode-research/package.json +84 -0
  230. package/skills/octocode-research/references/GUARDRAILS.md +40 -0
  231. package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +178 -0
  232. package/skills/octocode-research/references/roast-prompt.md +149 -0
  233. package/skills/octocode-research/scripts/server-init.d.ts +2 -0
  234. package/skills/octocode-research/scripts/server-init.js +2 -0
  235. package/skills/octocode-research/scripts/server.d.ts +8 -0
  236. package/skills/octocode-research/scripts/server.js +445 -0
  237. package/skills/octocode-research/src/__tests__/integration/circuitBreaker.test.ts +205 -0
  238. package/skills/octocode-research/src/__tests__/integration/routes.test.ts +374 -0
  239. package/skills/octocode-research/src/__tests__/unit/circuitBreaker.test.ts +245 -0
  240. package/skills/octocode-research/src/__tests__/unit/errorHandler.test.ts +183 -0
  241. package/skills/octocode-research/src/__tests__/unit/httpPreprocess.test.ts +157 -0
  242. package/skills/octocode-research/src/__tests__/unit/logger.test.ts +143 -0
  243. package/skills/octocode-research/src/__tests__/unit/queryParser.test.ts +130 -0
  244. package/skills/octocode-research/src/__tests__/unit/responseBuilder.test.ts +469 -0
  245. package/skills/octocode-research/src/__tests__/unit/retry.test.ts +205 -0
  246. package/skills/octocode-research/src/index.ts +186 -0
  247. package/skills/octocode-research/src/mcpCache.ts +49 -0
  248. package/skills/octocode-research/src/middleware/errorHandler.ts +65 -0
  249. package/skills/octocode-research/src/middleware/logger.ts +61 -0
  250. package/skills/octocode-research/src/middleware/queryParser.ts +115 -0
  251. package/skills/octocode-research/src/middleware/readiness.ts +17 -0
  252. package/skills/octocode-research/src/routes/github.ts +197 -0
  253. package/skills/octocode-research/src/routes/local.ts +175 -0
  254. package/skills/octocode-research/src/routes/lsp.ts +177 -0
  255. package/skills/octocode-research/src/routes/package.ts +127 -0
  256. package/skills/octocode-research/src/routes/prompts.ts +138 -0
  257. package/skills/octocode-research/src/routes/tools.ts +677 -0
  258. package/skills/octocode-research/src/server-init.ts +363 -0
  259. package/skills/octocode-research/src/server.ts +285 -0
  260. package/skills/octocode-research/src/types/errorGuards.ts +151 -0
  261. package/skills/octocode-research/src/types/express.d.ts +76 -0
  262. package/skills/octocode-research/src/types/guards.ts +98 -0
  263. package/skills/octocode-research/src/types/mcp.ts +119 -0
  264. package/skills/octocode-research/src/types/responses.ts +199 -0
  265. package/skills/octocode-research/src/types/toolTypes.ts +33 -0
  266. package/skills/octocode-research/src/utils/asyncTimeout.ts +116 -0
  267. package/skills/octocode-research/src/utils/circuitBreaker.ts +492 -0
  268. package/skills/octocode-research/src/utils/colors.ts +53 -0
  269. package/skills/octocode-research/src/utils/errorQueue.ts +71 -0
  270. package/skills/octocode-research/src/utils/logEmoji.ts +103 -0
  271. package/skills/octocode-research/src/utils/logger.ts +413 -0
  272. package/skills/octocode-research/src/utils/resilience.ts +169 -0
  273. package/skills/octocode-research/src/utils/responseBuilder.ts +495 -0
  274. package/skills/octocode-research/src/utils/responseFactory.ts +100 -0
  275. package/skills/octocode-research/src/utils/responseParser.ts +272 -0
  276. package/skills/octocode-research/src/utils/retry.ts +280 -0
  277. package/skills/octocode-research/src/utils/routeFactory.ts +117 -0
  278. package/skills/octocode-research/src/utils/url.ts +20 -0
  279. package/skills/octocode-research/src/validation/httpPreprocess.ts +155 -0
  280. package/skills/octocode-research/src/validation/index.ts +2 -0
  281. package/skills/octocode-research/src/validation/schemas.ts +578 -0
  282. package/skills/octocode-research/src/validation/toolCallSchema.ts +132 -0
  283. package/skills/octocode-research/tsconfig.json +21 -0
  284. package/skills/octocode-research/tsdown.config.ts +42 -0
  285. package/skills/octocode-research/vitest.config.ts +20 -0
  286. package/skills/octocode-researcher/SKILL.md +461 -0
  287. package/skills/octocode-researcher/references/fallbacks.md +120 -0
  288. package/skills/{octocode-local-search → octocode-researcher}/references/tool-reference.md +132 -49
  289. package/skills/{octocode-local-search → octocode-researcher}/references/workflow-patterns.md +204 -4
  290. package/skills/octocode-rfc-generator/SKILL.md +223 -0
  291. package/skills/octocode-rfc-generator/references/rfc-template.md +193 -0
  292. package/skills/octocode-roast/SKILL.md +63 -21
  293. package/skills/octocode-implement/SKILL.md +0 -293
  294. package/skills/octocode-implement/references/execution-phases.md +0 -317
  295. package/skills/octocode-implement/references/tool-reference.md +0 -403
  296. package/skills/octocode-implement/references/workflow-patterns.md +0 -385
  297. package/skills/octocode-local-search/SKILL.md +0 -449
  298. package/skills/octocode-pr-review/SKILL.md +0 -391
  299. package/skills/octocode-pr-review/references/domain-reviewers.md +0 -105
  300. package/skills/octocode-pr-review/references/execution-lifecycle.md +0 -116
  301. package/skills/octocode-pr-review/references/research-flows.md +0 -75
  302. package/skills/octocode-research/references/tool-reference.md +0 -304
  303. package/skills/octocode-research/references/workflow-patterns.md +0 -325
@@ -0,0 +1,3061 @@
1
+ import * as ts from 'typescript';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ buildConsumedFromModule,
6
+ computeAbstractness,
7
+ computeBarrelDepth,
8
+ computeCognitiveComplexity,
9
+ computeHotFiles,
10
+ computeInstability,
11
+ detectBarrelExplosion,
12
+ detectBoundaryViolations,
13
+ detectCognitiveComplexity,
14
+ detectCommonJsInEsm,
15
+ detectCriticalPaths,
16
+ detectDeadExports,
17
+ detectDeadFiles,
18
+ detectDeadReExports,
19
+ detectDependencyCycles,
20
+ detectDistanceFromMainSequence,
21
+ detectDuplicateFlowStructures,
22
+ detectDuplicateFunctionBodies,
23
+ detectEmptyCatchBlocks,
24
+ detectExcessiveParameters,
25
+ detectExportStarLeak,
26
+ detectFeatureEnvy,
27
+ detectFunctionOptimization,
28
+ detectGodFunctions,
29
+ detectGodModuleCoupling,
30
+ detectGodModules,
31
+ detectHighCoupling,
32
+ detectHighHalsteadEffort,
33
+ detectLayerViolations,
34
+ detectLowCohesion,
35
+ detectLowMaintainability,
36
+ detectMegaFolders,
37
+ detectNamespaceImport,
38
+ detectOrphanModules,
39
+ detectSdpViolations,
40
+ detectSwitchNoDefault,
41
+ detectTestOnlyModules,
42
+ detectUnreachableModules,
43
+ detectUnsafeAny,
44
+ detectUntestedCriticalCode,
45
+ detectUnusedNpmDeps,
46
+ isLikelyEntrypoint,
47
+ mergeOverlappingChains,
48
+ } from './index.js';
49
+
50
+ import type {
51
+ DependencyProfile,
52
+ DependencyState,
53
+ DuplicateGroup,
54
+ FileEntry,
55
+ FunctionEntry,
56
+ RedundantFlowGroup,
57
+ } from '../types/index.js';
58
+ import type {
59
+ CodeLocation,
60
+ DependencySummary,
61
+ FileCriticality,
62
+ } from '../types/index.js';
63
+
64
+ function emptyState(): DependencyState {
65
+ return {
66
+ files: new Set(),
67
+ outgoing: new Map(),
68
+ incoming: new Map(),
69
+ incomingFromTests: new Map(),
70
+ incomingFromProduction: new Map(),
71
+ externalCounts: new Map(),
72
+ unresolvedCounts: new Map(),
73
+ declaredExportsByFile: new Map(),
74
+ importedSymbolsByFile: new Map(),
75
+ reExportsByFile: new Map(),
76
+ };
77
+ }
78
+
79
+ function addEdge(state: DependencyState, from: string, to: string): void {
80
+ state.files.add(from);
81
+ state.files.add(to);
82
+ if (!state.outgoing.has(from)) state.outgoing.set(from, new Set());
83
+ state.outgoing.get(from)!.add(to);
84
+ if (!state.incoming.has(to)) state.incoming.set(to, new Set());
85
+ state.incoming.get(to)!.add(from);
86
+ }
87
+
88
+ const emptyProfile: DependencyProfile = {
89
+ internalDependencies: [],
90
+ externalDependencies: [],
91
+ unresolvedDependencies: [],
92
+ declaredExports: [],
93
+ importedSymbols: [],
94
+ reExports: [],
95
+ };
96
+
97
+ function makeFn(overrides: Partial<FunctionEntry> = {}): FunctionEntry {
98
+ return {
99
+ kind: 'FunctionDeclaration',
100
+ name: 'testFn',
101
+ nameHint: 'testFn',
102
+ file: 'src/file.ts',
103
+ lineStart: 1,
104
+ lineEnd: 10,
105
+ columnStart: 1,
106
+ columnEnd: 1,
107
+ statementCount: 5,
108
+ complexity: 1,
109
+ maxBranchDepth: 0,
110
+ maxLoopDepth: 0,
111
+ returns: 1,
112
+ awaits: 0,
113
+ calls: 0,
114
+ loops: 0,
115
+ lengthLines: 10,
116
+ cognitiveComplexity: 0,
117
+ ...overrides,
118
+ };
119
+ }
120
+
121
+ function makeFileEntry(overrides: Partial<FileEntry> = {}): FileEntry {
122
+ return {
123
+ package: 'test',
124
+ file: 'src/file.ts',
125
+ parseEngine: 'typescript',
126
+ nodeCount: 100,
127
+ kindCounts: {},
128
+ functions: [],
129
+ flows: [],
130
+ dependencyProfile: emptyProfile,
131
+ ...overrides,
132
+ };
133
+ }
134
+
135
+ function minimalDepSummary(
136
+ overrides: Partial<DependencySummary> = {}
137
+ ): DependencySummary {
138
+ return {
139
+ totalModules: 0,
140
+ totalEdges: 0,
141
+ unresolvedEdgeCount: 0,
142
+ externalDependencyFiles: 0,
143
+ rootsCount: 0,
144
+ leavesCount: 0,
145
+ roots: [],
146
+ leaves: [],
147
+ criticalModules: [],
148
+ testOnlyModules: [],
149
+ unresolvedSample: [],
150
+ outgoingTop: [],
151
+ inboundTop: [],
152
+ cycles: [],
153
+ criticalPaths: [],
154
+ ...overrides,
155
+ };
156
+ }
157
+
158
+ describe('isLikelyEntrypoint', () => {
159
+ it('matches config files', () => {
160
+ expect(isLikelyEntrypoint('vite.config.ts')).toBe(true);
161
+ expect(isLikelyEntrypoint('webpack.config.js')).toBe(true);
162
+ });
163
+
164
+ it('matches public entrypoint pattern', () => {
165
+ expect(isLikelyEntrypoint('src/public.ts')).toBe(true);
166
+ });
167
+ });
168
+
169
+ describe('computeInstability', () => {
170
+ it('returns 0 when both counts are 0', () => {
171
+ expect(computeInstability(0, 0)).toBe(0);
172
+ });
173
+
174
+ it('returns 0 for maximally stable (only depended on)', () => {
175
+ expect(computeInstability(10, 0)).toBe(0);
176
+ });
177
+
178
+ it('returns 1 for maximally unstable (only depends)', () => {
179
+ expect(computeInstability(0, 10)).toBe(1);
180
+ });
181
+
182
+ it('returns 0.5 for equal inbound/outbound', () => {
183
+ expect(computeInstability(5, 5)).toBe(0.5);
184
+ });
185
+
186
+ it('computes fractional instability', () => {
187
+ expect(computeInstability(3, 7)).toBeCloseTo(0.7);
188
+ });
189
+ });
190
+
191
+ describe('detectSdpViolations', () => {
192
+ it('returns empty for no files', () => {
193
+ expect(detectSdpViolations(emptyState())).toEqual([]);
194
+ });
195
+
196
+ it('detects when stable module depends on unstable module', () => {
197
+ const state = emptyState();
198
+ state.files.add('src/a.ts');
199
+ state.files.add('src/b.ts');
200
+ state.files.add('src/c.ts');
201
+ state.files.add('src/d.ts');
202
+ state.files.add('src/e.ts');
203
+ addEdge(state, 'src/c.ts', 'src/a.ts');
204
+ addEdge(state, 'src/d.ts', 'src/a.ts');
205
+ addEdge(state, 'src/e.ts', 'src/a.ts');
206
+ addEdge(state, 'src/a.ts', 'src/b.ts');
207
+ addEdge(state, 'src/b.ts', 'src/c.ts');
208
+ addEdge(state, 'src/b.ts', 'src/d.ts');
209
+ addEdge(state, 'src/b.ts', 'src/e.ts');
210
+
211
+ const findings = detectSdpViolations(state);
212
+ expect(findings.length).toBeGreaterThan(0);
213
+ expect(findings[0].category).toBe('architecture-sdp-violation');
214
+ expect(findings[0].file).toBe('src/a.ts');
215
+ });
216
+
217
+ it('does not flag when stable depends on stable', () => {
218
+ const state = emptyState();
219
+ state.files.add('src/a.ts');
220
+ state.files.add('src/b.ts');
221
+ state.files.add('src/c.ts');
222
+ addEdge(state, 'src/a.ts', 'src/b.ts');
223
+ addEdge(state, 'src/c.ts', 'src/a.ts');
224
+ addEdge(state, 'src/b.ts', 'src/c.ts');
225
+ const findings = detectSdpViolations(state);
226
+ expect(findings).toEqual([]);
227
+ });
228
+
229
+ it('skips test files', () => {
230
+ const state = emptyState();
231
+ state.files.add('src/a.test.ts');
232
+ state.files.add('src/b.ts');
233
+ addEdge(state, 'src/a.test.ts', 'src/b.ts');
234
+ expect(detectSdpViolations(state)).toEqual([]);
235
+ });
236
+
237
+ it('assigns high severity for large delta', () => {
238
+ const state = emptyState();
239
+ state.files.add('src/stable.ts');
240
+ state.files.add('src/unstable.ts');
241
+ for (let i = 0; i < 10; i++) {
242
+ const f = `src/dep${i}.ts`;
243
+ state.files.add(f);
244
+ addEdge(state, f, 'src/stable.ts');
245
+ }
246
+ addEdge(state, 'src/stable.ts', 'src/unstable.ts');
247
+ for (let i = 0; i < 10; i++) {
248
+ const f = `src/lib${i}.ts`;
249
+ state.files.add(f);
250
+ addEdge(state, 'src/unstable.ts', f);
251
+ }
252
+ const findings = detectSdpViolations(state);
253
+ const sdp = findings.find(f => f.file === 'src/stable.ts');
254
+ expect(sdp).toBeDefined();
255
+ expect(sdp!.severity).toBe('high');
256
+ });
257
+ });
258
+
259
+ describe('detectHighCoupling', () => {
260
+ it('returns empty for uncoupled modules', () => {
261
+ const state = emptyState();
262
+ state.files.add('src/a.ts');
263
+ expect(detectHighCoupling(state)).toEqual([]);
264
+ });
265
+
266
+ it('detects modules above threshold', () => {
267
+ const state = emptyState();
268
+ const hub = 'src/hub.ts';
269
+ state.files.add(hub);
270
+ for (let i = 0; i < 10; i++) {
271
+ const f = `src/dep${i}.ts`;
272
+ state.files.add(f);
273
+ addEdge(state, hub, f);
274
+ }
275
+ for (let i = 0; i < 8; i++) {
276
+ const f = `src/consumer${i}.ts`;
277
+ state.files.add(f);
278
+ addEdge(state, f, hub);
279
+ }
280
+ const findings = detectHighCoupling(state, 15);
281
+ const hubFinding = findings.find(f => f.file === hub);
282
+ expect(hubFinding).toBeDefined();
283
+ expect(hubFinding!.category).toBe('high-coupling');
284
+ });
285
+
286
+ it('assigns high severity above 25', () => {
287
+ const state = emptyState();
288
+ const hub = 'src/hub.ts';
289
+ state.files.add(hub);
290
+ for (let i = 0; i < 26; i++) {
291
+ const f = `src/m${i}.ts`;
292
+ state.files.add(f);
293
+ addEdge(state, f, hub);
294
+ }
295
+ const findings = detectHighCoupling(state, 15);
296
+ expect(findings[0].severity).toBe('high');
297
+ });
298
+
299
+ it('respects custom threshold', () => {
300
+ const state = emptyState();
301
+ const hub = 'src/hub.ts';
302
+ state.files.add(hub);
303
+ for (let i = 0; i < 5; i++) {
304
+ const f = `src/m${i}.ts`;
305
+ state.files.add(f);
306
+ addEdge(state, f, hub);
307
+ }
308
+ expect(detectHighCoupling(state, 3).length).toBeGreaterThan(0);
309
+ expect(detectHighCoupling(state, 10)).toEqual([]);
310
+ });
311
+ });
312
+
313
+ describe('detectGodModuleCoupling', () => {
314
+ it('returns empty for low fan-in/fan-out', () => {
315
+ const state = emptyState();
316
+ state.files.add('src/a.ts');
317
+ state.files.add('src/b.ts');
318
+ addEdge(state, 'src/a.ts', 'src/b.ts');
319
+ expect(detectGodModuleCoupling(state)).toEqual([]);
320
+ });
321
+
322
+ it('detects high fan-in', () => {
323
+ const state = emptyState();
324
+ const hub = 'src/utils.ts';
325
+ state.files.add(hub);
326
+ for (let i = 0; i < 22; i++) {
327
+ const f = `src/consumer${i}.ts`;
328
+ state.files.add(f);
329
+ addEdge(state, f, hub);
330
+ }
331
+ const findings = detectGodModuleCoupling(state, 20, 15);
332
+ expect(findings.some(f => f.title.includes('fan-in'))).toBe(true);
333
+ });
334
+
335
+ it('detects high fan-out', () => {
336
+ const state = emptyState();
337
+ const hub = 'src/orchestrator.ts';
338
+ state.files.add(hub);
339
+ for (let i = 0; i < 18; i++) {
340
+ const f = `src/service${i}.ts`;
341
+ state.files.add(f);
342
+ addEdge(state, hub, f);
343
+ }
344
+ const findings = detectGodModuleCoupling(state, 20, 15);
345
+ expect(findings.some(f => f.title.includes('fan-out'))).toBe(true);
346
+ });
347
+
348
+ it('can detect both fan-in and fan-out for same module', () => {
349
+ const state = emptyState();
350
+ const hub = 'src/mega.ts';
351
+ state.files.add(hub);
352
+ for (let i = 0; i < 25; i++) {
353
+ const f = `src/in${i}.ts`;
354
+ state.files.add(f);
355
+ addEdge(state, f, hub);
356
+ }
357
+ for (let i = 0; i < 20; i++) {
358
+ const f = `src/out${i}.ts`;
359
+ state.files.add(f);
360
+ addEdge(state, hub, f);
361
+ }
362
+ const findings = detectGodModuleCoupling(state, 20, 15);
363
+ const hubFindings = findings.filter(f => f.file === hub);
364
+ expect(hubFindings.length).toBe(2);
365
+ });
366
+ });
367
+
368
+ describe('detectOrphanModules', () => {
369
+ it('returns empty when all modules are connected', () => {
370
+ const state = emptyState();
371
+ addEdge(state, 'src/a.ts', 'src/b.ts');
372
+ expect(detectOrphanModules(state)).toEqual([]);
373
+ });
374
+
375
+ it('detects disconnected module', () => {
376
+ const state = emptyState();
377
+ state.files.add('src/orphan.ts');
378
+ addEdge(state, 'src/a.ts', 'src/b.ts');
379
+ const findings = detectOrphanModules(state);
380
+ expect(findings.length).toBe(1);
381
+ expect(findings[0].category).toBe('orphan-module');
382
+ expect(findings[0].file).toBe('src/orphan.ts');
383
+ });
384
+
385
+ it('skips entrypoints', () => {
386
+ const state = emptyState();
387
+ state.files.add('src/index.ts');
388
+ expect(detectOrphanModules(state)).toEqual([]);
389
+ });
390
+
391
+ it('skips test files', () => {
392
+ const state = emptyState();
393
+ state.files.add('src/foo.test.ts');
394
+ expect(detectOrphanModules(state)).toEqual([]);
395
+ });
396
+ });
397
+
398
+ describe('detectUnreachableModules', () => {
399
+ it('returns empty when all reachable from entrypoint', () => {
400
+ const state = emptyState();
401
+ addEdge(state, 'src/index.ts', 'src/a.ts');
402
+ addEdge(state, 'src/a.ts', 'src/b.ts');
403
+ expect(detectUnreachableModules(state)).toEqual([]);
404
+ });
405
+
406
+ it('detects module unreachable from entrypoints', () => {
407
+ const state = emptyState();
408
+ addEdge(state, 'src/index.ts', 'src/a.ts');
409
+ addEdge(state, 'src/island.ts', 'src/leaf.ts');
410
+ const findings = detectUnreachableModules(state);
411
+ const unreachable = findings.map(f => f.file).sort();
412
+ expect(unreachable).toContain('src/island.ts');
413
+ expect(unreachable).toContain('src/leaf.ts');
414
+ });
415
+
416
+ it('flags subgraphs not reachable from named entrypoints', () => {
417
+ const state = emptyState();
418
+ addEdge(state, 'src/main.ts', 'src/a.ts');
419
+ addEdge(state, 'src/orphan.ts', 'src/b.ts');
420
+ const findings = detectUnreachableModules(state);
421
+ expect(findings.map(f => f.file).sort()).toEqual([
422
+ 'src/b.ts',
423
+ 'src/orphan.ts',
424
+ ]);
425
+ });
426
+
427
+ it('uses roots as entrypoints when no index/main files exist', () => {
428
+ const state = emptyState();
429
+ addEdge(state, 'src/alpha.ts', 'src/a.ts');
430
+ addEdge(state, 'src/beta.ts', 'src/b.ts');
431
+ const findings = detectUnreachableModules(state);
432
+ expect(findings).toEqual([]);
433
+ });
434
+
435
+ it('handles cycles gracefully', () => {
436
+ const state = emptyState();
437
+ addEdge(state, 'src/index.ts', 'src/a.ts');
438
+ addEdge(state, 'src/a.ts', 'src/b.ts');
439
+ addEdge(state, 'src/b.ts', 'src/a.ts');
440
+ expect(detectUnreachableModules(state)).toEqual([]);
441
+ });
442
+ });
443
+
444
+ describe('detectUnusedNpmDeps', () => {
445
+ it('returns empty when all deps are used', () => {
446
+ const ext = new Map([['src/a.ts', new Set(['lodash', '@types/node'])]]);
447
+ expect(detectUnusedNpmDeps(ext, { lodash: '^4.0.0' })).toEqual([]);
448
+ });
449
+
450
+ it('detects unused production dependency', () => {
451
+ const ext = new Map([['src/a.ts', new Set(['lodash'])]]);
452
+ const findings = detectUnusedNpmDeps(ext, { lodash: '^4', express: '^4' });
453
+ expect(findings.length).toBe(1);
454
+ expect(findings[0].title).toContain('express');
455
+ expect(findings[0].severity).toBe('medium');
456
+ });
457
+
458
+ it('detects unused devDependency with low severity', () => {
459
+ const ext = new Map<string, Set<string>>();
460
+ const findings = detectUnusedNpmDeps(ext, {}, { vitest: '^1.0' });
461
+ expect(findings.length).toBe(1);
462
+ expect(findings[0].severity).toBe('low');
463
+ });
464
+
465
+ it('handles scoped packages correctly', () => {
466
+ const ext = new Map([['src/a.ts', new Set(['@scope/pkg/utils'])]]);
467
+ expect(detectUnusedNpmDeps(ext, { '@scope/pkg': '^1.0' })).toEqual([]);
468
+ });
469
+
470
+ it('reports both unused prod and dev deps', () => {
471
+ const ext = new Map<string, Set<string>>();
472
+ const findings = detectUnusedNpmDeps(
473
+ ext,
474
+ { react: '^18' },
475
+ { jest: '^29' }
476
+ );
477
+ expect(findings.length).toBe(2);
478
+ });
479
+ });
480
+
481
+ describe('detectBoundaryViolations', () => {
482
+ it('returns empty for same-package imports', () => {
483
+ const state = emptyState();
484
+ addEdge(state, 'packages/foo/src/a.ts', 'packages/foo/src/b.ts');
485
+ expect(detectBoundaryViolations(state)).toEqual([]);
486
+ });
487
+
488
+ it('allows cross-package import via index', () => {
489
+ const state = emptyState();
490
+ addEdge(state, 'packages/foo/src/a.ts', 'packages/bar/src/index.ts');
491
+ expect(detectBoundaryViolations(state)).toEqual([]);
492
+ });
493
+
494
+ it('detects cross-package import bypassing index', () => {
495
+ const state = emptyState();
496
+ addEdge(state, 'packages/foo/src/a.ts', 'packages/bar/src/utils/helper.ts');
497
+ const findings = detectBoundaryViolations(state);
498
+ expect(findings.length).toBe(1);
499
+ expect(findings[0].category).toBe('package-boundary-violation');
500
+ expect(findings[0].severity).toBe('medium');
501
+ });
502
+
503
+ it('assigns high severity for internal/ path imports', () => {
504
+ const state = emptyState();
505
+ addEdge(
506
+ state,
507
+ 'packages/foo/src/a.ts',
508
+ 'packages/bar/src/internal/secret.ts'
509
+ );
510
+ const findings = detectBoundaryViolations(state);
511
+ expect(findings[0].severity).toBe('high');
512
+ });
513
+
514
+ it('skips non-package files', () => {
515
+ const state = emptyState();
516
+ addEdge(state, 'src/a.ts', 'src/b.ts');
517
+ expect(detectBoundaryViolations(state)).toEqual([]);
518
+ });
519
+ });
520
+
521
+ describe('computeBarrelDepth', () => {
522
+ it('returns 0 for file with no re-exports', () => {
523
+ const state = emptyState();
524
+ state.reExportsByFile.set('src/a.ts', []);
525
+ expect(computeBarrelDepth('src/a.ts', state)).toBe(0);
526
+ });
527
+
528
+ it('returns 1 for single-level barrel', () => {
529
+ const state = emptyState();
530
+ state.reExportsByFile.set('src/index.ts', [
531
+ {
532
+ sourceModule: './a',
533
+ resolvedModule: 'src/a.ts',
534
+ exportedAs: 'A',
535
+ importedName: 'A',
536
+ isStar: false,
537
+ isTypeOnly: false,
538
+ },
539
+ ]);
540
+ state.reExportsByFile.set('src/a.ts', []);
541
+ expect(computeBarrelDepth('src/index.ts', state)).toBe(1);
542
+ });
543
+
544
+ it('returns 2 for two-level barrel chain', () => {
545
+ const state = emptyState();
546
+ state.reExportsByFile.set('src/index.ts', [
547
+ {
548
+ sourceModule: './sub',
549
+ resolvedModule: 'src/sub/index.ts',
550
+ exportedAs: '*',
551
+ importedName: '*',
552
+ isStar: true,
553
+ isTypeOnly: false,
554
+ },
555
+ ]);
556
+ state.reExportsByFile.set('src/sub/index.ts', [
557
+ {
558
+ sourceModule: './a',
559
+ resolvedModule: 'src/sub/a.ts',
560
+ exportedAs: 'A',
561
+ importedName: 'A',
562
+ isStar: false,
563
+ isTypeOnly: false,
564
+ },
565
+ ]);
566
+ state.reExportsByFile.set('src/sub/a.ts', []);
567
+ expect(computeBarrelDepth('src/index.ts', state)).toBe(2);
568
+ });
569
+
570
+ it('handles cycles without infinite loop', () => {
571
+ const state = emptyState();
572
+ state.reExportsByFile.set('src/a.ts', [
573
+ {
574
+ sourceModule: './b',
575
+ resolvedModule: 'src/b.ts',
576
+ exportedAs: '*',
577
+ importedName: '*',
578
+ isStar: true,
579
+ isTypeOnly: false,
580
+ },
581
+ ]);
582
+ state.reExportsByFile.set('src/b.ts', [
583
+ {
584
+ sourceModule: './a',
585
+ resolvedModule: 'src/a.ts',
586
+ exportedAs: '*',
587
+ importedName: '*',
588
+ isStar: true,
589
+ isTypeOnly: false,
590
+ },
591
+ ]);
592
+ expect(computeBarrelDepth('src/a.ts', state)).toBe(2);
593
+ });
594
+ });
595
+
596
+ describe('detectBarrelExplosion', () => {
597
+ it('returns empty for small barrel', () => {
598
+ const state = emptyState();
599
+ state.reExportsByFile.set('src/index.ts', [
600
+ {
601
+ sourceModule: './a',
602
+ resolvedModule: 'src/a.ts',
603
+ exportedAs: 'A',
604
+ importedName: 'A',
605
+ isStar: false,
606
+ isTypeOnly: false,
607
+ },
608
+ ]);
609
+ expect(detectBarrelExplosion(state, 30)).toEqual([]);
610
+ });
611
+
612
+ it('detects barrel exceeding symbol threshold', () => {
613
+ const state = emptyState();
614
+ const reexports = Array.from({ length: 35 }, (_, i) => ({
615
+ sourceModule: `./m${i}`,
616
+ resolvedModule: `src/m${i}.ts`,
617
+ exportedAs: `S${i}`,
618
+ importedName: `S${i}`,
619
+ isStar: false,
620
+ isTypeOnly: false,
621
+ }));
622
+ state.reExportsByFile.set('src/index.ts', reexports);
623
+ const findings = detectBarrelExplosion(state, 30);
624
+ expect(
625
+ findings.some(
626
+ f =>
627
+ f.category === 'barrel-explosion' &&
628
+ f.title.includes('Barrel explosion')
629
+ )
630
+ ).toBe(true);
631
+ });
632
+ });
633
+
634
+ describe('detectGodModules', () => {
635
+ it('returns empty for small modules', () => {
636
+ const state = emptyState();
637
+ const files: FileEntry[] = [
638
+ makeFileEntry({ functions: [makeFn({ statementCount: 10 })] }),
639
+ ];
640
+ expect(detectGodModules(files, state)).toEqual([]);
641
+ });
642
+
643
+ it('detects module with many statements', () => {
644
+ const state = emptyState();
645
+ const fns = Array.from({ length: 10 }, (_, i) =>
646
+ makeFn({ name: `fn${i}`, statementCount: 60 })
647
+ );
648
+ const files: FileEntry[] = [makeFileEntry({ functions: fns })];
649
+ const findings = detectGodModules(files, state, 500);
650
+ expect(findings.length).toBe(1);
651
+ expect(findings[0].category).toBe('god-module');
652
+ });
653
+
654
+ it('detects module with many exports', () => {
655
+ const state = emptyState();
656
+ const exports = Array.from({ length: 25 }, (_, i) => ({
657
+ name: `exp${i}`,
658
+ kind: 'value' as const,
659
+ }));
660
+ state.declaredExportsByFile.set('src/file.ts', exports);
661
+ const files: FileEntry[] = [makeFileEntry()];
662
+ const findings = detectGodModules(files, state, 9999, 20);
663
+ expect(findings.length).toBe(1);
664
+ });
665
+ });
666
+
667
+ describe('detectMegaFolders', () => {
668
+ it('returns empty when no folder crosses the concentration threshold', () => {
669
+ const files = [
670
+ makeFileEntry({ file: 'src/a.ts' }),
671
+ makeFileEntry({ file: 'src/b.ts' }),
672
+ makeFileEntry({ file: 'src/feature/c.ts' }),
673
+ makeFileEntry({ file: 'src/feature/d.ts' }),
674
+ ];
675
+ expect(detectMegaFolders(files, 3, 0.6)).toEqual([]);
676
+ });
677
+
678
+ it('flags large concentrated folder and includes decomposition evidence', () => {
679
+ const files = [
680
+ ...Array.from({ length: 6 }, (_, i) =>
681
+ makeFileEntry({ file: `src/core/file-${i}.ts` })
682
+ ),
683
+ makeFileEntry({ file: 'src/feature/a.ts' }),
684
+ makeFileEntry({ file: 'src/feature/b.ts' }),
685
+ ];
686
+ const findings = detectMegaFolders(files, 5, 0.5);
687
+ expect(findings).toHaveLength(1);
688
+ expect(findings[0].category).toBe('mega-folder');
689
+ expect(findings[0].title).toContain('src/core');
690
+ expect((findings[0].evidence as Record<string, unknown>).fileCount).toBe(6);
691
+ });
692
+ });
693
+
694
+ describe('detectGodFunctions', () => {
695
+ it('returns empty for small functions', () => {
696
+ const files: FileEntry[] = [
697
+ makeFileEntry({ functions: [makeFn({ statementCount: 50 })] }),
698
+ ];
699
+ expect(detectGodFunctions(files, 100)).toEqual([]);
700
+ });
701
+
702
+ it('detects function exceeding statement threshold', () => {
703
+ const files: FileEntry[] = [
704
+ makeFileEntry({
705
+ functions: [makeFn({ statementCount: 120, name: 'bigFn' })],
706
+ }),
707
+ ];
708
+ const findings = detectGodFunctions(files, 100);
709
+ expect(findings.length).toBe(1);
710
+ expect(findings[0].category).toBe('god-function');
711
+ expect(findings[0].title).toContain('bigFn');
712
+ });
713
+ });
714
+
715
+ describe('computeCognitiveComplexity', () => {
716
+ function parseExpr(code: string): ts.Node {
717
+ const src = ts.createSourceFile(
718
+ 'test.ts',
719
+ code,
720
+ ts.ScriptTarget.ESNext,
721
+ true
722
+ );
723
+ return src.statements[0];
724
+ }
725
+
726
+ it('returns 0 for simple function', () => {
727
+ const node = parseExpr('function f() { return 1; }');
728
+ expect(computeCognitiveComplexity(node)).toBe(0);
729
+ });
730
+
731
+ it('counts simple if as 1', () => {
732
+ const node = parseExpr('function f(x: boolean) { if (x) { return 1; } }');
733
+ expect(computeCognitiveComplexity(node)).toBe(1);
734
+ });
735
+
736
+ it('penalizes nesting', () => {
737
+ const node = parseExpr(
738
+ 'function f(a: boolean, b: boolean) { if (a) { if (b) { return 1; } } }'
739
+ );
740
+ expect(computeCognitiveComplexity(node)).toBe(3);
741
+ });
742
+
743
+ it('counts for loop', () => {
744
+ const node = parseExpr(
745
+ 'function f(arr: number[]) { for (const x of arr) { console.log(x); } }'
746
+ );
747
+ expect(computeCognitiveComplexity(node)).toBeGreaterThan(0);
748
+ });
749
+
750
+ it('counts logical operators', () => {
751
+ const node = parseExpr(
752
+ 'function f(a: boolean, b: boolean) { return a && b; }'
753
+ );
754
+ expect(computeCognitiveComplexity(node)).toBe(1);
755
+ });
756
+
757
+ it('deeply nested structures have high complexity', () => {
758
+ const code = `function f(a: boolean, b: boolean, c: boolean) {
759
+ if (a) {
760
+ for (let i = 0; i < 10; i++) {
761
+ if (b) {
762
+ while (c) {
763
+ break;
764
+ }
765
+ }
766
+ }
767
+ }
768
+ }`;
769
+ const node = parseExpr(code);
770
+ expect(computeCognitiveComplexity(node)).toBe(10);
771
+ });
772
+ });
773
+
774
+ describe('detectCognitiveComplexity', () => {
775
+ it('returns empty for low-complexity functions', () => {
776
+ const files: FileEntry[] = [
777
+ makeFileEntry({ functions: [makeFn({ cognitiveComplexity: 5 })] }),
778
+ ];
779
+ expect(detectCognitiveComplexity(files, 15)).toEqual([]);
780
+ });
781
+
782
+ it('detects high cognitive complexity', () => {
783
+ const files: FileEntry[] = [
784
+ makeFileEntry({
785
+ functions: [makeFn({ cognitiveComplexity: 20, name: 'complexFn' })],
786
+ }),
787
+ ];
788
+ const findings = detectCognitiveComplexity(files, 15);
789
+ expect(findings.length).toBe(1);
790
+ expect(findings[0].category).toBe('cognitive-complexity');
791
+ });
792
+
793
+ it('assigns high severity above 25', () => {
794
+ const files: FileEntry[] = [
795
+ makeFileEntry({ functions: [makeFn({ cognitiveComplexity: 30 })] }),
796
+ ];
797
+ const findings = detectCognitiveComplexity(files, 15);
798
+ expect(findings[0].severity).toBe('high');
799
+ });
800
+ });
801
+
802
+ describe('detectLayerViolations', () => {
803
+ it('returns empty with no layer config', () => {
804
+ expect(detectLayerViolations(emptyState(), [])).toEqual([]);
805
+ });
806
+
807
+ it('returns empty when imports respect layer order', () => {
808
+ const state = emptyState();
809
+ addEdge(state, 'src/ui/page.ts', 'src/service/api.ts');
810
+ addEdge(state, 'src/service/api.ts', 'src/repository/db.ts');
811
+ const findings = detectLayerViolations(state, [
812
+ 'ui',
813
+ 'service',
814
+ 'repository',
815
+ ]);
816
+ expect(findings).toEqual([]);
817
+ });
818
+
819
+ it('detects backward layer import', () => {
820
+ const state = emptyState();
821
+ addEdge(state, 'src/repository/db.ts', 'src/ui/page.ts');
822
+ const findings = detectLayerViolations(state, [
823
+ 'ui',
824
+ 'service',
825
+ 'repository',
826
+ ]);
827
+ expect(findings.length).toBe(1);
828
+ expect(findings[0].category).toBe('layer-violation');
829
+ expect(findings[0].severity).toBe('high');
830
+ });
831
+
832
+ it('ignores files not in any layer', () => {
833
+ const state = emptyState();
834
+ addEdge(state, 'src/utils/helper.ts', 'src/ui/page.ts');
835
+ const findings = detectLayerViolations(state, [
836
+ 'ui',
837
+ 'service',
838
+ 'repository',
839
+ ]);
840
+ expect(findings).toEqual([]);
841
+ });
842
+
843
+ it('detects multiple violations', () => {
844
+ const state = emptyState();
845
+ addEdge(state, 'src/repository/db.ts', 'src/ui/page.ts');
846
+ addEdge(state, 'src/service/api.ts', 'src/ui/button.ts');
847
+ const findings = detectLayerViolations(state, [
848
+ 'ui',
849
+ 'service',
850
+ 'repository',
851
+ ]);
852
+ expect(findings.length).toBe(2);
853
+ });
854
+ });
855
+
856
+ describe('detectLowCohesion', () => {
857
+ it('returns empty when file has few exports', () => {
858
+ const state = emptyState();
859
+ state.files.add('src/small.ts');
860
+ state.declaredExportsByFile.set('src/small.ts', [
861
+ { name: 'a', kind: 'value' },
862
+ { name: 'b', kind: 'value' },
863
+ ]);
864
+ expect(detectLowCohesion(state)).toEqual([]);
865
+ });
866
+
867
+ it('returns empty for entrypoint files', () => {
868
+ const state = emptyState();
869
+ state.files.add('src/index.ts');
870
+ state.declaredExportsByFile.set('src/index.ts', [
871
+ { name: 'a', kind: 'value' },
872
+ { name: 'b', kind: 'value' },
873
+ { name: 'c', kind: 'value' },
874
+ { name: 'd', kind: 'value' },
875
+ ]);
876
+ expect(detectLowCohesion(state)).toEqual([]);
877
+ });
878
+
879
+ it('returns empty when all consumers import the same symbols', () => {
880
+ const state = emptyState();
881
+ state.files.add('src/lib.ts');
882
+ state.files.add('src/a.ts');
883
+ state.files.add('src/b.ts');
884
+ state.declaredExportsByFile.set('src/lib.ts', [
885
+ { name: 'x', kind: 'value' },
886
+ { name: 'y', kind: 'value' },
887
+ { name: 'z', kind: 'value' },
888
+ { name: 'w', kind: 'value' },
889
+ ]);
890
+ state.importedSymbolsByFile.set('src/a.ts', [
891
+ {
892
+ sourceModule: './lib',
893
+ resolvedModule: 'src/lib.ts',
894
+ importedName: 'x',
895
+ localName: 'x',
896
+ isTypeOnly: false,
897
+ },
898
+ {
899
+ sourceModule: './lib',
900
+ resolvedModule: 'src/lib.ts',
901
+ importedName: 'y',
902
+ localName: 'y',
903
+ isTypeOnly: false,
904
+ },
905
+ ]);
906
+ state.importedSymbolsByFile.set('src/b.ts', [
907
+ {
908
+ sourceModule: './lib',
909
+ resolvedModule: 'src/lib.ts',
910
+ importedName: 'x',
911
+ localName: 'x',
912
+ isTypeOnly: false,
913
+ },
914
+ {
915
+ sourceModule: './lib',
916
+ resolvedModule: 'src/lib.ts',
917
+ importedName: 'y',
918
+ localName: 'y',
919
+ isTypeOnly: false,
920
+ },
921
+ ]);
922
+ expect(detectLowCohesion(state)).toEqual([]);
923
+ });
924
+
925
+ it('detects low cohesion when consumers import non-overlapping symbols', () => {
926
+ const state = emptyState();
927
+ state.files.add('src/junkdrawer.ts');
928
+ state.files.add('src/a.ts');
929
+ state.files.add('src/b.ts');
930
+ state.declaredExportsByFile.set('src/junkdrawer.ts', [
931
+ { name: 'alpha', kind: 'value' },
932
+ { name: 'beta', kind: 'value' },
933
+ { name: 'gamma', kind: 'value' },
934
+ { name: 'delta', kind: 'value' },
935
+ ]);
936
+ state.importedSymbolsByFile.set('src/a.ts', [
937
+ {
938
+ sourceModule: './junkdrawer',
939
+ resolvedModule: 'src/junkdrawer.ts',
940
+ importedName: 'alpha',
941
+ localName: 'alpha',
942
+ isTypeOnly: false,
943
+ },
944
+ {
945
+ sourceModule: './junkdrawer',
946
+ resolvedModule: 'src/junkdrawer.ts',
947
+ importedName: 'beta',
948
+ localName: 'beta',
949
+ isTypeOnly: false,
950
+ },
951
+ ]);
952
+ state.importedSymbolsByFile.set('src/b.ts', [
953
+ {
954
+ sourceModule: './junkdrawer',
955
+ resolvedModule: 'src/junkdrawer.ts',
956
+ importedName: 'gamma',
957
+ localName: 'gamma',
958
+ isTypeOnly: false,
959
+ },
960
+ {
961
+ sourceModule: './junkdrawer',
962
+ resolvedModule: 'src/junkdrawer.ts',
963
+ importedName: 'delta',
964
+ localName: 'delta',
965
+ isTypeOnly: false,
966
+ },
967
+ ]);
968
+ const findings = detectLowCohesion(state);
969
+ expect(findings.length).toBe(1);
970
+ expect(findings[0].category).toBe('low-cohesion');
971
+ expect(findings[0].file).toBe('src/junkdrawer.ts');
972
+ });
973
+
974
+ it('reports LCOM component count in reason', () => {
975
+ const state = emptyState();
976
+ state.files.add('src/utils.ts');
977
+ state.files.add('src/a.ts');
978
+ state.files.add('src/b.ts');
979
+ state.files.add('src/c.ts');
980
+ state.declaredExportsByFile.set('src/utils.ts', [
981
+ { name: 'e1', kind: 'value' },
982
+ { name: 'e2', kind: 'value' },
983
+ { name: 'e3', kind: 'value' },
984
+ { name: 'e4', kind: 'value' },
985
+ ]);
986
+ state.importedSymbolsByFile.set('src/a.ts', [
987
+ {
988
+ sourceModule: './utils',
989
+ resolvedModule: 'src/utils.ts',
990
+ importedName: 'e1',
991
+ localName: 'e1',
992
+ isTypeOnly: false,
993
+ },
994
+ ]);
995
+ state.importedSymbolsByFile.set('src/b.ts', [
996
+ {
997
+ sourceModule: './utils',
998
+ resolvedModule: 'src/utils.ts',
999
+ importedName: 'e2',
1000
+ localName: 'e2',
1001
+ isTypeOnly: false,
1002
+ },
1003
+ ]);
1004
+ state.importedSymbolsByFile.set('src/c.ts', [
1005
+ {
1006
+ sourceModule: './utils',
1007
+ resolvedModule: 'src/utils.ts',
1008
+ importedName: 'e3',
1009
+ localName: 'e3',
1010
+ isTypeOnly: false,
1011
+ },
1012
+ ]);
1013
+ const findings = detectLowCohesion(state);
1014
+ expect(findings.length).toBe(1);
1015
+ expect(findings[0].reason).toContain('3');
1016
+ });
1017
+
1018
+ it('skips test files', () => {
1019
+ const state = emptyState();
1020
+ state.files.add('src/utils.test.ts');
1021
+ state.declaredExportsByFile.set('src/utils.test.ts', [
1022
+ { name: 'a', kind: 'value' },
1023
+ { name: 'b', kind: 'value' },
1024
+ { name: 'c', kind: 'value' },
1025
+ { name: 'd', kind: 'value' },
1026
+ ]);
1027
+ expect(detectLowCohesion(state)).toEqual([]);
1028
+ });
1029
+ });
1030
+
1031
+ describe('computeHotFiles', () => {
1032
+ function minimalDepSummary(
1033
+ overrides: Partial<DependencySummary> = {}
1034
+ ): DependencySummary {
1035
+ return {
1036
+ totalModules: 0,
1037
+ totalEdges: 0,
1038
+ unresolvedEdgeCount: 0,
1039
+ externalDependencyFiles: 0,
1040
+ rootsCount: 0,
1041
+ leavesCount: 0,
1042
+ roots: [],
1043
+ leaves: [],
1044
+ criticalModules: [],
1045
+ testOnlyModules: [],
1046
+ unresolvedSample: [],
1047
+ outgoingTop: [],
1048
+ inboundTop: [],
1049
+ cycles: [],
1050
+ criticalPaths: [],
1051
+ ...overrides,
1052
+ };
1053
+ }
1054
+
1055
+ it('returns empty for empty input', () => {
1056
+ const result = computeHotFiles(
1057
+ emptyState(),
1058
+ minimalDepSummary(),
1059
+ new Map()
1060
+ );
1061
+ expect(result).toEqual([]);
1062
+ });
1063
+
1064
+ it('scores files by fan-in + complexity', () => {
1065
+ const state = emptyState();
1066
+ const hub = 'src/hub.ts';
1067
+ state.files.add(hub);
1068
+ for (let i = 0; i < 10; i++) {
1069
+ const f = `src/c${i}.ts`;
1070
+ state.files.add(f);
1071
+ addEdge(state, f, hub);
1072
+ }
1073
+ const critMap = new Map<string, FileCriticality>();
1074
+ critMap.set(hub, {
1075
+ file: hub,
1076
+ complexityRisk: 5,
1077
+ highComplexityFunctions: 3,
1078
+ functionCount: 8,
1079
+ flows: 20,
1080
+ score: 50,
1081
+ });
1082
+ const result = computeHotFiles(state, minimalDepSummary(), critMap);
1083
+ expect(result.length).toBeGreaterThan(0);
1084
+ expect(result[0].file).toBe(hub);
1085
+ expect(result[0].riskScore).toBeGreaterThan(0);
1086
+ expect(result[0].fanIn).toBe(10);
1087
+ });
1088
+
1089
+ it('boosts score for files in cycles', () => {
1090
+ const state = emptyState();
1091
+ state.files.add('src/a.ts');
1092
+ state.files.add('src/b.ts');
1093
+ addEdge(state, 'src/a.ts', 'src/b.ts');
1094
+ addEdge(state, 'src/b.ts', 'src/a.ts');
1095
+ const depSummary = minimalDepSummary({
1096
+ cycles: [{ path: ['src/a.ts', 'src/b.ts', 'src/a.ts'], nodeCount: 2 }],
1097
+ });
1098
+ const critMap = new Map<string, FileCriticality>();
1099
+ critMap.set('src/a.ts', {
1100
+ file: 'src/a.ts',
1101
+ complexityRisk: 1,
1102
+ highComplexityFunctions: 0,
1103
+ functionCount: 1,
1104
+ flows: 0,
1105
+ score: 10,
1106
+ });
1107
+ critMap.set('src/b.ts', {
1108
+ file: 'src/b.ts',
1109
+ complexityRisk: 1,
1110
+ highComplexityFunctions: 0,
1111
+ functionCount: 1,
1112
+ flows: 0,
1113
+ score: 10,
1114
+ });
1115
+ const result = computeHotFiles(state, depSummary, critMap);
1116
+ expect(result.some(f => f.inCycle)).toBe(true);
1117
+ });
1118
+
1119
+ it('returns files sorted by riskScore descending', () => {
1120
+ const state = emptyState();
1121
+ state.files.add('src/hot.ts');
1122
+ state.files.add('src/cold.ts');
1123
+ for (let i = 0; i < 10; i++) addEdge(state, `src/dep${i}.ts`, 'src/hot.ts');
1124
+ const critMap = new Map<string, FileCriticality>();
1125
+ critMap.set('src/hot.ts', {
1126
+ file: 'src/hot.ts',
1127
+ complexityRisk: 5,
1128
+ highComplexityFunctions: 3,
1129
+ functionCount: 10,
1130
+ flows: 20,
1131
+ score: 100,
1132
+ });
1133
+ critMap.set('src/cold.ts', {
1134
+ file: 'src/cold.ts',
1135
+ complexityRisk: 1,
1136
+ highComplexityFunctions: 0,
1137
+ functionCount: 1,
1138
+ flows: 0,
1139
+ score: 5,
1140
+ });
1141
+ const result = computeHotFiles(state, minimalDepSummary(), critMap);
1142
+ if (result.length >= 2) {
1143
+ expect(result[0].riskScore).toBeGreaterThanOrEqual(result[1].riskScore);
1144
+ }
1145
+ });
1146
+
1147
+ it('limits results to top 20', () => {
1148
+ const state = emptyState();
1149
+ for (let i = 0; i < 50; i++) {
1150
+ const f = `src/file${i}.ts`;
1151
+ state.files.add(f);
1152
+ addEdge(state, `src/consumer${i}.ts`, f);
1153
+ }
1154
+ const critMap = new Map<string, FileCriticality>();
1155
+ for (const f of state.files) {
1156
+ critMap.set(f, {
1157
+ file: f,
1158
+ complexityRisk: 1,
1159
+ highComplexityFunctions: 0,
1160
+ functionCount: 1,
1161
+ flows: 0,
1162
+ score: 5,
1163
+ });
1164
+ }
1165
+ const result = computeHotFiles(state, minimalDepSummary(), critMap);
1166
+ expect(result.length).toBeLessThanOrEqual(20);
1167
+ });
1168
+
1169
+ it('skips test files', () => {
1170
+ const state = emptyState();
1171
+ state.files.add('src/a.test.ts');
1172
+ for (let i = 0; i < 10; i++)
1173
+ addEdge(state, `src/c${i}.ts`, 'src/a.test.ts');
1174
+ const critMap = new Map<string, FileCriticality>();
1175
+ critMap.set('src/a.test.ts', {
1176
+ file: 'src/a.test.ts',
1177
+ complexityRisk: 1,
1178
+ highComplexityFunctions: 0,
1179
+ functionCount: 1,
1180
+ flows: 0,
1181
+ score: 50,
1182
+ });
1183
+ const result = computeHotFiles(state, minimalDepSummary(), critMap);
1184
+ expect(result.some(f => f.file === 'src/a.test.ts')).toBe(false);
1185
+ });
1186
+ });
1187
+
1188
+ describe('buildConsumedFromModule', () => {
1189
+ it('returns empty maps for no imports', () => {
1190
+ const result = buildConsumedFromModule(emptyState());
1191
+ expect(result.production.size).toBe(0);
1192
+ expect(result.test.size).toBe(0);
1193
+ });
1194
+
1195
+ it('collects consumed symbols per module in production map', () => {
1196
+ const state = emptyState();
1197
+ state.importedSymbolsByFile.set('src/a.ts', [
1198
+ {
1199
+ sourceModule: './lib',
1200
+ resolvedModule: 'src/lib.ts',
1201
+ importedName: 'foo',
1202
+ localName: 'foo',
1203
+ isTypeOnly: false,
1204
+ },
1205
+ {
1206
+ sourceModule: './lib',
1207
+ resolvedModule: 'src/lib.ts',
1208
+ importedName: 'bar',
1209
+ localName: 'bar',
1210
+ isTypeOnly: false,
1211
+ },
1212
+ ]);
1213
+ const result = buildConsumedFromModule(state);
1214
+ expect(result.production.get('src/lib.ts')?.size).toBe(2);
1215
+ expect(result.production.get('src/lib.ts')?.has('foo')).toBe(true);
1216
+ });
1217
+
1218
+ it('routes test file imports to the test map', () => {
1219
+ const state = emptyState();
1220
+ state.importedSymbolsByFile.set('src/a.test.ts', [
1221
+ {
1222
+ sourceModule: './lib',
1223
+ resolvedModule: 'src/lib.ts',
1224
+ importedName: 'foo',
1225
+ localName: 'foo',
1226
+ isTypeOnly: false,
1227
+ },
1228
+ ]);
1229
+ const result = buildConsumedFromModule(state);
1230
+ expect(result.production.size).toBe(0);
1231
+ expect(result.test.get('src/lib.ts')?.has('foo')).toBe(true);
1232
+ });
1233
+
1234
+ it('collects symbols from re-exports in production map', () => {
1235
+ const state = emptyState();
1236
+ state.reExportsByFile.set('src/barrel.ts', [
1237
+ {
1238
+ sourceModule: './lib',
1239
+ resolvedModule: 'src/lib.ts',
1240
+ exportedAs: 'X',
1241
+ importedName: 'X',
1242
+ isStar: false,
1243
+ isTypeOnly: false,
1244
+ },
1245
+ ]);
1246
+ const result = buildConsumedFromModule(state);
1247
+ expect(result.production.get('src/lib.ts')?.has('X')).toBe(true);
1248
+ });
1249
+
1250
+ it('skips re-exports with unresolved target', () => {
1251
+ const state = emptyState();
1252
+ state.reExportsByFile.set('src/barrel.ts', [
1253
+ {
1254
+ sourceModule: './unresolved',
1255
+ exportedAs: 'Y',
1256
+ importedName: 'Y',
1257
+ isStar: false,
1258
+ isTypeOnly: false,
1259
+ },
1260
+ ]);
1261
+ const result = buildConsumedFromModule(state);
1262
+ expect(result.production.size).toBe(0);
1263
+ });
1264
+ });
1265
+
1266
+ describe('detectDuplicateFunctionBodies', () => {
1267
+ function makeDupGroup(
1268
+ overrides: Partial<DuplicateGroup> = {}
1269
+ ): DuplicateGroup {
1270
+ return {
1271
+ hash: 'abc123',
1272
+ signature: 'handleError',
1273
+ kind: 'ArrowFunction',
1274
+ occurrences: 3,
1275
+ filesCount: 2,
1276
+ locations: [
1277
+ {
1278
+ kind: 'ArrowFunction',
1279
+ name: 'handleError',
1280
+ nameHint: 'handleError',
1281
+ file: 'src/a.ts',
1282
+ lineStart: 1,
1283
+ lineEnd: 10,
1284
+ columnStart: 1,
1285
+ columnEnd: 1,
1286
+ statementCount: 8,
1287
+ complexity: 3,
1288
+ maxBranchDepth: 1,
1289
+ maxLoopDepth: 0,
1290
+ returns: 1,
1291
+ awaits: 0,
1292
+ calls: 2,
1293
+ loops: 0,
1294
+ lengthLines: 10,
1295
+ cognitiveComplexity: 2,
1296
+ hash: 'abc',
1297
+ metrics: {
1298
+ complexity: 3,
1299
+ maxBranchDepth: 1,
1300
+ maxLoopDepth: 0,
1301
+ returns: 1,
1302
+ awaits: 0,
1303
+ calls: 2,
1304
+ loops: 0,
1305
+ },
1306
+ },
1307
+ {
1308
+ kind: 'ArrowFunction',
1309
+ name: 'handleError',
1310
+ nameHint: 'handleError',
1311
+ file: 'src/b.ts',
1312
+ lineStart: 5,
1313
+ lineEnd: 15,
1314
+ columnStart: 1,
1315
+ columnEnd: 1,
1316
+ statementCount: 8,
1317
+ complexity: 3,
1318
+ maxBranchDepth: 1,
1319
+ maxLoopDepth: 0,
1320
+ returns: 1,
1321
+ awaits: 0,
1322
+ calls: 2,
1323
+ loops: 0,
1324
+ lengthLines: 10,
1325
+ cognitiveComplexity: 2,
1326
+ hash: 'abc',
1327
+ metrics: {
1328
+ complexity: 3,
1329
+ maxBranchDepth: 1,
1330
+ maxLoopDepth: 0,
1331
+ returns: 1,
1332
+ awaits: 0,
1333
+ calls: 2,
1334
+ loops: 0,
1335
+ },
1336
+ },
1337
+ ],
1338
+ ...overrides,
1339
+ };
1340
+ }
1341
+
1342
+ it('returns empty for empty input', () => {
1343
+ expect(detectDuplicateFunctionBodies([])).toEqual([]);
1344
+ });
1345
+
1346
+ it('creates finding for duplicate group', () => {
1347
+ const findings = detectDuplicateFunctionBodies([makeDupGroup()]);
1348
+ expect(findings.length).toBe(1);
1349
+ expect(findings[0].category).toBe('duplicate-function-body');
1350
+ expect(findings[0].title).toContain('handleError');
1351
+ });
1352
+
1353
+ it('assigns low severity for 2 occurrences', () => {
1354
+ const findings = detectDuplicateFunctionBodies([
1355
+ makeDupGroup({ occurrences: 2 }),
1356
+ ]);
1357
+ expect(findings[0].severity).toBe('low');
1358
+ });
1359
+
1360
+ it('assigns medium severity for 3-5 occurrences', () => {
1361
+ const findings = detectDuplicateFunctionBodies([
1362
+ makeDupGroup({ occurrences: 4 }),
1363
+ ]);
1364
+ expect(findings[0].severity).toBe('medium');
1365
+ });
1366
+
1367
+ it('assigns high severity for 6+ occurrences', () => {
1368
+ const findings = detectDuplicateFunctionBodies([
1369
+ makeDupGroup({ occurrences: 7 }),
1370
+ ]);
1371
+ expect(findings[0].severity).toBe('high');
1372
+ });
1373
+
1374
+ it('includes all file locations in files field', () => {
1375
+ const findings = detectDuplicateFunctionBodies([makeDupGroup()]);
1376
+ expect(findings[0].files.length).toBe(2);
1377
+ });
1378
+
1379
+ it('uses plural "files" in reason when filesCount > 1', () => {
1380
+ const findings = detectDuplicateFunctionBodies([
1381
+ makeDupGroup({ filesCount: 3, occurrences: 4 }),
1382
+ ]);
1383
+ expect(findings[0].reason).toContain('3 file');
1384
+ expect(findings[0].reason).toContain('s');
1385
+ });
1386
+ });
1387
+
1388
+ describe('detectDuplicateFlowStructures', () => {
1389
+ function makeFlowGroup(
1390
+ overrides: Partial<RedundantFlowGroup> = {}
1391
+ ): RedundantFlowGroup {
1392
+ return {
1393
+ kind: 'IfStatement',
1394
+ occurrences: 5,
1395
+ filesCount: 3,
1396
+ locations: [
1397
+ {
1398
+ kind: 'IfStatement',
1399
+ file: 'src/a.ts',
1400
+ lineStart: 10,
1401
+ lineEnd: 20,
1402
+ columnStart: 1,
1403
+ columnEnd: 1,
1404
+ statementCount: 5,
1405
+ hash: 'x',
1406
+ },
1407
+ {
1408
+ kind: 'IfStatement',
1409
+ file: 'src/b.ts',
1410
+ lineStart: 15,
1411
+ lineEnd: 25,
1412
+ columnStart: 1,
1413
+ columnEnd: 1,
1414
+ statementCount: 5,
1415
+ hash: 'x',
1416
+ },
1417
+ ],
1418
+ ...overrides,
1419
+ };
1420
+ }
1421
+
1422
+ it('returns empty for empty input', () => {
1423
+ expect(detectDuplicateFlowStructures([], 3)).toEqual([]);
1424
+ });
1425
+
1426
+ it('skips groups below threshold', () => {
1427
+ const findings = detectDuplicateFlowStructures(
1428
+ [makeFlowGroup({ occurrences: 2 })],
1429
+ 3
1430
+ );
1431
+ expect(findings).toEqual([]);
1432
+ });
1433
+
1434
+ it('creates finding above threshold', () => {
1435
+ const findings = detectDuplicateFlowStructures([makeFlowGroup()], 3);
1436
+ expect(findings.length).toBe(1);
1437
+ expect(findings[0].category).toBe('duplicate-flow-structure');
1438
+ });
1439
+
1440
+ it('assigns high severity for 10+ occurrences', () => {
1441
+ const findings = detectDuplicateFlowStructures(
1442
+ [makeFlowGroup({ occurrences: 12 })],
1443
+ 3
1444
+ );
1445
+ expect(findings[0].severity).toBe('high');
1446
+ });
1447
+
1448
+ it('assigns medium severity for fewer occurrences', () => {
1449
+ const findings = detectDuplicateFlowStructures(
1450
+ [makeFlowGroup({ occurrences: 5 })],
1451
+ 3
1452
+ );
1453
+ expect(findings[0].severity).toBe('medium');
1454
+ });
1455
+ });
1456
+
1457
+ describe('detectFunctionOptimization', () => {
1458
+ it('returns empty for simple functions', () => {
1459
+ const files = [
1460
+ makeFileEntry({
1461
+ functions: [
1462
+ makeFn({
1463
+ complexity: 5,
1464
+ maxBranchDepth: 2,
1465
+ maxLoopDepth: 1,
1466
+ statementCount: 10,
1467
+ }),
1468
+ ],
1469
+ }),
1470
+ ];
1471
+ expect(detectFunctionOptimization(files, 30)).toEqual([]);
1472
+ });
1473
+
1474
+ it('flags high complexity', () => {
1475
+ const files = [
1476
+ makeFileEntry({
1477
+ functions: [makeFn({ complexity: 35, name: 'complexFn' })],
1478
+ }),
1479
+ ];
1480
+ const findings = detectFunctionOptimization(files, 30);
1481
+ expect(findings.length).toBe(1);
1482
+ expect(findings[0].severity).toBe('high');
1483
+ });
1484
+
1485
+ it('flags deep branch nesting', () => {
1486
+ const files = [
1487
+ makeFileEntry({
1488
+ functions: [makeFn({ maxBranchDepth: 8, name: 'deepFn' })],
1489
+ }),
1490
+ ];
1491
+ const findings = detectFunctionOptimization(files, 30);
1492
+ expect(findings.length).toBe(1);
1493
+ expect(findings[0].reason).toContain('Branch depth');
1494
+ });
1495
+
1496
+ it('flags deep loop nesting', () => {
1497
+ const files = [
1498
+ makeFileEntry({
1499
+ functions: [makeFn({ maxLoopDepth: 5, name: 'loopFn' })],
1500
+ }),
1501
+ ];
1502
+ const findings = detectFunctionOptimization(files, 30);
1503
+ expect(findings.length).toBe(1);
1504
+ expect(findings[0].reason).toContain('Nested loops');
1505
+ });
1506
+
1507
+ it('flags large function bodies', () => {
1508
+ const files = [
1509
+ makeFileEntry({
1510
+ functions: [makeFn({ statementCount: 30, name: 'bigFn' })],
1511
+ }),
1512
+ ];
1513
+ const findings = detectFunctionOptimization(files, 30);
1514
+ expect(findings.length).toBe(1);
1515
+ expect(findings[0].severity).toBe('medium');
1516
+ });
1517
+
1518
+ it('combines multiple alerts', () => {
1519
+ const files = [
1520
+ makeFileEntry({
1521
+ functions: [
1522
+ makeFn({ complexity: 40, maxBranchDepth: 8, name: 'badFn' }),
1523
+ ],
1524
+ }),
1525
+ ];
1526
+ const findings = detectFunctionOptimization(files, 30);
1527
+ expect(findings[0].reason).toContain('Cyclomatic');
1528
+ expect(findings[0].reason).toContain('Branch depth');
1529
+ });
1530
+ });
1531
+
1532
+ describe('detectTestOnlyModules', () => {
1533
+ it('returns empty when no test-only modules', () => {
1534
+ expect(detectTestOnlyModules(minimalDepSummary())).toEqual([]);
1535
+ });
1536
+
1537
+ it('creates finding for test-only module', () => {
1538
+ const summary = minimalDepSummary({
1539
+ testOnlyModules: [
1540
+ {
1541
+ file: 'src/test-helper.ts',
1542
+ outboundCount: 0,
1543
+ inboundCount: 1,
1544
+ inboundFromProduction: 0,
1545
+ inboundFromTests: 1,
1546
+ externalDependencyCount: 0,
1547
+ unresolvedDependencyCount: 0,
1548
+ },
1549
+ ],
1550
+ });
1551
+ const findings = detectTestOnlyModules(summary);
1552
+ expect(findings.length).toBe(1);
1553
+ expect(findings[0].category).toBe('dependency-test-only');
1554
+ expect(findings[0].severity).toBe('medium');
1555
+ });
1556
+
1557
+ it('limits to 25 findings', () => {
1558
+ const modules = Array.from({ length: 30 }, (_, i) => ({
1559
+ file: `src/helper${i}.ts`,
1560
+ outboundCount: 0,
1561
+ inboundCount: 1,
1562
+ inboundFromProduction: 0,
1563
+ inboundFromTests: 1,
1564
+ externalDependencyCount: 0,
1565
+ unresolvedDependencyCount: 0,
1566
+ }));
1567
+ const findings = detectTestOnlyModules(
1568
+ minimalDepSummary({ testOnlyModules: modules })
1569
+ );
1570
+ expect(findings.length).toBe(25);
1571
+ });
1572
+ });
1573
+
1574
+ describe('detectDependencyCycles (detector)', () => {
1575
+ it('returns empty for no cycles', () => {
1576
+ expect(detectDependencyCycles(minimalDepSummary(), emptyState())).toEqual(
1577
+ []
1578
+ );
1579
+ });
1580
+
1581
+ it('creates finding per cycle', () => {
1582
+ const state = emptyState();
1583
+ state.files.add('a.ts');
1584
+ state.files.add('b.ts');
1585
+ const summary = minimalDepSummary({
1586
+ cycles: [{ path: ['a.ts', 'b.ts', 'a.ts'], nodeCount: 2 }],
1587
+ });
1588
+ const findings = detectDependencyCycles(summary, state);
1589
+ expect(findings.length).toBe(1);
1590
+ expect(findings[0].category).toBe('dependency-cycle');
1591
+ expect(findings[0].severity).toBe('high');
1592
+ });
1593
+
1594
+ it('limits to 15 cycle findings', () => {
1595
+ const cycles = Array.from({ length: 20 }, (_, i) => ({
1596
+ path: [`src/a${i}.ts`, `src/b${i}.ts`, `src/a${i}.ts`],
1597
+ nodeCount: 2,
1598
+ }));
1599
+ const findings = detectDependencyCycles(
1600
+ minimalDepSummary({ cycles }),
1601
+ emptyState()
1602
+ );
1603
+ expect(findings.length).toBe(15);
1604
+ });
1605
+ });
1606
+
1607
+ describe('detectCriticalPaths (detector)', () => {
1608
+ it('returns empty for no critical paths', () => {
1609
+ expect(detectCriticalPaths(minimalDepSummary(), emptyState(), 30)).toEqual(
1610
+ []
1611
+ );
1612
+ });
1613
+
1614
+ it('creates finding for high-score path', () => {
1615
+ const state = emptyState();
1616
+ const summary = minimalDepSummary({
1617
+ criticalPaths: [
1618
+ {
1619
+ start: 'a.ts',
1620
+ path: ['a.ts', 'b.ts', 'c.ts'],
1621
+ score: 300,
1622
+ length: 3,
1623
+ containsCycle: false,
1624
+ },
1625
+ ],
1626
+ });
1627
+ const findings = detectCriticalPaths(summary, state, 30);
1628
+ expect(findings.length).toBe(1);
1629
+ expect(findings[0].category).toBe('dependency-critical-path');
1630
+ });
1631
+
1632
+ it('skips paths below score threshold', () => {
1633
+ const summary = minimalDepSummary({
1634
+ criticalPaths: [
1635
+ {
1636
+ start: 'a.ts',
1637
+ path: ['a.ts', 'b.ts'],
1638
+ score: 10,
1639
+ length: 2,
1640
+ containsCycle: false,
1641
+ },
1642
+ ],
1643
+ });
1644
+ const findings = detectCriticalPaths(summary, emptyState(), 30);
1645
+ expect(findings).toEqual([]);
1646
+ });
1647
+
1648
+ it('assigns critical severity for very high scores', () => {
1649
+ const summary = minimalDepSummary({
1650
+ criticalPaths: [
1651
+ {
1652
+ start: 'a.ts',
1653
+ path: ['a.ts', 'b.ts'],
1654
+ score: 500,
1655
+ length: 2,
1656
+ containsCycle: false,
1657
+ },
1658
+ ],
1659
+ });
1660
+ const findings = detectCriticalPaths(summary, emptyState(), 30);
1661
+ expect(findings[0].severity).toBe('critical');
1662
+ });
1663
+ });
1664
+
1665
+ describe('mergeOverlappingChains', () => {
1666
+ type FindingDraft = Omit<import('../types/index.js').Finding, 'id'>;
1667
+
1668
+ const makeChainFinding = (file: string, files: string[]): FindingDraft => ({
1669
+ severity: 'high',
1670
+ category: 'dependency-critical-path',
1671
+ file,
1672
+ lineStart: 1,
1673
+ lineEnd: 1,
1674
+ title: `Critical dependency chain risk: ${files.length} files`,
1675
+ reason: `Chain from ${file}.`,
1676
+ files,
1677
+ suggestedFix: { strategy: 'test', steps: ['step1'] },
1678
+ });
1679
+
1680
+ it('returns input unchanged when 0 or 1 findings', () => {
1681
+ expect(mergeOverlappingChains([])).toEqual([]);
1682
+ const single = [makeChainFinding('a.ts', ['a.ts', 'b.ts'])];
1683
+ expect(mergeOverlappingChains(single)).toEqual(single);
1684
+ });
1685
+
1686
+ it('merges chains with >80% overlap', () => {
1687
+ const shared = Array.from({ length: 10 }, (_, i) => `shared-${i}.ts`);
1688
+ const chain1 = makeChainFinding('e1.ts', ['e1.ts', ...shared]);
1689
+ const chain2 = makeChainFinding('e2.ts', ['e2.ts', ...shared]);
1690
+ const result = mergeOverlappingChains([chain1, chain2]);
1691
+ expect(result).toHaveLength(1);
1692
+ expect(result[0].title).toContain('2 entry points');
1693
+ expect(result[0].reason).toContain('Also reached from: e2.ts');
1694
+ expect(result[0].files.length).toBeGreaterThanOrEqual(11);
1695
+ });
1696
+
1697
+ it('does NOT merge chains with <80% overlap', () => {
1698
+ const f1 = makeChainFinding('a.ts', ['a.ts', 'shared.ts']);
1699
+ const f2 = makeChainFinding('b.ts', ['b.ts', 'other.ts']);
1700
+ const result = mergeOverlappingChains([f1, f2]);
1701
+ expect(result).toHaveLength(2);
1702
+ });
1703
+
1704
+ it('merges multiple chains into one when overlap stays above threshold', () => {
1705
+ const shared = Array.from({ length: 20 }, (_, i) => `m${i}.ts`);
1706
+ const chains = Array.from({ length: 3 }, (_, i) =>
1707
+ makeChainFinding(`entry-${i}.ts`, [`entry-${i}.ts`, ...shared])
1708
+ );
1709
+ const result = mergeOverlappingChains(chains);
1710
+ expect(result).toHaveLength(1);
1711
+ expect(result[0].title).toContain('3 entry points');
1712
+ });
1713
+
1714
+ it('keeps non-overlapping chains separate while merging overlapping ones', () => {
1715
+ const shared = Array.from({ length: 10 }, (_, i) => `s${i}.ts`);
1716
+ const overlap1 = makeChainFinding('o1.ts', ['o1.ts', ...shared]);
1717
+ const overlap2 = makeChainFinding('o2.ts', ['o2.ts', ...shared]);
1718
+ const distinct = makeChainFinding('d.ts', [
1719
+ 'd.ts',
1720
+ 'unique1.ts',
1721
+ 'unique2.ts',
1722
+ ]);
1723
+
1724
+ const result = mergeOverlappingChains([overlap1, overlap2, distinct]);
1725
+ expect(result).toHaveLength(2);
1726
+ expect(result.find(f => f.title.includes('entry points'))).toBeDefined();
1727
+ expect(result.find(f => f.file === 'd.ts')).toBeDefined();
1728
+ });
1729
+ });
1730
+
1731
+ describe('detectCriticalPaths — computed fix & merging', () => {
1732
+ it('names the highest-fan-out module in suggestedFix.strategy', () => {
1733
+ const state = emptyState();
1734
+ addEdge(state, 'a.ts', 'hub.ts');
1735
+ addEdge(state, 'hub.ts', 'c.ts');
1736
+ addEdge(state, 'hub.ts', 'd.ts');
1737
+ addEdge(state, 'hub.ts', 'e.ts');
1738
+ const summary = minimalDepSummary({
1739
+ criticalPaths: [
1740
+ {
1741
+ start: 'a.ts',
1742
+ path: ['a.ts', 'hub.ts', 'c.ts'],
1743
+ score: 300,
1744
+ length: 3,
1745
+ containsCycle: false,
1746
+ },
1747
+ ],
1748
+ });
1749
+ const findings = detectCriticalPaths(summary, state, 30);
1750
+ expect(findings.length).toBe(1);
1751
+ expect(findings[0].suggestedFix.strategy).toContain('hub.ts');
1752
+ expect(findings[0].suggestedFix.strategy).toContain('fan-out: 3');
1753
+ expect(findings[0].suggestedFix.steps[0]).toContain('hub.ts');
1754
+ });
1755
+
1756
+ it('uses first module as hotspot when all have zero fan-out', () => {
1757
+ const state = emptyState();
1758
+ state.files.add('x.ts');
1759
+ state.files.add('y.ts');
1760
+ const summary = minimalDepSummary({
1761
+ criticalPaths: [
1762
+ {
1763
+ start: 'x.ts',
1764
+ path: ['x.ts', 'y.ts'],
1765
+ score: 300,
1766
+ length: 2,
1767
+ containsCycle: false,
1768
+ },
1769
+ ],
1770
+ });
1771
+ const findings = detectCriticalPaths(summary, state, 30);
1772
+ expect(findings[0].suggestedFix.strategy).toContain('x.ts');
1773
+ expect(findings[0].suggestedFix.strategy).toContain('fan-out: 0');
1774
+ });
1775
+
1776
+ it('includes fan-in in the hotspot strategy', () => {
1777
+ const state = emptyState();
1778
+ addEdge(state, 'caller1.ts', 'hub.ts');
1779
+ addEdge(state, 'caller2.ts', 'hub.ts');
1780
+ addEdge(state, 'hub.ts', 'dep.ts');
1781
+ addEdge(state, 'hub.ts', 'dep2.ts');
1782
+ addEdge(state, 'hub.ts', 'dep3.ts');
1783
+ const summary = minimalDepSummary({
1784
+ criticalPaths: [
1785
+ {
1786
+ start: 'caller1.ts',
1787
+ path: ['caller1.ts', 'hub.ts', 'dep.ts'],
1788
+ score: 300,
1789
+ length: 3,
1790
+ containsCycle: false,
1791
+ },
1792
+ ],
1793
+ });
1794
+ const findings = detectCriticalPaths(summary, state, 30);
1795
+ expect(findings[0].suggestedFix.strategy).toContain('fan-in: 2');
1796
+ });
1797
+
1798
+ it('merges overlapping chain findings', () => {
1799
+ const state = emptyState();
1800
+ const shared = Array.from({ length: 10 }, (_, i) => `m${i}.ts`);
1801
+ for (let i = 0; i < shared.length - 1; i++) {
1802
+ addEdge(state, shared[i], shared[i + 1]);
1803
+ }
1804
+ addEdge(state, 'e1.ts', shared[0]);
1805
+ addEdge(state, 'e2.ts', shared[0]);
1806
+
1807
+ const summary = minimalDepSummary({
1808
+ criticalPaths: [
1809
+ {
1810
+ start: 'e1.ts',
1811
+ path: ['e1.ts', ...shared],
1812
+ score: 300,
1813
+ length: shared.length + 1,
1814
+ containsCycle: false,
1815
+ },
1816
+ {
1817
+ start: 'e2.ts',
1818
+ path: ['e2.ts', ...shared],
1819
+ score: 300,
1820
+ length: shared.length + 1,
1821
+ containsCycle: false,
1822
+ },
1823
+ ],
1824
+ });
1825
+ const findings = detectCriticalPaths(summary, state, 30);
1826
+ expect(findings).toHaveLength(1);
1827
+ expect(findings[0].title).toContain('entry points');
1828
+ });
1829
+ });
1830
+
1831
+ describe('detectDeadFiles', () => {
1832
+ it('returns empty when no dead files', () => {
1833
+ const state = emptyState();
1834
+ addEdge(state, 'src/a.ts', 'src/b.ts');
1835
+ expect(
1836
+ detectDeadFiles(minimalDepSummary({ roots: ['src/a.ts'] }), state)
1837
+ ).toEqual([]);
1838
+ });
1839
+
1840
+ it('flags root file with zero outgoing', () => {
1841
+ const state = emptyState();
1842
+ state.files.add('src/dead.ts');
1843
+ const findings = detectDeadFiles(
1844
+ minimalDepSummary({ roots: ['src/dead.ts'] }),
1845
+ state
1846
+ );
1847
+ expect(findings.length).toBe(1);
1848
+ expect(findings[0].category).toBe('dead-file');
1849
+ });
1850
+
1851
+ it('skips entrypoints', () => {
1852
+ const state = emptyState();
1853
+ state.files.add('src/index.ts');
1854
+ const findings = detectDeadFiles(
1855
+ minimalDepSummary({ roots: ['src/index.ts'] }),
1856
+ state
1857
+ );
1858
+ expect(findings).toEqual([]);
1859
+ });
1860
+
1861
+ it('skips test files', () => {
1862
+ const state = emptyState();
1863
+ state.files.add('src/foo.test.ts');
1864
+ const findings = detectDeadFiles(
1865
+ minimalDepSummary({ roots: ['src/foo.test.ts'] }),
1866
+ state
1867
+ );
1868
+ expect(findings).toEqual([]);
1869
+ });
1870
+
1871
+ it('skips roots with outgoing dependencies', () => {
1872
+ const state = emptyState();
1873
+ addEdge(state, 'src/root.ts', 'src/dep.ts');
1874
+ const findings = detectDeadFiles(
1875
+ minimalDepSummary({ roots: ['src/root.ts'] }),
1876
+ state
1877
+ );
1878
+ expect(findings).toEqual([]);
1879
+ });
1880
+ });
1881
+
1882
+ describe('detectDeadExports', () => {
1883
+ it('returns empty when all exports consumed', () => {
1884
+ const state = emptyState();
1885
+ state.files.add('src/lib.ts');
1886
+ state.declaredExportsByFile.set('src/lib.ts', [
1887
+ { name: 'foo', kind: 'value' },
1888
+ ]);
1889
+ const consumed = new Map([['src/lib.ts', new Set(['foo'])]]);
1890
+ expect(detectDeadExports(state, consumed)).toEqual([]);
1891
+ });
1892
+
1893
+ it('flags unused exports', () => {
1894
+ const state = emptyState();
1895
+ state.files.add('src/lib.ts');
1896
+ state.declaredExportsByFile.set('src/lib.ts', [
1897
+ { name: 'used', kind: 'value' },
1898
+ { name: 'dead', kind: 'value', lineStart: 10 },
1899
+ ]);
1900
+ const consumed = new Map([['src/lib.ts', new Set(['used'])]]);
1901
+ const findings = detectDeadExports(state, consumed);
1902
+ expect(findings.length).toBe(1);
1903
+ expect(findings[0].title).toContain('dead');
1904
+ });
1905
+
1906
+ it('assigns medium severity for type exports', () => {
1907
+ const state = emptyState();
1908
+ state.files.add('src/lib.ts');
1909
+ state.declaredExportsByFile.set('src/lib.ts', [
1910
+ { name: 'MyType', kind: 'type' },
1911
+ ]);
1912
+ const findings = detectDeadExports(state, new Map());
1913
+ expect(findings[0].severity).toBe('medium');
1914
+ });
1915
+
1916
+ it('assigns high severity for value exports', () => {
1917
+ const state = emptyState();
1918
+ state.files.add('src/lib.ts');
1919
+ state.declaredExportsByFile.set('src/lib.ts', [
1920
+ { name: 'myFn', kind: 'value' },
1921
+ ]);
1922
+ const findings = detectDeadExports(state, new Map());
1923
+ expect(findings[0].severity).toBe('high');
1924
+ });
1925
+
1926
+ it('skips namespace-imported modules', () => {
1927
+ const state = emptyState();
1928
+ state.files.add('src/lib.ts');
1929
+ state.declaredExportsByFile.set('src/lib.ts', [
1930
+ { name: 'foo', kind: 'value' },
1931
+ ]);
1932
+ const consumed = new Map([['src/lib.ts', new Set(['*'])]]);
1933
+ expect(detectDeadExports(state, consumed)).toEqual([]);
1934
+ });
1935
+ });
1936
+
1937
+ describe('detectDeadReExports', () => {
1938
+ it('returns empty for consumed re-exports', () => {
1939
+ const state = emptyState();
1940
+ state.reExportsByFile.set('src/index.ts', [
1941
+ {
1942
+ sourceModule: './a',
1943
+ resolvedModule: 'src/a.ts',
1944
+ exportedAs: 'X',
1945
+ importedName: 'X',
1946
+ isStar: false,
1947
+ isTypeOnly: false,
1948
+ },
1949
+ ]);
1950
+ const consumed = new Map([['src/index.ts', new Set(['X'])]]);
1951
+ expect(detectDeadReExports(state, consumed)).toEqual([]);
1952
+ });
1953
+
1954
+ it('flags unused re-exports', () => {
1955
+ const state = emptyState();
1956
+ state.reExportsByFile.set('src/index.ts', [
1957
+ {
1958
+ sourceModule: './a',
1959
+ resolvedModule: 'src/a.ts',
1960
+ exportedAs: 'Dead',
1961
+ importedName: 'Dead',
1962
+ isStar: false,
1963
+ isTypeOnly: false,
1964
+ },
1965
+ ]);
1966
+ const findings = detectDeadReExports(state, new Map());
1967
+ expect(findings.some(f => f.category === 'dead-re-export')).toBe(true);
1968
+ });
1969
+
1970
+ it('detects duplicate re-export sources', () => {
1971
+ const state = emptyState();
1972
+ state.reExportsByFile.set('src/barrel.ts', [
1973
+ {
1974
+ sourceModule: './a',
1975
+ resolvedModule: 'src/a.ts',
1976
+ exportedAs: 'Foo',
1977
+ importedName: 'Foo',
1978
+ isStar: false,
1979
+ isTypeOnly: false,
1980
+ },
1981
+ {
1982
+ sourceModule: './b',
1983
+ resolvedModule: 'src/b.ts',
1984
+ exportedAs: 'Foo',
1985
+ importedName: 'Foo',
1986
+ isStar: false,
1987
+ isTypeOnly: false,
1988
+ },
1989
+ ]);
1990
+ const consumed = new Map([['src/barrel.ts', new Set(['Foo'])]]);
1991
+ const findings = detectDeadReExports(state, consumed);
1992
+ expect(findings.some(f => f.category === 're-export-duplication')).toBe(
1993
+ true
1994
+ );
1995
+ });
1996
+
1997
+ it('detects shadowed re-exports', () => {
1998
+ const state = emptyState();
1999
+ state.declaredExportsByFile.set('src/barrel.ts', [
2000
+ { name: 'Conflict', kind: 'value' },
2001
+ ]);
2002
+ state.reExportsByFile.set('src/barrel.ts', [
2003
+ {
2004
+ sourceModule: './a',
2005
+ resolvedModule: 'src/a.ts',
2006
+ exportedAs: 'Conflict',
2007
+ importedName: 'Conflict',
2008
+ isStar: false,
2009
+ isTypeOnly: false,
2010
+ },
2011
+ ]);
2012
+ const consumed = new Map([['src/barrel.ts', new Set(['Conflict'])]]);
2013
+ const findings = detectDeadReExports(state, consumed);
2014
+ expect(findings.some(f => f.category === 're-export-shadowed')).toBe(true);
2015
+ });
2016
+ });
2017
+
2018
+ describe('detectExcessiveParameters', () => {
2019
+ it('returns empty for functions within threshold', () => {
2020
+ const files = [makeFileEntry({ functions: [makeFn({ params: 3 })] })];
2021
+ expect(detectExcessiveParameters(files, 5)).toEqual([]);
2022
+ });
2023
+
2024
+ it('flags functions exceeding threshold', () => {
2025
+ const files = [
2026
+ makeFileEntry({ functions: [makeFn({ params: 7, name: 'manyArgs' })] }),
2027
+ ];
2028
+ const findings = detectExcessiveParameters(files, 5);
2029
+ expect(findings.length).toBe(1);
2030
+ expect(findings[0].category).toBe('excessive-parameters');
2031
+ expect(findings[0].title).toContain('manyArgs');
2032
+ });
2033
+
2034
+ it('assigns high severity for >7 params', () => {
2035
+ const files = [makeFileEntry({ functions: [makeFn({ params: 9 })] })];
2036
+ const findings = detectExcessiveParameters(files, 5);
2037
+ expect(findings[0].severity).toBe('high');
2038
+ });
2039
+
2040
+ it('assigns medium severity for 6-7 params', () => {
2041
+ const files = [makeFileEntry({ functions: [makeFn({ params: 6 })] })];
2042
+ const findings = detectExcessiveParameters(files, 5);
2043
+ expect(findings[0].severity).toBe('medium');
2044
+ });
2045
+
2046
+ it('skips functions with no param count', () => {
2047
+ const files = [makeFileEntry({ functions: [makeFn()] })];
2048
+ expect(detectExcessiveParameters(files, 5)).toEqual([]);
2049
+ });
2050
+
2051
+ it('skips test files', () => {
2052
+ const files = [
2053
+ makeFileEntry({
2054
+ file: 'src/a.test.ts',
2055
+ functions: [makeFn({ params: 10 })],
2056
+ }),
2057
+ ];
2058
+ expect(detectExcessiveParameters(files, 5)).toEqual([]);
2059
+ });
2060
+ });
2061
+
2062
+ describe('detectEmptyCatchBlocks', () => {
2063
+ it('returns empty when no empty catches', () => {
2064
+ const files = [makeFileEntry()];
2065
+ expect(detectEmptyCatchBlocks(files)).toEqual([]);
2066
+ });
2067
+
2068
+ it('flags empty catch blocks', () => {
2069
+ const loc: CodeLocation = {
2070
+ file: 'src/file.ts',
2071
+ lineStart: 10,
2072
+ lineEnd: 12,
2073
+ };
2074
+ const files = [makeFileEntry({ emptyCatches: [loc] })];
2075
+ const findings = detectEmptyCatchBlocks(files);
2076
+ expect(findings.length).toBe(1);
2077
+ expect(findings[0].category).toBe('empty-catch');
2078
+ expect(findings[0].severity).toBe('medium');
2079
+ expect(findings[0].lineStart).toBe(10);
2080
+ });
2081
+
2082
+ it('creates finding per empty catch', () => {
2083
+ const locs: CodeLocation[] = [
2084
+ { file: 'src/file.ts', lineStart: 10, lineEnd: 12 },
2085
+ { file: 'src/file.ts', lineStart: 30, lineEnd: 32 },
2086
+ ];
2087
+ const files = [makeFileEntry({ emptyCatches: locs })];
2088
+ const findings = detectEmptyCatchBlocks(files);
2089
+ expect(findings.length).toBe(2);
2090
+ });
2091
+
2092
+ it('skips test files', () => {
2093
+ const loc: CodeLocation = {
2094
+ file: 'src/a.test.ts',
2095
+ lineStart: 10,
2096
+ lineEnd: 12,
2097
+ };
2098
+ const files = [
2099
+ makeFileEntry({ file: 'src/a.test.ts', emptyCatches: [loc] }),
2100
+ ];
2101
+ expect(detectEmptyCatchBlocks(files)).toEqual([]);
2102
+ });
2103
+ });
2104
+
2105
+ describe('detectSwitchNoDefault', () => {
2106
+ it('returns empty when no switches without default', () => {
2107
+ const files = [makeFileEntry()];
2108
+ expect(detectSwitchNoDefault(files)).toEqual([]);
2109
+ });
2110
+
2111
+ it('flags switch without default', () => {
2112
+ const loc: CodeLocation = {
2113
+ file: 'src/file.ts',
2114
+ lineStart: 15,
2115
+ lineEnd: 30,
2116
+ };
2117
+ const files = [makeFileEntry({ switchesWithoutDefault: [loc] })];
2118
+ const findings = detectSwitchNoDefault(files);
2119
+ expect(findings.length).toBe(1);
2120
+ expect(findings[0].category).toBe('switch-no-default');
2121
+ expect(findings[0].severity).toBe('low');
2122
+ });
2123
+
2124
+ it('skips test files', () => {
2125
+ const loc: CodeLocation = {
2126
+ file: 'src/a.test.ts',
2127
+ lineStart: 15,
2128
+ lineEnd: 30,
2129
+ };
2130
+ const files = [
2131
+ makeFileEntry({ file: 'src/a.test.ts', switchesWithoutDefault: [loc] }),
2132
+ ];
2133
+ expect(detectSwitchNoDefault(files)).toEqual([]);
2134
+ });
2135
+ });
2136
+
2137
+ describe('detectUnsafeAny', () => {
2138
+ it('returns empty for files below threshold', () => {
2139
+ const files = [makeFileEntry({ anyCount: 3 })];
2140
+ expect(detectUnsafeAny(files, 5)).toEqual([]);
2141
+ });
2142
+
2143
+ it('flags files exceeding threshold', () => {
2144
+ const files = [makeFileEntry({ anyCount: 8 })];
2145
+ const findings = detectUnsafeAny(files, 5);
2146
+ expect(findings.length).toBe(1);
2147
+ expect(findings[0].category).toBe('unsafe-any');
2148
+ });
2149
+
2150
+ it('assigns high severity for >10 any usages', () => {
2151
+ const files = [makeFileEntry({ anyCount: 15 })];
2152
+ const findings = detectUnsafeAny(files, 5);
2153
+ expect(findings[0].severity).toBe('high');
2154
+ });
2155
+
2156
+ it('assigns medium severity for 6-10 any usages', () => {
2157
+ const files = [makeFileEntry({ anyCount: 7 })];
2158
+ const findings = detectUnsafeAny(files, 5);
2159
+ expect(findings[0].severity).toBe('medium');
2160
+ });
2161
+
2162
+ it('skips files with no anyCount', () => {
2163
+ const files = [makeFileEntry()];
2164
+ expect(detectUnsafeAny(files, 5)).toEqual([]);
2165
+ });
2166
+ });
2167
+
2168
+ describe('detectHighHalsteadEffort', () => {
2169
+ it('returns empty for functions below thresholds', () => {
2170
+ const files = [
2171
+ makeFileEntry({
2172
+ functions: [
2173
+ makeFn({
2174
+ halstead: {
2175
+ operators: 10,
2176
+ operands: 10,
2177
+ distinctOperators: 5,
2178
+ distinctOperands: 5,
2179
+ vocabulary: 10,
2180
+ length: 20,
2181
+ volume: 100,
2182
+ difficulty: 5,
2183
+ effort: 500,
2184
+ time: 28,
2185
+ estimatedBugs: 0.03,
2186
+ },
2187
+ }),
2188
+ ],
2189
+ }),
2190
+ ];
2191
+ expect(detectHighHalsteadEffort(files)).toEqual([]);
2192
+ });
2193
+
2194
+ it('flags functions with high effort', () => {
2195
+ const files = [
2196
+ makeFileEntry({
2197
+ functions: [
2198
+ makeFn({
2199
+ name: 'heavyFn',
2200
+ halstead: {
2201
+ operators: 100,
2202
+ operands: 200,
2203
+ distinctOperators: 30,
2204
+ distinctOperands: 50,
2205
+ vocabulary: 80,
2206
+ length: 300,
2207
+ volume: 50000,
2208
+ difficulty: 20,
2209
+ effort: 1_000_000,
2210
+ time: 55556,
2211
+ estimatedBugs: 1.5,
2212
+ },
2213
+ }),
2214
+ ],
2215
+ }),
2216
+ ];
2217
+ const findings = detectHighHalsteadEffort(files);
2218
+ expect(findings.length).toBe(1);
2219
+ expect(findings[0].category).toBe('halstead-effort');
2220
+ expect(findings[0].title).toContain('heavyFn');
2221
+ });
2222
+
2223
+ it('flags functions with high estimated bugs', () => {
2224
+ const files = [
2225
+ makeFileEntry({
2226
+ functions: [
2227
+ makeFn({
2228
+ halstead: {
2229
+ operators: 50,
2230
+ operands: 100,
2231
+ distinctOperators: 15,
2232
+ distinctOperands: 25,
2233
+ vocabulary: 40,
2234
+ length: 150,
2235
+ volume: 10000,
2236
+ difficulty: 10,
2237
+ effort: 100_000,
2238
+ time: 5556,
2239
+ estimatedBugs: 3.5,
2240
+ },
2241
+ }),
2242
+ ],
2243
+ }),
2244
+ ];
2245
+ const findings = detectHighHalsteadEffort(files, 500_000, 2.0);
2246
+ expect(findings.length).toBe(1);
2247
+ expect(findings[0].reason).toContain('estimatedBugs');
2248
+ });
2249
+
2250
+ it('skips functions without halstead data', () => {
2251
+ const files = [makeFileEntry({ functions: [makeFn()] })];
2252
+ expect(detectHighHalsteadEffort(files)).toEqual([]);
2253
+ });
2254
+ });
2255
+
2256
+ describe('detectLowMaintainability', () => {
2257
+ it('returns empty for functions above threshold', () => {
2258
+ const files = [
2259
+ makeFileEntry({ functions: [makeFn({ maintainabilityIndex: 50 })] }),
2260
+ ];
2261
+ expect(detectLowMaintainability(files, 20)).toEqual([]);
2262
+ });
2263
+
2264
+ it('flags functions below threshold', () => {
2265
+ const files = [
2266
+ makeFileEntry({
2267
+ functions: [makeFn({ maintainabilityIndex: 15, name: 'hardFn' })],
2268
+ }),
2269
+ ];
2270
+ const findings = detectLowMaintainability(files, 20);
2271
+ expect(findings.length).toBe(1);
2272
+ expect(findings[0].category).toBe('low-maintainability');
2273
+ expect(findings[0].title).toContain('hardFn');
2274
+ });
2275
+
2276
+ it('assigns critical severity for MI < 10', () => {
2277
+ const files = [
2278
+ makeFileEntry({ functions: [makeFn({ maintainabilityIndex: 5 })] }),
2279
+ ];
2280
+ const findings = detectLowMaintainability(files, 20);
2281
+ expect(findings[0].severity).toBe('critical');
2282
+ });
2283
+
2284
+ it('assigns high severity for MI 10-19', () => {
2285
+ const files = [
2286
+ makeFileEntry({ functions: [makeFn({ maintainabilityIndex: 15 })] }),
2287
+ ];
2288
+ const findings = detectLowMaintainability(files, 20);
2289
+ expect(findings[0].severity).toBe('high');
2290
+ });
2291
+
2292
+ it('skips functions without maintainability index', () => {
2293
+ const files = [makeFileEntry({ functions: [makeFn()] })];
2294
+ expect(detectLowMaintainability(files, 20)).toEqual([]);
2295
+ });
2296
+ });
2297
+
2298
+ describe('computeAbstractness', () => {
2299
+ it('returns 0 for file with no type exports', () => {
2300
+ expect(
2301
+ computeAbstractness([
2302
+ { name: 'foo', kind: 'value' },
2303
+ { name: 'bar', kind: 'value' },
2304
+ ])
2305
+ ).toBe(0);
2306
+ });
2307
+
2308
+ it('returns 1 for file with only type exports', () => {
2309
+ expect(
2310
+ computeAbstractness([
2311
+ { name: 'Foo', kind: 'type' },
2312
+ { name: 'Bar', kind: 'type' },
2313
+ ])
2314
+ ).toBe(1);
2315
+ });
2316
+
2317
+ it('returns 0.5 for equal mix', () => {
2318
+ expect(
2319
+ computeAbstractness([
2320
+ { name: 'Foo', kind: 'type' },
2321
+ { name: 'foo', kind: 'value' },
2322
+ ])
2323
+ ).toBe(0.5);
2324
+ });
2325
+
2326
+ it('returns 0 for empty exports', () => {
2327
+ expect(computeAbstractness([])).toBe(0);
2328
+ });
2329
+ });
2330
+
2331
+ describe('detectDistanceFromMainSequence', () => {
2332
+ it('returns empty for no files', () => {
2333
+ expect(detectDistanceFromMainSequence(emptyState())).toEqual([]);
2334
+ });
2335
+
2336
+ it('flags files in Zone of Pain (concrete + stable)', () => {
2337
+ const state = emptyState();
2338
+ const hub = 'src/hub.ts';
2339
+ state.files.add(hub);
2340
+ state.declaredExportsByFile.set(hub, [
2341
+ { name: 'fn1', kind: 'value' },
2342
+ { name: 'fn2', kind: 'value' },
2343
+ { name: 'fn3', kind: 'value' },
2344
+ ]);
2345
+ for (let i = 0; i < 10; i++) {
2346
+ const f = `src/dep${i}.ts`;
2347
+ state.files.add(f);
2348
+ addEdge(state, f, hub);
2349
+ }
2350
+ const findings = detectDistanceFromMainSequence(state);
2351
+ const hubFinding = findings.find(f => f.file === hub);
2352
+ expect(hubFinding).toBeDefined();
2353
+ expect(hubFinding!.category).toBe('distance-from-main-sequence');
2354
+ expect(hubFinding!.reason).toContain('Zone of Pain');
2355
+ });
2356
+
2357
+ it('does not flag files on the main sequence', () => {
2358
+ const state = emptyState();
2359
+ state.files.add('src/a.ts');
2360
+ state.files.add('src/b.ts');
2361
+ state.declaredExportsByFile.set('src/a.ts', [
2362
+ { name: 'MyType', kind: 'type' },
2363
+ ]);
2364
+ addEdge(state, 'src/b.ts', 'src/a.ts');
2365
+ const findings = detectDistanceFromMainSequence(state);
2366
+ expect(findings).toEqual([]);
2367
+ });
2368
+
2369
+ it('flags files in Zone of Uselessness (abstract + unstable)', () => {
2370
+ const state = emptyState();
2371
+ const file = 'src/unused-abstractions.ts';
2372
+ state.files.add(file);
2373
+ state.declaredExportsByFile.set(file, [
2374
+ { name: 'IFoo', kind: 'type' },
2375
+ { name: 'IBar', kind: 'type' },
2376
+ { name: 'IBaz', kind: 'type' },
2377
+ ]);
2378
+ for (let i = 0; i < 8; i++) {
2379
+ const dep = `src/dep${i}.ts`;
2380
+ state.files.add(dep);
2381
+ addEdge(state, file, dep);
2382
+ }
2383
+ const findings = detectDistanceFromMainSequence(state);
2384
+ const f = findings.find(f => f.file === file);
2385
+ expect(f).toBeDefined();
2386
+ expect(f!.reason).toContain('Zone of Uselessness');
2387
+ });
2388
+
2389
+ it('skips test files', () => {
2390
+ const state = emptyState();
2391
+ state.files.add('src/a.test.ts');
2392
+ state.declaredExportsByFile.set('src/a.test.ts', [
2393
+ { name: 'fn', kind: 'value' },
2394
+ ]);
2395
+ expect(detectDistanceFromMainSequence(state)).toEqual([]);
2396
+ });
2397
+
2398
+ it('skips files with no exports', () => {
2399
+ const state = emptyState();
2400
+ state.files.add('src/empty.ts');
2401
+ expect(detectDistanceFromMainSequence(state)).toEqual([]);
2402
+ });
2403
+ });
2404
+
2405
+ describe('detectFeatureEnvy', () => {
2406
+ it('returns empty when no envy detected', () => {
2407
+ const state = emptyState();
2408
+ state.files.add('src/a.ts');
2409
+ state.importedSymbolsByFile.set('src/a.ts', [
2410
+ {
2411
+ sourceModule: './b',
2412
+ resolvedModule: 'src/b.ts',
2413
+ importedName: 'x',
2414
+ localName: 'x',
2415
+ isTypeOnly: false,
2416
+ },
2417
+ {
2418
+ sourceModule: './c',
2419
+ resolvedModule: 'src/c.ts',
2420
+ importedName: 'y',
2421
+ localName: 'y',
2422
+ isTypeOnly: false,
2423
+ },
2424
+ ]);
2425
+ expect(detectFeatureEnvy(state)).toEqual([]);
2426
+ });
2427
+
2428
+ it('flags file that imports many symbols from single target', () => {
2429
+ const state = emptyState();
2430
+ state.files.add('src/envious.ts');
2431
+ state.files.add('src/target.ts');
2432
+ const imports = Array.from({ length: 8 }, (_, i) => ({
2433
+ sourceModule: './target',
2434
+ resolvedModule: 'src/target.ts',
2435
+ importedName: `sym${i}`,
2436
+ localName: `sym${i}`,
2437
+ isTypeOnly: false,
2438
+ }));
2439
+ imports.push({
2440
+ sourceModule: './other',
2441
+ resolvedModule: 'src/other.ts',
2442
+ importedName: 'z',
2443
+ localName: 'z',
2444
+ isTypeOnly: false,
2445
+ });
2446
+ state.importedSymbolsByFile.set('src/envious.ts', imports);
2447
+ const findings = detectFeatureEnvy(state);
2448
+ expect(findings.length).toBe(1);
2449
+ expect(findings[0].category).toBe('feature-envy');
2450
+ expect(findings[0].file).toBe('src/envious.ts');
2451
+ expect(findings[0].reason).toContain('src/target.ts');
2452
+ });
2453
+
2454
+ it('does not flag when imports are spread evenly', () => {
2455
+ const state = emptyState();
2456
+ state.files.add('src/balanced.ts');
2457
+ const imports = [];
2458
+ for (let m = 0; m < 5; m++) {
2459
+ for (let s = 0; s < 2; s++) {
2460
+ imports.push({
2461
+ sourceModule: `./mod${m}`,
2462
+ resolvedModule: `src/mod${m}.ts`,
2463
+ importedName: `fn${s}`,
2464
+ localName: `fn${s}`,
2465
+ isTypeOnly: false,
2466
+ });
2467
+ }
2468
+ }
2469
+ state.importedSymbolsByFile.set('src/balanced.ts', imports);
2470
+ expect(detectFeatureEnvy(state)).toEqual([]);
2471
+ });
2472
+
2473
+ it('skips test files', () => {
2474
+ const state = emptyState();
2475
+ state.files.add('src/a.test.ts');
2476
+ const imports = Array.from({ length: 10 }, (_, i) => ({
2477
+ sourceModule: './target',
2478
+ resolvedModule: 'src/target.ts',
2479
+ importedName: `sym${i}`,
2480
+ localName: `sym${i}`,
2481
+ isTypeOnly: false,
2482
+ }));
2483
+ state.importedSymbolsByFile.set('src/a.test.ts', imports);
2484
+ expect(detectFeatureEnvy(state)).toEqual([]);
2485
+ });
2486
+
2487
+ it('requires minimum total imports', () => {
2488
+ const state = emptyState();
2489
+ state.files.add('src/small.ts');
2490
+ state.importedSymbolsByFile.set('src/small.ts', [
2491
+ {
2492
+ sourceModule: './target',
2493
+ resolvedModule: 'src/target.ts',
2494
+ importedName: 'x',
2495
+ localName: 'x',
2496
+ isTypeOnly: false,
2497
+ },
2498
+ ]);
2499
+ expect(detectFeatureEnvy(state)).toEqual([]);
2500
+ });
2501
+ });
2502
+
2503
+ describe('detectUntestedCriticalCode', () => {
2504
+ it('returns empty when no hot files provided', () => {
2505
+ const state = emptyState();
2506
+ const findings = detectUntestedCriticalCode(state, [], new Map());
2507
+ expect(findings).toEqual([]);
2508
+ });
2509
+
2510
+ it('flags hot file with no test imports', () => {
2511
+ const state = emptyState();
2512
+ state.files.add('src/core.ts');
2513
+ state.incomingFromTests.set('src/core.ts', new Set());
2514
+ const hotFiles = [
2515
+ {
2516
+ file: 'src/core.ts',
2517
+ riskScore: 50,
2518
+ fanIn: 10,
2519
+ fanOut: 5,
2520
+ complexityScore: 20,
2521
+ exportCount: 8,
2522
+ inCycle: false,
2523
+ onCriticalPath: true,
2524
+ },
2525
+ ];
2526
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2527
+ expect(findings.length).toBe(1);
2528
+ expect(findings[0].category).toBe('untested-critical-code');
2529
+ expect(findings[0].file).toBe('src/core.ts');
2530
+ expect(findings[0].severity).toBe('high');
2531
+ expect(findings[0].tags).toContain('testing');
2532
+ expect(findings[0].tags).toContain('coverage');
2533
+ });
2534
+
2535
+ it('does not flag hot file that has test imports', () => {
2536
+ const state = emptyState();
2537
+ state.files.add('src/core.ts');
2538
+ state.incomingFromTests.set('src/core.ts', new Set(['src/core.test.ts']));
2539
+ const hotFiles = [
2540
+ {
2541
+ file: 'src/core.ts',
2542
+ riskScore: 50,
2543
+ fanIn: 10,
2544
+ fanOut: 5,
2545
+ complexityScore: 20,
2546
+ exportCount: 8,
2547
+ inCycle: false,
2548
+ onCriticalPath: true,
2549
+ },
2550
+ ];
2551
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2552
+ expect(findings).toEqual([]);
2553
+ });
2554
+
2555
+ it('flags critical severity for in-cycle + high risk + untested', () => {
2556
+ const state = emptyState();
2557
+ state.files.add('src/hub.ts');
2558
+ const hotFiles = [
2559
+ {
2560
+ file: 'src/hub.ts',
2561
+ riskScore: 80,
2562
+ fanIn: 20,
2563
+ fanOut: 10,
2564
+ complexityScore: 40,
2565
+ exportCount: 15,
2566
+ inCycle: true,
2567
+ onCriticalPath: true,
2568
+ },
2569
+ ];
2570
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2571
+ expect(findings.length).toBe(1);
2572
+ expect(findings[0].severity).toBe('critical');
2573
+ });
2574
+
2575
+ it('flags high-complexity files on critical paths even if not in hotFiles list', () => {
2576
+ const state = emptyState();
2577
+ state.files.add('src/deep.ts');
2578
+ const critMap = new Map<string, FileCriticality>([
2579
+ [
2580
+ 'src/deep.ts',
2581
+ {
2582
+ file: 'src/deep.ts',
2583
+ complexityRisk: 50,
2584
+ highComplexityFunctions: 3,
2585
+ functionCount: 5,
2586
+ flows: 2,
2587
+ score: 60,
2588
+ },
2589
+ ],
2590
+ ]);
2591
+ const findings = detectUntestedCriticalCode(state, [], critMap);
2592
+ expect(findings.length).toBe(1);
2593
+ expect(findings[0].file).toBe('src/deep.ts');
2594
+ expect(findings[0].reason).toContain('complexity');
2595
+ });
2596
+
2597
+ it('skips test files themselves', () => {
2598
+ const state = emptyState();
2599
+ state.files.add('src/core.test.ts');
2600
+ const hotFiles = [
2601
+ {
2602
+ file: 'src/core.test.ts',
2603
+ riskScore: 50,
2604
+ fanIn: 10,
2605
+ fanOut: 5,
2606
+ complexityScore: 20,
2607
+ exportCount: 8,
2608
+ inCycle: false,
2609
+ onCriticalPath: true,
2610
+ },
2611
+ ];
2612
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2613
+ expect(findings).toEqual([]);
2614
+ });
2615
+
2616
+ it('deduplicates files appearing in both hotFiles and criticality map', () => {
2617
+ const state = emptyState();
2618
+ state.files.add('src/shared.ts');
2619
+ const hotFiles = [
2620
+ {
2621
+ file: 'src/shared.ts',
2622
+ riskScore: 50,
2623
+ fanIn: 10,
2624
+ fanOut: 5,
2625
+ complexityScore: 20,
2626
+ exportCount: 8,
2627
+ inCycle: false,
2628
+ onCriticalPath: false,
2629
+ },
2630
+ ];
2631
+ const critMap = new Map<string, FileCriticality>([
2632
+ [
2633
+ 'src/shared.ts',
2634
+ {
2635
+ file: 'src/shared.ts',
2636
+ complexityRisk: 50,
2637
+ highComplexityFunctions: 3,
2638
+ functionCount: 5,
2639
+ flows: 2,
2640
+ score: 60,
2641
+ },
2642
+ ],
2643
+ ]);
2644
+ const findings = detectUntestedCriticalCode(state, hotFiles, critMap);
2645
+ expect(findings.length).toBe(1);
2646
+ });
2647
+
2648
+ it('includes risk details in reason', () => {
2649
+ const state = emptyState();
2650
+ state.files.add('src/risky.ts');
2651
+ const hotFiles = [
2652
+ {
2653
+ file: 'src/risky.ts',
2654
+ riskScore: 60,
2655
+ fanIn: 15,
2656
+ fanOut: 8,
2657
+ complexityScore: 30,
2658
+ exportCount: 12,
2659
+ inCycle: true,
2660
+ onCriticalPath: false,
2661
+ },
2662
+ ];
2663
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2664
+ expect(findings[0].reason).toContain('risk');
2665
+ expect(findings[0].reason).toContain('60');
2666
+ });
2667
+
2668
+ it('limits output to top 25 findings', () => {
2669
+ const state = emptyState();
2670
+ const hotFiles = Array.from({ length: 40 }, (_, i) => {
2671
+ const file = `src/mod${i}.ts`;
2672
+ state.files.add(file);
2673
+ return {
2674
+ file,
2675
+ riskScore: 50,
2676
+ fanIn: 10,
2677
+ fanOut: 5,
2678
+ complexityScore: 20,
2679
+ exportCount: 8,
2680
+ inCycle: false,
2681
+ onCriticalPath: true,
2682
+ };
2683
+ });
2684
+ const findings = detectUntestedCriticalCode(state, hotFiles, new Map());
2685
+ expect(findings.length).toBeLessThanOrEqual(25);
2686
+ });
2687
+ });
2688
+
2689
+ describe('detectNamespaceImport', () => {
2690
+ it('flags import * as X from internal module', () => {
2691
+ const state = emptyState();
2692
+ state.files.add('src/consumer.ts');
2693
+ state.files.add('src/utils.ts');
2694
+ state.importedSymbolsByFile.set('src/consumer.ts', [
2695
+ {
2696
+ sourceModule: './utils',
2697
+ resolvedModule: 'src/utils.ts',
2698
+ importedName: '*',
2699
+ localName: 'utils',
2700
+ isTypeOnly: false,
2701
+ lineStart: 1,
2702
+ lineEnd: 1,
2703
+ },
2704
+ ]);
2705
+ const findings = detectNamespaceImport(state);
2706
+ expect(findings.length).toBe(1);
2707
+ expect(findings[0].category).toBe('namespace-import');
2708
+ expect(findings[0].title).toContain('import * as utils');
2709
+ });
2710
+
2711
+ it('flags import * as X from external module', () => {
2712
+ const state = emptyState();
2713
+ state.files.add('src/app.ts');
2714
+ state.importedSymbolsByFile.set('src/app.ts', [
2715
+ {
2716
+ sourceModule: 'lodash',
2717
+ importedName: '*',
2718
+ localName: 'lodash',
2719
+ isTypeOnly: false,
2720
+ lineStart: 3,
2721
+ lineEnd: 3,
2722
+ },
2723
+ ]);
2724
+ const findings = detectNamespaceImport(state);
2725
+ expect(findings.length).toBe(1);
2726
+ expect(findings[0].severity).toBe('medium');
2727
+ });
2728
+
2729
+ it('skips type-only namespace imports', () => {
2730
+ const state = emptyState();
2731
+ state.files.add('src/consumer.ts');
2732
+ state.importedSymbolsByFile.set('src/consumer.ts', [
2733
+ {
2734
+ sourceModule: './types',
2735
+ resolvedModule: 'src/types.ts',
2736
+ importedName: '*',
2737
+ localName: 'T',
2738
+ isTypeOnly: true,
2739
+ lineStart: 1,
2740
+ lineEnd: 1,
2741
+ },
2742
+ ]);
2743
+ const findings = detectNamespaceImport(state);
2744
+ expect(findings.length).toBe(0);
2745
+ });
2746
+
2747
+ it('skips require() calls (handled by CJS detector)', () => {
2748
+ const state = emptyState();
2749
+ state.files.add('src/app.ts');
2750
+ state.importedSymbolsByFile.set('src/app.ts', [
2751
+ {
2752
+ sourceModule: 'fs',
2753
+ importedName: '*',
2754
+ localName: 'require',
2755
+ isTypeOnly: false,
2756
+ lineStart: 1,
2757
+ lineEnd: 1,
2758
+ },
2759
+ ]);
2760
+ const findings = detectNamespaceImport(state);
2761
+ expect(findings.length).toBe(0);
2762
+ });
2763
+
2764
+ it('skips test files', () => {
2765
+ const state = emptyState();
2766
+ state.files.add('src/app.test.ts');
2767
+ state.importedSymbolsByFile.set('src/app.test.ts', [
2768
+ {
2769
+ sourceModule: './utils',
2770
+ importedName: '*',
2771
+ localName: 'utils',
2772
+ isTypeOnly: false,
2773
+ lineStart: 1,
2774
+ lineEnd: 1,
2775
+ },
2776
+ ]);
2777
+ const findings = detectNamespaceImport(state);
2778
+ expect(findings.length).toBe(0);
2779
+ });
2780
+
2781
+ it('escalates severity for high-fan-in internal targets', () => {
2782
+ const state = emptyState();
2783
+ state.files.add('src/consumer.ts');
2784
+ state.files.add('src/shared.ts');
2785
+ state.importedSymbolsByFile.set('src/consumer.ts', [
2786
+ {
2787
+ sourceModule: './shared',
2788
+ resolvedModule: 'src/shared.ts',
2789
+ importedName: '*',
2790
+ localName: 'shared',
2791
+ isTypeOnly: false,
2792
+ lineStart: 2,
2793
+ lineEnd: 2,
2794
+ },
2795
+ ]);
2796
+ const incomingSet = new Set([
2797
+ 'a.ts',
2798
+ 'b.ts',
2799
+ 'c.ts',
2800
+ 'd.ts',
2801
+ 'e.ts',
2802
+ 'f.ts',
2803
+ ]);
2804
+ state.incoming.set('src/shared.ts', incomingSet);
2805
+ const findings = detectNamespaceImport(state);
2806
+ expect(findings.length).toBe(1);
2807
+ expect(findings[0].severity).toBe('high');
2808
+ });
2809
+ });
2810
+
2811
+ describe('detectCommonJsInEsm', () => {
2812
+ it('flags require() as commonjs-in-esm when file has no ESM imports', () => {
2813
+ const state = emptyState();
2814
+ state.files.add('src/legacy.ts');
2815
+ state.importedSymbolsByFile.set('src/legacy.ts', [
2816
+ {
2817
+ sourceModule: 'fs',
2818
+ importedName: '*',
2819
+ localName: 'require',
2820
+ isTypeOnly: false,
2821
+ lineStart: 1,
2822
+ lineEnd: 1,
2823
+ },
2824
+ ]);
2825
+ const findings = detectCommonJsInEsm(state);
2826
+ expect(findings.length).toBe(1);
2827
+ expect(findings[0].category).toBe('commonjs-in-esm');
2828
+ expect(findings[0].severity).toBe('medium');
2829
+ });
2830
+
2831
+ it('flags require() as mixed-module-format when file also has ESM imports', () => {
2832
+ const state = emptyState();
2833
+ state.files.add('src/hybrid.ts');
2834
+ state.importedSymbolsByFile.set('src/hybrid.ts', [
2835
+ {
2836
+ sourceModule: 'path',
2837
+ importedName: 'default',
2838
+ localName: 'path',
2839
+ isTypeOnly: false,
2840
+ lineStart: 1,
2841
+ lineEnd: 1,
2842
+ },
2843
+ {
2844
+ sourceModule: 'fs',
2845
+ importedName: '*',
2846
+ localName: 'require',
2847
+ isTypeOnly: false,
2848
+ lineStart: 2,
2849
+ lineEnd: 2,
2850
+ },
2851
+ ]);
2852
+ const findings = detectCommonJsInEsm(state);
2853
+ expect(findings.length).toBe(1);
2854
+ expect(findings[0].category).toBe('mixed-module-format');
2855
+ expect(findings[0].severity).toBe('high');
2856
+ });
2857
+
2858
+ it('skips test files', () => {
2859
+ const state = emptyState();
2860
+ state.files.add('src/app.test.ts');
2861
+ state.importedSymbolsByFile.set('src/app.test.ts', [
2862
+ {
2863
+ sourceModule: 'fs',
2864
+ importedName: '*',
2865
+ localName: 'require',
2866
+ isTypeOnly: false,
2867
+ lineStart: 1,
2868
+ lineEnd: 1,
2869
+ },
2870
+ ]);
2871
+ const findings = detectCommonJsInEsm(state);
2872
+ expect(findings.length).toBe(0);
2873
+ });
2874
+
2875
+ it('skips files with only named ESM imports', () => {
2876
+ const state = emptyState();
2877
+ state.files.add('src/clean.ts');
2878
+ state.importedSymbolsByFile.set('src/clean.ts', [
2879
+ {
2880
+ sourceModule: 'path',
2881
+ importedName: 'join',
2882
+ localName: 'join',
2883
+ isTypeOnly: false,
2884
+ lineStart: 1,
2885
+ lineEnd: 1,
2886
+ },
2887
+ ]);
2888
+ const findings = detectCommonJsInEsm(state);
2889
+ expect(findings.length).toBe(0);
2890
+ });
2891
+
2892
+ it('flags multiple require() calls separately', () => {
2893
+ const state = emptyState();
2894
+ state.files.add('src/legacy.ts');
2895
+ state.importedSymbolsByFile.set('src/legacy.ts', [
2896
+ {
2897
+ sourceModule: 'fs',
2898
+ importedName: '*',
2899
+ localName: 'require',
2900
+ isTypeOnly: false,
2901
+ lineStart: 1,
2902
+ lineEnd: 1,
2903
+ },
2904
+ {
2905
+ sourceModule: 'path',
2906
+ importedName: '*',
2907
+ localName: 'require',
2908
+ isTypeOnly: false,
2909
+ lineStart: 2,
2910
+ lineEnd: 2,
2911
+ },
2912
+ ]);
2913
+ const findings = detectCommonJsInEsm(state);
2914
+ expect(findings.length).toBe(2);
2915
+ });
2916
+ });
2917
+
2918
+ describe('detectExportStarLeak', () => {
2919
+ it('flags export * from internal module', () => {
2920
+ const state = emptyState();
2921
+ state.files.add('src/index.ts');
2922
+ state.files.add('src/utils.ts');
2923
+ state.reExportsByFile.set('src/index.ts', [
2924
+ {
2925
+ sourceModule: './utils',
2926
+ resolvedModule: 'src/utils.ts',
2927
+ exportedAs: '*',
2928
+ importedName: '*',
2929
+ isStar: true,
2930
+ isTypeOnly: false,
2931
+ lineStart: 1,
2932
+ lineEnd: 1,
2933
+ },
2934
+ ]);
2935
+ state.declaredExportsByFile.set('src/utils.ts', [
2936
+ { name: 'foo', kind: 'value' },
2937
+ { name: 'bar', kind: 'value' },
2938
+ ]);
2939
+ const findings = detectExportStarLeak(state);
2940
+ expect(findings.length).toBe(1);
2941
+ expect(findings[0].category).toBe('export-star-leak');
2942
+ expect(findings[0].reason).toContain('2 symbols');
2943
+ });
2944
+
2945
+ it('escalates severity when target has many exports', () => {
2946
+ const state = emptyState();
2947
+ state.files.add('src/barrel.ts');
2948
+ state.files.add('src/large.ts');
2949
+ state.reExportsByFile.set('src/barrel.ts', [
2950
+ {
2951
+ sourceModule: './large',
2952
+ resolvedModule: 'src/large.ts',
2953
+ exportedAs: '*',
2954
+ importedName: '*',
2955
+ isStar: true,
2956
+ isTypeOnly: false,
2957
+ lineStart: 1,
2958
+ lineEnd: 1,
2959
+ },
2960
+ ]);
2961
+ const exports = Array.from({ length: 25 }, (_, i) => ({
2962
+ name: `fn${i}`,
2963
+ kind: 'value' as const,
2964
+ }));
2965
+ state.declaredExportsByFile.set('src/large.ts', exports);
2966
+ const findings = detectExportStarLeak(state);
2967
+ expect(findings.length).toBe(1);
2968
+ expect(findings[0].severity).toBe('high');
2969
+ });
2970
+
2971
+ it('escalates severity for chained export-star', () => {
2972
+ const state = emptyState();
2973
+ state.files.add('src/barrel.ts');
2974
+ state.files.add('src/sub-barrel.ts');
2975
+ state.reExportsByFile.set('src/barrel.ts', [
2976
+ {
2977
+ sourceModule: './sub-barrel',
2978
+ resolvedModule: 'src/sub-barrel.ts',
2979
+ exportedAs: '*',
2980
+ importedName: '*',
2981
+ isStar: true,
2982
+ isTypeOnly: false,
2983
+ lineStart: 1,
2984
+ lineEnd: 1,
2985
+ },
2986
+ ]);
2987
+ state.reExportsByFile.set('src/sub-barrel.ts', [
2988
+ {
2989
+ sourceModule: './deep',
2990
+ resolvedModule: 'src/deep.ts',
2991
+ exportedAs: '*',
2992
+ importedName: '*',
2993
+ isStar: true,
2994
+ isTypeOnly: false,
2995
+ lineStart: 1,
2996
+ lineEnd: 1,
2997
+ },
2998
+ ]);
2999
+ const findings = detectExportStarLeak(state);
3000
+ const barrelFinding = findings.find(f => f.file === 'src/barrel.ts');
3001
+ expect(barrelFinding).toBeDefined();
3002
+ expect(barrelFinding!.severity).toBe('high');
3003
+ expect(barrelFinding!.reason).toContain('chains');
3004
+ });
3005
+
3006
+ it('skips type-only export *', () => {
3007
+ const state = emptyState();
3008
+ state.files.add('src/index.ts');
3009
+ state.reExportsByFile.set('src/index.ts', [
3010
+ {
3011
+ sourceModule: './types',
3012
+ resolvedModule: 'src/types.ts',
3013
+ exportedAs: '*',
3014
+ importedName: '*',
3015
+ isStar: true,
3016
+ isTypeOnly: true,
3017
+ lineStart: 1,
3018
+ lineEnd: 1,
3019
+ },
3020
+ ]);
3021
+ const findings = detectExportStarLeak(state);
3022
+ expect(findings.length).toBe(0);
3023
+ });
3024
+
3025
+ it('skips test files', () => {
3026
+ const state = emptyState();
3027
+ state.files.add('src/test-utils.test.ts');
3028
+ state.reExportsByFile.set('src/test-utils.test.ts', [
3029
+ {
3030
+ sourceModule: './helpers',
3031
+ exportedAs: '*',
3032
+ importedName: '*',
3033
+ isStar: true,
3034
+ isTypeOnly: false,
3035
+ lineStart: 1,
3036
+ lineEnd: 1,
3037
+ },
3038
+ ]);
3039
+ const findings = detectExportStarLeak(state);
3040
+ expect(findings.length).toBe(0);
3041
+ });
3042
+
3043
+ it('flags named re-exports as non-leak', () => {
3044
+ const state = emptyState();
3045
+ state.files.add('src/index.ts');
3046
+ state.reExportsByFile.set('src/index.ts', [
3047
+ {
3048
+ sourceModule: './utils',
3049
+ resolvedModule: 'src/utils.ts',
3050
+ exportedAs: 'foo',
3051
+ importedName: 'foo',
3052
+ isStar: false,
3053
+ isTypeOnly: false,
3054
+ lineStart: 1,
3055
+ lineEnd: 1,
3056
+ },
3057
+ ]);
3058
+ const findings = detectExportStarLeak(state);
3059
+ expect(findings.length).toBe(0);
3060
+ });
3061
+ });