pi-lens 3.8.44 → 3.8.45

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 (68) hide show
  1. package/CHANGELOG.md +2425 -2349
  2. package/README.md +70 -42
  3. package/clients/ast-grep-client.ts +5 -5
  4. package/clients/cache/rule-cache.ts +2 -1
  5. package/clients/dispatch/dispatcher.ts +49 -0
  6. package/clients/dispatch/integration.ts +75 -2
  7. package/clients/dispatch/plan.ts +5 -2
  8. package/clients/dispatch/runners/cpp-check.ts +73 -6
  9. package/clients/dispatch/runners/go-vet.ts +7 -7
  10. package/clients/dispatch/runners/index.ts +4 -0
  11. package/clients/dispatch/runners/python-slop.ts +2 -2
  12. package/clients/dispatch/runners/rust-clippy.ts +9 -9
  13. package/clients/dispatch/runners/swiftlint.ts +129 -0
  14. package/clients/dispatch/runners/tree-sitter.ts +164 -54
  15. package/clients/dispatch/runners/utils/runner-helpers.ts +76 -25
  16. package/clients/dispatch/runners/vale.ts +175 -0
  17. package/clients/dispatch/types.ts +3 -2
  18. package/clients/feature-hints.ts +79 -0
  19. package/clients/file-role.ts +7 -0
  20. package/clients/go-client.ts +1 -1
  21. package/clients/installer/index.ts +45 -0
  22. package/clients/knip-client.ts +39 -4
  23. package/clients/language-policy.ts +13 -3
  24. package/clients/language-profile.ts +21 -0
  25. package/clients/latency-logger.ts +2 -0
  26. package/clients/lens-config.ts +94 -0
  27. package/clients/lsp/client.ts +5 -1
  28. package/clients/lsp/index.ts +128 -16
  29. package/clients/lsp/server-strategies.ts +6 -3
  30. package/clients/lsp/server.ts +168 -16
  31. package/clients/read-expansion.ts +72 -6
  32. package/clients/read-guard.ts +148 -17
  33. package/clients/review-graph/builder.ts +314 -36
  34. package/clients/review-graph/query.ts +55 -10
  35. package/clients/review-graph/service.ts +12 -8
  36. package/clients/review-graph/workspace-modules.ts +482 -0
  37. package/clients/runtime-session.ts +18 -21
  38. package/clients/runtime-turn.ts +101 -56
  39. package/clients/rust-client.ts +1 -1
  40. package/clients/sg-runner.ts +50 -47
  41. package/clients/source-groups.ts +140 -0
  42. package/clients/test-runner-client.ts +37 -2
  43. package/clients/tool-policy.ts +26 -3
  44. package/clients/tree-sitter-client.ts +153 -0
  45. package/clients/tree-sitter-query-loader.ts +23 -7
  46. package/clients/tree-sitter-symbol-extractor.ts +54 -0
  47. package/index.ts +36 -20
  48. package/package.json +3 -1
  49. package/rules/tree-sitter-queries/c/hardcoded-secrets.yml +55 -0
  50. package/rules/tree-sitter-queries/c/memset-sensitive-data.yml +50 -0
  51. package/rules/tree-sitter-queries/c/no-bit-fields.yml +48 -0
  52. package/rules/tree-sitter-queries/c/no-octal-literals.yml +46 -0
  53. package/rules/tree-sitter-queries/c/no-pointer-arithmetic-array-access.yml +50 -0
  54. package/rules/tree-sitter-queries/c/no-redundant-pointer-ops.yml +57 -0
  55. package/rules/tree-sitter-queries/c/no-reserved-identifiers.yml +49 -0
  56. package/rules/tree-sitter-queries/c/no-stdlib-name-as-id.yml +53 -0
  57. package/rules/tree-sitter-queries/c/non-case-label-in-switch.yml +56 -0
  58. package/rules/tree-sitter-queries/c/noreturn-returns.yml +60 -0
  59. package/scripts/analyze-pi-lens-logs.mjs +1015 -0
  60. package/skills/ast-grep/SKILL.md +17 -14
  61. package/skills/lsp-navigation/SKILL.md +45 -30
  62. package/tools/ast-grep-search.js +9 -6
  63. package/tools/ast-grep-search.ts +14 -6
  64. package/tools/lsp-diagnostics.js +228 -48
  65. package/tools/lsp-diagnostics.ts +382 -64
  66. package/tools/lsp-navigation.js +128 -8
  67. package/tools/lsp-navigation.ts +193 -10
  68. /package/rules/tree-sitter-queries/{c → c-disabled}/case-range-multiple-values.yml +0 -0
package/README.md CHANGED
@@ -13,7 +13,7 @@ pi-lens focuses on real-time inline code feedback for AI agents.
13
13
  On every `write` and `edit`, pi-lens runs a fast, language-aware pipeline (checks depend on file language, project config, and installed tools):
14
14
 
15
15
  1. **Secrets scan** — blocking; aborts the write if credentials are detected
16
- 2. **Auto-format** — deferred to `agent_end` by default; queued files are formatted once after all agent tool calls complete. Use `--immediate-format` for per-edit formatting
16
+ 2. **Auto-format** — deferred to `agent_end` by default; queued files are formatted once after all agent tool calls complete. Use `--immediate-format` or global config `format.mode: "immediate"` for per-edit formatting
17
17
  3. **Auto-fix** — safe autofixes from 6 tools (Biome `check --write`, Ruff `check --fix`, ESLint `--fix`, stylelint `--fix`, sqlfluff `fix`, RuboCop `-a`) applied before analysis
18
18
  4. **LSP file sync** — opens/updates the file in active language servers
19
19
  5. **Dispatch lint** — parallel runner groups: LSP diagnostics, tree-sitter structural rules, ast-grep security/correctness rules, fact rules, language-specific linters, experimental Semgrep security scans, similarity detection
@@ -83,13 +83,15 @@ pi-lens includes **37 language server definitions**. LSP is **enabled by default
83
83
  { "warmFiles": ["src/main.cpp", "src/lib.cpp"] }
84
84
  ```
85
85
 
86
- LSP servers for: TypeScript, Deno, Python (pyright + pylsp), Go, Rust, Ruby (ruby-lsp + solargraph), PHP, C# (omnisharp), F#, Java, Kotlin, Swift, Dart, Lua, C/C++, Zig, Haskell, Elixir, Gleam, OCaml, Clojure, Terraform, Nix, Bash, Docker, YAML, JSON, HTML, TOML, Prisma, Vue, Svelte, ESLint, CSS.
86
+ **Agent LSP tools:** `lsp_diagnostics` can check one file, a directory, or an explicit `filePaths` batch with bounded concurrency. `lsp_navigation` provides definitions, references, hover, workspace symbols, call hierarchy, rename edits, and `findSymbol` for filtered document-symbol lookup.
87
+
88
+ LSP servers for: TypeScript, Deno, Python (pyright/basedpyright + jedi), Go, Rust, Ruby (ruby-lsp + solargraph), PHP, C# (omnisharp), F#, Java, Kotlin, Swift, Dart, Lua, C/C++, Zig, Haskell, Elixir, Gleam, OCaml, Clojure, Terraform, Nix, Bash, Docker, YAML, JSON, HTML, TOML, Prisma, Vue, Svelte, ESLint, CSS.
87
89
 
88
90
  ### Formatters
89
91
 
90
- pi-lens auto-detects and runs **26 formatters** based on project config:
92
+ pi-lens auto-detects and runs **32 formatters** based on project config:
91
93
 
92
- biome, prettier, ruff, black, sqlfluff, gofmt, rustfmt, zig fmt, dart format, shfmt, nixfmt, mix format, ocamlformat, clang-format, ktlint, rubocop, standardrb, gleam format, terraform fmt, php-cs-fixer, csharpier, fantomas, swiftformat, stylua, ormolu, taplo
94
+ biome, prettier, oxfmt, ruff, black, sqlfluff, gofmt, rustfmt, zig fmt, dart format, shfmt, nixfmt, mix format, ocamlformat, clang-format, ktlint, rubocop, standardrb, gleam format, terraform fmt, php-cs-fixer, csharpier, fantomas, swiftformat, stylua, ormolu, taplo, fish_indent, google-java-format, cljfmt, cmake-format, psscriptanalyzer-format
93
95
 
94
96
  Detection rules:
95
97
 
@@ -110,7 +112,7 @@ pi-lens enforces a **read-before-edit** policy on all file writes and edits. Bef
110
112
  - **File-modified block** — blocks if the file changed on disk since the last read (auto-format, external tool, or a previous edit that was then reformatted)
111
113
  - **Out-of-range block** — blocks if the edit target lines fall outside the ranges previously read, ensuring the agent cannot modify code it hasn't seen
112
114
 
113
- Coverage is tracked across multiple reads: two reads of lines 1–100 and 101–200 together satisfy a full-file write. Symbol-expanded reads (small reads silently widened to the enclosing symbol via tree-sitter) count toward coverage at the symbol level. Markdown, text, and log files are exempt.
115
+ Coverage is tracked across multiple reads: two reads of lines 1–100 and 101–200 together satisfy a full-file write. Symbol-expanded reads (small reads silently widened to the enclosing symbol via tree-sitter) count toward coverage at the symbol level. Markdown files generate a warning instead of blocking (edits outside the section-expanded read range are warned, not silently passed). Plain-text (`.txt`) and log (`.log`) files remain fully exempt.
114
116
 
115
117
  Override for a single edit: `/lens-allow-edit <path>`
116
118
 
@@ -127,12 +129,14 @@ Supported: TypeScript, TSX, JavaScript, JSX, Python, Go, Rust, Ruby.
127
129
  Covers JavaScript/TypeScript, Python, Go, Rust, Ruby, Shell, and CMake. A TypeScript AST-based fact-rule engine extracts function-level metrics and evaluates quality and security rules inline. Blocking rules surface immediately at write time; advisory rules are available via `/lens-booboo`.
128
130
 
129
131
  **Blocking (surface inline at write time):**
132
+
130
133
  - **cors-wildcard** — `Access-Control-Allow-Origin: *` in server-side code
131
134
  - **error-swallowing** — empty catch block (skips documented local fallbacks and fs-boundary catches)
132
135
  - **no-commented-credentials** — password/token/secret in commented-out code
133
136
  - **high-entropy-string** — string literals with suspiciously high Shannon entropy (possible hardcoded secret)
134
137
 
135
138
  **Advisory (accessible via `/lens-booboo`):**
139
+
136
140
  - **high-complexity** / **no-complex-conditionals** — cyclomatic complexity and deeply nested conditions
137
141
  - **high-fan-out** — function calls too many distinct functions (coordination smell)
138
142
  - **unsafe-boundary** — dangerous `any` casts at API boundaries
@@ -273,6 +277,26 @@ pi --lens-semgrep # Enable Semgrep dispatch when a local/configured Semg
273
277
  pi --lens-semgrep-config p/ci # Explicit Semgrep config for dispatch (requires --lens-semgrep)
274
278
  ```
275
279
 
280
+ ## Global Config
281
+
282
+ pi-lens reads optional user preferences from `~/.pi-lens/config.json` (`%USERPROFILE%\\.pi-lens\\config.json` on Windows). Unknown keys are ignored, and missing or invalid config falls back to defaults.
283
+
284
+ Hide the diagnostics widget by default and run formatting immediately after write/edit tool calls instead of at `agent_end`:
285
+
286
+ ```json
287
+ {
288
+ "widget": {
289
+ "visible": false
290
+ },
291
+ "format": {
292
+ "enabled": true,
293
+ "mode": "immediate"
294
+ }
295
+ }
296
+ ```
297
+
298
+ `format.mode` can be `"deferred"` (default) or `"immediate"`. Set `format.enabled` to `false` to match `--no-autoformat`. `/lens-widget-toggle` still works as a session-only override.
299
+
276
300
  ## Environment Variables
277
301
 
278
302
  - `PILENS_DATA_DIR` — redirect per-project state (scanner caches,
@@ -291,51 +315,55 @@ pi --lens-semgrep-config p/ci # Explicit Semgrep config for dispatch (requires
291
315
  - `/lens-widget-toggle` — show/hide the pi-lens diagnostics widget below the editor
292
316
  - `/lens-booboo` — full quality report for current project state
293
317
  - `/lens-health` — runtime health, latency, and diagnostic telemetry
318
+ - `/lens-allow-edit <path>` — override the read-before-edit guard for a single edit
294
319
  - `/lens-tools` — tool installation status: globally installed, auto-installed, or npx fallback
295
320
  - `/lens-tdi` — Technical Debt Index (TDI) and project health trend
296
321
  - `/lens-semgrep` — manage experimental Semgrep dispatch (`status`, `init`, `enable`, `disable`, `clear`)
297
322
 
298
323
  ## Language Coverage
299
324
 
300
- pi-lens supports **35+ languages** through dispatch runners and LSP integration.
325
+ pi-lens supports **36+ languages** through dispatch runners and LSP integration.
301
326
 
302
327
  Formatting uses a single selected formatter per file: explicit project config wins, otherwise pi-lens uses a smart default where supported, and config-first ecosystems do not autoformat without config.
303
328
 
304
329
  Dispatch is diagnostics-oriented: automatic formatting and safe autofix happen in the post-write pipeline rather than through dispatch format-check runners.
305
330
 
306
- | Language | LSP | Dispatch Runners | Formatter |
307
- | --------------------- | --- | -------------------------------------------------------------------------------------------------------------- | ------------------- |
308
- | JavaScript/TypeScript | ✓ | lsp, ts-lsp, biome-check-json, tree-sitter, ast-grep-napi, type-safety, similarity, fact-rules, eslint, oxlint | biome, prettier |
309
- | Python | ✓ | lsp, pyright, ruff-lint, tree-sitter, python-slop | ruff, black |
310
- | Go | ✓ | lsp, go-vet, golangci-lint, tree-sitter | gofmt |
311
- | Rust | ✓ | lsp, rust-clippy, tree-sitter | rustfmt |
312
- | Ruby | ✓ | lsp, rubocop, tree-sitter | rubocop, standardrb |
313
- | C/C++ | ✓ | lsp, cpp-check | clang-format |
314
- | Shell | ✓ | lsp, shellcheck | shfmt |
315
- | CSS/SCSS/Less | ✓ | lsp, stylelint | biome, prettier |
316
- | HTML | ✓ | lsp, htmlhint | prettier |
317
- | YAML | ✓ | lsp, yamllint | prettier |
318
- | JSON | ✓ | lsp | biome, prettier |
319
- | SQL | | sqlfluff | sqlfluff |
320
- | Markdown | | spellcheck, markdownlint | prettier |
321
- | Docker | ✓ | lsp, hadolint | — |
322
- | PHP | | lsp, php-lint, phpstan | php-cs-fixer |
323
- | PowerShell | | lsp, psscriptanalyzer | |
324
- | Prisma | ✓ | lsp, prisma-validate | — |
325
- | C# | ✓ | lsp, dotnet-build | csharpier |
326
- | F# | ✓ | lsp | fantomas |
327
- | Java | ✓ | lsp, javac | — |
328
- | Kotlin | ✓ | lsp, ktlint | ktlint |
329
- | Swift | ✓ | lsp | swiftformat |
330
- | Dart | ✓ | lsp, dart-analyze | dart format |
331
- | Lua | ✓ | lsp | stylua |
332
- | Zig | ✓ | lsp, zig-check | zig fmt |
333
- | Haskell | ✓ | lsp | ormolu |
334
- | Elixir | ✓ | lsp, elixir-check, credo | mix format |
335
- | Gleam | ✓ | lsp, gleam-check | gleam format |
336
- | OCaml | ✓ | lsp | ocamlformat |
337
- | Clojure | ✓ | lsp | |
338
- | Terraform | ✓ | lsp, tflint | terraform fmt |
339
- | Nix | ✓ | lsp | nixfmt |
340
- | TOML | ✓ | lsp, taplo | taplo |
341
- | CMake | ✓ | lsp | |
331
+ | Language | LSP | Dispatch Runners | Formatter |
332
+ | --------------------- | --- | -------------------------------------------------------------------------------------------------------------- | ----------------------- |
333
+ | JavaScript/TypeScript | ✓ | lsp, ts-lsp, biome-check-json, tree-sitter, ast-grep-napi, type-safety, similarity, fact-rules, eslint, oxlint | biome, prettier |
334
+ | Python | ✓ | lsp, pyright, ruff-lint, tree-sitter, python-slop | ruff, black |
335
+ | Go | ✓ | lsp, go-vet, golangci-lint, tree-sitter | gofmt |
336
+ | Rust | ✓ | lsp, rust-clippy, tree-sitter | rustfmt |
337
+ | Ruby | ✓ | lsp, rubocop, tree-sitter | rubocop, standardrb |
338
+ | C/C++ | ✓ | lsp, cpp-check, tree-sitter | clang-format |
339
+ | Shell | ✓ | lsp, shellcheck | shfmt |
340
+ | Fish | ✓ | lsp, fish-indent | fish_indent |
341
+ | CSS/SCSS/Less | ✓ | lsp, stylelint | biome, prettier |
342
+ | HTML | ✓ | lsp, htmlhint | prettier |
343
+ | YAML | ✓ | lsp, yamllint | prettier |
344
+ | JSON | | lsp | biome, prettier |
345
+ | Svelte | | lsp | |
346
+ | Vue | ✓ | lsp | — |
347
+ | SQL | | sqlfluff | sqlfluff |
348
+ | Markdown | | spellcheck, markdownlint, vale | prettier |
349
+ | Docker | ✓ | lsp, hadolint | — |
350
+ | PHP | ✓ | lsp, php-lint, phpstan | php-cs-fixer |
351
+ | PowerShell | ✓ | lsp, psscriptanalyzer | psscriptanalyzer-format |
352
+ | Prisma | ✓ | lsp, prisma-validate | — |
353
+ | C# | ✓ | lsp, dotnet-build | csharpier |
354
+ | F# | ✓ | lsp | fantomas |
355
+ | Java | ✓ | lsp, javac | google-java-format |
356
+ | Kotlin | ✓ | lsp, ktlint, detekt | ktlint |
357
+ | Swift | ✓ | lsp, swiftlint | swiftformat |
358
+ | Dart | ✓ | lsp, dart-analyze | dart format |
359
+ | Lua | ✓ | lsp | stylua |
360
+ | Zig | ✓ | lsp, zig-check | zig fmt |
361
+ | Haskell | ✓ | lsp | ormolu |
362
+ | Elixir | ✓ | lsp, elixir-check, credo | mix format |
363
+ | Gleam | ✓ | lsp, gleam-check | gleam format |
364
+ | OCaml | ✓ | lsp | ocamlformat |
365
+ | Clojure | ✓ | lsp | cljfmt |
366
+ | Terraform | ✓ | lsp, tflint | terraform fmt |
367
+ | Nix | ✓ | lsp | nixfmt |
368
+ | TOML | ✓ | lsp, taplo | taplo |
369
+ | CMake | ✓ | lsp | cmake-format |
@@ -171,14 +171,14 @@ export class AstGrepClient {
171
171
  /**
172
172
  * Run a one-off scan with a temporary rule and configuration
173
173
  */
174
- private runTempScan(
174
+ private async runTempScanAsync(
175
175
  dir: string,
176
176
  ruleId: string,
177
177
  ruleYaml: string,
178
178
  timeout = 30000,
179
- ): AstGrepMatch[] {
179
+ ): Promise<AstGrepMatch[]> {
180
180
  if (!this.isAvailable()) return [];
181
- return this.runner.tempScan(dir, ruleId, ruleYaml, timeout);
181
+ return this.runner.tempScanAsync(dir, ruleId, ruleYaml, timeout);
182
182
  }
183
183
 
184
184
  /**
@@ -201,7 +201,7 @@ severity: info
201
201
  message: found
202
202
  `;
203
203
 
204
- const matches = this.runTempScan(dir, "find-functions", ruleYaml);
204
+ const matches = await this.runTempScanAsync(dir, "find-functions", ruleYaml);
205
205
  if (matches.length === 0) return [];
206
206
 
207
207
  return this.groupSimilarFunctions(matches);
@@ -279,7 +279,7 @@ severity: info
279
279
  message: found
280
280
  `;
281
281
 
282
- const matches = this.runTempScan(dir, "find-functions", ruleYaml, 15000);
282
+ const matches = await this.runTempScanAsync(dir, "find-functions", ruleYaml, 15000);
283
283
  this.log(`scanExports output length: ${matches.length}`);
284
284
 
285
285
  for (const item of matches) {
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import { getProjectDataDir } from "../file-utils.js";
12
12
 
13
- const CACHE_VERSION = "v1";
13
+ const CACHE_VERSION = "v2";
14
14
 
15
15
  export interface QueryCacheEntry {
16
16
  version: string;
@@ -27,6 +27,7 @@ export interface QueryCacheEntry {
27
27
  post_filter?: string;
28
28
  // biome-ignore lint/suspicious/noExplicitAny: Flexible filter params
29
29
  post_filter_params?: Record<string, any>;
30
+ filePath?: string;
30
31
  }>;
31
32
  }
32
33
 
@@ -14,10 +14,12 @@
14
14
  * - BaselineStore: Track pre-existing issues for delta mode
15
15
  */
16
16
 
17
+ import * as fs from "node:fs";
17
18
  import * as path from "node:path";
18
19
  import type { FileKind } from "../file-kinds.js";
19
20
  import { recordRunner } from "../widget-state.js";
20
21
  import { detectFileKind } from "../file-kinds.js";
22
+ import { detectFileRole } from "../file-role.js";
21
23
  import { isTestFile } from "../file-utils.js";
22
24
  import { getPrimaryDispatchGroup } from "../language-policy.js";
23
25
  import { resolveLanguageRootForFile } from "../language-profile.js";
@@ -116,6 +118,26 @@ async function checkToolAvailability(
116
118
 
117
119
  // --- Dispatch Context Factory ---
118
120
 
121
+ function readFilePrefix(filePath: string, maxBytes = 4096): string | undefined {
122
+ let fd: number | undefined;
123
+ try {
124
+ fd = fs.openSync(filePath, "r");
125
+ const buffer = Buffer.alloc(maxBytes);
126
+ const bytesRead = fs.readSync(fd, buffer, 0, maxBytes, 0);
127
+ return buffer.subarray(0, bytesRead).toString("utf8");
128
+ } catch {
129
+ return undefined;
130
+ } finally {
131
+ if (fd !== undefined) {
132
+ try {
133
+ fs.closeSync(fd);
134
+ } catch {
135
+ // ignore close errors
136
+ }
137
+ }
138
+ }
139
+ }
140
+
119
141
  export function createDispatchContext(
120
142
  filePath: string,
121
143
  cwd: string,
@@ -130,11 +152,16 @@ export function createDispatchContext(
130
152
  );
131
153
  const normalizedFilePath = normalizeMapKey(absoluteFilePath);
132
154
  const kind = detectFileKind(normalizedFilePath);
155
+ const fileRole = detectFileRole(
156
+ normalizedFilePath,
157
+ readFilePrefix(normalizedFilePath),
158
+ );
133
159
 
134
160
  return {
135
161
  filePath: normalizedFilePath,
136
162
  cwd: normalizedCwd,
137
163
  kind,
164
+ fileRole,
138
165
  pi,
139
166
  autofix: false,
140
167
  deltaMode: !pi.getFlag("no-delta"),
@@ -598,6 +625,15 @@ async function runGroup(
598
625
  status: result.status,
599
626
  diagnosticCount: result.diagnostics.length,
600
627
  semantic: result.semantic ?? semantic,
628
+ diagnostics:
629
+ result.diagnostics.length > 0
630
+ ? result.diagnostics.map((d) => ({
631
+ rule: d.rule,
632
+ message: d.message.slice(0, 120),
633
+ line: d.line,
634
+ semantic: d.semantic,
635
+ }))
636
+ : undefined,
601
637
  });
602
638
  recordRunner(
603
639
  ctx.filePath,
@@ -634,6 +670,19 @@ export async function dispatchForFile(
634
670
  registry: RunnerRegistryContract,
635
671
  ): Promise<DispatchResult> {
636
672
  const _overallStart = Date.now();
673
+ if (ctx.fileRole === "generated") {
674
+ return {
675
+ diagnostics: [],
676
+ blockers: [],
677
+ warnings: [],
678
+ baselineWarningCount: 0,
679
+ fixed: [],
680
+ resolvedCount: 0,
681
+ output: "",
682
+ blockerOutput: "",
683
+ hasBlockers: false,
684
+ };
685
+ }
637
686
  const allDiagnostics: Diagnostic[] = [];
638
687
  let stopped = false;
639
688
  const runnerLatencies: RunnerLatency[] = [];
@@ -42,6 +42,7 @@ export type { DispatchLatencyReport, RunnerLatency };
42
42
  export { clearLatencyReports, formatLatencyReport, getLatencyReports };
43
43
 
44
44
  import * as nodeFs from "node:fs";
45
+ import { fileURLToPath } from "node:url";
45
46
  import { formatCascadeNeighborDiagnostics } from "../cascade-format.js";
46
47
  import { logCascade } from "../cascade-logger.js";
47
48
  import type { CascadeResult } from "../cascade-types.js";
@@ -58,6 +59,7 @@ import {
58
59
  computeImpactCascade,
59
60
  formatImpactCascade,
60
61
  } from "../review-graph/service.js";
62
+ import { clearModuleGraphCache } from "../review-graph/workspace-modules.js";
61
63
  import { RUNTIME_CONFIG } from "../runtime-config.js";
62
64
  // Register fact providers
63
65
  import { registerProvider, runProviders } from "./fact-runner.js";
@@ -402,6 +404,7 @@ export function resetDispatchBaselines(): void {
402
404
  resetSessionSlopScore();
403
405
  clearCoverageNoticeState();
404
406
  clearReviewGraphWorkspaceCache();
407
+ clearModuleGraphCache();
405
408
  neighborTouchCache.clear();
406
409
  recentlyCleanNeighborCache.clear();
407
410
  primaryFilesThisTurn.clear();
@@ -470,7 +473,14 @@ function ensureCascadeTurnScope(turnSeq: number): void {
470
473
  const CASCADE_TTL_MS = 240_000;
471
474
  const MAX_PER_FILE = RUNTIME_CONFIG.pipeline.cascadeMaxDiagnosticsPerFile;
472
475
  const MAX_FILES = RUNTIME_CONFIG.pipeline.cascadeMaxFiles;
473
- const CASCADE_GRAPH_KINDS = new Set(["jsts", "python", "go", "rust", "ruby"]);
476
+ const CASCADE_GRAPH_KINDS = new Set([
477
+ "jsts",
478
+ "python",
479
+ "go",
480
+ "rust",
481
+ "ruby",
482
+ "cxx",
483
+ ]);
474
484
 
475
485
  /**
476
486
  * Unified cascade orchestration — builds graph, discovers neighbors, and
@@ -559,7 +569,70 @@ export async function computeCascadeForFile(
559
569
  metadata: { graphBuildMode: graphBuildInfo.mode },
560
570
  });
561
571
 
562
- impact = computeImpactCascade(graph, normalizedFile);
572
+ impact = computeImpactCascade(graph, normalizedFile, cwd);
573
+
574
+ // Symbol-level blast radius via LSP references (precision upgrade over
575
+ // file-level import edges). Only when changed symbols are detected.
576
+ // Keep the budget tight: 750ms per symbol, 1200ms total, max 3 symbols.
577
+ if (impact.changedSymbols.length > 0) {
578
+ const lspService = getLSPService();
579
+ const symbolNodeIds =
580
+ graph.symbolNodesByFile.get(normalizedFileKey) ?? [];
581
+ const refFiles = new Set<string>();
582
+ const refsStart = Date.now();
583
+ for (const symbolName of impact.changedSymbols.slice(0, 3)) {
584
+ const symbolNodeId = symbolNodeIds.find((id) => {
585
+ const node = graph.nodes.get(id);
586
+ return node?.symbolName === symbolName;
587
+ });
588
+ if (!symbolNodeId) continue;
589
+ const node = graph.nodes.get(symbolNodeId);
590
+ const line = Number(node?.metadata?.line ?? 0);
591
+ const column = Number(node?.metadata?.column ?? 0);
592
+ if (line <= 0) continue;
593
+ try {
594
+ const refs = await Promise.race([
595
+ lspService.references(normalizedFile, line - 1, column - 1, false),
596
+ new Promise<never>((_, reject) =>
597
+ setTimeout(() => reject(new Error("timeout")), 750),
598
+ ),
599
+ ]);
600
+ for (const ref of refs) {
601
+ let resolved: string;
602
+ try {
603
+ resolved = ref.uri.startsWith("file://")
604
+ ? fileURLToPath(ref.uri)
605
+ : ref.uri;
606
+ } catch {
607
+ continue;
608
+ }
609
+ if (
610
+ normalizeMapKey(resolved) !== normalizedFileKey &&
611
+ nodeFs.existsSync(resolved)
612
+ ) {
613
+ refFiles.add(normalizeMapKey(resolved));
614
+ }
615
+ }
616
+ } catch {
617
+ // Timeout or LSP error — fall back to import-graph neighbors
618
+ }
619
+ if (Date.now() - refsStart > 1200) break; // Hard ceiling
620
+ }
621
+ if (refFiles.size > 0) {
622
+ impact.neighborFiles = [
623
+ ...new Set([...impact.neighborFiles, ...refFiles]),
624
+ ];
625
+ logCascade({
626
+ phase: "neighbor_snapshot",
627
+ filePath,
628
+ neighborFile: "[lsp-references]",
629
+ diagnosticCount: refFiles.size,
630
+ durationMs: Date.now() - refsStart,
631
+ autoPropagate: false,
632
+ metadata: { lspReferences: true },
633
+ });
634
+ }
635
+ }
563
636
 
564
637
  // Sort by relationship strength (B6) then cap to MAX_FILES.
565
638
  // directImporters are most impactful, then callers, then reference edges.
@@ -99,8 +99,11 @@ export const LANGUAGE_CAPABILITY_MATRIX: Record<
99
99
  },
100
100
  cxx: {
101
101
  name: "C/C++ Linting",
102
- capabilities: ["types", "lint"],
103
- writeGroups: [primary("cxx")],
102
+ capabilities: ["types", "lint", "smells"],
103
+ writeGroups: [
104
+ primary("cxx"),
105
+ { mode: "all", runnerIds: ["tree-sitter"], filterKinds: ["cxx"] },
106
+ ],
104
107
  },
105
108
  cmake: {
106
109
  name: "CMake Processing",
@@ -1,3 +1,4 @@
1
+ import * as fs from "node:fs";
1
2
  import * as path from "node:path";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { PRIORITY } from "../priorities.js";
@@ -12,13 +13,79 @@ type CompilerSpec =
12
13
  | { command: string; args: string[]; flavor: "gcc" | "msvc" }
13
14
  | undefined;
14
15
 
16
+ const C_SOURCE_EXTENSIONS = new Set([".c"]);
17
+ const C_HEADER_EXTENSIONS = new Set([".h"]);
18
+ const CPP_SOURCE_EXTENSIONS = new Set([
19
+ ".c++",
20
+ ".cc",
21
+ ".cp",
22
+ ".cpp",
23
+ ".cxx",
24
+ ".c++m",
25
+ ".cppm",
26
+ ".cxxm",
27
+ ".ixx",
28
+ ".cu",
29
+ ".hip",
30
+ ".mm",
31
+ ".clcpp",
32
+ ]);
33
+ const CPP_HEADER_EXTENSIONS = new Set([
34
+ ".hh",
35
+ ".hpp",
36
+ ".hxx",
37
+ ".inl",
38
+ ".ipp",
39
+ ".tpp",
40
+ ".txx",
41
+ ]);
42
+
43
+ function headerLooksLikeCpp(absPath: string): boolean {
44
+ try {
45
+ const content = fs.readFileSync(absPath, "utf-8");
46
+ return /\b(namespace|template|class|constexpr|concept|using)\b|std::|\b(public|private|protected)\s*:/.test(
47
+ content,
48
+ );
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ function getGccLikeCandidates(absPath: string): Array<{
55
+ command: string;
56
+ args: string[];
57
+ }> {
58
+ const ext = path.extname(absPath).toLowerCase();
59
+ const cMode =
60
+ C_SOURCE_EXTENSIONS.has(ext) ||
61
+ (C_HEADER_EXTENSIONS.has(ext) && !headerLooksLikeCpp(absPath));
62
+ const cppMode =
63
+ CPP_SOURCE_EXTENSIONS.has(ext) || CPP_HEADER_EXTENSIONS.has(ext);
64
+
65
+ if (cMode) {
66
+ const cArgs = C_HEADER_EXTENSIONS.has(ext)
67
+ ? ["-x", "c-header", "-fsyntax-only", absPath]
68
+ : ["-x", "c", "-fsyntax-only", absPath];
69
+ return [
70
+ { command: "clang", args: cArgs },
71
+ { command: "gcc", args: cArgs },
72
+ { command: "cc", args: cArgs },
73
+ ];
74
+ }
75
+
76
+ if (cppMode || ext) {
77
+ return [
78
+ { command: "clang++", args: ["-fsyntax-only", absPath] },
79
+ { command: "g++", args: ["-fsyntax-only", absPath] },
80
+ { command: "c++", args: ["-fsyntax-only", absPath] },
81
+ ];
82
+ }
83
+
84
+ return [];
85
+ }
86
+
15
87
  async function resolveCompiler(absPath: string): Promise<CompilerSpec> {
16
- const gccLike: Array<{ command: string; args: string[] }> = [
17
- { command: "clang++", args: ["-fsyntax-only", absPath] },
18
- { command: "g++", args: ["-fsyntax-only", absPath] },
19
- { command: "c++", args: ["-fsyntax-only", absPath] },
20
- ];
21
- for (const candidate of gccLike) {
88
+ for (const candidate of getGccLikeCandidates(absPath)) {
22
89
  const probe = await safeSpawnAsync(candidate.command, ["--version"], {
23
90
  timeout: 5000,
24
91
  });
@@ -4,6 +4,7 @@
4
4
  * Runs `go vet` for Go files to catch common mistakes.
5
5
  */
6
6
 
7
+ import { GoClient } from "../../go-client.js";
7
8
  import { safeSpawnAsync } from "../../safe-spawn.js";
8
9
  import { stripAnsi } from "../../sanitize.js";
9
10
  import { parseGoVetOutput } from "./utils/diagnostic-parsers.js";
@@ -14,6 +15,8 @@ import type {
14
15
  } from "../types.js";
15
16
  import { PRIORITY } from "../priorities.js";
16
17
 
18
+ const goClient = new GoClient();
19
+
17
20
  const goVetRunner: RunnerDefinition = {
18
21
  id: "go-vet",
19
22
  appliesTo: ["go"],
@@ -21,17 +24,14 @@ const goVetRunner: RunnerDefinition = {
21
24
  enabledByDefault: true,
22
25
 
23
26
  async run(ctx: DispatchContext): Promise<RunnerResult> {
24
- // Check if go is available
25
- const check = await safeSpawnAsync("go", ["version"], {
26
- timeout: 5000,
27
- });
28
-
29
- if (check.error || check.status !== 0) {
27
+ // Resolve go path using platform-aware lookup (handles system install paths on Windows)
28
+ const goExe = goClient.findGoPath();
29
+ if (!goExe) {
30
30
  return { status: "skipped", diagnostics: [], semantic: "none" };
31
31
  }
32
32
 
33
33
  // Run go vet on the file
34
- const result = await safeSpawnAsync("go", ["vet", ctx.filePath], {
34
+ const result = await safeSpawnAsync(goExe, ["vet", ctx.filePath], {
35
35
  timeout: 30000,
36
36
  });
37
37
 
@@ -43,8 +43,10 @@ import similarityRunner from "./similarity.js";
43
43
  import spellcheckRunner from "./spellcheck.js";
44
44
  import sqlfluffRunner from "./sqlfluff.js";
45
45
  import stylelintRunner from "./stylelint.js";
46
+ import swiftlintRunner from "./swiftlint.js";
46
47
  import taploRunner from "./taplo.js";
47
48
  import tflintRunner from "./tflint.js";
49
+ import valeRunner from "./vale.js";
48
50
  // Import tree-sitter runner
49
51
  import treeSitterRunner from "./tree-sitter.js";
50
52
  import tsLspRunner from "./ts-lsp.js";
@@ -83,11 +85,13 @@ export function registerDefaultRunners(registry: RunnerRegistry): void {
83
85
  registry.register(markdownlintRunner); // Markdown lint (priority 30)
84
86
  registry.register(mypyRunner); // Python type checking — mypy (priority 20, config-gated)
85
87
  registry.register(stylelintRunner); // CSS/SCSS/Less lint (priority 10, config-gated)
88
+ registry.register(swiftlintRunner); // Swift lint — out-of-the-box defaults (priority 20)
86
89
  registry.register(shfmtRunner); // Shell formatting check (priority 10)
87
90
  registry.register(fishIndentRunner); // Fish script formatting check (priority 10)
88
91
  registry.register(factRulesRunner); // FactRule pipeline — all registered rules (priority 21)
89
92
  registry.register(htmlhintRunner); // HTML linting — tag pairs, attribute rules (priority 20)
90
93
  registry.register(hadolintRunner); // Dockerfile linting — syntax, best practices (priority 20)
94
+ registry.register(valeRunner); // Prose/style linting for Markdown — config-gated (.vale.ini) (priority 30)
91
95
  registry.register(phpLintRunner); // PHP syntax validation via php -l (priority 20)
92
96
  registry.register(psScriptAnalyzerRunner); // PowerShell linting via PSScriptAnalyzer module (priority 20)
93
97
  registry.register(prismaValidateRunner); // Prisma schema validation via CLI (priority 20)
@@ -21,7 +21,7 @@ import type {
21
21
  import {
22
22
  createConfigFinder,
23
23
  getSgCommand,
24
- isSgAvailable,
24
+ isSgAvailableAsync,
25
25
  } from "./utils/runner-helpers.js";
26
26
 
27
27
  const findSlopConfig = createConfigFinder("python-slop-rules");
@@ -35,7 +35,7 @@ const pythonSlopRunner: RunnerDefinition = {
35
35
 
36
36
  async run(ctx: DispatchContext): Promise<RunnerResult> {
37
37
  // Check if ast-grep is available
38
- if (!isSgAvailable()) {
38
+ if (!await isSgAvailableAsync()) {
39
39
  return { status: "skipped", diagnostics: [], semantic: "none" };
40
40
  }
41
41