pi-lens 2.1.1 → 2.2.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 CHANGED
@@ -298,6 +298,21 @@ All notable changes to pi-lens will be documented in this file.
298
298
  ### Changed
299
299
  - **Improved ast-grep tool descriptions**: Better pattern guidance to prevent overly broad searches.
300
300
 
301
+ ## [2.2.1] - 2026-03-29
302
+
303
+ ### Fixed
304
+ - **No auto-install**: Runners (biome, pyright) now use direct CLI commands instead of `npx`. If not installed, gracefully skip instead of attempting to download.
305
+
306
+ ## [2.2.0] - 2026-03-29
307
+
308
+ ### Added
309
+ - **`/lens-rate` command**: Visual code quality scoring across 6 dimensions (Type Safety, Complexity, Security, Architecture, Dead Code, Tests). Shows grade A-F and colored progress bars.
310
+ - **Pyright runner**: Real Python type-checking via pyright. Catches type errors like `result: str = add(1, 2)` that ruff misses. Runs alongside ruff (pyright for types, ruff for linting).
311
+ - **Vitest config**: Increased test timeout to 15s for CLI spawn tests. Fixes flaky test failures when npx downloads packages.
312
+
313
+ ### Fixed
314
+ - **Test flakiness**: Availability tests (biome, knip, jscpd) no longer timeout when npx is downloading packages.
315
+
301
316
  ## [1.3.0] - 2026-03-23
302
317
 
303
318
  ### Changed
package/README.md CHANGED
@@ -16,6 +16,43 @@ pi install git:github.com/apmantza/pi-lens
16
16
 
17
17
  ---
18
18
 
19
+ ## What's New (v2.2)
20
+
21
+ ### `/lens-rate` — Code Quality Scoring
22
+
23
+ Visual scoring breakdown across 6 dimensions with grade A-F:
24
+
25
+ ```
26
+ ┌─────────────────────────────────────────────────────────┐
27
+ │ 📊 CODE QUALITY SCORE: 85/100 (B) │
28
+ ├─────────────────────────────────────────────────────────┤
29
+ │ 🔷 Type Safety 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 85 │
30
+ │ 🧩 Complexity 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 82 │
31
+ │ 🔒 Security 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100 │
32
+ │ 🏗️ Architecture 🟩🟩🟩🟩🟩🟩🟩🟩⬜⬜ 80 │
33
+ │ 🗑️ Dead Code 🟩🟩🟩🟩🟩🟩🟩🟩🟩⬜ 90 │
34
+ │ ✅ Tests 🟩🟩🟩🟩🟩🟩🟩🟩🟩🟩 100 │
35
+ └─────────────────────────────────────────────────────────┘
36
+ ```
37
+
38
+ Scores are calculated from real scan data: type-coverage, complexity metrics, secret detection, architect rules, knip, and test results.
39
+
40
+ ### Python Type-Checking (Pyright)
41
+
42
+ Python files now get **real type-checking** via pyright, not just linting:
43
+
44
+ ```python
45
+ # ruff won't catch this, but pyright will:
46
+ def add(x: int, y: int) -> int:
47
+ return x + y
48
+
49
+ result: str = add(1, 2) # ❌ pyright: Type "int" not assignable to "str"
50
+ ```
51
+
52
+ Pyright runs alongside ruff — pyright catches type errors, ruff catches style issues.
53
+
54
+ ---
55
+
19
56
  ## What's New (v2.1)
20
57
 
21
58
  ### Content-Level Secret Scanning
@@ -358,6 +395,7 @@ Each rule includes a `message` and `note` that are shown in diagnostics, so the
358
395
  | `type-coverage` | `npm i -D type-coverage` | TypeScript `any` coverage percentage |
359
396
  | `madge` | `npm i -D madge` | Circular dependency detection |
360
397
  | `ruff` | `pip install ruff` | Python lint + format + autofix |
398
+ | `pyright` | `pip install pyright` or `npm i -g pyright` | Python type-checking (optional, graceful skip if not installed) |
361
399
 
362
400
  ---
363
401
 
@@ -1,27 +1,38 @@
1
1
  /**
2
2
  * Biome runner for dispatch system
3
+ *
4
+ * Requires: @biomejs/biome (npm install -D @biomejs/biome)
3
5
  */
4
6
  import { spawnSync } from "node:child_process";
7
+ // Cache biome availability check
8
+ let biomeAvailable = null;
9
+ function isBiomeAvailable() {
10
+ if (biomeAvailable !== null)
11
+ return biomeAvailable;
12
+ // Check if biome CLI is available (do NOT auto-install via npx)
13
+ const check = spawnSync("biome", ["--version"], {
14
+ encoding: "utf-8",
15
+ timeout: 5000,
16
+ shell: true,
17
+ });
18
+ biomeAvailable = !check.error && check.status === 0;
19
+ return biomeAvailable;
20
+ }
5
21
  const biomeRunner = {
6
22
  id: "biome-lint",
7
23
  appliesTo: ["jsts", "json"],
8
24
  priority: 10,
9
25
  enabledByDefault: true,
10
26
  async run(ctx) {
11
- // Check if biome is available
12
- const check = spawnSync("npx", ["@biomejs/biome", "--version"], {
13
- encoding: "utf-8",
14
- timeout: 5000,
15
- shell: true,
16
- });
17
- if (check.error || check.status !== 0) {
27
+ // Skip if biome is not installed
28
+ if (!isBiomeAvailable()) {
18
29
  return { status: "skipped", diagnostics: [], semantic: "none" };
19
30
  }
20
- // Run biome check
31
+ // Run biome check (use direct command, not npx)
21
32
  const args = ctx.autofix
22
33
  ? ["check", "--write", ctx.filePath]
23
34
  : ["check", ctx.filePath];
24
- const result = spawnSync("npx", ["@biomejs/biome", ...args], {
35
+ const result = spawnSync("biome", args, {
25
36
  encoding: "utf-8",
26
37
  timeout: 30000,
27
38
  shell: true,
@@ -1,5 +1,7 @@
1
1
  /**
2
2
  * Biome runner for dispatch system
3
+ *
4
+ * Requires: @biomejs/biome (npm install -D @biomejs/biome)
3
5
  */
4
6
 
5
7
  import { spawnSync } from "node:child_process";
@@ -10,6 +12,22 @@ import type {
10
12
  RunnerResult,
11
13
  } from "../types.js";
12
14
 
15
+ // Cache biome availability check
16
+ let biomeAvailable: boolean | null = null;
17
+
18
+ function isBiomeAvailable(): boolean {
19
+ if (biomeAvailable !== null) return biomeAvailable;
20
+
21
+ // Check if biome CLI is available (do NOT auto-install via npx)
22
+ const check = spawnSync("biome", ["--version"], {
23
+ encoding: "utf-8",
24
+ timeout: 5000,
25
+ shell: true,
26
+ });
27
+ biomeAvailable = !check.error && check.status === 0;
28
+ return biomeAvailable;
29
+ }
30
+
13
31
  const biomeRunner: RunnerDefinition = {
14
32
  id: "biome-lint",
15
33
  appliesTo: ["jsts", "json"],
@@ -17,23 +35,17 @@ const biomeRunner: RunnerDefinition = {
17
35
  enabledByDefault: true,
18
36
 
19
37
  async run(ctx: DispatchContext): Promise<RunnerResult> {
20
- // Check if biome is available
21
- const check = spawnSync("npx", ["@biomejs/biome", "--version"], {
22
- encoding: "utf-8",
23
- timeout: 5000,
24
- shell: true,
25
- });
26
-
27
- if (check.error || check.status !== 0) {
38
+ // Skip if biome is not installed
39
+ if (!isBiomeAvailable()) {
28
40
  return { status: "skipped", diagnostics: [], semantic: "none" };
29
41
  }
30
42
 
31
- // Run biome check
43
+ // Run biome check (use direct command, not npx)
32
44
  const args = ctx.autofix
33
45
  ? ["check", "--write", ctx.filePath]
34
46
  : ["check", ctx.filePath];
35
47
 
36
- const result = spawnSync("npx", ["@biomejs/biome", ...args], {
48
+ const result = spawnSync("biome", args, {
37
49
  encoding: "utf-8",
38
50
  timeout: 30000,
39
51
  shell: true,
@@ -7,12 +7,14 @@ import architectRunner from "./architect.js";
7
7
  import astGrepRunner from "./ast-grep.js";
8
8
  import biomeRunner from "./biome.js";
9
9
  import goVetRunner from "./go-vet.js";
10
+ import pyrightRunner from "./pyright.js";
10
11
  import ruffRunner from "./ruff.js";
11
12
  import rustClippyRunner from "./rust-clippy.js";
12
13
  import tsLspRunner from "./ts-lsp.js";
13
14
  import typeSafetyRunner from "./type-safety.js";
14
15
  // Register all runners (ordered by priority)
15
- registerRunner(tsLspRunner);
16
+ registerRunner(tsLspRunner); // TypeScript type-checking
17
+ registerRunner(pyrightRunner); // Python type-checking
16
18
  registerRunner(biomeRunner);
17
19
  registerRunner(ruffRunner);
18
20
  registerRunner(typeSafetyRunner);
@@ -8,13 +8,15 @@ import architectRunner from "./architect.js";
8
8
  import astGrepRunner from "./ast-grep.js";
9
9
  import biomeRunner from "./biome.js";
10
10
  import goVetRunner from "./go-vet.js";
11
+ import pyrightRunner from "./pyright.js";
11
12
  import ruffRunner from "./ruff.js";
12
13
  import rustClippyRunner from "./rust-clippy.js";
13
14
  import tsLspRunner from "./ts-lsp.js";
14
15
  import typeSafetyRunner from "./type-safety.js";
15
16
 
16
17
  // Register all runners (ordered by priority)
17
- registerRunner(tsLspRunner);
18
+ registerRunner(tsLspRunner); // TypeScript type-checking
19
+ registerRunner(pyrightRunner); // Python type-checking
18
20
  registerRunner(biomeRunner);
19
21
  registerRunner(ruffRunner);
20
22
  registerRunner(typeSafetyRunner);
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Pyright runner for dispatch system
3
+ *
4
+ * Provides real Python type-checking (not just linting).
5
+ * Catches type errors like: result: str = add(1, 2) # Type "int" not assignable to "str"
6
+ *
7
+ * Requires: pyright (pip install pyright or npm install -g pyright)
8
+ */
9
+ import { spawnSync } from "node:child_process";
10
+ // Cache pyright availability check
11
+ let pyrightAvailable = null;
12
+ function isPyrightAvailable() {
13
+ if (pyrightAvailable !== null)
14
+ return pyrightAvailable;
15
+ // Check if pyright CLI is available (do NOT auto-install via npx)
16
+ const check = spawnSync("pyright", ["--version"], {
17
+ encoding: "utf-8",
18
+ timeout: 5000,
19
+ shell: true,
20
+ });
21
+ pyrightAvailable = !check.error && check.status === 0;
22
+ return pyrightAvailable;
23
+ }
24
+ const pyrightRunner = {
25
+ id: "pyright",
26
+ appliesTo: ["python"],
27
+ priority: 5, // Higher priority than ruff (10) - type errors are more important
28
+ enabledByDefault: true,
29
+ async run(ctx) {
30
+ // Skip if pyright is not installed
31
+ if (!isPyrightAvailable()) {
32
+ return { status: "skipped", diagnostics: [], semantic: "none" };
33
+ }
34
+ // Run pyright with JSON output (use direct command, not npx)
35
+ const result = spawnSync("pyright", ["--outputjson", ctx.filePath], {
36
+ encoding: "utf-8",
37
+ timeout: 60000,
38
+ shell: true,
39
+ });
40
+ // Pyright returns non-zero when errors found, that's OK
41
+ if (result.error) {
42
+ return { status: "skipped", diagnostics: [], semantic: "none" };
43
+ }
44
+ const output = (result.stdout || "").trim();
45
+ if (!output) {
46
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
47
+ }
48
+ try {
49
+ const data = JSON.parse(output);
50
+ const diagnostics = parsePyrightOutput(data, ctx.filePath);
51
+ if (diagnostics.length === 0) {
52
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
53
+ }
54
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
55
+ return {
56
+ status: hasErrors ? "failed" : "succeeded",
57
+ diagnostics,
58
+ semantic: "warning",
59
+ };
60
+ }
61
+ catch {
62
+ // JSON parse failed, skip
63
+ return { status: "skipped", diagnostics: [], semantic: "none" };
64
+ }
65
+ },
66
+ };
67
+ function parsePyrightOutput(data, filePath) {
68
+ if (!data.generalDiagnostics)
69
+ return [];
70
+ return data.generalDiagnostics
71
+ .filter((d) => {
72
+ // Only include errors and warnings, skip informational
73
+ return d.severity === "error" || d.severity === "warning";
74
+ })
75
+ .map((d) => ({
76
+ id: `pyright-${d.range.start.line}-${d.rule}`,
77
+ message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
78
+ filePath,
79
+ line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
80
+ column: d.range.start.character + 1,
81
+ severity: d.severity === "error" ? "error" : "warning",
82
+ semantic: d.severity === "error" ? "blocking" : "warning",
83
+ tool: "pyright",
84
+ rule: d.rule,
85
+ fixable: false, // Pyright can't auto-fix, only suggest
86
+ }));
87
+ }
88
+ export default pyrightRunner;
@@ -0,0 +1,84 @@
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: "python",
10
+ autofix: false,
11
+ deltaMode: false,
12
+ baselines: { get: () => [], add: () => { }, save: () => { } },
13
+ pi: {},
14
+ hasTool: async () => false,
15
+ log: () => { },
16
+ };
17
+ }
18
+ describe("pyright runner", () => {
19
+ const require = createRequire(import.meta.url);
20
+ it("should have correct runner definition", async () => {
21
+ const pyrightModule = await import("./pyright.js");
22
+ const runner = pyrightModule.default;
23
+ expect(runner.id).toBe("pyright");
24
+ expect(runner.appliesTo).toEqual(["python"]);
25
+ expect(runner.priority).toBe(5); // Higher priority than ruff
26
+ expect(runner.enabledByDefault).toBe(true);
27
+ });
28
+ it("should detect pyright availability", () => {
29
+ const { spawnSync } = require("node:child_process");
30
+ const result = spawnSync("npx", ["pyright", "--version"], {
31
+ encoding: "utf-8",
32
+ timeout: 10000,
33
+ shell: true,
34
+ });
35
+ expect(result.error || result.status !== 0 ? "not available" : "available").toBe("available");
36
+ });
37
+ it("should type-check Python files and find errors", async () => {
38
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_${Date.now()}.py`);
39
+ fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
40
+ return x + y
41
+
42
+ result: str = add(1, 2)
43
+
44
+ def greet(name: str) -> str:
45
+ return "Hello " + name
46
+
47
+ greet(123)
48
+ `);
49
+ try {
50
+ const pyrightModule = await import("./pyright.js");
51
+ const runner = pyrightModule.default;
52
+ const result = await runner.run(createMockContext(tmpFile));
53
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
54
+ expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
55
+ expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
56
+ }
57
+ finally {
58
+ fs.unlinkSync(tmpFile);
59
+ }
60
+ });
61
+ it("should pass valid Python files", async () => {
62
+ const tmpFile = path.join(process.env.TEMP || "/tmp", `pyright_test_ok_${Date.now()}.py`);
63
+ fs.writeFileSync(tmpFile, `def add(x: int, y: int) -> int:
64
+ return x + y
65
+
66
+ result: str = str(add(1, 2))
67
+
68
+ def greet(name: str) -> str:
69
+ return "Hello " + name
70
+
71
+ greet("world")
72
+ `);
73
+ try {
74
+ const pyrightModule = await import("./pyright.js");
75
+ const runner = pyrightModule.default;
76
+ const result = await runner.run(createMockContext(tmpFile));
77
+ expect(result.status).toBe("succeeded");
78
+ expect(result.diagnostics.length).toBe(0);
79
+ }
80
+ finally {
81
+ fs.unlinkSync(tmpFile);
82
+ }
83
+ });
84
+ });
@@ -0,0 +1,109 @@
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: "python" 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
+ describe("pyright runner", () => {
22
+ const require = createRequire(import.meta.url);
23
+
24
+ it("should have correct runner definition", async () => {
25
+ const pyrightModule = await import("./pyright.js");
26
+ const runner = pyrightModule.default;
27
+
28
+ expect(runner.id).toBe("pyright");
29
+ expect(runner.appliesTo).toEqual(["python"]);
30
+ expect(runner.priority).toBe(5); // Higher priority than ruff
31
+ expect(runner.enabledByDefault).toBe(true);
32
+ });
33
+
34
+ it("should detect pyright availability", () => {
35
+ const { spawnSync } =
36
+ require("node:child_process") as typeof import("node:child_process");
37
+ const result = spawnSync("npx", ["pyright", "--version"], {
38
+ encoding: "utf-8",
39
+ timeout: 10000,
40
+ shell: true,
41
+ });
42
+ expect(
43
+ result.error || result.status !== 0 ? "not available" : "available",
44
+ ).toBe("available");
45
+ });
46
+
47
+ it("should type-check Python files and find errors", async () => {
48
+ const tmpFile = path.join(
49
+ process.env.TEMP || "/tmp",
50
+ `pyright_test_${Date.now()}.py`,
51
+ );
52
+ fs.writeFileSync(
53
+ tmpFile,
54
+ `def add(x: int, y: int) -> int:
55
+ return x + y
56
+
57
+ result: str = add(1, 2)
58
+
59
+ def greet(name: str) -> str:
60
+ return "Hello " + name
61
+
62
+ greet(123)
63
+ `,
64
+ );
65
+
66
+ try {
67
+ const pyrightModule = await import("./pyright.js");
68
+ const runner = pyrightModule.default;
69
+ const result = await runner.run(createMockContext(tmpFile));
70
+
71
+ expect(result.diagnostics.length).toBeGreaterThanOrEqual(2);
72
+ expect(result.diagnostics.some((d) => d.tool === "pyright")).toBe(true);
73
+ expect(result.diagnostics.some((d) => d.severity === "error")).toBe(true);
74
+ } finally {
75
+ fs.unlinkSync(tmpFile);
76
+ }
77
+ });
78
+
79
+ it("should pass valid Python files", async () => {
80
+ const tmpFile = path.join(
81
+ process.env.TEMP || "/tmp",
82
+ `pyright_test_ok_${Date.now()}.py`,
83
+ );
84
+ fs.writeFileSync(
85
+ tmpFile,
86
+ `def add(x: int, y: int) -> int:
87
+ return x + y
88
+
89
+ result: str = str(add(1, 2))
90
+
91
+ def greet(name: str) -> str:
92
+ return "Hello " + name
93
+
94
+ greet("world")
95
+ `,
96
+ );
97
+
98
+ try {
99
+ const pyrightModule = await import("./pyright.js");
100
+ const runner = pyrightModule.default;
101
+ const result = await runner.run(createMockContext(tmpFile));
102
+
103
+ expect(result.status).toBe("succeeded");
104
+ expect(result.diagnostics.length).toBe(0);
105
+ } finally {
106
+ fs.unlinkSync(tmpFile);
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Pyright runner for dispatch system
3
+ *
4
+ * Provides real Python type-checking (not just linting).
5
+ * Catches type errors like: result: str = add(1, 2) # Type "int" not assignable to "str"
6
+ *
7
+ * Requires: pyright (pip install pyright or npm install -g pyright)
8
+ */
9
+
10
+ import { spawnSync } from "node:child_process";
11
+ import type {
12
+ Diagnostic,
13
+ DispatchContext,
14
+ RunnerDefinition,
15
+ RunnerResult,
16
+ } from "../types.js";
17
+
18
+ // Cache pyright availability check
19
+ let pyrightAvailable: boolean | null = null;
20
+
21
+ function isPyrightAvailable(): boolean {
22
+ if (pyrightAvailable !== null) return pyrightAvailable;
23
+
24
+ // Check if pyright CLI is available (do NOT auto-install via npx)
25
+ const check = spawnSync("pyright", ["--version"], {
26
+ encoding: "utf-8",
27
+ timeout: 5000,
28
+ shell: true,
29
+ });
30
+ pyrightAvailable = !check.error && check.status === 0;
31
+ return pyrightAvailable;
32
+ }
33
+
34
+ const pyrightRunner: RunnerDefinition = {
35
+ id: "pyright",
36
+ appliesTo: ["python"],
37
+ priority: 5, // Higher priority than ruff (10) - type errors are more important
38
+ enabledByDefault: true,
39
+
40
+ async run(ctx: DispatchContext): Promise<RunnerResult> {
41
+ // Skip if pyright is not installed
42
+ if (!isPyrightAvailable()) {
43
+ return { status: "skipped", diagnostics: [], semantic: "none" };
44
+ }
45
+
46
+ // Run pyright with JSON output (use direct command, not npx)
47
+ const result = spawnSync("pyright", ["--outputjson", ctx.filePath], {
48
+ encoding: "utf-8",
49
+ timeout: 60000,
50
+ shell: true,
51
+ });
52
+
53
+ // Pyright returns non-zero when errors found, that's OK
54
+ if (result.error) {
55
+ return { status: "skipped", diagnostics: [], semantic: "none" };
56
+ }
57
+
58
+ const output = (result.stdout || "").trim();
59
+ if (!output) {
60
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
61
+ }
62
+
63
+ try {
64
+ const data = JSON.parse(output);
65
+ const diagnostics = parsePyrightOutput(data, ctx.filePath);
66
+
67
+ if (diagnostics.length === 0) {
68
+ return { status: "succeeded", diagnostics: [], semantic: "none" };
69
+ }
70
+
71
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
72
+
73
+ return {
74
+ status: hasErrors ? "failed" : "succeeded",
75
+ diagnostics,
76
+ semantic: "warning",
77
+ };
78
+ } catch {
79
+ // JSON parse failed, skip
80
+ return { status: "skipped", diagnostics: [], semantic: "none" };
81
+ }
82
+ },
83
+ };
84
+
85
+ interface PyrightDiagnostic {
86
+ file: string;
87
+ severity: "error" | "warning" | "information";
88
+ message: string;
89
+ range: {
90
+ start: { line: number; character: number };
91
+ end: { line: number; character: number };
92
+ };
93
+ rule: string;
94
+ }
95
+
96
+ interface PyrightResult {
97
+ generalDiagnostics: PyrightDiagnostic[];
98
+ }
99
+
100
+ function parsePyrightOutput(
101
+ data: PyrightResult,
102
+ filePath: string,
103
+ ): Diagnostic[] {
104
+ if (!data.generalDiagnostics) return [];
105
+
106
+ return data.generalDiagnostics
107
+ .filter((d) => {
108
+ // Only include errors and warnings, skip informational
109
+ return d.severity === "error" || d.severity === "warning";
110
+ })
111
+ .map((d) => ({
112
+ id: `pyright-${d.range.start.line}-${d.rule}`,
113
+ message: d.message.split("\n")[0], // First line only (pyright has multi-line messages)
114
+ filePath,
115
+ line: d.range.start.line + 1, // Pyright is 0-indexed, we're 1-indexed
116
+ column: d.range.start.character + 1,
117
+ severity: d.severity === "error" ? "error" : "warning",
118
+ semantic: d.severity === "error" ? "blocking" : "warning",
119
+ tool: "pyright",
120
+ rule: d.rule,
121
+ fixable: false, // Pyright can't auto-fix, only suggest
122
+ }));
123
+ }
124
+
125
+ export default pyrightRunner;