pi-lens 4.1.0 → 4.1.2

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 (206) hide show
  1. package/CHANGELOG.md +185 -0
  2. package/README.md +106 -0
  3. package/dist/clients/actionable-warnings-logger.js +2 -1
  4. package/dist/clients/actionable-warnings.js +14 -5
  5. package/dist/clients/advisory-provenance.js +48 -18
  6. package/dist/clients/ast-grep-client.js +1 -3
  7. package/dist/clients/atomic-write-staging.js +16 -4
  8. package/dist/clients/bash-file-access.js +99 -6
  9. package/dist/clients/biome-client.js +1 -3
  10. package/dist/clients/blocker-freshness.js +21 -8
  11. package/dist/clients/blocker-past-eof.js +16 -6
  12. package/dist/clients/bounded-cache.js +24 -8
  13. package/dist/clients/bounded-pid-file-lock.js +4 -3
  14. package/dist/clients/bounded-telemetry.js +14 -0
  15. package/dist/clients/cache-manager.js +6 -7
  16. package/dist/clients/cache-observability.js +782 -110
  17. package/dist/clients/call-graph.js +238 -63
  18. package/dist/clients/codebase-model.js +13 -5
  19. package/dist/clients/complexity-client.js +1 -3
  20. package/dist/clients/dead-code-client.js +19 -8
  21. package/dist/clients/dead-code-logger.js +1 -2
  22. package/dist/clients/deadline-utils.js +3 -1
  23. package/dist/clients/debug-heap.js +8 -1
  24. package/dist/clients/degradation-ledger.js +22 -1
  25. package/dist/clients/demoted-finding-render.js +155 -0
  26. package/dist/clients/dependency-checker.js +11 -7
  27. package/dist/clients/deps/pi-tui.js +8 -2
  28. package/dist/clients/deps/typebox.js +8 -3
  29. package/dist/clients/diagnostic-dispositions.js +39 -2
  30. package/dist/clients/diagnostic-line-freshness.js +5 -1
  31. package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
  32. package/dist/clients/dispatch/dispatcher.js +13 -1
  33. package/dist/clients/dispatch/facts/function-facts.js +3 -1
  34. package/dist/clients/dispatch/facts/import-facts.js +2 -1
  35. package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
  36. package/dist/clients/dispatch/integration.js +9 -4
  37. package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
  38. package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
  39. package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
  40. package/dist/clients/dispatch/runners/actionlint.js +5 -2
  41. package/dist/clients/dispatch/runners/biome-check.js +10 -14
  42. package/dist/clients/dispatch/runners/cpp-check.js +5 -1
  43. package/dist/clients/dispatch/runners/detekt.js +9 -6
  44. package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
  45. package/dist/clients/dispatch/runners/elixir-check.js +3 -1
  46. package/dist/clients/dispatch/runners/eslint.js +25 -6
  47. package/dist/clients/dispatch/runners/fish-indent.js +1 -1
  48. package/dist/clients/dispatch/runners/gleam-check.js +1 -1
  49. package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
  50. package/dist/clients/dispatch/runners/hadolint.js +22 -15
  51. package/dist/clients/dispatch/runners/helm-lint.js +4 -1
  52. package/dist/clients/dispatch/runners/htmlhint.js +33 -16
  53. package/dist/clients/dispatch/runners/javac.js +1 -1
  54. package/dist/clients/dispatch/runners/ktlint.js +1 -1
  55. package/dist/clients/dispatch/runners/lsp.js +1 -1
  56. package/dist/clients/dispatch/runners/markdownlint.js +34 -12
  57. package/dist/clients/dispatch/runners/mypy.js +15 -17
  58. package/dist/clients/dispatch/runners/oxlint.js +231 -15
  59. package/dist/clients/dispatch/runners/php-lint.js +1 -1
  60. package/dist/clients/dispatch/runners/phpstan.js +52 -15
  61. package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
  62. package/dist/clients/dispatch/runners/pyright.js +23 -14
  63. package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
  64. package/dist/clients/dispatch/runners/shellcheck.js +7 -9
  65. package/dist/clients/dispatch/runners/shfmt.js +5 -2
  66. package/dist/clients/dispatch/runners/spellcheck.js +10 -15
  67. package/dist/clients/dispatch/runners/sqlfluff.js +25 -17
  68. package/dist/clients/dispatch/runners/stylelint.js +40 -20
  69. package/dist/clients/dispatch/runners/swiftlint.js +8 -8
  70. package/dist/clients/dispatch/runners/taplo.js +110 -23
  71. package/dist/clients/dispatch/runners/terragrunt.js +13 -8
  72. package/dist/clients/dispatch/runners/tflint.js +25 -15
  73. package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
  74. package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
  75. package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
  76. package/dist/clients/dispatch/runners/utils/tool-failure.js +152 -1
  77. package/dist/clients/dispatch/runners/vale.js +16 -14
  78. package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
  79. package/dist/clients/dispatch/runners/yamllint.js +13 -16
  80. package/dist/clients/dispatch/runners/zig-check.js +1 -1
  81. package/dist/clients/dispatch/suppress-writer.js +2 -1
  82. package/dist/clients/dispatch/types.js +6 -1
  83. package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
  84. package/dist/clients/extension-log.js +5 -3
  85. package/dist/clients/feature-hints.js +8 -1
  86. package/dist/clients/file-kinds.js +34 -129
  87. package/dist/clients/file-utils.js +17 -1
  88. package/dist/clients/finding-delivery-gate.js +79 -2
  89. package/dist/clients/formatters.js +144 -27
  90. package/dist/clients/freshness.js +38 -0
  91. package/dist/clients/git-guard.js +43 -13
  92. package/dist/clients/go-client.js +1 -3
  93. package/dist/clients/grammar-source.js +1 -1
  94. package/dist/clients/gzip-stage-write.js +30 -4
  95. package/dist/clients/host-edit-normalize.js +1 -3
  96. package/dist/clients/install-diagnostics.js +4 -2
  97. package/dist/clients/installer/index.js +86 -77
  98. package/dist/clients/instance-reaper.js +5 -2
  99. package/dist/clients/instance-registry.js +4 -2
  100. package/dist/clients/jscpd-client.js +11 -3
  101. package/dist/clients/knip-client.js +29 -15
  102. package/dist/clients/language-profile.js +11 -3
  103. package/dist/clients/latency-logger.js +13 -2
  104. package/dist/clients/lens-config.js +1 -1
  105. package/dist/clients/lens-map.js +8 -3
  106. package/dist/clients/live-bus-emitter.js +1 -1
  107. package/dist/clients/lsp/cascade-tier.js +27 -2
  108. package/dist/clients/lsp/client.js +144 -5
  109. package/dist/clients/lsp/config.js +4 -6
  110. package/dist/clients/lsp/edits.js +119 -32
  111. package/dist/clients/lsp/index.js +245 -41
  112. package/dist/clients/lsp/inferred-project.js +2 -1
  113. package/dist/clients/lsp/jvm-runtime.js +4 -1
  114. package/dist/clients/lsp/launch.js +11 -3
  115. package/dist/clients/lsp/pending-aux-coverage.js +152 -0
  116. package/dist/clients/lsp/server.js +19 -7
  117. package/dist/clients/lsp/tsserver-sync.js +10 -2
  118. package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
  119. package/dist/clients/lsp-document-symbols.js +1 -3
  120. package/dist/clients/lsp-mutation.js +10 -20
  121. package/dist/clients/mcp/host-shim.js +1 -1
  122. package/dist/clients/memory-sampler.js +73 -7
  123. package/dist/clients/metrics-client.js +1 -3
  124. package/dist/clients/middle-man-analysis.js +8 -3
  125. package/dist/clients/module-report-lsp.js +7 -1
  126. package/dist/clients/module-report.js +28 -18
  127. package/dist/clients/ndjson-logger.js +117 -28
  128. package/dist/clients/opaque-mutation-scan.js +220 -0
  129. package/dist/clients/package-manager.js +6 -1
  130. package/dist/clients/pipeline.js +57 -9
  131. package/dist/clients/project-conventions.js +2 -8
  132. package/dist/clients/project-diagnostics/cache.js +10 -7
  133. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
  134. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
  135. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
  136. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
  137. package/dist/clients/project-diagnostics/scanner.js +22 -10
  138. package/dist/clients/project-lens-config.js +12 -5
  139. package/dist/clients/project-report.js +62 -27
  140. package/dist/clients/project-snapshot-fingerprint.js +48 -0
  141. package/dist/clients/project-snapshot-persist-worker.js +15 -1
  142. package/dist/clients/project-snapshot.js +472 -66
  143. package/dist/clients/read-expansion.js +12 -2
  144. package/dist/clients/read-guard-logger.js +12 -6
  145. package/dist/clients/read-guard-tool-lines.js +5 -2
  146. package/dist/clients/read-guard.js +88 -19
  147. package/dist/clients/resource-sampler.js +1 -1
  148. package/dist/clients/reverse-deps.js +6 -5
  149. package/dist/clients/review-graph/builder.js +236 -48
  150. package/dist/clients/review-graph/git-identity.js +8 -2
  151. package/dist/clients/review-graph/import-resolvers.js +10 -1
  152. package/dist/clients/review-graph/query.js +2 -1
  153. package/dist/clients/review-graph/revision-drift.js +21 -0
  154. package/dist/clients/review-graph/tsconfig-paths.js +4 -2
  155. package/dist/clients/review-graph/workspace-modules.js +8 -5
  156. package/dist/clients/ruff-client.js +6 -5
  157. package/dist/clients/runtime-agent-end.js +63 -31
  158. package/dist/clients/runtime-context.js +12 -4
  159. package/dist/clients/runtime-coordinator.js +127 -4
  160. package/dist/clients/runtime-session.js +55 -0
  161. package/dist/clients/runtime-tool-call.js +59 -3
  162. package/dist/clients/runtime-tool-result.js +148 -34
  163. package/dist/clients/runtime-turn.js +194 -8
  164. package/dist/clients/rust-client.js +1 -3
  165. package/dist/clients/safe-spawn.js +150 -11
  166. package/dist/clients/security-scan-client.js +1 -3
  167. package/dist/clients/session-event-guard.js +184 -0
  168. package/dist/clients/session-summary.js +3 -1
  169. package/dist/clients/sg-runner.js +1 -3
  170. package/dist/clients/source-filter.js +16 -8
  171. package/dist/clients/source-walker.js +1 -1
  172. package/dist/clients/spawn-timeout-cooldown.js +96 -0
  173. package/dist/clients/startup-scan.js +9 -3
  174. package/dist/clients/tool-definition.js +7 -0
  175. package/dist/clients/tool-policy.js +26 -11
  176. package/dist/clients/tool-render.js +2 -1
  177. package/dist/clients/tree-sitter-client.js +10 -4
  178. package/dist/clients/tree-sitter-logger.js +2 -1
  179. package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
  180. package/dist/clients/warm-attach.js +12 -5
  181. package/dist/clients/widget-state.js +27 -11
  182. package/dist/clients/word-index.js +3 -1
  183. package/dist/clients/workspace-topology.js +8 -2
  184. package/dist/clients/zizmor-config.js +9 -1
  185. package/dist/i18n.js +13 -5
  186. package/dist/index.js +15811 -12810
  187. package/dist/mcp/server.js +3 -1
  188. package/dist/tools/ast-grep-replace.js +68 -12
  189. package/dist/tools/lens-diagnostic-mark.js +21 -7
  190. package/dist/tools/lens-diagnostics.js +174 -38
  191. package/dist/tools/lsp-diagnostics.js +24 -7
  192. package/dist/tools/lsp-navigation.js +3 -1
  193. package/dist/tools/module-report.js +1 -1
  194. package/dist/tools/project-report.js +1 -1
  195. package/dist/tools/symbol-search.js +5 -2
  196. package/docs/durable-store-audit-1202.md +1 -1
  197. package/docs/servercapabilities.md +1 -1
  198. package/package.json +160 -143
  199. package/rules/typos/_typos.toml +2 -2
  200. package/scripts/analyze-pi-lens-logs.mjs +31 -13
  201. package/scripts/download-grammars.js +9 -3
  202. package/scripts/install-selftest.mjs +99 -11
  203. package/scripts/lib/host-provided-deps.mjs +75 -0
  204. package/scripts/lib/warm-loader-cache.mjs +285 -0
  205. package/scripts/rpc-load-check.mjs +30 -9
  206. package/scripts/warm-loader-cache.mjs +233 -0
@@ -2,7 +2,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
2
2
  import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
3
3
  import { PRIORITY } from "../priorities.js";
4
4
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
- import { skipUnlessToolRan } from "./utils/tool-failure.js";
5
+ import { parseToolRun } from "./utils/tool-failure.js";
6
+ import { finishParsedRun } from "./utils/tool-failure.js";
6
7
  const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
7
8
  // sqlfluff exit codes (its CLI docs): 0 = clean, 1 = violations found, 2 = a
8
9
  // user error that stopped the lint (unknown dialect, unreadable config).
@@ -127,7 +128,7 @@ const sqlfluffRunner = {
127
128
  ctx.log("sqlfluff: no config detected, using ANSI dialect defaults");
128
129
  }
129
130
  let cmd = null;
130
- if (await (sqlfluff.isAvailableAsync(cwd))) {
131
+ if (await sqlfluff.isAvailableAsync(cwd)) {
131
132
  cmd = sqlfluff.getCommand(cwd);
132
133
  }
133
134
  else {
@@ -135,10 +136,19 @@ const sqlfluffRunner = {
135
136
  }
136
137
  if (!cmd)
137
138
  return { status: "skipped", diagnostics: [], semantic: "none" };
138
- const args = ["lint", "--format", "json", ctx.filePath];
139
+ // #1937: this used to be `args.splice(2, 0, "--dialect", "ansi")` on
140
+ // `["lint", "--format", "json", file]`, which inserts BETWEEN the flag
141
+ // and its value and yields `lint --format --dialect ansi json file`.
142
+ // sqlfluff rejects `--dialect` as the value of `--format` and exits 2,
143
+ // so every SQL project WITHOUT a .sqlfluff — exactly the case this
144
+ // dialect default exists to serve — was never linted. Built in order
145
+ // rather than patched by index, and pinned by the argv-tie assertion
146
+ // in runners/captured-real-output.test.ts.
147
+ const args = ["lint", "--format", "json"];
139
148
  if (!hasConfig) {
140
- args.splice(2, 0, "--dialect", "ansi");
149
+ args.push("--dialect", "ansi");
141
150
  }
151
+ args.push(ctx.filePath);
142
152
  const result = await safeSpawnAsync(cmd, args, {
143
153
  timeout: 20000,
144
154
  cwd,
@@ -146,21 +156,19 @@ const sqlfluffRunner = {
146
156
  // #1816: this runner read `result.status` zero times, so a user error
147
157
  // fell through `parseSqlfluffOutput`'s JSON catch to zero violations and
148
158
  // reported the SQL file as clean.
149
- const skipped = skipUnlessToolRan("sqlfluff", {
159
+ // #1948: sqlfluff exit 1 with a JSON report we read nothing out of is a
160
+ // parser break, not a clean file.
161
+ const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath), { parseOutput: result.stdout ?? "" });
162
+ if (run.skipped)
163
+ return run.skipped;
164
+ const diagnostics = run.diagnostics;
165
+ return finishParsedRun({
166
+ tool: "sqlfluff",
167
+ ctx,
150
168
  result,
151
- exitCodes: SQLFLUFF_EXIT_CODES,
152
- });
153
- if (skipped)
154
- return skipped;
155
- const diagnostics = parseSqlfluffOutput(result.stdout ?? "", ctx.filePath);
156
- if (diagnostics.length === 0) {
157
- return { status: "succeeded", diagnostics: [], semantic: "none" };
158
- }
159
- return {
160
- status: "failed",
161
169
  diagnostics,
162
- semantic: "warning",
163
- };
170
+ classify: () => ({ status: "failed", semantic: "warning" }),
171
+ });
164
172
  },
165
173
  };
166
174
  export default sqlfluffRunner;
@@ -3,7 +3,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js";
4
4
  import { PRIORITY } from "../priorities.js";
5
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
6
- import { skipUnlessToolRan } from "./utils/tool-failure.js";
6
+ import { parseToolRun } from "./utils/tool-failure.js";
7
+ import { finishParsedRun } from "./utils/tool-failure.js";
7
8
  const stylelint = createAvailabilityChecker("stylelint", ".cmd");
8
9
  // stylelint exit codes (its "Exit codes" docs page): 0 = no problems, 1 = a
9
10
  // fatal error, 2 = lint problems found, 78 = a configuration error.
@@ -12,11 +13,34 @@ const stylelint = createAvailabilityChecker("stylelint", ".cmd");
12
13
  // NOT a live stylelint: it is this repo's own hand-written fixture
13
14
  // (tests/clients/dispatch/runners/stylelint-fixable.test.ts), which reports
14
15
  // findings under exit 1. Rather than rewrite a fixture whose provenance is
15
- // unknown, the table keeps 1 permissive. That is safe because a genuinely
16
- // fatal stylelint under `--formatter json` writes nothing to stdout, so the
17
- // nothing-to-parse rule still catches it. 78 is the rejection this table
16
+ // unknown, the table keeps 1 permissive. 78 is the rejection this table
18
17
  // exists to name.
18
+ //
19
+ // #1937 corrected the safety argument that used to sit here: it claimed a
20
+ // fatal stylelint "writes nothing to stdout", which is true but useless,
21
+ // because a stylelint that DID find problems writes nothing to stdout either.
22
+ // `stylelintReport` below is what now separates the two.
19
23
  const STYLELINT_EXIT_CODES = { ran: [1, 2] };
24
+ /**
25
+ * The JSON report out of a stylelint run, whichever stream carried it (#1937).
26
+ *
27
+ * stylelint 16+ writes the formatted report to STDERR whenever the run
28
+ * "errored" — which is every run that found an error-severity warning, i.e.
29
+ * every run with something to say. Reading stdout alone parsed nothing and
30
+ * called the stylesheet clean, the #1933 vale shape. Verified against
31
+ * stylelint 17.8.0 in tests/fixtures/runner-output/stylelint/.
32
+ *
33
+ * Only a stream that actually starts a JSON array counts. A crash's stack
34
+ * trace on stderr must stay "nothing to parse" so #1816's empty-result skip
35
+ * still fires instead of reporting a clean file.
36
+ */
37
+ export function stylelintReport(stdout, stderr) {
38
+ for (const stream of [stdout, stderr]) {
39
+ if (stream?.trimStart().startsWith("["))
40
+ return stream;
41
+ }
42
+ return "";
43
+ }
20
44
  // Stylelint's standard JSON output reports only aggregate fixableErrorCount /
21
45
  // fixableWarningCount per file — there is no per-warning fix flag in the CLI
22
46
  // surface. To route the actionable warnings here without a tool rewrite, we
@@ -104,7 +128,7 @@ const stylelintRunner = {
104
128
  ctx.log("stylelint: no config detected, running with default rules");
105
129
  }
106
130
  let cmd = null;
107
- if (await (stylelint.isAvailableAsync(cwd))) {
131
+ if (await stylelint.isAvailableAsync(cwd)) {
108
132
  cmd = stylelint.getCommand(cwd);
109
133
  }
110
134
  else {
@@ -116,23 +140,19 @@ const stylelintRunner = {
116
140
  // #1816: this runner read `result.status` zero times, so an exit-78
117
141
  // config error (or an exit-1 crash) fell through parseStylelintJson's
118
142
  // catch to zero diagnostics and reported a clean stylesheet.
119
- const skipped = skipUnlessToolRan("stylelint", {
143
+ const raw = stylelintReport(result.stdout, result.stderr);
144
+ // #1948: the stdout-vs-stderr report bug landed here as exit 2 plus a
145
+ // full JSON report and zero parsed warnings. That now leaves a record.
146
+ const run = parseToolRun("stylelint", { result, output: raw, exitCodes: STYLELINT_EXIT_CODES }, (out) => parseStylelintJson(out, ctx.filePath));
147
+ if (run.skipped)
148
+ return run.skipped;
149
+ const diagnostics = run.diagnostics;
150
+ return finishParsedRun({
151
+ tool: "stylelint",
152
+ ctx,
120
153
  result,
121
- exitCodes: STYLELINT_EXIT_CODES,
122
- });
123
- if (skipped)
124
- return skipped;
125
- const raw = result.stdout ?? "";
126
- const diagnostics = parseStylelintJson(raw, ctx.filePath);
127
- if (diagnostics.length === 0) {
128
- return { status: "succeeded", diagnostics: [], semantic: "none" };
129
- }
130
- const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
131
- return {
132
- status: hasBlocking ? "failed" : "succeeded",
133
154
  diagnostics,
134
- semantic: hasBlocking ? "blocking" : "warning",
135
- };
155
+ });
136
156
  },
137
157
  };
138
158
  export default stylelintRunner;
@@ -2,7 +2,7 @@ import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { PRIORITY } from "../priorities.js";
4
4
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
5
- import { skipUnlessToolRan } from "./utils/tool-failure.js";
5
+ import { parseToolRun } from "./utils/tool-failure.js";
6
6
  const swiftlint = createAvailabilityChecker("swiftlint", ".exe");
7
7
  // SwiftLint rules whose rule class declares a `corrector` — i.e. rules that
8
8
  // `swiftlint --fix` rewrites deterministically. The JSON reporter does not
@@ -118,7 +118,7 @@ const swiftlintRunner = {
118
118
  async run(ctx) {
119
119
  const cwd = ctx.cwd || process.cwd();
120
120
  let cmd = null;
121
- if (await (swiftlint.isAvailableAsync(cwd))) {
121
+ if (await swiftlint.isAvailableAsync(cwd)) {
122
122
  cmd = swiftlint.getCommand(cwd);
123
123
  }
124
124
  else {
@@ -133,12 +133,12 @@ const swiftlintRunner = {
133
133
  // stably enough to call any of them a rejection, so classification
134
134
  // rests on the conservative rule — a nonzero exit with nothing to parse
135
135
  // never ran.
136
- const skipped = skipUnlessToolRan("swiftlint", { result });
137
- if (skipped)
138
- return skipped;
139
- // SwiftLint exits non-zero on violations — stdout still has the JSON
140
- const raw = result.stdout || "";
141
- const diagnostics = parseSwiftLintOutput(raw, ctx.filePath);
136
+ // SwiftLint exits non-zero on violations — stdout still has the JSON, so
137
+ // a nonzero exit whose JSON yields nothing is a parser break (#1948).
138
+ const run = parseToolRun("swiftlint", { result }, (out) => parseSwiftLintOutput(out, ctx.filePath));
139
+ if (run.skipped)
140
+ return run.skipped;
141
+ const diagnostics = run.diagnostics;
142
142
  if (diagnostics.length === 0) {
143
143
  return { status: "succeeded", diagnostics: [], semantic: "none" };
144
144
  }
@@ -4,28 +4,101 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
4
4
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
5
  import { PRIORITY } from "../priorities.js";
6
6
  import { createAvailabilityChecker, lspPrimaryCoversFile, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
7
- import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
7
+ import { parseToolRun } from "./utils/tool-failure.js";
8
8
  const taplo = createAvailabilityChecker("taplo", ".exe");
9
- function parseTaploOutput(raw, filePath) {
10
- try {
11
- const parsed = JSON.parse(raw);
12
- const errors = parsed.errors ?? [];
13
- return errors.map((err, idx) => ({
14
- id: `taplo-${err.kind}-${err.range?.start.line ?? idx}`,
15
- message: `[${err.kind}] ${err.message}`,
9
+ // Verified against taplo 0.10.0: 0 = clean, 1 = the file is invalid (lint
10
+ // findings, or a schema it could not load), 2 = clap rejected the invocation.
11
+ //
12
+ // #1937 round 2: the parser fix alone turned the old silent-clean into a false
13
+ // BLOCKING diagnostic. clap prints `error: unexpected argument '--output'
14
+ // found`, which starts with the same lowercase `error:` a real taplo
15
+ // diagnostic does, so a mistyped flag would have reddened every valid TOML
16
+ // file. Both readings are wrong; the exit code is what separates them.
17
+ const TAPLO_EXIT_CODES = { ran: [1] };
18
+ /** `error: <summary>` or `warning: <summary>` at the head of a codespan block. */
19
+ const HEADER_PATTERN = /^\s*(error|warning):(.*)$/;
20
+ /** The box-drawing lead taplo puts on a codespan location line. */
21
+ const LOCATION_LEAD = "┌─";
22
+ /** The `:line:column` tail of a codespan location line. */
23
+ const LOCATION_PATTERN = /:(\d+):(\d+)$/;
24
+ /**
25
+ * Parse `taplo lint` output (#1937).
26
+ *
27
+ * This parser used to `JSON.parse` a `{ errors: [{ range, message, kind }] }`
28
+ * envelope that taplo has never emitted, fed by a `--output=json` flag taplo
29
+ * has never accepted. Real taplo rejected the flag with exit 2 and a usage
30
+ * error, the JSON parse threw, the catch returned [], and malformed TOML was
31
+ * reported clean. Same shape as the vale parser in #1933.
32
+ *
33
+ * Real taplo 0.10.0 writes codespan diagnostics to STDERR, interleaved with
34
+ * uppercase tracing lines:
35
+ *
36
+ * error: invalid TOML
37
+ * ┌─ /path/to/bad.toml:2:9
38
+ * │
39
+ * 2 │ [package
40
+ *
41
+ * Verified against tests/fixtures/runner-output/taplo/real.captured.json.
42
+ *
43
+ * A header alone is NOT enough to emit a diagnostic. The codespan location
44
+ * line must follow it. clap's own failures start with the same lowercase
45
+ * `error:` and carry no location, so without that requirement a rejected
46
+ * invocation becomes a blocking finding on line 1 of a perfectly valid file
47
+ * (#1937 round 2). The exit-code table in the runner is the primary guard;
48
+ * this is the structural one, and each covers a case the other does not.
49
+ */
50
+ export function parseTaploOutput(raw, filePath) {
51
+ const diagnostics = [];
52
+ const lines = (raw ?? "").split(/\r?\n/);
53
+ for (let i = 0; i < lines.length; i++) {
54
+ // Lowercase `error:`/`warning:` starts a diagnostic. taplo's tracing
55
+ // lines are uppercase (`ERROR taplo:lint_files: ...`), so a
56
+ // case-sensitive match keeps them out.
57
+ //
58
+ // Both patterns below trim in code rather than with a trailing `\s*`,
59
+ // and the location one tests for its box-drawing lead separately
60
+ // instead of skipping to it with `.*`. Either shape backtracks
61
+ // super-linearly on a long line, and this parser reads bytes a third
62
+ // party wrote.
63
+ const header = HEADER_PATTERN.exec(lines[i].trimEnd());
64
+ if (!header)
65
+ continue;
66
+ const severityWord = header[1];
67
+ const summary = header[2].trim();
68
+ if (!summary)
69
+ continue;
70
+ // The location arrives on a following `┌─ file:line:col` line. Scan a
71
+ // short window rather than assuming adjacency: taplo puts a gutter line
72
+ // between them in some layouts.
73
+ let line = null;
74
+ let column = 1;
75
+ for (let j = i + 1; j < Math.min(i + 4, lines.length); j++) {
76
+ if (!lines[j].includes(LOCATION_LEAD))
77
+ continue;
78
+ const location = LOCATION_PATTERN.exec(lines[j].trimEnd());
79
+ if (!location)
80
+ continue;
81
+ line = Number.parseInt(location[1], 10) || 1;
82
+ column = Number.parseInt(location[2], 10) || 1;
83
+ break;
84
+ }
85
+ if (line === null)
86
+ continue;
87
+ const severity = severityWord === "warning" ? "warning" : "error";
88
+ diagnostics.push({
89
+ id: `taplo-${severity}-${line}-${column}`,
90
+ message: summary,
16
91
  filePath,
17
- line: (err.range?.start.line ?? 0) + 1,
18
- column: (err.range?.start.col ?? 0) + 1,
19
- severity: "error",
20
- semantic: "blocking",
92
+ line,
93
+ column,
94
+ severity,
95
+ semantic: severity === "error" ? "blocking" : "warning",
21
96
  tool: "taplo",
22
- rule: err.kind,
97
+ rule: `taplo/${severity}`,
23
98
  fixable: false,
24
- }));
25
- }
26
- catch {
27
- return [];
99
+ });
28
100
  }
101
+ return diagnostics;
29
102
  }
30
103
  const taploRunner = {
31
104
  id: "taplo",
@@ -57,7 +130,7 @@ const taploRunner = {
57
130
  // different extension, so it stays as the checker's fallback only.
58
131
  let cmd = findLocalBinUpwards("taplo", cwd) ?? null;
59
132
  if (!cmd) {
60
- if (await (taplo.isAvailableAsync(cwd))) {
133
+ if (await taplo.isAvailableAsync(cwd)) {
61
134
  cmd = taplo.getCommand(cwd);
62
135
  }
63
136
  else {
@@ -67,11 +140,25 @@ const taploRunner = {
67
140
  if (!cmd)
68
141
  return { status: "skipped", diagnostics: [], semantic: "none" };
69
142
  const absPath = path.resolve(cwd, ctx.filePath);
70
- const result = await safeSpawnAsync(cmd, ["check", "--output=json", absPath], { cwd, timeout: 15000 });
71
- if (spawnFailedWithNoOutput(result)) {
72
- return { status: "skipped", diagnostics: [], semantic: "none" };
73
- }
74
- const diagnostics = parseTaploOutput(result.stdout || "", ctx.filePath);
143
+ // #1937: `--output=json` is not a taplo flag. taplo rejected it with a
144
+ // usage error, so this runner never linted anything. `--colors never`
145
+ // keeps ANSI escapes out of the bytes the parser reads.
146
+ const result = await safeSpawnAsync(cmd, ["check", "--colors", "never", absPath], { cwd, timeout: 15000 });
147
+ // taplo reports on stderr and leaves stdout empty, so the
148
+ // "nothing to parse" test must read the same string the parser gets —
149
+ // otherwise every real run looks like a failed spawn.
150
+ const raw = result.stderr?.trim() ? result.stderr : (result.stdout ?? "");
151
+ //
152
+ // taplo exits 1 only when a file is INVALID, and it does not always draw
153
+ // a codespan block when it does: a schema it could not load is reported
154
+ // through its tracing output alone. Claiming that file clean would be an
155
+ // outright lie, so taplo opts into `skipWhenParsedNothing` (#1948). This
156
+ // replaces a hand-rolled per-runner copy of the same rule, which wrote
157
+ // its own `runner-empty-result` row with its own wording.
158
+ const run = parseToolRun("taplo", { result, output: raw, exitCodes: TAPLO_EXIT_CODES }, (out) => parseTaploOutput(out, ctx.filePath), { skipWhenParsedNothing: true });
159
+ if (run.skipped)
160
+ return run.skipped;
161
+ const diagnostics = run.diagnostics;
75
162
  if (diagnostics.length === 0) {
76
163
  return { status: "succeeded", diagnostics: [], semantic: "none" };
77
164
  }
@@ -5,6 +5,7 @@ import { getLinterPolicyForCwd } from "../../tool-policy.js";
5
5
  import { PRIORITY } from "../priorities.js";
6
6
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
7
7
  import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
8
+ import { finishParsedRun } from "./utils/tool-failure.js";
8
9
  const terragrunt = createAvailabilityChecker("terragrunt", ".exe");
9
10
  function normalizeSeverity(raw) {
10
11
  if (typeof raw === "number")
@@ -149,15 +150,19 @@ const terragruntRunner = {
149
150
  if (spawnFailedWithNoOutput(result))
150
151
  return SKIPPED;
151
152
  const diagnostics = parseTerragruntOutput(result.stdout || "", ctx.filePath, absPath);
152
- if (diagnostics.length === 0) {
153
- return { status: "succeeded", diagnostics: [], semantic: "none" };
154
- }
155
- const hasErrors = diagnostics.some((d) => d.severity === "error");
156
- return {
157
- status: hasErrors ? "failed" : "succeeded",
153
+ return finishParsedRun({
154
+ tool: "terragrunt",
155
+ ctx,
156
+ result,
158
157
  diagnostics,
159
- semantic: hasErrors ? "blocking" : "warning",
160
- };
158
+ classify: (diagnostics) => {
159
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
160
+ return {
161
+ status: hasErrors ? "failed" : "succeeded",
162
+ semantic: hasErrors ? "blocking" : "warning",
163
+ };
164
+ },
165
+ });
161
166
  },
162
167
  };
163
168
  export default terragruntRunner;
@@ -2,9 +2,10 @@ import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
4
4
  import { findNearestDirWithAnyBasename } from "../../workspace-topology.js";
5
- import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
6
- import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
7
5
  import { PRIORITY } from "../priorities.js";
6
+ import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
7
+ import { parseToolRun } from "./utils/tool-failure.js";
8
+ import { finishParsedRun } from "./utils/tool-failure.js";
8
9
  const tflint = createAvailabilityChecker("tflint", ".exe");
9
10
  const TFLINT_CONFIG = ".tflint.hcl";
10
11
  /**
@@ -58,7 +59,7 @@ const tflintRunner = {
58
59
  return { status: "skipped", diagnostics: [], semantic: "none" };
59
60
  }
60
61
  let cmd = null;
61
- if (await (tflint.isAvailableAsync(cwd))) {
62
+ if (await tflint.isAvailableAsync(cwd)) {
62
63
  cmd = tflint.getCommand(cwd);
63
64
  }
64
65
  else {
@@ -82,19 +83,28 @@ const tflintRunner = {
82
83
  cwd: fileDir,
83
84
  timeout: 30000,
84
85
  });
85
- if (spawnFailedWithNoOutput(result)) {
86
- return { status: "skipped", diagnostics: [], semantic: "none" };
87
- }
88
- const diagnostics = parseTflintOutput(result.stdout || "", ctx.filePath);
89
- if (diagnostics.length === 0) {
90
- return { status: "succeeded", diagnostics: [], semantic: "none" };
91
- }
92
- const hasErrors = diagnostics.some((d) => d.severity === "error");
93
- return {
94
- status: hasErrors ? "failed" : "succeeded",
86
+ // #1948: tflint exits nonzero and writes its JSON report to stdout, so a
87
+ // nonzero exit whose report yields zero diagnostics is a parser break,
88
+ // not a clean file. No exit-code table: tflint's nonzero codes are not
89
+ // verified against a real binary here, so the conservative
90
+ // nothing-to-parse rule stays the only discriminator.
91
+ const run = parseToolRun("tflint", { result }, (out) => parseTflintOutput(out, ctx.filePath));
92
+ if (run.skipped)
93
+ return run.skipped;
94
+ const diagnostics = run.diagnostics;
95
+ return finishParsedRun({
96
+ tool: "tflint",
97
+ ctx,
98
+ result,
95
99
  diagnostics,
96
- semantic: hasErrors ? "blocking" : "warning",
97
- };
100
+ classify: (diagnostics) => {
101
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
102
+ return {
103
+ status: hasErrors ? "failed" : "succeeded",
104
+ semantic: hasErrors ? "blocking" : "warning",
105
+ };
106
+ },
107
+ });
98
108
  },
99
109
  };
100
110
  export default tflintRunner;
@@ -300,7 +300,9 @@ const ENTITY_EXTRACTION_LINE_THRESHOLD = 5;
300
300
  */
301
301
  function relativeForIgnoreGlob(filePath, root) {
302
302
  const rel = path.relative(root, filePath);
303
- const normalized = (rel.startsWith("..") ? filePath : rel).split(path.sep).join("/");
303
+ const normalized = (rel.startsWith("..") ? filePath : rel)
304
+ .split(path.sep)
305
+ .join("/");
304
306
  return normalized;
305
307
  }
306
308
  function matchesIgnorePaths(filePath, root, patterns) {
@@ -311,7 +313,17 @@ function matchesIgnorePaths(filePath, root, patterns) {
311
313
  }
312
314
  const treeSitterRunner = {
313
315
  id: "tree-sitter",
314
- appliesTo: ["jsts", "python", "go", "rust", "ruby", "cxx", "csharp", "php", "css"],
316
+ appliesTo: [
317
+ "jsts",
318
+ "python",
319
+ "go",
320
+ "rust",
321
+ "ruby",
322
+ "cxx",
323
+ "csharp",
324
+ "php",
325
+ "css",
326
+ ],
315
327
  priority: PRIORITY.STRUCTURAL_ANALYSIS,
316
328
  enabledByDefault: true,
317
329
  skipTestFiles: false, // Run on test files too (structural issues matter there)
@@ -323,7 +335,9 @@ const treeSitterRunner = {
323
335
  logTreeSitter({
324
336
  phase: "runner_skip",
325
337
  filePath: ctx.filePath,
326
- reason: isTreeSitterWasmAborted() ? "wasm_aborted" : "client_unavailable",
338
+ reason: isTreeSitterWasmAborted()
339
+ ? "wasm_aborted"
340
+ : "client_unavailable",
327
341
  status: "skipped",
328
342
  });
329
343
  return { status: "skipped", diagnostics: [], semantic: "none" };
@@ -80,5 +80,11 @@ export async function probeAvailabilityCandidates(candidates, probeArgs, timeout
80
80
  // above rather than through a thrown spawn.
81
81
  }
82
82
  }
83
- return { foundPath: null, sawTransient, transientCause, hostStallMs, evidence };
83
+ return {
84
+ foundPath: null,
85
+ sawTransient,
86
+ transientCause,
87
+ hostStallMs,
88
+ evidence,
89
+ };
84
90
  }
@@ -19,6 +19,7 @@ import { getServersForFileWithConfig, isServerDisabled, } from "../../../lsp/con
19
19
  import { findGlobalBinary } from "../../../package-manager.js";
20
20
  import { safeSpawnAsync } from "../../../safe-spawn.js";
21
21
  import { getToolCommandSpec, shouldAutoInstallTool, } from "../../../tool-policy.js";
22
+ import { isInSpawnTimeoutCooldown } from "../../../spawn-timeout-cooldown.js";
22
23
  import { classifyProbeFailure, createAvailabilityLatch, describeInstallAttempt, describeProbeEvidence, isLatchingOutcome, logAvailabilityDecision, startHostStallSampler, transientRetryDelayMs, } from "./availability-policy.js";
23
24
  export { createAvailabilityLatch, classifyProbeFailure, describeProbeEvidence, describeUnavailability, isTransientDecision, logAvailabilityDecision, startHostStallSampler, } from "./availability-policy.js";
24
25
  /**
@@ -777,6 +778,27 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
777
778
  return false;
778
779
  }
779
780
  const cmd = await findCommand(resolvedCwd);
781
+ // #1995: a command cooling down after a RUNTIME timeout (lint or
782
+ // autofix lane blew its real budget) must not re-probe on every
783
+ // edit - the positive verdict is effectively cooled. Consult-only:
784
+ // probe timeouts do NOT arm the cooldown here, because a single
785
+ // slow --version under host stall (#1467) is transient evidence,
786
+ // and session-long suppression from it would contradict the
787
+ // #1494 retry ladder this classification exists for.
788
+ if (isInSpawnTimeoutCooldown(cmd)) {
789
+ noteDecision(cache, resolvedCwd, {
790
+ available: false,
791
+ outcome: "transient",
792
+ cause: "probe-timeout",
793
+ elapsedMs: 0,
794
+ classifiedBy: "caller",
795
+ evidence: {
796
+ command: cmd,
797
+ status: null,
798
+ },
799
+ });
800
+ return false;
801
+ }
780
802
  const env = await options.environment?.(resolvedCwd);
781
803
  // The probe budget is enforced by a HOST-side timer, so host event-loop
782
804
  // stalls are charged to the child. Measure the stall that overlapped the
@@ -856,7 +878,8 @@ export function createAvailabilityChecker(command, windowsExt = "", versionArgs
856
878
  outcome: cache.outcome,
857
879
  cause: cache.cause,
858
880
  elapsedMs: cache.elapsedMs,
859
- latched: cache.available !== false || isLatchingOutcome(cache.outcome ?? "missing"),
881
+ latched: cache.available !== false ||
882
+ isLatchingOutcome(cache.outcome ?? "missing"),
860
883
  retryAtMs: cache.retryAtMs,
861
884
  };
862
885
  }