pi-lens 3.8.47 โ†’ 3.8.51

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 (492) hide show
  1. package/CHANGELOG.md +231 -1
  2. package/README.md +37 -6
  3. package/dist/clients/actionable-warnings-logger.js +51 -0
  4. package/dist/clients/actionable-warnings.js +542 -0
  5. package/dist/clients/agent-behavior-client.js +119 -0
  6. package/dist/clients/ast-grep-client.js +391 -0
  7. package/dist/clients/ast-grep-parser.js +86 -0
  8. package/dist/clients/ast-grep-rule-manager.js +91 -0
  9. package/dist/clients/ast-grep-tool-logger.js +150 -0
  10. package/dist/clients/ast-grep-types.js +9 -0
  11. package/dist/clients/ast-grep-yaml-synth.js +103 -0
  12. package/dist/clients/bash-file-access.js +360 -0
  13. package/dist/clients/biome-client.js +183 -0
  14. package/dist/clients/bootstrap.js +40 -0
  15. package/dist/clients/cache/rule-cache.js +75 -0
  16. package/dist/clients/cache-manager.js +273 -0
  17. package/dist/clients/call-graph.js +325 -0
  18. package/dist/clients/cascade-format.js +21 -0
  19. package/dist/clients/cascade-logger.js +25 -0
  20. package/dist/clients/cascade-types.js +1 -0
  21. package/dist/clients/code-quality-warnings.js +189 -0
  22. package/dist/clients/codebase-model.js +136 -0
  23. package/dist/clients/complexity-client.js +676 -0
  24. package/dist/clients/dependency-checker.js +359 -0
  25. package/dist/clients/diagnostic-logger.js +87 -0
  26. package/dist/clients/diagnostic-tracker.js +105 -0
  27. package/dist/clients/dispatch/diagnostic-taxonomy.js +66 -0
  28. package/dist/clients/dispatch/dispatcher.js +758 -0
  29. package/dist/clients/dispatch/fact-provider-types.js +1 -0
  30. package/dist/clients/dispatch/fact-rule-runner.js +17 -0
  31. package/dist/clients/dispatch/fact-runner.js +19 -0
  32. package/dist/clients/dispatch/fact-scheduler.js +68 -0
  33. package/dist/clients/dispatch/fact-store.js +47 -0
  34. package/dist/clients/dispatch/facts/comment-facts.js +35 -0
  35. package/dist/clients/dispatch/facts/file-content.js +19 -0
  36. package/dist/clients/dispatch/facts/function-facts.js +204 -0
  37. package/dist/clients/dispatch/facts/import-facts.js +163 -0
  38. package/dist/clients/dispatch/facts/try-catch-facts.js +112 -0
  39. package/dist/clients/dispatch/integration.js +1054 -0
  40. package/dist/clients/dispatch/plan.js +295 -0
  41. package/dist/clients/dispatch/priorities.js +21 -0
  42. package/dist/clients/dispatch/rules/async-noise.js +39 -0
  43. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +27 -0
  44. package/dist/clients/dispatch/rules/error-obscuring.js +31 -0
  45. package/dist/clients/dispatch/rules/error-swallowing.js +28 -0
  46. package/dist/clients/dispatch/rules/high-complexity.js +36 -0
  47. package/dist/clients/dispatch/rules/high-fan-out.js +43 -0
  48. package/dist/clients/dispatch/rules/missing-error-propagation.js +62 -0
  49. package/dist/clients/dispatch/rules/pass-through-wrappers.js +38 -0
  50. package/dist/clients/dispatch/rules/placeholder-comments.js +40 -0
  51. package/dist/clients/dispatch/rules/quality-rules.js +297 -0
  52. package/dist/clients/dispatch/rules/sonar-rules.js +493 -0
  53. package/dist/clients/dispatch/rules/unsafe-boundary.js +95 -0
  54. package/dist/clients/dispatch/runner-context.js +52 -0
  55. package/dist/clients/dispatch/runners/actionlint.js +110 -0
  56. package/dist/clients/dispatch/runners/ast-grep-napi.js +302 -0
  57. package/dist/clients/dispatch/runners/biome-check.js +120 -0
  58. package/dist/clients/dispatch/runners/biome.js +69 -0
  59. package/dist/clients/dispatch/runners/cpp-check.js +225 -0
  60. package/dist/clients/dispatch/runners/credo.js +71 -0
  61. package/dist/clients/dispatch/runners/dart-analyze.js +202 -0
  62. package/dist/clients/dispatch/runners/detekt.js +165 -0
  63. package/dist/clients/dispatch/runners/dotnet-build.js +159 -0
  64. package/dist/clients/dispatch/runners/elixir-check.js +136 -0
  65. package/dist/clients/dispatch/runners/eslint.js +126 -0
  66. package/dist/clients/dispatch/runners/fact-rules.js +30 -0
  67. package/dist/clients/dispatch/runners/fish-indent.js +69 -0
  68. package/dist/clients/dispatch/runners/gleam-check.js +87 -0
  69. package/dist/clients/dispatch/runners/go-vet.js +50 -0
  70. package/dist/clients/dispatch/runners/golangci-lint.js +118 -0
  71. package/dist/clients/dispatch/runners/hadolint.js +71 -0
  72. package/dist/clients/dispatch/runners/htmlhint.js +90 -0
  73. package/dist/clients/dispatch/runners/index.js +105 -0
  74. package/dist/clients/dispatch/runners/javac.js +76 -0
  75. package/dist/clients/dispatch/runners/ktlint.js +136 -0
  76. package/dist/clients/dispatch/runners/lsp.js +217 -0
  77. package/dist/clients/dispatch/runners/markdownlint.js +113 -0
  78. package/dist/clients/dispatch/runners/mypy.js +68 -0
  79. package/dist/clients/dispatch/runners/oxlint.js +178 -0
  80. package/dist/clients/dispatch/runners/php-lint.js +62 -0
  81. package/dist/clients/dispatch/runners/phpstan.js +73 -0
  82. package/dist/clients/dispatch/runners/prisma-validate.js +62 -0
  83. package/dist/clients/dispatch/runners/psscriptanalyzer.js +149 -0
  84. package/dist/clients/dispatch/runners/pyright.js +115 -0
  85. package/dist/clients/dispatch/runners/python-slop.js +106 -0
  86. package/dist/clients/dispatch/runners/rubocop.js +90 -0
  87. package/dist/clients/dispatch/runners/ruff.js +94 -0
  88. package/dist/clients/dispatch/runners/rust-clippy.js +149 -0
  89. package/dist/clients/dispatch/runners/semgrep.js +197 -0
  90. package/dist/clients/dispatch/runners/shellcheck.js +160 -0
  91. package/dist/clients/dispatch/runners/shfmt.js +86 -0
  92. package/dist/clients/dispatch/runners/spellcheck.js +111 -0
  93. package/dist/clients/dispatch/runners/sqlfluff.js +146 -0
  94. package/dist/clients/dispatch/runners/stylelint.js +116 -0
  95. package/dist/clients/dispatch/runners/swiftlint.js +144 -0
  96. package/dist/clients/dispatch/runners/taplo.js +61 -0
  97. package/dist/clients/dispatch/runners/tflint.js +68 -0
  98. package/dist/clients/dispatch/runners/tree-sitter.js +690 -0
  99. package/dist/clients/dispatch/runners/ts-lsp.js +109 -0
  100. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +149 -0
  101. package/dist/clients/dispatch/runners/utils/lazy-installer.js +38 -0
  102. package/dist/clients/dispatch/runners/utils/runner-helpers.js +385 -0
  103. package/dist/clients/dispatch/runners/utils.js +36 -0
  104. package/dist/clients/dispatch/runners/vale.js +105 -0
  105. package/dist/clients/dispatch/runners/yaml-rule-parser.js +167 -0
  106. package/dist/clients/dispatch/runners/yamllint.js +70 -0
  107. package/dist/clients/dispatch/runners/zig-check.js +87 -0
  108. package/dist/clients/dispatch/tool-profile.js +30 -0
  109. package/dist/clients/dispatch/types.js +13 -0
  110. package/dist/clients/dispatch/utils/format-utils.js +45 -0
  111. package/dist/clients/dispatch/utils/lsp-diagnostics.js +28 -0
  112. package/{clients/env-utils.ts โ†’ dist/clients/env-utils.js} +9 -9
  113. package/dist/clients/event-loop-monitor.js +56 -0
  114. package/dist/clients/feature-hints.js +49 -0
  115. package/dist/clients/file-kinds.js +401 -0
  116. package/dist/clients/file-role.js +102 -0
  117. package/dist/clients/file-time.js +155 -0
  118. package/dist/clients/file-utils.js +454 -0
  119. package/dist/clients/fix-worklog.js +87 -0
  120. package/dist/clients/format-service.js +184 -0
  121. package/dist/clients/formatters.js +917 -0
  122. package/dist/clients/generated-artifacts.js +146 -0
  123. package/dist/clients/git-guard.js +32 -0
  124. package/dist/clients/gitleaks-client.js +317 -0
  125. package/dist/clients/go-client.js +84 -0
  126. package/dist/clients/govulncheck-client.js +378 -0
  127. package/dist/clients/indent-retarget.js +84 -0
  128. package/dist/clients/installer/index.js +1997 -0
  129. package/dist/clients/jscpd-client.js +282 -0
  130. package/dist/clients/knip-client.js +371 -0
  131. package/dist/clients/language-policy.js +232 -0
  132. package/dist/clients/language-profile.js +329 -0
  133. package/dist/clients/latency-logger.js +44 -0
  134. package/dist/clients/lens-config.js +135 -0
  135. package/dist/clients/lens-engine.js +91 -0
  136. package/dist/clients/lens-events.js +70 -0
  137. package/dist/clients/log-cleanup.js +203 -0
  138. package/dist/clients/lsp/aggregation.js +83 -0
  139. package/dist/clients/lsp/client.js +1053 -0
  140. package/dist/clients/lsp/config.js +165 -0
  141. package/dist/clients/lsp/edits.js +228 -0
  142. package/dist/clients/lsp/index.js +1405 -0
  143. package/dist/clients/lsp/interactive-install.js +355 -0
  144. package/dist/clients/lsp/language.js +175 -0
  145. package/dist/clients/lsp/launch.js +755 -0
  146. package/{clients/lsp/lsp-index.ts โ†’ dist/clients/lsp/lsp-index.js} +1 -2
  147. package/dist/clients/lsp/path-utils.js +5 -0
  148. package/dist/clients/lsp/server-strategies.js +59 -0
  149. package/dist/clients/lsp/server.js +1600 -0
  150. package/dist/clients/mcp/analyze.js +170 -0
  151. package/dist/clients/mcp/host-shim.js +27 -0
  152. package/dist/clients/mcp/ipc.js +84 -0
  153. package/dist/clients/mcp/review.js +115 -0
  154. package/dist/clients/mcp/session.js +136 -0
  155. package/dist/clients/metrics-client.js +108 -0
  156. package/dist/clients/metrics-history.js +388 -0
  157. package/dist/clients/oldtext-autopatch.js +127 -0
  158. package/dist/clients/package-root.js +38 -0
  159. package/dist/clients/partial-edit-apply.js +43 -0
  160. package/dist/clients/path-utils.js +205 -0
  161. package/dist/clients/pipeline.js +835 -0
  162. package/dist/clients/production-readiness.js +517 -0
  163. package/dist/clients/project-changes.js +68 -0
  164. package/dist/clients/project-conventions.js +177 -0
  165. package/dist/clients/project-diagnostics/cache.js +51 -0
  166. package/dist/clients/project-diagnostics/runner-adapters/knip.js +44 -0
  167. package/dist/clients/project-diagnostics/scanner.js +159 -0
  168. package/dist/clients/project-diagnostics/types.js +1 -0
  169. package/dist/clients/project-metadata.js +710 -0
  170. package/dist/clients/project-scan-policy.js +49 -0
  171. package/dist/clients/project-snapshot.js +137 -0
  172. package/dist/clients/read-expansion.js +289 -0
  173. package/dist/clients/read-guard-logger.js +77 -0
  174. package/dist/clients/read-guard-tool-lines.js +1002 -0
  175. package/dist/clients/read-guard.js +855 -0
  176. package/dist/clients/reverse-deps.js +182 -0
  177. package/dist/clients/review-graph/builder.js +984 -0
  178. package/dist/clients/review-graph/format.js +33 -0
  179. package/dist/clients/review-graph/query.js +166 -0
  180. package/dist/clients/review-graph/service.js +44 -0
  181. package/dist/clients/review-graph/types.js +1 -0
  182. package/dist/clients/review-graph/workspace-modules.js +445 -0
  183. package/dist/clients/ruff-client.js +159 -0
  184. package/dist/clients/rules-scanner.js +118 -0
  185. package/dist/clients/runner-tracker.js +153 -0
  186. package/dist/clients/runtime-agent-end.js +227 -0
  187. package/dist/clients/runtime-config.js +73 -0
  188. package/dist/clients/runtime-context.js +42 -0
  189. package/dist/clients/runtime-coordinator.js +365 -0
  190. package/dist/clients/runtime-session.js +868 -0
  191. package/dist/clients/runtime-tool-result.js +509 -0
  192. package/dist/clients/runtime-turn.js +602 -0
  193. package/dist/clients/rust-client.js +83 -0
  194. package/dist/clients/safe-spawn.js +301 -0
  195. package/dist/clients/sanitize.js +291 -0
  196. package/dist/clients/scan-utils.js +80 -0
  197. package/dist/clients/search-read-registration.js +66 -0
  198. package/dist/clients/secrets-scanner.js +181 -0
  199. package/dist/clients/semgrep-config.js +157 -0
  200. package/dist/clients/session-state-store.js +97 -0
  201. package/dist/clients/session-summary.js +37 -0
  202. package/dist/clients/sg-runner.js +496 -0
  203. package/dist/clients/source-filter.js +274 -0
  204. package/dist/clients/source-groups.js +96 -0
  205. package/dist/clients/startup-scan.js +255 -0
  206. package/dist/clients/startup-timing.js +32 -0
  207. package/dist/clients/symbol-types.js +5 -0
  208. package/dist/clients/test-runner-client.js +766 -0
  209. package/dist/clients/todo-scanner.js +198 -0
  210. package/dist/clients/tool-event.js +19 -0
  211. package/dist/clients/tool-policy.js +1856 -0
  212. package/dist/clients/tree-sitter-cache.js +244 -0
  213. package/dist/clients/tree-sitter-client.js +1235 -0
  214. package/dist/clients/tree-sitter-fixer.js +127 -0
  215. package/dist/clients/tree-sitter-logger.js +25 -0
  216. package/dist/clients/tree-sitter-navigator.js +269 -0
  217. package/dist/clients/tree-sitter-query-loader.js +428 -0
  218. package/dist/clients/tree-sitter-symbol-extractor.js +654 -0
  219. package/dist/clients/ts-service.js +130 -0
  220. package/dist/clients/type-coverage-client.js +128 -0
  221. package/dist/clients/types.js +11 -0
  222. package/dist/clients/typescript-client.js +509 -0
  223. package/dist/clients/widget-state.js +533 -0
  224. package/dist/clients/word-index.js +250 -0
  225. package/dist/commands/booboo.js +1412 -0
  226. package/dist/i18n.js +61 -0
  227. package/dist/index.js +1743 -0
  228. package/dist/mcp/analyze-cli.js +105 -0
  229. package/dist/mcp/server.js +745 -0
  230. package/dist/mcp/worker.js +46 -0
  231. package/dist/tools/ast-dump.js +62 -0
  232. package/dist/tools/ast-grep-replace.js +176 -0
  233. package/dist/tools/ast-grep-search.js +334 -0
  234. package/dist/tools/lens-diagnostics.js +469 -0
  235. package/{tools โ†’ dist/tools}/lsp-navigation.js +368 -16
  236. package/package.json +28 -18
  237. package/rules/ast-grep-rules/rules/nested-ternary-js.yml +2 -0
  238. package/rules/ast-grep-rules/rules/nested-ternary.yml +2 -0
  239. package/rules/ast-grep-rules/rules/no-constant-condition-js.yml +11 -18
  240. package/rules/ast-grep-rules/rules/no-constant-condition.yml +11 -18
  241. package/rules/ast-grep-rules/rules/no-mutable-export.yml +13 -0
  242. package/rules/ast-grep-rules/rules/no-octal-literal.yml +14 -0
  243. package/rules/ast-grep-rules/rules/no-sort-without-comparator.yml +14 -0
  244. package/rules/ast-grep-rules/rules/redos-nested-quantifier.yml +23 -0
  245. package/rules/ast-grep-rules/rules/switch-without-default.yml +19 -0
  246. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/constructor-super-js.yml +2 -0
  247. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/constructor-super.yml +3 -0
  248. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/no-hardcoded-secrets-js.yml +3 -0
  249. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/no-hardcoded-secrets.yml +3 -0
  250. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/no-process-env.yml +3 -0
  251. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/unchecked-sync-fs-js.yml +3 -0
  252. package/rules/ast-grep-rules/{rules โ†’ rules-disabled}/unchecked-sync-fs.yml +3 -0
  253. package/rules/ast-grep-rules/slop-patterns.yml +26 -35
  254. package/rules/rule-catalog.json +3 -5
  255. package/rules/tree-sitter-queries/java/{infinite-loop.yml โ†’ infinite-loop-java.yml} +1 -1
  256. package/rules/tree-sitter-queries/typescript/no-equality-in-for-condition.yml +40 -0
  257. package/rules/tree-sitter-queries/typescript/no-jump-in-finally.yml +47 -0
  258. package/scripts/analyze-pi-lens-logs.mjs +211 -7
  259. package/scripts/download-grammars.js +15 -0
  260. package/skills/ast-grep/SKILL.md +71 -2
  261. package/skills/write-ast-grep-rule/SKILL.md +93 -3
  262. package/skills/write-tree-sitter-rule/SKILL.md +36 -0
  263. package/clients/actionable-warnings-logger.ts +0 -65
  264. package/clients/actionable-warnings.ts +0 -653
  265. package/clients/agent-behavior-client.ts +0 -155
  266. package/clients/amain-types.ts +0 -165
  267. package/clients/ast-grep-client.ts +0 -396
  268. package/clients/ast-grep-parser.ts +0 -130
  269. package/clients/ast-grep-rule-manager.ts +0 -104
  270. package/clients/ast-grep-types.ts +0 -106
  271. package/clients/biome-client.ts +0 -657
  272. package/clients/bootstrap.ts +0 -83
  273. package/clients/cache/rule-cache.ts +0 -110
  274. package/clients/cache-manager.ts +0 -361
  275. package/clients/cascade-format.ts +0 -27
  276. package/clients/cascade-logger.ts +0 -78
  277. package/clients/cascade-types.ts +0 -36
  278. package/clients/code-quality-warnings.ts +0 -313
  279. package/clients/complexity-client.ts +0 -922
  280. package/clients/dependency-checker.ts +0 -466
  281. package/clients/diagnostic-logger.ts +0 -151
  282. package/clients/diagnostic-tracker.ts +0 -163
  283. package/clients/dispatch/diagnostic-taxonomy.ts +0 -81
  284. package/clients/dispatch/dispatcher.ts +0 -986
  285. package/clients/dispatch/fact-provider-types.ts +0 -22
  286. package/clients/dispatch/fact-rule-runner.ts +0 -22
  287. package/clients/dispatch/fact-runner.ts +0 -28
  288. package/clients/dispatch/fact-scheduler.ts +0 -79
  289. package/clients/dispatch/fact-store.ts +0 -65
  290. package/clients/dispatch/facts/comment-facts.ts +0 -59
  291. package/clients/dispatch/facts/file-content.ts +0 -20
  292. package/clients/dispatch/facts/function-facts.ts +0 -256
  293. package/clients/dispatch/facts/import-facts.ts +0 -68
  294. package/clients/dispatch/facts/try-catch-facts.ts +0 -177
  295. package/clients/dispatch/integration.ts +0 -1326
  296. package/clients/dispatch/plan.ts +0 -331
  297. package/clients/dispatch/priorities.ts +0 -22
  298. package/clients/dispatch/rules/async-noise.ts +0 -50
  299. package/clients/dispatch/rules/async-unnecessary-wrapper.ts +0 -35
  300. package/clients/dispatch/rules/error-obscuring.ts +0 -40
  301. package/clients/dispatch/rules/error-swallowing.ts +0 -35
  302. package/clients/dispatch/rules/high-complexity.ts +0 -44
  303. package/clients/dispatch/rules/high-fan-out.ts +0 -55
  304. package/clients/dispatch/rules/missing-error-propagation.ts +0 -71
  305. package/clients/dispatch/rules/pass-through-wrappers.ts +0 -52
  306. package/clients/dispatch/rules/placeholder-comments.ts +0 -47
  307. package/clients/dispatch/rules/quality-rules.ts +0 -375
  308. package/clients/dispatch/rules/sonar-rules.ts +0 -508
  309. package/clients/dispatch/rules/unsafe-boundary.ts +0 -104
  310. package/clients/dispatch/runner-context.ts +0 -61
  311. package/clients/dispatch/runners/actionlint.ts +0 -145
  312. package/clients/dispatch/runners/ast-grep-napi.ts +0 -576
  313. package/clients/dispatch/runners/biome-check.ts +0 -166
  314. package/clients/dispatch/runners/biome.ts +0 -78
  315. package/clients/dispatch/runners/cpp-check.ts +0 -267
  316. package/clients/dispatch/runners/credo.ts +0 -99
  317. package/clients/dispatch/runners/dart-analyze.ts +0 -226
  318. package/clients/dispatch/runners/detekt.ts +0 -192
  319. package/clients/dispatch/runners/dotnet-build.ts +0 -195
  320. package/clients/dispatch/runners/elixir-check.ts +0 -149
  321. package/clients/dispatch/runners/eslint.ts +0 -155
  322. package/clients/dispatch/runners/fact-rules.ts +0 -44
  323. package/clients/dispatch/runners/fish-indent.ts +0 -83
  324. package/clients/dispatch/runners/gleam-check.ts +0 -108
  325. package/clients/dispatch/runners/go-vet.ts +0 -66
  326. package/clients/dispatch/runners/golangci-lint.ts +0 -175
  327. package/clients/dispatch/runners/hadolint.ts +0 -103
  328. package/clients/dispatch/runners/htmlhint.ts +0 -118
  329. package/clients/dispatch/runners/index.ts +0 -113
  330. package/clients/dispatch/runners/javac.ts +0 -99
  331. package/clients/dispatch/runners/ktlint.ts +0 -173
  332. package/clients/dispatch/runners/lsp.ts +0 -243
  333. package/clients/dispatch/runners/markdownlint.ts +0 -132
  334. package/clients/dispatch/runners/mypy.ts +0 -89
  335. package/clients/dispatch/runners/oxlint.ts +0 -214
  336. package/clients/dispatch/runners/php-lint.ts +0 -82
  337. package/clients/dispatch/runners/phpstan.ts +0 -113
  338. package/clients/dispatch/runners/prisma-validate.ts +0 -89
  339. package/clients/dispatch/runners/psscriptanalyzer.ts +0 -177
  340. package/clients/dispatch/runners/pyright.ts +0 -143
  341. package/clients/dispatch/runners/python-slop.ts +0 -133
  342. package/clients/dispatch/runners/rubocop.ts +0 -144
  343. package/clients/dispatch/runners/ruff.ts +0 -133
  344. package/clients/dispatch/runners/rust-clippy.ts +0 -181
  345. package/clients/dispatch/runners/semgrep.ts +0 -271
  346. package/clients/dispatch/runners/shellcheck.ts +0 -195
  347. package/clients/dispatch/runners/shfmt.ts +0 -100
  348. package/clients/dispatch/runners/similarity.ts +0 -510
  349. package/clients/dispatch/runners/spellcheck.ts +0 -145
  350. package/clients/dispatch/runners/sqlfluff.ts +0 -174
  351. package/clients/dispatch/runners/stylelint.ts +0 -155
  352. package/clients/dispatch/runners/swiftlint.ts +0 -199
  353. package/clients/dispatch/runners/taplo.ts +0 -93
  354. package/clients/dispatch/runners/tflint.ts +0 -100
  355. package/clients/dispatch/runners/tree-sitter.ts +0 -812
  356. package/clients/dispatch/runners/ts-lsp.ts +0 -136
  357. package/clients/dispatch/runners/type-safety.ts +0 -197
  358. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +0 -223
  359. package/clients/dispatch/runners/utils/lazy-installer.ts +0 -54
  360. package/clients/dispatch/runners/utils/runner-helpers.ts +0 -572
  361. package/clients/dispatch/runners/utils.ts +0 -58
  362. package/clients/dispatch/runners/vale.ts +0 -175
  363. package/clients/dispatch/runners/yaml-rule-parser.ts +0 -417
  364. package/clients/dispatch/runners/yamllint.ts +0 -92
  365. package/clients/dispatch/runners/zig-check.ts +0 -113
  366. package/clients/dispatch/tool-profile.ts +0 -41
  367. package/clients/dispatch/types.ts +0 -185
  368. package/clients/dispatch/utils/format-utils.ts +0 -56
  369. package/clients/dispatch/utils/lsp-diagnostics.ts +0 -42
  370. package/clients/feature-hints.ts +0 -79
  371. package/clients/file-kinds.ts +0 -467
  372. package/clients/file-role.ts +0 -145
  373. package/clients/file-time.ts +0 -208
  374. package/clients/file-utils.ts +0 -503
  375. package/clients/fix-worklog.ts +0 -121
  376. package/clients/format-service.ts +0 -276
  377. package/clients/formatters.ts +0 -1028
  378. package/clients/generated-artifacts.ts +0 -140
  379. package/clients/git-guard.ts +0 -41
  380. package/clients/go-client.ts +0 -242
  381. package/clients/indent-retarget.ts +0 -90
  382. package/clients/installer/index.ts +0 -2325
  383. package/clients/jscpd-client.ts +0 -348
  384. package/clients/knip-client.ts +0 -437
  385. package/clients/language-policy.ts +0 -263
  386. package/clients/language-profile.ts +0 -258
  387. package/clients/latency-logger.ts +0 -74
  388. package/clients/lens-config.ts +0 -177
  389. package/clients/lens-events.ts +0 -151
  390. package/clients/log-cleanup.ts +0 -255
  391. package/clients/lsp/aggregation.ts +0 -91
  392. package/clients/lsp/client.ts +0 -1505
  393. package/clients/lsp/config.ts +0 -216
  394. package/clients/lsp/edits.ts +0 -294
  395. package/clients/lsp/index.ts +0 -1439
  396. package/clients/lsp/interactive-install.ts +0 -424
  397. package/clients/lsp/language.ts +0 -223
  398. package/clients/lsp/launch.ts +0 -928
  399. package/clients/lsp/path-utils.ts +0 -12
  400. package/clients/lsp/server-strategies.ts +0 -81
  401. package/clients/lsp/server.ts +0 -2051
  402. package/clients/metrics-client.ts +0 -153
  403. package/clients/metrics-history.ts +0 -510
  404. package/clients/oldtext-autopatch.ts +0 -114
  405. package/clients/package-root.ts +0 -44
  406. package/clients/partial-edit-apply.ts +0 -76
  407. package/clients/path-utils.ts +0 -223
  408. package/clients/pipeline.ts +0 -1131
  409. package/clients/production-readiness.ts +0 -552
  410. package/clients/project-changes.ts +0 -112
  411. package/clients/project-conventions.ts +0 -215
  412. package/clients/project-index.ts +0 -403
  413. package/clients/project-metadata.ts +0 -809
  414. package/clients/project-scan-policy.ts +0 -79
  415. package/clients/project-snapshot.ts +0 -221
  416. package/clients/read-expansion.ts +0 -283
  417. package/clients/read-guard-logger.ts +0 -101
  418. package/clients/read-guard-tool-lines.ts +0 -804
  419. package/clients/read-guard.ts +0 -1044
  420. package/clients/reverse-deps.ts +0 -244
  421. package/clients/review-graph/builder.ts +0 -927
  422. package/clients/review-graph/format.ts +0 -51
  423. package/clients/review-graph/query.ts +0 -162
  424. package/clients/review-graph/service.ts +0 -69
  425. package/clients/review-graph/types.ts +0 -46
  426. package/clients/review-graph/workspace-modules.ts +0 -497
  427. package/clients/ruff-client.ts +0 -511
  428. package/clients/rules-scanner.ts +0 -149
  429. package/clients/runner-tracker.ts +0 -215
  430. package/clients/runtime-agent-end.ts +0 -331
  431. package/clients/runtime-config.ts +0 -77
  432. package/clients/runtime-context.ts +0 -79
  433. package/clients/runtime-coordinator.ts +0 -472
  434. package/clients/runtime-session.ts +0 -784
  435. package/clients/runtime-tool-result.ts +0 -650
  436. package/clients/runtime-turn.ts +0 -656
  437. package/clients/rust-client.ts +0 -270
  438. package/clients/safe-spawn.ts +0 -339
  439. package/clients/sanitize.ts +0 -356
  440. package/clients/scan-utils.ts +0 -75
  441. package/clients/secrets-scanner.ts +0 -214
  442. package/clients/semgrep-config.ts +0 -203
  443. package/clients/session-summary.ts +0 -53
  444. package/clients/sg-runner.ts +0 -470
  445. package/clients/source-filter.ts +0 -263
  446. package/clients/source-groups.ts +0 -140
  447. package/clients/startup-scan.ts +0 -150
  448. package/clients/state-matrix.ts +0 -202
  449. package/clients/subprocess-client.ts +0 -101
  450. package/clients/symbol-types.ts +0 -77
  451. package/clients/test-runner-client.ts +0 -1134
  452. package/clients/todo-scanner.ts +0 -243
  453. package/clients/tool-availability.ts +0 -250
  454. package/clients/tool-policy.ts +0 -2063
  455. package/clients/tree-sitter-cache.ts +0 -316
  456. package/clients/tree-sitter-client.ts +0 -1541
  457. package/clients/tree-sitter-fixer.ts +0 -217
  458. package/clients/tree-sitter-logger.ts +0 -51
  459. package/clients/tree-sitter-navigator.ts +0 -329
  460. package/clients/tree-sitter-query-loader.ts +0 -521
  461. package/clients/tree-sitter-symbol-extractor.ts +0 -442
  462. package/clients/ts-service.ts +0 -154
  463. package/clients/type-coverage-client.ts +0 -164
  464. package/clients/type-safety-client.ts +0 -193
  465. package/clients/types.ts +0 -59
  466. package/clients/typescript-client.ts +0 -698
  467. package/clients/widget-state.ts +0 -477
  468. package/commands/booboo.ts +0 -1837
  469. package/i18n.ts +0 -66
  470. package/index.ts +0 -1962
  471. package/tools/ast-grep-replace.js +0 -75
  472. package/tools/ast-grep-replace.ts +0 -112
  473. package/tools/ast-grep-search.js +0 -168
  474. package/tools/ast-grep-search.ts +0 -222
  475. package/tools/lsp-diagnostics.ts +0 -706
  476. package/tools/lsp-navigation.ts +0 -1124
  477. package/tools/shared.ts +0 -31
  478. package/tsconfig.json +0 -18
  479. /package/{tools โ†’ dist/tools}/lsp-diagnostics.js +0 -0
  480. /package/{tools โ†’ dist/tools}/shared.js +0 -0
  481. /package/rules/tree-sitter-queries/{abap โ†’ abap-disabled}/delete-where.yml +0 -0
  482. /package/rules/tree-sitter-queries/{cobol โ†’ cobol-disabled}/alter-statement.yml +0 -0
  483. /package/rules/tree-sitter-queries/{cobol โ†’ cobol-disabled}/lock-table-cobol.yml +0 -0
  484. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/delete-update-where.yml +0 -0
  485. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/end-loop-semicolon.yml +0 -0
  486. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/fetch-bulk-collect-limit.yml +0 -0
  487. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/forallsave-exceptions.yml +0 -0
  488. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/lock-table.yml +0 -0
  489. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/nchar-nvarchar2-bytes.yml +0 -0
  490. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/no-synchronize.yml +0 -0
  491. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/not-null-initialization.yml +0 -0
  492. /package/rules/tree-sitter-queries/{plsql โ†’ plsql-disabled}/raise-application-error-codes.yml +0 -0
@@ -1,1837 +0,0 @@
1
- import * as nodeFs from "node:fs";
2
- import * as path from "node:path";
3
- import type {
4
- ExtensionAPI,
5
- ExtensionContext,
6
- } from "@earendil-works/pi-coding-agent";
7
- import type { AstGrepClient } from "../clients/ast-grep-client.js";
8
- import type { ComplexityClient } from "../clients/complexity-client.js";
9
- import type { DependencyChecker } from "../clients/dependency-checker.js";
10
- import { createDispatchContext } from "../clients/dispatch/dispatcher.js";
11
- import { evaluateRules } from "../clients/dispatch/fact-rule-runner.js";
12
- import { runProviders } from "../clients/dispatch/fact-runner.js";
13
- import { FactStore } from "../clients/dispatch/fact-store.js";
14
- import {
15
- getKnipIgnorePatterns,
16
- getProjectIgnoreGlobs,
17
- isTestFile,
18
- } from "../clients/file-utils.js";
19
- import type { JscpdClient } from "../clients/jscpd-client.js";
20
- import type { KnipClient } from "../clients/knip-client.js";
21
- import { validateProductionReadiness } from "../clients/production-readiness.js";
22
- import {
23
- buildProjectIndex,
24
- type ProjectIndex,
25
- } from "../clients/project-index.js";
26
- import {
27
- detectProjectMetadata,
28
- getAvailableCommands,
29
- } from "../clients/project-metadata.js";
30
- import { RunnerTracker } from "../clients/runner-tracker.js";
31
- import { safeSpawn } from "../clients/safe-spawn.js";
32
- import {
33
- collectSourceFiles,
34
- getFilterStats,
35
- } from "../clients/source-filter.js";
36
- import { calculateSimilarity } from "../clients/state-matrix.js";
37
- import type { TodoScanner } from "../clients/todo-scanner.js";
38
- import { TreeSitterClient } from "../clients/tree-sitter-client.js";
39
- import { queryLoader } from "../clients/tree-sitter-query-loader.js";
40
- import type { TypeCoverageClient } from "../clients/type-coverage-client.js";
41
- // Side-effect import: registers all fact providers and fact rules
42
- import "../clients/dispatch/integration.js";
43
-
44
- const ROOT_MARKERS = [
45
- "package.json",
46
- "tsconfig.json",
47
- ".git",
48
- "Cargo.toml",
49
- "go.mod",
50
- "pyproject.toml",
51
- ];
52
-
53
- function hasRootMarker(dir: string): boolean {
54
- return ROOT_MARKERS.some((m) => nodeFs.existsSync(path.join(dir, m)));
55
- }
56
-
57
- function resolveProjectRoot(startDir: string): string {
58
- // Walk up: find nearest ancestor with a root marker
59
- let dir = startDir;
60
- const fsRoot = path.parse(dir).root;
61
- while (dir !== fsRoot) {
62
- if (hasRootMarker(dir)) return dir;
63
- const parent = path.dirname(dir);
64
- if (parent === dir) break;
65
- dir = parent;
66
- }
67
-
68
- // Walk down one level: if exactly one immediate subdir has a root marker, use it
69
- try {
70
- const entries = nodeFs.readdirSync(startDir, { withFileTypes: true });
71
- const candidates = entries
72
- .filter((e) => e.isDirectory())
73
- .map((e) => path.join(startDir, e.name))
74
- .filter(hasRootMarker);
75
- if (candidates.length === 1) return candidates[0];
76
- } catch {
77
- // unreadable dir โ€” fall through
78
- }
79
-
80
- return startDir;
81
- }
82
-
83
- // Module-level singleton โ€” web-tree-sitter WASM must only be initialized once per process
84
- let _sharedTreeSitterClient: TreeSitterClient | null = null;
85
- function getSharedTreeSitterClient(): TreeSitterClient {
86
- if (!_sharedTreeSitterClient) {
87
- _sharedTreeSitterClient = new TreeSitterClient();
88
- }
89
- return _sharedTreeSitterClient;
90
- }
91
-
92
- const EXT_TO_LANG: Record<string, string> = {
93
- ".ts": "typescript",
94
- ".mts": "typescript",
95
- ".cts": "typescript",
96
- ".tsx": "typescript",
97
- ".js": "javascript",
98
- ".mjs": "javascript",
99
- ".cjs": "javascript",
100
- ".jsx": "javascript",
101
- ".py": "python",
102
- ".go": "go",
103
- ".rs": "rust",
104
- ".rb": "ruby",
105
- };
106
-
107
- const getExtensionDir = () => {
108
- if (typeof __dirname !== "undefined") {
109
- return __dirname;
110
- }
111
- return ".";
112
- };
113
-
114
- /**
115
- * Centralized test file exclusion for booboo runners.
116
- * Mirrors the dispatch system's skipTestFiles behavior.
117
- */
118
- function shouldIncludeFile(filePath: string): boolean {
119
- return !isTestFile(filePath);
120
- }
121
-
122
- export async function handleBooboo(
123
- args: string,
124
- ctx: ExtensionContext,
125
- clients: {
126
- astGrep: AstGrepClient;
127
- complexity: ComplexityClient;
128
- todo: TodoScanner;
129
- knip: KnipClient;
130
- jscpd: JscpdClient;
131
- typeCoverage: TypeCoverageClient;
132
- depChecker: DependencyChecker;
133
- },
134
- pi: ExtensionAPI,
135
- ) {
136
- const requestedPath = args.trim() || ctx.cwd || process.cwd();
137
- const targetPath = resolveProjectRoot(path.resolve(requestedPath));
138
- const reviewRoot = targetPath;
139
-
140
- // Build --globs exclusion args for sg scan from centralized .gitignore matching.
141
- const sgExcludeGlobs = getProjectIgnoreGlobs(targetPath).flatMap((glob) => [
142
- "--globs",
143
- `!${glob}`,
144
- ]);
145
-
146
- const categoryKey = (name: string) => name.toLowerCase().replace(/\s+/g, "-");
147
-
148
- // Tunable thresholds โ€” adjust these to reduce false positives across all projects
149
- const FACT_SEVERITY_FILTER = new Set(["error", "warning"]);
150
- const MIN_TREE_SITTER_HITS_PER_RULE = 3;
151
-
152
- // Detect project metadata for richer reporting
153
- const projectMeta = detectProjectMetadata(targetPath);
154
- const langs = new Set(projectMeta.languages);
155
-
156
- // No noisy notification at start - just run the review silently
157
-
158
- // Detect project type once for all runners
159
- const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
160
-
161
- // Collect source files once with unified artifact filtering
162
- // This ensures all scanners work on the same deduplicated file set
163
- const sourceFiles = collectSourceFiles(targetPath);
164
- const allFiles = collectSourceFiles(targetPath, {
165
- extensions: [
166
- ".ts",
167
- ".tsx",
168
- ".js",
169
- ".jsx",
170
- ".mjs",
171
- ".cjs",
172
- ".py",
173
- ".go",
174
- ".rs",
175
- ".rb",
176
- ],
177
- });
178
- const filterStats = getFilterStats(allFiles, sourceFiles);
179
-
180
- if (filterStats.skipped > 0) {
181
- const byTypeStr = Object.entries(filterStats.byType)
182
- .map(([ext, count]) => `${count} ${ext}`)
183
- .join(", ");
184
- // biome-ignore lint/suspicious/noConsole: CLI output
185
- console.log(
186
- `[lens-booboo] Filtered ${filterStats.skipped} build artifacts (${byTypeStr}), scanning ${filterStats.kept} source files`,
187
- );
188
- }
189
-
190
- // Get available commands for the project
191
- const availableCommands = getAvailableCommands(projectMeta);
192
-
193
- // Load false positives from fix session to filter them out
194
- const sessionFile = path.join(reviewRoot, ".pi-lens", "fix-session.json");
195
- let falsePositives: string[] = [];
196
- try {
197
- const sessionData = JSON.parse(
198
- nodeFs.readFileSync(sessionFile, "utf-8") || "{}",
199
- );
200
- falsePositives = sessionData.falsePositives || [];
201
- } catch {
202
- // No session file yet
203
- }
204
-
205
- // Helper to check if an issue is marked as false positive
206
- const isFalsePositive = (
207
- category: string,
208
- file: string,
209
- line?: number,
210
- ): boolean => {
211
- const fpKey =
212
- line !== undefined
213
- ? `${category}:${file}:${line}`
214
- : `${category}:${file}`;
215
- return falsePositives.some(
216
- (fp) => fp === fpKey || fp.startsWith(`${category}:${file}`),
217
- );
218
- };
219
-
220
- // Summary counts for terminal display
221
- const summaryItems: {
222
- category: string;
223
- count: number;
224
- severity: "๐Ÿ”ด" | "๐ŸŸก" | "๐ŸŸข" | "โ„น๏ธ";
225
- fixable: boolean;
226
- }[] = [];
227
- const fullReport: string[] = [];
228
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
229
- const reviewDir = path.join(reviewRoot, ".pi-lens", "reviews");
230
-
231
- // Initialize runner tracker (no per-runner progress to avoid UI overwriting)
232
- const tracker = new RunnerTracker();
233
-
234
- // Helper to format elapsed time
235
- const formatElapsed = (ms: number): string =>
236
- ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
237
-
238
- // Runner 1: Design smells via ast-grep
239
- await tracker.run("ast-grep (design smells)", async () => {
240
- if (!(await clients.astGrep.ensureAvailable())) {
241
- return { findings: 0, status: "skipped" };
242
- }
243
-
244
- const configPath = path.join(
245
- getExtensionDir(),
246
- "..",
247
- "rules",
248
- "ast-grep-rules",
249
- ".sgconfig.yml",
250
- );
251
-
252
- try {
253
- const result = safeSpawn(
254
- "npx",
255
- [
256
- "sg",
257
- "scan",
258
- "--config",
259
- configPath,
260
- "--json",
261
- "--globs",
262
- "!**/*.test.ts",
263
- "--globs",
264
- "!**/*.spec.ts",
265
- "--globs",
266
- "!**/*.poc.test.ts",
267
- "--globs",
268
- "!**/test-utils.ts",
269
- "--globs",
270
- "!**/test-*.ts",
271
- "--globs",
272
- "!**/__tests__/**",
273
- "--globs",
274
- "!**/tests/**",
275
- "--globs",
276
- "!**/.pi-lens/**",
277
- "--globs",
278
- "!**/.pi/**",
279
- "--globs",
280
- "!**/node_modules/**",
281
- "--globs",
282
- "!**/vendor/**",
283
- "--globs",
284
- "!**/third_party/**",
285
- "--globs",
286
- "!**/third-party/**",
287
- "--globs",
288
- "!**/.git/**",
289
- "--globs",
290
- "!**/.ruff_cache/**",
291
- ...sgExcludeGlobs,
292
- targetPath,
293
- ],
294
- {
295
- timeout: 30000,
296
- },
297
- );
298
-
299
- const output = result.stdout || result.stderr || "";
300
- if (output.trim() && result.status !== undefined) {
301
- const issues: Array<{
302
- file: string;
303
- line: number;
304
- rule: string;
305
- message: string;
306
- }> = [];
307
-
308
- const parseItems = (raw: string): Record<string, any>[] => {
309
- const trimmed = raw.trim();
310
- if (trimmed.startsWith("[")) {
311
- try {
312
- return JSON.parse(trimmed);
313
- } catch {
314
- return [];
315
- }
316
- }
317
- return raw.split("\n").flatMap((l: string) => {
318
- try {
319
- return [JSON.parse(l)];
320
- } catch {
321
- return [];
322
- }
323
- });
324
- };
325
-
326
- for (const item of parseItems(output)) {
327
- const ruleId =
328
- item.ruleId || item.rule?.title || item.name || "unknown";
329
- const ruleDesc = clients.astGrep.getRuleDescription?.(ruleId);
330
- const message = ruleDesc?.message || item.message || ruleId;
331
- const lineNum =
332
- item.labels?.[0]?.range?.start?.line ||
333
- item.spans?.[0]?.range?.start?.line ||
334
- item.range?.start?.line ||
335
- 0;
336
-
337
- issues.push({
338
- file: item.file || item.path || targetPath,
339
- line: lineNum + 1,
340
- rule: ruleId,
341
- message: message,
342
- });
343
- }
344
-
345
- const filteredIssues = issues.filter(
346
- (issue) =>
347
- !isFalsePositive(categoryKey("ast-grep"), issue.file, issue.line),
348
- );
349
-
350
- if (filteredIssues.length > 0) {
351
- summaryItems.push({
352
- category: "ast-grep",
353
- count: filteredIssues.length,
354
- severity: filteredIssues.length > 10 ? "๐Ÿ”ด" : "๐ŸŸก",
355
- fixable: true,
356
- });
357
-
358
- let fullSection = `## ast-grep (Structural Issues)\n\n**${filteredIssues.length} issue(s) found**\n\n`;
359
- fullSection +=
360
- "| Line | Rule | Message |\n|------|------|--------|\n";
361
- for (const issue of filteredIssues) {
362
- fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
363
- }
364
-
365
- fullSection += "\n### ๐Ÿ’ก How to Fix\n\n";
366
- const seenRules = new Set<string>();
367
- for (const issue of filteredIssues.slice(0, 5)) {
368
- if (seenRules.has(issue.rule)) continue;
369
- seenRules.add(issue.rule);
370
- const ruleDesc = clients.astGrep.getRuleDescription?.(issue.rule);
371
- if (ruleDesc?.note || ruleDesc?.fix) {
372
- fullSection += `**${issue.rule}:**\n`;
373
- if (ruleDesc.note) fullSection += `${ruleDesc.note}\n\n`;
374
- if (ruleDesc.fix)
375
- fullSection += `Suggested fix:\n\`\`\`typescript\n${ruleDesc.fix}\n\`\`\`\n\n`;
376
- }
377
- }
378
-
379
- fullReport.push(fullSection);
380
- }
381
-
382
- return { findings: filteredIssues.length, status: "done" };
383
- }
384
- return { findings: 0, status: "done" };
385
- } catch {
386
- return { findings: 0, status: "error" };
387
- }
388
- });
389
-
390
- // Runner 2: Similar functions
391
- await tracker.run("ast-grep (similar functions)", async () => {
392
- if (!(await clients.astGrep.ensureAvailable())) {
393
- return { findings: 0, status: "skipped" };
394
- }
395
-
396
- const similarGroups = await clients.astGrep.findSimilarFunctions(
397
- targetPath,
398
- "typescript",
399
- );
400
-
401
- // Filter out test files using centralized exclusion
402
- const filteredGroups = similarGroups
403
- .map((group) => ({
404
- ...group,
405
- functions: group.functions.filter((fn) => shouldIncludeFile(fn.file)),
406
- }))
407
- .filter((group) => group.functions.length > 1); // Need at least 2 non-test functions
408
-
409
- if (filteredGroups.length > 0) {
410
- summaryItems.push({
411
- category: "Similar Functions",
412
- count: filteredGroups.length,
413
- severity: "๐ŸŸก",
414
- fixable: true,
415
- });
416
-
417
- let fullSection = `## Similar Functions\n\n**${filteredGroups.length} group(s) of structurally similar functions**\n\n`;
418
- for (const group of filteredGroups) {
419
- fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
420
- fullSection +=
421
- "| Function | File | Line |\n|----------|------|------|\n";
422
- for (const fn of group.functions) {
423
- fullSection += `| ${fn.name} | ${fn.file} | ${fn.line} |\n`;
424
- }
425
- fullSection += "\n";
426
- }
427
- fullReport.push(fullSection);
428
- }
429
-
430
- return { findings: filteredGroups.length, status: "done" };
431
- });
432
-
433
- // Runner 3: Semantic similarity
434
- await tracker.run("semantic similarity (Amain)", async () => {
435
- try {
436
- const absoluteFiles = collectSourceFiles(targetPath, {
437
- extensions: [".ts"],
438
- }).filter(shouldIncludeFile);
439
-
440
- if (absoluteFiles.length === 0) {
441
- return { findings: 0, status: "done" };
442
- }
443
- const index = await buildProjectIndex(targetPath, absoluteFiles);
444
- const topPairs = findTopSimilarPairs(index, 10);
445
-
446
- if (topPairs.length > 0) {
447
- summaryItems.push({
448
- category: "Semantic Duplicates",
449
- count: topPairs.length,
450
- severity: "๐ŸŸก",
451
- fixable: true,
452
- });
453
-
454
- let fullSection = `## Semantic Duplicates (Amain Algorithm)\n\n`;
455
- fullSection += `**${topPairs.length} pair(s) with >=${(SEMANTIC_SIMILARITY_THRESHOLD * 100).toFixed(0)}% semantic similarity**\n\n`;
456
- fullSection +=
457
- "Functions with different names/variables but similar logic structures.\n\n";
458
-
459
- for (const pair of topPairs) {
460
- fullSection += `### ${pair.func1} โ†” ${pair.func2}\n\n`;
461
- fullSection += `- Similarity: **${(pair.similarity * 100).toFixed(1)}%**\n`;
462
- fullSection += `- Consider consolidating or extracting shared logic\n\n`;
463
- }
464
- fullReport.push(fullSection);
465
- }
466
-
467
- return { findings: topPairs.length, status: "done" };
468
- } catch (err) {
469
- console.error("[booboo] Semantic similarity analysis failed:", err);
470
- return { findings: 0, status: "error" };
471
- }
472
- });
473
-
474
- // Runner 4: Complexity metrics
475
- await tracker.run("complexity metrics", async () => {
476
- const results: import("../clients/complexity-client.js").FileComplexity[] =
477
- [];
478
- const aiSlopIssues: string[] = [];
479
- // Use pre-collected sourceFiles (already filtered for artifacts)
480
- const files = sourceFiles.filter(shouldIncludeFile);
481
-
482
- for (const fullPath of files) {
483
- if (clients.complexity.isSupportedFile(fullPath)) {
484
- const metrics = clients.complexity.analyzeFile(fullPath);
485
- if (metrics) {
486
- results.push(metrics);
487
- // AI slop check - already filtered by shouldIncludeFile above
488
- const warnings = clients.complexity
489
- .checkThresholds(metrics)
490
- .filter((w) => !w.includes("entropy") && !w.includes("AI-style"));
491
- if (warnings.length > 0) {
492
- aiSlopIssues.push(` ${metrics.filePath}:`);
493
- for (const w of warnings) {
494
- aiSlopIssues.push(` โš  ${w}`);
495
- }
496
- }
497
- }
498
- }
499
- }
500
-
501
- if (results.length > 0) {
502
- const avgMI =
503
- results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
504
- results.length;
505
- const avgCognitive =
506
- results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
507
- const avgCyclomatic =
508
- results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
509
- results.length;
510
- const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
511
- const maxCognitive = Math.max(
512
- ...results.map((r) => r.cognitiveComplexity),
513
- );
514
- const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
515
-
516
- // Only flag files with EXTREME issues (tuned to reduce false positives)
517
- // MI < 20 is "critically unmaintainable" (was < 40, too aggressive)
518
- const severeLowMI = results
519
- .filter((r) => r.maintainabilityIndex < 20 && !isTestFile(r.filePath))
520
- .sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
521
- // Cognitive > 80 is extreme (was > 30, flagged too many files)
522
- const veryHighCognitive = results
523
- .filter((r) => r.cognitiveComplexity > 80 && !isTestFile(r.filePath))
524
- .sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
525
- // Deep nesting > 8 levels is extreme (was > 5, normal code hits this)
526
- const deepNesting = results
527
- .filter((r) => r.maxNestingDepth > 8 && !isTestFile(r.filePath))
528
- .sort((a, b) => b.maxNestingDepth - a.maxNestingDepth);
529
-
530
- let findings = 0;
531
-
532
- if (severeLowMI.length > 0) {
533
- findings += severeLowMI.length;
534
- summaryItems.push({
535
- category: "Low Maintainability",
536
- count: severeLowMI.length,
537
- severity: "๐Ÿ”ด",
538
- fixable: false,
539
- });
540
- }
541
- if (veryHighCognitive.length > 0) {
542
- findings += veryHighCognitive.length;
543
- summaryItems.push({
544
- category: "Very High Complexity",
545
- count: veryHighCognitive.length,
546
- severity: "๐Ÿ”ด",
547
- fixable: true,
548
- });
549
- }
550
- if (deepNesting.length > 0) {
551
- findings += deepNesting.length;
552
- summaryItems.push({
553
- category: "Deep Nesting",
554
- count: deepNesting.length,
555
- severity: "๐ŸŸก",
556
- fixable: true,
557
- });
558
- }
559
- if (aiSlopIssues.length > 0) {
560
- findings += Math.floor(aiSlopIssues.length / 2);
561
- summaryItems.push({
562
- category: "AI Slop",
563
- count: Math.floor(aiSlopIssues.length / 2),
564
- severity: "๐ŸŸก",
565
- fixable: true,
566
- });
567
- }
568
-
569
- let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
570
- fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n`;
571
- fullSection += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
572
- fullSection += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
573
- fullSection += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
574
- fullSection += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
575
- fullSection += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
576
- fullSection += `| Max Nesting Depth | ${maxNesting} |\n`;
577
- fullSection += `| Total Files | ${results.length} |\n\n`;
578
-
579
- // Report severe issues (thresholds match findings count)
580
- if (severeLowMI.length > 0) {
581
- fullSection += `### Low Maintainability (MI < 20)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
582
- for (const f of severeLowMI) {
583
- fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
584
- }
585
- fullSection += "\n";
586
- }
587
-
588
- if (veryHighCognitive.length > 0) {
589
- fullSection += `### Very High Cognitive Complexity (> 80)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
590
- for (const f of veryHighCognitive) {
591
- fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
592
- }
593
- fullSection += "\n";
594
- }
595
-
596
- if (deepNesting.length > 0) {
597
- fullSection += `### Deep Nesting (> 8 levels)\n\n| File | Nesting | Cognitive | MI |\n|------|---------|-----------|-----|\n`;
598
- for (const f of deepNesting) {
599
- fullSection += `| ${f.filePath} | ${f.maxNestingDepth} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} |\n`;
600
- }
601
- fullSection += "\n";
602
- }
603
-
604
- if (aiSlopIssues.length > 0) {
605
- fullSection += `### AI Slop Indicators\n\n`;
606
- for (const issue of aiSlopIssues) {
607
- fullSection += `${issue}\n`;
608
- }
609
- fullSection += "\n";
610
- }
611
-
612
- fullReport.push(fullSection);
613
- return { findings, status: "done" };
614
- }
615
-
616
- return { findings: 0, status: "done" };
617
- });
618
-
619
- // Runner 4: Tree-sitter patterns โ€” language-aware, driven by .yml rule files
620
- // Uses the same queryLoader + singleton client as the per-write dispatch runner.
621
- // Covers all languages: TypeScript, JavaScript, Python, Go, Rust, Ruby.
622
- await tracker.run("tree-sitter patterns", async () => {
623
- const client = getSharedTreeSitterClient();
624
- if (!client.isAvailable()) return { findings: 0, status: "skipped" };
625
-
626
- const initialized = await client.init();
627
- if (!initialized) return { findings: 0, status: "skipped" };
628
-
629
- await queryLoader.loadQueries(targetPath);
630
- const allQueries = queryLoader.getAllQueries();
631
- if (allQueries.length === 0) return { findings: 0, status: "skipped" };
632
-
633
- // Deduplicate structural rules that fire per nesting level
634
- const DEDUP_PER_FILE = new Set(["deep-promise-chain", "deep-nesting"]);
635
-
636
- interface TSIssue {
637
- file: string;
638
- line: number;
639
- ruleId: string;
640
- severity: string;
641
- message: string;
642
- }
643
-
644
- const byRule = new Map<string, TSIssue[]>();
645
- let findings = 0;
646
-
647
- for (const filePath of allFiles) {
648
- if (isTestFile(filePath)) continue;
649
- const ext = filePath.slice(filePath.lastIndexOf("."));
650
- const langId = EXT_TO_LANG[ext];
651
- if (!langId) continue;
652
-
653
- const langQueries = allQueries.filter(
654
- (q) =>
655
- q.language === langId ||
656
- (langId === "javascript" && q.language === "typescript"),
657
- );
658
- if (langQueries.length === 0) continue;
659
-
660
- for (const query of langQueries) {
661
- let matches;
662
- try {
663
- matches = await client.runQueryOnFile(query, filePath, langId, {
664
- maxResults: 20,
665
- });
666
- } catch {
667
- continue;
668
- }
669
- if (!matches?.length) continue;
670
-
671
- const relFile = path.relative(targetPath, filePath);
672
- const bucket = byRule.get(query.id) ?? [];
673
-
674
- if (DEDUP_PER_FILE.has(query.id)) {
675
- if (!bucket.some((h) => h.file === relFile)) {
676
- bucket.push({
677
- file: relFile,
678
- line: matches[0].line ?? 1,
679
- ruleId: query.id,
680
- severity: query.severity,
681
- message: query.message,
682
- });
683
- findings++;
684
- }
685
- } else {
686
- for (const m of matches) {
687
- bucket.push({
688
- file: relFile,
689
- line: m.line ?? 1,
690
- ruleId: query.id,
691
- severity: query.severity,
692
- message: query.message,
693
- });
694
- findings++;
695
- }
696
- }
697
- byRule.set(query.id, bucket);
698
- }
699
- }
700
-
701
- // Suppress rules with fewer than N hits (false positives from one-off matches)
702
- for (const [ruleId, bucket] of byRule) {
703
- if (bucket.length < MIN_TREE_SITTER_HITS_PER_RULE) {
704
- byRule.delete(ruleId);
705
- findings -= bucket.length;
706
- }
707
- }
708
-
709
- if (findings === 0) return { findings: 0, status: "done" };
710
-
711
- const errorCount = [...byRule.values()]
712
- .flat()
713
- .filter((i) => i.severity === "error").length;
714
- summaryItems.push({
715
- category: "Tree-sitter Patterns",
716
- count: findings,
717
- severity: errorCount > 0 ? "๐Ÿ”ด" : "๐ŸŸก",
718
- fixable: true,
719
- });
720
-
721
- // Sort rules by hit count descending
722
- const sorted = [...byRule.entries()].sort(
723
- (a, b) => b[1].length - a[1].length,
724
- );
725
- let fullSection = `## Tree-sitter Patterns\n\n**${findings} issue(s) across ${byRule.size} rule(s)**\n\n`;
726
- for (const [ruleId, issues] of sorted) {
727
- const sev = issues[0].severity === "error" ? "๐Ÿ”ด" : "๐ŸŸก";
728
- fullSection += `### ${sev} ${ruleId} (${issues.length})\n\n`;
729
- fullSection += `${issues[0].message}\n\n`;
730
- fullSection += "| File | Line |\n|------|------|\n";
731
- for (const issue of issues.slice(0, 10)) {
732
- fullSection += `| ${issue.file} | ${issue.line} |\n`;
733
- }
734
- if (issues.length > 10)
735
- fullSection += `| ... | +${issues.length - 10} more |\n`;
736
- fullSection += "\n";
737
- }
738
- fullReport.push(fullSection);
739
-
740
- return { findings, status: "done" };
741
- });
742
-
743
- // Runner 4b: Fact rules โ€” semantic analysis over TS/JS files
744
- // Runs all registered fact rules (error-obscuring, async-noise, unsafe-boundary, etc.)
745
- // using the same provider/rule pipeline as the per-write dispatch system.
746
- await tracker.run("fact rules", async () => {
747
- const boobooFacts = new FactStore();
748
- const tsFiles = allFiles.filter((f) => /\.tsx?$/.test(f) && !isTestFile(f));
749
- if (tsFiles.length === 0) return { findings: 0, status: "skipped" };
750
-
751
- interface FactIssue {
752
- file: string;
753
- line: number;
754
- ruleId: string;
755
- severity: string;
756
- message: string;
757
- }
758
- const byRule = new Map<string, FactIssue[]>();
759
- let findings = 0;
760
-
761
- for (const filePath of tsFiles) {
762
- boobooFacts.clearFileFactsFor(filePath);
763
- const ctx = createDispatchContext(
764
- filePath,
765
- targetPath,
766
- pi,
767
- boobooFacts,
768
- false,
769
- );
770
-
771
- try {
772
- await runProviders(ctx);
773
- } catch {
774
- continue;
775
- }
776
-
777
- const diagnostics = evaluateRules(ctx).filter((d) =>
778
- FACT_SEVERITY_FILTER.has(d.severity ?? "warning"),
779
- );
780
- for (const diag of diagnostics) {
781
- const relFile = path.relative(targetPath, filePath);
782
- const bucket = byRule.get(diag.rule ?? diag.id) ?? [];
783
- bucket.push({
784
- file: relFile,
785
- line: diag.line ?? 1,
786
- ruleId: diag.rule ?? diag.id,
787
- severity: diag.severity ?? "warning",
788
- message: diag.message ?? "",
789
- });
790
- byRule.set(diag.rule ?? diag.id, bucket);
791
- findings++;
792
- }
793
- }
794
-
795
- if (findings === 0) return { findings: 0, status: "done" };
796
-
797
- const errorCount = [...byRule.values()]
798
- .flat()
799
- .filter((i) => i.severity === "error").length;
800
- summaryItems.push({
801
- category: "Fact Rules",
802
- count: findings,
803
- severity: errorCount > 0 ? "๐Ÿ”ด" : "๐ŸŸก",
804
- fixable: true,
805
- });
806
-
807
- const sorted = [...byRule.entries()].sort(
808
- (a, b) => b[1].length - a[1].length,
809
- );
810
- let fullSection = `## Fact Rules (Semantic Analysis)\n\n**${findings} issue(s) across ${byRule.size} rule(s)**\n\n`;
811
- for (const [ruleId, issues] of sorted) {
812
- const sev = issues[0].severity === "error" ? "๐Ÿ”ด" : "๐ŸŸก";
813
- fullSection += `### ${sev} ${ruleId} (${issues.length})\n\n`;
814
- fullSection += `${issues[0].message}\n\n`;
815
- fullSection += "| File | Line |\n|------|------|\n";
816
- for (const issue of issues.slice(0, 10)) {
817
- fullSection += `| ${issue.file} | ${issue.line} |\n`;
818
- }
819
- if (issues.length > 10)
820
- fullSection += `| ... | +${issues.length - 10} more |\n`;
821
- fullSection += "\n";
822
- }
823
- fullReport.push(fullSection);
824
-
825
- return { findings, status: "done" };
826
- });
827
-
828
- // Runner 5: TODOs (cache test edit)
829
- await tracker.run("TODO scanner", async () => {
830
- const todoResult = clients.todo.scanDirectory(targetPath);
831
-
832
- if (todoResult.items.length > 0) {
833
- summaryItems.push({
834
- category: "TODOs",
835
- count: todoResult.items.length,
836
- severity: "โ„น๏ธ",
837
- fixable: false,
838
- });
839
-
840
- let fullSection = `## TODOs / Annotations\n\n`;
841
- fullSection += `**${todoResult.items.length} annotation(s) found**\n\n`;
842
- fullSection +=
843
- "| Type | File | Line | Text |\n|------|------|------|------|\n";
844
- for (const item of todoResult.items) {
845
- fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
846
- }
847
- fullSection += "\n";
848
- fullReport.push(fullSection);
849
- }
850
-
851
- return { findings: todoResult.items.length, status: "done" };
852
- });
853
-
854
- // Runner 6: Dead code (JS/TS only โ€” Knip is a JS/TS tool)
855
- await tracker.run("dead code (Knip)", async () => {
856
- if (!langs.has("javascript") && !langs.has("typescript")) {
857
- return { findings: 0, status: "skipped" };
858
- }
859
- if (!(await clients.knip.ensureAvailable())) {
860
- return { findings: 0, status: "skipped" };
861
- }
862
-
863
- const knipResult = await clients.knip.analyze(
864
- targetPath,
865
- getKnipIgnorePatterns(),
866
- );
867
-
868
- // Filter out test file issues as additional safeguard
869
- const filteredIssues = knipResult.issues.filter(
870
- (issue) => !issue.file || shouldIncludeFile(issue.file),
871
- );
872
-
873
- if (filteredIssues.length > 0) {
874
- summaryItems.push({
875
- category: "Dead Code",
876
- count: filteredIssues.length,
877
- severity: "๐ŸŸก",
878
- fixable: true,
879
- });
880
-
881
- let fullSection = `## Dead Code (Knip)\n\n`;
882
- fullSection += `**${filteredIssues.length} issue(s) found**\n\n`;
883
- fullSection += "| Type | Name | File |\n|------|------|------|\n";
884
- for (const issue of filteredIssues) {
885
- fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
886
- }
887
- fullSection += "\n";
888
- fullReport.push(fullSection);
889
- }
890
-
891
- return { findings: filteredIssues.length, status: "done" };
892
- });
893
-
894
- // Runner 7: Duplicate code
895
- await tracker.run("duplicate code (jscpd)", async () => {
896
- if (!(await clients.jscpd.ensureAvailable())) {
897
- return { findings: 0, status: "skipped" };
898
- }
899
-
900
- // In TS projects, exclude .js files (they're compiled artifacts)
901
- const jscpdResult = await clients.jscpd.scan(
902
- targetPath,
903
- 5,
904
- 50,
905
- isTsProject,
906
- );
907
-
908
- // Filter out test file duplicates using centralized exclusion
909
- const filteredClones = jscpdResult.clones.filter(
910
- (dup) => shouldIncludeFile(dup.fileA) && shouldIncludeFile(dup.fileB),
911
- );
912
-
913
- if (filteredClones.length > 0) {
914
- summaryItems.push({
915
- category: "Duplicates",
916
- count: filteredClones.length,
917
- severity: "๐ŸŸก",
918
- fixable: true,
919
- });
920
-
921
- let fullSection = `## Code Duplication (jscpd)\n\n`;
922
- fullSection += `**${filteredClones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n`;
923
- fullSection +=
924
- "| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n";
925
- for (const dup of filteredClones) {
926
- fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
927
- }
928
- fullSection += "\n";
929
- fullReport.push(fullSection);
930
- }
931
-
932
- return { findings: filteredClones.length, status: "done" };
933
- });
934
-
935
- // Runner 8: Type coverage
936
- await tracker.run("type coverage", async () => {
937
- if (!langs.has("typescript")) {
938
- return { findings: 0, status: "skipped" };
939
- }
940
- if (!clients.typeCoverage.isAvailable()) {
941
- return { findings: 0, status: "skipped" };
942
- }
943
-
944
- const tcResult = clients.typeCoverage.scan(targetPath);
945
-
946
- if (tcResult.percentage < 100) {
947
- // Filter out test file locations using centralized exclusion
948
- const filteredLocations = tcResult.untypedLocations.filter((u) =>
949
- shouldIncludeFile(u.file),
950
- );
951
-
952
- const filesWithLowCoverage = new Set(
953
- filteredLocations
954
- .filter(() => tcResult.percentage < 90)
955
- .map((u) => u.file),
956
- ).size;
957
-
958
- summaryItems.push({
959
- category: "Type Coverage",
960
- count: filesWithLowCoverage || 1,
961
- severity: tcResult.percentage < 90 ? "๐ŸŸก" : "โ„น๏ธ",
962
- fixable: false,
963
- });
964
-
965
- let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
966
- fullSection +=
967
- "Type coverage highlights identifiers that resolve to `any` (implicit or explicit). Inferred non-`any` types are treated as typed.\n\n";
968
- const byFile: Record<string, number> = {};
969
- for (const u of filteredLocations) {
970
- byFile[u.file] = (byFile[u.file] || 0) + 1;
971
- }
972
- const sortedFiles = Object.entries(byFile)
973
- .filter(([file]) => shouldIncludeFile(file))
974
- .sort((a, b) => b[1] - a[1])
975
- .slice(0, 10);
976
-
977
- if (sortedFiles.length > 0) {
978
- fullSection += `### Top Files by Any-Typed Identifier Count\n\n| File | Any-Typed Count |\n|------|-----------------|\n`;
979
- for (const [file, count] of sortedFiles) {
980
- fullSection += `| ${file} | ${count} |\n`;
981
- }
982
- if (Object.keys(byFile).length > 10) {
983
- fullSection += `| ... | +${Object.keys(byFile).length - 10} more files |\n`;
984
- }
985
- }
986
- fullSection += "\n";
987
- fullReport.push(fullSection);
988
-
989
- return { findings: filesWithLowCoverage || 1, status: "done" };
990
- }
991
-
992
- return { findings: 0, status: "done" };
993
- });
994
-
995
- // Runner 9: Circular deps (JS/TS only โ€” Madge is a JS/TS tool)
996
- await tracker.run("circular deps (Madge)", async () => {
997
- if (!langs.has("javascript") && !langs.has("typescript")) {
998
- return { findings: 0, status: "skipped" };
999
- }
1000
- if (!(await clients.depChecker.ensureAvailable())) {
1001
- return { findings: 0, status: "skipped" };
1002
- }
1003
-
1004
- const { circular } = await clients.depChecker.scanProject(targetPath);
1005
-
1006
- // Filter out circular deps involving only test files using centralized exclusion
1007
- const filteredCircular = circular.filter((dep) => {
1008
- // Keep if ANY file in the chain is not a test file
1009
- return dep.path.some((file) => shouldIncludeFile(file));
1010
- });
1011
-
1012
- if (filteredCircular.length > 0) {
1013
- summaryItems.push({
1014
- category: "Circular Deps",
1015
- count: filteredCircular.length,
1016
- severity: "๐Ÿ”ด",
1017
- fixable: false,
1018
- });
1019
-
1020
- let fullSection = `## Circular Dependencies (Madge)\n\n`;
1021
- fullSection += `**${filteredCircular.length} circular chain(s) found**\n\n`;
1022
- for (const dep of filteredCircular) {
1023
- fullSection += `- ${dep.path.join(" โ†’ ")}\n`;
1024
- }
1025
- fullReport.push(`${fullSection}\n`);
1026
- }
1027
-
1028
- return { findings: filteredCircular.length, status: "done" };
1029
- });
1030
-
1031
- // Runner 11: Production Readiness (inspired by pi-validate)
1032
- await tracker.run("production readiness", async () => {
1033
- const readiness = validateProductionReadiness(targetPath);
1034
-
1035
- // Add to summary if not perfect
1036
- if (readiness.overallScore < 100) {
1037
- const severity =
1038
- readiness.grade === "A"
1039
- ? "๐ŸŸข"
1040
- : readiness.grade === "B"
1041
- ? "๐ŸŸข"
1042
- : readiness.grade === "C"
1043
- ? "๐ŸŸก"
1044
- : "๐ŸŸ ";
1045
-
1046
- // Count issues across all categories
1047
- const totalIssues_ = Object.values(readiness.categories).reduce(
1048
- (sum, cat) => sum + cat.issues.length,
1049
- 0,
1050
- );
1051
-
1052
- if (totalIssues_ > 0) {
1053
- summaryItems.push({
1054
- category: "Production Readiness",
1055
- count: totalIssues_,
1056
- severity: severity as "๐Ÿ”ด" | "๐ŸŸก" | "๐ŸŸข" | "โ„น๏ธ",
1057
- fixable: true,
1058
- });
1059
- }
1060
- }
1061
-
1062
- // Add to full report
1063
- let section = `## Production Readiness\n\n`;
1064
- section += `**Score:** ${readiness.overallScore}/100 **Grade:** ${readiness.grade}\n\n`;
1065
-
1066
- for (const [key, cat] of Object.entries(readiness.categories)) {
1067
- section += `### ${key.charAt(0).toUpperCase() + key.slice(1)} (${cat.score}/100)\n\n`;
1068
- if (cat.details.length > 0) {
1069
- for (const detail of cat.details) {
1070
- section += `- ${detail}\n`;
1071
- }
1072
- }
1073
- if (cat.issues.length > 0) {
1074
- for (const issue of cat.issues) {
1075
- section += `- โš ๏ธ ${issue}\n`;
1076
- }
1077
- }
1078
- if (cat.details.length === 0 && cat.issues.length === 0) {
1079
- section += `- โœ… No issues\n`;
1080
- }
1081
- section += "\n";
1082
- }
1083
-
1084
- fullReport.push(section);
1085
-
1086
- // Add metadata to report
1087
- const criticalIssues = [];
1088
- for (const [key, cat] of Object.entries(readiness.categories)) {
1089
- for (const issue of cat.issues) {
1090
- // Flag critical issues
1091
- if (key === "code" && issue.includes("debugger")) {
1092
- criticalIssues.push(`[CRITICAL] ${issue}`);
1093
- } else if (key === "tests" && cat.score < 50) {
1094
- criticalIssues.push(`[CRITICAL] No tests found`);
1095
- }
1096
- }
1097
- }
1098
-
1099
- return {
1100
- findings: Object.values(readiness.categories).reduce(
1101
- (sum, cat) => sum + cat.issues.length,
1102
- 0,
1103
- ),
1104
- status: "done",
1105
- };
1106
- });
1107
-
1108
- // Runner 12: Compiler checks (language-aware)
1109
- // Runs the project's native type-checker/compiler for whole-workspace type errors.
1110
- // Each language uses its canonical batch tool โ€” these catch cross-file breakage
1111
- // that per-file LSP checks miss (e.g. broken imports, declaration emit errors).
1112
- await tracker.run("compiler checks", async () => {
1113
- interface CompilerIssue {
1114
- file: string;
1115
- line: number;
1116
- col: number;
1117
- severity: string;
1118
- code: string;
1119
- message: string;
1120
- compiler: string;
1121
- }
1122
-
1123
- const issues: CompilerIssue[] = [];
1124
-
1125
- // TypeScript: tsc --noEmit
1126
- if (
1127
- langs.has("typescript") &&
1128
- nodeFs.existsSync(path.join(targetPath, "tsconfig.json"))
1129
- ) {
1130
- const result = safeSpawn(
1131
- "npx",
1132
- ["tsc", "--noEmit", "--pretty", "false"],
1133
- {
1134
- cwd: targetPath,
1135
- timeout: 60_000,
1136
- },
1137
- );
1138
- const output = (result.stdout || "") + (result.stderr || "");
1139
- // tsc --pretty false format: "file(line,col): error TS####: message"
1140
- const tscRe =
1141
- /^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$/gm;
1142
- for (const m of output.matchAll(tscRe)) {
1143
- const [, file, line, col, sev, code, msg] = m;
1144
- const absFile = path.isAbsolute(file)
1145
- ? file
1146
- : path.join(targetPath, file);
1147
- if (shouldIncludeFile(absFile)) {
1148
- issues.push({
1149
- file: path.relative(targetPath, absFile),
1150
- line: parseInt(line, 10),
1151
- col: parseInt(col, 10),
1152
- severity: sev,
1153
- code,
1154
- message: msg.trim(),
1155
- compiler: "tsc",
1156
- });
1157
- }
1158
- }
1159
- }
1160
-
1161
- // Go: go vet ./...
1162
- if (langs.has("go") && nodeFs.existsSync(path.join(targetPath, "go.mod"))) {
1163
- const result = safeSpawn("go", ["vet", "./..."], {
1164
- cwd: targetPath,
1165
- timeout: 60_000,
1166
- });
1167
- const output = (result.stderr || "") + (result.stdout || "");
1168
- // go vet format: "file.go:line:col: message" or "file.go:line: message"
1169
- const goRe = /^(.+\.go):(\d+)(?::(\d+))?:\s+(.+)$/gm;
1170
- for (const m of output.matchAll(goRe)) {
1171
- const [, file, line, col, msg] = m;
1172
- const absFile = path.isAbsolute(file)
1173
- ? file
1174
- : path.join(targetPath, file);
1175
- issues.push({
1176
- file: path.relative(targetPath, absFile),
1177
- line: parseInt(line, 10),
1178
- col: col ? parseInt(col, 10) : 1,
1179
- severity: "error",
1180
- code: "go-vet",
1181
- message: msg.trim(),
1182
- compiler: "go vet",
1183
- });
1184
- }
1185
- }
1186
-
1187
- // Rust: cargo check --message-format=json
1188
- if (
1189
- langs.has("rust") &&
1190
- nodeFs.existsSync(path.join(targetPath, "Cargo.toml"))
1191
- ) {
1192
- const result = safeSpawn(
1193
- "cargo",
1194
- ["check", "--message-format=json", "--quiet"],
1195
- {
1196
- cwd: targetPath,
1197
- timeout: 120_000,
1198
- },
1199
- );
1200
- const output = result.stdout || "";
1201
- for (const line of output.split("\n")) {
1202
- if (!line.trim()) continue;
1203
- try {
1204
- const msg = JSON.parse(line);
1205
- if (msg.reason !== "compiler-message") continue;
1206
- const inner = msg.message;
1207
- if (!inner || !["error", "warning"].includes(inner.level)) continue;
1208
- const span =
1209
- inner.spans?.find((s: { is_primary: boolean }) => s.is_primary) ??
1210
- inner.spans?.[0];
1211
- if (!span) continue;
1212
- const absFile = span.file_name
1213
- ? path.isAbsolute(span.file_name)
1214
- ? span.file_name
1215
- : path.join(targetPath, span.file_name)
1216
- : targetPath;
1217
- issues.push({
1218
- file: path.relative(targetPath, absFile),
1219
- line: span.line_start ?? 1,
1220
- col: span.column_start ?? 1,
1221
- severity: inner.level,
1222
- code: inner.code?.code ?? "cargo",
1223
- message: inner.message,
1224
- compiler: "cargo check",
1225
- });
1226
- } catch {
1227
- // non-JSON line
1228
- }
1229
- }
1230
- }
1231
-
1232
- // Python: pyright --outputjson (preferred) or mypy
1233
- if (langs.has("python")) {
1234
- const hasPyright =
1235
- safeSpawn("pyright", ["--version"], { timeout: 5_000 }).status === 0;
1236
- if (hasPyright) {
1237
- const result = safeSpawn("pyright", ["--outputjson", "."], {
1238
- cwd: targetPath,
1239
- timeout: 60_000,
1240
- });
1241
- const output = result.stdout || "";
1242
- try {
1243
- const json = JSON.parse(output);
1244
- for (const diag of json?.generalDiagnostics ?? []) {
1245
- if (!["error", "warning"].includes(diag.severity)) continue;
1246
- const absFile = diag.file
1247
- ? path.isAbsolute(diag.file)
1248
- ? diag.file
1249
- : path.join(targetPath, diag.file)
1250
- : targetPath;
1251
- if (shouldIncludeFile(absFile)) {
1252
- issues.push({
1253
- file: path.relative(targetPath, absFile),
1254
- line: (diag.range?.start?.line ?? 0) + 1,
1255
- col: (diag.range?.start?.character ?? 0) + 1,
1256
- severity: diag.severity,
1257
- code: diag.rule ?? "pyright",
1258
- message: diag.message,
1259
- compiler: "pyright",
1260
- });
1261
- }
1262
- }
1263
- } catch {
1264
- // pyright didn't produce valid JSON
1265
- }
1266
- }
1267
- }
1268
-
1269
- // Ruby: rubocop --format json
1270
- if (
1271
- langs.has("ruby") &&
1272
- nodeFs.existsSync(path.join(targetPath, "Gemfile"))
1273
- ) {
1274
- const hasRubocop =
1275
- safeSpawn("rubocop", ["--version"], { timeout: 5_000 }).status === 0;
1276
- if (hasRubocop) {
1277
- const result = safeSpawn(
1278
- "rubocop",
1279
- [
1280
- "--format",
1281
- "json",
1282
- "--no-color",
1283
- "--display-only-fail-level-offenses",
1284
- ],
1285
- { cwd: targetPath, timeout: 60_000 },
1286
- );
1287
- const output = result.stdout || "";
1288
- try {
1289
- const json = JSON.parse(output);
1290
- for (const fileResult of json?.files ?? []) {
1291
- const absFile = path.isAbsolute(fileResult.path)
1292
- ? fileResult.path
1293
- : path.join(targetPath, fileResult.path);
1294
- if (!shouldIncludeFile(absFile)) continue;
1295
- for (const offense of fileResult.offenses ?? []) {
1296
- const sev =
1297
- offense.severity === "error" || offense.severity === "fatal"
1298
- ? "error"
1299
- : "warning";
1300
- issues.push({
1301
- file: path.relative(targetPath, absFile),
1302
- line: offense.location?.line ?? 1,
1303
- col: offense.location?.column ?? 1,
1304
- severity: sev,
1305
- code: offense.cop_name ?? "rubocop",
1306
- message: offense.message ?? "",
1307
- compiler: "rubocop",
1308
- });
1309
- }
1310
- }
1311
- } catch {
1312
- // rubocop didn't produce valid JSON
1313
- }
1314
- }
1315
- }
1316
-
1317
- // Java: mvn compile (preferred) or javac on discovered .java files
1318
- if (nodeFs.existsSync(path.join(targetPath, "pom.xml"))) {
1319
- const result = safeSpawn(
1320
- "mvn",
1321
- ["compile", "-q", "-B", "--no-transfer-progress"],
1322
- {
1323
- cwd: targetPath,
1324
- timeout: 120_000,
1325
- },
1326
- );
1327
- const output = (result.stdout || "") + (result.stderr || "");
1328
- const mvnRe = /^\[ERROR\]\s+([^\s[]+\.java):\[(\d+),(\d+)\]\s+([^\n]+)/gm;
1329
- for (const m of output.matchAll(mvnRe)) {
1330
- const [, file, line, col, msg] = m;
1331
- const absFile = path.isAbsolute(file)
1332
- ? file
1333
- : path.join(targetPath, file);
1334
- if (shouldIncludeFile(absFile)) {
1335
- issues.push({
1336
- file: path.relative(targetPath, absFile),
1337
- line: parseInt(line, 10),
1338
- col: parseInt(col, 10),
1339
- severity: "error",
1340
- code: "javac",
1341
- message: msg.trim(),
1342
- compiler: "mvn compile",
1343
- });
1344
- }
1345
- }
1346
- } else if (
1347
- nodeFs.existsSync(path.join(targetPath, "build.gradle")) ||
1348
- nodeFs.existsSync(path.join(targetPath, "build.gradle.kts"))
1349
- ) {
1350
- const gradleCmd = nodeFs.existsSync(path.join(targetPath, "gradlew"))
1351
- ? "./gradlew"
1352
- : "gradle";
1353
- const result = safeSpawn(gradleCmd, ["compileJava", "--quiet"], {
1354
- cwd: targetPath,
1355
- timeout: 120_000,
1356
- });
1357
- const output = (result.stdout || "") + (result.stderr || "");
1358
- const gradleRe =
1359
- /^([^\s:]+\.(?:java|kt)):\s*(\d+):\s*(?:error|warning):\s+([^\n]+)/gm;
1360
- for (const m of output.matchAll(gradleRe)) {
1361
- const [, file, line, msg] = m;
1362
- const absFile = path.isAbsolute(file)
1363
- ? file
1364
- : path.join(targetPath, file);
1365
- if (shouldIncludeFile(absFile)) {
1366
- issues.push({
1367
- file: path.relative(targetPath, absFile),
1368
- line: parseInt(line, 10),
1369
- col: 1,
1370
- severity: "error",
1371
- code: "gradle",
1372
- message: msg.trim(),
1373
- compiler: "gradle compileJava",
1374
- });
1375
- }
1376
- }
1377
- }
1378
-
1379
- // C#: dotnet build
1380
- if (
1381
- nodeFs
1382
- .readdirSync(targetPath)
1383
- .some((e) => /\.(sln|slnx|csproj)$/i.test(e))
1384
- ) {
1385
- const hasDotnet =
1386
- safeSpawn("dotnet", ["--version"], { timeout: 5_000 }).status === 0;
1387
- if (hasDotnet) {
1388
- const result = safeSpawn(
1389
- "dotnet",
1390
- ["build", "--no-incremental", "-v", "minimal"],
1391
- { cwd: targetPath, timeout: 120_000 },
1392
- );
1393
- const output = (result.stdout || "") + (result.stderr || "");
1394
- const csRe =
1395
- /^([^\s(]+\.cs)\((\d+),(\d+)\):\s+(error|warning)\s+([A-Z]+\d+):\s+([^[]+)/gm;
1396
- for (const m of output.matchAll(csRe)) {
1397
- const [, file, line, col, sev, code, msg] = m;
1398
- const absFile = path.isAbsolute(file)
1399
- ? file
1400
- : path.join(targetPath, file);
1401
- if (shouldIncludeFile(absFile)) {
1402
- issues.push({
1403
- file: path.relative(targetPath, absFile),
1404
- line: parseInt(line, 10),
1405
- col: parseInt(col, 10),
1406
- severity: sev,
1407
- code,
1408
- message: msg.trim(),
1409
- compiler: "dotnet build",
1410
- });
1411
- }
1412
- }
1413
- }
1414
- }
1415
-
1416
- // Dart: dart analyze
1417
- if (nodeFs.existsSync(path.join(targetPath, "pubspec.yaml"))) {
1418
- const hasDart =
1419
- safeSpawn("dart", ["--version"], { timeout: 5_000 }).status === 0;
1420
- if (hasDart) {
1421
- const result = safeSpawn("dart", ["analyze", "--format=machine"], {
1422
- cwd: targetPath,
1423
- timeout: 60_000,
1424
- });
1425
- const output = (result.stdout || "") + (result.stderr || "");
1426
- // severity|type|code|file|line|col|length|message
1427
- for (const line of output.split(/\r?\n/)) {
1428
- const parts = line.split("|");
1429
- if (parts.length < 8) continue;
1430
- const [sev, , code, file, lineNum, , , ...msgParts] = parts;
1431
- const absFile = path.isAbsolute(file)
1432
- ? file
1433
- : path.join(targetPath, file);
1434
- if (!shouldIncludeFile(absFile)) continue;
1435
- issues.push({
1436
- file: path.relative(targetPath, absFile),
1437
- line: parseInt(lineNum, 10),
1438
- col: 1,
1439
- severity: sev.toLowerCase() === "error" ? "error" : "warning",
1440
- code: code.trim(),
1441
- message: msgParts.join("|").trim(),
1442
- compiler: "dart analyze",
1443
- });
1444
- }
1445
- }
1446
- }
1447
-
1448
- // Gleam: gleam check
1449
- if (nodeFs.existsSync(path.join(targetPath, "gleam.toml"))) {
1450
- const hasGleam =
1451
- safeSpawn("gleam", ["--version"], { timeout: 5_000 }).status === 0;
1452
- if (hasGleam) {
1453
- const result = safeSpawn("gleam", ["check"], {
1454
- cwd: targetPath,
1455
- timeout: 60_000,
1456
- });
1457
- const output = (result.stdout || "") + (result.stderr || "");
1458
- const gleamLines = output.split("\n");
1459
- const gleamHeaderRe = /^([^:]+):(\d+):(\d+)[ \t]*(?:error|warning)/;
1460
- for (let i = 0; i < gleamLines.length; i++) {
1461
- const m = gleamHeaderRe.exec(gleamLines[i]);
1462
- if (!m) continue;
1463
- const [, file, line, col] = m;
1464
- const msg = gleamLines[i + 1]?.trim() ?? "";
1465
- const absFile = path.isAbsolute(file)
1466
- ? file
1467
- : path.join(targetPath, file);
1468
- if (shouldIncludeFile(absFile)) {
1469
- issues.push({
1470
- file: path.relative(targetPath, absFile),
1471
- line: parseInt(line, 10),
1472
- col: parseInt(col, 10),
1473
- severity: "error",
1474
- code: "gleam",
1475
- message: msg,
1476
- compiler: "gleam check",
1477
- });
1478
- }
1479
- }
1480
- }
1481
- }
1482
-
1483
- // Zig: zig build (or zig check on entry files)
1484
- if (nodeFs.existsSync(path.join(targetPath, "build.zig"))) {
1485
- const hasZig =
1486
- safeSpawn("zig", ["version"], { timeout: 5_000 }).status === 0;
1487
- if (hasZig) {
1488
- const result = safeSpawn("zig", ["build", "--summary", "none"], {
1489
- cwd: targetPath,
1490
- timeout: 120_000,
1491
- });
1492
- const output = (result.stdout || "") + (result.stderr || "");
1493
- const zigLineRe =
1494
- /^([^:]+):(\d+):(\d+):[ \t]*(error|warning|note):[ \t]*(.+)/;
1495
- for (const zigLine of output.split("\n")) {
1496
- const m = zigLineRe.exec(zigLine);
1497
- if (!m) continue;
1498
- const [, file, line, col, sev, msg] = m;
1499
- const absFile = path.isAbsolute(file)
1500
- ? file
1501
- : path.join(targetPath, file);
1502
- if (!absFile.includes("zig-cache") && shouldIncludeFile(absFile)) {
1503
- issues.push({
1504
- file: path.relative(targetPath, absFile),
1505
- line: parseInt(line, 10),
1506
- col: parseInt(col, 10),
1507
- severity: sev === "error" ? "error" : "warning",
1508
- code: "zig",
1509
- message: msg.trim(),
1510
- compiler: "zig build",
1511
- });
1512
- }
1513
- }
1514
- }
1515
- }
1516
-
1517
- // Elixir: mix compile
1518
- if (nodeFs.existsSync(path.join(targetPath, "mix.exs"))) {
1519
- const hasMix =
1520
- safeSpawn("mix", ["--version"], { timeout: 5_000 }).status === 0;
1521
- if (hasMix) {
1522
- const result = safeSpawn("mix", ["compile", "--warnings-as-errors"], {
1523
- cwd: targetPath,
1524
- timeout: 60_000,
1525
- });
1526
- const output = (result.stdout || "") + (result.stderr || "");
1527
- const elixirLines = output.split(/\r?\n/);
1528
- for (let i = 0; i < elixirLines.length; i++) {
1529
- const line = elixirLines[i];
1530
- const prefixMatch = line.match(/^(?:warning|error):\s+/);
1531
- if (!prefixMatch) continue;
1532
- const msg = line.slice(prefixMatch[0].length);
1533
- const nextLine = elixirLines[i + 1];
1534
- if (!nextLine) continue;
1535
- const locMatch = nextLine.trim().match(/^([^:]+):(\d+)$/);
1536
- if (!locMatch) continue;
1537
- const [, file, lineNum] = locMatch;
1538
- const absFile = path.isAbsolute(file)
1539
- ? file
1540
- : path.join(targetPath, file);
1541
- if (shouldIncludeFile(absFile)) {
1542
- issues.push({
1543
- file: path.relative(targetPath, absFile),
1544
- line: parseInt(lineNum, 10),
1545
- col: 1,
1546
- severity: "warning",
1547
- code: "mix",
1548
- message: msg.trim(),
1549
- compiler: "mix compile",
1550
- });
1551
- }
1552
- }
1553
- }
1554
- }
1555
-
1556
- if (issues.length === 0) return { findings: 0, status: "done" };
1557
-
1558
- // Group by compiler for reporting
1559
- const byCompiler: Record<string, CompilerIssue[]> = {};
1560
- for (const issue of issues) {
1561
- if (!byCompiler[issue.compiler]) byCompiler[issue.compiler] = [];
1562
- byCompiler[issue.compiler].push(issue);
1563
- }
1564
-
1565
- const errorCount = issues.filter((i) => i.severity === "error").length;
1566
- summaryItems.push({
1567
- category: "Compiler Errors",
1568
- count: issues.length,
1569
- severity: errorCount > 0 ? "๐Ÿ”ด" : "๐ŸŸก",
1570
- fixable: true,
1571
- });
1572
-
1573
- let fullSection = `## Compiler Checks\n\n**${issues.length} issue(s) found** (${errorCount} errors)\n\n`;
1574
- for (const [compiler, compIssues] of Object.entries(byCompiler)) {
1575
- fullSection += `### ${compiler} (${compIssues.length})\n\n`;
1576
- fullSection +=
1577
- "| File | Line | Code | Message |\n|------|------|------|---------|\n";
1578
- for (const issue of compIssues.slice(0, 30)) {
1579
- const sev = issue.severity === "error" ? "๐Ÿ”ด" : "๐ŸŸก";
1580
- fullSection += `| ${issue.file} | ${issue.line} | ${sev} ${issue.code} | ${issue.message} |\n`;
1581
- }
1582
- if (compIssues.length > 30) {
1583
- fullSection += `| ... | | | +${compIssues.length - 30} more |\n`;
1584
- }
1585
- fullSection += "\n";
1586
- }
1587
- fullReport.push(fullSection);
1588
-
1589
- return { findings: issues.length, status: "done" };
1590
- });
1591
-
1592
- // --- Create structured JSON report ---
1593
- nodeFs.mkdirSync(reviewDir, { recursive: true });
1594
- const projectName = path.basename(reviewRoot);
1595
-
1596
- const totalIssues = summaryItems.reduce((sum, s) => sum + s.count, 0);
1597
- const fixableCount = summaryItems
1598
- .filter((s) => s.fixable)
1599
- .reduce((sum, s) => sum + s.count, 0);
1600
- const refactorNeeded = summaryItems
1601
- .filter((s) => !s.fixable)
1602
- .reduce((sum, s) => sum + s.count, 0);
1603
-
1604
- // Build runner summary
1605
- const runnerSummary = tracker.getRunners().map((r) => ({
1606
- name: r.name,
1607
- status: r.status,
1608
- findings: r.findings,
1609
- time: formatElapsed(r.elapsedMs),
1610
- }));
1611
-
1612
- const jsonReport = {
1613
- meta: {
1614
- timestamp: new Date().toISOString(),
1615
- project: projectName,
1616
- path: targetPath,
1617
- totalIssues,
1618
- fixableCount,
1619
- refactorNeeded,
1620
- // New: runner execution details
1621
- runners: runnerSummary,
1622
- totalTime: formatElapsed(
1623
- runnerSummary.reduce((sum, r) => {
1624
- const ms = r.time.endsWith("ms")
1625
- ? parseInt(r.time, 10)
1626
- : parseFloat(r.time) * 1000;
1627
- return sum + (Number.isNaN(ms) ? 0 : ms);
1628
- }, 0),
1629
- ),
1630
- },
1631
- // New: project metadata
1632
- project: {
1633
- type: projectMeta.type,
1634
- name: projectMeta.name,
1635
- version: projectMeta.version,
1636
- packageManager: projectMeta.packageManager,
1637
- languages: projectMeta.languages,
1638
- hasTests: projectMeta.hasTests,
1639
- testFramework: projectMeta.testFramework,
1640
- hasLinting: projectMeta.hasLinting,
1641
- linter: projectMeta.linter,
1642
- hasFormatting: projectMeta.hasFormatting,
1643
- formatter: projectMeta.formatter,
1644
- hasTypeScript: projectMeta.hasTypeScript,
1645
- configFiles: projectMeta.configFiles,
1646
- scripts: projectMeta.scripts,
1647
- },
1648
- // New: available commands for the project
1649
- commands: availableCommands,
1650
- byCategory: summaryItems.reduce(
1651
- (acc, item) => {
1652
- acc[item.category] = {
1653
- count: item.count,
1654
- severity: item.severity,
1655
- fixable: item.fixable,
1656
- falsePositivePrefix: `${categoryKey(item.category)}:`,
1657
- };
1658
- return acc;
1659
- },
1660
- {} as Record<
1661
- string,
1662
- {
1663
- count: number;
1664
- severity: string;
1665
- fixable: boolean;
1666
- falsePositivePrefix: string;
1667
- }
1668
- >,
1669
- ),
1670
- howToMarkFalsePositive: {
1671
- command: "Ignore via AGENTS.md rules or suppress comments",
1672
- format: "Add to .claude/rules or use biome/oxlint ignore comments",
1673
- examples: [
1674
- "// biome-ignore lint/suspicious/noConsole: intentional debug",
1675
- "// oxlint-disable-next-line no-console",
1676
- ],
1677
- },
1678
- sessionFile: path.join(reviewRoot, ".pi-lens", "fix-session.json"),
1679
- details: fullReport.join("\n"),
1680
- };
1681
-
1682
- const jsonPath = path.join(reviewDir, `booboo-${timestamp}.json`);
1683
- nodeFs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), "utf-8");
1684
-
1685
- // --- Create markdown report ---
1686
-
1687
- // Build project info section
1688
- let projectSection = `## Project Info\n\n**Type:** ${projectMeta.type}`;
1689
- if (projectMeta.name) projectSection += ` | **Name:** ${projectMeta.name}`;
1690
- if (projectMeta.version)
1691
- projectSection += ` | **Version:** ${projectMeta.version}`;
1692
- if (projectMeta.packageManager)
1693
- projectSection += `\n**Package Manager:** ${projectMeta.packageManager}`;
1694
- if (projectMeta.languages.length > 0)
1695
- projectSection += `\n**Languages:** ${projectMeta.languages.join(", ")}`;
1696
-
1697
- // Tools
1698
- const tools: string[] = [];
1699
- if (projectMeta.testFramework) tools.push(`๐Ÿงช ${projectMeta.testFramework}`);
1700
- else if (projectMeta.hasTests) tools.push("๐Ÿงช tests");
1701
- if (projectMeta.linter) tools.push(`๐Ÿ” ${projectMeta.linter}`);
1702
- if (projectMeta.formatter) tools.push(`โœจ ${projectMeta.formatter}`);
1703
- if (tools.length > 0) projectSection += `\n**Tools:** ${tools.join(" | ")}`;
1704
-
1705
- // Available commands
1706
- if (availableCommands.length > 0) {
1707
- projectSection += `\n\n### Available Commands\n\n| Action | Command |\n|--------|---------|`;
1708
- for (const cmd of availableCommands) {
1709
- projectSection += `\n| ${cmd.action} | \`${cmd.command}\` |`;
1710
- }
1711
- }
1712
-
1713
- const mdReport = `# Code Review: ${projectName}
1714
-
1715
- **Scanned:** ${jsonReport.meta.timestamp}
1716
- **Path:** \`${targetPath}\`
1717
- **Summary:** ${jsonReport.meta.totalIssues} issues | ${jsonReport.meta.fixableCount} fixable | ${jsonReport.meta.refactorNeeded} need refactor
1718
- **Total Time:** ${jsonReport.meta.totalTime}
1719
-
1720
- ${projectSection}
1721
-
1722
- ## Runner Summary
1723
-
1724
- | Runner | Status | Findings | Time |
1725
- |--------|--------|----------|------|
1726
- ${runnerSummary.map((r) => `| ${r.name} | ${r.status} | ${r.findings} | ${r.time} |`).join("\n")}
1727
-
1728
- ---
1729
-
1730
- ${fullReport.join("\n")}`;
1731
-
1732
- const mdPath = path.join(reviewDir, `booboo-${timestamp}.md`);
1733
- nodeFs.writeFileSync(mdPath, mdReport, "utf-8");
1734
-
1735
- // --- Brief terminal summary ---
1736
- if (summaryItems.length === 0) {
1737
- ctx.ui.notify("โœ“ Code review clean", "info");
1738
- } else {
1739
- const { totalIssues } = jsonReport.meta;
1740
-
1741
- // Build runner lines for terminal output
1742
- const runnerLines = tracker
1743
- .getRunners()
1744
- .filter((r) => r.findings > 0)
1745
- .map(
1746
- (r) =>
1747
- ` ${r.status === "error" ? "โœ—" : "โš "} ${r.name}: ${r.findings} finding${r.findings !== 1 ? "s" : ""} (${formatElapsed(r.elapsedMs)})`,
1748
- );
1749
-
1750
- const summaryLines = [
1751
- `๐Ÿ“Š Code Review: ${totalIssues} issues`,
1752
- ...runnerLines,
1753
- ` โฑ๏ธ Total: ${jsonReport.meta.totalTime}`,
1754
- `๐Ÿ“„ MD: ${mdPath}`,
1755
- ];
1756
-
1757
- ctx.ui.notify(summaryLines.join("\n"), "info");
1758
- }
1759
- }
1760
-
1761
- // ============================================================================
1762
- // Semantic Similarity Helper
1763
- // ============================================================================
1764
-
1765
- interface SimilarPair {
1766
- func1: string;
1767
- func2: string;
1768
- similarity: number;
1769
- }
1770
-
1771
- const SEMANTIC_SIMILARITY_THRESHOLD = 0.98;
1772
- const MIN_SIMILARITY_TRANSITIONS = 40;
1773
- const MAX_TRANSITION_RATIO = 1.8;
1774
-
1775
- /**
1776
- * Find top N most similar function pairs in the project index
1777
- * Uses canonical pair ordering to avoid duplicates (A,B) vs (B,A)
1778
- */
1779
- function findTopSimilarPairs(
1780
- index: ProjectIndex,
1781
- maxPairs: number,
1782
- ): SimilarPair[] {
1783
- const entries = Array.from(index.entries.values());
1784
- const seenPairs = new Set<string>();
1785
- const pairs: SimilarPair[] = [];
1786
-
1787
- for (let i = 0; i < entries.length; i++) {
1788
- for (let j = i + 1; j < entries.length; j++) {
1789
- const entry1 = entries[i];
1790
- const entry2 = entries[j];
1791
-
1792
- // Skip if same file (we want cross-file duplicates)
1793
- if (entry1.filePath === entry2.filePath) continue;
1794
-
1795
- // Skip low-signal functions where matrix noise dominates.
1796
- if (
1797
- entry1.transitionCount < MIN_SIMILARITY_TRANSITIONS ||
1798
- entry2.transitionCount < MIN_SIMILARITY_TRANSITIONS
1799
- ) {
1800
- continue;
1801
- }
1802
-
1803
- // Skip pairs with very different complexity/size; these are often
1804
- // boilerplate-wrapper false positives (shared try/catch/logging shell).
1805
- const maxTransitions = Math.max(
1806
- entry1.transitionCount,
1807
- entry2.transitionCount,
1808
- );
1809
- const minTransitions = Math.min(
1810
- entry1.transitionCount,
1811
- entry2.transitionCount,
1812
- );
1813
- if (minTransitions <= 0) continue;
1814
- if (maxTransitions / minTransitions > MAX_TRANSITION_RATIO) continue;
1815
-
1816
- const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
1817
-
1818
- if (similarity >= SEMANTIC_SIMILARITY_THRESHOLD) {
1819
- // Canonical pair key (sorted to avoid duplicates)
1820
- const pairKey = [entry1.id, entry2.id]
1821
- .sort((a, b) => a.localeCompare(b))
1822
- .join("::");
1823
- if (seenPairs.has(pairKey)) continue;
1824
- seenPairs.add(pairKey);
1825
-
1826
- pairs.push({
1827
- func1: entry1.id,
1828
- func2: entry2.id,
1829
- similarity,
1830
- });
1831
- }
1832
- }
1833
- }
1834
-
1835
- // Sort by similarity descending, take top N
1836
- return pairs.sort((a, b) => b.similarity - a.similarity).slice(0, maxPairs);
1837
- }