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,162 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it, beforeAll, afterAll } from "vitest";
4
+ import type { DispatchContext } from "../types.js";
5
+
6
+ function createMockContext(
7
+ filePath: string,
8
+ kind: "jsts" | "python" | "go" | "rust" = "jsts",
9
+ cwd?: string,
10
+ ): DispatchContext {
11
+ return {
12
+ filePath,
13
+ cwd: cwd || process.cwd(),
14
+ kind,
15
+ autofix: false,
16
+ deltaMode: false,
17
+ baselines: { get: () => undefined, set: () => {}, clear: () => {} } as any,
18
+ pi: { getFlag: () => false } as any,
19
+ hasTool: async () => false,
20
+ log: () => {},
21
+ };
22
+ }
23
+
24
+ describe("architect runner", () => {
25
+ const testDir = path.join(process.env.TEMP || "/tmp", `architect_test_${Date.now()}`);
26
+ const configPath = path.join(testDir, ".pi-lens", "architect.yaml");
27
+
28
+ beforeAll(() => {
29
+ // Create test config
30
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
31
+ fs.writeFileSync(
32
+ configPath,
33
+ `version: "1.0"
34
+ rules:
35
+ - pattern: "**/*.ts"
36
+ max_lines: 50
37
+ must_not:
38
+ - pattern: 'hardcoded_secret_12345'
39
+ message: "No hardcoded secrets"
40
+ fix: "Use process.env.SECRET"
41
+ - pattern: 'console\.log'
42
+ message: "No console.log in production"
43
+ `,
44
+ );
45
+ });
46
+
47
+ afterAll(() => {
48
+ try {
49
+ if (fs.existsSync(testDir)) {
50
+ fs.rmSync(testDir, { recursive: true, force: true });
51
+ }
52
+ } catch {
53
+ // Ignore cleanup errors
54
+ }
55
+ });
56
+
57
+ it("should load default config when no user config exists", async () => {
58
+ const module = await import("./architect.js");
59
+ const runner = module.default;
60
+
61
+ // Use a unique temp dir with no user config (will fall back to default)
62
+ const noUserConfigDir = path.join(process.env.TEMP || "/tmp", `no_arch_user_config_${Date.now()}`);
63
+ fs.mkdirSync(noUserConfigDir, { recursive: true });
64
+
65
+ // Create a very large file that should trigger default max_lines rule
66
+ const tmpFile = path.join(noUserConfigDir, `large_${Date.now()}.ts`);
67
+ fs.writeFileSync(tmpFile, Array(5000).fill("// line").join("\n"));
68
+
69
+ try {
70
+ const result = await runner.run(
71
+ createMockContext(tmpFile, "jsts", noUserConfigDir),
72
+ );
73
+ // Should use default config and find violations
74
+ expect(result.status).toBe("succeeded");
75
+ // Should have size violation from default config
76
+ expect(result.diagnostics.some((d) => d.message.includes("line limit"))).toBe(true);
77
+ } finally {
78
+ try {
79
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
80
+ if (fs.existsSync(noUserConfigDir)) fs.rmdirSync(noUserConfigDir);
81
+ } catch {}
82
+ }
83
+ });
84
+
85
+ it("should detect file size violations", async () => {
86
+ const module = await import("./architect.js");
87
+ const runner = module.default;
88
+
89
+ const tmpFile = path.join(testDir, `large_file_${Date.now()}.ts`);
90
+ // Create file with 100 lines (exceeds 50 line limit)
91
+ fs.writeFileSync(tmpFile, Array(100).fill("// line").join("\n"));
92
+
93
+ try {
94
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
95
+ expect(result.status).toBe("succeeded");
96
+ expect(result.diagnostics.length).toBeGreaterThan(0);
97
+ expect(result.diagnostics.some((d) => d.message.includes("50 line limit"))).toBe(
98
+ true,
99
+ );
100
+ } finally {
101
+ try {
102
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
103
+ } catch {}
104
+ }
105
+ });
106
+
107
+ it("should detect pattern violations", async () => {
108
+ const module = await import("./architect.js");
109
+ const runner = module.default;
110
+
111
+ const tmpFile = path.join(testDir, `bad_patterns_${Date.now()}.ts`);
112
+ fs.writeFileSync(
113
+ tmpFile,
114
+ `const x = hardcoded_secret_12345;
115
+ console.log(x);
116
+ `,
117
+ );
118
+
119
+ try {
120
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
121
+ expect(result.status).toBe("succeeded");
122
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
123
+ expect(
124
+ result.diagnostics.some((d) => d.message.includes("hardcoded")),
125
+ ).toBe(true);
126
+ expect(
127
+ result.diagnostics.some((d) => d.message.includes("console.log")),
128
+ ).toBe(true);
129
+ } finally {
130
+ try {
131
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
132
+ } catch {}
133
+ }
134
+ });
135
+
136
+ it("should return no diagnostics for clean files", async () => {
137
+ const module = await import("./architect.js");
138
+ const runner = module.default;
139
+
140
+ const tmpFile = path.join(testDir, `clean_${Date.now()}.ts`);
141
+ // Small file (20 lines) with no violations
142
+ fs.writeFileSync(tmpFile, Array(20).fill("// clean code").join("\n"));
143
+
144
+ try {
145
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
146
+ expect(result.status).toBe("succeeded");
147
+ expect(result.diagnostics.length).toBe(0);
148
+ } finally {
149
+ try {
150
+ if (fs.existsSync(tmpFile)) fs.unlinkSync(tmpFile);
151
+ } catch {}
152
+ }
153
+ });
154
+
155
+ it("should skip test files", async () => {
156
+ const module = await import("./architect.js");
157
+ const runner = module.default;
158
+
159
+ // The runner should have skipTestFiles: true
160
+ expect(runner.skipTestFiles).toBe(true);
161
+ });
162
+ });
@@ -23,6 +23,7 @@ const architectRunner: RunnerDefinition = {
23
23
  appliesTo: ["jsts", "python", "go", "rust", "cxx", "shell", "cmake"],
24
24
  priority: 40,
25
25
  enabledByDefault: true,
26
+ skipTestFiles: true, // Skip test files - rules can be noisy there
26
27
 
27
28
  async run(ctx: DispatchContext): Promise<RunnerResult> {
28
29
  const relPath = ctx.filePath.replace(ctx.cwd, "").replace(/\\/g, "/");
@@ -44,15 +45,29 @@ const architectRunner: RunnerDefinition = {
44
45
  // Check for violations
45
46
  const violations = architectClient.checkFile(relPath, content);
46
47
  for (const v of violations) {
48
+ // Build message with inline fix guidance
49
+ let message = v.message;
50
+ let fixSuggestion: string | undefined = v.fix;
51
+
52
+ if (v.fix) {
53
+ const fixPreview = v.fix.length > 60 ? `${v.fix.substring(0, 60)}...` : v.fix;
54
+ message += `\n💡 Suggested fix: ${fixPreview}`;
55
+ } else if (v.note) {
56
+ const notePreview = v.note.length > 80 ? `${v.note.substring(0, 80)}...` : v.note;
57
+ message += `\n📝 ${notePreview}`;
58
+ }
59
+
47
60
  diagnostics.push({
48
61
  id: `architect-${v.line || 0}-${v.pattern}`,
49
- message: v.message,
62
+ message,
50
63
  filePath: ctx.filePath,
51
64
  line: v.line,
52
- severity: "error",
53
- semantic: "blocking",
65
+ severity: "warning",
66
+ semantic: "warning",
54
67
  tool: "architect",
55
68
  rule: v.pattern,
69
+ fixable: !!v.fix,
70
+ fixSuggestion,
56
71
  });
57
72
  }
58
73
 
@@ -64,8 +79,8 @@ const architectRunner: RunnerDefinition = {
64
79
  id: `architect-size-${lineCount}`,
65
80
  message: sizeViolation.message,
66
81
  filePath: ctx.filePath,
67
- severity: "error",
68
- semantic: "blocking",
82
+ severity: "warning",
83
+ semantic: "warning",
69
84
  tool: "architect",
70
85
  rule: "file-size-limit",
71
86
  fixSuggestion: "Split into smaller modules",
@@ -77,9 +92,9 @@ const architectRunner: RunnerDefinition = {
77
92
  }
78
93
 
79
94
  return {
80
- status: "failed",
95
+ status: "succeeded", // Warnings don't fail the run
81
96
  diagnostics,
82
- semantic: "blocking",
97
+ semantic: "warning",
83
98
  };
84
99
  },
85
100
  };
@@ -0,0 +1,462 @@
1
+ /**
2
+ * ast-grep NAPI runner for dispatch system
3
+ *
4
+ * Uses @ast-grep/napi for programmatic parsing instead of CLI.
5
+ * Handles TypeScript/JavaScript/CSS/HTML files with YAML rule support.
6
+ *
7
+ * Replaces CLI-based runners for faster performance (100x speedup).
8
+ */
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
13
+ // Lazy load the napi package
14
+ let sg;
15
+ async function loadSg() {
16
+ if (sg)
17
+ return sg;
18
+ try {
19
+ sg = await import("@ast-grep/napi");
20
+ return sg;
21
+ }
22
+ catch {
23
+ return undefined;
24
+ }
25
+ }
26
+ // Supported extensions for NAPI
27
+ const SUPPORTED_EXTS = [".ts", ".tsx", ".js", ".jsx", ".css", ".html", ".htm"];
28
+ function canHandle(filePath) {
29
+ return SUPPORTED_EXTS.includes(path.extname(filePath).toLowerCase());
30
+ }
31
+ function getLang(filePath, sgModule) {
32
+ const ext = path.extname(filePath).toLowerCase();
33
+ switch (ext) {
34
+ case ".ts": return sgModule.Lang.TypeScript;
35
+ case ".tsx": return sgModule.Lang.Tsx;
36
+ case ".js":
37
+ case ".jsx": return sgModule.Lang.JavaScript;
38
+ case ".css": return sgModule.Lang.Css;
39
+ case ".html":
40
+ case ".htm": return sgModule.Lang.Html;
41
+ default: return undefined;
42
+ }
43
+ }
44
+ function loadYamlRules(ruleDir) {
45
+ const rules = [];
46
+ if (!fs.existsSync(ruleDir))
47
+ return rules;
48
+ const files = fs.readdirSync(ruleDir).filter(f => f.endsWith(".yml"));
49
+ for (const file of files) {
50
+ try {
51
+ const content = fs.readFileSync(path.join(ruleDir, file), "utf-8");
52
+ // Split by --- to handle multiple YAML documents in one file
53
+ const documents = content.split(/^---$/m).filter(d => d.trim());
54
+ for (const doc of documents) {
55
+ const rule = parseSimpleYaml(doc.trim());
56
+ if (rule && rule.id) {
57
+ rules.push(rule);
58
+ }
59
+ }
60
+ }
61
+ catch {
62
+ // Skip invalid files
63
+ }
64
+ }
65
+ return rules;
66
+ }
67
+ function parseSimpleYaml(content) {
68
+ const lines = content.split("\n");
69
+ const rule = { id: "", metadata: {} };
70
+ let currentSection = "root";
71
+ let sectionStack = [];
72
+ let multilineBuffer = [];
73
+ let multilineKey = "";
74
+ function getCurrentObj() {
75
+ if (sectionStack.length === 0)
76
+ return rule;
77
+ return sectionStack[sectionStack.length - 1].obj;
78
+ }
79
+ function getIndent(line) {
80
+ let count = 0;
81
+ for (const char of line) {
82
+ if (char === " ")
83
+ count++;
84
+ else if (char === "\t")
85
+ count += 2;
86
+ else
87
+ break;
88
+ }
89
+ return count;
90
+ }
91
+ for (let i = 0; i < lines.length; i++) {
92
+ const line = lines[i];
93
+ const trimmed = line.trim();
94
+ if (!trimmed || trimmed.startsWith("#"))
95
+ continue;
96
+ if (trimmed === "---")
97
+ continue;
98
+ const indent = getIndent(line);
99
+ // Pop stack if indent decreased
100
+ while (sectionStack.length > 0 && indent <= sectionStack[sectionStack.length - 1].indent) {
101
+ sectionStack.pop();
102
+ }
103
+ // Check for multiline continuation
104
+ if (line.startsWith(" ") && !trimmed.includes(":") && multilineKey) {
105
+ multilineBuffer.push(trimmed);
106
+ continue;
107
+ }
108
+ // Flush multiline buffer
109
+ if (multilineKey && multilineBuffer.length > 0) {
110
+ const value = multilineBuffer.join("\n");
111
+ const current = getCurrentObj();
112
+ if (multilineKey === "pattern" && current) {
113
+ current.pattern = value;
114
+ }
115
+ multilineKey = "";
116
+ multilineBuffer = [];
117
+ }
118
+ const colonIndex = trimmed.indexOf(":");
119
+ const key = colonIndex > 0 ? trimmed.substring(0, colonIndex).trim() : trimmed;
120
+ const value = colonIndex > 0 ? trimmed.substring(colonIndex + 1).trim() : "";
121
+ if (key === "id") {
122
+ rule.id = value.replace(/^["']|["']$/g, "");
123
+ }
124
+ else if (key === "language") {
125
+ rule.language = value;
126
+ }
127
+ else if (key === "severity") {
128
+ rule.severity = value;
129
+ }
130
+ else if (key === "message") {
131
+ if (value === "|") {
132
+ multilineKey = "message";
133
+ }
134
+ else {
135
+ rule.message = value.replace(/^["']|["']$/g, "");
136
+ }
137
+ }
138
+ else if (key === "metadata") {
139
+ currentSection = "metadata";
140
+ const newObj = {};
141
+ rule.metadata = newObj;
142
+ sectionStack.push({ name: "metadata", indent, obj: newObj });
143
+ }
144
+ else if (key === "rule") {
145
+ currentSection = "rule";
146
+ const newObj = {};
147
+ rule.rule = newObj;
148
+ sectionStack.push({ name: "rule", indent, obj: newObj });
149
+ }
150
+ else if (sectionStack.length > 0) {
151
+ const current = getCurrentObj();
152
+ const currentSectionName = sectionStack[sectionStack.length - 1]?.name;
153
+ if (key === "weight" && currentSectionName === "metadata") {
154
+ if (!rule.metadata)
155
+ rule.metadata = {};
156
+ rule.metadata.weight = parseInt(value, 10) || 3;
157
+ }
158
+ else if (key === "category" && currentSectionName === "metadata") {
159
+ if (!rule.metadata)
160
+ rule.metadata = {};
161
+ rule.metadata.category = value.replace(/^["']|["']$/g, "");
162
+ }
163
+ else if (key === "pattern") {
164
+ if (value === "|") {
165
+ multilineKey = "pattern";
166
+ }
167
+ else {
168
+ // Strip all surrounding quotes (handle nested quotes from YAML)
169
+ let stripped = value;
170
+ while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
171
+ stripped = stripped.slice(1, -1);
172
+ }
173
+ while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
174
+ stripped = stripped.slice(1, -1);
175
+ }
176
+ current.pattern = stripped;
177
+ }
178
+ }
179
+ else if (key === "kind") {
180
+ current.kind = value;
181
+ }
182
+ else if (key === "regex") {
183
+ // Strip all surrounding quotes
184
+ let stripped = value;
185
+ while (stripped.startsWith('"') && stripped.endsWith('"') && stripped.length > 1) {
186
+ stripped = stripped.slice(1, -1);
187
+ }
188
+ while (stripped.startsWith("'") && stripped.endsWith("'") && stripped.length > 1) {
189
+ stripped = stripped.slice(1, -1);
190
+ }
191
+ current.regex = stripped;
192
+ }
193
+ else if (key === "has" || key === "not") {
194
+ const newObj = {};
195
+ current[key] = newObj;
196
+ sectionStack.push({ name: key, indent, obj: newObj });
197
+ }
198
+ else if (key === "any" || key === "all") {
199
+ if (!current[key])
200
+ current[key] = [];
201
+ // Check if next lines with more indent are list items
202
+ let j = i + 1;
203
+ while (j < lines.length) {
204
+ const nextLine = lines[j];
205
+ const nextTrimmed = nextLine.trim();
206
+ if (!nextTrimmed || nextTrimmed.startsWith("#")) {
207
+ j++;
208
+ continue;
209
+ }
210
+ const nextIndent = getIndent(nextLine);
211
+ if (nextIndent <= indent)
212
+ break;
213
+ if (nextTrimmed.startsWith("- ")) {
214
+ // New list item
215
+ const itemObj = {};
216
+ current[key].push(itemObj);
217
+ sectionStack.push({ name: key, indent: nextIndent, obj: itemObj });
218
+ // Parse the item content after "- "
219
+ const itemContent = nextTrimmed.substring(2);
220
+ if (itemContent.includes(":")) {
221
+ const [itemKey, itemVal] = itemContent.split(":", 2);
222
+ if (itemKey.trim() === "pattern") {
223
+ itemObj.pattern = itemVal.trim().replace(/^["']|["']$/g, "");
224
+ }
225
+ else if (itemKey.trim() === "kind") {
226
+ itemObj.kind = itemVal.trim();
227
+ }
228
+ }
229
+ else if (itemContent) {
230
+ // Assume it's a pattern
231
+ itemObj.pattern = itemContent.replace(/^["']|["']$/g, "");
232
+ }
233
+ }
234
+ j++;
235
+ }
236
+ }
237
+ }
238
+ }
239
+ // Flush remaining multiline buffer
240
+ if (multilineKey && multilineBuffer.length > 0) {
241
+ const value = multilineBuffer.join("\n");
242
+ const current = getCurrentObj();
243
+ if (multilineKey === "pattern" && current) {
244
+ current.pattern = value;
245
+ }
246
+ else if (multilineKey === "message") {
247
+ rule.message = value;
248
+ }
249
+ }
250
+ return rule.id ? rule : null;
251
+ }
252
+ /**
253
+ * Check if a rule uses structured conditions (has/any/all/not/regex)
254
+ */
255
+ function isStructuredRule(rule) {
256
+ if (!rule.rule)
257
+ return false;
258
+ return !!(rule.rule.has || rule.rule.any || rule.rule.all || rule.rule.not || rule.rule.regex);
259
+ }
260
+ /**
261
+ * Execute a structured rule using manual AST traversal
262
+ */
263
+ function executeStructuredRule(rootNode, condition, matches = []) {
264
+ // Start with finding nodes by kind or pattern
265
+ let candidates = [];
266
+ if (condition.pattern) {
267
+ // Use pattern matching via findAll
268
+ try {
269
+ candidates = rootNode.findAll(condition.pattern);
270
+ }
271
+ catch {
272
+ return matches;
273
+ }
274
+ }
275
+ else if (condition.kind) {
276
+ // Manual traversal for kind matching
277
+ function findByKind(node, kind) {
278
+ const results = [];
279
+ if (node.kind() === kind) {
280
+ results.push(node);
281
+ }
282
+ for (const child of node.children()) {
283
+ results.push(...findByKind(child, kind));
284
+ }
285
+ return results;
286
+ }
287
+ candidates = findByKind(rootNode, condition.kind);
288
+ }
289
+ else {
290
+ // No kind or pattern, search all nodes
291
+ function getAllNodes(node) {
292
+ const results = [node];
293
+ for (const child of node.children()) {
294
+ results.push(...getAllNodes(child));
295
+ }
296
+ return results;
297
+ }
298
+ candidates = getAllNodes(rootNode);
299
+ }
300
+ // Filter candidates by conditions
301
+ for (const candidate of candidates) {
302
+ let matchesCondition = true;
303
+ // Check 'has' condition
304
+ if (condition.has && matchesCondition) {
305
+ const subMatches = executeStructuredRule(candidate, condition.has, []);
306
+ if (subMatches.length === 0)
307
+ matchesCondition = false;
308
+ }
309
+ // Check 'not' condition
310
+ if (condition.not && matchesCondition) {
311
+ const subMatches = executeStructuredRule(candidate, condition.not, []);
312
+ if (subMatches.length > 0)
313
+ matchesCondition = false;
314
+ }
315
+ // Check 'any' condition (at least one must match)
316
+ if (condition.any && matchesCondition) {
317
+ let anyMatches = false;
318
+ for (const subCondition of condition.any) {
319
+ const subMatches = executeStructuredRule(candidate, subCondition, []);
320
+ if (subMatches.length > 0) {
321
+ anyMatches = true;
322
+ break;
323
+ }
324
+ }
325
+ if (!anyMatches)
326
+ matchesCondition = false;
327
+ }
328
+ // Check 'all' condition (all must match)
329
+ if (condition.all && matchesCondition) {
330
+ for (const subCondition of condition.all) {
331
+ const subMatches = executeStructuredRule(candidate, subCondition, []);
332
+ if (subMatches.length === 0) {
333
+ matchesCondition = false;
334
+ break;
335
+ }
336
+ }
337
+ }
338
+ // Check 'regex' condition
339
+ if (condition.regex && matchesCondition) {
340
+ const text = candidate.text();
341
+ const regex = new RegExp(condition.regex);
342
+ if (!regex.test(text))
343
+ matchesCondition = false;
344
+ }
345
+ if (matchesCondition) {
346
+ matches.push(candidate);
347
+ }
348
+ }
349
+ return matches;
350
+ }
351
+ const astGrepNapiRunner = {
352
+ id: "ast-grep-napi",
353
+ appliesTo: ["jsts"], // TypeScript/JavaScript only
354
+ priority: 15, // Run early (after type checkers, before other linters)
355
+ enabledByDefault: true,
356
+ skipTestFiles: true,
357
+ async run(ctx) {
358
+ const startTime = Date.now();
359
+ if (!canHandle(ctx.filePath)) {
360
+ return { status: "skipped", diagnostics: [], semantic: "none" };
361
+ }
362
+ const sgModule = await loadSg();
363
+ if (!sgModule) {
364
+ return { status: "skipped", diagnostics: [], semantic: "none" };
365
+ }
366
+ if (!fs.existsSync(ctx.filePath)) {
367
+ return { status: "skipped", diagnostics: [], semantic: "none" };
368
+ }
369
+ const lang = getLang(ctx.filePath, sgModule);
370
+ if (!lang) {
371
+ return { status: "skipped", diagnostics: [], semantic: "none" };
372
+ }
373
+ const content = fs.readFileSync(ctx.filePath, "utf-8");
374
+ let root;
375
+ try {
376
+ root = sgModule.parse(lang, content);
377
+ }
378
+ catch {
379
+ return { status: "skipped", diagnostics: [], semantic: "none" };
380
+ }
381
+ const diagnostics = [];
382
+ const rootNode = root.root();
383
+ // Load rules from ts-slop-rules only (complementary to ast-grep CLI)
384
+ const ruleDirs = [
385
+ path.join(process.cwd(), "rules/ts-slop-rules/rules"),
386
+ ];
387
+ for (const ruleDir of ruleDirs) {
388
+ const rules = loadYamlRules(ruleDir);
389
+ for (const rule of rules) {
390
+ // Skip rules for different languages (case-insensitive)
391
+ const lang = rule.language?.toLowerCase();
392
+ if (lang && lang !== "typescript" && lang !== "javascript") {
393
+ continue;
394
+ }
395
+ try {
396
+ let matches = [];
397
+ if (isStructuredRule(rule) && rule.rule) {
398
+ // Use structured rule execution
399
+ matches = executeStructuredRule(rootNode, rule.rule, []);
400
+ }
401
+ else if (rule.rule?.pattern || rule.rule?.kind) {
402
+ // Use simple pattern matching
403
+ const pattern = rule.rule.pattern || rule.rule.kind;
404
+ if (pattern) {
405
+ try {
406
+ matches = rootNode.findAll(pattern);
407
+ }
408
+ catch {
409
+ // Pattern failed, try manual traversal for kind
410
+ if (rule.rule.kind) {
411
+ function findByKind(node, kind) {
412
+ const results = [];
413
+ if (node.kind() === kind)
414
+ results.push(node);
415
+ for (const child of node.children()) {
416
+ results.push(...findByKind(child, kind));
417
+ }
418
+ return results;
419
+ }
420
+ matches = findByKind(rootNode, rule.rule.kind);
421
+ }
422
+ }
423
+ }
424
+ }
425
+ for (const match of matches) {
426
+ const range = match.range();
427
+ const weight = rule.metadata?.weight || 3;
428
+ const severity = weight >= 4 ? "error" : "warning";
429
+ diagnostics.push({
430
+ id: `ast-grep-napi-${range.start.line}-${rule.id}`,
431
+ message: `[${rule.metadata?.category || "slop"}] ${rule.message || rule.id}`,
432
+ filePath: ctx.filePath,
433
+ line: range.start.line + 1,
434
+ column: range.start.column + 1,
435
+ severity,
436
+ semantic: severity === "error" ? "blocking" : "warning",
437
+ tool: "ast-grep-napi",
438
+ rule: rule.id,
439
+ fixable: false,
440
+ });
441
+ }
442
+ }
443
+ catch {
444
+ // Rule failed, skip
445
+ }
446
+ }
447
+ }
448
+ const elapsed = Date.now() - startTime;
449
+ if (diagnostics.length > 0 || elapsed > 50) {
450
+ console.error(`[ast-grep-napi] ${ctx.filePath}: ${elapsed}ms, ${diagnostics.length} issues`);
451
+ }
452
+ if (diagnostics.length === 0) {
453
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
454
+ }
455
+ return {
456
+ status: "failed",
457
+ diagnostics,
458
+ semantic: "warning",
459
+ };
460
+ },
461
+ };
462
+ export default astGrepNapiRunner;