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,339 @@
1
+ /**
2
+ * Similarity Runner: Detect semantic code reuse opportunities
3
+ *
4
+ * Uses Amain's 57×72 state matrix algorithm to find similar functions.
5
+ * Integrated into dispatch flow as a warning (non-blocking) suggestion.
6
+ */
7
+
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import * as ts from "typescript";
11
+ import { EXCLUDED_DIRS } from "../../file-utils.js";
12
+ import {
13
+ buildProjectIndex,
14
+ findSimilarFunctions,
15
+ loadIndex,
16
+ type ProjectIndex,
17
+ } from "../../project-index.js";
18
+ import { buildStateMatrix, countTransitions } from "../../state-matrix.js";
19
+ import type {
20
+ Diagnostic,
21
+ DispatchContext,
22
+ RunnerDefinition,
23
+ RunnerResult,
24
+ } from "../types.js";
25
+
26
+ // ============================================================================
27
+ // Configuration
28
+ // ============================================================================
29
+
30
+ const CONFIG = {
31
+ SIMILARITY_THRESHOLD: 0.75, // 75% minimum similarity
32
+ MIN_TRANSITIONS: 20, // Skip functions with <20 AST transitions
33
+ MAX_SUGGESTIONS: 3, // Max 3 suggestions per file
34
+ USAGE_THRESHOLD: 2, // Only suggest utilities with 2+ uses (placeholder)
35
+ };
36
+
37
+ // ============================================================================
38
+ // Runner Implementation
39
+ // ============================================================================
40
+
41
+ const similarityRunner: RunnerDefinition = {
42
+ id: "similarity",
43
+ appliesTo: ["jsts"], // TypeScript/JavaScript only for MVP
44
+ priority: 35, // After ts-lsp, before ast-grep
45
+ enabledByDefault: true,
46
+
47
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
48
+ const { filePath } = ctx;
49
+
50
+ // Only check TypeScript files
51
+ if (!filePath.match(/\.tsx?$/)) {
52
+ return { status: "skipped", diagnostics: [], semantic: "none" };
53
+ }
54
+
55
+ // Load file content
56
+ const content = await fs.readFile(filePath, "utf-8").catch(() => null);
57
+ if (!content) {
58
+ return { status: "skipped", diagnostics: [], semantic: "none" };
59
+ }
60
+
61
+ // Find project root and load index
62
+ const projectRoot = await findProjectRoot(filePath);
63
+ if (!projectRoot) {
64
+ return { status: "skipped", diagnostics: [], semantic: "none" };
65
+ }
66
+
67
+ const index = await loadOrBuildIndex(projectRoot);
68
+ if (!index || index.entries.size === 0) {
69
+ return { status: "skipped", diagnostics: [], semantic: "none" };
70
+ }
71
+
72
+ // Parse the file
73
+ const sourceFile = ts.createSourceFile(
74
+ filePath,
75
+ content,
76
+ ts.ScriptTarget.Latest,
77
+ true,
78
+ ts.ScriptKind.TS,
79
+ );
80
+
81
+ // Extract functions and check for similarities
82
+ const newFunctions = extractFunctions(sourceFile, content);
83
+
84
+ const diagnostics: Diagnostic[] = [];
85
+
86
+ for (const func of newFunctions) {
87
+ // Guardrail: Skip tiny functions
88
+ if (func.transitionCount < CONFIG.MIN_TRANSITIONS) {
89
+ continue;
90
+ }
91
+
92
+ // Find similar functions in index
93
+ const matches = findSimilarFunctions(
94
+ func.matrix,
95
+ index,
96
+ CONFIG.SIMILARITY_THRESHOLD,
97
+ CONFIG.MAX_SUGGESTIONS,
98
+ );
99
+
100
+ // Create diagnostic for each match
101
+ for (const match of matches) {
102
+ // Skip if it's the same function (self-match by path/name)
103
+ if (
104
+ match.targetId ===
105
+ `${path.relative(projectRoot, filePath)}:${func.name}`
106
+ ) {
107
+ continue;
108
+ }
109
+
110
+ diagnostics.push({
111
+ id: `similarity-${func.name}-${match.targetId}`,
112
+ tool: "similarity",
113
+ filePath,
114
+ line: func.line,
115
+ column: func.column,
116
+ message: buildSuggestionMessage(func, match),
117
+ severity: "warning", // 🟡 Not blocking
118
+ semantic: "warning",
119
+ });
120
+ }
121
+ }
122
+
123
+ // Return limited number of suggestions
124
+ const limitedResults = diagnostics.slice(0, CONFIG.MAX_SUGGESTIONS);
125
+
126
+ if (limitedResults.length === 0) {
127
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
128
+ }
129
+
130
+ return {
131
+ status: "succeeded",
132
+ diagnostics: limitedResults,
133
+ semantic: "warning",
134
+ };
135
+ },
136
+ };
137
+
138
+ // ============================================================================
139
+ // Function Extraction
140
+ // ============================================================================
141
+
142
+ interface ExtractedFunction {
143
+ name: string;
144
+ line: number;
145
+ column: number;
146
+ matrix: number[][];
147
+ transitionCount: number;
148
+ signature: string;
149
+ }
150
+
151
+ function extractFunctions(
152
+ sourceFile: ts.SourceFile,
153
+ _fullContent: string,
154
+ ): ExtractedFunction[] {
155
+ const functions: ExtractedFunction[] = [];
156
+
157
+ function visit(node: ts.Node) {
158
+ // Function declarations
159
+ if (ts.isFunctionDeclaration(node) && node.name) {
160
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
161
+ node.getStart(sourceFile),
162
+ );
163
+ const funcCode = getNodeText(node, sourceFile);
164
+ const matrix = buildStateMatrix(funcCode);
165
+ const transitionCount = countTransitions(matrix);
166
+
167
+ functions.push({
168
+ name: node.name.text,
169
+ line: line + 1, // 1-indexed
170
+ column: character + 1, // 1-indexed
171
+ matrix,
172
+ transitionCount,
173
+ signature: getSignature(node),
174
+ });
175
+ }
176
+
177
+ // Arrow functions assigned to const
178
+ if (ts.isVariableStatement(node)) {
179
+ extractArrowFunctions(node, functions, sourceFile);
180
+ }
181
+
182
+ ts.forEachChild(node, visit);
183
+ }
184
+
185
+ visit(sourceFile);
186
+ return functions;
187
+ }
188
+
189
+ function extractArrowFunctions(
190
+ node: ts.VariableStatement,
191
+ functions: ExtractedFunction[],
192
+ sourceFile: ts.SourceFile,
193
+ ): void {
194
+ for (const decl of node.declarationList.declarations) {
195
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) {
196
+ continue;
197
+ }
198
+
199
+ const func = decl.initializer;
200
+ if (!ts.isArrowFunction(func) && !ts.isFunctionExpression(func)) {
201
+ continue;
202
+ }
203
+
204
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(
205
+ node.getStart(sourceFile),
206
+ );
207
+ const funcCode = getNodeText(func, sourceFile);
208
+ const matrix = buildStateMatrix(funcCode);
209
+ const transitionCount = countTransitions(matrix);
210
+
211
+ functions.push({
212
+ name: decl.name.text,
213
+ line: line + 1,
214
+ column: character + 1,
215
+ matrix,
216
+ transitionCount,
217
+ signature: getArrowSignature(func),
218
+ });
219
+ }
220
+ }
221
+
222
+ function getNodeText(node: ts.Node, sourceFile: ts.SourceFile): string {
223
+ return sourceFile.text.substring(node.getStart(sourceFile), node.getEnd());
224
+ }
225
+
226
+ function getSignature(node: ts.FunctionDeclaration): string {
227
+ const params = node.parameters
228
+ .map((p) => (ts.isIdentifier(p.name) ? p.name.text : "param"))
229
+ .join(", ");
230
+ return `(${params})`;
231
+ }
232
+
233
+ function getArrowSignature(
234
+ node: ts.ArrowFunction | ts.FunctionExpression,
235
+ ): string {
236
+ const params = node.parameters
237
+ .map((p) => (ts.isIdentifier(p.name) ? p.name.text : "param"))
238
+ .join(", ");
239
+ return `(${params})`;
240
+ }
241
+
242
+ // ============================================================================
243
+ // Message Building
244
+ // ============================================================================
245
+
246
+ function buildSuggestionMessage(
247
+ func: ExtractedFunction,
248
+ match: {
249
+ targetId: string;
250
+ targetName: string;
251
+ targetLocation: string;
252
+ similarity: number;
253
+ },
254
+ ): string {
255
+ const similarityPct = Math.round(match.similarity * 100);
256
+ const parts = match.targetId.split(":");
257
+ const file = parts[0];
258
+ const name = parts[1] || match.targetName;
259
+ const location = `${file}:1`; // TODO: get actual line
260
+
261
+ return `Function '${func.name}' has ${similarityPct}% similarity to existing utility '${name}()' in ${location}. Consider reusing the existing utility.`;
262
+ }
263
+
264
+ // ============================================================================
265
+ // Index Management
266
+ // ============================================================================
267
+
268
+ const indexCache = new Map<string, ProjectIndex>();
269
+
270
+ async function findProjectRoot(filePath: string): Promise<string | null> {
271
+ let dir = path.dirname(filePath);
272
+ while (dir !== path.dirname(dir)) {
273
+ try {
274
+ await fs.access(path.join(dir, "package.json"));
275
+ return dir;
276
+ } catch {
277
+ dir = path.dirname(dir);
278
+ }
279
+ }
280
+ return null;
281
+ }
282
+
283
+ async function loadOrBuildIndex(
284
+ projectRoot: string,
285
+ ): Promise<ProjectIndex | null> {
286
+ // Check cache
287
+ const cached = indexCache.get(projectRoot);
288
+ if (cached) {
289
+ return cached;
290
+ }
291
+
292
+ // Try to load existing index
293
+ const existing = await loadIndex(projectRoot);
294
+ if (existing) {
295
+ indexCache.set(projectRoot, existing);
296
+ return existing;
297
+ }
298
+
299
+ // Build new index
300
+ const { glob } = await import("glob");
301
+ // Build ignore patterns from centralized EXCLUDED_DIRS
302
+ const ignorePatterns = [
303
+ ...EXCLUDED_DIRS.map((d) => `**/${d}/**`),
304
+ "**/*.test.ts",
305
+ "**/*.spec.ts",
306
+ "**/*.poc.test.ts",
307
+ ];
308
+ const files = await glob("**/*.ts", {
309
+ cwd: projectRoot,
310
+ ignore: ignorePatterns,
311
+ });
312
+
313
+ if (files.length === 0) {
314
+ return null;
315
+ }
316
+
317
+ const absoluteFiles = files.map((f) => path.join(projectRoot, f));
318
+ const index = await buildProjectIndex(projectRoot, absoluteFiles);
319
+
320
+ indexCache.set(projectRoot, index);
321
+ return index;
322
+ }
323
+
324
+ // ============================================================================
325
+ // Testing Helper
326
+ // ============================================================================
327
+
328
+ export async function buildIndexForTesting(
329
+ projectRoot: string,
330
+ ): Promise<ProjectIndex> {
331
+ const index = await loadOrBuildIndex(projectRoot);
332
+ if (!index) {
333
+ throw new Error("Failed to build index");
334
+ }
335
+ return index;
336
+ }
337
+
338
+ export { CONFIG };
339
+ export default similarityRunner;
@@ -0,0 +1,106 @@
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
+ import { safeSpawn } from "../../safe-spawn.js";
22
+ import { createAvailabilityChecker } from "./utils/runner-helpers.js";
23
+ const typos = createAvailabilityChecker("typos", ".exe");
24
+ /**
25
+ * Parse typos-cli JSON output (JSON Lines format)
26
+ *
27
+ * Each line is a JSON object:
28
+ * {
29
+ * "path": "file.md",
30
+ * "line_num": 42,
31
+ * "byte_offset": 1234,
32
+ * "typo": "recieve",
33
+ * "corrections": ["receive"]
34
+ * }
35
+ */
36
+ function parseTyposOutput(raw, filePath) {
37
+ const diagnostics = [];
38
+ if (!raw.trim()) {
39
+ return diagnostics;
40
+ }
41
+ const lines = raw.trim().split("\n").filter((l) => l.trim());
42
+ for (const line of lines) {
43
+ try {
44
+ const parsed = JSON.parse(line);
45
+ if (!parsed.typo || !parsed.line_num)
46
+ continue;
47
+ const corrections = parsed.corrections?.join(", ") || "no suggestions";
48
+ const message = `Typo: "${parsed.typo}" → ${corrections}`;
49
+ diagnostics.push({
50
+ id: `typos-${parsed.line_num}-${parsed.typo}`,
51
+ message,
52
+ filePath,
53
+ line: parsed.line_num,
54
+ column: 1, // typos-cli doesn't provide column, just byte offset
55
+ severity: "warning",
56
+ semantic: "warning",
57
+ tool: "typos",
58
+ rule: "typo",
59
+ fixable: !!parsed.corrections?.length,
60
+ fixSuggestion: parsed.corrections?.[0],
61
+ });
62
+ }
63
+ catch {
64
+ // Skip invalid JSON lines
65
+ continue;
66
+ }
67
+ }
68
+ return diagnostics;
69
+ }
70
+ const spellcheckRunner = {
71
+ id: "spellcheck",
72
+ appliesTo: ["markdown"],
73
+ priority: 30, // Run after code quality checks (biome=10, slop=25)
74
+ enabledByDefault: true,
75
+ skipTestFiles: false, // Check docs in test files too
76
+ async run(ctx) {
77
+ // Skip if typos-cli is not installed
78
+ if (!typos.isAvailable(ctx.cwd || process.cwd())) {
79
+ return { status: "skipped", diagnostics: [], semantic: "none" };
80
+ }
81
+ // Run typos-cli with JSON output
82
+ // --format json: Output JSON Lines
83
+ // --exclude <pattern>: Could be used to exclude code blocks if needed
84
+ const args = ["--format", "json", ctx.filePath];
85
+ const result = safeSpawn(typos.getCommand(), args, {
86
+ timeout: 15000,
87
+ });
88
+ // typos-cli exits with code 2 if typos found, 0 if clean
89
+ const hasTypos = result.status === 2 || result.stdout?.trim();
90
+ if (!hasTypos) {
91
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
92
+ }
93
+ // Parse diagnostics
94
+ const raw = result.stdout + result.stderr;
95
+ const diagnostics = parseTyposOutput(raw, ctx.filePath);
96
+ if (diagnostics.length === 0) {
97
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
98
+ }
99
+ return {
100
+ status: "failed",
101
+ diagnostics,
102
+ semantic: "warning",
103
+ };
104
+ },
105
+ };
106
+ export default spellcheckRunner;
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Tests for spellcheck runner (typos-cli)
3
+ */
4
+ import * as fs from "node:fs";
5
+ import { createRequire } from "node:module";
6
+ import * as path from "node:path";
7
+ import { describe, expect, it } from "vitest";
8
+ function createMockContext(filePath) {
9
+ return {
10
+ filePath,
11
+ cwd: process.cwd(),
12
+ kind: "markdown",
13
+ autofix: false,
14
+ deltaMode: false,
15
+ baselines: { get: () => [], add: () => { }, save: () => { } },
16
+ pi: {},
17
+ hasTool: async () => false,
18
+ log: () => { },
19
+ };
20
+ }
21
+ describe("spellcheck runner", () => {
22
+ const require = createRequire(import.meta.url);
23
+ it("should have correct runner definition", async () => {
24
+ const spellcheckModule = await import("./spellcheck.js");
25
+ const runner = spellcheckModule.default;
26
+ expect(runner.id).toBe("spellcheck");
27
+ expect(runner.appliesTo).toEqual(["markdown"]);
28
+ expect(runner.priority).toBe(30);
29
+ expect(runner.enabledByDefault).toBe(true);
30
+ expect(runner.skipTestFiles).toBe(false); // Check docs in test files too
31
+ });
32
+ it("should detect typos-cli availability", () => {
33
+ const { spawnSync } = require("node:child_process");
34
+ const result = spawnSync("typos", ["--version"], {
35
+ encoding: "utf-8",
36
+ timeout: 10000,
37
+ shell: true,
38
+ });
39
+ expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy(); // May or may not be installed
40
+ });
41
+ it("should detect typos in markdown content", async () => {
42
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_test_${Date.now()}.md`);
43
+ fs.writeFileSync(tmpFile, `# README
44
+
45
+ This is a documnet about recieving data.
46
+ The seperation of concerns is important.
47
+ `);
48
+ try {
49
+ const spellcheckModule = await import("./spellcheck.js");
50
+ const runner = spellcheckModule.default;
51
+ const result = await runner.run(createMockContext(tmpFile));
52
+ // If typos-cli is installed, should detect typos
53
+ // If not installed, will be skipped
54
+ if (result.status !== "skipped") {
55
+ // Should detect at least "documnet" and "recieving"
56
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
57
+ expect(result.diagnostics.some((d) => d.tool === "typos" &&
58
+ (d.message.includes("documnet") ||
59
+ d.message.includes("recieving") ||
60
+ d.message.includes("seperation")))).toBe(true);
61
+ }
62
+ }
63
+ finally {
64
+ if (fs.existsSync(tmpFile)) {
65
+ fs.unlinkSync(tmpFile);
66
+ }
67
+ }
68
+ });
69
+ it("should suggest corrections for typos", async () => {
70
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_fix_${Date.now()}.md`);
71
+ fs.writeFileSync(tmpFile, `# Test
72
+
73
+ This is a recieving test.
74
+ `);
75
+ try {
76
+ const spellcheckModule = await import("./spellcheck.js");
77
+ const runner = spellcheckModule.default;
78
+ const result = await runner.run(createMockContext(tmpFile));
79
+ if (result.status !== "skipped" && result.diagnostics.length > 0) {
80
+ // Should have fix suggestions
81
+ const fixableDiags = result.diagnostics.filter((d) => d.fixable);
82
+ expect(fixableDiags.length).toBeGreaterThanOrEqual(1);
83
+ expect(fixableDiags.some((d) => d.fixSuggestion?.toLowerCase().includes("receive"))).toBe(true);
84
+ }
85
+ }
86
+ finally {
87
+ if (fs.existsSync(tmpFile)) {
88
+ fs.unlinkSync(tmpFile);
89
+ }
90
+ }
91
+ });
92
+ it("should pass clean markdown files", async () => {
93
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_ok_${Date.now()}.md`);
94
+ fs.writeFileSync(tmpFile, `# Clean README
95
+
96
+ This is a correct document about receiving data.
97
+ The separation of concerns is important.
98
+ All spelling is proper in this file.
99
+ `);
100
+ try {
101
+ const spellcheckModule = await import("./spellcheck.js");
102
+ const runner = spellcheckModule.default;
103
+ const result = await runner.run(createMockContext(tmpFile));
104
+ if (result.status !== "skipped") {
105
+ // Should have no typos
106
+ expect(result.diagnostics.length).toBe(0);
107
+ expect(result.status).toBe("succeeded");
108
+ }
109
+ }
110
+ finally {
111
+ if (fs.existsSync(tmpFile)) {
112
+ fs.unlinkSync(tmpFile);
113
+ }
114
+ }
115
+ });
116
+ it("should handle JSON parse errors gracefully", async () => {
117
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_json_${Date.now()}.md`);
118
+ fs.writeFileSync(tmpFile, `# Test\n\nSimple file.`);
119
+ try {
120
+ const spellcheckModule = await import("./spellcheck.js");
121
+ const runner = spellcheckModule.default;
122
+ const result = await runner.run(createMockContext(tmpFile));
123
+ // Should not crash on JSON parse issues
124
+ expect(["succeeded", "failed", "skipped"]).toContain(result.status);
125
+ }
126
+ finally {
127
+ if (fs.existsSync(tmpFile)) {
128
+ fs.unlinkSync(tmpFile);
129
+ }
130
+ }
131
+ });
132
+ it("should skip when typos-cli is not available", async () => {
133
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_skip_${Date.now()}.md`);
134
+ fs.writeFileSync(tmpFile, `# Test\n\nContent with typo: recieve.`);
135
+ try {
136
+ const spellcheckModule = await import("./spellcheck.js");
137
+ const runner = spellcheckModule.default;
138
+ // Check if typos is available
139
+ const { spawnSync } = require("node:child_process");
140
+ const checkResult = spawnSync("typos", ["--version"], {
141
+ encoding: "utf-8",
142
+ timeout: 5000,
143
+ shell: true,
144
+ });
145
+ const isAvailable = !checkResult.error && checkResult.status === 0;
146
+ const result = await runner.run(createMockContext(tmpFile));
147
+ if (!isAvailable) {
148
+ expect(result.status).toBe("skipped");
149
+ expect(result.diagnostics).toHaveLength(0);
150
+ }
151
+ }
152
+ finally {
153
+ if (fs.existsSync(tmpFile)) {
154
+ fs.unlinkSync(tmpFile);
155
+ }
156
+ }
157
+ });
158
+ });