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.
Files changed (77) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +4 -10
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/format-service.test.js +245 -0
  5. package/clients/__tests__/formatters.test.js +271 -0
  6. package/clients/agent-behavior-client.test.js +94 -0
  7. package/clients/biome-client.test.js +144 -0
  8. package/clients/cache-manager.test.js +197 -0
  9. package/clients/complexity-client.test.js +234 -0
  10. package/clients/dependency-checker.test.js +60 -0
  11. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  12. package/clients/dispatch/__tests__/runner-registration.test.js +234 -0
  13. package/clients/dispatch/__tests__/runner-registration.test.ts +2 -2
  14. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  15. package/clients/dispatch/dispatcher.format.test.js +46 -0
  16. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  17. package/clients/dispatch/dispatcher.test.js +116 -0
  18. package/clients/dispatch/runners/architect.test.js +138 -0
  19. package/clients/dispatch/runners/ast-grep-napi.test.js +106 -0
  20. package/clients/dispatch/runners/lsp.js +42 -5
  21. package/clients/dispatch/runners/oxlint.test.js +230 -0
  22. package/clients/dispatch/runners/pyright.test.js +98 -0
  23. package/clients/dispatch/runners/python-slop.test.js +203 -0
  24. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  25. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  26. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  27. package/clients/dispatch/utils/format-utils.js +1 -6
  28. package/clients/dispatch/utils/format-utils.ts +1 -6
  29. package/clients/dogfood.test.js +201 -0
  30. package/clients/file-kinds.test.js +169 -0
  31. package/clients/formatters.js +1 -1
  32. package/clients/go-client.test.js +127 -0
  33. package/clients/jscpd-client.test.js +127 -0
  34. package/clients/knip-client.test.js +112 -0
  35. package/clients/lsp/__tests__/client.test.js +310 -0
  36. package/clients/lsp/__tests__/client.test.ts +1 -46
  37. package/clients/lsp/__tests__/config.test.js +167 -0
  38. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  39. package/clients/lsp/__tests__/integration.test.js +127 -0
  40. package/clients/lsp/__tests__/launch.test.js +313 -0
  41. package/clients/lsp/__tests__/server.test.js +259 -0
  42. package/clients/lsp/__tests__/service.test.js +435 -0
  43. package/clients/lsp/client.js +32 -44
  44. package/clients/lsp/client.ts +36 -45
  45. package/clients/lsp/launch.js +11 -6
  46. package/clients/lsp/launch.ts +11 -6
  47. package/clients/lsp/server.js +27 -2
  48. package/clients/metrics-client.test.js +141 -0
  49. package/clients/ruff-client.test.js +132 -0
  50. package/clients/rust-client.test.js +108 -0
  51. package/clients/sanitize.test.js +177 -0
  52. package/clients/secrets-scanner.test.js +100 -0
  53. package/clients/test-runner-client.test.js +192 -0
  54. package/clients/todo-scanner.test.js +301 -0
  55. package/clients/type-coverage-client.test.js +105 -0
  56. package/clients/typescript-client.codefix.test.js +157 -0
  57. package/clients/typescript-client.test.js +105 -0
  58. package/commands/rate.test.js +119 -0
  59. package/index.ts +66 -72
  60. package/package.json +1 -1
  61. package/clients/bus/bus.js +0 -191
  62. package/clients/bus/bus.ts +0 -251
  63. package/clients/bus/events.js +0 -214
  64. package/clients/bus/events.ts +0 -279
  65. package/clients/bus/index.js +0 -8
  66. package/clients/bus/index.ts +0 -9
  67. package/clients/bus/integration.js +0 -158
  68. package/clients/bus/integration.ts +0 -227
  69. package/clients/dispatch/bus-dispatcher.js +0 -178
  70. package/clients/dispatch/bus-dispatcher.ts +0 -258
  71. package/clients/services/__tests__/effect-integration.test.ts +0 -111
  72. package/clients/services/effect-integration.js +0 -198
  73. package/clients/services/effect-integration.ts +0 -276
  74. package/clients/services/index.js +0 -7
  75. package/clients/services/index.ts +0 -8
  76. package/clients/services/runner-service.js +0 -134
  77. 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
+ });
@@ -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,