pi-lens 3.2.0 → 3.3.1
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.
- package/CHANGELOG.md +20 -0
- package/README.md +4 -10
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/agent-behavior-client.test.js +94 -0
- package/clients/biome-client.test.js +144 -0
- package/clients/cache-manager.test.js +197 -0
- package/clients/complexity-client.test.js +234 -0
- package/clients/dependency-checker.test.js +60 -0
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.test.js +116 -0
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
- package/clients/dispatch/runners/lsp.js +42 -5
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/pyright.test.js +98 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/utils/format-utils.js +1 -6
- package/clients/dispatch/utils/format-utils.ts +1 -6
- package/clients/dogfood.test.js +201 -0
- package/clients/file-kinds.test.js +169 -0
- package/clients/formatters.js +1 -1
- package/clients/go-client.test.js +127 -0
- package/clients/jscpd-client.test.js +127 -0
- package/clients/knip-client.test.js +112 -0
- package/clients/lsp/__tests__/client.test.js +310 -0
- package/clients/lsp/__tests__/client.test.ts +1 -46
- package/clients/lsp/__tests__/config.test.js +167 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/launch.test.js +313 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/service.test.js +435 -0
- package/clients/lsp/client.js +32 -44
- package/clients/lsp/client.ts +36 -45
- package/clients/lsp/launch.js +11 -6
- package/clients/lsp/launch.ts +11 -6
- package/clients/lsp/server.js +27 -2
- package/clients/metrics-client.test.js +141 -0
- package/clients/ruff-client.test.js +132 -0
- package/clients/rust-client.test.js +108 -0
- package/clients/sanitize.test.js +177 -0
- package/clients/secrets-scanner.test.js +100 -0
- package/clients/test-runner-client.test.js +192 -0
- package/clients/todo-scanner.test.js +301 -0
- package/clients/type-coverage-client.test.js +105 -0
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.test.js +105 -0
- package/commands/rate.test.js +119 -0
- package/index.ts +66 -72
- package/package.json +1 -1
- package/clients/bus/bus.js +0 -191
- package/clients/bus/bus.ts +0 -251
- package/clients/bus/events.js +0 -214
- package/clients/bus/events.ts +0 -279
- package/clients/bus/index.js +0 -8
- package/clients/bus/index.ts +0 -9
- package/clients/bus/integration.js +0 -158
- package/clients/bus/integration.ts +0 -227
- package/clients/dispatch/bus-dispatcher.js +0 -178
- package/clients/dispatch/bus-dispatcher.ts +0 -258
- package/clients/services/__tests__/effect-integration.test.ts +0 -111
- package/clients/services/effect-integration.js +0 -198
- package/clients/services/effect-integration.ts +0 -276
- package/clients/services/index.js +0 -7
- package/clients/services/index.ts +0 -8
- package/clients/services/runner-service.js +0 -134
- package/clients/services/runner-service.ts +0 -225
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for shellcheck runner
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
function createMockContext(filePath) {
|
|
9
|
+
return {
|
|
10
|
+
filePath,
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
kind: "shell",
|
|
13
|
+
autofix: false,
|
|
14
|
+
deltaMode: false,
|
|
15
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
16
|
+
pi: {},
|
|
17
|
+
hasTool: async () => false,
|
|
18
|
+
log: () => { },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
// Helper for safe file cleanup
|
|
22
|
+
function safeUnlink(filePath) {
|
|
23
|
+
try {
|
|
24
|
+
if (fs.existsSync(filePath)) {
|
|
25
|
+
fs.unlinkSync(filePath);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Ignore cleanup errors on Windows
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
describe("shellcheck runner", () => {
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
it("should have correct runner definition", async () => {
|
|
35
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
36
|
+
const runner = shellcheckModule.default;
|
|
37
|
+
expect(runner.id).toBe("shellcheck");
|
|
38
|
+
expect(runner.appliesTo).toEqual(["shell"]);
|
|
39
|
+
expect(runner.priority).toBe(20);
|
|
40
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
41
|
+
expect(runner.skipTestFiles).toBe(false);
|
|
42
|
+
});
|
|
43
|
+
it("should detect shellcheck availability", () => {
|
|
44
|
+
const { spawnSync } = require("node:child_process");
|
|
45
|
+
const result = spawnSync("shellcheck", ["--version"], {
|
|
46
|
+
encoding: "utf-8",
|
|
47
|
+
timeout: 10000,
|
|
48
|
+
shell: true,
|
|
49
|
+
});
|
|
50
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy();
|
|
51
|
+
});
|
|
52
|
+
it("should detect undefined variable", async () => {
|
|
53
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_test_${Date.now()}.sh`);
|
|
54
|
+
fs.writeFileSync(tmpFile, ["#!/bin/bash", "# Test script with issues", 'echo "\$UNDEFINED_VAR"', ""].join("\n"));
|
|
55
|
+
try {
|
|
56
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
57
|
+
const runner = shellcheckModule.default;
|
|
58
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
59
|
+
if (result.status !== "skipped") {
|
|
60
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
61
|
+
expect(result.diagnostics.some((d) => d.tool === "shellcheck" &&
|
|
62
|
+
(d.message.includes("undefined") ||
|
|
63
|
+
d.message.includes("SC2154")))).toBe(true);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
safeUnlink(tmpFile);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
it("should pass clean shell scripts", async () => {
|
|
71
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `shellcheck_ok_${Date.now()}.sh`);
|
|
72
|
+
fs.writeFileSync(tmpFile, [
|
|
73
|
+
"#!/bin/bash",
|
|
74
|
+
"# Clean shell script",
|
|
75
|
+
"set -euo pipefail",
|
|
76
|
+
"",
|
|
77
|
+
"main() {",
|
|
78
|
+
' local name="\${1:-world}"',
|
|
79
|
+
' echo "Hello, \${name}!"',
|
|
80
|
+
"}",
|
|
81
|
+
"",
|
|
82
|
+
'main "\$@"',
|
|
83
|
+
"",
|
|
84
|
+
].join("\n"));
|
|
85
|
+
try {
|
|
86
|
+
const shellcheckModule = await import("./shellcheck.js");
|
|
87
|
+
const runner = shellcheckModule.default;
|
|
88
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
89
|
+
if (result.status !== "skipped") {
|
|
90
|
+
expect(result.diagnostics.length).toBe(0);
|
|
91
|
+
expect(result.status).toBe("succeeded");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
safeUnlink(tmpFile);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for spellcheck runner (typos-cli)
|
|
3
|
+
*/
|
|
4
|
+
import * as fs from "node:fs";
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
import * as path from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
function createMockContext(filePath) {
|
|
9
|
+
return {
|
|
10
|
+
filePath,
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
kind: "markdown",
|
|
13
|
+
autofix: false,
|
|
14
|
+
deltaMode: false,
|
|
15
|
+
baselines: { get: () => [], add: () => { }, save: () => { } },
|
|
16
|
+
pi: {},
|
|
17
|
+
hasTool: async () => false,
|
|
18
|
+
log: () => { },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe("spellcheck runner", () => {
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
it("should have correct runner definition", async () => {
|
|
24
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
25
|
+
const runner = spellcheckModule.default;
|
|
26
|
+
expect(runner.id).toBe("spellcheck");
|
|
27
|
+
expect(runner.appliesTo).toEqual(["markdown"]);
|
|
28
|
+
expect(runner.priority).toBe(30);
|
|
29
|
+
expect(runner.enabledByDefault).toBe(true);
|
|
30
|
+
expect(runner.skipTestFiles).toBe(false); // Check docs in test files too
|
|
31
|
+
});
|
|
32
|
+
it("should detect typos-cli availability", () => {
|
|
33
|
+
const { spawnSync } = require("node:child_process");
|
|
34
|
+
const result = spawnSync("typos", ["--version"], {
|
|
35
|
+
encoding: "utf-8",
|
|
36
|
+
timeout: 10000,
|
|
37
|
+
shell: true,
|
|
38
|
+
});
|
|
39
|
+
expect(result.error || result.status !== 0 ? "not available" : "available").toBeTruthy(); // May or may not be installed
|
|
40
|
+
});
|
|
41
|
+
it("should detect typos in markdown content", async () => {
|
|
42
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_test_${Date.now()}.md`);
|
|
43
|
+
fs.writeFileSync(tmpFile, `# README
|
|
44
|
+
|
|
45
|
+
This is a documnet about recieving data.
|
|
46
|
+
The seperation of concerns is important.
|
|
47
|
+
`);
|
|
48
|
+
try {
|
|
49
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
50
|
+
const runner = spellcheckModule.default;
|
|
51
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
52
|
+
// If typos-cli is installed, should detect typos
|
|
53
|
+
// If not installed, will be skipped
|
|
54
|
+
if (result.status !== "skipped") {
|
|
55
|
+
// Should detect at least "documnet" and "recieving"
|
|
56
|
+
expect(result.diagnostics.length).toBeGreaterThanOrEqual(1);
|
|
57
|
+
expect(result.diagnostics.some((d) => d.tool === "typos" &&
|
|
58
|
+
(d.message.includes("documnet") ||
|
|
59
|
+
d.message.includes("recieving") ||
|
|
60
|
+
d.message.includes("seperation")))).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
if (fs.existsSync(tmpFile)) {
|
|
65
|
+
fs.unlinkSync(tmpFile);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("should suggest corrections for typos", async () => {
|
|
70
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_fix_${Date.now()}.md`);
|
|
71
|
+
fs.writeFileSync(tmpFile, `# Test
|
|
72
|
+
|
|
73
|
+
This is a recieving test.
|
|
74
|
+
`);
|
|
75
|
+
try {
|
|
76
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
77
|
+
const runner = spellcheckModule.default;
|
|
78
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
79
|
+
if (result.status !== "skipped" && result.diagnostics.length > 0) {
|
|
80
|
+
// Should have fix suggestions
|
|
81
|
+
const fixableDiags = result.diagnostics.filter((d) => d.fixable);
|
|
82
|
+
expect(fixableDiags.length).toBeGreaterThanOrEqual(1);
|
|
83
|
+
expect(fixableDiags.some((d) => d.fixSuggestion?.toLowerCase().includes("receive"))).toBe(true);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (fs.existsSync(tmpFile)) {
|
|
88
|
+
fs.unlinkSync(tmpFile);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
it("should pass clean markdown files", async () => {
|
|
93
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_ok_${Date.now()}.md`);
|
|
94
|
+
fs.writeFileSync(tmpFile, `# Clean README
|
|
95
|
+
|
|
96
|
+
This is a correct document about receiving data.
|
|
97
|
+
The separation of concerns is important.
|
|
98
|
+
All spelling is proper in this file.
|
|
99
|
+
`);
|
|
100
|
+
try {
|
|
101
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
102
|
+
const runner = spellcheckModule.default;
|
|
103
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
104
|
+
if (result.status !== "skipped") {
|
|
105
|
+
// Should have no typos
|
|
106
|
+
expect(result.diagnostics.length).toBe(0);
|
|
107
|
+
expect(result.status).toBe("succeeded");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (fs.existsSync(tmpFile)) {
|
|
112
|
+
fs.unlinkSync(tmpFile);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
it("should handle JSON parse errors gracefully", async () => {
|
|
117
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_json_${Date.now()}.md`);
|
|
118
|
+
fs.writeFileSync(tmpFile, `# Test\n\nSimple file.`);
|
|
119
|
+
try {
|
|
120
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
121
|
+
const runner = spellcheckModule.default;
|
|
122
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
123
|
+
// Should not crash on JSON parse issues
|
|
124
|
+
expect(["succeeded", "failed", "skipped"]).toContain(result.status);
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
if (fs.existsSync(tmpFile)) {
|
|
128
|
+
fs.unlinkSync(tmpFile);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
it("should skip when typos-cli is not available", async () => {
|
|
133
|
+
const tmpFile = path.join(process.env.TEMP || "/tmp", `spellcheck_skip_${Date.now()}.md`);
|
|
134
|
+
fs.writeFileSync(tmpFile, `# Test\n\nContent with typo: recieve.`);
|
|
135
|
+
try {
|
|
136
|
+
const spellcheckModule = await import("./spellcheck.js");
|
|
137
|
+
const runner = spellcheckModule.default;
|
|
138
|
+
// Check if typos is available
|
|
139
|
+
const { spawnSync } = require("node:child_process");
|
|
140
|
+
const checkResult = spawnSync("typos", ["--version"], {
|
|
141
|
+
encoding: "utf-8",
|
|
142
|
+
timeout: 5000,
|
|
143
|
+
shell: true,
|
|
144
|
+
});
|
|
145
|
+
const isAvailable = !checkResult.error && checkResult.status === 0;
|
|
146
|
+
const result = await runner.run(createMockContext(tmpFile));
|
|
147
|
+
if (!isAvailable) {
|
|
148
|
+
expect(result.status).toBe("skipped");
|
|
149
|
+
expect(result.diagnostics).toHaveLength(0);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
if (fs.existsSync(tmpFile)) {
|
|
154
|
+
fs.unlinkSync(tmpFile);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting utilities for dispatch system
|
|
3
|
-
*
|
|
4
|
-
* Consolidated from:
|
|
5
|
-
* - clients/dispatch/dispatcher.ts
|
|
6
|
-
* - clients/dispatch/bus-dispatcher.ts
|
|
7
|
-
* - clients/services/effect-integration.ts
|
|
2
|
+
* Shared formatting utilities for the dispatch system.
|
|
8
3
|
*/
|
|
9
4
|
export const EMOJI = {
|
|
10
5
|
blocking: "🔴",
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Shared formatting utilities for dispatch system
|
|
3
|
-
*
|
|
4
|
-
* Consolidated from:
|
|
5
|
-
* - clients/dispatch/dispatcher.ts
|
|
6
|
-
* - clients/dispatch/bus-dispatcher.ts
|
|
7
|
-
* - clients/services/effect-integration.ts
|
|
2
|
+
* Shared formatting utilities for the dispatch system.
|
|
8
3
|
*/
|
|
9
4
|
|
|
10
5
|
import type { Diagnostic, OutputSemantic } from "../types.js";
|
|
@@ -0,0 +1,201 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for file-kinds.ts
|
|
3
|
+
* Centralized file type detection
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { detectFileKind, getExtensionsForKind, getFileKindLabel, getLanguageId, isCodeKind, isConfigKind, isFileKind, isScannableFile, } from "./file-kinds.js";
|
|
7
|
+
describe("detectFileKind", () => {
|
|
8
|
+
it("should detect JavaScript/TypeScript files", () => {
|
|
9
|
+
expect(detectFileKind("src/app.ts")).toBe("jsts");
|
|
10
|
+
expect(detectFileKind("src/app.tsx")).toBe("jsts");
|
|
11
|
+
expect(detectFileKind("src/app.js")).toBe("jsts");
|
|
12
|
+
expect(detectFileKind("src/app.jsx")).toBe("jsts");
|
|
13
|
+
expect(detectFileKind("src/app.mjs")).toBe("jsts");
|
|
14
|
+
expect(detectFileKind("src/app.cjs")).toBe("jsts");
|
|
15
|
+
});
|
|
16
|
+
it("should detect Python files", () => {
|
|
17
|
+
expect(detectFileKind("app.py")).toBe("python");
|
|
18
|
+
expect(detectFileKind("tests/test_app.py")).toBe("python");
|
|
19
|
+
});
|
|
20
|
+
it("should detect Go files", () => {
|
|
21
|
+
expect(detectFileKind("main.go")).toBe("go");
|
|
22
|
+
expect(detectFileKind("pkg/utils.go")).toBe("go");
|
|
23
|
+
});
|
|
24
|
+
it("should detect Rust files", () => {
|
|
25
|
+
expect(detectFileKind("main.rs")).toBe("rust");
|
|
26
|
+
expect(detectFileKind("lib/app.rs")).toBe("rust");
|
|
27
|
+
});
|
|
28
|
+
it("should detect C++ files", () => {
|
|
29
|
+
expect(detectFileKind("main.cpp")).toBe("cxx");
|
|
30
|
+
expect(detectFileKind("header.hpp")).toBe("cxx");
|
|
31
|
+
expect(detectFileKind("file.cc")).toBe("cxx");
|
|
32
|
+
expect(detectFileKind("file.hxx")).toBe("cxx");
|
|
33
|
+
});
|
|
34
|
+
it("should detect CMake files", () => {
|
|
35
|
+
expect(detectFileKind("CMakeLists.txt")).toBe("cmake");
|
|
36
|
+
expect(detectFileKind("build.cmake")).toBe("cmake");
|
|
37
|
+
});
|
|
38
|
+
it("should detect Shell files", () => {
|
|
39
|
+
expect(detectFileKind("script.sh")).toBe("shell");
|
|
40
|
+
expect(detectFileKind("script.bash")).toBe("shell");
|
|
41
|
+
expect(detectFileKind("Makefile")).toBe("shell");
|
|
42
|
+
});
|
|
43
|
+
it("should detect JSON files", () => {
|
|
44
|
+
expect(detectFileKind("config.json")).toBe("json");
|
|
45
|
+
expect(detectFileKind("package.json")).toBe("json");
|
|
46
|
+
});
|
|
47
|
+
it("should detect Markdown files", () => {
|
|
48
|
+
expect(detectFileKind("README.md")).toBe("markdown");
|
|
49
|
+
expect(detectFileKind("docs/guide.mdx")).toBe("markdown");
|
|
50
|
+
});
|
|
51
|
+
it("should detect CSS files", () => {
|
|
52
|
+
expect(detectFileKind("style.css")).toBe("css");
|
|
53
|
+
expect(detectFileKind("style.scss")).toBe("css");
|
|
54
|
+
expect(detectFileKind("style.less")).toBe("css");
|
|
55
|
+
});
|
|
56
|
+
it("should detect YAML files", () => {
|
|
57
|
+
expect(detectFileKind("config.yaml")).toBe("yaml");
|
|
58
|
+
expect(detectFileKind("config.yml")).toBe("yaml");
|
|
59
|
+
});
|
|
60
|
+
it("should return undefined for unknown extensions", () => {
|
|
61
|
+
expect(detectFileKind("file.xyz")).toBeUndefined();
|
|
62
|
+
expect(detectFileKind("file")).toBeUndefined();
|
|
63
|
+
});
|
|
64
|
+
it("should handle case-insensitive extensions", () => {
|
|
65
|
+
expect(detectFileKind("file.TS")).toBe("jsts");
|
|
66
|
+
expect(detectFileKind("file.PY")).toBe("python");
|
|
67
|
+
});
|
|
68
|
+
it("should handle paths with special characters", () => {
|
|
69
|
+
expect(detectFileKind("/path/to/file.ts")).toBe("jsts");
|
|
70
|
+
expect(detectFileKind("C:\\path\\to\\file.py")).toBe("python");
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
describe("isFileKind", () => {
|
|
74
|
+
it("should check single file kind", () => {
|
|
75
|
+
expect(isFileKind("app.ts", "jsts")).toBe(true);
|
|
76
|
+
expect(isFileKind("app.py", "jsts")).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
it("should check multiple file kinds", () => {
|
|
79
|
+
expect(isFileKind("app.ts", ["jsts", "python"])).toBe(true);
|
|
80
|
+
expect(isFileKind("app.py", ["jsts", "python"])).toBe(true);
|
|
81
|
+
expect(isFileKind("app.go", ["jsts", "python"])).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
it("should return false for undefined file kind", () => {
|
|
84
|
+
expect(isFileKind("file.xyz", "jsts")).toBe(false);
|
|
85
|
+
expect(isFileKind("file.xyz", ["jsts", "python"])).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("isCodeKind", () => {
|
|
89
|
+
it("should identify code file kinds", () => {
|
|
90
|
+
expect(isCodeKind("jsts")).toBe(true);
|
|
91
|
+
expect(isCodeKind("python")).toBe(true);
|
|
92
|
+
expect(isCodeKind("go")).toBe(true);
|
|
93
|
+
expect(isCodeKind("rust")).toBe(true);
|
|
94
|
+
expect(isCodeKind("cxx")).toBe(true);
|
|
95
|
+
expect(isCodeKind("shell")).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
it("should reject non-code file kinds", () => {
|
|
98
|
+
expect(isCodeKind("json")).toBe(false);
|
|
99
|
+
expect(isCodeKind("markdown")).toBe(false);
|
|
100
|
+
expect(isCodeKind("css")).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
describe("isConfigKind", () => {
|
|
104
|
+
it("should identify config file kinds", () => {
|
|
105
|
+
expect(isConfigKind("json")).toBe(true);
|
|
106
|
+
expect(isConfigKind("yaml")).toBe(true);
|
|
107
|
+
expect(isConfigKind("markdown")).toBe(true);
|
|
108
|
+
expect(isConfigKind("css")).toBe(true);
|
|
109
|
+
});
|
|
110
|
+
it("should reject non-config file kinds", () => {
|
|
111
|
+
expect(isConfigKind("jsts")).toBe(false);
|
|
112
|
+
expect(isConfigKind("python")).toBe(false);
|
|
113
|
+
expect(isConfigKind("go")).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
describe("isScannableFile", () => {
|
|
117
|
+
it("should return true for code files", () => {
|
|
118
|
+
expect(isScannableFile("app.ts")).toBe(true);
|
|
119
|
+
expect(isScannableFile("app.py")).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
it("should return true for config files", () => {
|
|
122
|
+
expect(isScannableFile("config.json")).toBe(true);
|
|
123
|
+
expect(isScannableFile("README.md")).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
it("should return false for test files", () => {
|
|
126
|
+
expect(isScannableFile("app.test.ts")).toBe(false);
|
|
127
|
+
expect(isScannableFile("app.spec.ts")).toBe(false);
|
|
128
|
+
expect(isScannableFile("test-app.ts")).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
it("should return false for unknown extensions", () => {
|
|
131
|
+
expect(isScannableFile("file.xyz")).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
describe("getLanguageId", () => {
|
|
135
|
+
it("should return correct language IDs", () => {
|
|
136
|
+
expect(getLanguageId("jsts")).toBe("typescript");
|
|
137
|
+
expect(getLanguageId("python")).toBe("python");
|
|
138
|
+
expect(getLanguageId("go")).toBe("go");
|
|
139
|
+
expect(getLanguageId("rust")).toBe("rust");
|
|
140
|
+
expect(getLanguageId("cxx")).toBe("cpp");
|
|
141
|
+
expect(getLanguageId("json")).toBe("json");
|
|
142
|
+
});
|
|
143
|
+
it("should return plaintext for unknown kinds", () => {
|
|
144
|
+
expect(getLanguageId("unknown")).toBe("plaintext");
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
describe("getExtensionsForKind", () => {
|
|
148
|
+
it("should return extensions for jsts", () => {
|
|
149
|
+
const exts = getExtensionsForKind("jsts");
|
|
150
|
+
expect(exts).toContain(".ts");
|
|
151
|
+
expect(exts).toContain(".tsx");
|
|
152
|
+
expect(exts).toContain(".js");
|
|
153
|
+
expect(exts).toContain(".jsx");
|
|
154
|
+
});
|
|
155
|
+
it("should return extensions for python", () => {
|
|
156
|
+
const exts = getExtensionsForKind("python");
|
|
157
|
+
expect(exts).toEqual([".py"]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
describe("getFileKindLabel", () => {
|
|
161
|
+
it("should return human-readable labels", () => {
|
|
162
|
+
expect(getFileKindLabel("jsts")).toBe("JavaScript/TypeScript");
|
|
163
|
+
expect(getFileKindLabel("python")).toBe("Python");
|
|
164
|
+
expect(getFileKindLabel("cxx")).toBe("C/C++");
|
|
165
|
+
});
|
|
166
|
+
it("should return kind as fallback", () => {
|
|
167
|
+
expect(getFileKindLabel("unknown")).toBe("unknown");
|
|
168
|
+
});
|
|
169
|
+
});
|
package/clients/formatters.js
CHANGED
|
@@ -459,7 +459,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
459
459
|
}
|
|
460
460
|
}
|
|
461
461
|
// Run formatter
|
|
462
|
-
const result = safeSpawn(cmd[0], cmd.slice(1), { timeout: 15000 });
|
|
462
|
+
const result = safeSpawn(cmd[0], cmd.slice(1), { timeout: 15000, cwd });
|
|
463
463
|
if (result.error) {
|
|
464
464
|
return {
|
|
465
465
|
success: false,
|