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