pi-lens 3.8.21 → 3.8.23

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 (92) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/dispatcher.ts +75 -91
  4. package/clients/dispatch/fact-provider-types.ts +22 -0
  5. package/clients/dispatch/fact-rule-runner.ts +22 -0
  6. package/clients/dispatch/fact-runner.ts +28 -0
  7. package/clients/dispatch/fact-scheduler.ts +78 -0
  8. package/clients/dispatch/fact-store.ts +67 -0
  9. package/clients/dispatch/facts/comment-facts.ts +59 -0
  10. package/clients/dispatch/facts/file-content.ts +20 -0
  11. package/clients/dispatch/facts/function-facts.ts +177 -0
  12. package/clients/dispatch/facts/try-catch-facts.ts +80 -0
  13. package/clients/dispatch/integration.ts +130 -24
  14. package/clients/dispatch/priorities.ts +22 -0
  15. package/clients/dispatch/rules/async-noise.ts +43 -0
  16. package/clients/dispatch/rules/error-obscuring.ts +40 -0
  17. package/clients/dispatch/rules/error-swallowing.ts +35 -0
  18. package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
  19. package/clients/dispatch/rules/placeholder-comments.ts +47 -0
  20. package/clients/dispatch/runners/architect.ts +2 -1
  21. package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
  22. package/clients/dispatch/runners/biome-check.ts +40 -8
  23. package/clients/dispatch/runners/biome.ts +2 -1
  24. package/clients/dispatch/runners/eslint.ts +34 -6
  25. package/clients/dispatch/runners/go-vet.ts +2 -1
  26. package/clients/dispatch/runners/golangci-lint.ts +2 -1
  27. package/clients/dispatch/runners/index.ts +29 -27
  28. package/clients/dispatch/runners/lsp.ts +60 -4
  29. package/clients/dispatch/runners/oxlint.ts +2 -1
  30. package/clients/dispatch/runners/pyright.ts +2 -1
  31. package/clients/dispatch/runners/python-slop.ts +2 -1
  32. package/clients/dispatch/runners/rubocop.ts +2 -1
  33. package/clients/dispatch/runners/ruff.ts +2 -1
  34. package/clients/dispatch/runners/rust-clippy.ts +2 -1
  35. package/clients/dispatch/runners/shellcheck.ts +2 -1
  36. package/clients/dispatch/runners/similarity.ts +2 -1
  37. package/clients/dispatch/runners/spellcheck.ts +2 -1
  38. package/clients/dispatch/runners/sqlfluff.ts +2 -1
  39. package/clients/dispatch/runners/tree-sitter.ts +469 -1
  40. package/clients/dispatch/runners/ts-lsp.ts +2 -1
  41. package/clients/dispatch/runners/type-safety.ts +2 -1
  42. package/clients/dispatch/runners/yamllint.ts +2 -1
  43. package/clients/dispatch/tool-profile.ts +40 -0
  44. package/clients/dispatch/types.ts +3 -13
  45. package/clients/lsp/client.ts +366 -12
  46. package/clients/lsp/index.ts +374 -76
  47. package/clients/lsp/launch.ts +42 -2
  48. package/clients/lsp/server.ts +186 -12
  49. package/clients/pipeline.ts +2 -2
  50. package/clients/runtime-context.ts +2 -2
  51. package/clients/runtime-session.ts +43 -5
  52. package/clients/session-summary.ts +21 -0
  53. package/clients/tree-sitter-client.ts +162 -0
  54. package/clients/tree-sitter-logger.ts +47 -0
  55. package/clients/tree-sitter-query-loader.ts +13 -2
  56. package/index.ts +67 -17
  57. package/package.json +3 -1
  58. package/rules/rule-catalog.json +64 -0
  59. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  60. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  61. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  62. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  63. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  64. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  65. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  66. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  67. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  68. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  69. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  70. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  71. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  72. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  73. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  74. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  75. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  76. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  77. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  78. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  79. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  80. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  81. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  82. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  83. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  84. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  85. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  86. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  87. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  88. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  89. package/scripts/validate-rule-catalog.mjs +227 -0
  90. package/skills/lsp-navigation/SKILL.md +15 -3
  91. package/tools/lsp-navigation.js +466 -79
  92. package/tools/lsp-navigation.ts +587 -85
@@ -0,0 +1,47 @@
1
+ import { isTestFile } from "../../file-utils.js";
2
+ import type { FactRule } from "../fact-provider-types.js";
3
+ import type { CommentSummary } from "../facts/comment-facts.js";
4
+ import type { Diagnostic } from "../types.js";
5
+
6
+ const PLACEHOLDER_PATTERNS = [
7
+ /add\s+more\s+validation/i,
8
+ /handle\s+(additional|more)\s+cases?/i,
9
+ /can\s+be\s+extended\s+in\s+the\s+future/i,
10
+ /extend\s+this\s+(logic|function|method|handler|module)/i,
11
+ /customize\s+this\s+(logic|behavior|function|method|handler)/i,
12
+ /future\s+enhancement/i,
13
+ /implement\s+.+\s+here/i,
14
+ ];
15
+
16
+ export const placeholderCommentsRule: FactRule = {
17
+ id: "placeholder-comments",
18
+ requires: ["file.comments"],
19
+ appliesTo(ctx) {
20
+ return /\.tsx?$/.test(ctx.filePath) && !isTestFile(ctx.filePath);
21
+ },
22
+ evaluate(ctx, store) {
23
+ const comments = store.getFileFact<CommentSummary[]>(ctx.filePath, "file.comments");
24
+ if (!comments) return [];
25
+
26
+ const diagnostics: Diagnostic[] = [];
27
+ for (const comment of comments) {
28
+ const matched = PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(comment.text));
29
+ if (!matched) continue;
30
+
31
+ diagnostics.push({
32
+ id: `placeholder-comments:${ctx.filePath}:${comment.line}:1`,
33
+ tool: "placeholder-comments",
34
+ filePath: ctx.filePath,
35
+ line: comment.line,
36
+ column: 1,
37
+ severity: "warning",
38
+ semantic: "warning",
39
+ rule: "placeholder-comments",
40
+ message:
41
+ "Placeholder comment detected. Prefer comments that describe current behavior over future intent.",
42
+ });
43
+ }
44
+
45
+ return diagnostics;
46
+ },
47
+ };
@@ -17,6 +17,7 @@ import type {
17
17
  RunnerDefinition,
18
18
  RunnerResult,
19
19
  } from "../types.js";
20
+ import { PRIORITY } from "../priorities.js";
20
21
  import { readFileContent } from "./utils.js";
21
22
 
22
23
  // Module-level singleton — loadConfig once per cwd, not on every file write
@@ -40,7 +41,7 @@ function getClient(cwd: string): ArchitectClient {
40
41
  const architectRunner: RunnerDefinition = {
41
42
  id: "architect",
42
43
  appliesTo: ["jsts", "python", "go", "rust", "cxx", "shell", "cmake"],
43
- priority: 40,
44
+ priority: PRIORITY.ARCHITECTURE,
44
45
  enabledByDefault: true,
45
46
  skipTestFiles: true, // Skip test files - rules can be noisy there
46
47
 
@@ -17,6 +17,7 @@ import type {
17
17
  RunnerDefinition,
18
18
  RunnerResult,
19
19
  } from "../types.js";
20
+ import { PRIORITY } from "../priorities.js";
20
21
  import {
21
22
  calculateRuleComplexity,
22
23
  hasUnsupportedConditions,
@@ -389,7 +390,7 @@ function getAllNodes(node: any, currentDepth: number): unknown[] {
389
390
  const astGrepNapiRunner: RunnerDefinition = {
390
391
  id: "ast-grep-napi",
391
392
  appliesTo: ["jsts"],
392
- priority: 15,
393
+ priority: PRIORITY.SPECIALIZED_ANALYSIS,
393
394
  enabledByDefault: true,
394
395
  skipTestFiles: true,
395
396
 
@@ -18,6 +18,7 @@ import type {
18
18
  RunnerDefinition,
19
19
  RunnerResult,
20
20
  } from "../types.js";
21
+ import { PRIORITY } from "../priorities.js";
21
22
 
22
23
  const BIOME_CONFIGS = ["biome.json", "biome.jsonc"];
23
24
 
@@ -52,12 +53,16 @@ interface BiomeDiagnostic {
52
53
  tags?: string[];
53
54
  }
54
55
 
55
- function parseBiomeJson(raw: string, filePath: string): Diagnostic[] {
56
+ function parseBiomeJson(
57
+ raw: string,
58
+ filePath: string,
59
+ ): { diagnostics: Diagnostic[]; parseError?: string } {
56
60
  try {
57
61
  const result = JSON.parse(raw);
58
62
  const diagnostics: BiomeDiagnostic[] = result.diagnostics || [];
59
63
 
60
- return diagnostics.map((d) => ({
64
+ return {
65
+ diagnostics: diagnostics.map((d) => ({
61
66
  id: `biome:${d.category}:${d.location.start.line}`,
62
67
  message: d.message,
63
68
  filePath,
@@ -67,16 +72,20 @@ function parseBiomeJson(raw: string, filePath: string): Diagnostic[] {
67
72
  semantic: d.severity === "error" ? "blocking" : ("warning" as const),
68
73
  tool: "biome",
69
74
  rule: d.category,
70
- }));
71
- } catch {
72
- return [];
75
+ })),
76
+ };
77
+ } catch (err) {
78
+ return {
79
+ diagnostics: [],
80
+ parseError: err instanceof Error ? err.message : String(err),
81
+ };
73
82
  }
74
83
  }
75
84
 
76
85
  const biomeCheckJsonRunner: RunnerDefinition = {
77
86
  id: "biome-check-json",
78
87
  appliesTo: ["jsts"],
79
- priority: 10,
88
+ priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
80
89
  enabledByDefault: true,
81
90
 
82
91
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -122,13 +131,36 @@ const biomeCheckJsonRunner: RunnerDefinition = {
122
131
  { timeout: 30000, cwd },
123
132
  );
124
133
 
125
- const diagnostics =
134
+ const parsed =
126
135
  checkResult.status === 0 || checkResult.status === 1
127
136
  ? parseBiomeJson(
128
137
  checkResult.stdout || checkResult.stderr || "",
129
138
  ctx.filePath,
130
139
  )
131
- : [];
140
+ : { diagnostics: [] as Diagnostic[] };
141
+
142
+ if (parsed.parseError) {
143
+ const raw = checkResult.stdout || checkResult.stderr || "";
144
+ const preview = raw.replace(/\s+/g, " ").slice(0, 160);
145
+ return {
146
+ status: "failed",
147
+ diagnostics: [
148
+ {
149
+ id: "biome:parse-error:1",
150
+ message: `Biome JSON parse failed: ${parsed.parseError}${preview ? ` (output preview: ${preview})` : ""}`,
151
+ filePath: ctx.filePath,
152
+ line: 1,
153
+ column: 1,
154
+ severity: "warning",
155
+ semantic: "warning",
156
+ tool: "biome",
157
+ },
158
+ ],
159
+ semantic: "warning",
160
+ };
161
+ }
162
+
163
+ const diagnostics = parsed.diagnostics;
132
164
 
133
165
  // Step 2: Auto-fix (silently)
134
166
  await safeSpawnAsync(
@@ -10,13 +10,14 @@ import type {
10
10
  RunnerDefinition,
11
11
  RunnerResult,
12
12
  } from "../types.js";
13
+ import { PRIORITY } from "../priorities.js";
13
14
  import { createBiomeParser } from "./utils/diagnostic-parsers.js";
14
15
  import { biome } from "./utils/runner-helpers.js";
15
16
 
16
17
  const biomeRunner: RunnerDefinition = {
17
18
  id: "biome-lint",
18
19
  appliesTo: ["jsts", "json"],
19
- priority: 10,
20
+ priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
20
21
  enabledByDefault: true,
21
22
 
22
23
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -17,6 +17,7 @@ import type {
17
17
  RunnerDefinition,
18
18
  RunnerResult,
19
19
  } from "../types.js";
20
+ import { PRIORITY } from "../priorities.js";
20
21
 
21
22
  const ESLINT_CONFIGS = [
22
23
  ".eslintrc",
@@ -75,7 +76,10 @@ interface EslintFileResult {
75
76
  messages: EslintMessage[];
76
77
  }
77
78
 
78
- function parseEslintJson(raw: string, filePath: string): Diagnostic[] {
79
+ function parseEslintJson(
80
+ raw: string,
81
+ filePath: string,
82
+ ): { diagnostics: Diagnostic[]; parseError?: string } {
79
83
  try {
80
84
  const results: EslintFileResult[] = JSON.parse(raw);
81
85
  const diagnostics: Diagnostic[] = [];
@@ -98,16 +102,19 @@ function parseEslintJson(raw: string, filePath: string): Diagnostic[] {
98
102
  }
99
103
  }
100
104
 
101
- return diagnostics;
102
- } catch {
103
- return [];
105
+ return { diagnostics };
106
+ } catch (err) {
107
+ return {
108
+ diagnostics: [],
109
+ parseError: err instanceof Error ? err.message : String(err),
110
+ };
104
111
  }
105
112
  }
106
113
 
107
114
  const eslintRunner: RunnerDefinition = {
108
115
  id: "eslint",
109
116
  appliesTo: ["jsts"],
110
- priority: 12,
117
+ priority: PRIORITY.LINT_SECONDARY,
111
118
  enabledByDefault: true,
112
119
 
113
120
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -166,7 +173,28 @@ const eslintRunner: RunnerDefinition = {
166
173
  return { status: "succeeded", diagnostics: [], semantic: "none" };
167
174
  }
168
175
 
169
- const diagnostics = parseEslintJson(raw, ctx.filePath);
176
+ const parsed = parseEslintJson(raw, ctx.filePath);
177
+ if (parsed.parseError && raw.trim().length > 0) {
178
+ const preview = raw.replace(/\s+/g, " ").slice(0, 160);
179
+ return {
180
+ status: "failed",
181
+ diagnostics: [
182
+ {
183
+ id: "eslint:parse-error:1",
184
+ message: `ESLint JSON parse failed: ${parsed.parseError}${preview ? ` (output preview: ${preview})` : ""}`,
185
+ filePath: ctx.filePath,
186
+ line: 1,
187
+ column: 1,
188
+ severity: "warning",
189
+ semantic: "warning",
190
+ tool: "eslint",
191
+ },
192
+ ],
193
+ semantic: "warning",
194
+ };
195
+ }
196
+
197
+ const diagnostics = parsed.diagnostics;
170
198
  if (diagnostics.length === 0) {
171
199
  return { status: "succeeded", diagnostics: [], semantic: "none" };
172
200
  }
@@ -12,11 +12,12 @@ import type {
12
12
  RunnerDefinition,
13
13
  RunnerResult,
14
14
  } from "../types.js";
15
+ import { PRIORITY } from "../priorities.js";
15
16
 
16
17
  const goVetRunner: RunnerDefinition = {
17
18
  id: "go-vet",
18
19
  appliesTo: ["go"],
19
- priority: 15,
20
+ priority: PRIORITY.SPECIALIZED_ANALYSIS,
20
21
  enabledByDefault: true,
21
22
 
22
23
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -21,6 +21,7 @@ import type {
21
21
  RunnerDefinition,
22
22
  RunnerResult,
23
23
  } from "../types.js";
24
+ import { PRIORITY } from "../priorities.js";
24
25
 
25
26
  const GOLANGCI_CONFIGS = [
26
27
  ".golangci.yml",
@@ -82,7 +83,7 @@ function parseGolangciJson(raw: string, filePath: string): Diagnostic[] {
82
83
  const golangciRunner: RunnerDefinition = {
83
84
  id: "golangci-lint",
84
85
  appliesTo: ["go"],
85
- priority: 20,
86
+ priority: PRIORITY.GENERAL_ANALYSIS,
86
87
  enabledByDefault: true,
87
88
 
88
89
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -2,7 +2,7 @@
2
2
  * Runner definitions for pi-lens dispatch system
3
3
  */
4
4
 
5
- import { registerRunner } from "../dispatcher.js";
5
+ import type { RunnerRegistry } from "../types.js";
6
6
  import architectRunner from "./architect.js";
7
7
  import astGrepNapiRunner from "./ast-grep-napi.js";
8
8
  import biomeRunner from "./biome.js";
@@ -28,29 +28,31 @@ import treeSitterRunner from "./tree-sitter.js";
28
28
  import tsLspRunner from "./ts-lsp.js";
29
29
  import typeSafetyRunner from "./type-safety.js";
30
30
 
31
- // Register all runners (ordered by priority)
32
- // Unified LSP runner for all languages (TypeScript, Python, Go, Rust, etc.) - priority 4
33
- registerRunner(lspRunner); // Unified LSP type-checking for all languages (priority 4)
34
- registerRunner(tsLspRunner); // TypeScript type-checking (priority 5) - fallback when --lens-lsp disabled
35
- registerRunner(pyrightRunner); // Python type-checking (priority 5) - fallback when --lens-lsp disabled
36
- registerRunner(biomeCheckJsonRunner); // Biome check with JSON output for diagnostic capture (priority 9)
37
- // DISABLED in post-write dispatch - ast-grep-napi can crash. Enabled via /lens-booboo plan only.
38
- registerRunner(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15, post-write disabled)
39
- registerRunner(biomeRunner); // Biome formatting/linting (priority 10)
40
- registerRunner(treeSitterRunner); // Tree-sitter structural analysis (priority 14)
41
- registerRunner(ruffRunner); // Python linting (priority 10)
42
- registerRunner(pythonSlopRunner); // Python slop via CLI (priority 25)
43
- registerRunner(typeSafetyRunner); // Type safety checks (priority 20)
44
- registerRunner(shellcheckRunner); // Shell script linting (priority 20)
45
- // DISABLED: registerRunner(astGrepRunner); // Replaced by ast-grep-napi for dispatch
46
- // CLI ast-grep kept for ast_grep_search/ast_grep_replace tools only
47
- registerRunner(similarityRunner); // Semantic reuse detection (priority 35)
48
- registerRunner(architectRunner); // Architectural rules (priority 40)
49
- registerRunner(eslintRunner); // ESLint (priority 12, jsts, config-gated)
50
- registerRunner(golangciRunner); // golangci-lint (priority 20, go, config-gated)
51
- registerRunner(rubocopRunner); // RuboCop lint (priority 10, ruby)
52
- registerRunner(spellcheckRunner); // Spellcheck for markdown/docs (priority 30)
53
- registerRunner(yamllintRunner); // YAML lint (priority 22)
54
- registerRunner(sqlfluffRunner); // SQL lint (priority 24)
55
- registerRunner(goVetRunner); // Go analysis (priority 50)
56
- registerRunner(rustClippyRunner); // Rust analysis (priority 50)
31
+ export function registerDefaultRunners(registry: RunnerRegistry): void {
32
+ // Register all runners (ordered by priority)
33
+ // Unified LSP runner for all languages (TypeScript, Python, Go, Rust, etc.) - priority 4
34
+ registry.register(lspRunner); // Unified LSP type-checking for all languages (priority 4)
35
+ registry.register(tsLspRunner); // TypeScript type-checking (priority 5) - fallback when --lens-lsp disabled
36
+ registry.register(pyrightRunner); // Python type-checking (priority 5) - fallback when --lens-lsp disabled
37
+ registry.register(biomeCheckJsonRunner); // Biome check with JSON output for diagnostic capture (priority 9)
38
+ // DISABLED in post-write dispatch - ast-grep-napi can crash. Enabled via /lens-booboo plan only.
39
+ registry.register(astGrepNapiRunner); // TS/JS structural analysis via NAPI (priority 15, post-write disabled)
40
+ registry.register(biomeRunner); // Biome formatting/linting (priority 10)
41
+ registry.register(treeSitterRunner); // Tree-sitter structural analysis (priority 14)
42
+ registry.register(ruffRunner); // Python linting (priority 10)
43
+ registry.register(pythonSlopRunner); // Python slop via CLI (priority 25)
44
+ registry.register(typeSafetyRunner); // Type safety checks (priority 20)
45
+ registry.register(shellcheckRunner); // Shell script linting (priority 20)
46
+ // DISABLED: registerRunner(astGrepRunner); // Replaced by ast-grep-napi for dispatch
47
+ // CLI ast-grep kept for ast_grep_search/ast_grep_replace tools only
48
+ registry.register(similarityRunner); // Semantic reuse detection (priority 35)
49
+ registry.register(architectRunner); // Architectural rules (priority 40)
50
+ registry.register(eslintRunner); // ESLint (priority 12, jsts, config-gated)
51
+ registry.register(golangciRunner); // golangci-lint (priority 20, go, config-gated)
52
+ registry.register(rubocopRunner); // RuboCop lint (priority 10, ruby)
53
+ registry.register(spellcheckRunner); // Spellcheck for markdown/docs (priority 30)
54
+ registry.register(yamllintRunner); // YAML lint (priority 22)
55
+ registry.register(sqlfluffRunner); // SQL lint (priority 24)
56
+ registry.register(goVetRunner); // Go analysis (priority 50)
57
+ registry.register(rustClippyRunner); // Rust analysis (priority 50)
58
+ }
@@ -21,10 +21,38 @@ import type {
21
21
  RunnerDefinition,
22
22
  RunnerResult,
23
23
  } from "../types.js";
24
+ import { PRIORITY } from "../priorities.js";
24
25
  import { readFileContent } from "./utils.js";
25
26
 
26
27
  const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
27
28
  const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
29
+ const MAX_CODE_ACTION_LOOKUPS = 6;
30
+ const MAX_CODE_ACTION_TITLES = 3;
31
+
32
+ function normalizeActionTitle(title: string): string {
33
+ return title.replace(/\s+/g, " ").trim();
34
+ }
35
+
36
+ function buildCodeActionSuggestion(
37
+ actions: import("../../lsp/client.js").LSPCodeAction[],
38
+ ): string | undefined {
39
+ if (!actions.length) return undefined;
40
+ const quickFixes = actions.filter((action) =>
41
+ action.kind?.startsWith("quickfix"),
42
+ );
43
+ if (!quickFixes.length) return undefined;
44
+
45
+ const titles = Array.from(
46
+ new Set(
47
+ quickFixes
48
+ .map((action) => normalizeActionTitle(action.title))
49
+ .filter((title) => title.length > 0),
50
+ ),
51
+ ).slice(0, MAX_CODE_ACTION_TITLES);
52
+
53
+ if (!titles.length) return undefined;
54
+ return `LSP quick fixes: ${titles.join("; ")}`;
55
+ }
28
56
 
29
57
  const lspRunner: RunnerDefinition = {
30
58
  id: "lsp",
@@ -42,7 +70,7 @@ const lspRunner: RunnerDefinition = {
42
70
  "css",
43
71
  "yaml",
44
72
  ],
45
- priority: 4, // Run before everything (even ts-lsp was priority 5)
73
+ priority: PRIORITY.LSP_PRIMARY,
46
74
  enabledByDefault: true,
47
75
 
48
76
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -125,9 +153,35 @@ const lspRunner: RunnerDefinition = {
125
153
 
126
154
  // Convert LSP diagnostics to our format
127
155
  // Defensive: filter out malformed diagnostics that may lack range
128
- const diagnostics: Diagnostic[] = lspDiags
129
- .filter((d) => d.range?.start?.line !== undefined)
130
- .map((d) => ({
156
+ const validLspDiags = lspDiags.filter((d) => d.range?.start?.line !== undefined);
157
+ const fixSuggestionByIndex = new Map<number, string>();
158
+
159
+ const blockingDiagIndexes = validLspDiags
160
+ .map((d, idx) => ({ d, idx }))
161
+ .filter(({ d }) => d.severity === 1)
162
+ .slice(0, MAX_CODE_ACTION_LOOKUPS);
163
+
164
+ for (const { d, idx } of blockingDiagIndexes) {
165
+ try {
166
+ const start = d.range.start;
167
+ const end = d.range.end ?? d.range.start;
168
+ const actions = await lspService.codeAction(
169
+ ctx.filePath,
170
+ start.line,
171
+ start.character,
172
+ end.line,
173
+ end.character,
174
+ );
175
+ const suggestion = buildCodeActionSuggestion(actions);
176
+ if (suggestion) {
177
+ fixSuggestionByIndex.set(idx, suggestion);
178
+ }
179
+ } catch {
180
+ // Best-effort enrichment only; base diagnostics remain authoritative.
181
+ }
182
+ }
183
+
184
+ const diagnostics: Diagnostic[] = validLspDiags.map((d, idx) => ({
131
185
  id: `lsp:${d.code ?? "unknown"}:${d.range.start.line}`,
132
186
  message: d.message,
133
187
  filePath: diagnosticPath,
@@ -143,6 +197,8 @@ const lspRunner: RunnerDefinition = {
143
197
  : "none",
144
198
  tool: "lsp",
145
199
  code: String(d.code ?? ""),
200
+ fixable: fixSuggestionByIndex.has(idx),
201
+ fixSuggestion: fixSuggestionByIndex.get(idx),
146
202
  }));
147
203
 
148
204
  const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
@@ -14,6 +14,7 @@ import type {
14
14
  RunnerDefinition,
15
15
  RunnerResult,
16
16
  } from "../types.js";
17
+ import { PRIORITY } from "../priorities.js";
17
18
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
18
19
 
19
20
  const oxlint = createAvailabilityChecker("oxlint", ".exe");
@@ -21,7 +22,7 @@ const oxlint = createAvailabilityChecker("oxlint", ".exe");
21
22
  const oxlintRunner: RunnerDefinition = {
22
23
  id: "oxlint",
23
24
  appliesTo: ["jsts"],
24
- priority: 12,
25
+ priority: PRIORITY.LINT_SECONDARY,
25
26
  enabledByDefault: false, // Opt-in: may conflict with ESLint in existing projects
26
27
  skipTestFiles: true,
27
28
 
@@ -16,6 +16,7 @@ import type {
16
16
  RunnerDefinition,
17
17
  RunnerResult,
18
18
  } from "../types.js";
19
+ import { PRIORITY } from "../priorities.js";
19
20
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
20
21
 
21
22
  const pyright = createAvailabilityChecker("pyright", ".exe");
@@ -23,7 +24,7 @@ const pyright = createAvailabilityChecker("pyright", ".exe");
23
24
  const pyrightRunner: RunnerDefinition = {
24
25
  id: "pyright",
25
26
  appliesTo: ["python"],
26
- priority: 5, // Higher priority than ruff (10) - type errors are more important
27
+ priority: PRIORITY.LSP_FALLBACK,
27
28
  enabledByDefault: true,
28
29
 
29
30
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -18,6 +18,7 @@ import type {
18
18
  RunnerDefinition,
19
19
  RunnerResult,
20
20
  } from "../types.js";
21
+ import { PRIORITY } from "../priorities.js";
21
22
  import {
22
23
  createConfigFinder,
23
24
  getSgCommand,
@@ -29,7 +30,7 @@ const findSlopConfig = createConfigFinder("python-slop-rules");
29
30
  const pythonSlopRunner: RunnerDefinition = {
30
31
  id: "python-slop",
31
32
  appliesTo: ["python"],
32
- priority: 25, // Between pyright (5) and ruff (10)
33
+ priority: PRIORITY.PYTHON_SLOP,
33
34
  enabledByDefault: true,
34
35
  skipTestFiles: true, // Slop rules can be noisy in test files
35
36
 
@@ -18,6 +18,7 @@ import type {
18
18
  RunnerDefinition,
19
19
  RunnerResult,
20
20
  } from "../types.js";
21
+ import { PRIORITY } from "../priorities.js";
21
22
 
22
23
  function findRubocop(cwd: string): { cmd: string; args: string[] } {
23
24
  // Prefer bundle exec if Gemfile exists
@@ -93,7 +94,7 @@ function parseRubocopJson(raw: string, filePath: string): Diagnostic[] {
93
94
  const rubocopRunner: RunnerDefinition = {
94
95
  id: "rubocop",
95
96
  appliesTo: ["ruby"],
96
- priority: 10,
97
+ priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
97
98
  enabledByDefault: true,
98
99
 
99
100
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -14,6 +14,7 @@ import type {
14
14
  RunnerDefinition,
15
15
  RunnerResult,
16
16
  } from "../types.js";
17
+ import { PRIORITY } from "../priorities.js";
17
18
  import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
18
19
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
19
20
 
@@ -55,7 +56,7 @@ function parseRuffJson(raw: string, filePath: string): Diagnostic[] {
55
56
  const ruffRunner: RunnerDefinition = {
56
57
  id: "ruff-lint",
57
58
  appliesTo: ["python"],
58
- priority: 10,
59
+ priority: PRIORITY.FORMAT_AND_LINT_PRIMARY,
59
60
  enabledByDefault: true,
60
61
 
61
62
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -15,11 +15,12 @@ import type {
15
15
  RunnerDefinition,
16
16
  RunnerResult,
17
17
  } from "../types.js";
18
+ import { PRIORITY } from "../priorities.js";
18
19
 
19
20
  const rustClippyRunner: RunnerDefinition = {
20
21
  id: "rust-clippy",
21
22
  appliesTo: ["rust"],
22
- priority: 15,
23
+ priority: PRIORITY.SPECIALIZED_ANALYSIS,
23
24
  enabledByDefault: true,
24
25
 
25
26
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -30,6 +30,7 @@ import type {
30
30
  RunnerDefinition,
31
31
  RunnerResult,
32
32
  } from "../types.js";
33
+ import { PRIORITY } from "../priorities.js";
33
34
 
34
35
  const shellcheck = createAvailabilityChecker("shellcheck", ".exe");
35
36
 
@@ -129,7 +130,7 @@ function parseShellcheckOutput(raw: string, filePath: string): Diagnostic[] {
129
130
  const shellcheckRunner: RunnerDefinition = {
130
131
  id: "shellcheck",
131
132
  appliesTo: ["shell"],
132
- priority: 20,
133
+ priority: PRIORITY.GENERAL_ANALYSIS,
133
134
  enabledByDefault: true,
134
135
  skipTestFiles: false, // Shell scripts in test directories should still be checked
135
136
 
@@ -24,6 +24,7 @@ import type {
24
24
  RunnerDefinition,
25
25
  RunnerResult,
26
26
  } from "../types.js";
27
+ import { PRIORITY } from "../priorities.js";
27
28
 
28
29
  // Singleton Rust client — initialised once, reused across runner invocations.
29
30
  const rustClient = new NativeRustCoreClient();
@@ -97,7 +98,7 @@ export function hasMeaningfulNameOverlap(sourceName: string, targetName: string)
97
98
  const similarityRunner: RunnerDefinition = {
98
99
  id: "similarity",
99
100
  appliesTo: ["jsts"], // TypeScript/JavaScript only for MVP
100
- priority: 35, // After ts-lsp, before ast-grep
101
+ priority: PRIORITY.SIMILARITY,
101
102
  enabledByDefault: true,
102
103
 
103
104
  async run(ctx: DispatchContext): Promise<RunnerResult> {
@@ -27,6 +27,7 @@ import type {
27
27
  RunnerDefinition,
28
28
  RunnerResult,
29
29
  } from "../types.js";
30
+ import { PRIORITY } from "../priorities.js";
30
31
 
31
32
  const typos = createAvailabilityChecker("typos", ".exe");
32
33
 
@@ -91,7 +92,7 @@ function parseTyposOutput(raw: string, filePath: string): Diagnostic[] {
91
92
  const spellcheckRunner: RunnerDefinition = {
92
93
  id: "spellcheck",
93
94
  appliesTo: ["markdown"],
94
- priority: 30, // Run after code quality checks (biome=10, slop=25)
95
+ priority: PRIORITY.DOC_QUALITY,
95
96
  enabledByDefault: true,
96
97
  skipTestFiles: false, // Check docs in test files too
97
98
 
@@ -9,6 +9,7 @@ import type {
9
9
  RunnerDefinition,
10
10
  RunnerResult,
11
11
  } from "../types.js";
12
+ import { PRIORITY } from "../priorities.js";
12
13
 
13
14
  const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
14
15
 
@@ -90,7 +91,7 @@ function parseSqlfluffOutput(raw: string, filePath: string): Diagnostic[] {
90
91
  const sqlfluffRunner: RunnerDefinition = {
91
92
  id: "sqlfluff",
92
93
  appliesTo: ["sql"],
93
- priority: 24,
94
+ priority: PRIORITY.SQL_LINT,
94
95
  enabledByDefault: true,
95
96
  skipTestFiles: false,
96
97