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,1864 @@
1
+ import * as ts from 'typescript';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import {
5
+ analyzeSourceFile,
6
+ buildDependencyCriticality,
7
+ collectMetrics,
8
+ computeHalstead,
9
+ computeMaintainabilityIndex,
10
+ countLinesInNode,
11
+ getFunctionName,
12
+ isFunctionLike,
13
+ } from './ts-analyzer.js';
14
+ import { DEFAULT_OPTS } from '../types/index.js';
15
+
16
+ import type {
17
+ DependencyProfile,
18
+ FileEntry,
19
+ FlowMaps,
20
+ PackageFileSummary,
21
+ TreeEntry,
22
+ } from '../types/index.js';
23
+
24
+ function parse(code: string, fileName = '/repo/src/test.ts'): ts.SourceFile {
25
+ return ts.createSourceFile(fileName, code, ts.ScriptTarget.ESNext, true);
26
+ }
27
+
28
+ function firstStatement(src: ts.SourceFile): ts.Node {
29
+ return src.statements[0];
30
+ }
31
+
32
+ function emptyPackageSummary(): PackageFileSummary {
33
+ return {
34
+ fileCount: 0,
35
+ nodeCount: 0,
36
+ functionCount: 0,
37
+ flowCount: 0,
38
+ kindCounts: {},
39
+ functions: [],
40
+ flows: [],
41
+ };
42
+ }
43
+
44
+ function emptyMaps(): FlowMaps {
45
+ return { flowMap: new Map(), controlMap: new Map() };
46
+ }
47
+
48
+ const emptyProfile: DependencyProfile = {
49
+ internalDependencies: [],
50
+ externalDependencies: [],
51
+ unresolvedDependencies: [],
52
+ declaredExports: [],
53
+ importedSymbols: [],
54
+ reExports: [],
55
+ };
56
+
57
+ const testOpts = { ...DEFAULT_OPTS, root: '/repo', emitTree: false };
58
+
59
+ describe('isFunctionLike', () => {
60
+ it('matches function declarations', () => {
61
+ const src = parse('function foo() {}');
62
+ expect(isFunctionLike(firstStatement(src))).toBe(true);
63
+ });
64
+
65
+ it('matches arrow functions in variable declarations', () => {
66
+ const src = parse('const f = () => {};');
67
+ const decl = (firstStatement(src) as ts.VariableStatement).declarationList
68
+ .declarations[0];
69
+ expect(isFunctionLike(decl.initializer!)).toBe(true);
70
+ });
71
+
72
+ it('matches method declarations in class', () => {
73
+ const src = parse('class A { method() {} }');
74
+ const cls = firstStatement(src) as ts.ClassDeclaration;
75
+ const method = cls.members[0];
76
+ expect(isFunctionLike(method)).toBe(true);
77
+ });
78
+
79
+ it('rejects non-function nodes', () => {
80
+ const src = parse('const x = 1;');
81
+ expect(isFunctionLike(firstStatement(src))).toBe(false);
82
+ });
83
+
84
+ it('matches getters and setters', () => {
85
+ const src = parse(
86
+ 'class A { get val() { return 1; } set val(v: number) {} }'
87
+ );
88
+ const cls = firstStatement(src) as ts.ClassDeclaration;
89
+ expect(isFunctionLike(cls.members[0])).toBe(true);
90
+ expect(isFunctionLike(cls.members[1])).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('getFunctionName', () => {
95
+ it('returns name of function declaration', () => {
96
+ const src = parse('function greet() {}');
97
+ expect(getFunctionName(firstStatement(src), src)).toBe('greet');
98
+ });
99
+
100
+ it('returns variable name for arrow function', () => {
101
+ const src = parse('const handler = () => {};');
102
+ const decl = (firstStatement(src) as ts.VariableStatement).declarationList
103
+ .declarations[0];
104
+ expect(getFunctionName(decl.initializer!, src)).toBe('handler');
105
+ });
106
+
107
+ it('returns <anonymous> for unnamed function expression', () => {
108
+ const src = parse('(function() {})');
109
+ const expr = (firstStatement(src) as ts.ExpressionStatement).expression;
110
+ const paren = (expr as ts.ParenthesizedExpression).expression;
111
+ expect(getFunctionName(paren, src)).toBe('<anonymous>');
112
+ });
113
+ });
114
+
115
+ describe('collectMetrics', () => {
116
+ it('returns base complexity of 1 for empty function', () => {
117
+ const src = parse('function f() {}');
118
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
119
+ const metrics = collectMetrics(fn.body!);
120
+ expect(metrics.complexity).toBe(1);
121
+ expect(metrics.maxBranchDepth).toBe(0);
122
+ expect(metrics.returns).toBe(0);
123
+ });
124
+
125
+ it('increments complexity for if statement', () => {
126
+ const src = parse('function f(x: boolean) { if (x) { return; } }');
127
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
128
+ const metrics = collectMetrics(fn.body!);
129
+ expect(metrics.complexity).toBeGreaterThan(1);
130
+ });
131
+
132
+ it('tracks max branch depth', () => {
133
+ const src = parse(`function f(a: boolean, b: boolean) {
134
+ if (a) { if (b) { return; } }
135
+ }`);
136
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
137
+ const metrics = collectMetrics(fn.body!);
138
+ expect(metrics.maxBranchDepth).toBe(2);
139
+ });
140
+
141
+ it('tracks loop depth', () => {
142
+ const src = parse(`function f() {
143
+ for (let i = 0; i < 10; i++) {
144
+ for (let j = 0; j < 10; j++) {}
145
+ }
146
+ }`);
147
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
148
+ const metrics = collectMetrics(fn.body!);
149
+ expect(metrics.maxLoopDepth).toBe(2);
150
+ expect(metrics.loops).toBe(2);
151
+ });
152
+
153
+ it('counts await expressions', () => {
154
+ const src = parse(
155
+ 'async function f() { await fetch("x"); await fetch("y"); }'
156
+ );
157
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
158
+ const metrics = collectMetrics(fn.body!);
159
+ expect(metrics.awaits).toBe(2);
160
+ });
161
+
162
+ it('counts call expressions', () => {
163
+ const src = parse('function f() { a(); b(); c(); }');
164
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
165
+ const metrics = collectMetrics(fn.body!);
166
+ expect(metrics.calls).toBe(3);
167
+ });
168
+
169
+ it('counts return and throw statements', () => {
170
+ const src = parse(
171
+ 'function f(x: boolean) { if (x) return 1; throw new Error(); }'
172
+ );
173
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
174
+ const metrics = collectMetrics(fn.body!);
175
+ expect(metrics.returns).toBe(2);
176
+ });
177
+
178
+ it('counts logical operators as complexity', () => {
179
+ const src = parse(
180
+ 'function f(a: boolean, b: boolean, c: boolean) { return a && b || c; }'
181
+ );
182
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
183
+ const metrics = collectMetrics(fn.body!);
184
+ expect(metrics.complexity).toBeGreaterThanOrEqual(3);
185
+ });
186
+
187
+ it('handles switch + catch', () => {
188
+ const src = parse(`function f(x: number) {
189
+ switch(x) { case 1: break; case 2: break; }
190
+ try {} catch(e) {}
191
+ }`);
192
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
193
+ const metrics = collectMetrics(fn.body!);
194
+ expect(metrics.complexity).toBeGreaterThan(2);
195
+ });
196
+ });
197
+
198
+ describe('buildDependencyCriticality', () => {
199
+ it('returns score of 1 for null input', () => {
200
+ const result = buildDependencyCriticality(null, testOpts);
201
+ expect(result.score).toBe(1);
202
+ expect(result.functionCount).toBe(0);
203
+ });
204
+
205
+ it('computes score based on complexity and function count', () => {
206
+ const entry: FileEntry = {
207
+ package: 'test',
208
+ file: 'src/a.ts',
209
+ parseEngine: 'typescript',
210
+ nodeCount: 100,
211
+ kindCounts: {},
212
+ functions: [
213
+ {
214
+ kind: 'FunctionDeclaration',
215
+ name: 'f1',
216
+ nameHint: 'f1',
217
+ file: 'src/a.ts',
218
+ lineStart: 1,
219
+ lineEnd: 10,
220
+ columnStart: 1,
221
+ columnEnd: 1,
222
+ statementCount: 5,
223
+ complexity: 10,
224
+ maxBranchDepth: 2,
225
+ maxLoopDepth: 1,
226
+ returns: 1,
227
+ awaits: 0,
228
+ calls: 3,
229
+ loops: 1,
230
+ lengthLines: 10,
231
+ cognitiveComplexity: 5,
232
+ },
233
+ ],
234
+ flows: [
235
+ {
236
+ kind: 'IfStatement',
237
+ file: 'src/a.ts',
238
+ lineStart: 2,
239
+ lineEnd: 4,
240
+ columnStart: 1,
241
+ columnEnd: 1,
242
+ statementCount: 2,
243
+ },
244
+ ],
245
+ dependencyProfile: emptyProfile,
246
+ };
247
+ const result = buildDependencyCriticality(entry, testOpts);
248
+ expect(result.score).toBeGreaterThan(1);
249
+ expect(result.functionCount).toBe(1);
250
+ expect(result.flows).toBe(1);
251
+ });
252
+
253
+ it('counts high complexity functions', () => {
254
+ const entry: FileEntry = {
255
+ package: 'test',
256
+ file: 'src/a.ts',
257
+ parseEngine: 'typescript',
258
+ nodeCount: 100,
259
+ kindCounts: {},
260
+ functions: [
261
+ {
262
+ kind: 'FunctionDeclaration',
263
+ name: 'complex',
264
+ nameHint: 'complex',
265
+ file: 'src/a.ts',
266
+ lineStart: 1,
267
+ lineEnd: 50,
268
+ columnStart: 1,
269
+ columnEnd: 1,
270
+ statementCount: 30,
271
+ complexity: 35,
272
+ maxBranchDepth: 5,
273
+ maxLoopDepth: 3,
274
+ returns: 4,
275
+ awaits: 0,
276
+ calls: 10,
277
+ loops: 3,
278
+ lengthLines: 50,
279
+ cognitiveComplexity: 20,
280
+ },
281
+ ],
282
+ flows: [],
283
+ dependencyProfile: emptyProfile,
284
+ };
285
+ const result = buildDependencyCriticality(entry, testOpts);
286
+ expect(result.highComplexityFunctions).toBe(1);
287
+ });
288
+ });
289
+
290
+ describe('countLinesInNode', () => {
291
+ it('counts lines of single-line node', () => {
292
+ const src = parse('const x = 1;');
293
+ expect(countLinesInNode(src, firstStatement(src))).toBe(1);
294
+ });
295
+
296
+ it('counts lines of multi-line function', () => {
297
+ const src = parse('function f() {\n const x = 1;\n return x;\n}');
298
+ expect(countLinesInNode(src, firstStatement(src))).toBe(4);
299
+ });
300
+ });
301
+
302
+ describe('analyzeSourceFile', () => {
303
+ it('extracts functions from source file', () => {
304
+ const src = parse(
305
+ 'function greet() { return "hi"; }\nconst add = (a: number, b: number) => a + b;'
306
+ );
307
+ const summary = emptyPackageSummary();
308
+ const maps = emptyMaps();
309
+ const trees: TreeEntry[] = [];
310
+ const result = analyzeSourceFile(
311
+ src,
312
+ 'test-pkg',
313
+ summary,
314
+ testOpts,
315
+ maps,
316
+ trees,
317
+ emptyProfile
318
+ );
319
+
320
+ expect(result.functions.length).toBe(2);
321
+ expect(result.functions[0].name).toBe('greet');
322
+ expect(result.package).toBe('test-pkg');
323
+ });
324
+
325
+ it('extracts control flows', () => {
326
+ const src = parse('function f(x: boolean) { if (x) { console.log(x); } }');
327
+ const summary = emptyPackageSummary();
328
+ const maps = emptyMaps();
329
+ const trees: TreeEntry[] = [];
330
+ const result = analyzeSourceFile(
331
+ src,
332
+ 'pkg',
333
+ summary,
334
+ testOpts,
335
+ maps,
336
+ trees,
337
+ emptyProfile
338
+ );
339
+
340
+ expect(result.flows.length).toBeGreaterThan(0);
341
+ expect(result.flows[0].kind).toBe('IfStatement');
342
+ });
343
+
344
+ it('counts nodes', () => {
345
+ const src = parse('const x = 1;\nfunction f() { return 2; }');
346
+ const summary = emptyPackageSummary();
347
+ const result = analyzeSourceFile(
348
+ src,
349
+ 'pkg',
350
+ summary,
351
+ testOpts,
352
+ emptyMaps(),
353
+ [],
354
+ emptyProfile
355
+ );
356
+ expect(result.nodeCount).toBeGreaterThan(0);
357
+ });
358
+
359
+ it('populates kindCounts', () => {
360
+ const src = parse('const x = 1;\nconst y = 2;');
361
+ const summary = emptyPackageSummary();
362
+ const result = analyzeSourceFile(
363
+ src,
364
+ 'pkg',
365
+ summary,
366
+ testOpts,
367
+ emptyMaps(),
368
+ [],
369
+ emptyProfile
370
+ );
371
+ expect(Object.keys(result.kindCounts).length).toBeGreaterThan(0);
372
+ });
373
+
374
+ it('updates package summary stats', () => {
375
+ const src = parse('function f() { return 1; }');
376
+ const summary = emptyPackageSummary();
377
+ analyzeSourceFile(
378
+ src,
379
+ 'pkg',
380
+ summary,
381
+ testOpts,
382
+ emptyMaps(),
383
+ [],
384
+ emptyProfile
385
+ );
386
+ expect(summary.fileCount).toBe(1);
387
+ expect(summary.functionCount).toBe(1);
388
+ });
389
+
390
+ it('adds duplicate functions to flowMap', () => {
391
+ const code = `function bigFn() {
392
+ const a = 1; const b = 2; const c = 3;
393
+ const d = 4; const e = 5; const f = 6;
394
+ return a + b + c + d + e + f;
395
+ }`;
396
+ const src = parse(code);
397
+ const maps = emptyMaps();
398
+ const summary = emptyPackageSummary();
399
+ analyzeSourceFile(
400
+ src,
401
+ 'pkg',
402
+ summary,
403
+ { ...testOpts, minFunctionStatements: 6 },
404
+ maps,
405
+ [],
406
+ emptyProfile
407
+ );
408
+ expect(maps.flowMap.size).toBeGreaterThan(0);
409
+ });
410
+
411
+ it('computes cognitive complexity for functions', () => {
412
+ const code = `function complex(a: boolean, b: boolean) {
413
+ if (a) { if (b) { return 1; } } return 0;
414
+ }`;
415
+ const src = parse(code);
416
+ const summary = emptyPackageSummary();
417
+ const result = analyzeSourceFile(
418
+ src,
419
+ 'pkg',
420
+ summary,
421
+ testOpts,
422
+ emptyMaps(),
423
+ [],
424
+ emptyProfile
425
+ );
426
+ const fn = result.functions.find(f => f.name === 'complex');
427
+ expect(fn?.cognitiveComplexity).toBeGreaterThan(0);
428
+ });
429
+
430
+ it('records param count', () => {
431
+ const src = parse('function f(a: number, b: string, c: boolean) {}');
432
+ const summary = emptyPackageSummary();
433
+ const result = analyzeSourceFile(
434
+ src,
435
+ 'pkg',
436
+ summary,
437
+ testOpts,
438
+ emptyMaps(),
439
+ [],
440
+ emptyProfile
441
+ );
442
+ expect(result.functions[0].params).toBe(3);
443
+ });
444
+
445
+ it('sets declared flag for function declarations', () => {
446
+ const src = parse('function named() {}');
447
+ const summary = emptyPackageSummary();
448
+ const result = analyzeSourceFile(
449
+ src,
450
+ 'pkg',
451
+ summary,
452
+ testOpts,
453
+ emptyMaps(),
454
+ [],
455
+ emptyProfile
456
+ );
457
+ expect(result.functions[0].declared).toBe(true);
458
+ });
459
+
460
+ it('collects empty catch blocks', () => {
461
+ const src = parse('function f() { try { throw 1; } catch(e) {} }');
462
+ const summary = emptyPackageSummary();
463
+ const result = analyzeSourceFile(
464
+ src,
465
+ 'pkg',
466
+ summary,
467
+ testOpts,
468
+ emptyMaps(),
469
+ [],
470
+ emptyProfile
471
+ );
472
+ expect(result.emptyCatches?.length).toBe(1);
473
+ });
474
+
475
+ it('does not flag non-empty catch blocks', () => {
476
+ const src = parse('function f() { try {} catch(e) { console.log(e); } }');
477
+ const summary = emptyPackageSummary();
478
+ const result = analyzeSourceFile(
479
+ src,
480
+ 'pkg',
481
+ summary,
482
+ testOpts,
483
+ emptyMaps(),
484
+ [],
485
+ emptyProfile
486
+ );
487
+ expect(result.emptyCatches?.length).toBe(0);
488
+ });
489
+
490
+ it('collects switches without default', () => {
491
+ const src = parse(
492
+ 'function f(x: number) { switch(x) { case 1: break; case 2: break; } }'
493
+ );
494
+ const summary = emptyPackageSummary();
495
+ const result = analyzeSourceFile(
496
+ src,
497
+ 'pkg',
498
+ summary,
499
+ testOpts,
500
+ emptyMaps(),
501
+ [],
502
+ emptyProfile
503
+ );
504
+ expect(result.switchesWithoutDefault?.length).toBe(1);
505
+ });
506
+
507
+ it('does not flag switches with default', () => {
508
+ const src = parse(
509
+ 'function f(x: number) { switch(x) { case 1: break; default: break; } }'
510
+ );
511
+ const summary = emptyPackageSummary();
512
+ const result = analyzeSourceFile(
513
+ src,
514
+ 'pkg',
515
+ summary,
516
+ testOpts,
517
+ emptyMaps(),
518
+ [],
519
+ emptyProfile
520
+ );
521
+ expect(result.switchesWithoutDefault?.length).toBe(0);
522
+ });
523
+
524
+ it('counts any type annotations', () => {
525
+ const src = parse(
526
+ 'const a: any = 1; const b: any = 2; function f(x: any) {}'
527
+ );
528
+ const summary = emptyPackageSummary();
529
+ const result = analyzeSourceFile(
530
+ src,
531
+ 'pkg',
532
+ summary,
533
+ testOpts,
534
+ emptyMaps(),
535
+ [],
536
+ emptyProfile
537
+ );
538
+ expect(result.anyCount).toBeGreaterThanOrEqual(3);
539
+ });
540
+
541
+ it('collects magic numbers', () => {
542
+ const src = parse(
543
+ 'function f() { let x = 42; let y = 99; return x + y + 300; }'
544
+ );
545
+ const summary = emptyPackageSummary();
546
+ const result = analyzeSourceFile(
547
+ src,
548
+ 'pkg',
549
+ summary,
550
+ testOpts,
551
+ emptyMaps(),
552
+ [],
553
+ emptyProfile
554
+ );
555
+ expect(result.magicNumbers!.length).toBeGreaterThanOrEqual(3);
556
+ });
557
+
558
+ it('excludes 0 and 1 from magic numbers', () => {
559
+ const src = parse('function f() { return 0 + 1; }');
560
+ const summary = emptyPackageSummary();
561
+ const result = analyzeSourceFile(
562
+ src,
563
+ 'pkg',
564
+ summary,
565
+ testOpts,
566
+ emptyMaps(),
567
+ [],
568
+ emptyProfile
569
+ );
570
+ expect(result.magicNumbers?.length).toBe(0);
571
+ });
572
+
573
+ it('excludes const declarations from magic numbers', () => {
574
+ const src = parse('const TIMEOUT = 5000;');
575
+ const summary = emptyPackageSummary();
576
+ const result = analyzeSourceFile(
577
+ src,
578
+ 'pkg',
579
+ summary,
580
+ testOpts,
581
+ emptyMaps(),
582
+ [],
583
+ emptyProfile
584
+ );
585
+ expect(result.magicNumbers?.length).toBe(0);
586
+ });
587
+
588
+ it('computes halstead metrics for functions', () => {
589
+ const src = parse('function f(a: number, b: number) { return a + b * 2; }');
590
+ const summary = emptyPackageSummary();
591
+ const result = analyzeSourceFile(
592
+ src,
593
+ 'pkg',
594
+ summary,
595
+ testOpts,
596
+ emptyMaps(),
597
+ [],
598
+ emptyProfile
599
+ );
600
+ expect(result.functions[0].halstead).toBeDefined();
601
+ expect(result.functions[0].halstead!.volume).toBeGreaterThan(0);
602
+ });
603
+
604
+ it('computes maintainability index for functions', () => {
605
+ const src = parse('function f(a: number) { return a + 1; }');
606
+ const summary = emptyPackageSummary();
607
+ const result = analyzeSourceFile(
608
+ src,
609
+ 'pkg',
610
+ summary,
611
+ testOpts,
612
+ emptyMaps(),
613
+ [],
614
+ emptyProfile
615
+ );
616
+ expect(result.functions[0].maintainabilityIndex).toBeDefined();
617
+ expect(result.functions[0].maintainabilityIndex!).toBeGreaterThan(0);
618
+ });
619
+ });
620
+
621
+ describe('computeHalstead', () => {
622
+ it('returns zeroes for empty body', () => {
623
+ const src = parse('function f() {}');
624
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
625
+ const h = computeHalstead(fn.body!);
626
+ expect(h.length).toBe(0);
627
+ expect(h.volume).toBe(0);
628
+ expect(h.effort).toBe(0);
629
+ });
630
+
631
+ it('counts operators and operands for simple expression', () => {
632
+ const src = parse('function f(a: number, b: number) { return a + b; }');
633
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
634
+ const h = computeHalstead(fn.body!);
635
+ expect(h.distinctOperators).toBeGreaterThan(0);
636
+ expect(h.distinctOperands).toBeGreaterThan(0);
637
+ expect(h.volume).toBeGreaterThan(0);
638
+ });
639
+
640
+ it('computes estimated bugs based on volume', () => {
641
+ const src = parse(`function f(x: number) {
642
+ const a = x + 1; const b = x - 2; const c = a * b;
643
+ return c / x + a - b;
644
+ }`);
645
+ const fn = firstStatement(src) as ts.FunctionDeclaration;
646
+ const h = computeHalstead(fn.body!);
647
+ expect(h.estimatedBugs).toBeGreaterThan(0);
648
+ expect(h.estimatedBugs).toBe(h.volume / 3000);
649
+ });
650
+
651
+ it('difficulty increases with repeated operands', () => {
652
+ const simpleCode = 'function f() { const a = 1; return a; }';
653
+ const repetitiveCode =
654
+ 'function f() { const a = 1; const b = a; const c = a; const d = a; return a + b + c + d; }';
655
+ const simple = computeHalstead(
656
+ (firstStatement(parse(simpleCode)) as ts.FunctionDeclaration).body!
657
+ );
658
+ const repetitive = computeHalstead(
659
+ (firstStatement(parse(repetitiveCode)) as ts.FunctionDeclaration).body!
660
+ );
661
+ expect(repetitive.difficulty).toBeGreaterThan(simple.difficulty);
662
+ });
663
+ });
664
+
665
+ describe('computeMaintainabilityIndex', () => {
666
+ it('returns high MI for simple code', () => {
667
+ const mi = computeMaintainabilityIndex(10, 1, 5);
668
+ expect(mi).toBeGreaterThan(50);
669
+ });
670
+
671
+ it('returns low MI for complex code', () => {
672
+ const mi = computeMaintainabilityIndex(50000, 50, 500);
673
+ expect(mi).toBeLessThan(20);
674
+ });
675
+
676
+ it('clamps to 0 minimum', () => {
677
+ const mi = computeMaintainabilityIndex(1e12, 1000, 100000);
678
+ expect(mi).toBe(0);
679
+ });
680
+
681
+ it('returns max ~100 for trivial code', () => {
682
+ const mi = computeMaintainabilityIndex(1, 1, 1);
683
+ expect(mi).toBeGreaterThan(90);
684
+ expect(mi).toBeLessThanOrEqual(100);
685
+ });
686
+ });
687
+
688
+ describe('collectSecurityData (via analyzeSourceFile)', () => {
689
+ it('detects eval() usage', () => {
690
+ const src = parse('function f(s: string) { eval(s); }');
691
+ const result = analyzeSourceFile(
692
+ src,
693
+ 'pkg',
694
+ emptyPackageSummary(),
695
+ testOpts,
696
+ emptyMaps(),
697
+ [],
698
+ emptyProfile
699
+ );
700
+ expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
701
+ });
702
+
703
+ it('detects new Function() usage', () => {
704
+ const src = parse('const fn = new Function("return 1");');
705
+ const result = analyzeSourceFile(
706
+ src,
707
+ 'pkg',
708
+ emptyPackageSummary(),
709
+ testOpts,
710
+ emptyMaps(),
711
+ [],
712
+ emptyProfile
713
+ );
714
+ expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
715
+ });
716
+
717
+ it('detects setTimeout with string arg', () => {
718
+ const src = parse('setTimeout("alert(1)", 100);');
719
+ const result = analyzeSourceFile(
720
+ src,
721
+ 'pkg',
722
+ emptyPackageSummary(),
723
+ testOpts,
724
+ emptyMaps(),
725
+ [],
726
+ emptyProfile
727
+ );
728
+ expect(result.evalUsages!.length).toBeGreaterThanOrEqual(1);
729
+ });
730
+
731
+ it('detects innerHTML assignment', () => {
732
+ const src = parse(
733
+ 'function f(el: HTMLElement) { el.innerHTML = "<b>hi</b>"; }'
734
+ );
735
+ const result = analyzeSourceFile(
736
+ src,
737
+ 'pkg',
738
+ emptyPackageSummary(),
739
+ testOpts,
740
+ emptyMaps(),
741
+ [],
742
+ emptyProfile
743
+ );
744
+ expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
745
+ });
746
+
747
+ it('detects outerHTML assignment', () => {
748
+ const src = parse(
749
+ 'function f(el: HTMLElement) { el.outerHTML = "<div/>"; }'
750
+ );
751
+ const result = analyzeSourceFile(
752
+ src,
753
+ 'pkg',
754
+ emptyPackageSummary(),
755
+ testOpts,
756
+ emptyMaps(),
757
+ [],
758
+ emptyProfile
759
+ );
760
+ expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
761
+ });
762
+
763
+ it('detects dangerouslySetInnerHTML JSX attribute', () => {
764
+ const src = parse(
765
+ '<div dangerouslySetInnerHTML={{ __html: s }} />;',
766
+ '/repo/src/test.tsx'
767
+ );
768
+ const result = analyzeSourceFile(
769
+ src,
770
+ 'pkg',
771
+ emptyPackageSummary(),
772
+ testOpts,
773
+ emptyMaps(),
774
+ [],
775
+ emptyProfile
776
+ );
777
+ expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
778
+ });
779
+
780
+ it('detects document.write call', () => {
781
+ const src = parse('document.write("<h1>Hello</h1>");');
782
+ const result = analyzeSourceFile(
783
+ src,
784
+ 'pkg',
785
+ emptyPackageSummary(),
786
+ testOpts,
787
+ emptyMaps(),
788
+ [],
789
+ emptyProfile
790
+ );
791
+ expect(result.unsafeHtmlAssignments!.length).toBeGreaterThanOrEqual(1);
792
+ });
793
+
794
+ it('detects hardcoded-secret suspicious strings from pattern matches', () => {
795
+ const src = parse("const cfg = `password = 'mysecret123'`;");
796
+ const result = analyzeSourceFile(
797
+ src,
798
+ 'pkg',
799
+ emptyPackageSummary(),
800
+ testOpts,
801
+ emptyMaps(),
802
+ [],
803
+ emptyProfile
804
+ );
805
+ expect(
806
+ result.suspiciousStrings!.some(s => s.kind === 'hardcoded-secret')
807
+ ).toBe(true);
808
+ });
809
+
810
+ it('skips placeholder patterns for secrets', () => {
811
+ const src = parse('const key = "YOUR_API_KEY_HERE";');
812
+ const result = analyzeSourceFile(
813
+ src,
814
+ 'pkg',
815
+ emptyPackageSummary(),
816
+ testOpts,
817
+ emptyMaps(),
818
+ [],
819
+ emptyProfile
820
+ );
821
+ const secrets = result.suspiciousStrings!.filter(
822
+ s => s.kind === 'hardcoded-secret'
823
+ );
824
+ expect(secrets.length).toBe(0);
825
+ });
826
+
827
+ it('detects high-entropy strings as potential secrets', () => {
828
+ const src = parse('const token = "aB3dE7gH9jK1mN5pQ8sT0uW2xY4zA6c";');
829
+ const result = analyzeSourceFile(
830
+ src,
831
+ 'pkg',
832
+ emptyPackageSummary(),
833
+ testOpts,
834
+ emptyMaps(),
835
+ [],
836
+ emptyProfile
837
+ );
838
+ expect(
839
+ result.suspiciousStrings!.some(s => s.kind === 'hardcoded-secret')
840
+ ).toBe(true);
841
+ });
842
+
843
+ it('tags error messages separately from secrets', () => {
844
+ const src = parse(
845
+ 'const msg = "invalid token provided for authentication service endpoint";'
846
+ );
847
+ const result = analyzeSourceFile(
848
+ src,
849
+ 'pkg',
850
+ emptyPackageSummary(),
851
+ testOpts,
852
+ emptyMaps(),
853
+ [],
854
+ emptyProfile
855
+ );
856
+ const secrets = result.suspiciousStrings!.filter(
857
+ s => s.kind === 'hardcoded-secret' && s.context === 'error-message'
858
+ );
859
+ expect(secrets.length).toBeGreaterThanOrEqual(0);
860
+ });
861
+
862
+ it('detects SQL injection risk in template literals', () => {
863
+ const src = parse('const q = `SELECT * FROM users WHERE id = ${userId}`;');
864
+ const result = analyzeSourceFile(
865
+ src,
866
+ 'pkg',
867
+ emptyPackageSummary(),
868
+ testOpts,
869
+ emptyMaps(),
870
+ [],
871
+ emptyProfile
872
+ );
873
+ expect(
874
+ result.suspiciousStrings!.some(s => s.kind === 'sql-injection')
875
+ ).toBe(true);
876
+ });
877
+
878
+ it('does not flag template without SQL keywords', () => {
879
+ const src = parse('const msg = `Hello ${name}`;');
880
+ const result = analyzeSourceFile(
881
+ src,
882
+ 'pkg',
883
+ emptyPackageSummary(),
884
+ testOpts,
885
+ emptyMaps(),
886
+ [],
887
+ emptyProfile
888
+ );
889
+ expect(
890
+ result.suspiciousStrings!.filter(s => s.kind === 'sql-injection').length
891
+ ).toBe(0);
892
+ });
893
+
894
+ it('collects regex literals', () => {
895
+ const src = parse('const re = /^[a-z]+$/;');
896
+ const result = analyzeSourceFile(
897
+ src,
898
+ 'pkg',
899
+ emptyPackageSummary(),
900
+ testOpts,
901
+ emptyMaps(),
902
+ [],
903
+ emptyProfile
904
+ );
905
+ expect(result.regexLiterals!.length).toBeGreaterThanOrEqual(1);
906
+ });
907
+
908
+ it('tags regex patterns that contain secret keywords as regex-definition', () => {
909
+ const code = "const secretRe = /password = 'foo'/i;";
910
+ const src = parse(code);
911
+ const result = analyzeSourceFile(
912
+ src,
913
+ 'pkg',
914
+ emptyPackageSummary(),
915
+ testOpts,
916
+ emptyMaps(),
917
+ [],
918
+ emptyProfile
919
+ );
920
+ const regexDefs = result.suspiciousStrings!.filter(
921
+ s => s.context === 'regex-definition'
922
+ );
923
+ expect(regexDefs.length).toBeGreaterThanOrEqual(1);
924
+ });
925
+
926
+ it('detects type assertion escapes (as any)', () => {
927
+ const src = parse('const x = (value as any).prop;');
928
+ const result = analyzeSourceFile(
929
+ src,
930
+ 'pkg',
931
+ emptyPackageSummary(),
932
+ testOpts,
933
+ emptyMaps(),
934
+ [],
935
+ emptyProfile
936
+ );
937
+ expect(result.typeAssertionEscapes!.asAny.length).toBeGreaterThanOrEqual(1);
938
+ });
939
+
940
+ it('detects double assertion (as unknown as T)', () => {
941
+ const src = parse('const x = (value as unknown as string);');
942
+ const result = analyzeSourceFile(
943
+ src,
944
+ 'pkg',
945
+ emptyPackageSummary(),
946
+ testOpts,
947
+ emptyMaps(),
948
+ [],
949
+ emptyProfile
950
+ );
951
+ expect(
952
+ result.typeAssertionEscapes!.doubleAssertion.length
953
+ ).toBeGreaterThanOrEqual(1);
954
+ });
955
+
956
+ it('detects non-null assertions', () => {
957
+ const src = parse('function f(x?: string) { return x!.length; }');
958
+ const result = analyzeSourceFile(
959
+ src,
960
+ 'pkg',
961
+ emptyPackageSummary(),
962
+ testOpts,
963
+ emptyMaps(),
964
+ [],
965
+ emptyProfile
966
+ );
967
+ expect(result.typeAssertionEscapes!.nonNull.length).toBeGreaterThanOrEqual(
968
+ 1
969
+ );
970
+ });
971
+ });
972
+
973
+ describe('async pattern detection (via analyzeSourceFile)', () => {
974
+ it('detects unprotected async (await without try-catch)', () => {
975
+ const src = parse('async function f() { await fetch("url"); }');
976
+ const result = analyzeSourceFile(
977
+ src,
978
+ 'pkg',
979
+ emptyPackageSummary(),
980
+ testOpts,
981
+ emptyMaps(),
982
+ [],
983
+ emptyProfile
984
+ );
985
+ expect(result.unprotectedAsync?.length).toBeGreaterThanOrEqual(1);
986
+ });
987
+
988
+ it('does not flag async with try-catch as unprotected', () => {
989
+ const src = parse(
990
+ 'async function f() { try { await fetch("url"); } catch(e) { console.error(e); } }'
991
+ );
992
+ const result = analyzeSourceFile(
993
+ src,
994
+ 'pkg',
995
+ emptyPackageSummary(),
996
+ testOpts,
997
+ emptyMaps(),
998
+ [],
999
+ emptyProfile
1000
+ );
1001
+ expect(result.unprotectedAsync?.length).toBe(0);
1002
+ });
1003
+
1004
+ it('does not flag async with .catch chain as unprotected', () => {
1005
+ const src = parse(
1006
+ 'async function f() { await fetch("url").catch(console.error); }'
1007
+ );
1008
+ const result = analyzeSourceFile(
1009
+ src,
1010
+ 'pkg',
1011
+ emptyPackageSummary(),
1012
+ testOpts,
1013
+ emptyMaps(),
1014
+ [],
1015
+ emptyProfile
1016
+ );
1017
+ expect(result.unprotectedAsync?.length).toBe(0);
1018
+ });
1019
+
1020
+ it('skips functions with zero awaits in metrics', () => {
1021
+ const src = parse('async function f() { return 1; }');
1022
+ const result = analyzeSourceFile(
1023
+ src,
1024
+ 'pkg',
1025
+ emptyPackageSummary(),
1026
+ testOpts,
1027
+ emptyMaps(),
1028
+ [],
1029
+ emptyProfile
1030
+ );
1031
+ expect(result.asyncWithoutAwait?.length ?? 0).toBe(0);
1032
+ expect(result.unprotectedAsync?.length ?? 0).toBe(0);
1033
+ });
1034
+ });
1035
+
1036
+ describe('collectPerformanceData (via analyzeSourceFile)', () => {
1037
+ it('detects await inside for loop', () => {
1038
+ const src = parse(`async function f(urls: string[]) {
1039
+ for (const url of urls) { await fetch(url); }
1040
+ }`);
1041
+ const result = analyzeSourceFile(
1042
+ src,
1043
+ 'pkg',
1044
+ emptyPackageSummary(),
1045
+ testOpts,
1046
+ emptyMaps(),
1047
+ [],
1048
+ emptyProfile
1049
+ );
1050
+ expect(result.awaitInLoopLocations!.length).toBeGreaterThanOrEqual(1);
1051
+ });
1052
+
1053
+ it('detects await inside while loop', () => {
1054
+ const src = parse(`async function f() {
1055
+ let i = 0;
1056
+ while (i < 10) { await fetch("url"); i++; }
1057
+ }`);
1058
+ const result = analyzeSourceFile(
1059
+ src,
1060
+ 'pkg',
1061
+ emptyPackageSummary(),
1062
+ testOpts,
1063
+ emptyMaps(),
1064
+ [],
1065
+ emptyProfile
1066
+ );
1067
+ expect(result.awaitInLoopLocations!.length).toBeGreaterThanOrEqual(1);
1068
+ });
1069
+
1070
+ it('does not flag await outside loop', () => {
1071
+ const src = parse('async function f() { await fetch("url"); }');
1072
+ const result = analyzeSourceFile(
1073
+ src,
1074
+ 'pkg',
1075
+ emptyPackageSummary(),
1076
+ testOpts,
1077
+ emptyMaps(),
1078
+ [],
1079
+ emptyProfile
1080
+ );
1081
+ expect(result.awaitInLoopLocations?.length).toBe(0);
1082
+ });
1083
+
1084
+ it('detects sync I/O calls (readFileSync)', () => {
1085
+ const src = parse('function f() { fs.readFileSync("/path", "utf8"); }');
1086
+ const result = analyzeSourceFile(
1087
+ src,
1088
+ 'pkg',
1089
+ emptyPackageSummary(),
1090
+ testOpts,
1091
+ emptyMaps(),
1092
+ [],
1093
+ emptyProfile
1094
+ );
1095
+ expect(result.syncIoCalls!.some(c => c.name === 'readFileSync')).toBe(true);
1096
+ });
1097
+
1098
+ it('detects sync I/O calls (writeFileSync)', () => {
1099
+ const src = parse('function f() { fs.writeFileSync("/path", "data"); }');
1100
+ const result = analyzeSourceFile(
1101
+ src,
1102
+ 'pkg',
1103
+ emptyPackageSummary(),
1104
+ testOpts,
1105
+ emptyMaps(),
1106
+ [],
1107
+ emptyProfile
1108
+ );
1109
+ expect(result.syncIoCalls!.some(c => c.name === 'writeFileSync')).toBe(
1110
+ true
1111
+ );
1112
+ });
1113
+
1114
+ it('detects setInterval timer calls', () => {
1115
+ const src = parse('function f() { setInterval(() => {}, 1000); }');
1116
+ const result = analyzeSourceFile(
1117
+ src,
1118
+ 'pkg',
1119
+ emptyPackageSummary(),
1120
+ testOpts,
1121
+ emptyMaps(),
1122
+ [],
1123
+ emptyProfile
1124
+ );
1125
+ expect(result.timerCalls!.some(t => t.kind === 'setInterval')).toBe(true);
1126
+ });
1127
+
1128
+ it('detects setTimeout timer calls', () => {
1129
+ const src = parse('function f() { setTimeout(() => {}, 500); }');
1130
+ const result = analyzeSourceFile(
1131
+ src,
1132
+ 'pkg',
1133
+ emptyPackageSummary(),
1134
+ testOpts,
1135
+ emptyMaps(),
1136
+ [],
1137
+ emptyProfile
1138
+ );
1139
+ expect(result.timerCalls!.some(t => t.kind === 'setTimeout')).toBe(true);
1140
+ });
1141
+
1142
+ it('marks timer as having cleanup when clearInterval present', () => {
1143
+ const src = parse(`function f() {
1144
+ const id = setInterval(() => {}, 1000);
1145
+ clearInterval(id);
1146
+ }`);
1147
+ const result = analyzeSourceFile(
1148
+ src,
1149
+ 'pkg',
1150
+ emptyPackageSummary(),
1151
+ testOpts,
1152
+ emptyMaps(),
1153
+ [],
1154
+ emptyProfile
1155
+ );
1156
+ expect(
1157
+ result.timerCalls!.some(t => t.kind === 'setInterval' && t.hasCleanup)
1158
+ ).toBe(true);
1159
+ });
1160
+
1161
+ it('marks timer without cleanup', () => {
1162
+ const src = parse('function f() { setInterval(() => {}, 1000); }');
1163
+ const result = analyzeSourceFile(
1164
+ src,
1165
+ 'pkg',
1166
+ emptyPackageSummary(),
1167
+ testOpts,
1168
+ emptyMaps(),
1169
+ [],
1170
+ emptyProfile
1171
+ );
1172
+ expect(
1173
+ result.timerCalls!.some(t => t.kind === 'setInterval' && !t.hasCleanup)
1174
+ ).toBe(true);
1175
+ });
1176
+
1177
+ it('detects addEventListener registrations', () => {
1178
+ const src = parse(
1179
+ 'function f(el: HTMLElement) { el.addEventListener("click", handler); }'
1180
+ );
1181
+ const result = analyzeSourceFile(
1182
+ src,
1183
+ 'pkg',
1184
+ emptyPackageSummary(),
1185
+ testOpts,
1186
+ emptyMaps(),
1187
+ [],
1188
+ emptyProfile
1189
+ );
1190
+ expect(result.listenerRegistrations!.length).toBeGreaterThanOrEqual(1);
1191
+ });
1192
+
1193
+ it('detects removeEventListener calls', () => {
1194
+ const src = parse(
1195
+ 'function f(el: HTMLElement) { el.removeEventListener("click", handler); }'
1196
+ );
1197
+ const result = analyzeSourceFile(
1198
+ src,
1199
+ 'pkg',
1200
+ emptyPackageSummary(),
1201
+ testOpts,
1202
+ emptyMaps(),
1203
+ [],
1204
+ emptyProfile
1205
+ );
1206
+ expect(result.listenerRemovals!.length).toBeGreaterThanOrEqual(1);
1207
+ });
1208
+
1209
+ it('detects .on() and .off() for event listeners', () => {
1210
+ const src = parse(`function f(emitter: any) {
1211
+ emitter.on("data", handler);
1212
+ emitter.off("data", handler);
1213
+ }`);
1214
+ const result = analyzeSourceFile(
1215
+ src,
1216
+ 'pkg',
1217
+ emptyPackageSummary(),
1218
+ testOpts,
1219
+ emptyMaps(),
1220
+ [],
1221
+ emptyProfile
1222
+ );
1223
+ expect(result.listenerRegistrations!.length).toBeGreaterThanOrEqual(1);
1224
+ expect(result.listenerRemovals!.length).toBeGreaterThanOrEqual(1);
1225
+ });
1226
+ });
1227
+
1228
+ describe('collectInputSourceProfile (via analyzeSourceFile)', () => {
1229
+ it('detects function with req parameter as high-confidence input source', () => {
1230
+ const src = parse('function handler(req: any) { eval(req.body); }');
1231
+ const result = analyzeSourceFile(
1232
+ src,
1233
+ 'pkg',
1234
+ emptyPackageSummary(),
1235
+ testOpts,
1236
+ emptyMaps(),
1237
+ [],
1238
+ emptyProfile
1239
+ );
1240
+ expect(result.inputSources!.length).toBeGreaterThanOrEqual(1);
1241
+ expect(result.inputSources![0].paramConfidence).toBe('high');
1242
+ expect(result.inputSources![0].sourceParams).toContain('req');
1243
+ });
1244
+
1245
+ it('detects sinks in function body', () => {
1246
+ const src = parse('function handler(req: any) { eval(req.query); }');
1247
+ const result = analyzeSourceFile(
1248
+ src,
1249
+ 'pkg',
1250
+ emptyPackageSummary(),
1251
+ testOpts,
1252
+ emptyMaps(),
1253
+ [],
1254
+ emptyProfile
1255
+ );
1256
+ expect(result.inputSources![0].hasSinkInBody).toBe(true);
1257
+ expect(result.inputSources![0].sinkKinds).toContain('eval');
1258
+ });
1259
+
1260
+ it('detects validation patterns (typeof check)', () => {
1261
+ const src = parse(
1262
+ 'function handler(req: any) { if (typeof req === "object") { console.log(req); } }'
1263
+ );
1264
+ const result = analyzeSourceFile(
1265
+ src,
1266
+ 'pkg',
1267
+ emptyPackageSummary(),
1268
+ testOpts,
1269
+ emptyMaps(),
1270
+ [],
1271
+ emptyProfile
1272
+ );
1273
+ expect(result.inputSources![0].hasValidation).toBe(true);
1274
+ });
1275
+
1276
+ it('detects validation via schema validators (zod/joi)', () => {
1277
+ const src = parse(
1278
+ 'function handler(body: any) { const result = z.parse(body); }'
1279
+ );
1280
+ const result = analyzeSourceFile(
1281
+ src,
1282
+ 'pkg',
1283
+ emptyPackageSummary(),
1284
+ testOpts,
1285
+ emptyMaps(),
1286
+ [],
1287
+ emptyProfile
1288
+ );
1289
+ expect(result.inputSources![0].hasValidation).toBe(true);
1290
+ });
1291
+
1292
+ it('does not collect input sources for test files', () => {
1293
+ const src = parse(
1294
+ 'function handler(req: any) { eval(req); }',
1295
+ '/repo/src/test.test.ts'
1296
+ );
1297
+ const result = analyzeSourceFile(
1298
+ src,
1299
+ 'pkg',
1300
+ emptyPackageSummary(),
1301
+ testOpts,
1302
+ emptyMaps(),
1303
+ [],
1304
+ emptyProfile
1305
+ );
1306
+ expect(result.inputSources).toBeUndefined();
1307
+ });
1308
+
1309
+ it('detects medium-confidence params (input, event)', () => {
1310
+ const src = parse('function handler(input: any) { console.log(input); }');
1311
+ const result = analyzeSourceFile(
1312
+ src,
1313
+ 'pkg',
1314
+ emptyPackageSummary(),
1315
+ testOpts,
1316
+ emptyMaps(),
1317
+ [],
1318
+ emptyProfile
1319
+ );
1320
+ expect(result.inputSources![0].paramConfidence).toBe('medium');
1321
+ });
1322
+
1323
+ it('tracks calls with input args', () => {
1324
+ const src = parse('function handler(req: any) { processData(req.body); }');
1325
+ const result = analyzeSourceFile(
1326
+ src,
1327
+ 'pkg',
1328
+ emptyPackageSummary(),
1329
+ testOpts,
1330
+ emptyMaps(),
1331
+ [],
1332
+ emptyProfile
1333
+ );
1334
+ expect(
1335
+ result.inputSources![0].callsWithInputArgs.length
1336
+ ).toBeGreaterThanOrEqual(1);
1337
+ });
1338
+
1339
+ it('does not collect for functions without source params', () => {
1340
+ const src = parse('function helper(count: number) { return count + 1; }');
1341
+ const result = analyzeSourceFile(
1342
+ src,
1343
+ 'pkg',
1344
+ emptyPackageSummary(),
1345
+ testOpts,
1346
+ emptyMaps(),
1347
+ [],
1348
+ emptyProfile
1349
+ );
1350
+ expect(result.inputSources?.length ?? 0).toBe(0);
1351
+ });
1352
+ });
1353
+
1354
+ describe('collectTopLevelEffects (via analyzeSourceFile)', () => {
1355
+ it('detects side-effect imports', () => {
1356
+ const src = parse("import './polyfill';");
1357
+ const result = analyzeSourceFile(
1358
+ src,
1359
+ 'pkg',
1360
+ emptyPackageSummary(),
1361
+ testOpts,
1362
+ emptyMaps(),
1363
+ [],
1364
+ emptyProfile
1365
+ );
1366
+ expect(
1367
+ result.topLevelEffects!.some(e => e.kind === 'side-effect-import')
1368
+ ).toBe(true);
1369
+ });
1370
+
1371
+ it('detects top-level setInterval', () => {
1372
+ const src = parse('setInterval(() => {}, 1000);');
1373
+ const result = analyzeSourceFile(
1374
+ src,
1375
+ 'pkg',
1376
+ emptyPackageSummary(),
1377
+ testOpts,
1378
+ emptyMaps(),
1379
+ [],
1380
+ emptyProfile
1381
+ );
1382
+ expect(result.topLevelEffects!.some(e => e.kind === 'timer')).toBe(true);
1383
+ });
1384
+
1385
+ it('detects top-level eval', () => {
1386
+ const src = parse('eval("console.log(1)");');
1387
+ const result = analyzeSourceFile(
1388
+ src,
1389
+ 'pkg',
1390
+ emptyPackageSummary(),
1391
+ testOpts,
1392
+ emptyMaps(),
1393
+ [],
1394
+ emptyProfile
1395
+ );
1396
+ expect(result.topLevelEffects!.some(e => e.kind === 'eval')).toBe(true);
1397
+ });
1398
+
1399
+ it('detects top-level new Function()', () => {
1400
+ const src = parse('new Function("return 1");');
1401
+ const result = analyzeSourceFile(
1402
+ src,
1403
+ 'pkg',
1404
+ emptyPackageSummary(),
1405
+ testOpts,
1406
+ emptyMaps(),
1407
+ [],
1408
+ emptyProfile
1409
+ );
1410
+ expect(result.topLevelEffects!.some(e => e.kind === 'eval')).toBe(true);
1411
+ });
1412
+
1413
+ it('detects top-level sync I/O in variable initializer', () => {
1414
+ const src = parse('const data = fs.readFileSync("/path", "utf8");');
1415
+ const result = analyzeSourceFile(
1416
+ src,
1417
+ 'pkg',
1418
+ emptyPackageSummary(),
1419
+ testOpts,
1420
+ emptyMaps(),
1421
+ [],
1422
+ emptyProfile
1423
+ );
1424
+ expect(result.topLevelEffects!.some(e => e.kind === 'sync-io')).toBe(true);
1425
+ });
1426
+
1427
+ it('detects top-level execSync', () => {
1428
+ const src = parse('const out = cp.execSync("ls");');
1429
+ const result = analyzeSourceFile(
1430
+ src,
1431
+ 'pkg',
1432
+ emptyPackageSummary(),
1433
+ testOpts,
1434
+ emptyMaps(),
1435
+ [],
1436
+ emptyProfile
1437
+ );
1438
+ expect(result.topLevelEffects!.some(e => e.kind === 'exec-sync')).toBe(
1439
+ true
1440
+ );
1441
+ });
1442
+
1443
+ it('detects top-level process.on listener', () => {
1444
+ const src = parse('process.on("uncaughtException", handler);');
1445
+ const result = analyzeSourceFile(
1446
+ src,
1447
+ 'pkg',
1448
+ emptyPackageSummary(),
1449
+ testOpts,
1450
+ emptyMaps(),
1451
+ [],
1452
+ emptyProfile
1453
+ );
1454
+ expect(
1455
+ result.topLevelEffects!.some(e => e.kind === 'process-handler')
1456
+ ).toBe(true);
1457
+ });
1458
+
1459
+ it('does not collect effects for test files', () => {
1460
+ const src = parse('eval("1");', '/repo/src/test.test.ts');
1461
+ const result = analyzeSourceFile(
1462
+ src,
1463
+ 'pkg',
1464
+ emptyPackageSummary(),
1465
+ testOpts,
1466
+ emptyMaps(),
1467
+ [],
1468
+ emptyProfile
1469
+ );
1470
+ expect(result.topLevelEffects).toBeUndefined();
1471
+ });
1472
+
1473
+ it('does not flag function declarations as effects', () => {
1474
+ const src = parse('function f() { fs.readFileSync("/path"); }');
1475
+ const result = analyzeSourceFile(
1476
+ src,
1477
+ 'pkg',
1478
+ emptyPackageSummary(),
1479
+ testOpts,
1480
+ emptyMaps(),
1481
+ [],
1482
+ emptyProfile
1483
+ );
1484
+ expect(result.topLevelEffects?.length ?? 0).toBe(0);
1485
+ });
1486
+
1487
+ it('skips regular import declarations', () => {
1488
+ const src = parse("import path from 'node:path';");
1489
+ const result = analyzeSourceFile(
1490
+ src,
1491
+ 'pkg',
1492
+ emptyPackageSummary(),
1493
+ testOpts,
1494
+ emptyMaps(),
1495
+ [],
1496
+ emptyProfile
1497
+ );
1498
+ expect(
1499
+ result.topLevelEffects?.some(e => e.kind === 'side-effect-import')
1500
+ ).toBeFalsy();
1501
+ });
1502
+ });
1503
+
1504
+ describe('collectPrototypePollutionSites (via analyzeSourceFile)', () => {
1505
+ it('detects Object.assign with 2+ args', () => {
1506
+ const src = parse('function f(a: any, b: any) { Object.assign(a, b); }');
1507
+ const result = analyzeSourceFile(
1508
+ src,
1509
+ 'pkg',
1510
+ emptyPackageSummary(),
1511
+ testOpts,
1512
+ emptyMaps(),
1513
+ [],
1514
+ emptyProfile
1515
+ );
1516
+ expect(
1517
+ result.prototypePollutionSites!.some(s => s.kind === 'object-assign')
1518
+ ).toBe(true);
1519
+ });
1520
+
1521
+ it('detects deep merge calls', () => {
1522
+ const src = parse(
1523
+ 'function f(target: any, src: any) { merge(target, src); }'
1524
+ );
1525
+ const result = analyzeSourceFile(
1526
+ src,
1527
+ 'pkg',
1528
+ emptyPackageSummary(),
1529
+ testOpts,
1530
+ emptyMaps(),
1531
+ [],
1532
+ emptyProfile
1533
+ );
1534
+ expect(
1535
+ result.prototypePollutionSites!.some(s => s.kind === 'deep-merge')
1536
+ ).toBe(true);
1537
+ });
1538
+
1539
+ it('detects computed property writes', () => {
1540
+ const src = parse(
1541
+ 'function f(obj: any, key: string, val: any) { obj[key] = val; }'
1542
+ );
1543
+ const result = analyzeSourceFile(
1544
+ src,
1545
+ 'pkg',
1546
+ emptyPackageSummary(),
1547
+ testOpts,
1548
+ emptyMaps(),
1549
+ [],
1550
+ emptyProfile
1551
+ );
1552
+ expect(
1553
+ result.prototypePollutionSites!.some(
1554
+ s => s.kind === 'computed-property-write'
1555
+ )
1556
+ ).toBe(true);
1557
+ });
1558
+
1559
+ it('marks computed writes from internal iteration as guarded', () => {
1560
+ const src =
1561
+ parse(`function f(obj: Record<string, any>, source: Record<string, any>) {
1562
+ for (const key of Object.keys(source)) { obj[key] = source[key]; }
1563
+ }`);
1564
+ const result = analyzeSourceFile(
1565
+ src,
1566
+ 'pkg',
1567
+ emptyPackageSummary(),
1568
+ testOpts,
1569
+ emptyMaps(),
1570
+ [],
1571
+ emptyProfile
1572
+ );
1573
+ const cpw = result.prototypePollutionSites!.filter(
1574
+ s => s.kind === 'computed-property-write'
1575
+ );
1576
+ expect(cpw.length).toBeGreaterThanOrEqual(1);
1577
+ expect(cpw.some(s => s.guarded)).toBe(true);
1578
+ });
1579
+
1580
+ it('marks computed writes with __proto__ guard as guarded', () => {
1581
+ const src = parse(`function f(obj: any, key: string, val: any) {
1582
+ if (key === '__proto__' || key === 'constructor') return;
1583
+ obj[key] = val;
1584
+ }`);
1585
+ const result = analyzeSourceFile(
1586
+ src,
1587
+ 'pkg',
1588
+ emptyPackageSummary(),
1589
+ testOpts,
1590
+ emptyMaps(),
1591
+ [],
1592
+ emptyProfile
1593
+ );
1594
+ const cpw = result.prototypePollutionSites!.filter(
1595
+ s => s.kind === 'computed-property-write'
1596
+ );
1597
+ expect(cpw.some(s => s.guarded)).toBe(true);
1598
+ });
1599
+
1600
+ it('does not flag string literal property access', () => {
1601
+ const src = parse('function f(obj: any) { obj["known"] = 1; }');
1602
+ const result = analyzeSourceFile(
1603
+ src,
1604
+ 'pkg',
1605
+ emptyPackageSummary(),
1606
+ testOpts,
1607
+ emptyMaps(),
1608
+ [],
1609
+ emptyProfile
1610
+ );
1611
+ const cpw = (result.prototypePollutionSites ?? []).filter(
1612
+ s => s.kind === 'computed-property-write'
1613
+ );
1614
+ expect(cpw.length).toBe(0);
1615
+ });
1616
+
1617
+ it('does not collect for test files', () => {
1618
+ const src = parse(
1619
+ 'function f(obj: any, key: string) { obj[key] = 1; }',
1620
+ '/repo/src/test.test.ts'
1621
+ );
1622
+ const result = analyzeSourceFile(
1623
+ src,
1624
+ 'pkg',
1625
+ emptyPackageSummary(),
1626
+ testOpts,
1627
+ emptyMaps(),
1628
+ [],
1629
+ emptyProfile
1630
+ );
1631
+ expect(result.prototypePollutionSites).toBeUndefined();
1632
+ });
1633
+ });
1634
+
1635
+ describe('collectTestProfile (via analyzeSourceFile)', () => {
1636
+ const testFileName = '/repo/src/feature.test.ts';
1637
+
1638
+ function parseTest(code: string): ts.SourceFile {
1639
+ return ts.createSourceFile(
1640
+ testFileName,
1641
+ code,
1642
+ ts.ScriptTarget.ESNext,
1643
+ true
1644
+ );
1645
+ }
1646
+
1647
+ it('collects test blocks with assertion counts', () => {
1648
+ const src = parseTest(`describe('suite', () => {
1649
+ it('test', () => { expect(1).toBe(1); });
1650
+ });`);
1651
+ const result = analyzeSourceFile(
1652
+ src,
1653
+ 'pkg',
1654
+ emptyPackageSummary(),
1655
+ testOpts,
1656
+ emptyMaps(),
1657
+ [],
1658
+ emptyProfile
1659
+ );
1660
+ expect(result.testProfile!.testBlocks.length).toBeGreaterThanOrEqual(1);
1661
+ expect(result.testProfile!.testBlocks[0].assertionCount).toBe(1);
1662
+ });
1663
+
1664
+ it('collects mock calls', () => {
1665
+ const src = parseTest(`test('mocked', () => { jest.mock('lodash'); });`);
1666
+ const result = analyzeSourceFile(
1667
+ src,
1668
+ 'pkg',
1669
+ emptyPackageSummary(),
1670
+ testOpts,
1671
+ emptyMaps(),
1672
+ [],
1673
+ emptyProfile
1674
+ );
1675
+ expect(result.testProfile!.mockCalls.length).toBeGreaterThanOrEqual(1);
1676
+ });
1677
+
1678
+ it('collects setup calls (beforeAll, afterEach)', () => {
1679
+ const src = parseTest(`describe('s', () => {
1680
+ beforeAll(() => {});
1681
+ afterEach(() => {});
1682
+ it('t', () => {});
1683
+ });`);
1684
+ const result = analyzeSourceFile(
1685
+ src,
1686
+ 'pkg',
1687
+ emptyPackageSummary(),
1688
+ testOpts,
1689
+ emptyMaps(),
1690
+ [],
1691
+ emptyProfile
1692
+ );
1693
+ expect(
1694
+ result.testProfile!.setupCalls.some(c => c.kind === 'beforeAll')
1695
+ ).toBe(true);
1696
+ expect(
1697
+ result.testProfile!.setupCalls.some(c => c.kind === 'afterEach')
1698
+ ).toBe(true);
1699
+ });
1700
+
1701
+ it('collects mutable state declarations (let at describe scope)', () => {
1702
+ const src = parseTest(`describe('s', () => {
1703
+ let counter = 0;
1704
+ it('t', () => { counter++; });
1705
+ });`);
1706
+ const result = analyzeSourceFile(
1707
+ src,
1708
+ 'pkg',
1709
+ emptyPackageSummary(),
1710
+ testOpts,
1711
+ emptyMaps(),
1712
+ [],
1713
+ emptyProfile
1714
+ );
1715
+ expect(result.testProfile!.mutableStateDecls.length).toBeGreaterThanOrEqual(
1716
+ 1
1717
+ );
1718
+ });
1719
+
1720
+ it('does not flag const at describe scope as mutable', () => {
1721
+ const src = parseTest(`describe('s', () => {
1722
+ const val = 42;
1723
+ it('t', () => { expect(val).toBe(42); });
1724
+ });`);
1725
+ const result = analyzeSourceFile(
1726
+ src,
1727
+ 'pkg',
1728
+ emptyPackageSummary(),
1729
+ testOpts,
1730
+ emptyMaps(),
1731
+ [],
1732
+ emptyProfile
1733
+ );
1734
+ expect(result.testProfile!.mutableStateDecls.length).toBe(0);
1735
+ });
1736
+
1737
+ it('collects focused tests (it.only)', () => {
1738
+ const src = parseTest(`describe('s', () => {
1739
+ it.only('focused', () => { expect(1).toBe(1); });
1740
+ });`);
1741
+ const result = analyzeSourceFile(
1742
+ src,
1743
+ 'pkg',
1744
+ emptyPackageSummary(),
1745
+ testOpts,
1746
+ emptyMaps(),
1747
+ [],
1748
+ emptyProfile
1749
+ );
1750
+ expect(result.testProfile!.focusedCalls.length).toBeGreaterThanOrEqual(1);
1751
+ });
1752
+
1753
+ it('collects timer controls (useFakeTimers, useRealTimers)', () => {
1754
+ const src = parseTest(`test('timers', () => {
1755
+ jest.useFakeTimers();
1756
+ jest.useRealTimers();
1757
+ });`);
1758
+ const result = analyzeSourceFile(
1759
+ src,
1760
+ 'pkg',
1761
+ emptyPackageSummary(),
1762
+ testOpts,
1763
+ emptyMaps(),
1764
+ [],
1765
+ emptyProfile
1766
+ );
1767
+ expect(
1768
+ result.testProfile!.timerControls.some(
1769
+ t => t.kind === 'jest.useFakeTimers'
1770
+ )
1771
+ ).toBe(true);
1772
+ expect(
1773
+ result.testProfile!.timerControls.some(
1774
+ t => t.kind === 'jest.useRealTimers'
1775
+ )
1776
+ ).toBe(true);
1777
+ });
1778
+
1779
+ it('collects spy/stub calls', () => {
1780
+ const src = parseTest(`test('spy', () => {
1781
+ const spy = jest.spyOn(Date, 'now');
1782
+ });`);
1783
+ const result = analyzeSourceFile(
1784
+ src,
1785
+ 'pkg',
1786
+ emptyPackageSummary(),
1787
+ testOpts,
1788
+ emptyMaps(),
1789
+ [],
1790
+ emptyProfile
1791
+ );
1792
+ expect(result.testProfile!.spyOrStubCalls!.length).toBeGreaterThanOrEqual(
1793
+ 1
1794
+ );
1795
+ expect(result.testProfile!.spyOrStubCalls![0].kind).toBe('spy');
1796
+ });
1797
+
1798
+ it('collects mockRestore calls', () => {
1799
+ const src = parseTest(`test('restore', () => {
1800
+ const spy = jest.spyOn(Date, 'now');
1801
+ spy.mockRestore();
1802
+ });`);
1803
+ const result = analyzeSourceFile(
1804
+ src,
1805
+ 'pkg',
1806
+ emptyPackageSummary(),
1807
+ testOpts,
1808
+ emptyMaps(),
1809
+ [],
1810
+ emptyProfile
1811
+ );
1812
+ expect(
1813
+ result.testProfile!.mockRestores!.some(r => r.kind === 'restore')
1814
+ ).toBe(true);
1815
+ });
1816
+
1817
+ it('collects restoreAllMocks calls', () => {
1818
+ const src = parseTest(`afterEach(() => { jest.restoreAllMocks(); });`);
1819
+ const result = analyzeSourceFile(
1820
+ src,
1821
+ 'pkg',
1822
+ emptyPackageSummary(),
1823
+ testOpts,
1824
+ emptyMaps(),
1825
+ [],
1826
+ emptyProfile
1827
+ );
1828
+ expect(
1829
+ result.testProfile!.mockRestores!.some(r => r.kind === 'restoreAll')
1830
+ ).toBe(true);
1831
+ });
1832
+
1833
+ it('does not collect test profile for non-test files', () => {
1834
+ const src = parse('function f() {}');
1835
+ const result = analyzeSourceFile(
1836
+ src,
1837
+ 'pkg',
1838
+ emptyPackageSummary(),
1839
+ testOpts,
1840
+ emptyMaps(),
1841
+ [],
1842
+ emptyProfile
1843
+ );
1844
+ expect(result.testProfile).toBeUndefined();
1845
+ });
1846
+
1847
+ it('collects test block names from string literals', () => {
1848
+ const src = parseTest(
1849
+ `it('should work correctly', () => { expect(true).toBe(true); });`
1850
+ );
1851
+ const result = analyzeSourceFile(
1852
+ src,
1853
+ 'pkg',
1854
+ emptyPackageSummary(),
1855
+ testOpts,
1856
+ emptyMaps(),
1857
+ [],
1858
+ emptyProfile
1859
+ );
1860
+ expect(result.testProfile!.testBlocks[0].name).toBe(
1861
+ 'should work correctly'
1862
+ );
1863
+ });
1864
+ });