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
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Shared runner utilities for pi-lens dispatch system
3
+ *
4
+ * Extracted common patterns from multiple runners to reduce duplication:
5
+ * - Venv-aware command finders
6
+ * - Availability checkers with caching
7
+ * - Config file finders
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { safeSpawn } from "../../../safe-spawn.js";
12
+ /**
13
+ * Find a command in venv first, then fall back to global.
14
+ * Checks common venv locations (.venv, venv) before trying global.
15
+ */
16
+ export function createVenvFinder(command, windowsExt = "", quoteWindows = false) {
17
+ return (cwd) => {
18
+ const venvPaths = [
19
+ `.venv/bin/${command}`,
20
+ `venv/bin/${command}`,
21
+ `.venv/Scripts/${command}${windowsExt}`,
22
+ `venv/Scripts/${command}${windowsExt}`,
23
+ ];
24
+ for (const venvPath of venvPaths) {
25
+ const fullPath = path.join(cwd, venvPath);
26
+ if (fs.existsSync(fullPath)) {
27
+ return quoteWindows && windowsExt
28
+ ? `"${fullPath}"`
29
+ : fullPath;
30
+ }
31
+ }
32
+ // Fall back to global
33
+ return command;
34
+ };
35
+ }
36
+ /**
37
+ * Create a cached availability checker for a command.
38
+ * The checker will look for the command in venv first, then global.
39
+ */
40
+ export function createAvailabilityChecker(command, windowsExt = "") {
41
+ const cache = {
42
+ available: null,
43
+ command: null,
44
+ };
45
+ const findCommand = createVenvFinder(command, windowsExt, true);
46
+ function isAvailable(cwd) {
47
+ if (cache.available !== null)
48
+ return cache.available;
49
+ const cmd = findCommand(cwd || process.cwd());
50
+ const result = safeSpawn(cmd, ["--version"], {
51
+ timeout: 5000,
52
+ });
53
+ cache.available = !result.error && result.status === 0;
54
+ if (cache.available) {
55
+ cache.command = cmd;
56
+ }
57
+ return cache.available;
58
+ }
59
+ function getCommand() {
60
+ return cache.command;
61
+ }
62
+ return { isAvailable, getCommand };
63
+ }
64
+ // =============================================================================
65
+ // CONFIG FILE FINDER FACTORY
66
+ // =============================================================================
67
+ /**
68
+ * Create a config file finder for rule directories.
69
+ * Common pattern used by slop runners and similar tools.
70
+ */
71
+ export function createConfigFinder(ruleDirName) {
72
+ return (cwd) => {
73
+ // Check for local config first
74
+ const localPath = path.join(cwd, "rules", ruleDirName, ".sgconfig.yml");
75
+ if (fs.existsSync(localPath)) {
76
+ return localPath;
77
+ }
78
+ // Fall back to extension rules
79
+ const extensionPaths = [
80
+ `rules/${ruleDirName}/.sgconfig.yml`,
81
+ `../rules/${ruleDirName}/.sgconfig.yml`,
82
+ ];
83
+ for (const candidate of extensionPaths) {
84
+ const fullPath = path.resolve(cwd, candidate);
85
+ if (fs.existsSync(fullPath)) {
86
+ return fullPath;
87
+ }
88
+ }
89
+ return undefined;
90
+ };
91
+ }
92
+ // =============================================================================
93
+ // SHARED AST-GREP AVAILABILITY
94
+ // =============================================================================
95
+ // Shared sg availability cache across all slop runners
96
+ let sgAvailable = null;
97
+ /**
98
+ * Check if ast-grep CLI is available (shared cache)
99
+ */
100
+ export function isSgAvailable() {
101
+ if (sgAvailable !== null)
102
+ return sgAvailable;
103
+ const check = safeSpawn("npx", ["sg", "--version"], {
104
+ timeout: 5000,
105
+ });
106
+ sgAvailable = !check.error && check.status === 0;
107
+ return sgAvailable;
108
+ }
109
+ // =============================================================================
110
+ // PRE-BUILT CHECKERS FOR COMMON TOOLS
111
+ // =============================================================================
112
+ export const pyright = createAvailabilityChecker("pyright", ".exe");
113
+ export const ruff = createAvailabilityChecker("ruff", ".exe");
114
+ export const biome = createAvailabilityChecker("biome");
115
+ export const sg = { isAvailable: isSgAvailable, getCommand: () => "npx" };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Shared runner utilities for pi-lens dispatch system
3
+ *
4
+ * Extracted common patterns from multiple runners to reduce duplication:
5
+ * - Venv-aware command finders
6
+ * - Availability checkers with caching
7
+ * - Config file finders
8
+ */
9
+
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import { safeSpawn } from "../../../safe-spawn.js";
13
+
14
+ // =============================================================================
15
+ // VENV-AWARE COMMAND FINDER
16
+ // =============================================================================
17
+
18
+ export interface VenvPathConfig {
19
+ unixPaths: string[];
20
+ windowsPaths: string[];
21
+ quoteWindowsPaths?: boolean;
22
+ }
23
+
24
+ /**
25
+ * Find a command in venv first, then fall back to global.
26
+ * Checks common venv locations (.venv, venv) before trying global.
27
+ */
28
+ export function createVenvFinder(
29
+ command: string,
30
+ windowsExt = "",
31
+ quoteWindows = false,
32
+ ): (cwd: string) => string {
33
+ return (cwd: string): string => {
34
+ const venvPaths = [
35
+ `.venv/bin/${command}`,
36
+ `venv/bin/${command}`,
37
+ `.venv/Scripts/${command}${windowsExt}`,
38
+ `venv/Scripts/${command}${windowsExt}`,
39
+ ];
40
+
41
+ for (const venvPath of venvPaths) {
42
+ const fullPath = path.join(cwd, venvPath);
43
+ if (fs.existsSync(fullPath)) {
44
+ return quoteWindows && windowsExt
45
+ ? `"${fullPath}"`
46
+ : fullPath;
47
+ }
48
+ }
49
+
50
+ // Fall back to global
51
+ return command;
52
+ };
53
+ }
54
+
55
+ // =============================================================================
56
+ // AVAILABILITY CHECKER FACTORY
57
+ // =============================================================================
58
+
59
+ type AvailabilityCache = {
60
+ available: boolean | null;
61
+ command: string | null;
62
+ };
63
+
64
+ /**
65
+ * Create a cached availability checker for a command.
66
+ * The checker will look for the command in venv first, then global.
67
+ */
68
+ export function createAvailabilityChecker(
69
+ command: string,
70
+ windowsExt = "",
71
+ ): {
72
+ isAvailable: (cwd?: string) => boolean;
73
+ getCommand: () => string | null;
74
+ } {
75
+ const cache: AvailabilityCache = {
76
+ available: null,
77
+ command: null,
78
+ };
79
+
80
+ const findCommand = createVenvFinder(command, windowsExt, true);
81
+
82
+ function isAvailable(cwd?: string): boolean {
83
+ if (cache.available !== null) return cache.available;
84
+
85
+ const cmd = findCommand(cwd || process.cwd());
86
+ const result = safeSpawn(cmd, ["--version"], {
87
+ timeout: 5000,
88
+ });
89
+
90
+ cache.available = !result.error && result.status === 0;
91
+ if (cache.available) {
92
+ cache.command = cmd;
93
+ }
94
+ return cache.available;
95
+ }
96
+
97
+ function getCommand(): string | null {
98
+ return cache.command;
99
+ }
100
+
101
+ return { isAvailable, getCommand };
102
+ }
103
+
104
+ // =============================================================================
105
+ // CONFIG FILE FINDER FACTORY
106
+ // =============================================================================
107
+
108
+ /**
109
+ * Create a config file finder for rule directories.
110
+ * Common pattern used by slop runners and similar tools.
111
+ */
112
+ export function createConfigFinder(
113
+ ruleDirName: string,
114
+ ): (cwd: string) => string | undefined {
115
+ return (cwd: string): string | undefined => {
116
+ // Check for local config first
117
+ const localPath = path.join(cwd, "rules", ruleDirName, ".sgconfig.yml");
118
+ if (fs.existsSync(localPath)) {
119
+ return localPath;
120
+ }
121
+
122
+ // Fall back to extension rules
123
+ const extensionPaths = [
124
+ `rules/${ruleDirName}/.sgconfig.yml`,
125
+ `../rules/${ruleDirName}/.sgconfig.yml`,
126
+ ];
127
+
128
+ for (const candidate of extensionPaths) {
129
+ const fullPath = path.resolve(cwd, candidate);
130
+ if (fs.existsSync(fullPath)) {
131
+ return fullPath;
132
+ }
133
+ }
134
+
135
+ return undefined;
136
+ };
137
+ }
138
+
139
+ // =============================================================================
140
+ // SHARED AST-GREP AVAILABILITY
141
+ // =============================================================================
142
+
143
+ // Shared sg availability cache across all slop runners
144
+ let sgAvailable: boolean | null = null;
145
+
146
+ /**
147
+ * Check if ast-grep CLI is available (shared cache)
148
+ */
149
+ export function isSgAvailable(): boolean {
150
+ if (sgAvailable !== null) return sgAvailable;
151
+
152
+ const check = safeSpawn("npx", ["sg", "--version"], {
153
+ timeout: 5000,
154
+ });
155
+
156
+ sgAvailable = !check.error && check.status === 0;
157
+ return sgAvailable;
158
+ }
159
+
160
+ // =============================================================================
161
+ // PRE-BUILT CHECKERS FOR COMMON TOOLS
162
+ // =============================================================================
163
+
164
+ export const pyright = createAvailabilityChecker("pyright", ".exe");
165
+ export const ruff = createAvailabilityChecker("ruff", ".exe");
166
+ export const biome = createAvailabilityChecker("biome");
167
+ export const sg = { isAvailable: isSgAvailable, getCommand: () => "npx" };
@@ -2,6 +2,7 @@
2
2
  * Shared utilities for runners
3
3
  */
4
4
  import * as fs from "node:fs";
5
+ import { safeSpawn } from "../../safe-spawn.js";
5
6
  /**
6
7
  * Read file content, returning undefined if it can't be read
7
8
  */
@@ -18,11 +19,8 @@ export function readFileContent(filePath) {
18
19
  */
19
20
  export function isCommandAvailable(command) {
20
21
  try {
21
- const { spawnSync } = require("node:child_process");
22
- const result = spawnSync(command, ["--version"], {
23
- encoding: "utf-8",
22
+ const result = safeSpawn(command, ["--version"], {
24
23
  timeout: 5000,
25
- shell: true,
26
24
  });
27
25
  return result.status === 0;
28
26
  }
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import * as fs from "node:fs";
6
+ import { safeSpawn } from "../../safe-spawn.js";
6
7
  import type { Diagnostic } from "../types.js";
7
8
 
8
9
  /**
@@ -21,11 +22,8 @@ export function readFileContent(filePath: string): string | undefined {
21
22
  */
22
23
  export function isCommandAvailable(command: string): boolean {
23
24
  try {
24
- const { spawnSync } = require("node:child_process");
25
- const result = spawnSync(command, ["--version"], {
26
- encoding: "utf-8",
25
+ const result = safeSpawn(command, ["--version"], {
27
26
  timeout: 5000,
28
- shell: true,
29
27
  });
30
28
  return result.status === 0;
31
29
  } catch {
@@ -48,7 +48,7 @@ export interface Diagnostic {
48
48
  /** Column (1-based) */
49
49
  column?: number;
50
50
  /** Severity level */
51
- severity: "error" | "warning" | "info";
51
+ severity: "error" | "warning" | "info" | "hint";
52
52
  /** Output semantic */
53
53
  semantic: OutputSemantic;
54
54
  /** Which tool produced this */
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared formatting utilities for dispatch system
3
+ *
4
+ * Consolidated from:
5
+ * - clients/dispatch/dispatcher.ts
6
+ * - clients/dispatch/bus-dispatcher.ts
7
+ * - clients/services/effect-integration.ts
8
+ */
9
+ export const EMOJI = {
10
+ blocking: "🔴",
11
+ warning: "🟡",
12
+ fixed: "✅",
13
+ info: "ℹ️",
14
+ silent: "📊",
15
+ none: "",
16
+ };
17
+ /**
18
+ * Format a single diagnostic for display
19
+ */
20
+ export function formatDiagnostic(d) {
21
+ const line = d.line ? `L${d.line}: ` : "";
22
+ const indented = d.message.split("\n").join("\n ");
23
+ return ` ${line}${indented}`;
24
+ }
25
+ /**
26
+ * Format a group of diagnostics with semantic header
27
+ */
28
+ export function formatDiagnostics(diagnostics, semantic, maxDisplay = 10) {
29
+ if (diagnostics.length === 0)
30
+ return "";
31
+ const emoji = EMOJI[semantic] ?? EMOJI.warning;
32
+ let output = "";
33
+ if (semantic === "blocking") {
34
+ output += `\n${emoji} STOP — ${diagnostics.length} issue(s) must be fixed:\n`;
35
+ }
36
+ else if (semantic === "warning") {
37
+ output += `\n${emoji} ${diagnostics.length} warning(s):\n`;
38
+ }
39
+ else if (semantic === "fixed") {
40
+ output += `\n${emoji} Auto-fixed ${diagnostics.length} issue(s):\n`;
41
+ }
42
+ for (const d of diagnostics.slice(0, maxDisplay)) {
43
+ output += `${formatDiagnostic(d)}\n`;
44
+ }
45
+ if (diagnostics.length > maxDisplay) {
46
+ output += ` ... and ${diagnostics.length - maxDisplay} more\n`;
47
+ }
48
+ return output;
49
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Shared formatting utilities for dispatch system
3
+ *
4
+ * Consolidated from:
5
+ * - clients/dispatch/dispatcher.ts
6
+ * - clients/dispatch/bus-dispatcher.ts
7
+ * - clients/services/effect-integration.ts
8
+ */
9
+
10
+ import type { Diagnostic, OutputSemantic } from "../types.js";
11
+
12
+ export const EMOJI: Record<string, string> = {
13
+ blocking: "🔴",
14
+ warning: "🟡",
15
+ fixed: "✅",
16
+ info: "ℹ️",
17
+ silent: "📊",
18
+ none: "",
19
+ };
20
+
21
+ /**
22
+ * Format a single diagnostic for display
23
+ */
24
+ export function formatDiagnostic(d: Diagnostic): string {
25
+ const line = d.line ? `L${d.line}: ` : "";
26
+ const indented = d.message.split("\n").join("\n ");
27
+ return ` ${line}${indented}`;
28
+ }
29
+
30
+ /**
31
+ * Format a group of diagnostics with semantic header
32
+ */
33
+ export function formatDiagnostics(
34
+ diagnostics: Diagnostic[],
35
+ semantic: OutputSemantic | string,
36
+ maxDisplay = 10,
37
+ ): string {
38
+ if (diagnostics.length === 0) return "";
39
+
40
+ const emoji = EMOJI[semantic] ?? EMOJI.warning;
41
+ let output = "";
42
+
43
+ if (semantic === "blocking") {
44
+ output += `\n${emoji} STOP — ${diagnostics.length} issue(s) must be fixed:\n`;
45
+ } else if (semantic === "warning") {
46
+ output += `\n${emoji} ${diagnostics.length} warning(s):\n`;
47
+ } else if (semantic === "fixed") {
48
+ output += `\n${emoji} Auto-fixed ${diagnostics.length} issue(s):\n`;
49
+ }
50
+
51
+ for (const d of diagnostics.slice(0, maxDisplay)) {
52
+ output += `${formatDiagnostic(d)}\n`;
53
+ }
54
+
55
+ if (diagnostics.length > maxDisplay) {
56
+ output += ` ... and ${diagnostics.length - maxDisplay} more\n`;
57
+ }
58
+
59
+ return output;
60
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Meta-test: Run similarity detection on pi-lens codebase
3
+ *
4
+ * This is a "dogfood" test - we run the reuse detection on our own code
5
+ * to see what it finds. Educational and useful for improving the algorithm!
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { glob } from "glob";
11
+ import { beforeAll, describe, expect, it } from "vitest";
12
+ import { buildProjectIndex, findSimilarFunctions, } from "./project-index.js";
13
+ import { calculateSimilarity as calcMatrixSimilarity } from "./state-matrix.js";
14
+ // Find project root by looking for package.json
15
+ async function findProjectRoot(startDir) {
16
+ let dir = startDir;
17
+ while (dir !== path.dirname(dir)) {
18
+ try {
19
+ await fs.access(path.join(dir, "package.json"));
20
+ return dir;
21
+ }
22
+ catch {
23
+ dir = path.dirname(dir);
24
+ }
25
+ }
26
+ throw new Error("Could not find project root (no package.json)");
27
+ }
28
+ // Test a known similar pair
29
+ const _SIMILAR_FUNCTIONS = {
30
+ description: "Extracting similar logic patterns in pi-lens",
31
+ pairs: [
32
+ {
33
+ name: "runners/index.ts pattern",
34
+ files: [
35
+ "clients/dispatch/runners/index.ts",
36
+ "clients/dispatch/runners/architect.ts",
37
+ ],
38
+ expected: "High similarity in runner registration patterns",
39
+ },
40
+ {
41
+ name: "Client pattern",
42
+ files: ["clients/typescript-client.ts", "clients/biome-client.ts"],
43
+ expected: "Similar client structures",
44
+ },
45
+ ],
46
+ };
47
+ describe("🐶 Dogfood Test: Similarity on pi-lens codebase", () => {
48
+ let index;
49
+ let projectRoot;
50
+ beforeAll(async () => {
51
+ // Find project root
52
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
53
+ projectRoot = await findProjectRoot(__dirname);
54
+ // Build index of the entire codebase
55
+ console.log("\n🏗️ Building index of pi-lens codebase...");
56
+ console.log(` Project root: ${projectRoot}`);
57
+ const files = await glob("clients/**/*.ts", {
58
+ cwd: projectRoot,
59
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"],
60
+ });
61
+ console.log(` Found ${files.length} source files`);
62
+ const absoluteFiles = files.map((f) => path.join(projectRoot, f));
63
+ index = await buildProjectIndex(projectRoot, absoluteFiles);
64
+ console.log(` Indexed ${index.entries.size} functions`);
65
+ // Show some indexed functions
66
+ const sample = Array.from(index.entries.values()).slice(0, 5);
67
+ console.log("\n📋 Sample indexed functions:");
68
+ sample.forEach((e, i) => {
69
+ console.log(` ${i + 1}. ${e.id} (${e.transitionCount} transitions)`);
70
+ });
71
+ }, 30000); // 30s timeout for indexing
72
+ describe("Index validation", () => {
73
+ it("should have indexed functions", () => {
74
+ expect(index.entries.size).toBeGreaterThan(0);
75
+ console.log(`\n✅ Indexed ${index.entries.size} functions`);
76
+ });
77
+ it("should have functions with >20 transitions", () => {
78
+ const complex = Array.from(index.entries.values()).filter((e) => e.transitionCount >= 20);
79
+ expect(complex.length).toBeGreaterThan(0);
80
+ console.log(`\n✅ ${complex.length} functions pass complexity guardrail`);
81
+ });
82
+ });
83
+ describe("Find similar functions in our own codebase", () => {
84
+ it("should find similar patterns among runners", async () => {
85
+ // Find runner files
86
+ const runnerEntries = Array.from(index.entries.values()).filter((e) => e.filePath.includes("dispatch/runners/"));
87
+ console.log(`\n🔍 Testing ${runnerEntries.length} runner functions`);
88
+ const similarities = [];
89
+ // Compare each pair
90
+ for (let i = 0; i < runnerEntries.length; i++) {
91
+ for (let j = i + 1; j < runnerEntries.length; j++) {
92
+ const entry1 = runnerEntries[i];
93
+ const entry2 = runnerEntries[j];
94
+ // Skip if same file
95
+ if (entry1.filePath === entry2.filePath)
96
+ continue;
97
+ const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
98
+ if (sim >= 0.75) {
99
+ similarities.push({
100
+ func1: entry1.id,
101
+ func2: entry2.id,
102
+ similarity: sim,
103
+ });
104
+ }
105
+ }
106
+ }
107
+ // Sort by similarity
108
+ similarities.sort((a, b) => b.similarity - a.similarity);
109
+ console.log(`\n📊 Found ${similarities.length} similar pairs (>75%):`);
110
+ similarities.slice(0, 5).forEach((s, i) => {
111
+ console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
112
+ console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
113
+ });
114
+ // Log findings but don't fail - this is exploratory
115
+ expect(similarities.length).toBeGreaterThanOrEqual(0);
116
+ });
117
+ it("should find similar client patterns", async () => {
118
+ const clientEntries = Array.from(index.entries.values()).filter((e) => e.filePath.includes("clients/") &&
119
+ e.filePath.includes("-client.ts") &&
120
+ !e.filePath.includes("test"));
121
+ console.log(`\n🔍 Testing ${clientEntries.length} client functions`);
122
+ const similarities = [];
123
+ for (let i = 0; i < clientEntries.length; i++) {
124
+ for (let j = i + 1; j < clientEntries.length; j++) {
125
+ const entry1 = clientEntries[i];
126
+ const entry2 = clientEntries[j];
127
+ if (entry1.filePath === entry2.filePath)
128
+ continue;
129
+ const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
130
+ if (sim >= 0.75) {
131
+ similarities.push({
132
+ func1: entry1.id,
133
+ func2: entry2.id,
134
+ similarity: sim,
135
+ });
136
+ }
137
+ }
138
+ }
139
+ similarities.sort((a, b) => b.similarity - a.similarity);
140
+ console.log(`\n📊 Found ${similarities.length} similar client patterns (>75%):`);
141
+ similarities.slice(0, 3).forEach((s, i) => {
142
+ console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
143
+ console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
144
+ });
145
+ expect(similarities.length).toBeGreaterThanOrEqual(0);
146
+ });
147
+ });
148
+ describe("Find potential refactor opportunities", () => {
149
+ it("should identify duplicate utility functions", () => {
150
+ // Look for functions with very high similarity (>90%)
151
+ const entries = Array.from(index.entries.values());
152
+ const seenPairs = new Set(); // Deduplicate A→B and B→A
153
+ const duplicates = [];
154
+ for (const entry of entries) {
155
+ const matches = findSimilarFunctions(entry.matrix, index, 0.9, 3);
156
+ for (const match of matches) {
157
+ if (match.targetId === entry.id)
158
+ continue;
159
+ // Canonical pair key (sorted to avoid A,B and B,A)
160
+ const pairKey = [entry.id, match.targetId].sort().join("::");
161
+ if (seenPairs.has(pairKey))
162
+ continue;
163
+ seenPairs.add(pairKey);
164
+ duplicates.push({
165
+ func: entry.id,
166
+ similarTo: match.targetId,
167
+ similarity: match.similarity,
168
+ });
169
+ }
170
+ }
171
+ console.log(`\n🎯 Found ${duplicates.length} unique potential duplicates (>90%):`);
172
+ duplicates.slice(0, 5).forEach((d, i) => {
173
+ console.log(` ${i + 1}. ${d.func}`);
174
+ console.log(` Similar to: ${d.similarTo}`);
175
+ console.log(` Match: ${(d.similarity * 100).toFixed(1)}%`);
176
+ });
177
+ // This is informational - we don't assert on it
178
+ expect(true).toBe(true);
179
+ });
180
+ });
181
+ describe("Complexity distribution", () => {
182
+ it("should show transition count distribution", () => {
183
+ const entries = Array.from(index.entries.values());
184
+ const transitionCounts = entries.map((e) => e.transitionCount);
185
+ const avg = transitionCounts.reduce((a, b) => a + b, 0) / transitionCounts.length;
186
+ const min = Math.min(...transitionCounts);
187
+ const max = Math.max(...transitionCounts);
188
+ const belowThreshold = transitionCounts.filter((c) => c < 20).length;
189
+ const aboveThreshold = transitionCounts.filter((c) => c >= 20).length;
190
+ console.log("\n📊 Complexity Distribution:");
191
+ console.log(` Total functions: ${entries.length}`);
192
+ console.log(` Below threshold (<20): ${belowThreshold}`);
193
+ console.log(` Above threshold (≥20): ${aboveThreshold}`);
194
+ console.log(` Min transitions: ${min}`);
195
+ console.log(` Max transitions: ${max}`);
196
+ console.log(` Average: ${avg.toFixed(1)}`);
197
+ // Most functions should pass the guardrail
198
+ expect(aboveThreshold).toBeGreaterThan(0);
199
+ });
200
+ });
201
+ });