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,255 @@
1
+ /**
2
+ * Project Index: Cache of state matrices for all utilities in the project
3
+ *
4
+ * Builds and maintains an index of exported functions for similarity detection.
5
+ * Stores 57×72 state matrices keyed by function location.
6
+ */
7
+ import * as fs from "node:fs/promises";
8
+ import * as path from "node:path";
9
+ import * as ts from "typescript";
10
+ import { buildStateMatrixFromFile, calculateSimilarity, countTransitions, } from "./state-matrix.js";
11
+ // ============================================================================
12
+ // Index Builder
13
+ // ============================================================================
14
+ /**
15
+ * Build project index by scanning TypeScript files
16
+ */
17
+ export async function buildProjectIndex(projectRoot, filePaths) {
18
+ const entries = new Map();
19
+ for (const filePath of filePaths) {
20
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
21
+ continue;
22
+ }
23
+ try {
24
+ const fileEntries = await indexFile(projectRoot, filePath);
25
+ for (const entry of fileEntries) {
26
+ entries.set(entry.id, entry);
27
+ }
28
+ }
29
+ catch (error) {
30
+ // Skip files that can't be parsed
31
+ console.error(`Failed to index ${filePath}:`, error);
32
+ }
33
+ }
34
+ return {
35
+ version: "1.0",
36
+ createdAt: new Date().toISOString(),
37
+ entries,
38
+ };
39
+ }
40
+ /**
41
+ * Index a single TypeScript file
42
+ */
43
+ async function indexFile(projectRoot, filePath) {
44
+ const entries = [];
45
+ const content = await fs.readFile(filePath, "utf-8");
46
+ const stats = await fs.stat(filePath);
47
+ const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
48
+ const relativePath = path.relative(projectRoot, filePath);
49
+ const exports = [];
50
+ // Find all exported functions
51
+ function visit(node) {
52
+ // Track all exports for reference
53
+ if (ts.isExportDeclaration(node) || hasExportModifier(node)) {
54
+ const name = getExportName(node);
55
+ if (name)
56
+ exports.push(name);
57
+ }
58
+ // Extract function declarations
59
+ if (ts.isFunctionDeclaration(node) && node.name) {
60
+ const functionName = node.name.text;
61
+ const _isExported = hasExportModifier(node);
62
+ // For now, index all named functions (we can filter to exports only later)
63
+ const id = `${relativePath}:${functionName}`;
64
+ // Build matrix just for this function's AST subtree
65
+ const matrix = buildFunctionMatrix(node, sourceFile);
66
+ const transitionCount = countTransitions(matrix);
67
+ // Skip trivial functions (<20 transitions)
68
+ if (transitionCount >= 20) {
69
+ entries.push({
70
+ id,
71
+ filePath: relativePath,
72
+ functionName,
73
+ signature: getFunctionSignature(node),
74
+ matrix,
75
+ transitionCount,
76
+ lastModified: stats.mtimeMs,
77
+ exports,
78
+ });
79
+ }
80
+ }
81
+ // Arrow functions assigned to variables (const fn = () => {})
82
+ if (ts.isVariableStatement(node)) {
83
+ extractArrowFunctions(node, entries, relativePath, sourceFile, stats, exports);
84
+ }
85
+ ts.forEachChild(node, visit);
86
+ }
87
+ visit(sourceFile);
88
+ return entries;
89
+ }
90
+ /**
91
+ * Extract arrow functions and function expressions from variable declarations
92
+ */
93
+ function extractArrowFunctions(node, entries, relativePath, sourceFile, stats, exports) {
94
+ for (const decl of node.declarationList.declarations) {
95
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) {
96
+ continue;
97
+ }
98
+ const func = decl.initializer;
99
+ if (!ts.isArrowFunction(func) && !ts.isFunctionExpression(func)) {
100
+ continue;
101
+ }
102
+ const functionName = decl.name.text;
103
+ const id = `${relativePath}:${functionName}`;
104
+ const matrix = buildFunctionMatrix(func, sourceFile);
105
+ const transitionCount = countTransitions(matrix);
106
+ // Skip trivial functions (<20 transitions)
107
+ if (transitionCount < 20) {
108
+ continue;
109
+ }
110
+ entries.push({
111
+ id,
112
+ filePath: relativePath,
113
+ functionName,
114
+ signature: getArrowFunctionSignature(func),
115
+ matrix,
116
+ transitionCount,
117
+ lastModified: stats.mtimeMs,
118
+ exports,
119
+ });
120
+ }
121
+ }
122
+ /**
123
+ * Build state matrix for a specific function node
124
+ */
125
+ function buildFunctionMatrix(functionNode, sourceFile) {
126
+ // Extract just the function's code as text
127
+ const start = functionNode.getStart(sourceFile);
128
+ const end = functionNode.getEnd();
129
+ const functionCode = sourceFile.text.substring(start, end);
130
+ // Build matrix for just this function
131
+ const funcSourceFile = ts.createSourceFile("func.ts", functionCode, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
132
+ return buildStateMatrixFromFile(funcSourceFile);
133
+ }
134
+ // ============================================================================
135
+ // Utilities
136
+ // ============================================================================
137
+ function hasExportModifier(node) {
138
+ const modifiers = node.modifiers;
139
+ if (!modifiers)
140
+ return false;
141
+ return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
142
+ }
143
+ function getExportName(node) {
144
+ if (ts.isFunctionDeclaration(node) && node.name) {
145
+ return node.name.text;
146
+ }
147
+ if (ts.isVariableStatement(node)) {
148
+ const decl = node.declarationList.declarations[0];
149
+ if (ts.isIdentifier(decl.name)) {
150
+ return decl.name.text;
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function getFunctionSignature(node) {
156
+ const params = node.parameters
157
+ .map((p) => {
158
+ const name = ts.isIdentifier(p.name) ? p.name.text : "param";
159
+ const type = p.type ? `:${sourceFileToString(p.type)}` : "";
160
+ return `${name}${type}`;
161
+ })
162
+ .join(", ");
163
+ const returnType = node.type ? ` => ${sourceFileToString(node.type)}` : "";
164
+ return `(${params})${returnType}`;
165
+ }
166
+ function getArrowFunctionSignature(node) {
167
+ const params = node.parameters
168
+ .map((p) => {
169
+ const name = ts.isIdentifier(p.name) ? p.name.text : "param";
170
+ const type = p.type ? `:${sourceFileToString(p.type)}` : "";
171
+ return `${name}${type}`;
172
+ })
173
+ .join(", ");
174
+ const returnType = node.type ? ` => ${sourceFileToString(node.type)}` : "";
175
+ return `(${params})${returnType}`;
176
+ }
177
+ function sourceFileToString(node) {
178
+ // Simple stringify - just get the text
179
+ const tempFile = ts.createSourceFile("temp.ts", "", ts.ScriptTarget.Latest);
180
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
181
+ return printer.printNode(ts.EmitHint.Unspecified, node, tempFile).trim();
182
+ }
183
+ // ============================================================================
184
+ // Similarity Queries
185
+ // ============================================================================
186
+ /**
187
+ * Find similar functions in the index
188
+ */
189
+ export function findSimilarFunctions(matrix, index, threshold = 0.75, maxResults = 3) {
190
+ const matches = [];
191
+ for (const entry of index.entries.values()) {
192
+ const similarity = calculateSimilarity(matrix, entry.matrix);
193
+ if (similarity >= threshold) {
194
+ matches.push({
195
+ targetId: entry.id,
196
+ targetName: entry.functionName,
197
+ targetLocation: `${entry.filePath}:1`, // TODO: get actual line
198
+ similarity,
199
+ signature: entry.signature,
200
+ });
201
+ }
202
+ }
203
+ // Sort by similarity descending, take top N
204
+ return matches
205
+ .sort((a, b) => b.similarity - a.similarity)
206
+ .slice(0, maxResults);
207
+ }
208
+ // ============================================================================
209
+ // Persistence
210
+ // ============================================================================
211
+ const INDEX_FILE = ".pi-lens/index.json";
212
+ /**
213
+ * Save index to disk
214
+ */
215
+ export async function saveIndex(index, projectRoot) {
216
+ const indexPath = path.join(projectRoot, INDEX_FILE);
217
+ await fs.mkdir(path.dirname(indexPath), { recursive: true });
218
+ // Convert Map to array for JSON serialization
219
+ const serialized = {
220
+ version: index.version,
221
+ createdAt: index.createdAt,
222
+ entries: Array.from(index.entries.entries()),
223
+ };
224
+ await fs.writeFile(indexPath, JSON.stringify(serialized, null, 2));
225
+ }
226
+ /**
227
+ * Load index from disk
228
+ */
229
+ export async function loadIndex(projectRoot) {
230
+ const indexPath = path.join(projectRoot, INDEX_FILE);
231
+ try {
232
+ const data = await fs.readFile(indexPath, "utf-8");
233
+ const parsed = JSON.parse(data);
234
+ return {
235
+ version: parsed.version,
236
+ createdAt: parsed.createdAt,
237
+ entries: new Map(parsed.entries),
238
+ };
239
+ }
240
+ catch {
241
+ return null;
242
+ }
243
+ }
244
+ /**
245
+ * Check if index exists and is fresh (<24 hours old)
246
+ */
247
+ export async function isIndexFresh(projectRoot) {
248
+ const index = await loadIndex(projectRoot);
249
+ if (!index)
250
+ return false;
251
+ const createdAt = new Date(index.createdAt).getTime();
252
+ const age = Date.now() - createdAt;
253
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
254
+ return age < maxAge;
255
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Project Index: Cache of state matrices for all utilities in the project
3
+ *
4
+ * Builds and maintains an index of exported functions for similarity detection.
5
+ * Stores 57×72 state matrices keyed by function location.
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 {
12
+ buildStateMatrixFromFile,
13
+ calculateSimilarity,
14
+ countTransitions,
15
+ } from "./state-matrix.js";
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface IndexEntry {
22
+ id: string; // "utils/date.ts:formatDate"
23
+ filePath: string; // Absolute path
24
+ functionName: string; // "formatDate"
25
+ signature: string; // "(date: Date, format: string) => string"
26
+ matrix: number[][]; // 57×72 state matrix
27
+ transitionCount: number; // For guardrail filtering
28
+ lastModified: number; // mtime for cache invalidation
29
+ exports: string[]; // All exports from file
30
+ }
31
+
32
+ export interface SimilarityMatch {
33
+ targetId: string; // "utils/date.ts:formatDate"
34
+ targetName: string; // "formatDate"
35
+ targetLocation: string; // "utils/date.ts:42"
36
+ similarity: number; // 0-100%
37
+ signature: string; // Target function signature
38
+ }
39
+
40
+ export interface ProjectIndex {
41
+ version: string;
42
+ createdAt: string;
43
+ entries: Map<string, IndexEntry>; // key: id
44
+ }
45
+
46
+ // ============================================================================
47
+ // Index Builder
48
+ // ============================================================================
49
+
50
+ /**
51
+ * Build project index by scanning TypeScript files
52
+ */
53
+ export async function buildProjectIndex(
54
+ projectRoot: string,
55
+ filePaths: string[],
56
+ ): Promise<ProjectIndex> {
57
+ const entries = new Map<string, IndexEntry>();
58
+
59
+ for (const filePath of filePaths) {
60
+ if (!filePath.endsWith(".ts") && !filePath.endsWith(".tsx")) {
61
+ continue;
62
+ }
63
+
64
+ try {
65
+ const fileEntries = await indexFile(projectRoot, filePath);
66
+ for (const entry of fileEntries) {
67
+ entries.set(entry.id, entry);
68
+ }
69
+ } catch (error) {
70
+ // Skip files that can't be parsed
71
+ console.error(`Failed to index ${filePath}:`, error);
72
+ }
73
+ }
74
+
75
+ return {
76
+ version: "1.0",
77
+ createdAt: new Date().toISOString(),
78
+ entries,
79
+ };
80
+ }
81
+
82
+ /**
83
+ * Index a single TypeScript file
84
+ */
85
+ async function indexFile(
86
+ projectRoot: string,
87
+ filePath: string,
88
+ ): Promise<IndexEntry[]> {
89
+ const entries: IndexEntry[] = [];
90
+ const content = await fs.readFile(filePath, "utf-8");
91
+ const stats = await fs.stat(filePath);
92
+
93
+ const sourceFile = ts.createSourceFile(
94
+ filePath,
95
+ content,
96
+ ts.ScriptTarget.Latest,
97
+ true,
98
+ ts.ScriptKind.TS,
99
+ );
100
+
101
+ const relativePath = path.relative(projectRoot, filePath);
102
+ const exports: string[] = [];
103
+
104
+ // Find all exported functions
105
+ function visit(node: ts.Node) {
106
+ // Track all exports for reference
107
+ if (ts.isExportDeclaration(node) || hasExportModifier(node)) {
108
+ const name = getExportName(node);
109
+ if (name) exports.push(name);
110
+ }
111
+
112
+ // Extract function declarations
113
+ if (ts.isFunctionDeclaration(node) && node.name) {
114
+ const functionName = node.name.text;
115
+ const _isExported = hasExportModifier(node);
116
+
117
+ // For now, index all named functions (we can filter to exports only later)
118
+ const id = `${relativePath}:${functionName}`;
119
+
120
+ // Build matrix just for this function's AST subtree
121
+ const matrix = buildFunctionMatrix(node, sourceFile);
122
+ const transitionCount = countTransitions(matrix);
123
+
124
+ // Skip trivial functions (<20 transitions)
125
+ if (transitionCount >= 20) {
126
+ entries.push({
127
+ id,
128
+ filePath: relativePath,
129
+ functionName,
130
+ signature: getFunctionSignature(node),
131
+ matrix,
132
+ transitionCount,
133
+ lastModified: stats.mtimeMs,
134
+ exports,
135
+ });
136
+ }
137
+ }
138
+
139
+ // Arrow functions assigned to variables (const fn = () => {})
140
+ if (ts.isVariableStatement(node)) {
141
+ extractArrowFunctions(
142
+ node,
143
+ entries,
144
+ relativePath,
145
+ sourceFile,
146
+ stats,
147
+ exports,
148
+ );
149
+ }
150
+
151
+ ts.forEachChild(node, visit);
152
+ }
153
+
154
+ visit(sourceFile);
155
+ return entries;
156
+ }
157
+
158
+ /**
159
+ * Extract arrow functions and function expressions from variable declarations
160
+ */
161
+ function extractArrowFunctions(
162
+ node: ts.VariableStatement,
163
+ entries: IndexEntry[],
164
+ relativePath: string,
165
+ sourceFile: ts.SourceFile,
166
+ stats: { mtimeMs: number },
167
+ exports: string[],
168
+ ): void {
169
+ for (const decl of node.declarationList.declarations) {
170
+ if (!ts.isIdentifier(decl.name) || !decl.initializer) {
171
+ continue;
172
+ }
173
+
174
+ const func = decl.initializer;
175
+ if (!ts.isArrowFunction(func) && !ts.isFunctionExpression(func)) {
176
+ continue;
177
+ }
178
+
179
+ const functionName = decl.name.text;
180
+ const id = `${relativePath}:${functionName}`;
181
+
182
+ const matrix = buildFunctionMatrix(func, sourceFile);
183
+ const transitionCount = countTransitions(matrix);
184
+
185
+ // Skip trivial functions (<20 transitions)
186
+ if (transitionCount < 20) {
187
+ continue;
188
+ }
189
+
190
+ entries.push({
191
+ id,
192
+ filePath: relativePath,
193
+ functionName,
194
+ signature: getArrowFunctionSignature(func),
195
+ matrix,
196
+ transitionCount,
197
+ lastModified: stats.mtimeMs,
198
+ exports,
199
+ });
200
+ }
201
+ }
202
+
203
+ /**
204
+ * Build state matrix for a specific function node
205
+ */
206
+ function buildFunctionMatrix(
207
+ functionNode:
208
+ | ts.FunctionDeclaration
209
+ | ts.ArrowFunction
210
+ | ts.FunctionExpression,
211
+ sourceFile: ts.SourceFile,
212
+ ): number[][] {
213
+ // Extract just the function's code as text
214
+ const start = functionNode.getStart(sourceFile);
215
+ const end = functionNode.getEnd();
216
+ const functionCode = sourceFile.text.substring(start, end);
217
+
218
+ // Build matrix for just this function
219
+ const funcSourceFile = ts.createSourceFile(
220
+ "func.ts",
221
+ functionCode,
222
+ ts.ScriptTarget.Latest,
223
+ true,
224
+ ts.ScriptKind.TS,
225
+ );
226
+
227
+ return buildStateMatrixFromFile(funcSourceFile);
228
+ }
229
+
230
+ // ============================================================================
231
+ // Utilities
232
+ // ============================================================================
233
+
234
+ function hasExportModifier(node: ts.Node): boolean {
235
+ const modifiers = (node as ts.HasModifiers).modifiers;
236
+ if (!modifiers) return false;
237
+ return modifiers.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
238
+ }
239
+
240
+ function getExportName(node: ts.Node): string | null {
241
+ if (ts.isFunctionDeclaration(node) && node.name) {
242
+ return node.name.text;
243
+ }
244
+ if (ts.isVariableStatement(node)) {
245
+ const decl = node.declarationList.declarations[0];
246
+ if (ts.isIdentifier(decl.name)) {
247
+ return decl.name.text;
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+
253
+ function getFunctionSignature(node: ts.FunctionDeclaration): string {
254
+ const params = node.parameters
255
+ .map((p) => {
256
+ const name = ts.isIdentifier(p.name) ? p.name.text : "param";
257
+ const type = p.type ? `:${sourceFileToString(p.type)}` : "";
258
+ return `${name}${type}`;
259
+ })
260
+ .join(", ");
261
+
262
+ const returnType = node.type ? ` => ${sourceFileToString(node.type)}` : "";
263
+ return `(${params})${returnType}`;
264
+ }
265
+
266
+ function getArrowFunctionSignature(
267
+ node: ts.ArrowFunction | ts.FunctionExpression,
268
+ ): string {
269
+ const params = node.parameters
270
+ .map((p) => {
271
+ const name = ts.isIdentifier(p.name) ? p.name.text : "param";
272
+ const type = p.type ? `:${sourceFileToString(p.type)}` : "";
273
+ return `${name}${type}`;
274
+ })
275
+ .join(", ");
276
+
277
+ const returnType = node.type ? ` => ${sourceFileToString(node.type)}` : "";
278
+ return `(${params})${returnType}`;
279
+ }
280
+
281
+ function sourceFileToString(node: ts.Node): string {
282
+ // Simple stringify - just get the text
283
+ const tempFile = ts.createSourceFile("temp.ts", "", ts.ScriptTarget.Latest);
284
+ const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
285
+ return printer.printNode(ts.EmitHint.Unspecified, node, tempFile).trim();
286
+ }
287
+
288
+ // ============================================================================
289
+ // Similarity Queries
290
+ // ============================================================================
291
+
292
+ /**
293
+ * Find similar functions in the index
294
+ */
295
+ export function findSimilarFunctions(
296
+ matrix: number[][],
297
+ index: ProjectIndex,
298
+ threshold = 0.75,
299
+ maxResults = 3,
300
+ ): SimilarityMatch[] {
301
+ const matches: SimilarityMatch[] = [];
302
+
303
+ for (const entry of index.entries.values()) {
304
+ const similarity = calculateSimilarity(matrix, entry.matrix);
305
+
306
+ if (similarity >= threshold) {
307
+ matches.push({
308
+ targetId: entry.id,
309
+ targetName: entry.functionName,
310
+ targetLocation: `${entry.filePath}:1`, // TODO: get actual line
311
+ similarity,
312
+ signature: entry.signature,
313
+ });
314
+ }
315
+ }
316
+
317
+ // Sort by similarity descending, take top N
318
+ return matches
319
+ .sort((a, b) => b.similarity - a.similarity)
320
+ .slice(0, maxResults);
321
+ }
322
+
323
+ // ============================================================================
324
+ // Persistence
325
+ // ============================================================================
326
+
327
+ const INDEX_FILE = ".pi-lens/index.json";
328
+
329
+ /**
330
+ * Save index to disk
331
+ */
332
+ export async function saveIndex(
333
+ index: ProjectIndex,
334
+ projectRoot: string,
335
+ ): Promise<void> {
336
+ const indexPath = path.join(projectRoot, INDEX_FILE);
337
+ await fs.mkdir(path.dirname(indexPath), { recursive: true });
338
+
339
+ // Convert Map to array for JSON serialization
340
+ const serialized = {
341
+ version: index.version,
342
+ createdAt: index.createdAt,
343
+ entries: Array.from(index.entries.entries()),
344
+ };
345
+
346
+ await fs.writeFile(indexPath, JSON.stringify(serialized, null, 2));
347
+ }
348
+
349
+ /**
350
+ * Load index from disk
351
+ */
352
+ export async function loadIndex(
353
+ projectRoot: string,
354
+ ): Promise<ProjectIndex | null> {
355
+ const indexPath = path.join(projectRoot, INDEX_FILE);
356
+
357
+ try {
358
+ const data = await fs.readFile(indexPath, "utf-8");
359
+ const parsed = JSON.parse(data);
360
+
361
+ return {
362
+ version: parsed.version,
363
+ createdAt: parsed.createdAt,
364
+ entries: new Map(parsed.entries),
365
+ };
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Check if index exists and is fresh (<24 hours old)
373
+ */
374
+ export async function isIndexFresh(projectRoot: string): Promise<boolean> {
375
+ const index = await loadIndex(projectRoot);
376
+ if (!index) return false;
377
+
378
+ const createdAt = new Date(index.createdAt).getTime();
379
+ const age = Date.now() - createdAt;
380
+ const maxAge = 24 * 60 * 60 * 1000; // 24 hours
381
+
382
+ return age < maxAge;
383
+ }