@oddessentials/odd-ai-reviewers 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (370) hide show
  1. package/README.md +190 -0
  2. package/dist/__tests__/hermetic-setup.d.ts +55 -0
  3. package/dist/__tests__/hermetic-setup.d.ts.map +1 -0
  4. package/dist/__tests__/hermetic-setup.js +62 -0
  5. package/dist/__tests__/hermetic-setup.js.map +1 -0
  6. package/dist/__tests__/test-utils/hermetic.d.ts +84 -0
  7. package/dist/__tests__/test-utils/hermetic.d.ts.map +1 -0
  8. package/dist/__tests__/test-utils/hermetic.js +147 -0
  9. package/dist/__tests__/test-utils/hermetic.js.map +1 -0
  10. package/dist/agents/ai_semantic_review.d.ts +12 -0
  11. package/dist/agents/ai_semantic_review.d.ts.map +1 -0
  12. package/dist/agents/ai_semantic_review.js +317 -0
  13. package/dist/agents/ai_semantic_review.js.map +1 -0
  14. package/dist/agents/control_flow/budget.d.ts +162 -0
  15. package/dist/agents/control_flow/budget.d.ts.map +1 -0
  16. package/dist/agents/control_flow/budget.js +331 -0
  17. package/dist/agents/control_flow/budget.js.map +1 -0
  18. package/dist/agents/control_flow/cfg-builder.d.ts +26 -0
  19. package/dist/agents/control_flow/cfg-builder.d.ts.map +1 -0
  20. package/dist/agents/control_flow/cfg-builder.js +776 -0
  21. package/dist/agents/control_flow/cfg-builder.js.map +1 -0
  22. package/dist/agents/control_flow/cfg-types.d.ts +186 -0
  23. package/dist/agents/control_flow/cfg-types.d.ts.map +1 -0
  24. package/dist/agents/control_flow/cfg-types.js +114 -0
  25. package/dist/agents/control_flow/cfg-types.js.map +1 -0
  26. package/dist/agents/control_flow/finding-generator.d.ts +118 -0
  27. package/dist/agents/control_flow/finding-generator.d.ts.map +1 -0
  28. package/dist/agents/control_flow/finding-generator.js +354 -0
  29. package/dist/agents/control_flow/finding-generator.js.map +1 -0
  30. package/dist/agents/control_flow/index.d.ts +39 -0
  31. package/dist/agents/control_flow/index.d.ts.map +1 -0
  32. package/dist/agents/control_flow/index.js +270 -0
  33. package/dist/agents/control_flow/index.js.map +1 -0
  34. package/dist/agents/control_flow/logger.d.ts +333 -0
  35. package/dist/agents/control_flow/logger.d.ts.map +1 -0
  36. package/dist/agents/control_flow/logger.js +607 -0
  37. package/dist/agents/control_flow/logger.js.map +1 -0
  38. package/dist/agents/control_flow/mitigation-detector.d.ts +207 -0
  39. package/dist/agents/control_flow/mitigation-detector.d.ts.map +1 -0
  40. package/dist/agents/control_flow/mitigation-detector.js +625 -0
  41. package/dist/agents/control_flow/mitigation-detector.js.map +1 -0
  42. package/dist/agents/control_flow/mitigation-patterns.d.ts +53 -0
  43. package/dist/agents/control_flow/mitigation-patterns.d.ts.map +1 -0
  44. package/dist/agents/control_flow/mitigation-patterns.js +620 -0
  45. package/dist/agents/control_flow/mitigation-patterns.js.map +1 -0
  46. package/dist/agents/control_flow/path-analyzer.d.ts +287 -0
  47. package/dist/agents/control_flow/path-analyzer.d.ts.map +1 -0
  48. package/dist/agents/control_flow/path-analyzer.js +695 -0
  49. package/dist/agents/control_flow/path-analyzer.js.map +1 -0
  50. package/dist/agents/control_flow/pattern-validator.d.ts +132 -0
  51. package/dist/agents/control_flow/pattern-validator.d.ts.map +1 -0
  52. package/dist/agents/control_flow/pattern-validator.js +420 -0
  53. package/dist/agents/control_flow/pattern-validator.js.map +1 -0
  54. package/dist/agents/control_flow/timeout-regex.d.ts +144 -0
  55. package/dist/agents/control_flow/timeout-regex.d.ts.map +1 -0
  56. package/dist/agents/control_flow/timeout-regex.js +339 -0
  57. package/dist/agents/control_flow/timeout-regex.js.map +1 -0
  58. package/dist/agents/control_flow/types.d.ts +782 -0
  59. package/dist/agents/control_flow/types.d.ts.map +1 -0
  60. package/dist/agents/control_flow/types.js +428 -0
  61. package/dist/agents/control_flow/types.js.map +1 -0
  62. package/dist/agents/control_flow/vulnerability-detector.d.ts +85 -0
  63. package/dist/agents/control_flow/vulnerability-detector.d.ts.map +1 -0
  64. package/dist/agents/control_flow/vulnerability-detector.js +493 -0
  65. package/dist/agents/control_flow/vulnerability-detector.js.map +1 -0
  66. package/dist/agents/date-utils.d.ts +19 -0
  67. package/dist/agents/date-utils.d.ts.map +1 -0
  68. package/dist/agents/date-utils.js +29 -0
  69. package/dist/agents/date-utils.js.map +1 -0
  70. package/dist/agents/index.d.ts +25 -0
  71. package/dist/agents/index.d.ts.map +1 -0
  72. package/dist/agents/index.js +50 -0
  73. package/dist/agents/index.js.map +1 -0
  74. package/dist/agents/json-utils.d.ts +34 -0
  75. package/dist/agents/json-utils.d.ts.map +1 -0
  76. package/dist/agents/json-utils.js +62 -0
  77. package/dist/agents/json-utils.js.map +1 -0
  78. package/dist/agents/local_llm.d.ts +24 -0
  79. package/dist/agents/local_llm.d.ts.map +1 -0
  80. package/dist/agents/local_llm.js +566 -0
  81. package/dist/agents/local_llm.js.map +1 -0
  82. package/dist/agents/metadata.d.ts +57 -0
  83. package/dist/agents/metadata.d.ts.map +1 -0
  84. package/dist/agents/metadata.js +45 -0
  85. package/dist/agents/metadata.js.map +1 -0
  86. package/dist/agents/opencode.d.ts +18 -0
  87. package/dist/agents/opencode.d.ts.map +1 -0
  88. package/dist/agents/opencode.js +364 -0
  89. package/dist/agents/opencode.js.map +1 -0
  90. package/dist/agents/path-filter.d.ts +25 -0
  91. package/dist/agents/path-filter.d.ts.map +1 -0
  92. package/dist/agents/path-filter.js +43 -0
  93. package/dist/agents/path-filter.js.map +1 -0
  94. package/dist/agents/pr_agent.d.ts +3 -0
  95. package/dist/agents/pr_agent.d.ts.map +1 -0
  96. package/dist/agents/pr_agent.js +312 -0
  97. package/dist/agents/pr_agent.js.map +1 -0
  98. package/dist/agents/retry.d.ts +12 -0
  99. package/dist/agents/retry.d.ts.map +1 -0
  100. package/dist/agents/retry.js +65 -0
  101. package/dist/agents/retry.js.map +1 -0
  102. package/dist/agents/reviewdog.d.ts +24 -0
  103. package/dist/agents/reviewdog.d.ts.map +1 -0
  104. package/dist/agents/reviewdog.js +259 -0
  105. package/dist/agents/reviewdog.js.map +1 -0
  106. package/dist/agents/security.d.ts +49 -0
  107. package/dist/agents/security.d.ts.map +1 -0
  108. package/dist/agents/security.js +302 -0
  109. package/dist/agents/security.js.map +1 -0
  110. package/dist/agents/semgrep.d.ts +8 -0
  111. package/dist/agents/semgrep.d.ts.map +1 -0
  112. package/dist/agents/semgrep.js +157 -0
  113. package/dist/agents/semgrep.js.map +1 -0
  114. package/dist/agents/types.d.ts +450 -0
  115. package/dist/agents/types.d.ts.map +1 -0
  116. package/dist/agents/types.js +127 -0
  117. package/dist/agents/types.js.map +1 -0
  118. package/dist/budget.d.ts +59 -0
  119. package/dist/budget.d.ts.map +1 -0
  120. package/dist/budget.js +82 -0
  121. package/dist/budget.js.map +1 -0
  122. package/dist/cache/key.d.ts +49 -0
  123. package/dist/cache/key.d.ts.map +1 -0
  124. package/dist/cache/key.js +71 -0
  125. package/dist/cache/key.js.map +1 -0
  126. package/dist/cache/store.d.ts +47 -0
  127. package/dist/cache/store.d.ts.map +1 -0
  128. package/dist/cache/store.js +328 -0
  129. package/dist/cache/store.js.map +1 -0
  130. package/dist/cli/commands/check.d.ts +60 -0
  131. package/dist/cli/commands/check.d.ts.map +1 -0
  132. package/dist/cli/commands/check.js +163 -0
  133. package/dist/cli/commands/check.js.map +1 -0
  134. package/dist/cli/commands/index.d.ts +12 -0
  135. package/dist/cli/commands/index.d.ts.map +1 -0
  136. package/dist/cli/commands/index.js +12 -0
  137. package/dist/cli/commands/index.js.map +1 -0
  138. package/dist/cli/commands/local-review.d.ts +149 -0
  139. package/dist/cli/commands/local-review.d.ts.map +1 -0
  140. package/dist/cli/commands/local-review.js +755 -0
  141. package/dist/cli/commands/local-review.js.map +1 -0
  142. package/dist/cli/config-wizard.d.ts +87 -0
  143. package/dist/cli/config-wizard.d.ts.map +1 -0
  144. package/dist/cli/config-wizard.js +240 -0
  145. package/dist/cli/config-wizard.js.map +1 -0
  146. package/dist/cli/dependencies/catalog.d.ts +44 -0
  147. package/dist/cli/dependencies/catalog.d.ts.map +1 -0
  148. package/dist/cli/dependencies/catalog.js +89 -0
  149. package/dist/cli/dependencies/catalog.js.map +1 -0
  150. package/dist/cli/dependencies/checker.d.ts +42 -0
  151. package/dist/cli/dependencies/checker.d.ts.map +1 -0
  152. package/dist/cli/dependencies/checker.js +240 -0
  153. package/dist/cli/dependencies/checker.js.map +1 -0
  154. package/dist/cli/dependencies/index.d.ts +16 -0
  155. package/dist/cli/dependencies/index.d.ts.map +1 -0
  156. package/dist/cli/dependencies/index.js +16 -0
  157. package/dist/cli/dependencies/index.js.map +1 -0
  158. package/dist/cli/dependencies/messages.d.ts +58 -0
  159. package/dist/cli/dependencies/messages.d.ts.map +1 -0
  160. package/dist/cli/dependencies/messages.js +183 -0
  161. package/dist/cli/dependencies/messages.js.map +1 -0
  162. package/dist/cli/dependencies/platform.d.ts +25 -0
  163. package/dist/cli/dependencies/platform.d.ts.map +1 -0
  164. package/dist/cli/dependencies/platform.js +42 -0
  165. package/dist/cli/dependencies/platform.js.map +1 -0
  166. package/dist/cli/dependencies/schemas.d.ts +65 -0
  167. package/dist/cli/dependencies/schemas.d.ts.map +1 -0
  168. package/dist/cli/dependencies/schemas.js +42 -0
  169. package/dist/cli/dependencies/schemas.js.map +1 -0
  170. package/dist/cli/dependencies/types.d.ts +112 -0
  171. package/dist/cli/dependencies/types.d.ts.map +1 -0
  172. package/dist/cli/dependencies/types.js +6 -0
  173. package/dist/cli/dependencies/types.js.map +1 -0
  174. package/dist/cli/dependencies/version.d.ts +67 -0
  175. package/dist/cli/dependencies/version.d.ts.map +1 -0
  176. package/dist/cli/dependencies/version.js +125 -0
  177. package/dist/cli/dependencies/version.js.map +1 -0
  178. package/dist/cli/git-context.d.ts +105 -0
  179. package/dist/cli/git-context.d.ts.map +1 -0
  180. package/dist/cli/git-context.js +313 -0
  181. package/dist/cli/git-context.js.map +1 -0
  182. package/dist/cli/interactive-prompts.d.ts +126 -0
  183. package/dist/cli/interactive-prompts.d.ts.map +1 -0
  184. package/dist/cli/interactive-prompts.js +128 -0
  185. package/dist/cli/interactive-prompts.js.map +1 -0
  186. package/dist/cli/options/index.d.ts +7 -0
  187. package/dist/cli/options/index.d.ts.map +1 -0
  188. package/dist/cli/options/index.js +11 -0
  189. package/dist/cli/options/index.js.map +1 -0
  190. package/dist/cli/options/local-review-options.d.ts +221 -0
  191. package/dist/cli/options/local-review-options.d.ts.map +1 -0
  192. package/dist/cli/options/local-review-options.js +332 -0
  193. package/dist/cli/options/local-review-options.js.map +1 -0
  194. package/dist/cli/output/colors.d.ts +154 -0
  195. package/dist/cli/output/colors.d.ts.map +1 -0
  196. package/dist/cli/output/colors.js +255 -0
  197. package/dist/cli/output/colors.js.map +1 -0
  198. package/dist/cli/output/errors.d.ts +157 -0
  199. package/dist/cli/output/errors.d.ts.map +1 -0
  200. package/dist/cli/output/errors.js +266 -0
  201. package/dist/cli/output/errors.js.map +1 -0
  202. package/dist/cli/output/index.d.ts +12 -0
  203. package/dist/cli/output/index.d.ts.map +1 -0
  204. package/dist/cli/output/index.js +15 -0
  205. package/dist/cli/output/index.js.map +1 -0
  206. package/dist/cli/output/progress.d.ts +237 -0
  207. package/dist/cli/output/progress.d.ts.map +1 -0
  208. package/dist/cli/output/progress.js +405 -0
  209. package/dist/cli/output/progress.js.map +1 -0
  210. package/dist/cli/signals.d.ts +145 -0
  211. package/dist/cli/signals.d.ts.map +1 -0
  212. package/dist/cli/signals.js +223 -0
  213. package/dist/cli/signals.js.map +1 -0
  214. package/dist/cli/validation-report.d.ts +106 -0
  215. package/dist/cli/validation-report.d.ts.map +1 -0
  216. package/dist/cli/validation-report.js +108 -0
  217. package/dist/cli/validation-report.js.map +1 -0
  218. package/dist/config/index.d.ts +9 -0
  219. package/dist/config/index.d.ts.map +1 -0
  220. package/dist/config/index.js +12 -0
  221. package/dist/config/index.js.map +1 -0
  222. package/dist/config/mitigation-config.d.ts +94 -0
  223. package/dist/config/mitigation-config.d.ts.map +1 -0
  224. package/dist/config/mitigation-config.js +430 -0
  225. package/dist/config/mitigation-config.js.map +1 -0
  226. package/dist/config/providers.d.ts +118 -0
  227. package/dist/config/providers.d.ts.map +1 -0
  228. package/dist/config/providers.js +229 -0
  229. package/dist/config/providers.js.map +1 -0
  230. package/dist/config/schemas.d.ts +278 -0
  231. package/dist/config/schemas.d.ts.map +1 -0
  232. package/dist/config/schemas.js +111 -0
  233. package/dist/config/schemas.js.map +1 -0
  234. package/dist/config/zero-config.d.ts +126 -0
  235. package/dist/config/zero-config.d.ts.map +1 -0
  236. package/dist/config/zero-config.js +243 -0
  237. package/dist/config/zero-config.js.map +1 -0
  238. package/dist/config.d.ts +110 -0
  239. package/dist/config.d.ts.map +1 -0
  240. package/dist/config.js +302 -0
  241. package/dist/config.js.map +1 -0
  242. package/dist/diff.d.ts +224 -0
  243. package/dist/diff.d.ts.map +1 -0
  244. package/dist/diff.js +832 -0
  245. package/dist/diff.js.map +1 -0
  246. package/dist/git-validators.d.ts +106 -0
  247. package/dist/git-validators.d.ts.map +1 -0
  248. package/dist/git-validators.js +224 -0
  249. package/dist/git-validators.js.map +1 -0
  250. package/dist/main.d.ts +61 -0
  251. package/dist/main.d.ts.map +1 -0
  252. package/dist/main.js +704 -0
  253. package/dist/main.js.map +1 -0
  254. package/dist/phases/execute.d.ts +60 -0
  255. package/dist/phases/execute.d.ts.map +1 -0
  256. package/dist/phases/execute.js +168 -0
  257. package/dist/phases/execute.js.map +1 -0
  258. package/dist/phases/index.d.ts +9 -0
  259. package/dist/phases/index.d.ts.map +1 -0
  260. package/dist/phases/index.js +9 -0
  261. package/dist/phases/index.js.map +1 -0
  262. package/dist/phases/preflight.d.ts +40 -0
  263. package/dist/phases/preflight.d.ts.map +1 -0
  264. package/dist/phases/preflight.js +122 -0
  265. package/dist/phases/preflight.js.map +1 -0
  266. package/dist/phases/report.d.ts +51 -0
  267. package/dist/phases/report.d.ts.map +1 -0
  268. package/dist/phases/report.js +152 -0
  269. package/dist/phases/report.js.map +1 -0
  270. package/dist/policy.d.ts +33 -0
  271. package/dist/policy.d.ts.map +1 -0
  272. package/dist/policy.js +34 -0
  273. package/dist/policy.js.map +1 -0
  274. package/dist/preflight.d.ts +181 -0
  275. package/dist/preflight.d.ts.map +1 -0
  276. package/dist/preflight.js +627 -0
  277. package/dist/preflight.js.map +1 -0
  278. package/dist/report/ado.d.ts +53 -0
  279. package/dist/report/ado.d.ts.map +1 -0
  280. package/dist/report/ado.js +411 -0
  281. package/dist/report/ado.js.map +1 -0
  282. package/dist/report/agent-icons.d.ts +36 -0
  283. package/dist/report/agent-icons.d.ts.map +1 -0
  284. package/dist/report/agent-icons.js +46 -0
  285. package/dist/report/agent-icons.js.map +1 -0
  286. package/dist/report/base.d.ts +30 -0
  287. package/dist/report/base.d.ts.map +1 -0
  288. package/dist/report/base.js +64 -0
  289. package/dist/report/base.js.map +1 -0
  290. package/dist/report/formats.d.ts +206 -0
  291. package/dist/report/formats.d.ts.map +1 -0
  292. package/dist/report/formats.js +481 -0
  293. package/dist/report/formats.js.map +1 -0
  294. package/dist/report/github.d.ts +44 -0
  295. package/dist/report/github.d.ts.map +1 -0
  296. package/dist/report/github.js +409 -0
  297. package/dist/report/github.js.map +1 -0
  298. package/dist/report/line-resolver.d.ts +208 -0
  299. package/dist/report/line-resolver.d.ts.map +1 -0
  300. package/dist/report/line-resolver.js +578 -0
  301. package/dist/report/line-resolver.js.map +1 -0
  302. package/dist/report/resolution.d.ts +158 -0
  303. package/dist/report/resolution.d.ts.map +1 -0
  304. package/dist/report/resolution.js +272 -0
  305. package/dist/report/resolution.js.map +1 -0
  306. package/dist/report/sanitize.d.ts +32 -0
  307. package/dist/report/sanitize.d.ts.map +1 -0
  308. package/dist/report/sanitize.js +84 -0
  309. package/dist/report/sanitize.js.map +1 -0
  310. package/dist/report/terminal.d.ts +440 -0
  311. package/dist/report/terminal.d.ts.map +1 -0
  312. package/dist/report/terminal.js +840 -0
  313. package/dist/report/terminal.js.map +1 -0
  314. package/dist/reviewignore.d.ts +125 -0
  315. package/dist/reviewignore.d.ts.map +1 -0
  316. package/dist/reviewignore.js +335 -0
  317. package/dist/reviewignore.js.map +1 -0
  318. package/dist/security-logger.d.ts +178 -0
  319. package/dist/security-logger.d.ts.map +1 -0
  320. package/dist/security-logger.js +256 -0
  321. package/dist/security-logger.js.map +1 -0
  322. package/dist/telemetry/backends/console.d.ts +24 -0
  323. package/dist/telemetry/backends/console.d.ts.map +1 -0
  324. package/dist/telemetry/backends/console.js +54 -0
  325. package/dist/telemetry/backends/console.js.map +1 -0
  326. package/dist/telemetry/backends/jsonl.d.ts +31 -0
  327. package/dist/telemetry/backends/jsonl.d.ts.map +1 -0
  328. package/dist/telemetry/backends/jsonl.js +121 -0
  329. package/dist/telemetry/backends/jsonl.js.map +1 -0
  330. package/dist/telemetry/emitter.d.ts +43 -0
  331. package/dist/telemetry/emitter.d.ts.map +1 -0
  332. package/dist/telemetry/emitter.js +83 -0
  333. package/dist/telemetry/emitter.js.map +1 -0
  334. package/dist/telemetry/hook.d.ts +53 -0
  335. package/dist/telemetry/hook.d.ts.map +1 -0
  336. package/dist/telemetry/hook.js +118 -0
  337. package/dist/telemetry/hook.js.map +1 -0
  338. package/dist/telemetry/index.d.ts +58 -0
  339. package/dist/telemetry/index.d.ts.map +1 -0
  340. package/dist/telemetry/index.js +143 -0
  341. package/dist/telemetry/index.js.map +1 -0
  342. package/dist/telemetry/types.d.ts +139 -0
  343. package/dist/telemetry/types.d.ts.map +1 -0
  344. package/dist/telemetry/types.js +133 -0
  345. package/dist/telemetry/types.js.map +1 -0
  346. package/dist/trust.d.ts +65 -0
  347. package/dist/trust.d.ts.map +1 -0
  348. package/dist/trust.js +78 -0
  349. package/dist/trust.js.map +1 -0
  350. package/dist/types/assert-never.d.ts +30 -0
  351. package/dist/types/assert-never.d.ts.map +1 -0
  352. package/dist/types/assert-never.js +32 -0
  353. package/dist/types/assert-never.js.map +1 -0
  354. package/dist/types/branded.d.ts +172 -0
  355. package/dist/types/branded.d.ts.map +1 -0
  356. package/dist/types/branded.js +262 -0
  357. package/dist/types/branded.js.map +1 -0
  358. package/dist/types/errors.d.ts +320 -0
  359. package/dist/types/errors.d.ts.map +1 -0
  360. package/dist/types/errors.js +551 -0
  361. package/dist/types/errors.js.map +1 -0
  362. package/dist/types/index.d.ts +37 -0
  363. package/dist/types/index.d.ts.map +1 -0
  364. package/dist/types/index.js +77 -0
  365. package/dist/types/index.js.map +1 -0
  366. package/dist/types/result.d.ts +323 -0
  367. package/dist/types/result.d.ts.map +1 -0
  368. package/dist/types/result.js +423 -0
  369. package/dist/types/result.js.map +1 -0
  370. package/package.json +63 -0
@@ -0,0 +1,840 @@
1
+ /**
2
+ * Terminal Reporter Module
3
+ *
4
+ * Formats findings for terminal display with support for:
5
+ * - Pretty format (colors, boxes, code snippets)
6
+ * - JSON format (with schema_version for compatibility)
7
+ * - SARIF 2.1.0 format (for IDE integration)
8
+ *
9
+ * Follows the same processing pipeline as GitHub/ADO reporters.
10
+ */
11
+ import { canonicalizeDiffFiles } from '../diff.js';
12
+ import { buildLineResolver, normalizeFindingsForDiff } from './line-resolver.js';
13
+ import { deduplicateFindings, deduplicatePartialFindings, sortFindings, countBySeverity, } from './formats.js';
14
+ import { ANSI, colorize, visibleLength, createColorizer, supportsUnicode, } from '../cli/output/colors.js';
15
+ import { formatDuration, getSeverityIndicator } from '../cli/output/progress.js';
16
+ // =============================================================================
17
+ // Constants
18
+ // =============================================================================
19
+ /** Current JSON output schema version */
20
+ export const JSON_SCHEMA_VERSION = '1.0.0';
21
+ /** SARIF schema URL */
22
+ export const SARIF_SCHEMA_URL = 'https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json';
23
+ /** Tool information URI */
24
+ export const TOOL_INFO_URI = 'https://github.com/oddessentials/odd-ai-reviewers';
25
+ /** Tool name for SARIF output */
26
+ export const TOOL_NAME = 'odd-ai-reviewers';
27
+ /** Default box width for findings */
28
+ const DEFAULT_BOX_WIDTH = 80;
29
+ /** Number of context lines for code snippets */
30
+ const CONTEXT_LINES = 3;
31
+ // =============================================================================
32
+ // Box Drawing Utilities
33
+ // =============================================================================
34
+ /**
35
+ * Unicode box drawing characters
36
+ */
37
+ export const BOX_CHARS = {
38
+ topLeft: '┌',
39
+ topRight: '┐',
40
+ bottomLeft: '└',
41
+ bottomRight: '┘',
42
+ horizontal: '─',
43
+ vertical: '│',
44
+ sectionDivider: '━',
45
+ };
46
+ /**
47
+ * ASCII fallback box drawing characters
48
+ */
49
+ export const ASCII_BOX_CHARS = {
50
+ topLeft: '+',
51
+ topRight: '+',
52
+ bottomLeft: '+',
53
+ bottomRight: '+',
54
+ horizontal: '-',
55
+ vertical: '|',
56
+ sectionDivider: '=',
57
+ };
58
+ /**
59
+ * Get box drawing characters based on unicode support
60
+ *
61
+ * @param useUnicode - Whether to use Unicode characters
62
+ * @returns Box drawing character set
63
+ */
64
+ export function getBoxChars(useUnicode) {
65
+ return useUnicode ? BOX_CHARS : ASCII_BOX_CHARS;
66
+ }
67
+ /**
68
+ * Draw a horizontal line
69
+ *
70
+ * @param width - Line width in characters
71
+ * @param char - Character to use
72
+ * @returns Horizontal line string
73
+ */
74
+ export function drawHorizontalLine(width, char) {
75
+ return char.repeat(width);
76
+ }
77
+ /**
78
+ * Draw a section divider
79
+ *
80
+ * @param width - Divider width
81
+ * @param useUnicode - Whether to use Unicode
82
+ * @returns Section divider string
83
+ */
84
+ export function drawSectionDivider(width, useUnicode) {
85
+ const chars = getBoxChars(useUnicode);
86
+ return chars.sectionDivider.repeat(width);
87
+ }
88
+ /**
89
+ * Pad text to fit within a box
90
+ *
91
+ * @param text - Text to pad
92
+ * @param width - Target width
93
+ * @param align - Alignment ('left', 'center', 'right')
94
+ * @returns Padded text
95
+ */
96
+ export function padToWidth(text, width, align = 'left') {
97
+ const visLen = visibleLength(text);
98
+ if (visLen >= width) {
99
+ return text;
100
+ }
101
+ const padding = width - visLen;
102
+ switch (align) {
103
+ case 'center': {
104
+ const leftPad = Math.floor(padding / 2);
105
+ const rightPad = padding - leftPad;
106
+ return ' '.repeat(leftPad) + text + ' '.repeat(rightPad);
107
+ }
108
+ case 'right':
109
+ return ' '.repeat(padding) + text;
110
+ case 'left':
111
+ default:
112
+ return text + ' '.repeat(padding);
113
+ }
114
+ }
115
+ /**
116
+ * Wrap text to fit within a maximum width
117
+ *
118
+ * @param text - Text to wrap
119
+ * @param maxWidth - Maximum width per line
120
+ * @returns Array of wrapped lines
121
+ */
122
+ export function wrapText(text, maxWidth) {
123
+ if (maxWidth <= 0)
124
+ return [text];
125
+ const words = text.split(/\s+/);
126
+ const lines = [];
127
+ let currentLine = '';
128
+ for (const word of words) {
129
+ if (!word)
130
+ continue;
131
+ const wordLen = visibleLength(word);
132
+ const currentLen = visibleLength(currentLine);
133
+ if (currentLen === 0) {
134
+ // First word on line - always add it even if too long
135
+ currentLine = word;
136
+ }
137
+ else if (currentLen + 1 + wordLen <= maxWidth) {
138
+ // Word fits with space
139
+ currentLine += ' ' + word;
140
+ }
141
+ else {
142
+ // Word doesn't fit - start new line
143
+ lines.push(currentLine);
144
+ currentLine = word;
145
+ }
146
+ }
147
+ if (currentLine) {
148
+ lines.push(currentLine);
149
+ }
150
+ return lines.length > 0 ? lines : [''];
151
+ }
152
+ // =============================================================================
153
+ // Code Snippet Extraction
154
+ // =============================================================================
155
+ /**
156
+ * Detect programming language from file extension
157
+ *
158
+ * @param filePath - File path
159
+ * @returns Language identifier or undefined
160
+ */
161
+ export function detectLanguage(filePath) {
162
+ const ext = filePath.split('.').pop()?.toLowerCase();
163
+ const langMap = {
164
+ ts: 'typescript',
165
+ tsx: 'typescript',
166
+ js: 'javascript',
167
+ jsx: 'javascript',
168
+ py: 'python',
169
+ rb: 'ruby',
170
+ go: 'go',
171
+ rs: 'rust',
172
+ java: 'java',
173
+ kt: 'kotlin',
174
+ swift: 'swift',
175
+ c: 'c',
176
+ cpp: 'cpp',
177
+ cc: 'cpp',
178
+ h: 'c',
179
+ hpp: 'cpp',
180
+ cs: 'csharp',
181
+ php: 'php',
182
+ sh: 'bash',
183
+ bash: 'bash',
184
+ zsh: 'bash',
185
+ yaml: 'yaml',
186
+ yml: 'yaml',
187
+ json: 'json',
188
+ md: 'markdown',
189
+ sql: 'sql',
190
+ css: 'css',
191
+ scss: 'scss',
192
+ less: 'less',
193
+ html: 'html',
194
+ xml: 'xml',
195
+ };
196
+ return ext ? langMap[ext] : undefined;
197
+ }
198
+ /**
199
+ * Extract code snippet from a diff patch for a specific line
200
+ *
201
+ * @param patch - Unified diff patch content
202
+ * @param targetLine - The line number to highlight (1-indexed in new file)
203
+ * @param contextLines - Number of context lines before/after
204
+ * @param filePath - File path for language detection
205
+ * @returns CodeSnippet or undefined if line not found
206
+ */
207
+ export function extractCodeSnippet(patch, targetLine, contextLines = CONTEXT_LINES, filePath) {
208
+ if (!patch || !targetLine) {
209
+ return undefined;
210
+ }
211
+ const lines = patch.split('\n');
212
+ const snippetLines = [];
213
+ let currentNewLine = 0;
214
+ let foundTarget = false;
215
+ let highlightIndex = -1;
216
+ // Build a map of new file lines from the patch
217
+ const newFileContent = [];
218
+ for (const line of lines) {
219
+ // Parse hunk header: @@ -oldStart,oldCount +newStart,newCount @@
220
+ if (line.startsWith('@@')) {
221
+ const match = line.match(/@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
222
+ if (match) {
223
+ currentNewLine = parseInt(match[1] ?? '0', 10);
224
+ }
225
+ continue;
226
+ }
227
+ // Skip metadata lines
228
+ if (line.startsWith('diff ') ||
229
+ line.startsWith('index ') ||
230
+ line.startsWith('--- ') ||
231
+ line.startsWith('+++ ')) {
232
+ continue;
233
+ }
234
+ const prefix = line[0];
235
+ if (prefix === '+') {
236
+ // Added line - exists in new file
237
+ newFileContent.push({
238
+ lineNumber: currentNewLine,
239
+ content: line.slice(1), // Remove prefix
240
+ });
241
+ currentNewLine++;
242
+ }
243
+ else if (prefix === '-') {
244
+ // Deleted line - does NOT exist in new file
245
+ // Don't increment currentNewLine
246
+ }
247
+ else if (prefix === ' ') {
248
+ // Context line - exists in new file
249
+ newFileContent.push({
250
+ lineNumber: currentNewLine,
251
+ content: line.slice(1), // Remove prefix
252
+ });
253
+ currentNewLine++;
254
+ }
255
+ else if (prefix === '\\') {
256
+ // "" marker - skip
257
+ }
258
+ }
259
+ // Find the target line and extract context
260
+ const targetIndex = newFileContent.findIndex((l) => l.lineNumber === targetLine);
261
+ if (targetIndex === -1) {
262
+ return undefined;
263
+ }
264
+ foundTarget = true;
265
+ // Calculate range for context
266
+ const startIdx = Math.max(0, targetIndex - contextLines);
267
+ const endIdx = Math.min(newFileContent.length - 1, targetIndex + contextLines);
268
+ for (let i = startIdx; i <= endIdx; i++) {
269
+ const lineData = newFileContent[i];
270
+ if (lineData) {
271
+ const isHighlighted = lineData.lineNumber === targetLine;
272
+ if (isHighlighted) {
273
+ highlightIndex = snippetLines.length;
274
+ }
275
+ snippetLines.push({
276
+ lineNumber: lineData.lineNumber,
277
+ content: lineData.content,
278
+ isHighlighted,
279
+ });
280
+ }
281
+ }
282
+ if (!foundTarget || snippetLines.length === 0) {
283
+ return undefined;
284
+ }
285
+ return {
286
+ lines: snippetLines,
287
+ highlightLine: highlightIndex,
288
+ language: filePath ? detectLanguage(filePath) : undefined,
289
+ };
290
+ }
291
+ // =============================================================================
292
+ // Finding Box Formatting
293
+ // =============================================================================
294
+ /**
295
+ * Format a code snippet for terminal display
296
+ *
297
+ * @param snippet - Code snippet to format
298
+ * @param colored - Whether to use colors
299
+ * @param boxWidth - Width of the containing box
300
+ * @returns Formatted code snippet lines
301
+ */
302
+ export function formatCodeSnippet(snippet, colored, boxWidth) {
303
+ const result = [];
304
+ const c = createColorizer(colored);
305
+ // Calculate line number width
306
+ const maxLineNum = Math.max(...snippet.lines.map((l) => l.lineNumber));
307
+ const lineNumWidth = Math.max(String(maxLineNum).length, 3);
308
+ // Content area width (inside box, accounting for padding and line numbers)
309
+ const contentWidth = boxWidth - 4 - lineNumWidth - 3; // 4 for box, 3 for " | "
310
+ for (const line of snippet.lines) {
311
+ const lineNumStr = String(line.lineNumber).padStart(lineNumWidth, ' ');
312
+ const prefix = line.isHighlighted ? '▸' : ' ';
313
+ // Truncate long lines
314
+ let content = line.content;
315
+ if (content.length > contentWidth) {
316
+ content = content.slice(0, contentWidth - 3) + '...';
317
+ }
318
+ if (line.isHighlighted) {
319
+ // Highlight the target line
320
+ const formattedLine = colored
321
+ ? `${c.cyan(prefix)} ${c.gray(lineNumStr)} ${c.gray('│')} ${colorize(content, ANSI.inverse, colored)}`
322
+ : `${prefix} ${lineNumStr} | ${content}`;
323
+ result.push(formattedLine);
324
+ }
325
+ else {
326
+ const formattedLine = colored
327
+ ? ` ${c.gray(lineNumStr)} ${c.gray('│')} ${c.dim(content)}`
328
+ : ` ${lineNumStr} | ${content}`;
329
+ result.push(formattedLine);
330
+ }
331
+ }
332
+ return result;
333
+ }
334
+ /**
335
+ * Format a single finding as a box
336
+ *
337
+ * @param finding - Finding to format
338
+ * @param context - Terminal context
339
+ * @param diffFiles - Diff files for code snippet extraction
340
+ * @param boxWidth - Width of the box
341
+ * @returns Formatted finding box as string
342
+ */
343
+ export function formatFindingBox(finding, context, diffFiles, boxWidth = DEFAULT_BOX_WIDTH) {
344
+ const colored = context.colored;
345
+ const useUnicode = context.useUnicode;
346
+ const chars = getBoxChars(useUnicode);
347
+ const c = createColorizer(colored);
348
+ const lines = [];
349
+ // Get severity color and indicator
350
+ const severityIndicator = getSeverityIndicator(finding.severity, useUnicode);
351
+ const severityLabel = finding.severity.toUpperCase();
352
+ // Content width inside the box (accounting for borders and padding)
353
+ const contentWidth = boxWidth - 4; // 2 for borders, 2 for padding
354
+ // ─────────────── Header ───────────────
355
+ // Format: ┌─ file.ts:42 ──────────────────────── ERROR ─┐
356
+ const location = finding.line ? `${finding.file}:${finding.line}` : finding.file;
357
+ const locationFormatted = colored ? c.cyan(location) : location;
358
+ const severityFormatted = colored
359
+ ? colorize(` ${severityIndicator} ${severityLabel} `, getSeverityColor(finding.severity), colored)
360
+ : ` ${severityIndicator} ${severityLabel} `;
361
+ // Calculate header spacing
362
+ const headerTextLen = visibleLength(location) + visibleLength(severityFormatted) + 4; // 4 for " ─ " and " ─"
363
+ const headerPadLen = Math.max(0, boxWidth - 2 - headerTextLen);
364
+ const topLine = `${chars.topLeft}${chars.horizontal} ${locationFormatted} ${drawHorizontalLine(headerPadLen, chars.horizontal)}${severityFormatted}${chars.horizontal}${chars.topRight}`;
365
+ lines.push(topLine);
366
+ // ─────────────── Message ───────────────
367
+ const messageLines = wrapText(finding.message, contentWidth);
368
+ for (const msgLine of messageLines) {
369
+ lines.push(`${chars.vertical} ${padToWidth(msgLine, contentWidth)} ${chars.vertical}`);
370
+ }
371
+ // Empty line after message
372
+ lines.push(`${chars.vertical} ${' '.repeat(contentWidth)} ${chars.vertical}`);
373
+ // ─────────────── Code Snippet ───────────────
374
+ if (finding.line && diffFiles) {
375
+ const file = diffFiles.find((f) => f.path === finding.file);
376
+ if (file?.patch) {
377
+ const snippet = extractCodeSnippet(file.patch, finding.line, CONTEXT_LINES, finding.file);
378
+ if (snippet) {
379
+ const snippetLines = formatCodeSnippet(snippet, colored, boxWidth);
380
+ for (const snippetLine of snippetLines) {
381
+ // Pad snippet lines to fit in box
382
+ const padding = contentWidth - visibleLength(snippetLine);
383
+ lines.push(`${chars.vertical} ${snippetLine}${' '.repeat(Math.max(0, padding))} ${chars.vertical}`);
384
+ }
385
+ // Empty line after code
386
+ lines.push(`${chars.vertical} ${' '.repeat(contentWidth)} ${chars.vertical}`);
387
+ }
388
+ }
389
+ }
390
+ // ─────────────── Suggestion ───────────────
391
+ if (finding.suggestion) {
392
+ const suggestionPrefix = '💡 ';
393
+ const suggestionContent = finding.suggestion;
394
+ const suggestionLines = wrapText(suggestionContent, contentWidth - 3);
395
+ for (let i = 0; i < suggestionLines.length; i++) {
396
+ const prefix = i === 0 ? suggestionPrefix : ' ';
397
+ const line = `${prefix}${suggestionLines[i] ?? ''}`;
398
+ const formattedLine = colored ? c.green(line) : line;
399
+ const padding = contentWidth - visibleLength(line);
400
+ lines.push(`${chars.vertical} ${formattedLine}${' '.repeat(Math.max(0, padding))} ${chars.vertical}`);
401
+ }
402
+ }
403
+ // ─────────────── Footer ───────────────
404
+ // Show source agent and rule ID if available
405
+ const footerParts = [];
406
+ if (finding.sourceAgent) {
407
+ footerParts.push(finding.sourceAgent);
408
+ }
409
+ if (finding.ruleId) {
410
+ footerParts.push(`[${finding.ruleId}]`);
411
+ }
412
+ if (footerParts.length > 0) {
413
+ const footerText = footerParts.join(' ');
414
+ const footerFormatted = colored ? c.gray(footerText) : footerText;
415
+ const footerLine = padToWidth(footerFormatted, contentWidth);
416
+ lines.push(`${chars.vertical} ${footerLine} ${chars.vertical}`);
417
+ }
418
+ // Bottom border
419
+ const bottomLine = `${chars.bottomLeft}${drawHorizontalLine(boxWidth - 2, chars.horizontal)}${chars.bottomRight}`;
420
+ lines.push(bottomLine);
421
+ return lines.join('\n');
422
+ }
423
+ /**
424
+ * Get ANSI color code for severity
425
+ */
426
+ function getSeverityColor(severity) {
427
+ switch (severity) {
428
+ case 'error':
429
+ return ANSI.red;
430
+ case 'warning':
431
+ return ANSI.yellow;
432
+ case 'info':
433
+ return ANSI.blue;
434
+ default:
435
+ return '';
436
+ }
437
+ }
438
+ /**
439
+ * Format all findings as a list of boxes
440
+ *
441
+ * @param findings - Findings to format
442
+ * @param context - Terminal context
443
+ * @param diffFiles - Diff files for code snippet extraction
444
+ * @returns Formatted findings list
445
+ */
446
+ export function formatFindingsList(findings, context, diffFiles) {
447
+ if (findings.length === 0) {
448
+ return '';
449
+ }
450
+ const boxes = findings.map((finding) => formatFindingBox(finding, context, diffFiles));
451
+ return boxes.join('\n\n');
452
+ }
453
+ // =============================================================================
454
+ // Summary Generation
455
+ // =============================================================================
456
+ /**
457
+ * Generate summary section with counts
458
+ *
459
+ * @param findings - Complete findings
460
+ * @param stats - Review statistics
461
+ * @param context - Terminal context
462
+ * @returns Formatted summary string
463
+ */
464
+ export function generateSummary(findings, stats, context) {
465
+ const c = createColorizer(context.colored);
466
+ const counts = countBySeverity(findings);
467
+ const lines = [];
468
+ // Header
469
+ lines.push('');
470
+ lines.push(c.bold('📊 SUMMARY'));
471
+ lines.push('');
472
+ // Counts section
473
+ const errorLabel = counts.error === 0
474
+ ? c.green(` Errors: ${counts.error}`)
475
+ : c.red(` Errors: ${counts.error}`);
476
+ const warningLabel = counts.warning === 0
477
+ ? c.green(` Warnings: ${counts.warning}`)
478
+ : c.yellow(` Warnings: ${counts.warning}`);
479
+ const infoLabel = ` Suggestions: ${counts.info}`;
480
+ lines.push(errorLabel);
481
+ lines.push(warningLabel);
482
+ lines.push(context.colored ? c.blue(infoLabel) : infoLabel);
483
+ lines.push('');
484
+ // Stats section
485
+ lines.push(` Files: ${stats.filesAnalyzed} analyzed`);
486
+ if (context.showCost && stats.estimatedCostUsd > 0) {
487
+ // Clamp cost to non-negative (FR-REL-002)
488
+ const cost = Math.max(0, stats.estimatedCostUsd);
489
+ lines.push(` Cost: $${cost.toFixed(4)} (estimated)`);
490
+ }
491
+ lines.push(` Time: ${formatDuration(stats.executionTimeMs)}`);
492
+ lines.push('');
493
+ return lines.join('\n');
494
+ }
495
+ /**
496
+ * Generate terminal summary (legacy interface)
497
+ */
498
+ export function generateTerminalSummary(findings, partialFindings, executionTimeMs, estimatedCostUsd) {
499
+ // Create minimal context for summary generation
500
+ const context = {
501
+ colored: true,
502
+ useUnicode: supportsUnicode(),
503
+ verbose: false,
504
+ quiet: false,
505
+ format: 'pretty',
506
+ showProgress: true,
507
+ showCost: true,
508
+ };
509
+ const allFindings = [...findings, ...partialFindings];
510
+ return generateSummary(allFindings, {
511
+ filesAnalyzed: 0,
512
+ linesChanged: 0,
513
+ executionTimeMs,
514
+ estimatedCostUsd,
515
+ }, context);
516
+ }
517
+ // =============================================================================
518
+ // Header Generation
519
+ // =============================================================================
520
+ /**
521
+ * Generate header section
522
+ *
523
+ * @param context - Terminal context
524
+ * @param stats - Stats for header
525
+ * @returns Formatted header string
526
+ */
527
+ export function generateHeader(context, stats) {
528
+ const c = createColorizer(context.colored);
529
+ const lines = [];
530
+ const version = context.version ?? 'unknown';
531
+ lines.push('');
532
+ lines.push(c.bold(`🔍 odd-ai-reviewers v${version}`));
533
+ lines.push(` Analyzing ${stats.fileCount} files (${stats.lineCount} lines changed)`);
534
+ if (context.configSource) {
535
+ const configDisplay = context.configSource.source === 'zero-config'
536
+ ? '(zero-config defaults)'
537
+ : (context.configSource.path ?? '.ai-review.yml');
538
+ lines.push(` Config: ${configDisplay} ✓`);
539
+ }
540
+ if (context.baseRef) {
541
+ const baseSource = context.baseSource ?? 'auto-detected';
542
+ lines.push(` Base: ${context.baseRef} (${baseSource})`);
543
+ }
544
+ lines.push('');
545
+ return lines.join('\n');
546
+ }
547
+ // =============================================================================
548
+ // Output Format Functions
549
+ // =============================================================================
550
+ /**
551
+ * Generate JSON output
552
+ *
553
+ * @param findings - Complete findings
554
+ * @param partialFindings - Partial findings from failed agents
555
+ * @param context - Terminal context
556
+ * @param diffFiles - Diff files for stats
557
+ * @returns JSON string (not pretty-printed)
558
+ */
559
+ export function generateJsonOutput(findings, partialFindings, context, diffFiles) {
560
+ const counts = countBySeverity(findings);
561
+ const totalLines = diffFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
562
+ const output = {
563
+ schema_version: JSON_SCHEMA_VERSION,
564
+ version: context.version ?? '0.0.0',
565
+ timestamp: new Date().toISOString(),
566
+ summary: {
567
+ errorCount: counts.error,
568
+ warningCount: counts.warning,
569
+ infoCount: counts.info,
570
+ filesAnalyzed: diffFiles.length,
571
+ linesChanged: totalLines,
572
+ executionTimeMs: context.executionTimeMs ?? 0,
573
+ // Clamp to non-negative (FR-REL-002)
574
+ estimatedCostUsd: Math.max(0, context.estimatedCostUsd ?? 0),
575
+ },
576
+ findings,
577
+ partialFindings,
578
+ passes: [], // Would need pass information from execution context
579
+ config: context.configSource ?? { source: 'file' },
580
+ };
581
+ // Single-line JSON output (no pretty-printing)
582
+ return JSON.stringify(output);
583
+ }
584
+ /**
585
+ * Map severity to SARIF level
586
+ */
587
+ function mapSeverityToSarifLevel(severity) {
588
+ switch (severity) {
589
+ case 'error':
590
+ return 'error';
591
+ case 'warning':
592
+ return 'warning';
593
+ case 'info':
594
+ return 'note';
595
+ default:
596
+ return 'note';
597
+ }
598
+ }
599
+ /**
600
+ * Generate SARIF 2.1.0 output
601
+ *
602
+ * @param findings - Complete findings
603
+ * @param context - Terminal context
604
+ * @returns SARIF JSON string
605
+ */
606
+ export function generateSarifOutput(findings, context) {
607
+ const results = findings.map((finding) => {
608
+ const result = {
609
+ ruleId: finding.ruleId ?? finding.sourceAgent,
610
+ level: mapSeverityToSarifLevel(finding.severity),
611
+ message: { text: finding.message },
612
+ locations: [
613
+ {
614
+ physicalLocation: {
615
+ artifactLocation: { uri: finding.file },
616
+ region: {
617
+ startLine: finding.line ?? 1,
618
+ ...(finding.endLine ? { endLine: finding.endLine } : {}),
619
+ },
620
+ },
621
+ },
622
+ ],
623
+ properties: {
624
+ sourceAgent: finding.sourceAgent,
625
+ },
626
+ };
627
+ // Add fix if suggestion exists
628
+ if (finding.suggestion) {
629
+ result.fixes = [
630
+ {
631
+ description: { text: finding.suggestion },
632
+ },
633
+ ];
634
+ }
635
+ return result;
636
+ });
637
+ const output = {
638
+ $schema: SARIF_SCHEMA_URL,
639
+ version: '2.1.0',
640
+ runs: [
641
+ {
642
+ tool: {
643
+ driver: {
644
+ name: TOOL_NAME,
645
+ version: context.version ?? '0.0.0',
646
+ informationUri: TOOL_INFO_URI,
647
+ rules: [], // Intentionally empty - AI agents don't have static rule IDs
648
+ },
649
+ },
650
+ results,
651
+ },
652
+ ],
653
+ };
654
+ return JSON.stringify(output);
655
+ }
656
+ /**
657
+ * Generate quiet mode output (errors only)
658
+ *
659
+ * @param findings - All findings
660
+ * @returns Minimal output string
661
+ */
662
+ export function generateQuietOutput(findings) {
663
+ const errorCount = findings.filter((f) => f.severity === 'error').length;
664
+ if (errorCount === 0) {
665
+ return 'No errors found\n';
666
+ }
667
+ return `${errorCount} error${errorCount === 1 ? '' : 's'} found\n`;
668
+ }
669
+ /**
670
+ * Generate verbose mode output additions
671
+ *
672
+ * @param context - Terminal context with verbose info
673
+ * @param diffFiles - Diff files for details
674
+ * @returns Verbose additions string
675
+ */
676
+ export function generateVerboseOutput(context, diffFiles) {
677
+ const c = createColorizer(context.colored);
678
+ const lines = [];
679
+ lines.push('');
680
+ lines.push(c.gray('─── Verbose Details ───'));
681
+ lines.push('');
682
+ // Git context details
683
+ if (context.baseRef) {
684
+ lines.push(c.gray(`Git base: ${context.baseRef}`));
685
+ }
686
+ // Config details
687
+ if (context.configSource) {
688
+ lines.push(c.gray(`Config: ${JSON.stringify(context.configSource)}`));
689
+ }
690
+ // File breakdown
691
+ lines.push(c.gray(''));
692
+ lines.push(c.gray('Files analyzed:'));
693
+ for (const file of diffFiles.slice(0, 10)) {
694
+ // Limit to first 10
695
+ lines.push(c.gray(` ${file.path} (+${file.additions}/-${file.deletions})`));
696
+ }
697
+ if (diffFiles.length > 10) {
698
+ lines.push(c.gray(` ... and ${diffFiles.length - 10} more`));
699
+ }
700
+ // Timing breakdown
701
+ if (context.executionTimeMs) {
702
+ lines.push(c.gray(''));
703
+ lines.push(c.gray(`Total execution time: ${formatDuration(context.executionTimeMs)}`));
704
+ }
705
+ lines.push('');
706
+ return lines.join('\n');
707
+ }
708
+ // =============================================================================
709
+ // Default Context
710
+ // =============================================================================
711
+ /**
712
+ * Create default terminal context
713
+ */
714
+ export function createDefaultContext() {
715
+ return {
716
+ colored: true,
717
+ useUnicode: supportsUnicode(),
718
+ verbose: false,
719
+ quiet: false,
720
+ format: 'pretty',
721
+ showProgress: true,
722
+ showCost: true,
723
+ };
724
+ }
725
+ // =============================================================================
726
+ // Main Reporter Function
727
+ // =============================================================================
728
+ /**
729
+ * Report findings to terminal.
730
+ *
731
+ * Follows the same pipeline as GitHub/ADO reporters:
732
+ * 1. Canonicalize diff files
733
+ * 2. Build line resolver
734
+ * 3. Normalize findings
735
+ * 4. Deduplicate
736
+ * 5. Sort
737
+ * 6. Format and output
738
+ *
739
+ * @param findings - Complete findings
740
+ * @param partialFindings - Findings from interrupted agents
741
+ * @param context - Terminal context (colors, format, etc.)
742
+ * @param config - Review configuration
743
+ * @param diffFiles - Diff files for context
744
+ * @returns Report result
745
+ */
746
+ export async function reportToTerminal(findings, partialFindings, context, config, diffFiles) {
747
+ try {
748
+ // 1. Canonicalize diff files
749
+ const canonicalFiles = canonicalizeDiffFiles(diffFiles);
750
+ // 2. Build line resolver
751
+ const resolver = buildLineResolver(canonicalFiles);
752
+ // 3. Normalize findings
753
+ const normalizedComplete = normalizeFindingsForDiff(findings, resolver);
754
+ const normalizedPartial = normalizeFindingsForDiff(partialFindings, resolver);
755
+ // 4. Deduplicate
756
+ const dedupedComplete = deduplicateFindings(normalizedComplete.findings);
757
+ const dedupedPartial = deduplicatePartialFindings(normalizedPartial.findings);
758
+ // 5. Sort
759
+ const sortedComplete = sortFindings(dedupedComplete);
760
+ const sortedPartial = sortFindings(dedupedPartial);
761
+ // For quiet mode, filter to errors only
762
+ const findingsToShow = context.quiet
763
+ ? sortedComplete.filter((f) => f.severity === 'error')
764
+ : sortedComplete;
765
+ // Calculate stats
766
+ const totalLines = canonicalFiles.reduce((sum, f) => sum + f.additions + f.deletions, 0);
767
+ const stats = {
768
+ fileCount: canonicalFiles.length,
769
+ lineCount: totalLines,
770
+ filesAnalyzed: canonicalFiles.length,
771
+ linesChanged: totalLines,
772
+ executionTimeMs: context.executionTimeMs ?? 0,
773
+ estimatedCostUsd: context.estimatedCostUsd ?? 0,
774
+ };
775
+ // 6. Format and output based on format type
776
+ let output;
777
+ switch (context.format) {
778
+ case 'json':
779
+ output = generateJsonOutput(sortedComplete, sortedPartial, context, canonicalFiles);
780
+ break;
781
+ case 'sarif':
782
+ output = generateSarifOutput(sortedComplete, context);
783
+ break;
784
+ case 'pretty':
785
+ default: {
786
+ const parts = [];
787
+ // Quiet mode: minimal output
788
+ if (context.quiet) {
789
+ output = generateQuietOutput(sortedComplete);
790
+ }
791
+ else {
792
+ // Header
793
+ parts.push(generateHeader(context, stats));
794
+ // Verbose details (before findings)
795
+ if (context.verbose) {
796
+ parts.push(generateVerboseOutput(context, canonicalFiles));
797
+ }
798
+ // Findings
799
+ if (findingsToShow.length > 0) {
800
+ parts.push(formatFindingsList(findingsToShow, context, canonicalFiles));
801
+ }
802
+ // Partial findings section
803
+ if (sortedPartial.length > 0 && !context.quiet) {
804
+ parts.push('');
805
+ parts.push(createColorizer(context.colored).yellow('⚠️ Partial Findings (from failed agents):'));
806
+ parts.push('');
807
+ parts.push(formatFindingsList(sortedPartial, context, canonicalFiles));
808
+ }
809
+ // Summary
810
+ parts.push(generateSummary(sortedComplete, stats, context));
811
+ output = parts.join('\n');
812
+ }
813
+ break;
814
+ }
815
+ }
816
+ // Write to stdout
817
+ process.stdout.write(output + '\n');
818
+ return {
819
+ success: true,
820
+ findingsCount: sortedComplete.length,
821
+ partialFindingsCount: sortedPartial.length,
822
+ };
823
+ }
824
+ catch (error) {
825
+ const errorMessage = error instanceof Error ? error.message : String(error);
826
+ return {
827
+ success: false,
828
+ findingsCount: 0,
829
+ partialFindingsCount: 0,
830
+ error: errorMessage,
831
+ };
832
+ }
833
+ }
834
+ /**
835
+ * Format a single finding for terminal display (legacy interface)
836
+ */
837
+ export function formatFindingForTerminal(finding, context) {
838
+ return formatFindingBox(finding, context);
839
+ }
840
+ //# sourceMappingURL=terminal.js.map