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,214 @@
1
+ /**
2
+ * Tests for spellcheck runner (typos-cli)
3
+ */
4
+
5
+ import * as fs from "node:fs";
6
+ import { createRequire } from "node:module";
7
+ import * as path from "node:path";
8
+ import { describe, expect, it } from "vitest";
9
+ import type { DispatchContext } from "../types.js";
10
+
11
+ function createMockContext(filePath: string): DispatchContext {
12
+ return {
13
+ filePath,
14
+ cwd: process.cwd(),
15
+ kind: "markdown" as any,
16
+ autofix: false,
17
+ deltaMode: false,
18
+ baselines: { get: () => [], add: () => {}, save: () => {} } as any,
19
+ pi: {} as any,
20
+ hasTool: async () => false,
21
+ log: () => {},
22
+ };
23
+ }
24
+
25
+ describe("spellcheck runner", () => {
26
+ const require = createRequire(import.meta.url);
27
+
28
+ it("should have correct runner definition", async () => {
29
+ const spellcheckModule = await import("./spellcheck.js");
30
+ const runner = spellcheckModule.default;
31
+
32
+ expect(runner.id).toBe("spellcheck");
33
+ expect(runner.appliesTo).toEqual(["markdown"]);
34
+ expect(runner.priority).toBe(30);
35
+ expect(runner.enabledByDefault).toBe(true);
36
+ expect(runner.skipTestFiles).toBe(false); // Check docs in test files too
37
+ });
38
+
39
+ it("should detect typos-cli availability", () => {
40
+ const { spawnSync } =
41
+ require("node:child_process") as typeof import("node:child_process");
42
+ const result = spawnSync("typos", ["--version"], {
43
+ encoding: "utf-8",
44
+ timeout: 10000,
45
+ shell: true,
46
+ });
47
+ expect(
48
+ result.error || result.status !== 0 ? "not available" : "available",
49
+ ).toBeTruthy(); // May or may not be installed
50
+ });
51
+
52
+ it("should detect typos in markdown content", async () => {
53
+ const tmpFile = path.join(
54
+ process.env.TEMP || "/tmp",
55
+ `spellcheck_test_${Date.now()}.md`,
56
+ );
57
+ fs.writeFileSync(
58
+ tmpFile,
59
+ `# README
60
+
61
+ This is a documnet about recieving data.
62
+ The seperation of concerns is important.
63
+ `,
64
+ );
65
+
66
+ try {
67
+ const spellcheckModule = await import("./spellcheck.js");
68
+ const runner = spellcheckModule.default;
69
+ const result = await runner.run(createMockContext(tmpFile));
70
+
71
+ // If typos-cli is installed, should detect typos
72
+ // If not installed, will be skipped
73
+ if (result.status !== "skipped") {
74
+ // Should detect at least "documnet" and "recieving"
75
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
76
+ expect(
77
+ result.diagnostics.some(
78
+ (d) =>
79
+ d.tool === "typos" &&
80
+ (d.message.includes("documnet") ||
81
+ d.message.includes("recieving") ||
82
+ d.message.includes("seperation")),
83
+ ),
84
+ ).toBe(true);
85
+ }
86
+ } finally {
87
+ if (fs.existsSync(tmpFile)) {
88
+ fs.unlinkSync(tmpFile);
89
+ }
90
+ }
91
+ });
92
+
93
+ it("should suggest corrections for typos", async () => {
94
+ const tmpFile = path.join(
95
+ process.env.TEMP || "/tmp",
96
+ `spellcheck_fix_${Date.now()}.md`,
97
+ );
98
+ fs.writeFileSync(
99
+ tmpFile,
100
+ `# Test
101
+
102
+ This is a recieving test.
103
+ `,
104
+ );
105
+
106
+ try {
107
+ const spellcheckModule = await import("./spellcheck.js");
108
+ const runner = spellcheckModule.default;
109
+ const result = await runner.run(createMockContext(tmpFile));
110
+
111
+ if (result.status !== "skipped" && result.diagnostics.length > 0) {
112
+ // Should have fix suggestions
113
+ const fixableDiags = result.diagnostics.filter((d) => d.fixable);
114
+ expect(fixableDiags.length).toBeGreaterThanOrEqual(1);
115
+ expect(
116
+ fixableDiags.some((d) =>
117
+ d.fixSuggestion?.toLowerCase().includes("receive"),
118
+ ),
119
+ ).toBe(true);
120
+ }
121
+ } finally {
122
+ if (fs.existsSync(tmpFile)) {
123
+ fs.unlinkSync(tmpFile);
124
+ }
125
+ }
126
+ });
127
+
128
+ it("should pass clean markdown files", async () => {
129
+ const tmpFile = path.join(
130
+ process.env.TEMP || "/tmp",
131
+ `spellcheck_ok_${Date.now()}.md`,
132
+ );
133
+ fs.writeFileSync(
134
+ tmpFile,
135
+ `# Clean README
136
+
137
+ This is a correct document about receiving data.
138
+ The separation of concerns is important.
139
+ All spelling is proper in this file.
140
+ `,
141
+ );
142
+
143
+ try {
144
+ const spellcheckModule = await import("./spellcheck.js");
145
+ const runner = spellcheckModule.default;
146
+ const result = await runner.run(createMockContext(tmpFile));
147
+
148
+ if (result.status !== "skipped") {
149
+ // Should have no typos
150
+ expect(result.diagnostics.length).toBe(0);
151
+ expect(result.status).toBe("succeeded");
152
+ }
153
+ } finally {
154
+ if (fs.existsSync(tmpFile)) {
155
+ fs.unlinkSync(tmpFile);
156
+ }
157
+ }
158
+ });
159
+
160
+ it("should handle JSON parse errors gracefully", async () => {
161
+ const tmpFile = path.join(
162
+ process.env.TEMP || "/tmp",
163
+ `spellcheck_json_${Date.now()}.md`,
164
+ );
165
+ fs.writeFileSync(tmpFile, `# Test\n\nSimple file.`);
166
+
167
+ try {
168
+ const spellcheckModule = await import("./spellcheck.js");
169
+ const runner = spellcheckModule.default;
170
+ const result = await runner.run(createMockContext(tmpFile));
171
+
172
+ // Should not crash on JSON parse issues
173
+ expect(["succeeded", "failed", "skipped"]).toContain(result.status);
174
+ } finally {
175
+ if (fs.existsSync(tmpFile)) {
176
+ fs.unlinkSync(tmpFile);
177
+ }
178
+ }
179
+ });
180
+
181
+ it("should skip when typos-cli is not available", async () => {
182
+ const tmpFile = path.join(
183
+ process.env.TEMP || "/tmp",
184
+ `spellcheck_skip_${Date.now()}.md`,
185
+ );
186
+ fs.writeFileSync(tmpFile, `# Test\n\nContent with typo: recieve.`);
187
+
188
+ try {
189
+ const spellcheckModule = await import("./spellcheck.js");
190
+ const runner = spellcheckModule.default;
191
+
192
+ // Check if typos is available
193
+ const { spawnSync } =
194
+ require("node:child_process") as typeof import("node:child_process");
195
+ const checkResult = spawnSync("typos", ["--version"], {
196
+ encoding: "utf-8",
197
+ timeout: 5000,
198
+ shell: true,
199
+ });
200
+
201
+ const isAvailable = !checkResult.error && checkResult.status === 0;
202
+ const result = await runner.run(createMockContext(tmpFile));
203
+
204
+ if (!isAvailable) {
205
+ expect(result.status).toBe("skipped");
206
+ expect(result.diagnostics).toHaveLength(0);
207
+ }
208
+ } finally {
209
+ if (fs.existsSync(tmpFile)) {
210
+ fs.unlinkSync(tmpFile);
211
+ }
212
+ }
213
+ });
214
+ });
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Spellcheck runner for dispatch system
3
+ *
4
+ * Uses typos-cli (Rust-based, fast, zero-config) to check spelling in:
5
+ * - Markdown files (.md, .mdx)
6
+ * - Code comments (optional, if typos is configured)
7
+ *
8
+ * Key features:
9
+ * - Fast (Rust-based, ~10x faster than cspell)
10
+ * - Low false positives (only checks known typos)
11
+ * - Zero-config by default
12
+ * - JSON output for easy parsing
13
+ *
14
+ * Alternative considered: cspell
15
+ * - cspell: More comprehensive, but higher false positives, needs config
16
+ * - typos-cli: Faster, less noise, works out of the box
17
+ *
18
+ * Install: cargo install typos-cli
19
+ * Or: npm install -g typos-cli (if wrapped)
20
+ */
21
+
22
+ import { safeSpawn } from "../../safe-spawn.js";
23
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
24
+ import type {
25
+ Diagnostic,
26
+ DispatchContext,
27
+ RunnerDefinition,
28
+ RunnerResult,
29
+ } from "../types.js";
30
+
31
+ const typos = createAvailabilityChecker("typos", ".exe");
32
+
33
+ /**
34
+ * Parse typos-cli JSON output (JSON Lines format)
35
+ *
36
+ * Each line is a JSON object:
37
+ * {
38
+ * "path": "file.md",
39
+ * "line_num": 42,
40
+ * "byte_offset": 1234,
41
+ * "typo": "recieve",
42
+ * "corrections": ["receive"]
43
+ * }
44
+ */
45
+ function parseTyposOutput(raw: string, filePath: string): Diagnostic[] {
46
+ const diagnostics: Diagnostic[] = [];
47
+
48
+ if (!raw.trim()) {
49
+ return diagnostics;
50
+ }
51
+
52
+ const lines = raw.trim().split("\n").filter((l) => l.trim());
53
+
54
+ for (const line of lines) {
55
+ try {
56
+ const parsed = JSON.parse(line) as {
57
+ path?: string;
58
+ line_num?: number;
59
+ byte_offset?: number;
60
+ typo?: string;
61
+ corrections?: string[];
62
+ };
63
+
64
+ if (!parsed.typo || !parsed.line_num) continue;
65
+
66
+ const corrections = parsed.corrections?.join(", ") || "no suggestions";
67
+ const message = `Typo: "${parsed.typo}" → ${corrections}`;
68
+
69
+ diagnostics.push({
70
+ id: `typos-${parsed.line_num}-${parsed.typo}`,
71
+ message,
72
+ filePath,
73
+ line: parsed.line_num,
74
+ column: 1, // typos-cli doesn't provide column, just byte offset
75
+ severity: "warning",
76
+ semantic: "warning",
77
+ tool: "typos",
78
+ rule: "typo",
79
+ fixable: !!parsed.corrections?.length,
80
+ fixSuggestion: parsed.corrections?.[0],
81
+ });
82
+ } catch {
83
+ // Skip invalid JSON lines
84
+ continue;
85
+ }
86
+ }
87
+
88
+ return diagnostics;
89
+ }
90
+
91
+ const spellcheckRunner: RunnerDefinition = {
92
+ id: "spellcheck",
93
+ appliesTo: ["markdown"],
94
+ priority: 30, // Run after code quality checks (biome=10, slop=25)
95
+ enabledByDefault: true,
96
+ skipTestFiles: false, // Check docs in test files too
97
+
98
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
99
+ // Skip if typos-cli is not installed
100
+ if (!typos.isAvailable(ctx.cwd || process.cwd())) {
101
+ return { status: "skipped", diagnostics: [], semantic: "none" };
102
+ }
103
+
104
+ // Run typos-cli with JSON output
105
+ // --format json: Output JSON Lines
106
+ // --exclude <pattern>: Could be used to exclude code blocks if needed
107
+ const args = ["--format", "json", ctx.filePath];
108
+
109
+ const result = safeSpawn(typos.getCommand()!, args, {
110
+ timeout: 15000,
111
+ });
112
+
113
+ // typos-cli exits with code 2 if typos found, 0 if clean
114
+ const hasTypos = result.status === 2 || result.stdout?.trim();
115
+
116
+ if (!hasTypos) {
117
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
118
+ }
119
+
120
+ // Parse diagnostics
121
+ const raw = result.stdout + result.stderr;
122
+ const diagnostics = parseTyposOutput(raw, ctx.filePath);
123
+
124
+ if (diagnostics.length === 0) {
125
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
126
+ }
127
+
128
+ return {
129
+ status: "failed",
130
+ diagnostics,
131
+ semantic: "warning",
132
+ };
133
+ },
134
+ };
135
+
136
+ export default spellcheckRunner;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Tree-sitter Structural Analysis Runner
3
+ *
4
+ * Executes all loaded tree-sitter query files from rules/tree-sitter-queries/
5
+ * for fast AST-based pattern matching.
6
+ */
7
+ import path from "node:path";
8
+ import { TreeSitterClient } from "../../tree-sitter-client.js";
9
+ import { queryLoader } from "../../tree-sitter-query-loader.js";
10
+ const treeSitterRunner = {
11
+ id: "tree-sitter",
12
+ appliesTo: ["jsts", "python"],
13
+ priority: 14, // Between oxlint (12) and ast-grep-napi (15)
14
+ enabledByDefault: true,
15
+ skipTestFiles: false, // Run on test files too (structural issues matter there)
16
+ async run(ctx) {
17
+ // Initialize tree-sitter client
18
+ const client = new TreeSitterClient();
19
+ if (!client.isAvailable()) {
20
+ return { status: "skipped", diagnostics: [], semantic: "none" };
21
+ }
22
+ const initialized = await client.init();
23
+ if (!initialized) {
24
+ return { status: "skipped", diagnostics: [], semantic: "none" };
25
+ }
26
+ // Determine language from file extension
27
+ const filePath = ctx.filePath;
28
+ const isPython = filePath.endsWith(".py");
29
+ const isTypeScript = filePath.endsWith(".ts");
30
+ const isTSX = filePath.endsWith(".tsx");
31
+ const isJavaScript = filePath.endsWith(".js") || filePath.endsWith(".jsx");
32
+ let languageId;
33
+ if (isPython) {
34
+ languageId = "python";
35
+ }
36
+ else if (isTSX) {
37
+ languageId = "tsx";
38
+ }
39
+ else if (isTypeScript) {
40
+ languageId = "typescript";
41
+ }
42
+ else if (isJavaScript) {
43
+ languageId = "javascript";
44
+ }
45
+ else {
46
+ return { status: "skipped", diagnostics: [], semantic: "none" };
47
+ }
48
+ // Load queries if not already loaded
49
+ if (!queryLoader.getAllQueries().length) {
50
+ await queryLoader.loadQueries();
51
+ }
52
+ // Get all loaded queries for this language
53
+ const allQueries = queryLoader.getAllQueries();
54
+ const languageQueries = allQueries.filter((q) => q.language === languageId ||
55
+ (isJavaScript && q.language === "typescript"));
56
+ if (languageQueries.length === 0) {
57
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
58
+ }
59
+ const diagnostics = [];
60
+ // Run each query against the file
61
+ for (const query of languageQueries) {
62
+ try {
63
+ // Extract directory from file path (use path.dirname for cross-platform)
64
+ const rootDir = path.dirname(filePath);
65
+ const matches = await client.structuralSearch(query.id, // Use query ID as pattern (findMatchingQuery will resolve it)
66
+ languageId, rootDir, { maxResults: 10, fileFilter: (f) => f === filePath });
67
+ for (const match of matches) {
68
+ // Get line/column from match (already 0-indexed from tree-sitter)
69
+ const line = match.line;
70
+ const column = match.column;
71
+ // Map severity to semantic
72
+ const semantic = query.severity === "error"
73
+ ? "blocking"
74
+ : query.severity === "warning"
75
+ ? "warning"
76
+ : "none";
77
+ diagnostics.push({
78
+ id: `tree-sitter:${query.id}:${line}`,
79
+ message: query.message,
80
+ filePath,
81
+ line: line + 1, // 1-indexed
82
+ column: column + 1, // 1-indexed
83
+ severity: query.severity,
84
+ semantic,
85
+ tool: "tree-sitter",
86
+ rule: query.id,
87
+ });
88
+ }
89
+ }
90
+ catch (err) {
91
+ // Individual query failure shouldn't stop other queries
92
+ console.error(`[tree-sitter] Query ${query.id} failed:`, err);
93
+ }
94
+ }
95
+ if (diagnostics.length === 0) {
96
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
97
+ }
98
+ // Check if any blocking issues
99
+ const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
100
+ return {
101
+ status: hasBlocking ? "failed" : "succeeded",
102
+ diagnostics,
103
+ semantic: hasBlocking ? "blocking" : "warning",
104
+ };
105
+ },
106
+ };
107
+ export default treeSitterRunner;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Tree-sitter Structural Analysis Runner
3
+ *
4
+ * Executes all loaded tree-sitter query files from rules/tree-sitter-queries/
5
+ * for fast AST-based pattern matching.
6
+ */
7
+
8
+ import path from "node:path";
9
+ import { TreeSitterClient } from "../../tree-sitter-client.js";
10
+ import { queryLoader } from "../../tree-sitter-query-loader.js";
11
+ import type {
12
+ Diagnostic,
13
+ DispatchContext,
14
+ RunnerDefinition,
15
+ RunnerResult,
16
+ } from "../types.js";
17
+
18
+ const treeSitterRunner: RunnerDefinition = {
19
+ id: "tree-sitter",
20
+ appliesTo: ["jsts", "python"],
21
+ priority: 14, // Between oxlint (12) and ast-grep-napi (15)
22
+ enabledByDefault: true,
23
+ skipTestFiles: false, // Run on test files too (structural issues matter there)
24
+
25
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
26
+ // Initialize tree-sitter client
27
+ const client = new TreeSitterClient();
28
+ if (!client.isAvailable()) {
29
+ return { status: "skipped", diagnostics: [], semantic: "none" };
30
+ }
31
+
32
+ const initialized = await client.init();
33
+ if (!initialized) {
34
+ return { status: "skipped", diagnostics: [], semantic: "none" };
35
+ }
36
+
37
+ // Determine language from file extension
38
+ const filePath = ctx.filePath;
39
+ const isPython = filePath.endsWith(".py");
40
+ const isTypeScript = filePath.endsWith(".ts");
41
+ const isTSX = filePath.endsWith(".tsx");
42
+ const isJavaScript = filePath.endsWith(".js") || filePath.endsWith(".jsx");
43
+
44
+ let languageId: string;
45
+ if (isPython) {
46
+ languageId = "python";
47
+ } else if (isTSX) {
48
+ languageId = "tsx";
49
+ } else if (isTypeScript) {
50
+ languageId = "typescript";
51
+ } else if (isJavaScript) {
52
+ languageId = "javascript";
53
+ } else {
54
+ return { status: "skipped", diagnostics: [], semantic: "none" };
55
+ }
56
+
57
+ // Load queries if not already loaded
58
+ if (!queryLoader.getAllQueries().length) {
59
+ await queryLoader.loadQueries();
60
+ }
61
+
62
+ // Get all loaded queries for this language
63
+ const allQueries = queryLoader.getAllQueries();
64
+ const languageQueries = allQueries.filter(
65
+ (q) =>
66
+ q.language === languageId ||
67
+ (isJavaScript && q.language === "typescript"),
68
+ );
69
+
70
+ if (languageQueries.length === 0) {
71
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
72
+ }
73
+
74
+ const diagnostics: Diagnostic[] = [];
75
+
76
+ // Run each query against the file
77
+ for (const query of languageQueries) {
78
+ try {
79
+ // Extract directory from file path (use path.dirname for cross-platform)
80
+ const rootDir = path.dirname(filePath);
81
+
82
+ const matches = await client.structuralSearch(
83
+ query.id, // Use query ID as pattern (findMatchingQuery will resolve it)
84
+ languageId,
85
+ rootDir,
86
+ { maxResults: 10, fileFilter: (f) => f === filePath },
87
+ );
88
+
89
+ for (const match of matches) {
90
+ // Get line/column from match (already 0-indexed from tree-sitter)
91
+ const line = match.line;
92
+ const column = match.column;
93
+
94
+ // Map severity to semantic
95
+ const semantic =
96
+ query.severity === "error"
97
+ ? "blocking"
98
+ : query.severity === "warning"
99
+ ? "warning"
100
+ : "none";
101
+
102
+ diagnostics.push({
103
+ id: `tree-sitter:${query.id}:${line}`,
104
+ message: query.message,
105
+ filePath,
106
+ line: line + 1, // 1-indexed
107
+ column: column + 1, // 1-indexed
108
+ severity: query.severity,
109
+ semantic,
110
+ tool: "tree-sitter",
111
+ rule: query.id,
112
+ });
113
+ }
114
+ } catch (err) {
115
+ // Individual query failure shouldn't stop other queries
116
+ console.error(`[tree-sitter] Query ${query.id} failed:`, err);
117
+ }
118
+ }
119
+
120
+ if (diagnostics.length === 0) {
121
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
122
+ }
123
+
124
+ // Check if any blocking issues
125
+ const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
126
+
127
+ return {
128
+ status: hasBlocking ? "failed" : "succeeded",
129
+ diagnostics,
130
+ semantic: hasBlocking ? "blocking" : "warning",
131
+ };
132
+ },
133
+ };
134
+
135
+ export default treeSitterRunner;