pi-lens 3.1.2 → 3.2.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 (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -1,180 +0,0 @@
1
- import * as fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import * as path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- function createMockContext(filePath) {
6
- return {
7
- filePath,
8
- cwd: process.cwd(),
9
- kind: "jsts",
10
- autofix: false,
11
- deltaMode: false,
12
- baselines: { get: () => [], add: () => { }, save: () => { } },
13
- pi: {},
14
- hasTool: async () => false,
15
- log: () => { },
16
- };
17
- }
18
- // Helper for safe file cleanup
19
- function safeUnlink(filePath) {
20
- try {
21
- if (fs.existsSync(filePath)) {
22
- fs.unlinkSync(filePath);
23
- }
24
- }
25
- catch {
26
- // Ignore cleanup errors on Windows
27
- }
28
- }
29
- describe("ts-slop runner", () => {
30
- const require = createRequire(import.meta.url);
31
- it("should have correct runner definition", async () => {
32
- const slopModule = await import("./ts-slop.js");
33
- const runner = slopModule.default;
34
- expect(runner.id).toBe("ts-slop");
35
- // NOTE: TS/JS slop is now handled by ast-grep-napi
36
- // This CLI runner is disabled by default as fallback
37
- expect(runner.appliesTo).toEqual([]); // Disabled - use ast-grep-napi
38
- expect(runner.priority).toBe(20);
39
- expect(runner.enabledByDefault).toBe(false);
40
- expect(runner.skipTestFiles).toBe(true);
41
- });
42
- it("should detect ast-grep availability", () => {
43
- const { spawnSync } = require("node:child_process");
44
- const result = spawnSync("npx", ["sg", "--version"], {
45
- encoding: "utf-8",
46
- timeout: 10000,
47
- shell: true,
48
- });
49
- expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
50
- });
51
- it("should detect for-index-length pattern (or other slop)", async () => {
52
- const tmpFile = path.join(process.env.TEMP || "/tmp", `ts_slop_test_for_${Date.now()}.ts`);
53
- fs.writeFileSync(tmpFile, `// Slop: using index loop instead of for-of
54
- function processItems(items: string[]) {
55
- for (let i = 0; i < items.length; i++) {
56
- console.log(items[i]);
57
- }
58
- }
59
- `);
60
- try {
61
- const slopModule = await import("./ts-slop.js");
62
- const runner = slopModule.default;
63
- const result = await runner.run(createMockContext(tmpFile));
64
- // Should detect at least some slop patterns
65
- // (specific patterns may vary based on ast-grep rule accuracy)
66
- expect(result.status).not.toBe("skipped");
67
- }
68
- finally {
69
- safeUnlink(tmpFile);
70
- }
71
- });
72
- it("should detect manual Math min/max pattern (or other slop)", async () => {
73
- const tmpFile = path.join(process.env.TEMP || "/tmp", `ts_slop_test_minmax_${Date.now()}.ts`);
74
- fs.writeFileSync(tmpFile, `// Slop: manual min/max instead of Math
75
- function getMax(a: number, b: number): number {
76
- if (a > b) {
77
- const m = a;
78
- } else {
79
- const m = b;
80
- }
81
- return m;
82
- }
83
- `);
84
- try {
85
- const slopModule = await import("./ts-slop.js");
86
- const runner = slopModule.default;
87
- const result = await runner.run(createMockContext(tmpFile));
88
- // Should detect at least some slop patterns
89
- // (specific patterns may vary based on ast-grep rule accuracy)
90
- expect(result.status).not.toBe("skipped");
91
- }
92
- finally {
93
- safeUnlink(tmpFile);
94
- }
95
- });
96
- it("should detect indexOf !== -1 pattern (or other slop)", async () => {
97
- const tmpFile = path.join(process.env.TEMP || "/tmp", `ts_slop_test_indexof_${Date.now()}.ts`);
98
- fs.writeFileSync(tmpFile, `// Slop: indexOf check instead of includes
99
- function hasItem(arr: string[], item: string): boolean {
100
- if (arr.indexOf(item) !== -1) {
101
- return true;
102
- }
103
- return false;
104
- }
105
- `);
106
- try {
107
- const slopModule = await import("./ts-slop.js");
108
- const runner = slopModule.default;
109
- const result = await runner.run(createMockContext(tmpFile));
110
- // Should detect at least some slop patterns
111
- // (specific patterns may vary based on ast-grep rule accuracy)
112
- expect(result.status).not.toBe("skipped");
113
- }
114
- finally {
115
- safeUnlink(tmpFile);
116
- }
117
- });
118
- it("should detect array length > 0 pattern", async () => {
119
- const tmpFile = path.join(process.env.TEMP || "/tmp", `ts_slop_test_length_${Date.now()}.ts`);
120
- fs.writeFileSync(tmpFile, `// Slop: length check instead of truthiness
121
- function processItems(arr: string[]): void {
122
- if (arr.length > 0) {
123
- console.log("has items");
124
- }
125
- }
126
- `);
127
- try {
128
- const slopModule = await import("./ts-slop.js");
129
- const runner = slopModule.default;
130
- const result = await runner.run(createMockContext(tmpFile));
131
- // This pattern may or may not be detected depending on rule specificity
132
- // Just verify the scan ran without errors
133
- expect(result.status).toBe("succeeded");
134
- }
135
- finally {
136
- safeUnlink(tmpFile);
137
- }
138
- });
139
- it("should pass clean TypeScript files", async () => {
140
- const tmpFile = path.join(process.env.TEMP || "/tmp", `ts_slop_test_ok_${Date.now()}.ts`);
141
- fs.writeFileSync(tmpFile, `// Clean TypeScript code
142
- function processItems(items: string[]): void {
143
- for (const item of items) {
144
- console.log(item);
145
- }
146
- }
147
-
148
- function findMax(a: number, b: number): number {
149
- return Math.max(a, b);
150
- }
151
-
152
- function contains(arr: string[], item: string): boolean {
153
- return arr.includes(item);
154
- }
155
-
156
- function hasItems(arr: string[]): boolean {
157
- return arr.length > 0; // This is actually OK, but let's see
158
- }
159
- `);
160
- try {
161
- const slopModule = await import("./ts-slop.js");
162
- const runner = slopModule.default;
163
- const result = await runner.run(createMockContext(tmpFile));
164
- // Should have minimal or no slop issues for clean code
165
- const slopIssues = result.diagnostics.filter((d) => d.tool === "ts-slop");
166
- // Allow for minor issues - the length check might still trigger
167
- expect(slopIssues.length).toBeLessThanOrEqual(1);
168
- }
169
- finally {
170
- try {
171
- if (fs.existsSync(tmpFile)) {
172
- safeUnlink(tmpFile);
173
- }
174
- }
175
- catch {
176
- // Ignore cleanup errors
177
- }
178
- }
179
- });
180
- });
@@ -1,230 +0,0 @@
1
- import * as fs from "node:fs";
2
- import { createRequire } from "node:module";
3
- import * as path from "node:path";
4
- import { describe, expect, it } from "vitest";
5
- import type { DispatchContext } from "../types.js";
6
-
7
- function createMockContext(filePath: string): DispatchContext {
8
- return {
9
- filePath,
10
- cwd: process.cwd(),
11
- kind: "jsts" as any,
12
- autofix: false,
13
- deltaMode: false,
14
- baselines: { get: () => [], add: () => {}, save: () => {} } as any,
15
- pi: {} as any,
16
- hasTool: async () => false,
17
- log: () => {},
18
- };
19
- }
20
-
21
- // Helper for safe file cleanup
22
- function safeUnlink(filePath: string): void {
23
- try {
24
- if (fs.existsSync(filePath)) {
25
- fs.unlinkSync(filePath);
26
- }
27
- } catch {
28
- // Ignore cleanup errors on Windows
29
- }
30
- }
31
-
32
- describe("ts-slop runner", () => {
33
- const require = createRequire(import.meta.url);
34
-
35
- it("should have correct runner definition", async () => {
36
- const slopModule = await import("./ts-slop.js");
37
- const runner = slopModule.default;
38
-
39
- expect(runner.id).toBe("ts-slop");
40
- // NOTE: TS/JS slop is now handled by ast-grep-napi
41
- // This CLI runner is disabled by default as fallback
42
- expect(runner.appliesTo).toEqual([]); // Disabled - use ast-grep-napi
43
- expect(runner.priority).toBe(20);
44
- expect(runner.enabledByDefault).toBe(false);
45
- expect(runner.skipTestFiles).toBe(true);
46
- });
47
-
48
- it("should detect ast-grep availability", () => {
49
- const { spawnSync } =
50
- require("node:child_process") as typeof import("node:child_process");
51
- const result = spawnSync("npx", ["sg", "--version"], {
52
- encoding: "utf-8",
53
- timeout: 10000,
54
- shell: true,
55
- });
56
- expect(
57
- result.error || result.status !== 0 ? "not available" : "available",
58
- ).toBe("available");
59
- });
60
-
61
- it("should detect for-index-length pattern (or other slop)", async () => {
62
- const tmpFile = path.join(
63
- process.env.TEMP || "/tmp",
64
- `ts_slop_test_for_${Date.now()}.ts`,
65
- );
66
- fs.writeFileSync(
67
- tmpFile,
68
- `// Slop: using index loop instead of for-of
69
- function processItems(items: string[]) {
70
- for (let i = 0; i < items.length; i++) {
71
- console.log(items[i]);
72
- }
73
- }
74
- `,
75
- );
76
-
77
- try {
78
- const slopModule = await import("./ts-slop.js");
79
- const runner = slopModule.default;
80
- const result = await runner.run(createMockContext(tmpFile));
81
-
82
- // Should detect at least some slop patterns
83
- // (specific patterns may vary based on ast-grep rule accuracy)
84
- expect(result.status).not.toBe("skipped");
85
- } finally {
86
- safeUnlink(tmpFile);
87
- }
88
- });
89
-
90
- it("should detect manual Math min/max pattern (or other slop)", async () => {
91
- const tmpFile = path.join(
92
- process.env.TEMP || "/tmp",
93
- `ts_slop_test_minmax_${Date.now()}.ts`,
94
- );
95
- fs.writeFileSync(
96
- tmpFile,
97
- `// Slop: manual min/max instead of Math
98
- function getMax(a: number, b: number): number {
99
- if (a > b) {
100
- const m = a;
101
- } else {
102
- const m = b;
103
- }
104
- return m;
105
- }
106
- `,
107
- );
108
-
109
- try {
110
- const slopModule = await import("./ts-slop.js");
111
- const runner = slopModule.default;
112
- const result = await runner.run(createMockContext(tmpFile));
113
-
114
- // Should detect at least some slop patterns
115
- // (specific patterns may vary based on ast-grep rule accuracy)
116
- expect(result.status).not.toBe("skipped");
117
- } finally {
118
- safeUnlink(tmpFile);
119
- }
120
- });
121
-
122
- it("should detect indexOf !== -1 pattern (or other slop)", async () => {
123
- const tmpFile = path.join(
124
- process.env.TEMP || "/tmp",
125
- `ts_slop_test_indexof_${Date.now()}.ts`,
126
- );
127
- fs.writeFileSync(
128
- tmpFile,
129
- `// Slop: indexOf check instead of includes
130
- function hasItem(arr: string[], item: string): boolean {
131
- if (arr.indexOf(item) !== -1) {
132
- return true;
133
- }
134
- return false;
135
- }
136
- `,
137
- );
138
-
139
- try {
140
- const slopModule = await import("./ts-slop.js");
141
- const runner = slopModule.default;
142
- const result = await runner.run(createMockContext(tmpFile));
143
-
144
- // Should detect at least some slop patterns
145
- // (specific patterns may vary based on ast-grep rule accuracy)
146
- expect(result.status).not.toBe("skipped");
147
- } finally {
148
- safeUnlink(tmpFile);
149
- }
150
- });
151
-
152
- it("should detect array length > 0 pattern", async () => {
153
- const tmpFile = path.join(
154
- process.env.TEMP || "/tmp",
155
- `ts_slop_test_length_${Date.now()}.ts`,
156
- );
157
- fs.writeFileSync(
158
- tmpFile,
159
- `// Slop: length check instead of truthiness
160
- function processItems(arr: string[]): void {
161
- if (arr.length > 0) {
162
- console.log("has items");
163
- }
164
- }
165
- `,
166
- );
167
-
168
- try {
169
- const slopModule = await import("./ts-slop.js");
170
- const runner = slopModule.default;
171
- const result = await runner.run(createMockContext(tmpFile));
172
-
173
- // This pattern may or may not be detected depending on rule specificity
174
- // Just verify the scan ran without errors
175
- expect(result.status).toBe("succeeded");
176
- } finally {
177
- safeUnlink(tmpFile);
178
- }
179
- });
180
-
181
- it("should pass clean TypeScript files", async () => {
182
- const tmpFile = path.join(
183
- process.env.TEMP || "/tmp",
184
- `ts_slop_test_ok_${Date.now()}.ts`,
185
- );
186
- fs.writeFileSync(
187
- tmpFile,
188
- `// Clean TypeScript code
189
- function processItems(items: string[]): void {
190
- for (const item of items) {
191
- console.log(item);
192
- }
193
- }
194
-
195
- function findMax(a: number, b: number): number {
196
- return Math.max(a, b);
197
- }
198
-
199
- function contains(arr: string[], item: string): boolean {
200
- return arr.includes(item);
201
- }
202
-
203
- function hasItems(arr: string[]): boolean {
204
- return arr.length > 0; // This is actually OK, but let's see
205
- }
206
- `,
207
- );
208
-
209
- try {
210
- const slopModule = await import("./ts-slop.js");
211
- const runner = slopModule.default;
212
- const result = await runner.run(createMockContext(tmpFile));
213
-
214
- // Should have minimal or no slop issues for clean code
215
- const slopIssues = result.diagnostics.filter(
216
- (d) => d.tool === "ts-slop",
217
- );
218
- // Allow for minor issues - the length check might still trigger
219
- expect(slopIssues.length).toBeLessThanOrEqual(1);
220
- } finally {
221
- try {
222
- if (fs.existsSync(tmpFile)) {
223
- safeUnlink(tmpFile);
224
- }
225
- } catch {
226
- // Ignore cleanup errors
227
- }
228
- }
229
- });
230
- });
@@ -1,201 +0,0 @@
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
- import * as fs from "node:fs/promises";
8
- import * as path from "node:path";
9
- import { fileURLToPath } from "node:url";
10
- import { glob } from "glob";
11
- import { beforeAll, describe, expect, it } from "vitest";
12
- import { buildProjectIndex, findSimilarFunctions, } from "./project-index.js";
13
- import { calculateSimilarity as calcMatrixSimilarity } from "./state-matrix.js";
14
- // Find project root by looking for package.json
15
- async function findProjectRoot(startDir) {
16
- let dir = startDir;
17
- while (dir !== path.dirname(dir)) {
18
- try {
19
- await fs.access(path.join(dir, "package.json"));
20
- return dir;
21
- }
22
- catch {
23
- dir = path.dirname(dir);
24
- }
25
- }
26
- throw new Error("Could not find project root (no package.json)");
27
- }
28
- // Test a known similar pair
29
- const _SIMILAR_FUNCTIONS = {
30
- description: "Extracting similar logic patterns in pi-lens",
31
- pairs: [
32
- {
33
- name: "runners/index.ts pattern",
34
- files: [
35
- "clients/dispatch/runners/index.ts",
36
- "clients/dispatch/runners/architect.ts",
37
- ],
38
- expected: "High similarity in runner registration patterns",
39
- },
40
- {
41
- name: "Client pattern",
42
- files: ["clients/typescript-client.ts", "clients/biome-client.ts"],
43
- expected: "Similar client structures",
44
- },
45
- ],
46
- };
47
- describe("🐶 Dogfood Test: Similarity on pi-lens codebase", () => {
48
- let index;
49
- let projectRoot;
50
- beforeAll(async () => {
51
- // Find project root
52
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
53
- projectRoot = await findProjectRoot(__dirname);
54
- // Build index of the entire codebase
55
- console.log("\n🏗️ Building index of pi-lens codebase...");
56
- console.log(` Project root: ${projectRoot}`);
57
- const files = await glob("clients/**/*.ts", {
58
- cwd: projectRoot,
59
- ignore: ["**/*.test.ts", "**/*.spec.ts", "**/node_modules/**"],
60
- });
61
- console.log(` Found ${files.length} source files`);
62
- const absoluteFiles = files.map((f) => path.join(projectRoot, f));
63
- index = await buildProjectIndex(projectRoot, absoluteFiles);
64
- console.log(` Indexed ${index.entries.size} functions`);
65
- // Show some indexed functions
66
- const sample = Array.from(index.entries.values()).slice(0, 5);
67
- console.log("\n📋 Sample indexed functions:");
68
- sample.forEach((e, i) => {
69
- console.log(` ${i + 1}. ${e.id} (${e.transitionCount} transitions)`);
70
- });
71
- }, 30000); // 30s timeout for indexing
72
- describe("Index validation", () => {
73
- it("should have indexed functions", () => {
74
- expect(index.entries.size).toBeGreaterThan(0);
75
- console.log(`\n✅ Indexed ${index.entries.size} functions`);
76
- });
77
- it("should have functions with >20 transitions", () => {
78
- const complex = Array.from(index.entries.values()).filter((e) => e.transitionCount >= 20);
79
- expect(complex.length).toBeGreaterThan(0);
80
- console.log(`\n✅ ${complex.length} functions pass complexity guardrail`);
81
- });
82
- });
83
- describe("Find similar functions in our own codebase", () => {
84
- it("should find similar patterns among runners", async () => {
85
- // Find runner files
86
- const runnerEntries = Array.from(index.entries.values()).filter((e) => e.filePath.includes("dispatch/runners/"));
87
- console.log(`\n🔍 Testing ${runnerEntries.length} runner functions`);
88
- const similarities = [];
89
- // Compare each pair
90
- for (let i = 0; i < runnerEntries.length; i++) {
91
- for (let j = i + 1; j < runnerEntries.length; j++) {
92
- const entry1 = runnerEntries[i];
93
- const entry2 = runnerEntries[j];
94
- // Skip if same file
95
- if (entry1.filePath === entry2.filePath)
96
- continue;
97
- const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
98
- if (sim >= 0.75) {
99
- similarities.push({
100
- func1: entry1.id,
101
- func2: entry2.id,
102
- similarity: sim,
103
- });
104
- }
105
- }
106
- }
107
- // Sort by similarity
108
- similarities.sort((a, b) => b.similarity - a.similarity);
109
- console.log(`\n📊 Found ${similarities.length} similar pairs (>75%):`);
110
- similarities.slice(0, 5).forEach((s, i) => {
111
- console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
112
- console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
113
- });
114
- // Log findings but don't fail - this is exploratory
115
- expect(similarities.length).toBeGreaterThanOrEqual(0);
116
- });
117
- it("should find similar client patterns", async () => {
118
- const clientEntries = Array.from(index.entries.values()).filter((e) => e.filePath.includes("clients/") &&
119
- e.filePath.includes("-client.ts") &&
120
- !e.filePath.includes("test"));
121
- console.log(`\n🔍 Testing ${clientEntries.length} client functions`);
122
- const similarities = [];
123
- for (let i = 0; i < clientEntries.length; i++) {
124
- for (let j = i + 1; j < clientEntries.length; j++) {
125
- const entry1 = clientEntries[i];
126
- const entry2 = clientEntries[j];
127
- if (entry1.filePath === entry2.filePath)
128
- continue;
129
- const sim = calcMatrixSimilarity(entry1.matrix, entry2.matrix);
130
- if (sim >= 0.75) {
131
- similarities.push({
132
- func1: entry1.id,
133
- func2: entry2.id,
134
- similarity: sim,
135
- });
136
- }
137
- }
138
- }
139
- similarities.sort((a, b) => b.similarity - a.similarity);
140
- console.log(`\n📊 Found ${similarities.length} similar client patterns (>75%):`);
141
- similarities.slice(0, 3).forEach((s, i) => {
142
- console.log(` ${i + 1}. ${s.func1} ↔ ${s.func2}`);
143
- console.log(` Similarity: ${(s.similarity * 100).toFixed(1)}%`);
144
- });
145
- expect(similarities.length).toBeGreaterThanOrEqual(0);
146
- });
147
- });
148
- describe("Find potential refactor opportunities", () => {
149
- it("should identify duplicate utility functions", () => {
150
- // Look for functions with very high similarity (>90%)
151
- const entries = Array.from(index.entries.values());
152
- const seenPairs = new Set(); // Deduplicate A→B and B→A
153
- const duplicates = [];
154
- for (const entry of entries) {
155
- const matches = findSimilarFunctions(entry.matrix, index, 0.9, 3);
156
- for (const match of matches) {
157
- if (match.targetId === entry.id)
158
- continue;
159
- // Canonical pair key (sorted to avoid A,B and B,A)
160
- const pairKey = [entry.id, match.targetId].sort().join("::");
161
- if (seenPairs.has(pairKey))
162
- continue;
163
- seenPairs.add(pairKey);
164
- duplicates.push({
165
- func: entry.id,
166
- similarTo: match.targetId,
167
- similarity: match.similarity,
168
- });
169
- }
170
- }
171
- console.log(`\n🎯 Found ${duplicates.length} unique potential duplicates (>90%):`);
172
- duplicates.slice(0, 5).forEach((d, i) => {
173
- console.log(` ${i + 1}. ${d.func}`);
174
- console.log(` Similar to: ${d.similarTo}`);
175
- console.log(` Match: ${(d.similarity * 100).toFixed(1)}%`);
176
- });
177
- // This is informational - we don't assert on it
178
- expect(true).toBe(true);
179
- });
180
- });
181
- describe("Complexity distribution", () => {
182
- it("should show transition count distribution", () => {
183
- const entries = Array.from(index.entries.values());
184
- const transitionCounts = entries.map((e) => e.transitionCount);
185
- const avg = transitionCounts.reduce((a, b) => a + b, 0) / transitionCounts.length;
186
- const min = Math.min(...transitionCounts);
187
- const max = Math.max(...transitionCounts);
188
- const belowThreshold = transitionCounts.filter((c) => c < 20).length;
189
- const aboveThreshold = transitionCounts.filter((c) => c >= 20).length;
190
- console.log("\n📊 Complexity Distribution:");
191
- console.log(` Total functions: ${entries.length}`);
192
- console.log(` Below threshold (<20): ${belowThreshold}`);
193
- console.log(` Above threshold (≥20): ${aboveThreshold}`);
194
- console.log(` Min transitions: ${min}`);
195
- console.log(` Max transitions: ${max}`);
196
- console.log(` Average: ${avg.toFixed(1)}`);
197
- // Most functions should pass the guardrail
198
- expect(aboveThreshold).toBeGreaterThan(0);
199
- });
200
- });
201
- });