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,74 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatDiagnostic } from "./utils/format-utils.js";
3
+ describe("formatDiagnostic inline output verification", () => {
4
+ it("should display complete architect messages (NOT truncated to 'No ')", () => {
5
+ // Simulate actual architect diagnostic
6
+ const diagnostic = {
7
+ id: "architect-1",
8
+ message: "No absolute Windows paths — breaks CI and cross-platform builds.",
9
+ filePath: "/test.ts",
10
+ line: 5,
11
+ severity: "warning",
12
+ semantic: "warning",
13
+ tool: "architect",
14
+ rule: "no-absolute-windows-paths",
15
+ };
16
+ const output = formatDiagnostic(diagnostic);
17
+ console.log("\n=== Architect Message Output ===");
18
+ console.log(output);
19
+ console.log("=================================\n");
20
+ // Verify complete message is shown
21
+ expect(output).toBe(" L5: No absolute Windows paths — breaks CI and cross-platform builds.");
22
+ });
23
+ it("should display code fix messages inline correctly", () => {
24
+ // This is what I actually see from ts-lsp runner
25
+ const diagnostic = {
26
+ id: "ts-12-2345",
27
+ message: "Property 'debug' is missing in type 'Config'\n💡 Quick fix: Add missing property 'debug'",
28
+ filePath: "/src/config.ts",
29
+ line: 12,
30
+ severity: "error",
31
+ semantic: "blocking",
32
+ tool: "ts-lsp",
33
+ rule: "TS2345",
34
+ fixable: true,
35
+ fixSuggestion: "Add missing property 'debug'",
36
+ };
37
+ const output = formatDiagnostic(diagnostic);
38
+ console.log("\n=== Code Fix Message Output ===");
39
+ console.log(output);
40
+ console.log("================================\n");
41
+ // Both lines should be properly indented
42
+ expect(output).toBe(" L12: Property 'debug' is missing in type 'Config'\n 💡 Quick fix: Add missing property 'debug'");
43
+ });
44
+ it("should prove architect 'No ' messages are complete (not noise)", () => {
45
+ // All the "No " messages from default-architect.yaml
46
+ const testMessages = [
47
+ "No absolute Windows paths — breaks CI and cross-platform builds.",
48
+ "No hardcoded localhost URLs — use environment variables or a config service.",
49
+ "No empty catch/except blocks. Swallowing errors makes debugging impossible — at least log the error.",
50
+ "No hardcoded secrets — use environment variables or a secrets manager.",
51
+ "No 'any' types — use 'unknown' or define a proper interface to maintain type safety.",
52
+ ];
53
+ for (let i = 0; i < testMessages.length; i++) {
54
+ const diagnostic = {
55
+ id: `architect-${i}`,
56
+ message: testMessages[i],
57
+ filePath: "/test.ts",
58
+ line: i + 1,
59
+ severity: "warning",
60
+ semantic: "warning",
61
+ tool: "architect",
62
+ rule: "test",
63
+ };
64
+ const output = formatDiagnostic(diagnostic);
65
+ // Each message should be complete, NOT truncated to just "No "
66
+ expect(output.length).toBeGreaterThan(15); // More than " L1: No "
67
+ expect(output).toContain("No ");
68
+ expect(output).toContain("—"); // Contains the em-dash explanation
69
+ // Verify it's not truncated
70
+ const messageAfterNo = output.split("No ")[1];
71
+ expect(messageAfterNo.length).toBeGreaterThan(5);
72
+ }
73
+ });
74
+ });
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Tests for declarative dispatch system
3
+ */
4
+ import { beforeEach, describe, expect, it } from "vitest";
5
+ import { createDispatchContext, getRunner, getRunnersForKind, listRunners, registerRunner, } from "./dispatcher.js";
6
+ // --- Test Runners ---
7
+ const testRunner1 = {
8
+ id: "test-runner-1",
9
+ appliesTo: ["jsts", "python"],
10
+ priority: 10,
11
+ enabledByDefault: true,
12
+ async run() {
13
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
14
+ },
15
+ };
16
+ const testRunner2 = {
17
+ id: "test-runner-2",
18
+ appliesTo: ["python"],
19
+ priority: 20,
20
+ enabledByDefault: true,
21
+ async run() {
22
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
23
+ },
24
+ };
25
+ const testRunnerWithCondition = {
26
+ id: "test-runner-conditional",
27
+ appliesTo: ["jsts"],
28
+ priority: 5,
29
+ enabledByDefault: false,
30
+ when: async (ctx) => ctx.autofix,
31
+ async run() {
32
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
33
+ },
34
+ };
35
+ // --- Tests ---
36
+ describe("Runner Registry", () => {
37
+ beforeEach(() => {
38
+ // Note: In a real test suite, we'd reset the registry between tests
39
+ registerRunner(testRunner1);
40
+ registerRunner(testRunner2);
41
+ registerRunner(testRunnerWithCondition);
42
+ });
43
+ it("should register a runner", () => {
44
+ const runner = getRunner("test-runner-1");
45
+ expect(runner).toBeDefined();
46
+ expect(runner?.id).toBe("test-runner-1");
47
+ });
48
+ it("should return undefined for unknown runner", () => {
49
+ const runner = getRunner("unknown-runner");
50
+ expect(runner).toBeUndefined();
51
+ });
52
+ it("should get runners for a specific kind", () => {
53
+ const jstsRunners = getRunnersForKind("jsts");
54
+ expect(jstsRunners.length).toBeGreaterThan(0);
55
+ expect(jstsRunners.some((r) => r.id === "test-runner-1")).toBe(true);
56
+ });
57
+ it("should return runners sorted by priority", () => {
58
+ const jstsRunners = getRunnersForKind("jsts");
59
+ const priorities = jstsRunners.map((r) => r.priority ?? 100);
60
+ for (let i = 1; i < priorities.length; i++) {
61
+ expect(priorities[i - 1]).toBeLessThanOrEqual(priorities[i]);
62
+ }
63
+ });
64
+ it("should list all registered runners", () => {
65
+ const all = listRunners();
66
+ expect(all.length).toBeGreaterThanOrEqual(3);
67
+ });
68
+ it("should reject duplicate registrations", () => {
69
+ // This should log an error but not throw
70
+ expect(() => registerRunner(testRunner1)).not.toThrow();
71
+ });
72
+ });
73
+ describe("Dispatch Context", () => {
74
+ it("should create a dispatch context", () => {
75
+ const mockPi = { getFlag: (flag) => flag === "autofix" };
76
+ const ctx = createDispatchContext("test.ts", "/project", mockPi);
77
+ // Path is normalized to absolute path (Windows compatibility)
78
+ expect(ctx.filePath).toContain("test.ts");
79
+ expect(ctx.cwd).toBe("/project");
80
+ expect(ctx.autofix).toBe(false);
81
+ expect(ctx.deltaMode).toBe(true);
82
+ });
83
+ it("should detect file kind", () => {
84
+ const mockPi = { getFlag: () => false };
85
+ const ctxTs = createDispatchContext("test.ts", "/project", mockPi);
86
+ expect(ctxTs.kind).toBe("jsts");
87
+ const ctxPy = createDispatchContext("test.py", "/project", mockPi);
88
+ expect(ctxPy.kind).toBe("python");
89
+ const ctxGo = createDispatchContext("test.go", "/project", mockPi);
90
+ expect(ctxGo.kind).toBe("go");
91
+ });
92
+ it("should respect autofix flag", () => {
93
+ const mockPiNoFix = { getFlag: (_f) => false };
94
+ const ctx1 = createDispatchContext("test.ts", "/project", mockPiNoFix);
95
+ expect(ctx1.autofix).toBe(false);
96
+ const mockPiWithFix = { getFlag: (f) => f === "autofix-biome" };
97
+ const ctx2 = createDispatchContext("test.ts", "/project", mockPiWithFix);
98
+ expect(ctx2.autofix).toBe(true);
99
+ });
100
+ });
101
+ describe("Conditional Runners", () => {
102
+ beforeEach(() => {
103
+ registerRunner(testRunnerWithCondition);
104
+ });
105
+ it("should respect when condition", async () => {
106
+ const runner = getRunner("test-runner-conditional");
107
+ expect(runner).toBeDefined();
108
+ const mockPiNoFix = { getFlag: () => false };
109
+ const ctxNoFix = createDispatchContext("test.ts", "/project", mockPiNoFix);
110
+ // When autofix is false, the condition should return false
111
+ if (runner?.when) {
112
+ const shouldRun = await runner.when(ctxNoFix);
113
+ expect(shouldRun).toBe(false);
114
+ }
115
+ });
116
+ });
@@ -0,0 +1,138 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it, beforeAll, afterAll } from "vitest";
4
+ function createMockContext(filePath, kind = "jsts", cwd) {
5
+ return {
6
+ filePath,
7
+ cwd: cwd || process.cwd(),
8
+ kind,
9
+ autofix: false,
10
+ deltaMode: false,
11
+ baselines: { get: () => undefined, set: () => { }, clear: () => { } },
12
+ pi: { getFlag: () => false },
13
+ hasTool: async () => false,
14
+ log: () => { },
15
+ };
16
+ }
17
+ describe("architect runner", () => {
18
+ const testDir = path.join(process.env.TEMP || "/tmp", `architect_test_${Date.now()}`);
19
+ const configPath = path.join(testDir, ".pi-lens", "architect.yaml");
20
+ beforeAll(() => {
21
+ // Create test config
22
+ fs.mkdirSync(path.dirname(configPath), { recursive: true });
23
+ fs.writeFileSync(configPath, `version: "1.0"
24
+ rules:
25
+ - pattern: "**/*.ts"
26
+ max_lines: 50
27
+ must_not:
28
+ - pattern: 'hardcoded_secret_12345'
29
+ message: "No hardcoded secrets"
30
+ fix: "Use process.env.SECRET"
31
+ - pattern: 'console\.log'
32
+ message: "No console.log in production"
33
+ `);
34
+ });
35
+ afterAll(() => {
36
+ try {
37
+ if (fs.existsSync(testDir)) {
38
+ fs.rmSync(testDir, { recursive: true, force: true });
39
+ }
40
+ }
41
+ catch {
42
+ // Ignore cleanup errors
43
+ }
44
+ });
45
+ it("should load default config when no user config exists", async () => {
46
+ const module = await import("./architect.js");
47
+ const runner = module.default;
48
+ // Use a unique temp dir with no user config (will fall back to default)
49
+ const noUserConfigDir = path.join(process.env.TEMP || "/tmp", `no_arch_user_config_${Date.now()}`);
50
+ fs.mkdirSync(noUserConfigDir, { recursive: true });
51
+ // Create a very large file that should trigger default max_lines rule
52
+ const tmpFile = path.join(noUserConfigDir, `large_${Date.now()}.ts`);
53
+ fs.writeFileSync(tmpFile, Array(5000).fill("// line").join("\n"));
54
+ try {
55
+ const result = await runner.run(createMockContext(tmpFile, "jsts", noUserConfigDir));
56
+ // Should use default config and find violations
57
+ expect(result.status).toBe("succeeded");
58
+ // Should have size violation from default config
59
+ expect(result.diagnostics.some((d) => d.message.includes("line limit"))).toBe(true);
60
+ }
61
+ finally {
62
+ try {
63
+ if (fs.existsSync(tmpFile))
64
+ fs.unlinkSync(tmpFile);
65
+ if (fs.existsSync(noUserConfigDir))
66
+ fs.rmdirSync(noUserConfigDir);
67
+ }
68
+ catch { }
69
+ }
70
+ });
71
+ it("should detect file size violations", async () => {
72
+ const module = await import("./architect.js");
73
+ const runner = module.default;
74
+ const tmpFile = path.join(testDir, `large_file_${Date.now()}.ts`);
75
+ // Create file with 100 lines (exceeds 50 line limit)
76
+ fs.writeFileSync(tmpFile, Array(100).fill("// line").join("\n"));
77
+ try {
78
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
79
+ expect(result.status).toBe("succeeded");
80
+ expect(result.diagnostics.length).toBeGreaterThan(0);
81
+ expect(result.diagnostics.some((d) => d.message.includes("50 line limit"))).toBe(true);
82
+ }
83
+ finally {
84
+ try {
85
+ if (fs.existsSync(tmpFile))
86
+ fs.unlinkSync(tmpFile);
87
+ }
88
+ catch { }
89
+ }
90
+ });
91
+ it("should detect pattern violations", async () => {
92
+ const module = await import("./architect.js");
93
+ const runner = module.default;
94
+ const tmpFile = path.join(testDir, `bad_patterns_${Date.now()}.ts`);
95
+ fs.writeFileSync(tmpFile, `const x = hardcoded_secret_12345;
96
+ console.log(x);
97
+ `);
98
+ try {
99
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
100
+ expect(result.status).toBe("succeeded");
101
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
102
+ expect(result.diagnostics.some((d) => d.message.includes("hardcoded"))).toBe(true);
103
+ expect(result.diagnostics.some((d) => d.message.includes("console.log"))).toBe(true);
104
+ }
105
+ finally {
106
+ try {
107
+ if (fs.existsSync(tmpFile))
108
+ fs.unlinkSync(tmpFile);
109
+ }
110
+ catch { }
111
+ }
112
+ });
113
+ it("should return no diagnostics for clean files", async () => {
114
+ const module = await import("./architect.js");
115
+ const runner = module.default;
116
+ const tmpFile = path.join(testDir, `clean_${Date.now()}.ts`);
117
+ // Small file (20 lines) with no violations
118
+ fs.writeFileSync(tmpFile, Array(20).fill("// clean code").join("\n"));
119
+ try {
120
+ const result = await runner.run(createMockContext(tmpFile, "jsts", testDir));
121
+ expect(result.status).toBe("succeeded");
122
+ expect(result.diagnostics.length).toBe(0);
123
+ }
124
+ finally {
125
+ try {
126
+ if (fs.existsSync(tmpFile))
127
+ fs.unlinkSync(tmpFile);
128
+ }
129
+ catch { }
130
+ }
131
+ });
132
+ it("should skip test files", async () => {
133
+ const module = await import("./architect.js");
134
+ const runner = module.default;
135
+ // The runner should have skipTestFiles: true
136
+ expect(runner.skipTestFiles).toBe(true);
137
+ });
138
+ });
@@ -0,0 +1,106 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ function createMockContext(filePath, kind = "jsts") {
5
+ return {
6
+ filePath,
7
+ cwd: process.cwd(),
8
+ kind,
9
+ autofix: false,
10
+ deltaMode: false,
11
+ baselines: { get: () => [], add: () => { }, save: () => { } },
12
+ pi: {},
13
+ hasTool: async () => false,
14
+ log: () => { },
15
+ };
16
+ }
17
+ describe("ast-grep-napi vs CLI comparison", () => {
18
+ it("should load the napi module", async () => {
19
+ const napiModule = await import("./ast-grep-napi.js");
20
+ expect(napiModule.default.id).toBe("ast-grep-napi");
21
+ expect(napiModule.default.appliesTo).toEqual(["jsts"]);
22
+ });
23
+ it("should scan TypeScript file and return succeeded status", async () => {
24
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `napi_test_${Date.now()}.ts`);
25
+ fs.writeFileSync(tmpFile, `// Test file with various patterns
26
+ function test(items: string[]) {
27
+ for (let i = 0; i < items.length; i++) {
28
+ console.log(items[i]);
29
+ }
30
+
31
+ try {
32
+ riskyOperation();
33
+ } catch (e) {
34
+ // empty catch
35
+ }
36
+
37
+ return await fetchData();
38
+ }
39
+
40
+ async function fetchData() {
41
+ return await Promise.resolve(42);
42
+ }
43
+
44
+ function riskyOperation() {
45
+ debugger;
46
+ }
47
+ `);
48
+ try {
49
+ // Test NAPI version
50
+ const napiModule = await import("./ast-grep-napi.js");
51
+ const napiRunner = napiModule.default;
52
+ console.time("napi");
53
+ let napiResult;
54
+ try {
55
+ napiResult = await napiRunner.run(createMockContext(tmpFile));
56
+ }
57
+ catch (error) {
58
+ console.error("NAPI runner threw error:", error);
59
+ throw error;
60
+ }
61
+ console.timeEnd("napi");
62
+ console.log("NAPI result status:", napiResult.status);
63
+ console.log("NAPI result semantic:", napiResult.semantic);
64
+ console.log("NAPI result diagnostics count:", napiResult.diagnostics?.length);
65
+ // Should complete successfully (not skipped, not failed)
66
+ expect(napiResult.status).toBe("succeeded");
67
+ expect(napiResult.semantic).toBe("warning"); // Has findings, so marked as warning
68
+ // Log findings
69
+ console.log("NAPI found:", napiResult.diagnostics.length, "issues");
70
+ console.log("\n=== NAPI FINDINGS ===");
71
+ napiResult.diagnostics.forEach((d, i) => {
72
+ console.log(`${i + 1}. Line ${d.line}: ${d.rule}`);
73
+ });
74
+ }
75
+ finally {
76
+ try {
77
+ if (fs.existsSync(tmpFile)) {
78
+ fs.unlinkSync(tmpFile);
79
+ }
80
+ }
81
+ catch {
82
+ // Ignore cleanup errors
83
+ }
84
+ }
85
+ });
86
+ it("should skip non-TS/JS files", async () => {
87
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `napi_test_py_${Date.now()}.py`);
88
+ fs.writeFileSync(tmpFile, "# Python file\nprint('hello')");
89
+ try {
90
+ const napiModule = await import("./ast-grep-napi.js");
91
+ const napiRunner = napiModule.default;
92
+ const result = await napiRunner.run(createMockContext(tmpFile, "python"));
93
+ expect(result.status).toBe("skipped");
94
+ }
95
+ finally {
96
+ try {
97
+ if (fs.existsSync(tmpFile)) {
98
+ fs.unlinkSync(tmpFile);
99
+ }
100
+ }
101
+ catch {
102
+ // Ignore cleanup errors
103
+ }
104
+ }
105
+ });
106
+ });
@@ -34,11 +34,48 @@ const lspRunner = {
34
34
  if (!content) {
35
35
  return { status: "skipped", diagnostics: [], semantic: "none" };
36
36
  }
37
- // Open file in LSP and get diagnostics
38
- await lspService.openFile(ctx.filePath, content);
39
- // getDiagnostics() internally calls waitForDiagnostics() with bus
40
- // subscription + 150ms debounce + 3s timeout
41
- const lspDiags = await lspService.getDiagnostics(ctx.filePath);
37
+ // Try to open file in LSP and get diagnostics
38
+ // If the server fails to spawn or crashes, this will be caught
39
+ let lspDiags = [];
40
+ let serverFailed = false;
41
+ let failureReason = "";
42
+ try {
43
+ await lspService.openFile(ctx.filePath, content);
44
+ // getDiagnostics() internally calls waitForDiagnostics() with bus
45
+ // subscription + 150ms debounce + 3s timeout
46
+ lspDiags = await lspService.getDiagnostics(ctx.filePath);
47
+ }
48
+ catch (err) {
49
+ serverFailed = true;
50
+ failureReason = err instanceof Error ? err.message : String(err);
51
+ // Check if this is a server spawn/connection error
52
+ if (failureReason.includes("spawn") ||
53
+ failureReason.includes("exited") ||
54
+ failureReason.includes("connection") ||
55
+ failureReason.includes("JSON RPC")) {
56
+ // Mark this server as broken so we don't keep trying
57
+ console.error(`[lsp-runner] LSP server failed for ${ctx.filePath}: ${failureReason}`);
58
+ }
59
+ }
60
+ // If server failed to provide diagnostics, report as failed status
61
+ if (serverFailed) {
62
+ return {
63
+ status: "failed",
64
+ diagnostics: [
65
+ {
66
+ id: `lsp:server-error:0`,
67
+ message: `LSP server failed: ${failureReason}`,
68
+ filePath: ctx.filePath,
69
+ line: 1,
70
+ column: 1,
71
+ severity: "error",
72
+ semantic: "warning", // Don't block - fallback to other runners
73
+ tool: "lsp",
74
+ },
75
+ ],
76
+ semantic: "warning",
77
+ };
78
+ }
42
79
  // Convert LSP diagnostics to our format
43
80
  // Defensive: filter out malformed diagnostics that may lack range
44
81
  const diagnostics = lspDiags