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,179 @@
1
+ /**
2
+ * Effect-TS Service Infrastructure for pi-lens
3
+ *
4
+ * Simplified implementation focusing on:
5
+ * - Concurrent runner execution
6
+ * - Timeout handling
7
+ * - Error recovery
8
+ */
9
+
10
+ import { Effect } from "effect";
11
+
12
+ // --- Error Types ---
13
+
14
+ export class RunnerError {
15
+ readonly _tag = "RunnerError";
16
+ constructor(
17
+ readonly runnerId: string,
18
+ readonly cause: unknown,
19
+ ) {}
20
+ }
21
+
22
+ export class TimeoutError {
23
+ readonly _tag = "TimeoutError";
24
+ constructor(
25
+ readonly operation: string,
26
+ readonly timeoutMs: number,
27
+ ) {}
28
+ }
29
+
30
+ // --- Result Types ---
31
+
32
+ export interface RunnerResult {
33
+ diagnostics: Array<{
34
+ id: string;
35
+ message: string;
36
+ severity: "error" | "warning" | "info" | "hint";
37
+ semantic?: "blocking" | "warning" | "fixed" | "silent" | "none";
38
+ }>;
39
+ durationMs: number;
40
+ error?: string;
41
+ }
42
+
43
+ export interface ConcurrentRunnerResult {
44
+ runnerId: string;
45
+ status: "success" | "failure" | "skipped";
46
+ diagnostics: Array<{
47
+ id: string;
48
+ message: string;
49
+ severity: "error" | "warning" | "info" | "hint";
50
+ semantic?: "blocking" | "warning" | "fixed" | "silent" | "none";
51
+ }>;
52
+ durationMs: number;
53
+ error?: string;
54
+ }
55
+
56
+ // --- Concurrent Execution Helper ---
57
+
58
+ /**
59
+ * Run multiple runners concurrently with Effect
60
+ *
61
+ * Features:
62
+ * - Parallel execution with Effect.all
63
+ * - Per-runner timeout handling
64
+ * - Graceful error recovery (individual failures don't stop others)
65
+ * - Automatic resource cleanup
66
+ */
67
+ export function runRunnersConcurrent(
68
+ filePath: string,
69
+ runnerIds: string[],
70
+ runSingle: (filePath: string, runnerId: string) => Promise<RunnerResult>,
71
+ timeoutMs = 30_000
72
+ ): Effect.Effect<ConcurrentRunnerResult[], never, never> {
73
+ return Effect.gen(function* () {
74
+ const startTime = Date.now();
75
+
76
+ // Run all runners in parallel
77
+ const results = yield* Effect.all(
78
+ runnerIds.map((runnerId) =>
79
+ Effect.gen(function* () {
80
+ const runnerStart = Date.now();
81
+
82
+ // Execute with timeout and error handling
83
+ const result = yield* Effect.tryPromise({
84
+ try: () => runSingle(filePath, runnerId),
85
+ catch: (err) => err,
86
+ }).pipe(
87
+ Effect.timeout(timeoutMs),
88
+ Effect.catchAll((err) =>
89
+ Effect.succeed({
90
+ diagnostics: [],
91
+ durationMs: Date.now() - runnerStart,
92
+ error: String(err),
93
+ })
94
+ )
95
+ );
96
+
97
+ const isError = "error" in result;
98
+
99
+ return {
100
+ runnerId,
101
+ status: isError ? "failure" as const : "success" as const,
102
+ diagnostics: isError ? [] : result.diagnostics,
103
+ durationMs: isError ? result.durationMs : result.durationMs,
104
+ error: isError ? result.error : undefined,
105
+ };
106
+ })
107
+ ),
108
+ { concurrency: "unbounded" }
109
+ );
110
+
111
+ return results;
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Run a single runner with timeout and error handling
117
+ */
118
+ export function runRunnerWithTimeout(
119
+ filePath: string,
120
+ runnerId: string,
121
+ runSingle: (filePath: string, runnerId: string) => Promise<RunnerResult>,
122
+ timeoutMs = 30_000
123
+ ): Effect.Effect<RunnerResult, RunnerError | TimeoutError, never> {
124
+ return Effect.gen(function* () {
125
+ const startTime = Date.now();
126
+
127
+ const result = yield* Effect.tryPromise({
128
+ try: () => runSingle(filePath, runnerId),
129
+ catch: (err) => new RunnerError(runnerId, err),
130
+ }).pipe(
131
+ Effect.timeout(timeoutMs),
132
+ Effect.mapError((err) => {
133
+ if (err instanceof RunnerError) return err;
134
+ return new TimeoutError(`runner:${runnerId}`, timeoutMs);
135
+ })
136
+ );
137
+
138
+ return {
139
+ diagnostics: result.diagnostics,
140
+ durationMs: Date.now() - startTime,
141
+ };
142
+ });
143
+ }
144
+
145
+ // --- Execution Helpers ---
146
+
147
+ /**
148
+ * Execute Effect and get result
149
+ */
150
+ export function executeEffect<T>(
151
+ effect: Effect.Effect<T, never, never>
152
+ ): Promise<T> {
153
+ return Effect.runPromise(effect);
154
+ }
155
+
156
+ /**
157
+ * Execute Effect with error handling
158
+ */
159
+ export function executeEffectWithError<T, E>(
160
+ effect: Effect.Effect<T, E, never>,
161
+ onError: (err: E) => T
162
+ ): Promise<T> {
163
+ return Effect.runPromise(
164
+ Effect.catchAll(effect, (err) => Effect.succeed(onError(err)))
165
+ );
166
+ }
167
+
168
+ // --- Error Formatting ---
169
+
170
+ export function formatError(err: RunnerError | TimeoutError): string {
171
+ switch (err._tag) {
172
+ case "RunnerError":
173
+ return `Runner ${err.runnerId} failed: ${err.cause}`;
174
+ case "TimeoutError":
175
+ return `Operation ${err.operation} timed out after ${err.timeoutMs}ms`;
176
+ default:
177
+ return "Unknown error";
178
+ }
179
+ }
@@ -4,10 +4,22 @@
4
4
  * Extracted from AstGrepClient to simplify the main client.
5
5
  * Handles: spawn, spawnSync, temp dir management, JSON parsing.
6
6
  */
7
- import { spawn, spawnSync } from "node:child_process";
7
+ import { spawn } from "node:child_process";
8
8
  import * as fs from "node:fs";
9
9
  import * as os from "node:os";
10
10
  import * as path from "node:path";
11
+ import { safeSpawn } from "./safe-spawn.js";
12
+ /**
13
+ * Escape an argument for Windows cmd.exe shell execution.
14
+ * Handles spaces, quotes, and special characters.
15
+ */
16
+ function escapeWindowsArg(arg) {
17
+ // If no special characters, return as-is
18
+ if (!/[\s\"]/.test(arg))
19
+ return arg;
20
+ // Escape quotes by doubling them
21
+ return `"${arg.replace(/"/g, "\"\"")}"`;
22
+ }
11
23
  export class SgRunner {
12
24
  constructor(verbose = false) {
13
25
  this.log = verbose
@@ -18,10 +30,8 @@ export class SgRunner {
18
30
  * Check if ast-grep CLI is available
19
31
  */
20
32
  isAvailable() {
21
- const result = spawnSync("npx", ["sg", "--version"], {
22
- encoding: "utf-8",
33
+ const result = safeSpawn("npx", ["sg", "--version"], {
23
34
  timeout: 10000,
24
- shell: true,
25
35
  });
26
36
  return !result.error && result.status === 0;
27
37
  }
@@ -30,10 +40,46 @@ export class SgRunner {
30
40
  */
31
41
  async exec(args) {
32
42
  return new Promise((resolve) => {
33
- const proc = spawn("npx", ["sg", ...args], {
34
- stdio: ["ignore", "pipe", "pipe"],
35
- shell: true,
36
- });
43
+ // On Windows with Git Bash/MSYS2, we need to use bash to properly
44
+ // handle $variables in patterns (prevent shell expansion)
45
+ const isWindows = process.platform === "win32";
46
+ const hasBash = process.env.MSYSTEM || process.env.GIT_SHELL;
47
+ let proc;
48
+ if (isWindows && hasBash) {
49
+ // Use bash -c with properly escaped command
50
+ // In bash, use single quotes around arguments containing $ to prevent expansion
51
+ const escapedArgs = args.map((arg) => {
52
+ // For bash, wrap $-containing args in single quotes
53
+ if (arg.includes("$")) {
54
+ return `'${arg.replace(/'/g, "'\\''")}'`;
55
+ }
56
+ // For other args with spaces/special chars, use double quotes
57
+ if (/[\s\"]/.test(arg)) {
58
+ return `"${arg.replace(/"/g, "\\\"")}"`;
59
+ }
60
+ return arg;
61
+ });
62
+ const bashCommand = `npx sg ${escapedArgs.join(" ")}`;
63
+ proc = spawn("bash", ["-c", bashCommand], {
64
+ stdio: ["ignore", "pipe", "pipe"],
65
+ windowsHide: true,
66
+ });
67
+ }
68
+ else if (isWindows) {
69
+ // Fallback: use cmd.exe with standard escaping
70
+ const fullCommand = `npx sg ${args.map(escapeWindowsArg).join(" ")}`;
71
+ proc = spawn(fullCommand, {
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ shell: true,
74
+ windowsHide: true,
75
+ });
76
+ }
77
+ else {
78
+ // Unix: normal spawn without shell
79
+ proc = spawn("npx", ["sg", ...args], {
80
+ stdio: ["ignore", "pipe", "pipe"],
81
+ });
82
+ }
37
83
  let stdout = "";
38
84
  let stderr = "";
39
85
  proc.stdout.on("data", (data) => (stdout += data.toString()));
@@ -78,11 +124,8 @@ export class SgRunner {
78
124
  * Run ast-grep synchronously (for simple scans)
79
125
  */
80
126
  execSync(args) {
81
- const result = spawnSync("npx", ["sg", ...args], {
82
- encoding: "utf-8",
127
+ const result = safeSpawn("npx", ["sg", ...args], {
83
128
  timeout: 30000,
84
- shell: true,
85
- maxBuffer: 32 * 1024 * 1024,
86
129
  });
87
130
  if (result.error) {
88
131
  return { output: "", error: result.error.message };
@@ -104,7 +147,38 @@ export class SgRunner {
104
147
  fs.mkdirSync(rulesSubdir, { recursive: true });
105
148
  fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
106
149
  fs.writeFileSync(ruleFile, ruleYaml);
107
- const result = spawnSync("npx", ["sg", "scan", "--config", configFile, "--json", dir], { encoding: "utf-8", timeout, shell: true });
150
+ const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
151
+ const output = result.stdout || result.stderr || "";
152
+ if (!output.trim())
153
+ return [];
154
+ const items = JSON.parse(output);
155
+ return Array.isArray(items) ? items : [items];
156
+ }
157
+ catch {
158
+ return [];
159
+ }
160
+ finally {
161
+ try {
162
+ fs.rmSync(sessionDir, { recursive: true, force: true });
163
+ }
164
+ catch (err) {
165
+ this.log(`Cleanup failed: ${err.message}`);
166
+ }
167
+ }
168
+ }
169
+ /**
170
+ * Run a rule file scan (temporary config approach) - alias for tempScan
171
+ */
172
+ scanWithRule(ruleYaml, dir, timeout = 30000) {
173
+ const sessionDir = path.join(os.tmpdir(), `sg-scan-${Date.now()}`);
174
+ const rulesSubdir = path.join(sessionDir, "rules");
175
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
176
+ const ruleFile = path.join(rulesSubdir, "rule.yml");
177
+ try {
178
+ fs.mkdirSync(rulesSubdir, { recursive: true });
179
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
180
+ fs.writeFileSync(ruleFile, ruleYaml);
181
+ const result = safeSpawn("npx", ["sg", "scan", "--config", configFile, "--json", dir], { timeout });
108
182
  const output = result.stdout || result.stderr || "";
109
183
  if (!output.trim())
110
184
  return [];
@@ -9,6 +9,19 @@ import { spawn, spawnSync } from "node:child_process";
9
9
  import * as fs from "node:fs";
10
10
  import * as os from "node:os";
11
11
  import * as path from "node:path";
12
+ import { safeSpawn } from "./safe-spawn.js";
13
+
14
+ /**
15
+ * Escape an argument for Windows cmd.exe shell execution.
16
+ * Handles spaces, quotes, and special characters.
17
+ */
18
+ function escapeWindowsArg(arg: string): string {
19
+ // If no special characters, return as-is
20
+ if (!/[\s\"]/.test(arg)) return arg;
21
+
22
+ // Escape quotes by doubling them
23
+ return `"${arg.replace(/"/g, "\"\"")}"`;
24
+ }
12
25
 
13
26
  export interface SgMatch {
14
27
  file: string;
@@ -38,10 +51,8 @@ export class SgRunner {
38
51
  * Check if ast-grep CLI is available
39
52
  */
40
53
  isAvailable(): boolean {
41
- const result = spawnSync("npx", ["sg", "--version"], {
42
- encoding: "utf-8",
54
+ const result = safeSpawn("npx", ["sg", "--version"], {
43
55
  timeout: 10000,
44
- shell: true,
45
56
  });
46
57
  return !result.error && result.status === 0;
47
58
  }
@@ -51,10 +62,46 @@ export class SgRunner {
51
62
  */
52
63
  async exec(args: string[]): Promise<SgResult> {
53
64
  return new Promise((resolve) => {
54
- const proc = spawn("npx", ["sg", ...args], {
55
- stdio: ["ignore", "pipe", "pipe"],
56
- shell: true,
57
- });
65
+ // On Windows with Git Bash/MSYS2, we need to use bash to properly
66
+ // handle $variables in patterns (prevent shell expansion)
67
+ const isWindows = process.platform === "win32";
68
+ const hasBash = process.env.MSYSTEM || process.env.GIT_SHELL;
69
+
70
+ let proc;
71
+ if (isWindows && hasBash) {
72
+ // Use bash -c with properly escaped command
73
+ // In bash, use single quotes around arguments containing $ to prevent expansion
74
+ const escapedArgs = args.map((arg) => {
75
+ // For bash, wrap $-containing args in single quotes
76
+ if (arg.includes("$")) {
77
+ return `'${arg.replace(/'/g, "'\\''")}'`;
78
+ }
79
+ // For other args with spaces/special chars, use double quotes
80
+ if (/[\s\"]/.test(arg)) {
81
+ return `"${arg.replace(/"/g, "\\\"")}"`;
82
+ }
83
+ return arg;
84
+ });
85
+ const bashCommand = `npx sg ${escapedArgs.join(" ")}`;
86
+ proc = spawn("bash", ["-c", bashCommand], {
87
+ stdio: ["ignore", "pipe", "pipe"],
88
+ windowsHide: true,
89
+ });
90
+ } else if (isWindows) {
91
+ // Fallback: use cmd.exe with standard escaping
92
+ const fullCommand = `npx sg ${args.map(escapeWindowsArg).join(" ")}`;
93
+ proc = spawn(fullCommand, {
94
+ stdio: ["ignore", "pipe", "pipe"],
95
+ shell: true,
96
+ windowsHide: true,
97
+ });
98
+ } else {
99
+ // Unix: normal spawn without shell
100
+ proc = spawn("npx", ["sg", ...args], {
101
+ stdio: ["ignore", "pipe", "pipe"],
102
+ });
103
+ }
104
+
58
105
  let stdout = "";
59
106
  let stderr = "";
60
107
 
@@ -101,11 +148,8 @@ export class SgRunner {
101
148
  * Run ast-grep synchronously (for simple scans)
102
149
  */
103
150
  execSync(args: string[]): { output: string; error?: string } {
104
- const result = spawnSync("npx", ["sg", ...args], {
105
- encoding: "utf-8",
151
+ const result = safeSpawn("npx", ["sg", ...args], {
106
152
  timeout: 30000,
107
- shell: true,
108
- maxBuffer: 32 * 1024 * 1024,
109
153
  });
110
154
 
111
155
  if (result.error) {
@@ -137,10 +181,50 @@ export class SgRunner {
137
181
  fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
138
182
  fs.writeFileSync(ruleFile, ruleYaml);
139
183
 
140
- const result = spawnSync(
184
+ const result = safeSpawn(
185
+ "npx",
186
+ ["sg", "scan", "--config", configFile, "--json", dir],
187
+ { timeout },
188
+ );
189
+
190
+ const output = result.stdout || result.stderr || "";
191
+ if (!output.trim()) return [];
192
+
193
+ const items = JSON.parse(output);
194
+ return Array.isArray(items) ? items : [items];
195
+ } catch {
196
+ return [];
197
+ } finally {
198
+ try {
199
+ fs.rmSync(sessionDir, { recursive: true, force: true });
200
+ } catch (err) {
201
+ this.log(`Cleanup failed: ${(err as Error).message}`);
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Run a rule file scan (temporary config approach) - alias for tempScan
208
+ */
209
+ scanWithRule(
210
+ ruleYaml: string,
211
+ dir: string,
212
+ timeout = 30000,
213
+ ): SgMatch[] {
214
+ const sessionDir = path.join(os.tmpdir(), `sg-scan-${Date.now()}`);
215
+ const rulesSubdir = path.join(sessionDir, "rules");
216
+ const configFile = path.join(sessionDir, ".sgconfig.yml");
217
+ const ruleFile = path.join(rulesSubdir, "rule.yml");
218
+
219
+ try {
220
+ fs.mkdirSync(rulesSubdir, { recursive: true });
221
+ fs.writeFileSync(configFile, `ruleDirs:\n - ./rules\n`);
222
+ fs.writeFileSync(ruleFile, ruleYaml);
223
+
224
+ const result = safeSpawn(
141
225
  "npx",
142
226
  ["sg", "scan", "--config", configFile, "--json", dir],
143
- { encoding: "utf-8", timeout, shell: true },
227
+ { timeout },
144
228
  );
145
229
 
146
230
  const output = result.stdout || result.stderr || "";
@@ -0,0 +1,160 @@
1
+ /**
2
+ * State Matrix Builder
3
+ *
4
+ * Implements Amain's 57×72 state transfer matrix construction.
5
+ * Counts parent→child transitions in the TypeScript AST.
6
+ */
7
+ import * as ts from "typescript";
8
+ import { getStateIndex, NUM_STATES, NUM_SYNTAX } from "./amain-types.js";
9
+ // ============================================================================
10
+ // Matrix Construction
11
+ // ============================================================================
12
+ /**
13
+ * Build a 57×72 state transfer matrix from TypeScript source code.
14
+ *
15
+ * matrix[i][j] = count of transitions from syntax state i to state j
16
+ *
17
+ * @param sourceCode TypeScript source code
18
+ * @returns 57×72 matrix of transition counts
19
+ */
20
+ export function buildStateMatrix(sourceCode) {
21
+ const sourceFile = ts.createSourceFile("temp.ts", sourceCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
22
+ // Initialize 57×72 matrix with zeros
23
+ const matrix = Array(NUM_SYNTAX)
24
+ .fill(0)
25
+ .map(() => Array(NUM_STATES).fill(0));
26
+ // Walk AST and count transitions
27
+ function visitNode(node, parentKind) {
28
+ const nodeState = getStateIndex(node);
29
+ if (parentKind !== undefined) {
30
+ const parentState = getStateIndex({ kind: parentKind });
31
+ // Only count transitions from syntax states (first 57)
32
+ if (parentState < NUM_SYNTAX) {
33
+ matrix[parentState][nodeState]++;
34
+ }
35
+ }
36
+ // Continue to children
37
+ ts.forEachChild(node, (child) => visitNode(child, node.kind));
38
+ }
39
+ visitNode(sourceFile);
40
+ return matrix;
41
+ }
42
+ /**
43
+ * Build matrix from a source file node (for incremental updates)
44
+ */
45
+ export function buildStateMatrixFromFile(sourceFile) {
46
+ // Initialize 57×72 matrix with zeros
47
+ const matrix = Array(NUM_SYNTAX)
48
+ .fill(0)
49
+ .map(() => Array(NUM_STATES).fill(0));
50
+ // Walk AST and count transitions
51
+ function visitNode(node, parentKind) {
52
+ const nodeState = getStateIndex(node);
53
+ if (parentKind !== undefined) {
54
+ const parentState = getStateIndex({ kind: parentKind });
55
+ // Only count transitions from syntax states (first 57)
56
+ if (parentState < NUM_SYNTAX) {
57
+ matrix[parentState][nodeState]++;
58
+ }
59
+ }
60
+ // Continue to children
61
+ ts.forEachChild(node, (child) => visitNode(child, node.kind));
62
+ }
63
+ visitNode(sourceFile);
64
+ return matrix;
65
+ }
66
+ // ============================================================================
67
+ // Probability Normalization
68
+ // ============================================================================
69
+ /**
70
+ * Convert count matrix to probability matrix.
71
+ * Each row sums to 1 (Markov chain property).
72
+ *
73
+ * @param matrix 57×72 count matrix
74
+ * @returns 57×72 probability matrix
75
+ */
76
+ export function toProbabilityMatrix(matrix) {
77
+ return matrix.map((row) => {
78
+ const sum = row.reduce((a, b) => a + b, 0);
79
+ if (sum === 0)
80
+ return row.map(() => 0);
81
+ return row.map((val) => val / sum);
82
+ });
83
+ }
84
+ // ============================================================================
85
+ // Similarity Calculation
86
+ // ============================================================================
87
+ /**
88
+ * Calculate cosine similarity between two state matrices.
89
+ * Returns 0-1 similarity score (1 = identical).
90
+ *
91
+ * @param matrix1 57×72 count matrix
92
+ * @param matrix2 57×72 count matrix
93
+ * @returns similarity score 0-1
94
+ */
95
+ export function calculateSimilarity(matrix1, matrix2) {
96
+ const prob1 = toProbabilityMatrix(matrix1);
97
+ const prob2 = toProbabilityMatrix(matrix2);
98
+ const similarities = [];
99
+ for (let i = 0; i < NUM_SYNTAX; i++) {
100
+ const row1 = prob1[i];
101
+ const row2 = prob2[i];
102
+ // Skip if both rows are empty
103
+ const hasData1 = row1.some((v) => v > 0);
104
+ const hasData2 = row2.some((v) => v > 0);
105
+ if (hasData1 || hasData2) {
106
+ // Calculate cosine similarity for this state
107
+ let dotProduct = 0;
108
+ let norm1 = 0;
109
+ let norm2 = 0;
110
+ for (let j = 0; j < NUM_STATES; j++) {
111
+ dotProduct += row1[j] * row2[j];
112
+ norm1 += row1[j] * row1[j];
113
+ norm2 += row2[j] * row2[j];
114
+ }
115
+ if (norm1 > 0 && norm2 > 0) {
116
+ similarities.push(dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)));
117
+ }
118
+ }
119
+ }
120
+ // Return average similarity across all states
121
+ if (similarities.length === 0)
122
+ return 0;
123
+ return similarities.reduce((a, b) => a + b, 0) / similarities.length;
124
+ }
125
+ /**
126
+ * Calculate similarity from source code directly (convenience method)
127
+ */
128
+ export function calculateSimilarityFromCode(code1, code2) {
129
+ const matrix1 = buildStateMatrix(code1);
130
+ const matrix2 = buildStateMatrix(code2);
131
+ return calculateSimilarity(matrix1, matrix2);
132
+ }
133
+ // ============================================================================
134
+ // Matrix Statistics (for guardrails)
135
+ // ============================================================================
136
+ /**
137
+ * Count total non-zero transitions in matrix (proxy for function complexity)
138
+ */
139
+ export function countTransitions(matrix) {
140
+ return matrix.flat().filter((v) => v > 0).length;
141
+ }
142
+ /**
143
+ * Check if function meets complexity threshold (>20 transitions)
144
+ */
145
+ export function isComplexEnough(matrix) {
146
+ return countTransitions(matrix) >= 20;
147
+ }
148
+ /**
149
+ * Serialize matrix for storage (sparse format to save space)
150
+ */
151
+ export function serializeMatrix(matrix) {
152
+ // Return full matrix for now - can optimize to sparse later if needed
153
+ return matrix;
154
+ }
155
+ /**
156
+ * Deserialize matrix from storage
157
+ */
158
+ export function deserializeMatrix(data) {
159
+ return data;
160
+ }