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,93 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatDiagnostic } from "./utils/format-utils.js";
3
+
4
+ describe("formatDiagnostic inline output verification", () => {
5
+ it("should display complete architect messages (NOT truncated to 'No ')", () => {
6
+ // Simulate actual architect diagnostic
7
+ const diagnostic = {
8
+ id: "architect-1",
9
+ message:
10
+ "No absolute Windows paths — breaks CI and cross-platform builds.",
11
+ filePath: "/test.ts",
12
+ line: 5,
13
+ severity: "warning" as const,
14
+ semantic: "warning" as const,
15
+ tool: "architect",
16
+ rule: "no-absolute-windows-paths",
17
+ };
18
+
19
+ const output = formatDiagnostic(diagnostic);
20
+
21
+ console.log("\n=== Architect Message Output ===");
22
+ console.log(output);
23
+ console.log("=================================\n");
24
+
25
+ // Verify complete message is shown
26
+ expect(output).toBe(
27
+ " L5: No absolute Windows paths — breaks CI and cross-platform builds.",
28
+ );
29
+ });
30
+
31
+ it("should display code fix messages inline correctly", () => {
32
+ // This is what I actually see from ts-lsp runner
33
+ const diagnostic = {
34
+ id: "ts-12-2345",
35
+ message:
36
+ "Property 'debug' is missing in type 'Config'\n💡 Quick fix: Add missing property 'debug'",
37
+ filePath: "/src/config.ts",
38
+ line: 12,
39
+ severity: "error" as const,
40
+ semantic: "blocking" as const,
41
+ tool: "ts-lsp",
42
+ rule: "TS2345",
43
+ fixable: true,
44
+ fixSuggestion: "Add missing property 'debug'",
45
+ };
46
+
47
+ const output = formatDiagnostic(diagnostic);
48
+
49
+ console.log("\n=== Code Fix Message Output ===");
50
+ console.log(output);
51
+ console.log("================================\n");
52
+
53
+ // Both lines should be properly indented
54
+ expect(output).toBe(
55
+ " L12: Property 'debug' is missing in type 'Config'\n 💡 Quick fix: Add missing property 'debug'",
56
+ );
57
+ });
58
+
59
+ it("should prove architect 'No ' messages are complete (not noise)", () => {
60
+ // All the "No " messages from default-architect.yaml
61
+ const testMessages = [
62
+ "No absolute Windows paths — breaks CI and cross-platform builds.",
63
+ "No hardcoded localhost URLs — use environment variables or a config service.",
64
+ "No empty catch/except blocks. Swallowing errors makes debugging impossible — at least log the error.",
65
+ "No hardcoded secrets — use environment variables or a secrets manager.",
66
+ "No 'any' types — use 'unknown' or define a proper interface to maintain type safety.",
67
+ ];
68
+
69
+ for (let i = 0; i < testMessages.length; i++) {
70
+ const diagnostic = {
71
+ id: `architect-${i}`,
72
+ message: testMessages[i],
73
+ filePath: "/test.ts",
74
+ line: i + 1,
75
+ severity: "warning" as const,
76
+ semantic: "warning" as const,
77
+ tool: "architect",
78
+ rule: "test",
79
+ };
80
+
81
+ const output = formatDiagnostic(diagnostic);
82
+
83
+ // Each message should be complete, NOT truncated to just "No "
84
+ expect(output.length).toBeGreaterThan(15); // More than " L1: No "
85
+ expect(output).toContain("No ");
86
+ expect(output).toContain("—"); // Contains the em-dash explanation
87
+
88
+ // Verify it's not truncated
89
+ const messageAfterNo = output.split("No ")[1];
90
+ expect(messageAfterNo.length).toBeGreaterThan(5);
91
+ }
92
+ });
93
+ });
@@ -14,6 +14,9 @@
14
14
  * - BaselineStore: Track pre-existing issues for delta mode
15
15
  */
16
16
  import { detectFileKind } from "../file-kinds.js";
17
+ import { isTestFile } from "../file-utils.js";
18
+ import { safeSpawn } from "../safe-spawn.js";
19
+ import { formatDiagnostics } from "./utils/format-utils.js";
17
20
  // --- In-Memory Baseline Store ---
18
21
  export function createBaselineStore() {
19
22
  const baselines = new Map();
@@ -45,10 +48,10 @@ export function getRunnersForKind(kind, filePath) {
45
48
  if (!kind)
46
49
  return [];
47
50
  const runners = [];
48
- const isTestFile = filePath ? isTest(filePath) : false;
51
+ const isTest = filePath ? isTestFile(filePath) : false;
49
52
  for (const runner of globalRegistry.values()) {
50
53
  // Skip runners that shouldn't run on test files
51
- if (isTestFile && runner.skipTestFiles)
54
+ if (isTest && runner.skipTestFiles)
52
55
  continue;
53
56
  if (runner.appliesTo.includes(kind) || runner.appliesTo.length === 0) {
54
57
  runners.push(runner);
@@ -56,19 +59,15 @@ export function getRunnersForKind(kind, filePath) {
56
59
  }
57
60
  return runners.sort((a, b) => a.priority - b.priority);
58
61
  }
59
- function isTest(filePath) {
60
- const normalized = filePath.replace(/\\/g, "/");
61
- return (normalized.includes(".test.") ||
62
- normalized.includes(".spec.") ||
63
- normalized.includes("/test/") ||
64
- normalized.includes("/tests/") ||
65
- normalized.includes("__tests__/") ||
66
- normalized.includes("test-utils") ||
67
- normalized.startsWith("test-"));
68
- }
69
62
  export function listRunners() {
70
63
  return Array.from(globalRegistry.values());
71
64
  }
65
+ /**
66
+ * Clear all registered runners. Used primarily for testing.
67
+ */
68
+ export function clearRunnerRegistry() {
69
+ globalRegistry.clear();
70
+ }
72
71
  // --- Tool Availability Cache ---
73
72
  const toolCache = new Map();
74
73
  function checkToolAvailability(command) {
@@ -76,11 +75,8 @@ function checkToolAvailability(command) {
76
75
  return toolCache.get(command);
77
76
  }
78
77
  try {
79
- const { spawnSync } = require("node:child_process");
80
- const result = spawnSync(command, ["--version"], {
81
- encoding: "utf-8",
78
+ const result = safeSpawn(command, ["--version"], {
82
79
  timeout: 5000,
83
- shell: true,
84
80
  });
85
81
  const available = result.status === 0;
86
82
  toolCache.set(command, available);
@@ -121,41 +117,6 @@ function filterDelta(after, before, keyFn) {
121
117
  const newItems = after.filter((d) => !beforeSet.has(keyFn(d)));
122
118
  return { new: newItems, fixed };
123
119
  }
124
- // --- Output Formatting ---
125
- const EMOJI = {
126
- blocking: "🔴",
127
- warning: "🟡",
128
- fixed: "✅",
129
- info: "ℹ️",
130
- silent: "📊",
131
- none: "",
132
- };
133
- function formatDiagnostic(d) {
134
- const line = d.line ? `L${d.line}: ` : "";
135
- return ` ${line}${d.message}`;
136
- }
137
- function formatDiagnostics(diagnostics, semantic, maxDisplay = 10) {
138
- if (diagnostics.length === 0)
139
- return "";
140
- const emoji = EMOJI[semantic] ?? EMOJI.warning;
141
- let output = "";
142
- if (semantic === "blocking") {
143
- output += `\n${emoji} STOP — ${diagnostics.length} issue(s) must be fixed:\n`;
144
- }
145
- else if (semantic === "warning") {
146
- output += `\n${emoji} ${diagnostics.length} warning(s):\n`;
147
- }
148
- else if (semantic === "fixed") {
149
- output += `\n${emoji} Auto-fixed ${diagnostics.length} issue(s):\n`;
150
- }
151
- for (const d of diagnostics.slice(0, maxDisplay)) {
152
- output += `${formatDiagnostic(d)}\n`;
153
- }
154
- if (diagnostics.length > maxDisplay) {
155
- output += ` ... and ${diagnostics.length - maxDisplay} more\n`;
156
- }
157
- return output;
158
- }
159
120
  // --- Main Dispatch Function ---
160
121
  export async function dispatchForFile(ctx, groups) {
161
122
  const allDiagnostics = [];
@@ -195,8 +156,13 @@ export async function dispatchForFile(ctx, groups) {
195
156
  ctx.baselines.set(ctx.filePath, [...allDiagnostics, ...diagnostics]);
196
157
  }
197
158
  allDiagnostics.push(...diagnostics);
198
- // Check for blockers
199
- if (semantic === "blocking" && diagnostics.length > 0) {
159
+ // Check for blockers - use result semantic (not group default) and check individual diagnostics
160
+ const resultSemantic = result.semantic ?? semantic;
161
+ if (resultSemantic === "blocking" && diagnostics.length > 0) {
162
+ stopped = true;
163
+ }
164
+ // Also check if any individual diagnostic is blocking
165
+ if (diagnostics.some((d) => d.semantic === "blocking")) {
200
166
  stopped = true;
201
167
  }
202
168
  }
@@ -16,7 +16,8 @@
16
16
 
17
17
  import type { FileKind } from "../file-kinds.js";
18
18
  import { detectFileKind } from "../file-kinds.js";
19
-
19
+ import { isTestFile } from "../file-utils.js";
20
+ import { safeSpawn } from "../safe-spawn.js";
20
21
  import type {
21
22
  BaselineStore,
22
23
  Diagnostic,
@@ -28,6 +29,7 @@ import type {
28
29
  RunnerGroup,
29
30
  RunnerResult,
30
31
  } from "./types.js";
32
+ import { formatDiagnostics } from "./utils/format-utils.js";
31
33
 
32
34
  // --- In-Memory Baseline Store ---
33
35
 
@@ -69,11 +71,11 @@ export function getRunnersForKind(
69
71
  ): RunnerDefinition[] {
70
72
  if (!kind) return [];
71
73
  const runners: RunnerDefinition[] = [];
72
- const isTestFile = filePath ? isTest(filePath) : false;
74
+ const isTest = filePath ? isTestFile(filePath) : false;
73
75
 
74
76
  for (const runner of globalRegistry.values()) {
75
77
  // Skip runners that shouldn't run on test files
76
- if (isTestFile && runner.skipTestFiles) continue;
78
+ if (isTest && runner.skipTestFiles) continue;
77
79
 
78
80
  if (runner.appliesTo.includes(kind) || runner.appliesTo.length === 0) {
79
81
  runners.push(runner);
@@ -82,23 +84,17 @@ export function getRunnersForKind(
82
84
  return runners.sort((a, b) => a.priority - b.priority);
83
85
  }
84
86
 
85
- function isTest(filePath: string): boolean {
86
- const normalized = filePath.replace(/\\/g, "/");
87
- return (
88
- normalized.includes(".test.") ||
89
- normalized.includes(".spec.") ||
90
- normalized.includes("/test/") ||
91
- normalized.includes("/tests/") ||
92
- normalized.includes("__tests__/") ||
93
- normalized.includes("test-utils") ||
94
- normalized.startsWith("test-")
95
- );
96
- }
97
-
98
87
  export function listRunners(): RunnerDefinition[] {
99
88
  return Array.from(globalRegistry.values());
100
89
  }
101
90
 
91
+ /**
92
+ * Clear all registered runners. Used primarily for testing.
93
+ */
94
+ export function clearRunnerRegistry(): void {
95
+ globalRegistry.clear();
96
+ }
97
+
102
98
  // --- Tool Availability Cache ---
103
99
 
104
100
  const toolCache = new Map<string, boolean>();
@@ -108,11 +104,8 @@ function checkToolAvailability(command: string): boolean {
108
104
  return toolCache.get(command)!;
109
105
  }
110
106
  try {
111
- const { spawnSync } = require("node:child_process");
112
- const result = spawnSync(command, ["--version"], {
113
- encoding: "utf-8",
107
+ const result = safeSpawn(command, ["--version"], {
114
108
  timeout: 5000,
115
- shell: true,
116
109
  });
117
110
  const available = result.status === 0;
118
111
  toolCache.set(command, available);
@@ -171,51 +164,6 @@ function filterDelta<T extends { id: string }>(
171
164
  return { new: newItems, fixed };
172
165
  }
173
166
 
174
- // --- Output Formatting ---
175
-
176
- const EMOJI: Record<string, string> = {
177
- blocking: "🔴",
178
- warning: "🟡",
179
- fixed: "✅",
180
- info: "ℹ️",
181
- silent: "📊",
182
- none: "",
183
- };
184
-
185
- function formatDiagnostic(d: Diagnostic): string {
186
- const line = d.line ? `L${d.line}: ` : "";
187
- return ` ${line}${d.message}`;
188
- }
189
-
190
- function formatDiagnostics(
191
- diagnostics: Diagnostic[],
192
- semantic: OutputSemantic,
193
- maxDisplay = 10,
194
- ): string {
195
- if (diagnostics.length === 0) return "";
196
-
197
- const emoji = EMOJI[semantic] ?? EMOJI.warning;
198
- let output = "";
199
-
200
- if (semantic === "blocking") {
201
- output += `\n${emoji} STOP — ${diagnostics.length} issue(s) must be fixed:\n`;
202
- } else if (semantic === "warning") {
203
- output += `\n${emoji} ${diagnostics.length} warning(s):\n`;
204
- } else if (semantic === "fixed") {
205
- output += `\n${emoji} Auto-fixed ${diagnostics.length} issue(s):\n`;
206
- }
207
-
208
- for (const d of diagnostics.slice(0, maxDisplay)) {
209
- output += `${formatDiagnostic(d)}\n`;
210
- }
211
-
212
- if (diagnostics.length > maxDisplay) {
213
- output += ` ... and ${diagnostics.length - maxDisplay} more\n`;
214
- }
215
-
216
- return output;
217
- }
218
-
219
167
  // --- Main Dispatch Function ---
220
168
 
221
169
  export async function dispatchForFile(
@@ -271,8 +219,13 @@ export async function dispatchForFile(
271
219
 
272
220
  allDiagnostics.push(...diagnostics);
273
221
 
274
- // Check for blockers
275
- if (semantic === "blocking" && diagnostics.length > 0) {
222
+ // Check for blockers - use result semantic (not group default) and check individual diagnostics
223
+ const resultSemantic = result.semantic ?? semantic;
224
+ if (resultSemantic === "blocking" && diagnostics.length > 0) {
225
+ stopped = true;
226
+ }
227
+ // Also check if any individual diagnostic is blocking
228
+ if (diagnostics.some((d) => d.semantic === "blocking")) {
276
229
  stopped = true;
277
230
  }
278
231
  }
@@ -21,11 +21,13 @@ export const TOOL_PLANS = {
21
21
  groups: [
22
22
  // TypeScript LSP always runs first - blocks on errors
23
23
  { mode: "all", runnerIds: ["ts-lsp"], filterKinds: ["jsts"] },
24
- // Then biome for fast linting
25
- { mode: "fallback", runnerIds: ["biome-lint"] },
24
+ // Then biome or oxlint for fast linting (user preference)
25
+ { mode: "fallback", runnerIds: ["biome-lint", "oxlint"] },
26
+ // Fast structural analysis via NAPI (weight >= 4 is blocking)
27
+ { mode: "all", runnerIds: ["ast-grep-napi"] },
26
28
  // Type safety checks
27
29
  { mode: "fallback", runnerIds: ["type-safety"] },
28
- // Structural analysis
30
+ // Comprehensive structural analysis via CLI (severity: error is blocking)
29
31
  { mode: "fallback", runnerIds: ["ast-grep"] },
30
32
  // Architectural rules
31
33
  { mode: "fallback", runnerIds: ["architect"] },
@@ -93,7 +95,8 @@ export const TOOL_PLANS = {
93
95
  markdown: {
94
96
  name: "Markdown Processing",
95
97
  groups: [
96
- // No specific linting for markdown yet
98
+ // Spellcheck for typos
99
+ { mode: "fallback", runnerIds: ["spellcheck"] },
97
100
  ],
98
101
  },
99
102
  /**
@@ -102,6 +105,8 @@ export const TOOL_PLANS = {
102
105
  shell: {
103
106
  name: "Shell Script Linting",
104
107
  groups: [
108
+ // Shellcheck for bash/sh/zsh linting
109
+ { mode: "fallback", runnerIds: ["shellcheck"] },
105
110
  // Architectural rules
106
111
  { mode: "fallback", runnerIds: ["architect"] },
107
112
  ],
@@ -25,11 +25,13 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
25
25
  groups: [
26
26
  // TypeScript LSP always runs first - blocks on errors
27
27
  { mode: "all", runnerIds: ["ts-lsp"], filterKinds: ["jsts"] },
28
- // Then biome for fast linting
29
- { mode: "fallback", runnerIds: ["biome-lint"] },
28
+ // Then biome or oxlint for fast linting (user preference)
29
+ { mode: "fallback", runnerIds: ["biome-lint", "oxlint"] },
30
+ // Fast structural analysis via NAPI (weight >= 4 is blocking)
31
+ { mode: "all", runnerIds: ["ast-grep-napi"] },
30
32
  // Type safety checks
31
33
  { mode: "fallback", runnerIds: ["type-safety"] },
32
- // Structural analysis
34
+ // Comprehensive structural analysis via CLI (severity: error is blocking)
33
35
  { mode: "fallback", runnerIds: ["ast-grep"] },
34
36
  // Architectural rules
35
37
  { mode: "fallback", runnerIds: ["architect"] },
@@ -103,7 +105,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
103
105
  markdown: {
104
106
  name: "Markdown Processing",
105
107
  groups: [
106
- // No specific linting for markdown yet
108
+ // Spellcheck for typos
109
+ { mode: "fallback", runnerIds: ["spellcheck"] },
107
110
  ],
108
111
  },
109
112
 
@@ -113,6 +116,8 @@ export const TOOL_PLANS: Record<string, ToolPlan> = {
113
116
  shell: {
114
117
  name: "Shell Script Linting",
115
118
  groups: [
119
+ // Shellcheck for bash/sh/zsh linting
120
+ { mode: "fallback", runnerIds: ["shellcheck"] },
116
121
  // Architectural rules
117
122
  { mode: "fallback", runnerIds: ["architect"] },
118
123
  ],
@@ -15,6 +15,7 @@ const architectRunner = {
15
15
  appliesTo: ["jsts", "python", "go", "rust", "cxx", "shell", "cmake"],
16
16
  priority: 40,
17
17
  enabledByDefault: true,
18
+ skipTestFiles: true, // Skip test files - rules can be noisy there
18
19
  async run(ctx) {
19
20
  const relPath = ctx.filePath.replace(ctx.cwd, "").replace(/\\/g, "/");
20
21
  const content = readFileContent(ctx.filePath);
@@ -30,15 +31,28 @@ const architectRunner = {
30
31
  // Check for violations
31
32
  const violations = architectClient.checkFile(relPath, content);
32
33
  for (const v of violations) {
34
+ // Build message with inline fix guidance
35
+ let message = v.message;
36
+ let fixSuggestion = v.fix;
37
+ if (v.fix) {
38
+ const fixPreview = v.fix.length > 60 ? `${v.fix.substring(0, 60)}...` : v.fix;
39
+ message += `\n💡 Suggested fix: ${fixPreview}`;
40
+ }
41
+ else if (v.note) {
42
+ const notePreview = v.note.length > 80 ? `${v.note.substring(0, 80)}...` : v.note;
43
+ message += `\n📝 ${notePreview}`;
44
+ }
33
45
  diagnostics.push({
34
46
  id: `architect-${v.line || 0}-${v.pattern}`,
35
- message: v.message,
47
+ message,
36
48
  filePath: ctx.filePath,
37
49
  line: v.line,
38
- severity: "error",
39
- semantic: "blocking",
50
+ severity: "warning",
51
+ semantic: "warning",
40
52
  tool: "architect",
41
53
  rule: v.pattern,
54
+ fixable: !!v.fix,
55
+ fixSuggestion,
42
56
  });
43
57
  }
44
58
  // Check file size limit
@@ -49,8 +63,8 @@ const architectRunner = {
49
63
  id: `architect-size-${lineCount}`,
50
64
  message: sizeViolation.message,
51
65
  filePath: ctx.filePath,
52
- severity: "error",
53
- semantic: "blocking",
66
+ severity: "warning",
67
+ semantic: "warning",
54
68
  tool: "architect",
55
69
  rule: "file-size-limit",
56
70
  fixSuggestion: "Split into smaller modules",
@@ -60,9 +74,9 @@ const architectRunner = {
60
74
  return { status: "succeeded", diagnostics: [], semantic: "none" };
61
75
  }
62
76
  return {
63
- status: "failed",
77
+ status: "succeeded", // Warnings don't fail the run
64
78
  diagnostics,
65
- semantic: "blocking",
79
+ semantic: "warning",
66
80
  };
67
81
  },
68
82
  };
@@ -0,0 +1,138 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it, beforeAll, afterAll } from "vitest";
4
+ function createMockContext(filePath, kind = "jsts", cwd) {
5
+ return {
6
+ filePath,
7
+ cwd: cwd || process.cwd(),
8
+ kind,
9
+ autofix: false,
10
+ deltaMode: false,
11
+ baselines: { get: () => undefined, set: () => { }, clear: () => { } },
12
+ pi: { getFlag: () => false },
13
+ hasTool: async () => false,
14
+ log: () => { },
15
+ };
16
+ }
17
+ describe("architect runner", () => {
18
+ const testDir = path.join(process.env.TEMP || "/tmp", `architect_test_${Date.now()}`);
19
+ const configPath = path.join(testDir, ".pi-lens", "architect.yaml");
20
+ beforeAll(() => {
21
+ // Create test config
22
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
23
+ fs.writeFileSync(configPath, `version: "1.0"
24
+ rules:
25
+ - pattern: "**/*.ts"
26
+ max_lines: 50
27
+ must_not:
28
+ - pattern: 'hardcoded_secret_12345'
29
+ message: "No hardcoded secrets"
30
+ fix: "Use process.env.SECRET"
31
+ - pattern: 'console\.log'
32
+ message: "No console.log in production"
33
+ `);
34
+ });
35
+ afterAll(() => {
36
+ try {
37
+ if (fs.existsSync(testDir)) {
38
+ fs.rmSync(testDir, { recursive: true, force: true });
39
+ }
40
+ }
41
+ catch {
42
+ // Ignore cleanup errors
43
+ }
44
+ });
45
+ it("should load default config when no user config exists", async () => {
46
+ const module = await import("./architect.js");
47
+ const runner = module.default;
48
+ // Use a unique temp dir with no user config (will fall back to default)
49
+ const noUserConfigDir = path.join(process.env.TEMP || "/tmp", `no_arch_user_config_${Date.now()}`);
50
+ fs.mkdirSync(noUserConfigDir, { recursive: true });
51
+ // Create a very large file that should trigger default max_lines rule
52
+ const tmpFile = path.join(noUserConfigDir, `large_${Date.now()}.ts`);
53
+ fs.writeFileSync(tmpFile, Array(5000).fill("// line").join("\n"));
54
+ try {
55
+ const result = await runner.run(createMockContext(tmpFile, "jsts", noUserConfigDir));
56
+ // Should use default config and find violations
57
+ expect(result.status).toBe("succeeded");
58
+ // Should have size violation from default config
59
+ expect(result.diagnostics.some((d) => d.message.includes("line limit"))).toBe(true);
60
+ }
61
+ finally {
62
+ try {
63
+ if (fs.existsSync(tmpFile))
64
+ fs.unlinkSync(tmpFile);
65
+ if (fs.existsSync(noUserConfigDir))
66
+ fs.rmdirSync(noUserConfigDir);
67
+ }
68
+ catch { }
69
+ }
70
+ });
71
+ it("should detect file size violations", async () => {
72
+ const module = await import("./architect.js");
73
+ const runner = module.default;
74
+ const tmpFile = path.join(testDir, `large_file_${Date.now()}.ts`);
75
+ // Create file with 100 lines (exceeds 50 line limit)
76
+ fs.writeFileSync(tmpFile, Array(100).fill("// line").join("\n"));
77
+ try {
78
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
79
+ expect(result.status).toBe("succeeded");
80
+ expect(result.diagnostics.length).toBeGreaterThan(0);
81
+ expect(result.diagnostics.some((d) => d.message.includes("50 line limit"))).toBe(true);
82
+ }
83
+ finally {
84
+ try {
85
+ if (fs.existsSync(tmpFile))
86
+ fs.unlinkSync(tmpFile);
87
+ }
88
+ catch { }
89
+ }
90
+ });
91
+ it("should detect pattern violations", async () => {
92
+ const module = await import("./architect.js");
93
+ const runner = module.default;
94
+ const tmpFile = path.join(testDir, `bad_patterns_${Date.now()}.ts`);
95
+ fs.writeFileSync(tmpFile, `const x = hardcoded_secret_12345;
96
+ console.log(x);
97
+ `);
98
+ try {
99
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
100
+ expect(result.status).toBe("succeeded");
101
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
102
+ expect(result.diagnostics.some((d) => d.message.includes("hardcoded"))).toBe(true);
103
+ expect(result.diagnostics.some((d) => d.message.includes("console.log"))).toBe(true);
104
+ }
105
+ finally {
106
+ try {
107
+ if (fs.existsSync(tmpFile))
108
+ fs.unlinkSync(tmpFile);
109
+ }
110
+ catch { }
111
+ }
112
+ });
113
+ it("should return no diagnostics for clean files", async () => {
114
+ const module = await import("./architect.js");
115
+ const runner = module.default;
116
+ const tmpFile = path.join(testDir, `clean_${Date.now()}.ts`);
117
+ // Small file (20 lines) with no violations
118
+ fs.writeFileSync(tmpFile, Array(20).fill("// clean code").join("\n"));
119
+ try {
120
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
121
+ expect(result.status).toBe("succeeded");
122
+ expect(result.diagnostics.length).toBe(0);
123
+ }
124
+ finally {
125
+ try {
126
+ if (fs.existsSync(tmpFile))
127
+ fs.unlinkSync(tmpFile);
128
+ }
129
+ catch { }
130
+ }
131
+ });
132
+ it("should skip test files", async () => {
133
+ const module = await import("./architect.js");
134
+ const runner = module.default;
135
+ // The runner should have skipTestFiles: true
136
+ expect(runner.skipTestFiles).toBe(true);
137
+ });
138
+ });