octocode-cli 1.2.8 → 1.2.10

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 (282) hide show
  1. package/README.md +45 -38
  2. package/out/octocode-cli.js +73 -11763
  3. package/package.json +35 -36
  4. package/skills/README.md +42 -114
  5. package/skills/{octocode-code-engineer → octocode-engineer}/.claude/settings.local.json +2 -1
  6. package/skills/octocode-engineer/README.md +99 -0
  7. package/skills/octocode-engineer/SKILL.md +413 -0
  8. package/skills/octocode-engineer/build.mjs +29 -0
  9. package/skills/{octocode-code-engineer → octocode-engineer}/eslint.config.mjs +3 -13
  10. package/skills/{octocode-code-engineer → octocode-engineer}/package.json +28 -27
  11. package/skills/octocode-engineer/references/ast-reference.md +166 -0
  12. package/skills/{octocode-code-engineer → octocode-engineer}/references/cli-reference.md +80 -6
  13. package/skills/octocode-engineer/references/externals.md +86 -0
  14. package/skills/{octocode-code-engineer → octocode-engineer}/references/output-files.md +46 -6
  15. package/skills/octocode-engineer/references/quality-indicators.md +202 -0
  16. package/skills/octocode-engineer/references/tool-workflows.md +298 -0
  17. package/skills/octocode-engineer/references/validation-playbooks.md +99 -0
  18. package/skills/octocode-engineer/scripts/ast/search.js +45 -0
  19. package/skills/octocode-engineer/scripts/ast/tree-search.js +27 -0
  20. package/skills/octocode-engineer/scripts/index.js +173 -0
  21. package/skills/octocode-engineer/scripts/run.js +179 -0
  22. package/skills/octocode-engineer/src/analysis/dependencies.ts +378 -0
  23. package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.test.ts +57 -0
  24. package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/discovery.ts +43 -0
  25. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.test.ts +113 -0
  26. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/search.ts +64 -1
  27. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.test.ts +118 -2
  28. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-sitter.ts +65 -3
  29. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.test.ts +281 -1
  30. package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/ts-analyzer.ts +173 -3
  31. package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.test.ts +73 -0
  32. package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/security.ts +62 -4
  33. package/skills/octocode-engineer/src/detector-gating.test.ts +59 -0
  34. package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/code-quality.ts +342 -0
  35. package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.ts +8 -0
  36. package/skills/{octocode-code-engineer → octocode-engineer}/src/index.test.ts +565 -11
  37. package/skills/octocode-engineer/src/index.ts +468 -0
  38. package/skills/octocode-engineer/src/pipeline/affected.test.ts +147 -0
  39. package/skills/octocode-engineer/src/pipeline/affected.ts +68 -0
  40. package/skills/octocode-engineer/src/pipeline/baseline.test.ts +276 -0
  41. package/skills/octocode-engineer/src/pipeline/baseline.ts +76 -0
  42. package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.test.ts +300 -53
  43. package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cli.ts +180 -36
  44. package/skills/octocode-engineer/src/pipeline/config-loader.test.ts +264 -0
  45. package/skills/octocode-engineer/src/pipeline/config-loader.ts +109 -0
  46. package/skills/octocode-engineer/src/pipeline/create-options.ts +55 -0
  47. package/skills/octocode-engineer/src/pipeline/health-score.test.ts +65 -0
  48. package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.ts +130 -17
  49. package/skills/octocode-engineer/src/pipeline/progress.ts +51 -0
  50. package/skills/octocode-engineer/src/pipeline/reporters.test.ts +155 -0
  51. package/skills/octocode-engineer/src/pipeline/reporters.ts +64 -0
  52. package/skills/octocode-engineer/src/reporting/graph-features.test.ts +279 -0
  53. package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/output-contract.test.ts +6 -0
  54. package/skills/octocode-engineer/src/reporting/summary-md.test.ts +1066 -0
  55. package/skills/octocode-engineer/src/reporting/summary-md.ts +1604 -0
  56. package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/writer.ts +136 -13
  57. package/skills/octocode-engineer/src/run.ts +78 -0
  58. package/skills/{octocode-code-engineer → octocode-engineer}/src/sanity.test.ts +1 -1
  59. package/skills/octocode-engineer/src/types/analysis.ts +25 -0
  60. package/skills/octocode-engineer/src/types/collectors.ts +134 -0
  61. package/skills/{octocode-code-engineer → octocode-engineer}/src/types/constants.ts +75 -41
  62. package/skills/octocode-engineer/src/types/core.ts +203 -0
  63. package/skills/octocode-engineer/src/types/dependency.ts +215 -0
  64. package/skills/octocode-engineer/src/types/file-entry.ts +108 -0
  65. package/skills/octocode-engineer/src/types/findings.ts +105 -0
  66. package/skills/{octocode-code-engineer → octocode-engineer}/src/types/index.ts +60 -30
  67. package/skills/octocode-engineer/src/types/tree-sitter.ts +38 -0
  68. package/skills/{octocode-code-engineer → octocode-engineer}/tsconfig.json +1 -0
  69. package/skills/octocode-research/.octocode/scan/.cache/analysis-cache.json +1 -0
  70. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/architecture.json +1 -0
  71. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/ast-trees.txt +5566 -0
  72. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/code-quality.json +1 -0
  73. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/dead-code.json +1 -0
  74. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/file-inventory.json +1 -0
  75. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/findings.json +1 -0
  76. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/graph.md +189 -0
  77. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/security.json +1 -0
  78. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.json +1 -0
  79. package/skills/octocode-research/.octocode/scan/2026-03-22T10-32-27-073Z/summary.md +265 -0
  80. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/architecture.json +1 -0
  81. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/ast-trees.txt +5555 -0
  82. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/code-quality.json +1 -0
  83. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/dead-code.json +1 -0
  84. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/file-inventory.json +1 -0
  85. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/findings.json +1 -0
  86. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/graph.md +190 -0
  87. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/security.json +1 -0
  88. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.json +1 -0
  89. package/skills/octocode-research/.octocode/scan/2026-03-22T10-40-10-469Z/summary.md +265 -0
  90. package/skills/octocode-research/CHANGELOG.md +60 -0
  91. package/skills/octocode-research/README.md +102 -388
  92. package/skills/octocode-research/SKILL.md +169 -498
  93. package/skills/octocode-research/package.json +19 -31
  94. package/skills/octocode-research/references/PARALLEL_AGENT_PROTOCOL.md +19 -0
  95. package/skills/octocode-research/references/SESSION_MANAGEMENT.md +38 -0
  96. package/skills/octocode-research/scripts/server-init.js +1 -1
  97. package/skills/octocode-research/scripts/server.d.ts +2 -1
  98. package/skills/octocode-research/scripts/server.js +329 -233
  99. package/skills/octocode-research/src/__tests__/integration/promptsRoutes.test.ts +180 -0
  100. package/skills/octocode-research/src/__tests__/integration/serverHttp.test.ts +221 -0
  101. package/skills/octocode-research/src/__tests__/integration/serverLifecycle.test.ts +194 -0
  102. package/skills/octocode-research/src/__tests__/integration/toolsRoutes.test.ts +501 -0
  103. package/skills/octocode-research/src/__tests__/unit/readiness.test.ts +61 -0
  104. package/skills/octocode-research/src/__tests__/unit/resilience.test.ts +192 -0
  105. package/skills/octocode-research/src/__tests__/unit/responseFactory.test.ts +172 -0
  106. package/skills/octocode-research/src/__tests__/unit/responseParser.test.ts +288 -0
  107. package/skills/octocode-research/src/__tests__/unit/schemas.test.ts +509 -0
  108. package/skills/octocode-research/src/index.ts +4 -124
  109. package/skills/octocode-research/src/middleware/queryParser.ts +0 -26
  110. package/skills/octocode-research/src/routes/lsp.ts +58 -59
  111. package/skills/octocode-research/src/routes/package.ts +35 -65
  112. package/skills/octocode-research/src/routes/prompts.ts +3 -3
  113. package/skills/octocode-research/src/routes/tools.ts +8 -20
  114. package/skills/octocode-research/src/server-init.ts +30 -237
  115. package/skills/octocode-research/src/server.ts +50 -23
  116. package/skills/octocode-research/src/types/errorGuards.ts +9 -80
  117. package/skills/octocode-research/src/types/guards.ts +0 -28
  118. package/skills/octocode-research/src/types/mcp.ts +11 -66
  119. package/skills/octocode-research/src/types/responses.ts +11 -129
  120. package/skills/octocode-research/src/utils/circuitBreaker.ts +0 -21
  121. package/skills/octocode-research/src/utils/logger.ts +1 -97
  122. package/skills/octocode-research/src/utils/resilience.ts +2 -12
  123. package/skills/octocode-research/src/utils/responseFactory.ts +0 -42
  124. package/skills/octocode-research/src/utils/responseParser.ts +3 -25
  125. package/skills/octocode-research/src/utils/retry.ts +0 -63
  126. package/skills/octocode-research/src/utils/routeFactory.ts +1 -1
  127. package/skills/octocode-research/src/validation/httpPreprocess.ts +0 -3
  128. package/skills/octocode-research/src/validation/index.ts +0 -1
  129. package/skills/octocode-research/src/validation/schemas.ts +0 -63
  130. package/skills/octocode-research/src/validation/toolCallSchema.ts +3 -3
  131. package/skills/octocode-research/tsdown.config.ts +4 -0
  132. package/skills/octocode-research/vitest.config.ts +3 -0
  133. package/skills/octocode-code-engineer/.plan/VALIDATED_PLAN.md +0 -223
  134. package/skills/octocode-code-engineer/README.md +0 -178
  135. package/skills/octocode-code-engineer/SKILL.md +0 -418
  136. package/skills/octocode-code-engineer/minify-scripts.mjs +0 -32
  137. package/skills/octocode-code-engineer/references/agent-ast-reading-rfc.md +0 -95
  138. package/skills/octocode-code-engineer/references/architecture-techniques.md +0 -121
  139. package/skills/octocode-code-engineer/references/ast-search.md +0 -210
  140. package/skills/octocode-code-engineer/references/ast-tree-search.md +0 -151
  141. package/skills/octocode-code-engineer/references/concepts.md +0 -107
  142. package/skills/octocode-code-engineer/references/finding-categories.md +0 -128
  143. package/skills/octocode-code-engineer/references/improvement-roadmap.md +0 -304
  144. package/skills/octocode-code-engineer/references/playbooks.md +0 -204
  145. package/skills/octocode-code-engineer/references/present-results.md +0 -136
  146. package/skills/octocode-code-engineer/references/tool-workflows.md +0 -566
  147. package/skills/octocode-code-engineer/references/validate-investigate.md +0 -225
  148. package/skills/octocode-code-engineer/scripts/analysis/dependencies.js +0 -1
  149. package/skills/octocode-code-engineer/scripts/analysis/dependency-summary.js +0 -1
  150. package/skills/octocode-code-engineer/scripts/analysis/discovery.js +0 -1
  151. package/skills/octocode-code-engineer/scripts/analysis/graph-analytics.js +0 -1
  152. package/skills/octocode-code-engineer/scripts/analysis/semantic.js +0 -1
  153. package/skills/octocode-code-engineer/scripts/ast/helpers.js +0 -1
  154. package/skills/octocode-code-engineer/scripts/ast/metrics.js +0 -1
  155. package/skills/octocode-code-engineer/scripts/ast/search.js +0 -2
  156. package/skills/octocode-code-engineer/scripts/ast/tree-search.js +0 -2
  157. package/skills/octocode-code-engineer/scripts/ast/tree-sitter.js +0 -1
  158. package/skills/octocode-code-engineer/scripts/ast/ts-analyzer.js +0 -1
  159. package/skills/octocode-code-engineer/scripts/collectors/chains.js +0 -1
  160. package/skills/octocode-code-engineer/scripts/collectors/effects.js +0 -1
  161. package/skills/octocode-code-engineer/scripts/collectors/input-sources.js +0 -1
  162. package/skills/octocode-code-engineer/scripts/collectors/performance.js +0 -1
  163. package/skills/octocode-code-engineer/scripts/collectors/prototype-pollution.js +0 -1
  164. package/skills/octocode-code-engineer/scripts/collectors/security.js +0 -1
  165. package/skills/octocode-code-engineer/scripts/collectors/test-profile.js +0 -1
  166. package/skills/octocode-code-engineer/scripts/common/is-direct-run.js +0 -1
  167. package/skills/octocode-code-engineer/scripts/common/utils.js +0 -1
  168. package/skills/octocode-code-engineer/scripts/detectors/code-quality.js +0 -1
  169. package/skills/octocode-code-engineer/scripts/detectors/cohesion.js +0 -1
  170. package/skills/octocode-code-engineer/scripts/detectors/coupling.js +0 -1
  171. package/skills/octocode-code-engineer/scripts/detectors/cycle.js +0 -1
  172. package/skills/octocode-code-engineer/scripts/detectors/dead-code.js +0 -1
  173. package/skills/octocode-code-engineer/scripts/detectors/import-style.js +0 -1
  174. package/skills/octocode-code-engineer/scripts/detectors/index.js +0 -1
  175. package/skills/octocode-code-engineer/scripts/detectors/security.js +0 -1
  176. package/skills/octocode-code-engineer/scripts/detectors/semantic.js +0 -1
  177. package/skills/octocode-code-engineer/scripts/detectors/shared.js +0 -1
  178. package/skills/octocode-code-engineer/scripts/detectors/test-quality.js +0 -1
  179. package/skills/octocode-code-engineer/scripts/index.js +0 -1
  180. package/skills/octocode-code-engineer/scripts/pipeline/cache.js +0 -1
  181. package/skills/octocode-code-engineer/scripts/pipeline/cli.js +0 -1
  182. package/skills/octocode-code-engineer/scripts/pipeline/main.js +0 -2
  183. package/skills/octocode-code-engineer/scripts/reporting/analysis.js +0 -1
  184. package/skills/octocode-code-engineer/scripts/reporting/summary-md.js +0 -1
  185. package/skills/octocode-code-engineer/scripts/reporting/writer.js +0 -1
  186. package/skills/octocode-code-engineer/scripts/types/constants.js +0 -1
  187. package/skills/octocode-code-engineer/scripts/types/index.js +0 -1
  188. package/skills/octocode-code-engineer/scripts/types/interfaces.js +0 -1
  189. package/skills/octocode-code-engineer/src/analysis/dependencies.ts +0 -406
  190. package/skills/octocode-code-engineer/src/index.ts +0 -403
  191. package/skills/octocode-code-engineer/src/reporting/summary-md.test.ts +0 -421
  192. package/skills/octocode-code-engineer/src/reporting/summary-md.ts +0 -714
  193. package/skills/octocode-code-engineer/src/types/interfaces.ts +0 -682
  194. package/skills/octocode-research/src/types/toolTypes.ts +0 -33
  195. package/skills/octocode-research/src/utils/logEmoji.ts +0 -103
  196. /package/skills/{octocode-code-engineer → octocode-engineer}/.octocode/rfc/RFC-code-engineer-weakness-fixes.md +0 -0
  197. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/architecture.ts.html +0 -0
  198. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-helpers.ts.html +0 -0
  199. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ast-search.ts.html +0 -0
  200. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/base.css +0 -0
  201. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/block-navigation.js +0 -0
  202. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cache.ts.html +0 -0
  203. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/cli.ts.html +0 -0
  204. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/clover.xml +0 -0
  205. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-effects.ts.html +0 -0
  206. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-input-sources.ts.html +0 -0
  207. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-performance.ts.html +0 -0
  208. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-prototype-pollution.ts.html +0 -0
  209. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-security.ts.html +0 -0
  210. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/collect-test-profile.ts.html +0 -0
  211. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/coverage-final.json +0 -0
  212. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependencies.ts.html +0 -0
  213. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/dependency-summary.ts.html +0 -0
  214. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/discovery.ts.html +0 -0
  215. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/favicon.png +0 -0
  216. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/graph-analytics.ts.html +0 -0
  217. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.html +0 -0
  218. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/index.ts.html +0 -0
  219. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/metrics.ts.html +0 -0
  220. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/pipeline.ts.html +0 -0
  221. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.css +0 -0
  222. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/prettify.js +0 -0
  223. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-analysis.ts.html +0 -0
  224. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/report-writer.ts.html +0 -0
  225. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/security-detectors.ts.html +0 -0
  226. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic-detectors.ts.html +0 -0
  227. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/semantic.ts.html +0 -0
  228. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sort-arrow-sprite.png +0 -0
  229. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/sorter.js +0 -0
  230. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/summary-md.ts.html +0 -0
  231. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/test-quality-detectors.ts.html +0 -0
  232. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/tree-sitter-analyzer.ts.html +0 -0
  233. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/ts-analyzer.ts.html +0 -0
  234. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/types.ts.html +0 -0
  235. /package/skills/{octocode-code-engineer → octocode-engineer}/coverage/utils.ts.html +0 -0
  236. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependencies.test.ts +0 -0
  237. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.test.ts +0 -0
  238. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/dependency-summary.ts +0 -0
  239. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.test.ts +0 -0
  240. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/graph-analytics.ts +0 -0
  241. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.test.ts +0 -0
  242. /package/skills/{octocode-code-engineer → octocode-engineer}/src/analysis/semantic.ts +0 -0
  243. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.test.ts +0 -0
  244. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/helpers.ts +0 -0
  245. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.test.ts +0 -0
  246. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/metrics.ts +0 -0
  247. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.test.ts +0 -0
  248. /package/skills/{octocode-code-engineer → octocode-engineer}/src/ast/tree-search.ts +0 -0
  249. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/chains.ts +0 -0
  250. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.test.ts +0 -0
  251. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/effects.ts +0 -0
  252. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.test.ts +0 -0
  253. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/input-sources.ts +0 -0
  254. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.test.ts +0 -0
  255. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/performance.ts +0 -0
  256. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.test.ts +0 -0
  257. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/prototype-pollution.ts +0 -0
  258. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.test.ts +0 -0
  259. /package/skills/{octocode-code-engineer → octocode-engineer}/src/collectors/test-profile.ts +0 -0
  260. /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.test.ts +0 -0
  261. /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/is-direct-run.ts +0 -0
  262. /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.test.ts +0 -0
  263. /package/skills/{octocode-code-engineer → octocode-engineer}/src/common/utils.ts +0 -0
  264. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cohesion.ts +0 -0
  265. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/coupling.ts +0 -0
  266. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/cycle.ts +0 -0
  267. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/dead-code.ts +0 -0
  268. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/import-style.ts +0 -0
  269. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/index.test.ts +0 -0
  270. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.test.ts +0 -0
  271. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/security.ts +0 -0
  272. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/semantic.ts +0 -0
  273. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/shared.ts +0 -0
  274. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.test.ts +0 -0
  275. /package/skills/{octocode-code-engineer → octocode-engineer}/src/detectors/test-quality.ts +0 -0
  276. /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.test.ts +0 -0
  277. /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/cache.ts +0 -0
  278. /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline/main.test.ts +0 -0
  279. /package/skills/{octocode-code-engineer → octocode-engineer}/src/pipeline.test.ts +0 -0
  280. /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.test.ts +0 -0
  281. /package/skills/{octocode-code-engineer → octocode-engineer}/src/reporting/analysis.ts +0 -0
  282. /package/skills/{octocode-code-engineer → octocode-engineer}/vitest.config.ts +0 -0
@@ -0,0 +1,109 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import type { AnalysisOptions } from '../types/index.js';
5
+
6
+ type ConfigOverrides = Partial<Omit<AnalysisOptions, 'root' | 'packageRoot' | 'clearCache'>>;
7
+
8
+ const CONFIG_NAMES = ['.octocode-scan.json', '.octocode-scan.jsonc'];
9
+
10
+ /**
11
+ * Loads config from file, auto-discovered config, or package.json#octocode.
12
+ * CLI flags always win over config file values.
13
+ * Inspired by knip's .knip.json and eslint's flat config.
14
+ */
15
+ export function loadConfigFile(
16
+ root: string,
17
+ explicitPath: string | null
18
+ ): ConfigOverrides | null {
19
+ if (explicitPath) {
20
+ const abs = path.isAbsolute(explicitPath)
21
+ ? explicitPath
22
+ : path.resolve(root, explicitPath);
23
+ return readJsonConfig(abs);
24
+ }
25
+
26
+ for (const name of CONFIG_NAMES) {
27
+ const candidate = path.join(root, name);
28
+ if (fs.existsSync(candidate)) {
29
+ return readJsonConfig(candidate);
30
+ }
31
+ }
32
+
33
+ const pkgJsonPath = path.join(root, 'package.json');
34
+ if (fs.existsSync(pkgJsonPath)) {
35
+ try {
36
+ const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
37
+ if (pkg.octocode && typeof pkg.octocode === 'object') {
38
+ return normalizeConfig(pkg.octocode);
39
+ }
40
+ } catch { /* skip */ }
41
+ }
42
+
43
+ return null;
44
+ }
45
+
46
+ function readJsonConfig(filePath: string): ConfigOverrides | null {
47
+ try {
48
+ let raw = fs.readFileSync(filePath, 'utf8');
49
+ raw = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
50
+ return normalizeConfig(JSON.parse(raw));
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ function normalizeConfig(obj: Record<string, unknown>): ConfigOverrides {
57
+ const result: Record<string, unknown> = {};
58
+
59
+ for (const [key, value] of Object.entries(obj)) {
60
+ const camelKey = key.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
61
+
62
+ if (camelKey === 'features' && typeof value === 'string') {
63
+ result[camelKey] = new Set(value.split(',').map(s => s.trim()));
64
+ } else if (camelKey === 'scope' && typeof value === 'string') {
65
+ result[camelKey] = value.split(',').map(s => s.trim());
66
+ } else if (camelKey === 'ignoreDirs' && Array.isArray(value)) {
67
+ result[camelKey] = new Set(value as string[]);
68
+ } else if (camelKey === 'thresholds' && typeof value === 'object' && value !== null) {
69
+ result[camelKey] = value;
70
+ } else {
71
+ result[camelKey] = value;
72
+ }
73
+ }
74
+
75
+ return result as ConfigOverrides;
76
+ }
77
+
78
+ /**
79
+ * Merges config file overrides into defaults, then CLI overrides on top.
80
+ * CLI args that differ from defaults always win.
81
+ */
82
+ export function mergeConfigIntoDefaults(
83
+ defaults: AnalysisOptions,
84
+ config: ConfigOverrides,
85
+ cliArgs: AnalysisOptions
86
+ ): AnalysisOptions {
87
+ const merged = { ...defaults };
88
+
89
+ for (const [key, value] of Object.entries(config)) {
90
+ if (key === 'thresholds' && typeof value === 'object' && value !== null) {
91
+ merged.thresholds = {
92
+ ...merged.thresholds,
93
+ ...(value as unknown as Record<string, number>),
94
+ };
95
+ } else {
96
+ (merged as Record<string, unknown>)[key] = value;
97
+ }
98
+ }
99
+
100
+ for (const key of Object.keys(cliArgs)) {
101
+ const cliVal = (cliArgs as unknown as Record<string, unknown>)[key];
102
+ const defVal = (defaults as unknown as Record<string, unknown>)[key];
103
+ if (cliVal !== defVal) {
104
+ (merged as unknown as Record<string, unknown>)[key] = cliVal;
105
+ }
106
+ }
107
+
108
+ return merged;
109
+ }
@@ -0,0 +1,55 @@
1
+ import path from 'node:path';
2
+
3
+ import { ALL_CATEGORIES, PILLAR_CATEGORIES } from '../types/index.js';
4
+
5
+ import type { AnalysisOptions } from '../types/index.js';
6
+
7
+ export interface CreateOptionsInput {
8
+ args: AnalysisOptions;
9
+ }
10
+
11
+ /**
12
+ * Transforms raw parsed CLI args into validated, normalized runtime options.
13
+ * This is Layer 2 in the 3-layer CLI pattern (args → options → engine).
14
+ *
15
+ * Responsible for:
16
+ * - Deriving computed fields (packageRoot)
17
+ * - Auto-enabling flags based on feature selection (test-quality → includeTests)
18
+ *
19
+ * Inspired by knip's create-options.ts, eslint's translate-cli-options.js,
20
+ * and dependency-cruiser's normalize-cli-options.mjs.
21
+ */
22
+ export function createOptions({ args }: CreateOptionsInput): AnalysisOptions {
23
+ const opts = { ...args };
24
+
25
+ opts.packageRoot = path.join(opts.root, 'packages');
26
+ autoEnableTestQuality(opts);
27
+
28
+ return opts;
29
+ }
30
+
31
+ function autoEnableTestQuality(opts: AnalysisOptions): void {
32
+ if (opts.features === null) return;
33
+
34
+ const testQualityCats = new Set(PILLAR_CATEGORIES['test-quality']);
35
+ if ([...opts.features].some(f => testQualityCats.has(f))) {
36
+ opts.includeTests = true;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Resolves `--exclude` into a features set by subtracting from ALL_CATEGORIES.
42
+ * Called during CLI arg parsing when --exclude is used.
43
+ */
44
+ export function resolveExcludeToFeatures(
45
+ excludeSet: Set<string>
46
+ ): Set<string> {
47
+ return new Set([...ALL_CATEGORIES].filter(c => !excludeSet.has(c)));
48
+ }
49
+
50
+ export class OptionsError extends Error {
51
+ constructor(message: string) {
52
+ super(message);
53
+ this.name = 'OptionsError';
54
+ }
55
+ }
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { computeGateScore } from './main.js';
4
+
5
+ describe('computeGateScore', () => {
6
+ it('returns 100 for 0 findings', () => {
7
+ expect(computeGateScore(0, 100)).toBe(100);
8
+ });
9
+
10
+ it('returns 100 for 0 findings and 0 files', () => {
11
+ expect(computeGateScore(0, 0)).toBe(100);
12
+ });
13
+
14
+ it('decreases as findings increase', () => {
15
+ const score10 = computeGateScore(10, 100);
16
+ const score50 = computeGateScore(50, 100);
17
+ const score200 = computeGateScore(200, 100);
18
+
19
+ expect(score10).toBeGreaterThan(score50);
20
+ expect(score50).toBeGreaterThan(score200);
21
+ });
22
+
23
+ it('returns higher score for same findings with more files', () => {
24
+ const smallProject = computeGateScore(50, 10);
25
+ const bigProject = computeGateScore(50, 1000);
26
+
27
+ expect(bigProject).toBeGreaterThan(smallProject);
28
+ });
29
+
30
+ it('is always between 0 and 100', () => {
31
+ for (const [f, t] of [
32
+ [0, 1],
33
+ [1, 1],
34
+ [100, 10],
35
+ [1000, 50],
36
+ [10000, 100],
37
+ ]) {
38
+ const score = computeGateScore(f, t);
39
+ expect(score).toBeGreaterThanOrEqual(0);
40
+ expect(score).toBeLessThanOrEqual(100);
41
+ }
42
+ });
43
+
44
+ it('returns reasonable score for typical project (10 findings / 100 files)', () => {
45
+ const score = computeGateScore(10, 100);
46
+ expect(score).toBeGreaterThan(90);
47
+ });
48
+
49
+ it('returns low score for finding-heavy project (500 findings / 50 files)', () => {
50
+ const score = computeGateScore(500, 50);
51
+ expect(score).toBeLessThanOrEqual(50);
52
+ });
53
+
54
+ it('handles totalFiles=0 without division by zero', () => {
55
+ expect(() => computeGateScore(10, 0)).not.toThrow();
56
+ const score = computeGateScore(10, 0);
57
+ expect(score).toBeGreaterThanOrEqual(0);
58
+ expect(score).toBeLessThanOrEqual(100);
59
+ });
60
+
61
+ it('returns integer values', () => {
62
+ const score = computeGateScore(17, 33);
63
+ expect(Number.isInteger(score)).toBe(true);
64
+ });
65
+ });
@@ -15,6 +15,12 @@ import {
15
15
  setCacheEntry,
16
16
  } from './cache.js';
17
17
  import { parseArgs } from './cli.js';
18
+ import { loadConfigFile, mergeConfigIntoDefaults } from './config-loader.js';
19
+ import { createOptions, OptionsError } from './create-options.js';
20
+ import { attachConsoleFeedback, bus } from './progress.js';
21
+ import { resolveAffectedFiles } from './affected.js';
22
+ import { saveBaseline, filterKnownFindings } from './baseline.js';
23
+ import { formatFindings } from './reporters.js';
18
24
  import { collectDependencyProfile } from '../analysis/dependencies.js';
19
25
  import { buildDependencySummary } from '../analysis/dependency-summary.js';
20
26
  import {
@@ -52,6 +58,7 @@ import {
52
58
  } from '../reporting/analysis.js';
53
59
  import { diverseTopRecommendations } from '../reporting/summary-md.js';
54
60
  import { generateMermaidGraph, writeMultiFileReport } from '../reporting/writer.js';
61
+ import type { GraphRenderOptions } from '../reporting/writer.js';
55
62
  import { PILLAR_CATEGORIES, SEMANTIC_CATEGORIES } from '../types/index.js';
56
63
 
57
64
  import type { SemanticProfile } from '../analysis/semantic.js';
@@ -211,8 +218,8 @@ function runSemanticPhase(
211
218
  }
212
219
  }
213
220
  return runSemanticDetectors(semanticCtx, profiles, {
214
- overrideChainThreshold: options.overrideChainThreshold,
215
- shotgunThreshold: options.shotgunThreshold,
221
+ overrideChainThreshold: options.thresholds.overrideChainThreshold,
222
+ shotgunThreshold: options.thresholds.shotgunThreshold,
216
223
  });
217
224
  } catch (err: unknown) {
218
225
  parseErrors.push({
@@ -415,8 +422,26 @@ interface ScanState {
415
422
  treeSitterError: string | null;
416
423
  }
417
424
 
418
- async function initScanState(): Promise<ScanState | null> {
419
- const options = parseArgs(process.argv.slice(2));
425
+ async function initScanState(
426
+ prebuiltOptions?: AnalysisOptions
427
+ ): Promise<ScanState | null> {
428
+ let options: AnalysisOptions;
429
+ if (prebuiltOptions) {
430
+ options = prebuiltOptions;
431
+ } else {
432
+ const { DEFAULT_OPTS } = await import('../types/constants.js');
433
+ const cliArgs = createOptions({ args: parseArgs(process.argv.slice(2)) });
434
+ const config = loadConfigFile(cliArgs.root, cliArgs.configFile);
435
+ options = config
436
+ ? mergeConfigIntoDefaults(DEFAULT_OPTS, config, cliArgs)
437
+ : cliArgs;
438
+ }
439
+
440
+ if (!options.json && !prebuiltOptions) {
441
+ attachConsoleFeedback();
442
+ }
443
+
444
+ bus.progress('startup', 'Options parsed', `root=${options.root}`);
420
445
 
421
446
  if (options.clearCache) {
422
447
  clearCache(options.root);
@@ -472,6 +497,8 @@ async function initScanState(): Promise<ScanState | null> {
472
497
  }
473
498
  }
474
499
 
500
+ bus.progress('discovery', `Found ${packages.length} package(s)`, packages.map(p => p.name).join(', '));
501
+
475
502
  return {
476
503
  options,
477
504
  packages,
@@ -537,6 +564,7 @@ type CachedResult = {
537
564
 
538
565
  function collectFileData(state: ScanState): void {
539
566
  const { options, packages, useTreeSitter, summary, flowMap, controlMap, trees, fileSummaries, parseErrors, dependencyState, packageFileStats } = state;
567
+ bus.progress('cache-check', options.noCache ? 'Cache disabled' : 'Loading cache');
540
568
  const cache = options.noCache ? null : loadCache(options.root);
541
569
  const newCache = createEmptyCache(options.root);
542
570
 
@@ -785,13 +813,15 @@ function collectFileData(state: ScanState): void {
785
813
  }
786
814
 
787
815
  summary.totalDependencyFiles = dependencyState.files.size;
816
+ bus.progress('parse', `Parsed ${fileSummaries.length} files`, `${state.cacheHits} cache hits`);
788
817
  }
789
818
 
790
- function analyzeAndReport(state: ScanState): void {
819
+ function analyzeAndReport(state: ScanState): number {
820
+ bus.progress('detect', 'Running detectors');
791
821
  const { options, effectiveParser, summary, flowMap, controlMap, trees, fileSummaries, parseErrors, dependencyState, allPkgJsonDeps, allPkgJsonDevDeps, isLegacyMode, outputDir, outputPath, treeSitterAvailable, treeSitterError } = state;
792
822
 
793
823
  const { duplicateFunctions, redundantFlows, duplicateFlowHints } =
794
- groupDuplicates(flowMap, controlMap, options.flowDupThreshold);
824
+ groupDuplicates(flowMap, controlMap, options.thresholds.flowDupThreshold);
795
825
 
796
826
  const fileCriticalityByPath = new Map<string, FileCriticality>(
797
827
  fileSummaries.map(item => [
@@ -804,6 +834,7 @@ function analyzeAndReport(state: ScanState): void {
804
834
  fileCriticalityByPath,
805
835
  options
806
836
  );
837
+ bus.progress('graph', 'Computing graph analytics');
807
838
  const graphAnalytics = computeGraphAnalytics(
808
839
  dependencyState,
809
840
  dependencySummary,
@@ -813,6 +844,7 @@ function analyzeAndReport(state: ScanState): void {
813
844
  ? buildAdvancedGraphFindings(graphAnalytics, dependencyState, fileSummaries)
814
845
  : [];
815
846
 
847
+ bus.progress('semantic', options.semantic ? 'Running semantic analysis' : 'Skipping semantic (not requested)');
816
848
  const semanticFindings = runSemanticPhase(
817
849
  fileSummaries,
818
850
  dependencyState,
@@ -863,6 +895,27 @@ function analyzeAndReport(state: ScanState): void {
863
895
  graphAnalytics,
864
896
  { flowEnabled: !!options.flow }
865
897
  );
898
+ if (options.affected) {
899
+ const affectedPaths = resolveAffectedFiles(
900
+ options.root, options.affected, dependencyState
901
+ );
902
+ if (affectedPaths.length > 0) {
903
+ const affectedSet = new Set(affectedPaths);
904
+ findings = findings.filter(f => affectedSet.has(f.file));
905
+ bus.progress('detect', `--affected: ${affectedPaths.length} files in scope, ${findings.length} findings`);
906
+ }
907
+ }
908
+
909
+ if (options.ignoreKnown) {
910
+ const { filtered, suppressedCount } = filterKnownFindings(
911
+ findings, options.ignoreKnown, options.root
912
+ );
913
+ if (suppressedCount > 0) {
914
+ bus.progress('detect', `--ignore-known: suppressed ${suppressedCount} known findings`);
915
+ }
916
+ findings = filtered;
917
+ }
918
+
866
919
  const reportAnalysis = computeReportAnalysisSummary(
867
920
  findings,
868
921
  enrichedFileSummaries,
@@ -948,7 +1001,19 @@ function analyzeAndReport(state: ScanState): void {
948
1001
  report.astTrees = trees;
949
1002
  }
950
1003
 
951
- if (options.json) {
1004
+ bus.progress('report', `${findings.length} findings generated`);
1005
+
1006
+ if (options.saveBaseline) {
1007
+ const baselinePath = saveBaseline(options.root, findings);
1008
+ if (!options.json) {
1009
+ console.error(`Baseline saved: ${path.relative(options.root, baselinePath)} (${findings.length} findings)`);
1010
+ }
1011
+ }
1012
+
1013
+ if (options.reporter !== 'default') {
1014
+ const formatted = formatFindings(findings, options.reporter, options.root);
1015
+ process.stdout.write(formatted + '\n');
1016
+ } else if (options.json) {
952
1017
  console.log(JSON.stringify(report));
953
1018
  } else {
954
1019
  printConsoleResults(
@@ -972,10 +1037,16 @@ function analyzeAndReport(state: ScanState): void {
972
1037
  );
973
1038
  }
974
1039
  if (options.graph) {
1040
+ const gOpts: GraphRenderOptions = {
1041
+ focus: options.focus,
1042
+ focusDepth: options.focusDepth,
1043
+ collapse: options.collapse,
1044
+ };
975
1045
  const graphMd = generateMermaidGraph(
976
1046
  dependencyState,
977
1047
  dependencySummary,
978
- fileCriticalityByPath
1048
+ fileCriticalityByPath,
1049
+ gOpts
979
1050
  );
980
1051
  const graphPath = outputPath.replace(/\.json$/, '-graph.md');
981
1052
  fs.writeFileSync(graphPath, graphMd, 'utf8');
@@ -986,13 +1057,20 @@ function analyzeAndReport(state: ScanState): void {
986
1057
  }
987
1058
  }
988
1059
  } else if (outputDir) {
1060
+ bus.progress('write', `Writing report to ${path.relative(options.root, outputDir)}`);
1061
+ const gOpts: GraphRenderOptions = {
1062
+ focus: options.focus,
1063
+ focusDepth: options.focusDepth,
1064
+ collapse: options.collapse,
1065
+ };
989
1066
  const outputFiles = writeMultiFileReport(
990
1067
  outputDir,
991
1068
  report,
992
1069
  options,
993
1070
  dependencyState,
994
1071
  dependencySummary,
995
- fileCriticalityByPath
1072
+ fileCriticalityByPath,
1073
+ gOpts
996
1074
  );
997
1075
  if (!options.json) {
998
1076
  const relDir = path.relative(options.root, outputDir);
@@ -1002,13 +1080,39 @@ function analyzeAndReport(state: ScanState): void {
1002
1080
  }
1003
1081
  }
1004
1082
  }
1083
+
1084
+ bus.progress('done', 'Scan complete', `${findings.length} findings`);
1085
+
1086
+ if (options.atLeast != null) {
1087
+ const totalFiles = summary.totalFiles ?? 1;
1088
+ const gateScore = computeGateScore(findings.length, totalFiles);
1089
+ if (gateScore < options.atLeast) {
1090
+ console.error(
1091
+ `Gate score ${gateScore} is below --at-least threshold ${options.atLeast}`
1092
+ );
1093
+ return -1;
1094
+ }
1095
+ }
1096
+
1097
+ return findings.length;
1098
+ }
1099
+
1100
+ export function computeGateScore(findingsCount: number, totalFiles: number): number {
1101
+ const ratio = findingsCount / Math.max(totalFiles, 1);
1102
+ return Math.round(100 / (1 + ratio / 10));
1005
1103
  }
1006
1104
 
1007
- async function main(): Promise<void> {
1008
- const state = await initScanState();
1009
- if (!state) return;
1105
+ export const EXIT_SUCCESS = 0;
1106
+ export const EXIT_FINDINGS = 1;
1107
+ export const EXIT_ERROR = 2;
1108
+
1109
+ async function main(options?: AnalysisOptions): Promise<number> {
1110
+ const state = await initScanState(options);
1111
+ if (!state) return EXIT_SUCCESS;
1010
1112
  collectFileData(state);
1011
- analyzeAndReport(state);
1113
+ const findingsCount = analyzeAndReport(state);
1114
+ if (findingsCount < 0) return EXIT_FINDINGS;
1115
+ return findingsCount > 0 ? EXIT_FINDINGS : EXIT_SUCCESS;
1012
1116
  }
1013
1117
 
1014
1118
  export { main };
@@ -1067,8 +1171,17 @@ function buildFindingStats(
1067
1171
  }
1068
1172
 
1069
1173
  if (isDirectRun(import.meta.url)) {
1070
- main().catch((error: unknown) => {
1071
- console.error(error);
1072
- process.exit(1);
1073
- });
1174
+ main()
1175
+ .then(code => {
1176
+ process.exitCode = code;
1177
+ })
1178
+ .catch((error: unknown) => {
1179
+ if (error instanceof OptionsError) {
1180
+ console.error(error.message);
1181
+ process.exitCode = EXIT_ERROR;
1182
+ } else {
1183
+ console.error(error);
1184
+ process.exitCode = EXIT_ERROR;
1185
+ }
1186
+ });
1074
1187
  }
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from 'node:events';
2
+
3
+ export type ProgressPhase =
4
+ | 'startup'
5
+ | 'cache-check'
6
+ | 'discovery'
7
+ | 'parse'
8
+ | 'dependencies'
9
+ | 'semantic'
10
+ | 'detect'
11
+ | 'graph'
12
+ | 'report'
13
+ | 'write'
14
+ | 'done';
15
+
16
+ export interface ProgressEvent {
17
+ phase: ProgressPhase;
18
+ message: string;
19
+ progress?: number;
20
+ detail?: string;
21
+ }
22
+
23
+ class ScanBus extends EventEmitter {
24
+ progress(phase: ProgressPhase, message: string, detail?: string): void {
25
+ this.emit('progress', { phase, message, detail } satisfies ProgressEvent);
26
+ }
27
+
28
+ summary(message: string): void {
29
+ this.emit('summary', message);
30
+ }
31
+
32
+ error(message: string, detail?: string): void {
33
+ this.emit('error', { message, detail });
34
+ }
35
+
36
+ reset(): void {
37
+ this.removeAllListeners();
38
+ }
39
+ }
40
+
41
+ export const bus = new ScanBus();
42
+
43
+ export function attachConsoleFeedback(): void {
44
+ bus.on('progress', (event: ProgressEvent) => {
45
+ if (event.detail) {
46
+ process.stderr.write(`[${event.phase}] ${event.message}: ${event.detail}\n`);
47
+ } else {
48
+ process.stderr.write(`[${event.phase}] ${event.message}\n`);
49
+ }
50
+ });
51
+ }
@@ -0,0 +1,155 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { formatFindings } from './reporters.js';
4
+ import type { Finding } from '../types/index.js';
5
+
6
+ function makeFinding(overrides: Partial<Finding>): Finding {
7
+ return {
8
+ id: 'f1',
9
+ severity: 'high',
10
+ category: 'dead-export',
11
+ file: '/repo/src/a.ts',
12
+ lineStart: 10,
13
+ lineEnd: 15,
14
+ title: 'Unused export foo',
15
+ reason: 'No consumers',
16
+ files: ['/repo/src/a.ts'],
17
+ suggestedFix: { strategy: 'remove', steps: ['Delete'] },
18
+ ...overrides,
19
+ };
20
+ }
21
+
22
+ describe('formatFindings', () => {
23
+ describe('compact reporter', () => {
24
+ it('formats one-line per finding with line number', () => {
25
+ const findings = [
26
+ makeFinding({
27
+ severity: 'high',
28
+ file: '/repo/src/a.ts',
29
+ lineStart: 10,
30
+ category: 'dead-export',
31
+ title: 'Unused foo',
32
+ }),
33
+ ];
34
+
35
+ const result = formatFindings(findings, 'compact', '/repo');
36
+ expect(result).toBe('high:src/a.ts:10 - [dead-export] Unused foo');
37
+ });
38
+
39
+ it('omits line number when lineStart is 0', () => {
40
+ const findings = [
41
+ makeFinding({
42
+ severity: 'low',
43
+ file: '/repo/src/b.ts',
44
+ lineStart: 0,
45
+ category: 'god-function',
46
+ title: 'Big func',
47
+ }),
48
+ ];
49
+
50
+ const result = formatFindings(findings, 'compact', '/repo');
51
+ expect(result).toBe('low:src/b.ts - [god-function] Big func');
52
+ });
53
+
54
+ it('handles empty findings list', () => {
55
+ expect(formatFindings([], 'compact', '/repo')).toBe('');
56
+ });
57
+
58
+ it('formats multiple findings separated by newlines', () => {
59
+ const findings = [
60
+ makeFinding({ severity: 'critical', title: 'A' }),
61
+ makeFinding({ severity: 'low', title: 'B' }),
62
+ ];
63
+ const lines = formatFindings(findings, 'compact', '/repo').split('\n');
64
+ expect(lines).toHaveLength(2);
65
+ });
66
+
67
+ it('handles file paths not under root', () => {
68
+ const findings = [
69
+ makeFinding({ file: '/other/place/x.ts', lineStart: 5, title: 'Out' }),
70
+ ];
71
+ const result = formatFindings(findings, 'compact', '/repo');
72
+ expect(result).toContain('/other/place/x.ts:5');
73
+ });
74
+ });
75
+
76
+ describe('github-actions reporter', () => {
77
+ it('maps critical severity to ::error', () => {
78
+ const findings = [
79
+ makeFinding({ severity: 'critical', lineStart: 5, title: 'Leak' }),
80
+ ];
81
+ const result = formatFindings(findings, 'github-actions', '/repo');
82
+ expect(result.startsWith('::error ')).toBe(true);
83
+ });
84
+
85
+ it('maps high severity to ::error', () => {
86
+ const findings = [
87
+ makeFinding({ severity: 'high', lineStart: 5, title: 'Bad' }),
88
+ ];
89
+ const result = formatFindings(findings, 'github-actions', '/repo');
90
+ expect(result.startsWith('::error ')).toBe(true);
91
+ });
92
+
93
+ it('maps medium severity to ::warning', () => {
94
+ const findings = [
95
+ makeFinding({ severity: 'medium', lineStart: 5, title: 'Warn' }),
96
+ ];
97
+ const result = formatFindings(findings, 'github-actions', '/repo');
98
+ expect(result.startsWith('::warning ')).toBe(true);
99
+ });
100
+
101
+ it('maps low severity to ::notice', () => {
102
+ const findings = [
103
+ makeFinding({ severity: 'low', lineStart: 5, title: 'Info' }),
104
+ ];
105
+ const result = formatFindings(findings, 'github-actions', '/repo');
106
+ expect(result.startsWith('::notice ')).toBe(true);
107
+ });
108
+
109
+ it('maps info severity to ::warning (default)', () => {
110
+ const findings = [
111
+ makeFinding({ severity: 'info', lineStart: 5, title: 'Note' }),
112
+ ];
113
+ const result = formatFindings(findings, 'github-actions', '/repo');
114
+ expect(result.startsWith('::warning ')).toBe(true);
115
+ });
116
+
117
+ it('includes file and line in annotation', () => {
118
+ const findings = [
119
+ makeFinding({
120
+ severity: 'high',
121
+ file: '/repo/src/a.ts',
122
+ lineStart: 42,
123
+ title: 'Found it',
124
+ category: 'unsafe-any',
125
+ }),
126
+ ];
127
+ const result = formatFindings(findings, 'github-actions', '/repo');
128
+ expect(result).toBe(
129
+ '::error file=src/a.ts,line=42::Found it [unsafe-any]'
130
+ );
131
+ });
132
+
133
+ it('defaults to line 1 when lineStart is 0', () => {
134
+ const findings = [
135
+ makeFinding({ severity: 'medium', lineStart: 0, title: 'X' }),
136
+ ];
137
+ const result = formatFindings(findings, 'github-actions', '/repo');
138
+ expect(result).toContain('line=1');
139
+ });
140
+
141
+ it('handles empty findings list', () => {
142
+ expect(formatFindings([], 'github-actions', '/repo')).toBe('');
143
+ });
144
+ });
145
+
146
+ describe('default reporter', () => {
147
+ it('returns empty string for default format', () => {
148
+ expect(formatFindings([], 'default', '/repo')).toBe('');
149
+ });
150
+
151
+ it('returns empty string even with findings', () => {
152
+ expect(formatFindings([makeFinding({})], 'default', '/repo')).toBe('');
153
+ });
154
+ });
155
+ });