pi-lens 2.2.9 โ†’ 3.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 (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -1,26 +1,89 @@
1
- import * as childProcess from "node:child_process";
2
1
  import * as nodeFs from "node:fs";
3
2
  import * as path from "node:path";
3
+ import { EXCLUDED_DIRS, isTestFile } from "../clients/file-utils.js";
4
+ import { validateProductionReadiness } from "../clients/production-readiness.js";
5
+ import { buildProjectIndex, } from "../clients/project-index.js";
6
+ import { detectProjectMetadata, formatProjectMetadata, getAvailableCommands, } from "../clients/project-metadata.js";
7
+ import { RunnerTracker } from "../clients/runner-tracker.js";
8
+ import { safeSpawn } from "../clients/safe-spawn.js";
4
9
  import { getSourceFiles } from "../clients/scan-utils.js";
10
+ import { calculateSimilarity } from "../clients/state-matrix.js";
11
+ import { TreeSitterClient } from "../clients/tree-sitter-client.js";
5
12
  const getExtensionDir = () => {
6
13
  if (typeof __dirname !== "undefined") {
7
14
  return __dirname;
8
15
  }
9
16
  return ".";
10
17
  };
18
+ /**
19
+ * Centralized test file exclusion for booboo runners.
20
+ * Mirrors the dispatch system's skipTestFiles behavior.
21
+ */
22
+ function shouldIncludeFile(filePath) {
23
+ return !isTestFile(filePath);
24
+ }
25
+ /** Standard test file glob exclusions for CLI tools */
26
+ const _TEST_FILE_EXCLUDES = [
27
+ "!**/*.test.ts",
28
+ "!**/*.test.tsx",
29
+ "!**/*.test.js",
30
+ "!**/*.test.jsx",
31
+ "!**/*.spec.ts",
32
+ "!**/*.spec.tsx",
33
+ "!**/*.spec.js",
34
+ "!**/*.spec.jsx",
35
+ "!**/*.poc.test.ts",
36
+ "!**/*.poc.test.tsx",
37
+ "!**/test-utils.ts",
38
+ "!**/test-*.ts",
39
+ "!**/__tests__/**",
40
+ "!**/tests/**",
41
+ "!**/test/**",
42
+ ];
11
43
  export async function handleBooboo(args, ctx, clients, pi) {
12
44
  const targetPath = args.trim() || ctx.cwd || process.cwd();
13
- ctx.ui.notify("๐Ÿ” Running full codebase review...", "info");
45
+ // Detect project metadata for richer reporting
46
+ const projectMeta = detectProjectMetadata(targetPath);
47
+ const metaDisplay = formatProjectMetadata(projectMeta);
48
+ ctx.ui.notify(`๐Ÿ” Running full codebase review...\n${metaDisplay}`, "info");
49
+ // Detect project type once for all runners
50
+ const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
51
+ // Get available commands for the project
52
+ const availableCommands = getAvailableCommands(projectMeta);
53
+ // Load false positives from fix session to filter them out
54
+ const sessionFile = path.join(process.cwd(), ".pi-lens", "fix-session.json");
55
+ let falsePositives = [];
56
+ try {
57
+ const sessionData = JSON.parse(nodeFs.readFileSync(sessionFile, "utf-8") || "{}");
58
+ falsePositives = sessionData.falsePositives || [];
59
+ }
60
+ catch {
61
+ // No session file yet
62
+ }
63
+ // Helper to check if an issue is marked as false positive
64
+ const isFalsePositive = (category, file, line) => {
65
+ const fpKey = line !== undefined
66
+ ? `${category}:${file}:${line}`
67
+ : `${category}:${file}`;
68
+ return falsePositives.some((fp) => fp === fpKey || fp.startsWith(`${category}:${file}`));
69
+ };
14
70
  // Summary counts for terminal display
15
71
  const summaryItems = [];
16
72
  const fullReport = [];
17
73
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
18
74
  const reviewDir = path.join(process.cwd(), ".pi-lens", "reviews");
19
- // Part 1: Design smells via ast-grep
20
- if (clients.astGrep.isAvailable()) {
75
+ // Initialize runner tracker (no per-runner progress to avoid UI overwriting)
76
+ const tracker = new RunnerTracker();
77
+ // Helper to format elapsed time
78
+ const formatElapsed = (ms) => ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
79
+ // Runner 1: Design smells via ast-grep
80
+ await tracker.run("ast-grep (design smells)", async () => {
81
+ if (!clients.astGrep.isAvailable()) {
82
+ return { findings: 0, status: "skipped" };
83
+ }
21
84
  const configPath = path.join(getExtensionDir(), "..", "rules", "ast-grep-rules", ".sgconfig.yml");
22
85
  try {
23
- const result = childProcess.spawnSync("npx", [
86
+ const result = safeSpawn("npx", [
24
87
  "sg",
25
88
  "scan",
26
89
  "--config",
@@ -31,15 +94,28 @@ export async function handleBooboo(args, ctx, clients, pi) {
31
94
  "--globs",
32
95
  "!**/*.spec.ts",
33
96
  "--globs",
97
+ "!**/*.poc.test.ts",
98
+ "--globs",
34
99
  "!**/test-utils.ts",
35
100
  "--globs",
101
+ "!**/test-*.ts",
102
+ "--globs",
103
+ "!**/__tests__/**",
104
+ "--globs",
105
+ "!**/tests/**",
106
+ "--globs",
36
107
  "!**/.pi-lens/**",
108
+ "--globs",
109
+ "!**/.pi/**",
110
+ "--globs",
111
+ "!**/node_modules/**",
112
+ "--globs",
113
+ "!**/.git/**",
114
+ "--globs",
115
+ "!**/.ruff_cache/**",
37
116
  targetPath,
38
117
  ], {
39
- encoding: "utf-8",
40
118
  timeout: 30000,
41
- shell: true,
42
- maxBuffer: 32 * 1024 * 1024, // 32MB
43
119
  });
44
120
  const output = result.stdout || result.stderr || "";
45
121
  if (output.trim() && result.status !== undefined) {
@@ -50,8 +126,7 @@ export async function handleBooboo(args, ctx, clients, pi) {
50
126
  try {
51
127
  return JSON.parse(trimmed);
52
128
  }
53
- catch (err) {
54
- void err;
129
+ catch {
55
130
  return [];
56
131
  }
57
132
  }
@@ -59,8 +134,7 @@ export async function handleBooboo(args, ctx, clients, pi) {
59
134
  try {
60
135
  return [JSON.parse(l)];
61
136
  }
62
- catch (err) {
63
- void err;
137
+ catch {
64
138
  return [];
65
139
  }
66
140
  });
@@ -74,45 +148,73 @@ export async function handleBooboo(args, ctx, clients, pi) {
74
148
  item.range?.start?.line ||
75
149
  0;
76
150
  issues.push({
151
+ file: item.file || item.path || targetPath,
77
152
  line: lineNum + 1,
78
153
  rule: ruleId,
79
154
  message: message,
80
155
  });
81
156
  }
82
- if (issues.length > 0) {
157
+ const filteredIssues = issues.filter((issue) => !isFalsePositive("ast_issues", issue.file, issue.line));
158
+ if (filteredIssues.length > 0) {
83
159
  summaryItems.push({
84
160
  category: "ast-grep",
85
- count: issues.length,
86
- severity: issues.length > 10 ? "๐Ÿ”ด" : "๐ŸŸก",
161
+ count: filteredIssues.length,
162
+ severity: filteredIssues.length > 10 ? "๐Ÿ”ด" : "๐ŸŸก",
87
163
  fixable: true,
88
164
  });
89
- let fullSection = `## ast-grep (Structural Issues)\n\n**${issues.length} issue(s) found**\n\n`;
165
+ let fullSection = `## ast-grep (Structural Issues)\n\n**${filteredIssues.length} issue(s) found**\n\n`;
90
166
  fullSection +=
91
167
  "| Line | Rule | Message |\n|------|------|--------|\n";
92
- for (const issue of issues) {
168
+ for (const issue of filteredIssues) {
93
169
  fullSection += `| ${issue.line} | ${issue.rule} | ${issue.message} |\n`;
94
170
  }
171
+ fullSection += "\n### ๐Ÿ’ก How to Fix\n\n";
172
+ const seenRules = new Set();
173
+ for (const issue of filteredIssues.slice(0, 5)) {
174
+ if (seenRules.has(issue.rule))
175
+ continue;
176
+ seenRules.add(issue.rule);
177
+ const ruleDesc = clients.astGrep.getRuleDescription?.(issue.rule);
178
+ if (ruleDesc?.note || ruleDesc?.fix) {
179
+ fullSection += `**${issue.rule}:**\n`;
180
+ if (ruleDesc.note)
181
+ fullSection += `${ruleDesc.note}\n\n`;
182
+ if (ruleDesc.fix)
183
+ fullSection += `Suggested fix:\n\`\`\`typescript\n${ruleDesc.fix}\n\`\`\`\n\n`;
184
+ }
185
+ }
95
186
  fullReport.push(fullSection);
96
187
  }
188
+ return { findings: filteredIssues.length, status: "done" };
97
189
  }
190
+ return { findings: 0, status: "done" };
98
191
  }
99
- catch (err) {
100
- const _err = err;
101
- // Ignored
192
+ catch {
193
+ return { findings: 0, status: "error" };
194
+ }
195
+ });
196
+ // Runner 2: Similar functions
197
+ await tracker.run("ast-grep (similar functions)", async () => {
198
+ if (!clients.astGrep.isAvailable()) {
199
+ return { findings: 0, status: "skipped" };
102
200
  }
103
- }
104
- // Part 2: Similar functions
105
- if (clients.astGrep.isAvailable()) {
106
201
  const similarGroups = await clients.astGrep.findSimilarFunctions(targetPath, "typescript");
107
- if (similarGroups.length > 0) {
202
+ // Filter out test files using centralized exclusion
203
+ const filteredGroups = similarGroups
204
+ .map((group) => ({
205
+ ...group,
206
+ functions: group.functions.filter((fn) => shouldIncludeFile(fn.file)),
207
+ }))
208
+ .filter((group) => group.functions.length > 1); // Need at least 2 non-test functions
209
+ if (filteredGroups.length > 0) {
108
210
  summaryItems.push({
109
211
  category: "Similar Functions",
110
- count: similarGroups.length,
212
+ count: filteredGroups.length,
111
213
  severity: "๐ŸŸก",
112
214
  fixable: true,
113
215
  });
114
- let fullSection = `## Similar Functions\n\n**${similarGroups.length} group(s) of structurally similar functions**\n\n`;
115
- for (const group of similarGroups) {
216
+ let fullSection = `## Similar Functions\n\n**${filteredGroups.length} group(s) of structurally similar functions**\n\n`;
217
+ for (const group of filteredGroups) {
116
218
  fullSection += `### Pattern: ${group.functions.map((f) => f.name).join(", ")}\n\n`;
117
219
  fullSection +=
118
220
  "| Function | File | Line |\n|----------|------|------|\n";
@@ -123,18 +225,74 @@ export async function handleBooboo(args, ctx, clients, pi) {
123
225
  }
124
226
  fullReport.push(fullSection);
125
227
  }
126
- }
127
- // Part 3: Complexity metrics
128
- const results = [];
129
- const aiSlopIssues = [];
130
- const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
131
- const files = getSourceFiles(targetPath, isTsProject);
132
- for (const fullPath of files) {
133
- if (clients.complexity.isSupportedFile(fullPath)) {
134
- const metrics = clients.complexity.analyzeFile(fullPath);
135
- if (metrics) {
136
- results.push(metrics);
137
- if (!/\.(test|spec)\.[jt]sx?$/.test(path.basename(fullPath))) {
228
+ return { findings: filteredGroups.length, status: "done" };
229
+ });
230
+ // Runner 3: Semantic similarity
231
+ await tracker.run("semantic similarity (Amain)", async () => {
232
+ try {
233
+ const { glob } = await import("glob");
234
+ const sourceFiles = await glob("**/*.ts", {
235
+ cwd: targetPath,
236
+ ignore: [
237
+ "**/node_modules/**",
238
+ "**/*.test.ts",
239
+ "**/*.test.tsx",
240
+ "**/*.spec.ts",
241
+ "**/*.spec.tsx",
242
+ "**/*.poc.test.ts",
243
+ "**/*.poc.test.tsx",
244
+ "**/test-utils.ts",
245
+ "**/test-*.ts",
246
+ "**/__tests__/**",
247
+ "**/tests/**",
248
+ "**/dist/**",
249
+ ],
250
+ });
251
+ if (sourceFiles.length === 0) {
252
+ return { findings: 0, status: "done" };
253
+ }
254
+ // Filter out test files using centralized exclusion
255
+ const absoluteFiles = sourceFiles
256
+ .map((f) => path.join(targetPath, f))
257
+ .filter(shouldIncludeFile);
258
+ const index = await buildProjectIndex(targetPath, absoluteFiles);
259
+ const topPairs = findTopSimilarPairs(index, 10);
260
+ if (topPairs.length > 0) {
261
+ summaryItems.push({
262
+ category: "Semantic Duplicates",
263
+ count: topPairs.length,
264
+ severity: "๐ŸŸก",
265
+ fixable: true,
266
+ });
267
+ let fullSection = `## Semantic Duplicates (Amain Algorithm)\n\n`;
268
+ fullSection += `**${topPairs.length} pair(s) with >75% semantic similarity**\n\n`;
269
+ fullSection +=
270
+ "Functions with different names/variables but similar logic structures.\n\n";
271
+ for (const pair of topPairs) {
272
+ fullSection += `### ${pair.func1} โ†” ${pair.func2}\n\n`;
273
+ fullSection += `- Similarity: **${(pair.similarity * 100).toFixed(1)}%**\n`;
274
+ fullSection += `- Consider consolidating or extracting shared logic\n\n`;
275
+ }
276
+ fullReport.push(fullSection);
277
+ }
278
+ return { findings: topPairs.length, status: "done" };
279
+ }
280
+ catch (err) {
281
+ console.error("[booboo] Semantic similarity analysis failed:", err);
282
+ return { findings: 0, status: "error" };
283
+ }
284
+ });
285
+ // Runner 4: Complexity metrics
286
+ await tracker.run("complexity metrics", async () => {
287
+ const results = [];
288
+ const aiSlopIssues = [];
289
+ const files = getSourceFiles(targetPath, isTsProject).filter(shouldIncludeFile);
290
+ for (const fullPath of files) {
291
+ if (clients.complexity.isSupportedFile(fullPath)) {
292
+ const metrics = clients.complexity.analyzeFile(fullPath);
293
+ if (metrics) {
294
+ results.push(metrics);
295
+ // AI slop check - already filtered by shouldIncludeFile above
138
296
  const warnings = clients.complexity.checkThresholds(metrics);
139
297
  if (warnings.length > 0) {
140
298
  aiSlopIssues.push(` ${metrics.filePath}:`);
@@ -145,227 +303,439 @@ export async function handleBooboo(args, ctx, clients, pi) {
145
303
  }
146
304
  }
147
305
  }
148
- }
149
- if (results.length > 0) {
150
- const avgMI = results.reduce((a, b) => a + b.maintainabilityIndex, 0) / results.length;
151
- const avgCognitive = results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
152
- const avgCyclomatic = results.reduce((a, b) => a + b.cyclomaticComplexity, 0) / results.length;
153
- const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
154
- const maxCognitive = Math.max(...results.map((r) => r.cognitiveComplexity));
155
- const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
156
- const lowMI = results
157
- .filter((r) => r.maintainabilityIndex < 60)
158
- .sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
159
- const highCognitive = results
160
- .filter((r) => r.cognitiveComplexity > 20)
161
- .sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
162
- let _summary = `[Complexity] ${results.length} file(s) scanned\n`;
163
- _summary += ` Maintainability: ${avgMI.toFixed(1)} avg | Cognitive: ${avgCognitive.toFixed(1)} avg | Max Nesting: ${maxNesting} levels\n`;
164
- if (lowMI.length > 0) {
165
- _summary += `\n Low Maintainability (MI < 60):\n`;
166
- for (const f of lowMI.slice(0, 5)) {
167
- _summary += ` โœ— ${f.filePath}: MI ${f.maintainabilityIndex.toFixed(1)}\n`;
168
- }
169
- if (lowMI.length > 5)
170
- _summary += ` ... and ${lowMI.length - 5} more\n`;
171
- }
172
- if (highCognitive.length > 0) {
173
- _summary += `\n High Cognitive Complexity (> 20):\n`;
174
- for (const f of highCognitive.slice(0, 5)) {
175
- _summary += ` โš  ${f.filePath}: ${f.cognitiveComplexity}\n`;
176
- }
177
- if (highCognitive.length > 5)
178
- _summary += ` ... and ${highCognitive.length - 5} more\n`;
179
- }
180
- if (aiSlopIssues.length > 0) {
181
- _summary += `\n[AI Slop Indicators]\n${aiSlopIssues.join("\n")}`;
182
- }
183
- // Add complexity summary items
184
- if (lowMI.length > 0) {
185
- summaryItems.push({
186
- category: "Low MI",
187
- count: lowMI.length,
188
- severity: lowMI.some((f) => f.maintainabilityIndex < 20) ? "๐Ÿ”ด" : "๐ŸŸก",
189
- fixable: false,
190
- });
306
+ if (results.length > 0) {
307
+ const avgMI = results.reduce((a, b) => a + b.maintainabilityIndex, 0) /
308
+ results.length;
309
+ const avgCognitive = results.reduce((a, b) => a + b.cognitiveComplexity, 0) / results.length;
310
+ const avgCyclomatic = results.reduce((a, b) => a + b.cyclomaticComplexity, 0) /
311
+ results.length;
312
+ const maxNesting = Math.max(...results.map((r) => r.maxNestingDepth));
313
+ const maxCognitive = Math.max(...results.map((r) => r.cognitiveComplexity));
314
+ const minMI = Math.min(...results.map((r) => r.maintainabilityIndex));
315
+ // Only flag files with EXTREME issues (tuned to reduce false positives)
316
+ // MI < 20 is "critically unmaintainable" (was < 40, too aggressive)
317
+ const severeLowMI = results
318
+ .filter((r) => r.maintainabilityIndex < 20 && !isTestFile(r.filePath))
319
+ .sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex);
320
+ // Cognitive > 80 is extreme (was > 30, flagged too many files)
321
+ const veryHighCognitive = results
322
+ .filter((r) => r.cognitiveComplexity > 80 && !isTestFile(r.filePath))
323
+ .sort((a, b) => b.cognitiveComplexity - a.cognitiveComplexity);
324
+ // Deep nesting > 8 levels is extreme (was > 5, normal code hits this)
325
+ const deepNesting = results
326
+ .filter((r) => r.maxNestingDepth > 8 && !isTestFile(r.filePath))
327
+ .sort((a, b) => b.maxNestingDepth - a.maxNestingDepth);
328
+ let findings = 0;
329
+ if (severeLowMI.length > 0) {
330
+ findings += severeLowMI.length;
331
+ summaryItems.push({
332
+ category: "Low Maintainability",
333
+ count: severeLowMI.length,
334
+ severity: "๐Ÿ”ด",
335
+ fixable: false,
336
+ });
337
+ }
338
+ if (veryHighCognitive.length > 0) {
339
+ findings += veryHighCognitive.length;
340
+ summaryItems.push({
341
+ category: "Very High Complexity",
342
+ count: veryHighCognitive.length,
343
+ severity: "๐Ÿ”ด",
344
+ fixable: true,
345
+ });
346
+ }
347
+ if (deepNesting.length > 0) {
348
+ findings += deepNesting.length;
349
+ summaryItems.push({
350
+ category: "Deep Nesting",
351
+ count: deepNesting.length,
352
+ severity: "๐ŸŸก",
353
+ fixable: true,
354
+ });
355
+ }
356
+ if (aiSlopIssues.length > 0) {
357
+ findings += Math.floor(aiSlopIssues.length / 2);
358
+ summaryItems.push({
359
+ category: "AI Slop",
360
+ count: Math.floor(aiSlopIssues.length / 2),
361
+ severity: "๐ŸŸก",
362
+ fixable: true,
363
+ });
364
+ }
365
+ let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
366
+ fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n`;
367
+ fullSection += `| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n`;
368
+ fullSection += `| Min Maintainability Index | ${minMI.toFixed(1)} |\n`;
369
+ fullSection += `| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n`;
370
+ fullSection += `| Max Cognitive Complexity | ${maxCognitive} |\n`;
371
+ fullSection += `| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n`;
372
+ fullSection += `| Max Nesting Depth | ${maxNesting} |\n`;
373
+ fullSection += `| Total Files | ${results.length} |\n\n`;
374
+ // Report severe issues (thresholds match findings count)
375
+ if (severeLowMI.length > 0) {
376
+ fullSection += `### Low Maintainability (MI < 40)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
377
+ for (const f of severeLowMI) {
378
+ fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
379
+ }
380
+ fullSection += "\n";
381
+ }
382
+ if (veryHighCognitive.length > 0) {
383
+ fullSection += `### Very High Cognitive Complexity (> 30)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
384
+ for (const f of veryHighCognitive) {
385
+ fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
386
+ }
387
+ fullSection += "\n";
388
+ }
389
+ if (deepNesting.length > 0) {
390
+ fullSection += `### Deep Nesting (> 5 levels)\n\n| File | Nesting | Cognitive | MI |\n|------|---------|-----------|-----|\n`;
391
+ for (const f of deepNesting) {
392
+ fullSection += `| ${f.filePath} | ${f.maxNestingDepth} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} |\n`;
393
+ }
394
+ fullSection += "\n";
395
+ }
396
+ // Only show "All Files" table in verbose mode - it's informational noise
397
+ if (pi.getFlag("lens-verbose")) {
398
+ fullSection += `### All Files\n\n| File | MI | Cognitive | Cyclomatic | Nesting | Entropy |\n|------|-----|-----------|------------|---------|--------|\n`;
399
+ for (const f of results.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex)) {
400
+ fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} | ${f.codeEntropy.toFixed(2)} |\n`;
401
+ }
402
+ fullSection += "\n";
403
+ }
404
+ if (aiSlopIssues.length > 0) {
405
+ fullSection += `### AI Slop Indicators\n\n`;
406
+ for (const issue of aiSlopIssues) {
407
+ fullSection += `${issue}\n`;
408
+ }
409
+ fullSection += "\n";
410
+ }
411
+ fullReport.push(fullSection);
412
+ return { findings, status: "done" };
191
413
  }
192
- if (highCognitive.length > 0) {
193
- summaryItems.push({
194
- category: "High Complexity",
195
- count: highCognitive.length,
414
+ return { findings: 0, status: "done" };
415
+ });
416
+ // Runner 4: Tree-sitter patterns (complementary to ast-grep)
417
+ // - Falls back to tree-sitter if ast-grep unavailable
418
+ // - Detects patterns ast-grep can't easily do (multi-statement, complex nesting)
419
+ // - Captures values for richer reporting
420
+ await tracker.run("tree-sitter patterns", async () => {
421
+ const client = new TreeSitterClient();
422
+ if (!client.isAvailable()) {
423
+ return { findings: 0, status: "skipped" };
424
+ }
425
+ const languageId = isTsProject ? "typescript" : "javascript";
426
+ let findings = 0;
427
+ const structuralIssues = [];
428
+ // Only run basic patterns if ast-grep is NOT available (avoid duplication)
429
+ const astGrepAvailable = clients.astGrep.isAvailable();
430
+ if (!astGrepAvailable) {
431
+ // Fallback: console.log detection (ast-grep normally handles this)
432
+ const consoleLogs = await client.structuralSearch("console.$METHOD($MSG)", languageId, targetPath, { maxResults: 30, fileFilter: shouldIncludeFile });
433
+ for (const match of consoleLogs) {
434
+ const method = match.captures.METHOD || "log";
435
+ if (["log", "debug", "info", "warn"].includes(method)) {
436
+ structuralIssues.push({
437
+ file: match.file,
438
+ line: match.line,
439
+ pattern: `console.${method}()`,
440
+ severity: "๐ŸŸก",
441
+ fixable: true,
442
+ note: astGrepAvailable
443
+ ? undefined
444
+ : "(fallback - ast-grep not available)",
445
+ });
446
+ findings++;
447
+ }
448
+ }
449
+ }
450
+ // Pattern 1: Nested promise chains (ast-grep struggles with multi-statement nesting)
451
+ // This detects: .then().catch().then() chains that could be async/await
452
+ const promiseChains = await client.structuralSearch("$PROMISE.then($$$HANDLER1).catch($$$HANDLER2).then($$$HANDLER3)", languageId, targetPath, { maxResults: 20, fileFilter: shouldIncludeFile });
453
+ for (const match of promiseChains) {
454
+ structuralIssues.push({
455
+ file: match.file,
456
+ line: match.line,
457
+ pattern: "deep promise chain (3+ levels)",
196
458
  severity: "๐ŸŸก",
197
459
  fixable: true,
460
+ note: "Consider converting to async/await for readability",
198
461
  });
462
+ findings++;
199
463
  }
200
- if (aiSlopIssues.length > 0) {
201
- summaryItems.push({
202
- category: "AI Slop",
203
- count: (aiSlopIssues.length / 2) | 0,
464
+ // Pattern 2: Callback pyramids (error-first callbacks nested 3+ levels)
465
+ const callbackPyramids = await client.structuralSearch("$FUNC($$$ARGS, ($ERR, $$$PARAMS) => { $$$BODY })", languageId, targetPath, { maxResults: 20, fileFilter: shouldIncludeFile });
466
+ // Filter for actual callback nesting (error parameter pattern)
467
+ const nestedCallbacks = callbackPyramids.filter((m) => {
468
+ const body = m.captures.BODY || "";
469
+ // Check if body contains another callback
470
+ return body.includes("(") && body.includes("=>");
471
+ });
472
+ for (const match of nestedCallbacks.slice(0, 10)) {
473
+ structuralIssues.push({
474
+ file: match.file,
475
+ line: match.line,
476
+ pattern: "callback pyramid (error-first pattern)",
204
477
  severity: "๐ŸŸก",
205
478
  fixable: true,
206
- }); // Each issue is 2 lines
207
- }
208
- let fullSection = `## Complexity Metrics\n\n**${results.length} file(s) scanned**\n\n`;
209
- fullSection += `### Summary\n\n| Metric | Value |\n|--------|-------|\n| Avg Maintainability Index | ${avgMI.toFixed(1)} |\n| Min Maintainability Index | ${minMI.toFixed(1)} |\n| Avg Cognitive Complexity | ${avgCognitive.toFixed(1)} |\n| Max Cognitive Complexity | ${maxCognitive} |\n| Avg Cyclomatic Complexity | ${avgCyclomatic.toFixed(1)} |\n| Max Nesting Depth | ${maxNesting} |\n| Total Files | ${results.length} |\n\n`;
210
- if (lowMI.length > 0) {
211
- fullSection += `### Low Maintainability (MI < 60)\n\n| File | MI | Cognitive | Cyclomatic | Nesting |\n|------|-----|-----------|------------|--------|\n`;
212
- for (const f of lowMI) {
213
- fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
214
- }
215
- fullSection += "\n";
479
+ note: "Consider promisify + async/await",
480
+ });
481
+ findings++;
216
482
  }
217
- if (highCognitive.length > 0) {
218
- fullSection += `### High Cognitive Complexity (> 20)\n\n| File | Cognitive | MI | Cyclomatic | Nesting |\n|------|-----------|-----|------------|--------|\n`;
219
- for (const f of highCognitive) {
220
- fullSection += `| ${f.filePath} | ${f.cognitiveComplexity} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} |\n`;
483
+ // Pattern 3: Mixed async patterns (async function + .then() + callback)
484
+ // Detects inconsistent async styles in same function
485
+ const asyncFunctions = await client.structuralSearch("async function $NAME($$$PARAMS) { $BODY }", languageId, targetPath, { maxResults: 50, fileFilter: shouldIncludeFile });
486
+ for (const match of asyncFunctions) {
487
+ const body = match.captures.BODY || "";
488
+ // Check if async function uses both await and .then()
489
+ const hasAwait = body.includes("await");
490
+ const hasThen = body.match(/\.\s*then\s*\(/);
491
+ if (hasAwait && hasThen) {
492
+ structuralIssues.push({
493
+ file: match.file,
494
+ line: match.line,
495
+ pattern: "mixed async/await + promise chains",
496
+ severity: "๐ŸŸก",
497
+ fixable: true,
498
+ note: "Use consistent async style (prefer await)",
499
+ });
500
+ findings++;
221
501
  }
222
- fullSection += "\n";
223
502
  }
224
- fullSection += `### All Files\n\n| File | MI | Cognitive | Cyclomatic | Nesting | Entropy |\n|------|-----|-----------|------------|---------|--------|\n`;
225
- for (const f of results.sort((a, b) => a.maintainabilityIndex - b.maintainabilityIndex)) {
226
- fullSection += `| ${f.filePath} | ${f.maintainabilityIndex.toFixed(1)} | ${f.cognitiveComplexity} | ${f.cyclomaticComplexity} | ${f.maxNestingDepth} | ${f.codeEntropy.toFixed(2)} |\n`;
503
+ // Pattern 4: Complex nested if/else (ast-grep can do this, but tree-sitter captures entire block)
504
+ const deepIfs = await client.structuralSearch("if ($COND1) { if ($COND2) { if ($COND3) { $$$BODY } } }", languageId, targetPath, { maxResults: 15, fileFilter: shouldIncludeFile });
505
+ for (const match of deepIfs) {
506
+ structuralIssues.push({
507
+ file: match.file,
508
+ line: match.line,
509
+ pattern: "deeply nested conditionals (3+ levels)",
510
+ severity: "๐ŸŸก",
511
+ fixable: true,
512
+ note: "Consider early returns or guard clauses",
513
+ });
514
+ findings++;
227
515
  }
228
- fullSection += "\n";
229
- if (aiSlopIssues.length > 0) {
230
- fullSection += `### AI Slop Indicators\n\n`;
231
- for (const issue of aiSlopIssues) {
232
- fullSection += `${issue}\n`;
516
+ // Add to summary if issues found
517
+ if (findings > 0) {
518
+ summaryItems.push({
519
+ category: astGrepAvailable
520
+ ? "Advanced Structural"
521
+ : "Structural Patterns (fallback)",
522
+ count: findings,
523
+ severity: "๐ŸŸก",
524
+ fixable: true,
525
+ });
526
+ // Build detailed report
527
+ let fullSection = `## ${astGrepAvailable ? "Advanced Structural" : "Structural Patterns"} (Tree-sitter)\n\n`;
528
+ fullSection += `**${findings} issue(s) found**`;
529
+ if (!astGrepAvailable) {
530
+ fullSection += ` *(ast-grep not available - showing basic + advanced patterns)*`;
233
531
  }
234
- fullSection += "\n";
532
+ fullSection += `\n\n`;
533
+ // Group by pattern type
534
+ const byPattern = {};
535
+ for (const issue of structuralIssues) {
536
+ if (!byPattern[issue.pattern])
537
+ byPattern[issue.pattern] = [];
538
+ byPattern[issue.pattern].push(issue);
539
+ }
540
+ for (const [pattern, issues] of Object.entries(byPattern)) {
541
+ fullSection += `### ${pattern} (${issues.length})\n\n`;
542
+ fullSection += "| File | Line | Note |\n|------|------|------|\n";
543
+ for (const issue of issues.slice(0, 10)) {
544
+ fullSection += `| ${issue.file} | ${issue.line} | ${issue.note || ""} |\n`;
545
+ }
546
+ if (issues.length > 10) {
547
+ fullSection += `| ... | ... | ... |\n`;
548
+ }
549
+ fullSection += "\n";
550
+ }
551
+ fullReport.push(fullSection);
235
552
  }
236
- fullReport.push(fullSection);
237
- }
238
- // Part 4: TODOs
239
- const todoResult = clients.todo.scanDirectory(targetPath);
240
- if (todoResult.items.length > 0) {
241
- summaryItems.push({
242
- category: "TODOs",
243
- count: todoResult.items.length,
244
- severity: "โ„น๏ธ",
245
- fixable: false,
246
- });
247
- let fullSection = `## TODOs / Annotations\n\n`;
553
+ return { findings, status: "done" };
554
+ });
555
+ // Runner 5: TODOs
556
+ await tracker.run("TODO scanner", async () => {
557
+ const todoResult = clients.todo.scanDirectory(targetPath);
248
558
  if (todoResult.items.length > 0) {
249
- fullSection += `**${todoResult.items.length} annotation(s) found**\n\n| Type | File | Line | Text |\n|------|------|------|------|\n`;
559
+ summaryItems.push({
560
+ category: "TODOs",
561
+ count: todoResult.items.length,
562
+ severity: "โ„น๏ธ",
563
+ fixable: false,
564
+ });
565
+ let fullSection = `## TODOs / Annotations\n\n`;
566
+ fullSection += `**${todoResult.items.length} annotation(s) found**\n\n`;
567
+ fullSection +=
568
+ "| Type | File | Line | Text |\n|------|------|------|------|\n";
250
569
  for (const item of todoResult.items) {
251
570
  fullSection += `| ${item.type} | ${item.file} | ${item.line} | ${item.message} |\n`;
252
571
  }
572
+ fullSection += "\n";
573
+ fullReport.push(fullSection);
253
574
  }
254
- else {
255
- fullSection += `No annotations found.\n`;
575
+ return { findings: todoResult.items.length, status: "done" };
576
+ });
577
+ // Runner 6: Dead code
578
+ await tracker.run("dead code (Knip)", async () => {
579
+ if (!clients.knip.isAvailable()) {
580
+ return { findings: 0, status: "skipped" };
256
581
  }
257
- fullSection += "\n";
258
- fullReport.push(fullSection);
259
- }
260
- // Part 5: Dead code
261
- if (clients.knip.isAvailable()) {
262
- const knipResult = clients.knip.analyze(targetPath);
263
- if (knipResult.issues.length > 0) {
582
+ // Exclude test files from Knip analysis
583
+ const knipResult = clients.knip.analyze(targetPath, [
584
+ "**/*.test.ts",
585
+ "**/*.test.tsx",
586
+ "**/*.test.js",
587
+ "**/*.spec.ts",
588
+ "**/*.spec.tsx",
589
+ "**/*.spec.js",
590
+ "**/*.poc.test.ts",
591
+ "**/*.poc.test.tsx",
592
+ "**/__tests__/**",
593
+ "**/tests/**",
594
+ ]);
595
+ // Filter out test file issues as additional safeguard
596
+ const filteredIssues = knipResult.issues.filter((issue) => !issue.file || shouldIncludeFile(issue.file));
597
+ if (filteredIssues.length > 0) {
264
598
  summaryItems.push({
265
599
  category: "Dead Code",
266
- count: knipResult.issues.length,
600
+ count: filteredIssues.length,
267
601
  severity: "๐ŸŸก",
268
602
  fixable: true,
269
603
  });
270
604
  let fullSection = `## Dead Code (Knip)\n\n`;
271
- if (knipResult.issues.length > 0) {
272
- fullSection += `**${knipResult.issues.length} issue(s) found**\n\n| Type | Name | File |\n|------|------|------|\n`;
273
- for (const issue of knipResult.issues) {
274
- fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
275
- }
276
- }
277
- else {
278
- fullSection += `No dead code issues found.\n`;
605
+ fullSection += `**${filteredIssues.length} issue(s) found**\n\n`;
606
+ fullSection += "| Type | Name | File |\n|------|------|------|\n";
607
+ for (const issue of filteredIssues) {
608
+ fullSection += `| ${issue.type} | ${issue.name} | ${issue.file ?? ""} |\n`;
279
609
  }
280
610
  fullSection += "\n";
281
611
  fullReport.push(fullSection);
282
612
  }
283
- }
284
- // Part 6: Duplicate code
285
- if (clients.jscpd.isAvailable()) {
286
- const jscpdResult = clients.jscpd.scan(targetPath);
287
- if (jscpdResult.clones.length > 0) {
613
+ return { findings: filteredIssues.length, status: "done" };
614
+ });
615
+ // Runner 7: Duplicate code
616
+ await tracker.run("duplicate code (jscpd)", async () => {
617
+ if (!clients.jscpd.isAvailable()) {
618
+ return { findings: 0, status: "skipped" };
619
+ }
620
+ // In TS projects, exclude .js files (they're compiled artifacts)
621
+ const jscpdResult = clients.jscpd.scan(targetPath, 5, 50, isTsProject);
622
+ // Filter out test file duplicates using centralized exclusion
623
+ const filteredClones = jscpdResult.clones.filter((dup) => shouldIncludeFile(dup.fileA) && shouldIncludeFile(dup.fileB));
624
+ if (filteredClones.length > 0) {
288
625
  summaryItems.push({
289
626
  category: "Duplicates",
290
- count: jscpdResult.clones.length,
627
+ count: filteredClones.length,
291
628
  severity: "๐ŸŸก",
292
629
  fixable: true,
293
630
  });
294
631
  let fullSection = `## Code Duplication (jscpd)\n\n`;
295
- if (jscpdResult.clones.length > 0) {
296
- fullSection += `**${jscpdResult.clones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n`;
297
- for (const dup of jscpdResult.clones) {
298
- fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
299
- }
300
- }
301
- else {
302
- fullSection += `No duplicate code found.\n`;
632
+ fullSection += `**${filteredClones.length} duplicate block(s) found** (${jscpdResult.duplicatedLines}/${jscpdResult.totalLines} lines, ${jscpdResult.percentage.toFixed(1)}%)\n\n`;
633
+ fullSection +=
634
+ "| File A | Line A | File B | Line B | Lines | Tokens |\n|--------|--------|--------|--------|-------|--------|\n";
635
+ for (const dup of filteredClones) {
636
+ fullSection += `| ${dup.fileA} | ${dup.startA} | ${dup.fileB} | ${dup.startB} | ${dup.lines} | ${dup.tokens} |\n`;
303
637
  }
304
638
  fullSection += "\n";
305
639
  fullReport.push(fullSection);
306
640
  }
307
- }
308
- // Part 7: Type coverage
309
- if (clients.typeCoverage.isAvailable()) {
641
+ return { findings: filteredClones.length, status: "done" };
642
+ });
643
+ // Runner 8: Type coverage
644
+ await tracker.run("type coverage", async () => {
645
+ if (!clients.typeCoverage.isAvailable()) {
646
+ return { findings: 0, status: "skipped" };
647
+ }
310
648
  const tcResult = clients.typeCoverage.scan(targetPath);
311
649
  if (tcResult.percentage < 100) {
312
- const untyped = tcResult.total - tcResult.typed;
650
+ // Filter out test file locations using centralized exclusion
651
+ const filteredLocations = tcResult.untypedLocations.filter((u) => shouldIncludeFile(u.file));
652
+ const filesWithLowCoverage = new Set(filteredLocations
653
+ .filter(() => tcResult.percentage < 90)
654
+ .map((u) => u.file)).size;
313
655
  summaryItems.push({
314
- category: "Untyped",
315
- count: untyped,
656
+ category: "Type Coverage",
657
+ count: filesWithLowCoverage || 1,
316
658
  severity: tcResult.percentage < 90 ? "๐ŸŸก" : "โ„น๏ธ",
317
659
  fixable: false,
318
660
  });
319
661
  let fullSection = `## Type Coverage\n\n**${tcResult.percentage.toFixed(1)}% typed** (${tcResult.typed}/${tcResult.total} identifiers)\n\n`;
320
- if (tcResult.untypedLocations.length > 0) {
321
- fullSection += `### Untyped Identifiers\n\n| File | Line | Column | Name |\n|------|------|--------|------|\n`;
322
- for (const u of tcResult.untypedLocations) {
323
- fullSection += `| ${u.file} | ${u.line} | ${u.column} | ${u.name} |\n`;
662
+ const byFile = {};
663
+ for (const u of filteredLocations) {
664
+ byFile[u.file] = (byFile[u.file] || 0) + 1;
665
+ }
666
+ const sortedFiles = Object.entries(byFile)
667
+ .filter(([file]) => shouldIncludeFile(file))
668
+ .sort((a, b) => b[1] - a[1])
669
+ .slice(0, 10);
670
+ if (sortedFiles.length > 0) {
671
+ fullSection += `### Top Files by Untyped Count\n\n| File | Untyped Count |\n|------|---------------|\n`;
672
+ for (const [file, count] of sortedFiles) {
673
+ fullSection += `| ${file} | ${count} |\n`;
674
+ }
675
+ if (Object.keys(byFile).length > 10) {
676
+ fullSection += `| ... | +${Object.keys(byFile).length - 10} more files |\n`;
324
677
  }
325
678
  }
326
679
  fullSection += "\n";
327
680
  fullReport.push(fullSection);
681
+ return { findings: filesWithLowCoverage || 1, status: "done" };
682
+ }
683
+ return { findings: 0, status: "done" };
684
+ });
685
+ // Runner 9: Circular deps
686
+ await tracker.run("circular deps (Madge)", async () => {
687
+ if (pi.getFlag("no-madge") || !clients.depChecker.isAvailable()) {
688
+ return { findings: 0, status: "skipped" };
328
689
  }
329
- }
330
- // Part 8: Circular deps
331
- if (!pi.getFlag("no-madge") && clients.depChecker.isAvailable()) {
332
690
  const { circular } = clients.depChecker.scanProject(targetPath);
333
- if (circular.length > 0) {
691
+ // Filter out circular deps involving only test files using centralized exclusion
692
+ const filteredCircular = circular.filter((dep) => {
693
+ // Keep if ANY file in the chain is not a test file
694
+ return dep.path.some((file) => shouldIncludeFile(file));
695
+ });
696
+ if (filteredCircular.length > 0) {
334
697
  summaryItems.push({
335
698
  category: "Circular Deps",
336
- count: circular.length,
699
+ count: filteredCircular.length,
337
700
  severity: "๐Ÿ”ด",
338
701
  fixable: false,
339
702
  });
340
- let fullSection = `## Circular Dependencies (Madge)\n\n**${circular.length} circular chain(s) found**\n\n`;
341
- for (const dep of circular) {
703
+ let fullSection = `## Circular Dependencies (Madge)\n\n`;
704
+ fullSection += `**${filteredCircular.length} circular chain(s) found**\n\n`;
705
+ for (const dep of filteredCircular) {
342
706
  fullSection += `- ${dep.path.join(" โ†’ ")}\n`;
343
707
  }
344
708
  fullReport.push(`${fullSection}\n`);
345
709
  }
346
- }
347
- // Part 9: Arch rules
348
- if (!clients.architect.hasConfig()) {
349
- clients.architect.loadConfig(process.cwd());
350
- }
351
- if (clients.architect.hasConfig()) {
710
+ return { findings: filteredCircular.length, status: "done" };
711
+ });
712
+ // Runner 10: Arch rules
713
+ await tracker.run("architectural rules", async () => {
714
+ if (!clients.architect.hasConfig()) {
715
+ clients.architect.loadConfig(process.cwd());
716
+ }
717
+ if (!clients.architect.hasConfig()) {
718
+ return { findings: 0, status: "skipped" };
719
+ }
720
+ // Detect TypeScript project - skip .js files in TS projects (compiled artifacts)
721
+ const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
352
722
  const archViolations = [];
353
723
  const archScanDir = (dir) => {
354
724
  for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
355
725
  const full = path.join(dir, entry.name);
356
726
  if (entry.isDirectory()) {
357
- if ([
358
- "node_modules",
359
- ".git",
360
- "dist",
361
- "build",
362
- ".next",
363
- ".pi-lens",
364
- ].includes(entry.name))
727
+ if (EXCLUDED_DIRS.includes(entry.name))
365
728
  continue;
366
729
  archScanDir(full);
367
730
  }
368
731
  else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
732
+ if (isTestFile(full))
733
+ continue;
734
+ // In TS projects, skip .js files (they're compiled artifacts)
735
+ if (isTsProject &&
736
+ /\.(js|jsx)$/.test(entry.name) &&
737
+ nodeFs.existsSync(full.replace(/\.(js|jsx)$/, ".ts")))
738
+ continue;
369
739
  const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
370
740
  const content = nodeFs.readFileSync(full, "utf-8");
371
741
  const lineCount = content.split("\n").length;
@@ -386,44 +756,254 @@ export async function handleBooboo(args, ctx, clients, pi) {
386
756
  severity: "๐Ÿ”ด",
387
757
  fixable: false,
388
758
  });
389
- let fullSection = `## Architectural Rules\n\n**${archViolations.length} violation(s) found**\n\n`;
759
+ let fullSection = `## Architectural Rules\n\n`;
760
+ fullSection += `**${archViolations.length} violation(s) found**\n\n`;
390
761
  for (const v of archViolations) {
391
762
  fullSection += `- **${v.file}**: ${v.message}\n`;
392
763
  }
393
764
  fullReport.push(`${fullSection}\n`);
394
765
  }
395
- }
766
+ return { findings: archViolations.length, status: "done" };
767
+ });
768
+ // Runner 11: Production Readiness (inspired by pi-validate)
769
+ await tracker.run("production readiness", async () => {
770
+ const readiness = validateProductionReadiness(targetPath);
771
+ // Add to summary if not perfect
772
+ if (readiness.overallScore < 100) {
773
+ const severity = readiness.grade === "A"
774
+ ? "๐ŸŸข"
775
+ : readiness.grade === "B"
776
+ ? "๐ŸŸข"
777
+ : readiness.grade === "C"
778
+ ? "๐ŸŸก"
779
+ : "๐ŸŸ ";
780
+ // Count issues across all categories
781
+ const totalIssues_ = Object.values(readiness.categories).reduce((sum, cat) => sum + cat.issues.length, 0);
782
+ if (totalIssues_ > 0) {
783
+ summaryItems.push({
784
+ category: "Production Readiness",
785
+ count: totalIssues_,
786
+ severity: severity,
787
+ fixable: true,
788
+ });
789
+ }
790
+ }
791
+ // Add to full report
792
+ let section = `## Production Readiness\n\n`;
793
+ section += `**Score:** ${readiness.overallScore}/100 **Grade:** ${readiness.grade}\n\n`;
794
+ for (const [key, cat] of Object.entries(readiness.categories)) {
795
+ section += `### ${key.charAt(0).toUpperCase() + key.slice(1)} (${cat.score}/100)\n\n`;
796
+ if (cat.details.length > 0) {
797
+ for (const detail of cat.details) {
798
+ section += `- ${detail}\n`;
799
+ }
800
+ }
801
+ if (cat.issues.length > 0) {
802
+ for (const issue of cat.issues) {
803
+ section += `- โš ๏ธ ${issue}\n`;
804
+ }
805
+ }
806
+ if (cat.details.length === 0 && cat.issues.length === 0) {
807
+ section += `- โœ… No issues\n`;
808
+ }
809
+ section += "\n";
810
+ }
811
+ fullReport.push(section);
812
+ // Add metadata to report
813
+ const criticalIssues = [];
814
+ for (const [key, cat] of Object.entries(readiness.categories)) {
815
+ for (const issue of cat.issues) {
816
+ // Flag critical issues
817
+ if (key === "code" && issue.includes("debugger")) {
818
+ criticalIssues.push(`[CRITICAL] ${issue}`);
819
+ }
820
+ else if (key === "tests" && cat.score < 50) {
821
+ criticalIssues.push(`[CRITICAL] No tests found`);
822
+ }
823
+ }
824
+ }
825
+ return {
826
+ findings: Object.values(readiness.categories).reduce((sum, cat) => sum + cat.issues.length, 0),
827
+ status: "done",
828
+ };
829
+ });
830
+ // --- Create structured JSON report ---
396
831
  nodeFs.mkdirSync(reviewDir, { recursive: true });
397
832
  const projectName = path.basename(process.cwd());
398
- const mdReport = `# Code Review: ${projectName}\n\n**Scanned:** ${new Date().toISOString()}\n\n**Path:** \`${targetPath}\`\n\n---\n\n${fullReport.join("\n")}`;
399
- const reportPath = path.join(reviewDir, `booboo-${timestamp}.md`);
400
- nodeFs.writeFileSync(reportPath, mdReport, "utf-8");
401
- // Build summary table for terminal
833
+ const totalIssues = summaryItems.reduce((sum, s) => sum + s.count, 0);
834
+ const fixableCount = summaryItems
835
+ .filter((s) => s.fixable)
836
+ .reduce((sum, s) => sum + s.count, 0);
837
+ const refactorNeeded = summaryItems
838
+ .filter((s) => !s.fixable)
839
+ .reduce((sum, s) => sum + s.count, 0);
840
+ // Build runner summary
841
+ const runnerSummary = tracker.getRunners().map((r) => ({
842
+ name: r.name,
843
+ status: r.status,
844
+ findings: r.findings,
845
+ time: formatElapsed(r.elapsedMs),
846
+ }));
847
+ const jsonReport = {
848
+ meta: {
849
+ timestamp: new Date().toISOString(),
850
+ project: projectName,
851
+ path: targetPath,
852
+ totalIssues,
853
+ fixableCount,
854
+ refactorNeeded,
855
+ // New: runner execution details
856
+ runners: runnerSummary,
857
+ totalTime: formatElapsed(runnerSummary.reduce((sum, r) => {
858
+ const ms = r.time.endsWith("ms")
859
+ ? parseInt(r.time, 10)
860
+ : parseFloat(r.time) * 1000;
861
+ return sum + (Number.isNaN(ms) ? 0 : ms);
862
+ }, 0)),
863
+ },
864
+ // New: project metadata
865
+ project: {
866
+ type: projectMeta.type,
867
+ name: projectMeta.name,
868
+ version: projectMeta.version,
869
+ packageManager: projectMeta.packageManager,
870
+ languages: projectMeta.languages,
871
+ hasTests: projectMeta.hasTests,
872
+ testFramework: projectMeta.testFramework,
873
+ hasLinting: projectMeta.hasLinting,
874
+ linter: projectMeta.linter,
875
+ hasFormatting: projectMeta.hasFormatting,
876
+ formatter: projectMeta.formatter,
877
+ hasTypeScript: projectMeta.hasTypeScript,
878
+ configFiles: projectMeta.configFiles,
879
+ scripts: projectMeta.scripts,
880
+ },
881
+ // New: available commands for the project
882
+ commands: availableCommands,
883
+ byCategory: summaryItems.reduce((acc, item) => {
884
+ acc[item.category] = {
885
+ count: item.count,
886
+ severity: item.severity,
887
+ fixable: item.fixable,
888
+ falsePositivePrefix: `${item.category.toLowerCase().replace(/\s+/g, "-")}:`,
889
+ };
890
+ return acc;
891
+ }, {}),
892
+ howToMarkFalsePositive: {
893
+ command: "Ignore via AGENTS.md rules or suppress comments",
894
+ format: "Add to .claude/rules or use biome/oxlint ignore comments",
895
+ examples: [
896
+ "// biome-ignore lint/suspicious/noConsole: intentional debug",
897
+ "// oxlint-disable-next-line no-console",
898
+ ],
899
+ },
900
+ sessionFile: path.join(process.cwd(), ".pi-lens", "fix-session.json"),
901
+ details: fullReport.join("\n"),
902
+ };
903
+ const jsonPath = path.join(reviewDir, `booboo-${timestamp}.json`);
904
+ nodeFs.writeFileSync(jsonPath, JSON.stringify(jsonReport, null, 2), "utf-8");
905
+ // --- Create markdown report ---
906
+ // Build project info section
907
+ let projectSection = `## Project Info\n\n**Type:** ${projectMeta.type}`;
908
+ if (projectMeta.name)
909
+ projectSection += ` | **Name:** ${projectMeta.name}`;
910
+ if (projectMeta.version)
911
+ projectSection += ` | **Version:** ${projectMeta.version}`;
912
+ if (projectMeta.packageManager)
913
+ projectSection += `\n**Package Manager:** ${projectMeta.packageManager}`;
914
+ if (projectMeta.languages.length > 0)
915
+ projectSection += `\n**Languages:** ${projectMeta.languages.join(", ")}`;
916
+ // Tools
917
+ const tools = [];
918
+ if (projectMeta.testFramework)
919
+ tools.push(`๐Ÿงช ${projectMeta.testFramework}`);
920
+ else if (projectMeta.hasTests)
921
+ tools.push("๐Ÿงช tests");
922
+ if (projectMeta.linter)
923
+ tools.push(`๐Ÿ” ${projectMeta.linter}`);
924
+ if (projectMeta.formatter)
925
+ tools.push(`โœจ ${projectMeta.formatter}`);
926
+ if (tools.length > 0)
927
+ projectSection += `\n**Tools:** ${tools.join(" | ")}`;
928
+ // Available commands
929
+ if (availableCommands.length > 0) {
930
+ projectSection += `\n\n### Available Commands\n\n| Action | Command |\n|--------|---------|`;
931
+ for (const cmd of availableCommands) {
932
+ projectSection += `\n| ${cmd.action} | \`${cmd.command}\` |`;
933
+ }
934
+ }
935
+ const mdReport = `# Code Review: ${projectName}
936
+
937
+ **Scanned:** ${jsonReport.meta.timestamp}
938
+ **Path:** \`${targetPath}\`
939
+ **Summary:** ${jsonReport.meta.totalIssues} issues | ${jsonReport.meta.fixableCount} fixable | ${jsonReport.meta.refactorNeeded} need refactor
940
+ **Total Time:** ${jsonReport.meta.totalTime}
941
+
942
+ ${projectSection}
943
+
944
+ ## Runner Summary
945
+
946
+ | Runner | Status | Findings | Time |
947
+ |--------|--------|----------|------|
948
+ ${runnerSummary.map((r) => `| ${r.name} | ${r.status} | ${r.findings} | ${r.time} |`).join("\n")}
949
+
950
+ ---
951
+
952
+ ${fullReport.join("\n")}`;
953
+ const mdPath = path.join(reviewDir, `booboo-${timestamp}.md`);
954
+ nodeFs.writeFileSync(mdPath, mdReport, "utf-8");
955
+ // --- Brief terminal summary ---
402
956
  if (summaryItems.length === 0) {
403
- ctx.ui.notify("โœ“ Code review clean โ€” saved to .pi-lens/reviews/", "info");
957
+ ctx.ui.notify("โœ“ Code review clean", "info");
404
958
  }
405
959
  else {
406
- const totalIssues = summaryItems.reduce((sum, s) => sum + s.count, 0);
407
- const fixableCount = summaryItems
408
- .filter((s) => s.fixable)
409
- .reduce((sum, s) => sum + s.count, 0);
410
- const refactorNeeded = totalIssues - fixableCount;
411
- let summary = `๐Ÿ“Š Code Review: ${totalIssues} issues found\n`;
412
- summary += `โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n`;
413
- for (const item of summaryItems) {
414
- summary += `${item.severity} ${item.category}: ${item.count}${item.fixable ? " (fixable)" : ""}\n`;
415
- }
416
- summary += `โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”\n`;
417
- if (fixableCount > 0 && refactorNeeded > 0) {
418
- summary += `๐Ÿ”ง ${fixableCount} fixable via /lens-booboo-fix | ๐Ÿ—๏ธ ${refactorNeeded} need /lens-booboo-refactor\n`;
419
- }
420
- else if (fixableCount > 0) {
421
- summary += `๐Ÿ”ง All ${fixableCount} issues fixable via /lens-booboo-fix\n`;
422
- }
423
- else {
424
- summary += `๐Ÿ—๏ธ All issues need /lens-booboo-refactor\n`;
425
- }
426
- summary += `๐Ÿ“„ Full report: ${reportPath}`;
427
- ctx.ui.notify(summary, "info");
960
+ const { totalIssues, fixableCount, refactorNeeded } = jsonReport.meta;
961
+ // Build runner lines for terminal output
962
+ const runnerLines = tracker
963
+ .getRunners()
964
+ .filter((r) => r.findings > 0)
965
+ .map((r) => ` ${r.status === "error" ? "โœ—" : "โš "} ${r.name}: ${r.findings} finding${r.findings !== 1 ? "s" : ""} (${formatElapsed(r.elapsedMs)})`);
966
+ const summaryLines = [
967
+ `๐Ÿ“Š Code Review: ${totalIssues} issues`,
968
+ ...runnerLines,
969
+ ` ๐Ÿ”ง ${fixableCount} fixable | ๐Ÿ—๏ธ ${refactorNeeded} refactor`,
970
+ ` โฑ๏ธ Total: ${jsonReport.meta.totalTime}`,
971
+ `๐Ÿ“„ JSON: ${jsonPath}`,
972
+ `๐Ÿ“„ MD: ${mdPath}`,
973
+ ];
974
+ ctx.ui.notify(summaryLines.join("\n"), "info");
975
+ }
976
+ }
977
+ /**
978
+ * Find top N most similar function pairs in the project index
979
+ * Uses canonical pair ordering to avoid duplicates (A,B) vs (B,A)
980
+ */
981
+ function findTopSimilarPairs(index, maxPairs) {
982
+ const entries = Array.from(index.entries.values());
983
+ const seenPairs = new Set();
984
+ const pairs = [];
985
+ for (let i = 0; i < entries.length; i++) {
986
+ for (let j = i + 1; j < entries.length; j++) {
987
+ const entry1 = entries[i];
988
+ const entry2 = entries[j];
989
+ // Skip if same file (we want cross-file duplicates)
990
+ if (entry1.filePath === entry2.filePath)
991
+ continue;
992
+ const similarity = calculateSimilarity(entry1.matrix, entry2.matrix);
993
+ if (similarity >= 0.75) {
994
+ // Canonical pair key (sorted to avoid duplicates)
995
+ const pairKey = [entry1.id, entry2.id].sort().join("::");
996
+ if (seenPairs.has(pairKey))
997
+ continue;
998
+ seenPairs.add(pairKey);
999
+ pairs.push({
1000
+ func1: entry1.id,
1001
+ func2: entry2.id,
1002
+ similarity,
1003
+ });
1004
+ }
1005
+ }
428
1006
  }
1007
+ // Sort by similarity descending, take top N
1008
+ return pairs.sort((a, b) => b.similarity - a.similarity).slice(0, maxPairs);
429
1009
  }