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,269 @@
1
+ /**
2
+ * Meta-test: Run similarity detection on pi-lens codebase
3
+ *
4
+ * This is a "dogfood" test - we run the reuse detection on our own code
5
+ * to see what it finds. Educational and useful for improving the algorithm!
6
+ */
7
+
8
+ import * as fs from "node:fs/promises";
9
+ import * as path from "node:path";
10
+ import { fileURLToPath } from "node:url";
11
+ import { glob } from "glob";
12
+ import { beforeAll, describe, expect, it } from "vitest";
13
+ import {
14
+ buildProjectIndex,
15
+ findSimilarFunctions,
16
+ type IndexEntry,
17
+ type ProjectIndex,
18
+ } from "./project-index.js";
19
+ import { calculateSimilarity as calcMatrixSimilarity } from "./state-matrix.js";
20
+
21
+ // Find project root by looking for package.json
22
+ async function findProjectRoot(startDir: string): Promise<string> {
23
+ let dir = startDir;
24
+ while (dir !== path.dirname(dir)) {
25
+ try {
26
+ await fs.access(path.join(dir, "package.json"));
27
+ return dir;
28
+ } catch {
29
+ dir = path.dirname(dir);
30
+ }
31
+ }
32
+ throw new Error("Could not find project root (no package.json)");
33
+ }
34
+
35
+ // Test a known similar pair
36
+ const _SIMILAR_FUNCTIONS = {
37
+ description: "Extracting similar logic patterns in pi-lens",
38
+ pairs: [
39
+ {
40
+ name: "runners/index.ts pattern",
41
+ files: [
42
+ "clients/dispatch/runners/index.ts",
43
+ "clients/dispatch/runners/architect.ts",
44
+ ],
45
+ expected: "High similarity in runner registration patterns",
46
+ },
47
+ {
48
+ name: "Client pattern",
49
+ files: ["clients/typescript-client.ts", "clients/biome-client.ts"],
50
+ expected: "Similar client structures",
51
+ },
52
+ ],
53
+ };
54
+
55
+ describe("🐶 Dogfood Test: Similarity on pi-lens codebase", () => {
56
+ let index: ProjectIndex;
57
+ let projectRoot: string;
58
+
59
+ beforeAll(async () => {
60
+ // Find project root
61
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
62
+ projectRoot = await findProjectRoot(__dirname);
63
+
64
+ // Build index of the entire codebase
65
+ console.log("\n🏗️ Building index of pi-lens codebase...");
66
+ console.log(` Project root: ${projectRoot}`);
67
+
68
+ const files = await glob("clients/**/*.ts", {
69
+ cwd: projectRoot,
70
+ ignore: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"],
71
+ });
72
+
73
+ console.log(` Found ${files.length} source files`);
74
+
75
+ const absoluteFiles = files.map((f) => path.join(projectRoot, f));
76
+ index = await buildProjectIndex(projectRoot, absoluteFiles);
77
+
78
+ console.log(` Indexed ${index.entries.size} functions`);
79
+
80
+ // Show some indexed functions
81
+ const sample = Array.from(index.entries.values()).slice(0, 5);
82
+ console.log("\n📋 Sample indexed functions:");
83
+ sample.forEach((e: IndexEntry, i: number) => {
84
+ console.log(` ${i + 1}. ${e.id} (${e.transitionCount} transitions)`);
85
+ });
86
+ }, 30000); // 30s timeout for indexing
87
+
88
+ describe("Index validation", () => {
89
+ it("should have indexed functions", () => {
90
+ expect(index.entries.size).toBeGreaterThan(0);
91
+ console.log(`\n✅ Indexed ${index.entries.size} functions`);
92
+ });
93
+
94
+ it("should have functions with >20 transitions", () => {
95
+ const complex = Array.from(index.entries.values()).filter(
96
+ (e) => e.transitionCount >= 20,
97
+ );
98
+ expect(complex.length).toBeGreaterThan(0);
99
+ console.log(`\n✅ ${complex.length} functions pass complexity guardrail`);
100
+ });
101
+ });
102
+
103
+ describe("Find similar functions in our own codebase", () => {
104
+ it("should find similar patterns among runners", async () => {
105
+ // Find runner files
106
+ const runnerEntries = Array.from(index.entries.values()).filter(
107
+ (e: IndexEntry) => e.filePath.includes("dispatch/runners/"),
108
+ );
109
+
110
+ console.log(`\n🔍 Testing ${runnerEntries.length} runner functions`);
111
+
112
+ const similarities: {
113
+ func1: string;
114
+ func2: string;
115
+ similarity: number;
116
+ }[] = [];
117
+
118
+ // Compare each pair
119
+ for (let i = 0; i < runnerEntries.length; i++) {
120
+ for (let j = i + 1; j < runnerEntries.length; j++) {
121
+ const entry1 = runnerEntries[i];
122
+ const entry2 = runnerEntries[j];
123
+
124
+ // Skip if same file
125
+ if (entry1.filePath === entry2.filePath) continue;
126
+
127
+ const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
128
+
129
+ if (sim >= 0.75) {
130
+ similarities.push({
131
+ func1: entry1.id,
132
+ func2: entry2.id,
133
+ similarity: sim,
134
+ });
135
+ }
136
+ }
137
+ }
138
+
139
+ // Sort by similarity
140
+ similarities.sort((a, b) => b.similarity - a.similarity);
141
+
142
+ console.log(`\n📊 Found ${similarities.length} similar pairs (>75%):`);
143
+ similarities.slice(0, 5).forEach((s, i) => {
144
+ console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
145
+ console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
146
+ });
147
+
148
+ // Log findings but don't fail - this is exploratory
149
+ expect(similarities.length).toBeGreaterThanOrEqual(0);
150
+ });
151
+
152
+ it("should find similar client patterns", async () => {
153
+ const clientEntries = Array.from(index.entries.values()).filter(
154
+ (e: IndexEntry) =>
155
+ e.filePath.includes("clients/") &&
156
+ e.filePath.includes("-client.ts") &&
157
+ !e.filePath.includes("test"),
158
+ );
159
+
160
+ console.log(`\n🔍 Testing ${clientEntries.length} client functions`);
161
+
162
+ const similarities: {
163
+ func1: string;
164
+ func2: string;
165
+ similarity: number;
166
+ }[] = [];
167
+
168
+ for (let i = 0; i < clientEntries.length; i++) {
169
+ for (let j = i + 1; j < clientEntries.length; j++) {
170
+ const entry1 = clientEntries[i];
171
+ const entry2 = clientEntries[j];
172
+
173
+ if (entry1.filePath === entry2.filePath) continue;
174
+
175
+ const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
176
+
177
+ if (sim >= 0.75) {
178
+ similarities.push({
179
+ func1: entry1.id,
180
+ func2: entry2.id,
181
+ similarity: sim,
182
+ });
183
+ }
184
+ }
185
+ }
186
+
187
+ similarities.sort((a, b) => b.similarity - a.similarity);
188
+
189
+ console.log(
190
+ `\n📊 Found ${similarities.length} similar client patterns (>75%):`,
191
+ );
192
+ similarities.slice(0, 3).forEach((s, i) => {
193
+ console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
194
+ console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
195
+ });
196
+
197
+ expect(similarities.length).toBeGreaterThanOrEqual(0);
198
+ });
199
+ });
200
+
201
+ describe("Find potential refactor opportunities", () => {
202
+ it("should identify duplicate utility functions", () => {
203
+ // Look for functions with very high similarity (>90%)
204
+ const entries = Array.from(index.entries.values());
205
+ const seenPairs = new Set<string>(); // Deduplicate A→B and B→A
206
+ const duplicates: {
207
+ func: string;
208
+ similarTo: string;
209
+ similarity: number;
210
+ }[] = [];
211
+
212
+ for (const entry of entries) {
213
+ const matches = findSimilarFunctions(entry.matrix, index, 0.9, 3);
214
+ for (const match of matches) {
215
+ if (match.targetId === entry.id) continue;
216
+
217
+ // Canonical pair key (sorted to avoid A,B and B,A)
218
+ const pairKey = [entry.id, match.targetId].sort().join("::");
219
+ if (seenPairs.has(pairKey)) continue;
220
+
221
+ seenPairs.add(pairKey);
222
+ duplicates.push({
223
+ func: entry.id,
224
+ similarTo: match.targetId,
225
+ similarity: match.similarity,
226
+ });
227
+ }
228
+ }
229
+
230
+ console.log(
231
+ `\n🎯 Found ${duplicates.length} unique potential duplicates (>90%):`,
232
+ );
233
+ duplicates.slice(0, 5).forEach((d, i) => {
234
+ console.log(` ${i + 1}. ${d.func}`);
235
+ console.log(` Similar to: ${d.similarTo}`);
236
+ console.log(` Match: ${(d.similarity * 100).toFixed(1)}%`);
237
+ });
238
+
239
+ // This is informational - we don't assert on it
240
+ expect(true).toBe(true);
241
+ });
242
+ });
243
+
244
+ describe("Complexity distribution", () => {
245
+ it("should show transition count distribution", () => {
246
+ const entries = Array.from(index.entries.values());
247
+ const transitionCounts = entries.map((e) => e.transitionCount);
248
+
249
+ const avg =
250
+ transitionCounts.reduce((a, b) => a + b, 0) / transitionCounts.length;
251
+ const min = Math.min(...transitionCounts);
252
+ const max = Math.max(...transitionCounts);
253
+
254
+ const belowThreshold = transitionCounts.filter((c) => c < 20).length;
255
+ const aboveThreshold = transitionCounts.filter((c) => c >= 20).length;
256
+
257
+ console.log("\n📊 Complexity Distribution:");
258
+ console.log(` Total functions: ${entries.length}`);
259
+ console.log(` Below threshold (<20): ${belowThreshold}`);
260
+ console.log(` Above threshold (≥20): ${aboveThreshold}`);
261
+ console.log(` Min transitions: ${min}`);
262
+ console.log(` Max transitions: ${max}`);
263
+ console.log(` Average: ${avg.toFixed(1)}`);
264
+
265
+ // Most functions should pass the guardrail
266
+ expect(aboveThreshold).toBeGreaterThan(0);
267
+ });
268
+ });
269
+ });
@@ -0,0 +1,152 @@
1
+ /**
2
+ * FileTime Tracking for pi-lens
3
+ *
4
+ * Prevents race conditions when auto-formatting or external tools modify files.
5
+ * Tracks file modification times and sizes to detect external changes.
6
+ *
7
+ * Inspired by OpenCode's FileTime system - ensures agents re-read files
8
+ * that have been modified externally (including by formatters).
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ // --- Singleton State ---
13
+ const globalState = {
14
+ reads: new Map(),
15
+ locks: new Map(),
16
+ };
17
+ // --- Public API ---
18
+ export class FileTime {
19
+ constructor(sessionID) {
20
+ this.sessionID = sessionID;
21
+ }
22
+ /**
23
+ * Record a file read with current stats
24
+ * Call this after ANY file modification (including formatting)
25
+ */
26
+ read(filePath) {
27
+ const absolutePath = path.resolve(filePath);
28
+ const stamp = createStamp(absolutePath);
29
+ let sessionReads = globalState.reads.get(this.sessionID);
30
+ if (!sessionReads) {
31
+ sessionReads = new Map();
32
+ globalState.reads.set(this.sessionID, sessionReads);
33
+ }
34
+ sessionReads.set(absolutePath, stamp);
35
+ return stamp;
36
+ }
37
+ /**
38
+ * Get last recorded stamp for a file
39
+ */
40
+ get(filePath) {
41
+ const absolutePath = path.resolve(filePath);
42
+ const sessionReads = globalState.reads.get(this.sessionID);
43
+ return sessionReads?.get(absolutePath);
44
+ }
45
+ /**
46
+ * Assert file hasn't changed since last read
47
+ * Throws error if file modified externally - forces agent to re-read
48
+ */
49
+ assert(filePath) {
50
+ const absolutePath = path.resolve(filePath);
51
+ const sessionReads = globalState.reads.get(this.sessionID);
52
+ const recorded = sessionReads?.get(absolutePath);
53
+ if (!recorded) {
54
+ throw new FileTimeError(`You must read file ${absolutePath} before modifying it. Use the read tool first.`, absolutePath, "not-read");
55
+ }
56
+ const current = createStamp(absolutePath);
57
+ const changed = current.mtime !== recorded.mtime ||
58
+ current.ctime !== recorded.ctime ||
59
+ current.size !== recorded.size;
60
+ if (changed) {
61
+ throw new FileTimeError(`File ${absolutePath} has been modified since it was last read.\n` +
62
+ `Last modification: ${new Date(current.mtime ?? Date.now()).toISOString()}\n` +
63
+ `Last read: ${recorded.readAt.toISOString()}\n\n` +
64
+ `Please read the file again before modifying it.`, absolutePath, "modified");
65
+ }
66
+ }
67
+ /**
68
+ * Check if file has changed (non-throwing version of assert)
69
+ */
70
+ hasChanged(filePath) {
71
+ const absolutePath = path.resolve(filePath);
72
+ const sessionReads = globalState.reads.get(this.sessionID);
73
+ const recorded = sessionReads?.get(absolutePath);
74
+ if (!recorded)
75
+ return true; // Never read = changed
76
+ const current = createStamp(absolutePath);
77
+ return (current.mtime !== recorded.mtime ||
78
+ current.ctime !== recorded.ctime ||
79
+ current.size !== recorded.size);
80
+ }
81
+ /**
82
+ * Acquire exclusive lock on file
83
+ * Prevents concurrent modifications to same file
84
+ */
85
+ async withLock(filePath, fn) {
86
+ const absolutePath = path.resolve(filePath);
87
+ // Wait for existing lock
88
+ while (globalState.locks.has(absolutePath)) {
89
+ const existing = globalState.locks.get(absolutePath);
90
+ if (existing)
91
+ await existing;
92
+ }
93
+ // Create new lock
94
+ const lockPromise = fn().finally(() => {
95
+ globalState.locks.delete(absolutePath);
96
+ });
97
+ globalState.locks.set(absolutePath, lockPromise.then(() => { }));
98
+ return lockPromise;
99
+ }
100
+ /**
101
+ * Clear all tracked files for this session
102
+ */
103
+ clear() {
104
+ globalState.reads.delete(this.sessionID);
105
+ }
106
+ /**
107
+ * Clear specific file tracking
108
+ */
109
+ clearFile(filePath) {
110
+ const absolutePath = path.resolve(filePath);
111
+ const sessionReads = globalState.reads.get(this.sessionID);
112
+ sessionReads?.delete(absolutePath);
113
+ }
114
+ }
115
+ // --- Error Type ---
116
+ export class FileTimeError extends Error {
117
+ constructor(message, filePath, reason) {
118
+ super(message);
119
+ this.name = "FileTimeError";
120
+ this.filePath = filePath;
121
+ this.reason = reason;
122
+ }
123
+ }
124
+ // --- Utilities ---
125
+ function createStamp(filePath) {
126
+ try {
127
+ const stats = fs.statSync(filePath);
128
+ return {
129
+ readAt: new Date(),
130
+ mtime: stats.mtime.getTime(),
131
+ ctime: stats.ctime.getTime(),
132
+ size: stats.size,
133
+ };
134
+ }
135
+ catch {
136
+ // File doesn't exist - return empty stamp
137
+ return {
138
+ readAt: new Date(),
139
+ mtime: undefined,
140
+ ctime: undefined,
141
+ size: undefined,
142
+ };
143
+ }
144
+ }
145
+ // --- Global Helpers ---
146
+ export function createFileTime(sessionID) {
147
+ return new FileTime(sessionID);
148
+ }
149
+ export function clearAllSessions() {
150
+ globalState.reads.clear();
151
+ globalState.locks.clear();
152
+ }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * FileTime Tracking for pi-lens
3
+ *
4
+ * Prevents race conditions when auto-formatting or external tools modify files.
5
+ * Tracks file modification times and sizes to detect external changes.
6
+ *
7
+ * Inspired by OpenCode's FileTime system - ensures agents re-read files
8
+ * that have been modified externally (including by formatters).
9
+ */
10
+
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
13
+
14
+ // --- Types ---
15
+
16
+ export interface FileStamp {
17
+ readAt: Date;
18
+ mtime: number | undefined;
19
+ ctime: number | undefined;
20
+ size: number | undefined;
21
+ }
22
+
23
+ interface FileTimeState {
24
+ reads: Map<string, Map<string, FileStamp>>; // sessionID -> filePath -> stamp
25
+ locks: Map<string, Promise<void>>; // filePath -> lock promise
26
+ }
27
+
28
+ // --- Singleton State ---
29
+
30
+ const globalState: FileTimeState = {
31
+ reads: new Map(),
32
+ locks: new Map(),
33
+ };
34
+
35
+ // --- Public API ---
36
+
37
+ export class FileTime {
38
+ private sessionID: string;
39
+
40
+ constructor(sessionID: string) {
41
+ this.sessionID = sessionID;
42
+ }
43
+
44
+ /**
45
+ * Record a file read with current stats
46
+ * Call this after ANY file modification (including formatting)
47
+ */
48
+ read(filePath: string): FileStamp {
49
+ const absolutePath = path.resolve(filePath);
50
+ const stamp = createStamp(absolutePath);
51
+
52
+ let sessionReads = globalState.reads.get(this.sessionID);
53
+ if (!sessionReads) {
54
+ sessionReads = new Map();
55
+ globalState.reads.set(this.sessionID, sessionReads);
56
+ }
57
+
58
+ sessionReads.set(absolutePath, stamp);
59
+ return stamp;
60
+ }
61
+
62
+ /**
63
+ * Get last recorded stamp for a file
64
+ */
65
+ get(filePath: string): FileStamp | undefined {
66
+ const absolutePath = path.resolve(filePath);
67
+ const sessionReads = globalState.reads.get(this.sessionID);
68
+ return sessionReads?.get(absolutePath);
69
+ }
70
+
71
+ /**
72
+ * Assert file hasn't changed since last read
73
+ * Throws error if file modified externally - forces agent to re-read
74
+ */
75
+ assert(filePath: string): void {
76
+ const absolutePath = path.resolve(filePath);
77
+ const sessionReads = globalState.reads.get(this.sessionID);
78
+ const recorded = sessionReads?.get(absolutePath);
79
+
80
+ if (!recorded) {
81
+ throw new FileTimeError(
82
+ `You must read file ${absolutePath} before modifying it. Use the read tool first.`,
83
+ absolutePath,
84
+ "not-read"
85
+ );
86
+ }
87
+
88
+ const current = createStamp(absolutePath);
89
+ const changed =
90
+ current.mtime !== recorded.mtime ||
91
+ current.ctime !== recorded.ctime ||
92
+ current.size !== recorded.size;
93
+
94
+ if (changed) {
95
+ throw new FileTimeError(
96
+ `File ${absolutePath} has been modified since it was last read.\n` +
97
+ `Last modification: ${new Date(current.mtime ?? Date.now()).toISOString()}\n` +
98
+ `Last read: ${recorded.readAt.toISOString()}\n\n` +
99
+ `Please read the file again before modifying it.`,
100
+ absolutePath,
101
+ "modified"
102
+ );
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Check if file has changed (non-throwing version of assert)
108
+ */
109
+ hasChanged(filePath: string): boolean {
110
+ const absolutePath = path.resolve(filePath);
111
+ const sessionReads = globalState.reads.get(this.sessionID);
112
+ const recorded = sessionReads?.get(absolutePath);
113
+
114
+ if (!recorded) return true; // Never read = changed
115
+
116
+ const current = createStamp(absolutePath);
117
+ return (
118
+ current.mtime !== recorded.mtime ||
119
+ current.ctime !== recorded.ctime ||
120
+ current.size !== recorded.size
121
+ );
122
+ }
123
+
124
+ /**
125
+ * Acquire exclusive lock on file
126
+ * Prevents concurrent modifications to same file
127
+ */
128
+ async withLock<T>(filePath: string, fn: () => Promise<T>): Promise<T> {
129
+ const absolutePath = path.resolve(filePath);
130
+
131
+ // Wait for existing lock
132
+ while (globalState.locks.has(absolutePath)) {
133
+ const existing = globalState.locks.get(absolutePath);
134
+ if (existing) await existing;
135
+ }
136
+
137
+ // Create new lock
138
+ const lockPromise = fn().finally(() => {
139
+ globalState.locks.delete(absolutePath);
140
+ });
141
+
142
+ globalState.locks.set(absolutePath, lockPromise.then(() => {}));
143
+ return lockPromise;
144
+ }
145
+
146
+ /**
147
+ * Clear all tracked files for this session
148
+ */
149
+ clear(): void {
150
+ globalState.reads.delete(this.sessionID);
151
+ }
152
+
153
+ /**
154
+ * Clear specific file tracking
155
+ */
156
+ clearFile(filePath: string): void {
157
+ const absolutePath = path.resolve(filePath);
158
+ const sessionReads = globalState.reads.get(this.sessionID);
159
+ sessionReads?.delete(absolutePath);
160
+ }
161
+ }
162
+
163
+ // --- Error Type ---
164
+
165
+ export class FileTimeError extends Error {
166
+ readonly filePath: string;
167
+ readonly reason: "not-read" | "modified";
168
+
169
+ constructor(message: string, filePath: string, reason: "not-read" | "modified") {
170
+ super(message);
171
+ this.name = "FileTimeError";
172
+ this.filePath = filePath;
173
+ this.reason = reason;
174
+ }
175
+ }
176
+
177
+ // --- Utilities ---
178
+
179
+ function createStamp(filePath: string): FileStamp {
180
+ try {
181
+ const stats = fs.statSync(filePath);
182
+ return {
183
+ readAt: new Date(),
184
+ mtime: stats.mtime.getTime(),
185
+ ctime: stats.ctime.getTime(),
186
+ size: stats.size,
187
+ };
188
+ } catch {
189
+ // File doesn't exist - return empty stamp
190
+ return {
191
+ readAt: new Date(),
192
+ mtime: undefined,
193
+ ctime: undefined,
194
+ size: undefined,
195
+ };
196
+ }
197
+ }
198
+
199
+ // --- Global Helpers ---
200
+
201
+ export function createFileTime(sessionID: string): FileTime {
202
+ return new FileTime(sessionID);
203
+ }
204
+
205
+ export function clearAllSessions(): void {
206
+ globalState.reads.clear();
207
+ globalState.locks.clear();
208
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Shared file path utilities for pi-lens
3
+ */
4
+ /**
5
+ * Directories to exclude from all scans (build outputs, dependencies, caches).
6
+ * Used consistently across all scanners to avoid noise from generated files.
7
+ */
8
+ export const EXCLUDED_DIRS = [
9
+ "node_modules",
10
+ ".git",
11
+ "dist",
12
+ "build",
13
+ ".next",
14
+ ".pi-lens",
15
+ ".pi", // pi agent directory
16
+ ".ruff_cache", // Python linter cache
17
+ "venv",
18
+ ".venv",
19
+ "coverage",
20
+ "__pycache__",
21
+ ".tox",
22
+ ".pytest_cache",
23
+ ];
24
+ /**
25
+ * Check if file path is a test/fixture/mock file.
26
+ * Used by secrets scanner, rate command, and dispatch runners
27
+ * to skip these files (false positives on fake credentials, etc).
28
+ */
29
+ export function isTestFile(filePath) {
30
+ const normalized = filePath.replace(/\\/g, "/");
31
+ return (normalized.includes(".test.") ||
32
+ normalized.includes(".spec.") ||
33
+ normalized.includes("/test/") ||
34
+ normalized.includes("/tests/") ||
35
+ normalized.includes("__tests__/") ||
36
+ normalized.includes("test-utils") ||
37
+ normalized.startsWith("test-") ||
38
+ normalized.includes(".fixture.") ||
39
+ normalized.includes(".mock."));
40
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared file path utilities for pi-lens
3
+ */
4
+
5
+ /**
6
+ * Directories to exclude from all scans (build outputs, dependencies, caches).
7
+ * Used consistently across all scanners to avoid noise from generated files.
8
+ */
9
+ export const EXCLUDED_DIRS = [
10
+ "node_modules",
11
+ ".git",
12
+ "dist",
13
+ "build",
14
+ ".next",
15
+ ".pi-lens",
16
+ ".pi", // pi agent directory
17
+ ".ruff_cache", // Python linter cache
18
+ "venv",
19
+ ".venv",
20
+ "coverage",
21
+ "__pycache__",
22
+ ".tox",
23
+ ".pytest_cache",
24
+ ];
25
+
26
+ /**
27
+ * Check if file path is a test/fixture/mock file.
28
+ * Used by secrets scanner, rate command, and dispatch runners
29
+ * to skip these files (false positives on fake credentials, etc).
30
+ */
31
+ export function isTestFile(filePath: string): boolean {
32
+ const normalized = filePath.replace(/\\/g, "/");
33
+ return (
34
+ normalized.includes(".test.") ||
35
+ normalized.includes(".spec.") ||
36
+ normalized.includes("/test/") ||
37
+ normalized.includes("/tests/") ||
38
+ normalized.includes("__tests__/") ||
39
+ normalized.includes("test-utils") ||
40
+ normalized.startsWith("test-") ||
41
+ normalized.includes(".fixture.") ||
42
+ normalized.includes(".mock.")
43
+ );
44
+ }