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
@@ -91,7 +91,11 @@ async function resolveCompiler(absPath, cwd) {
91
91
  const clCmd = clChecker.getCommand(cwd);
92
92
  if (clCmd) {
93
93
  // Already probed in a previous turn and resolved.
94
- return { command: clCmd, args: ["/nologo", "/Zs", absPath], flavor: "msvc" };
94
+ return {
95
+ command: clCmd,
96
+ args: ["/nologo", "/Zs", absPath],
97
+ flavor: "msvc",
98
+ };
95
99
  }
96
100
  const clProbe = await safeSpawnAsync("cl", [], { timeout: 5000 });
97
101
  if (!clProbe.error && clProbe.status !== null) {
@@ -4,7 +4,7 @@ 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 } from "./utils/runner-helpers.js";
7
- import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
7
+ import { parseToolRun } from "./utils/tool-failure.js";
8
8
  const detekt = createAvailabilityChecker("detekt", ".bat");
9
9
  const DETEKT_CONFIG_CANDIDATES = [
10
10
  "detekt.yml",
@@ -146,11 +146,14 @@ const detektRunner = {
146
146
  return { status: "skipped", diagnostics: [], semantic: "none" };
147
147
  const absPath = path.resolve(cwd, ctx.filePath);
148
148
  const result = await safeSpawnAsync(cmd, ["--input", absPath, "--config", configPath], { cwd, timeout: 60000 });
149
+ // #1948: detekt reports across both streams, so the gate and the parser
150
+ // read the same concatenated string. A nonzero exit that yields zero
151
+ // findings out of real output is a parser break, not clean Kotlin.
149
152
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
150
- if (spawnFailedWithNoOutput(result, raw)) {
151
- return { status: "skipped", diagnostics: [], semantic: "none" };
152
- }
153
- const diagnostics = parseDetektOutput(raw, ctx.filePath);
153
+ const run = parseToolRun("detekt", { result, output: raw }, (out) => parseDetektOutput(out, ctx.filePath));
154
+ if (run.skipped)
155
+ return run.skipped;
156
+ const diagnostics = run.diagnostics;
154
157
  if (diagnostics.length === 0) {
155
158
  return { status: "succeeded", diagnostics: [], semantic: "none" };
156
159
  }
@@ -163,4 +166,4 @@ const detektRunner = {
163
166
  },
164
167
  };
165
168
  export default detektRunner;
166
- export { parseDetektOutput, DETEKT_FIXABLE_RULES };
169
+ export { DETEKT_FIXABLE_RULES, parseDetektOutput };
@@ -120,7 +120,7 @@ const dotnetBuildRunner = {
120
120
  skipTestFiles: false,
121
121
  async run(ctx) {
122
122
  const cwd = ctx.cwd || process.cwd();
123
- if (!(await (dotnet.isAvailableAsync(cwd)))) {
123
+ if (!(await dotnet.isAvailableAsync(cwd))) {
124
124
  return { status: "skipped", diagnostics: [], semantic: "none" };
125
125
  }
126
126
  const cmd = dotnet.getCommand(cwd);
@@ -88,7 +88,9 @@ function parseElixirOutput(raw, filePath, cwd = process.cwd()) {
88
88
  if (matchesTarget(sourcePath)) {
89
89
  diagnostics.push({
90
90
  id: `elixir-check-${severity}-${lineStr}-${colStr || "1"}`,
91
- message: severity === "error" ? `[error] ${message.trim()}` : message.trim(),
91
+ message: severity === "error"
92
+ ? `[error] ${message.trim()}`
93
+ : message.trim(),
92
94
  filePath,
93
95
  line: Number.parseInt(lineStr, 10) || 1,
94
96
  column: Number.parseInt(colStr || "1", 10) || 1,
@@ -88,13 +88,22 @@ const eslintRunner = {
88
88
  return { status: "skipped", diagnostics: [], semantic: "none" };
89
89
  }
90
90
  const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-error-on-unmatched-pattern", ctx.filePath], { timeout: 30000, cwd });
91
- // ESLint exits 1 when there are lint errors, 2 on fatal/config error
91
+ // ESLint exits 2 on fatal/config errors — nothing was linted, so this
92
+ // is an unavailable run, not a clean or failed one.
92
93
  if (result.status === 2) {
93
94
  return { status: "skipped", diagnostics: [], semantic: "none" };
94
95
  }
95
- const raw = result.stdout || result.stderr || "";
96
- if (result.status === 0) {
97
- return { status: "succeeded", diagnostics: [], semantic: "none" };
96
+ // ESLint exits 0 whenever nothing reached ERROR severity — that
97
+ // includes a run that found only warnings (#1954), which also prints a
98
+ // full JSON report. So parse stdout unconditionally and branch on the
99
+ // parsed diagnostic count instead of letting the exit code discard the
100
+ // warning case. Stderr is only a fallback for a failing run whose
101
+ // stdout went missing; parsing stderr noise on a healthy exit-0 run
102
+ // would turn deprecation chatter into spurious parse-error findings.
103
+ const stdout = result.stdout ?? "";
104
+ let raw = stdout;
105
+ if (raw.length === 0 && result.status !== 0) {
106
+ raw = result.stderr || "";
98
107
  }
99
108
  const parsed = parseEslintJson(raw, ctx.filePath);
100
109
  if (parsed.parseError && raw.trim().length > 0) {
@@ -104,7 +113,9 @@ const eslintRunner = {
104
113
  diagnostics: [
105
114
  {
106
115
  id: "eslint:parse-error:1",
107
- message: "ESLint JSON parse failed: " + parsed.parseError + (preview ? " (output preview: " + preview + ")" : ""),
116
+ message: "ESLint JSON parse failed: " +
117
+ parsed.parseError +
118
+ (preview ? " (output preview: " + preview + ")" : ""),
108
119
  filePath: ctx.filePath,
109
120
  line: 1,
110
121
  column: 1,
@@ -121,8 +132,16 @@ const eslintRunner = {
121
132
  return { status: "succeeded", diagnostics: [], semantic: "none" };
122
133
  }
123
134
  const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
135
+ // A warning-only result on exit 0 is ESLint's normal outcome, not a
136
+ // failure: exit 0 means nothing hit ERROR severity. Keying status off
137
+ // blocking severity plus exit code (the sibling convention from oxlint,
138
+ // biome-check, golangci-lint, rubocop) keeps plan.ts's fallback group
139
+ // ["eslint", "oxlint", "biome-check-json"] stopping at eslint instead
140
+ // of re-running oxlint and biome-check-json on every warning-only save.
141
+ // The findings reach delivery regardless of status — dispatcher.ts
142
+ // buckets by each diagnostic's own `semantic`.
124
143
  return {
125
- status: "failed",
144
+ status: !hasErrors && result.status === 0 ? "succeeded" : "failed",
126
145
  diagnostics,
127
146
  semantic: hasErrors ? "blocking" : "warning",
128
147
  };
@@ -11,7 +11,7 @@ const fishIndentRunner = {
11
11
  skipTestFiles: false,
12
12
  async run(ctx) {
13
13
  const cwd = ctx.cwd || process.cwd();
14
- const available = await (fishIndent.isAvailableAsync(cwd));
14
+ const available = await fishIndent.isAvailableAsync(cwd);
15
15
  if (!available)
16
16
  return { status: "skipped", diagnostics: [], semantic: "none" };
17
17
  const cmd = fishIndent.getCommand(cwd);
@@ -43,7 +43,7 @@ const gleamCheckRunner = {
43
43
  skipTestFiles: false,
44
44
  async run(ctx) {
45
45
  const cwd = ctx.cwd || process.cwd();
46
- if (!(await (gleam.isAvailableAsync(cwd)))) {
46
+ if (!(await gleam.isAvailableAsync(cwd))) {
47
47
  return { status: "skipped", diagnostics: [], semantic: "none" };
48
48
  }
49
49
  const cmd = gleam.getCommand(cwd);
@@ -31,7 +31,9 @@ function describeReplacement(replacement) {
31
31
  return "Delete this code";
32
32
  if (replacement.NewLines && replacement.NewLines.length > 0) {
33
33
  const preview = replacement.NewLines[0].trim();
34
- const hint = replacement.NewLines.length > 1 ? ` (+${replacement.NewLines.length - 1} more lines)` : "";
34
+ const hint = replacement.NewLines.length > 1
35
+ ? ` (+${replacement.NewLines.length - 1} more lines)`
36
+ : "";
35
37
  return `Replace with: ${preview}${hint}`;
36
38
  }
37
39
  return "Apply golangci-lint suggested fix";
@@ -3,7 +3,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
4
4
  import { PRIORITY } from "../priorities.js";
5
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
6
- import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
6
+ import { parseToolRun } from "./utils/tool-failure.js";
7
+ import { finishParsedRun } from "./utils/tool-failure.js";
7
8
  const hadolint = createAvailabilityChecker("hadolint", ".exe");
8
9
  function parseHadolintOutput(raw, filePath) {
9
10
  try {
@@ -43,7 +44,7 @@ const hadolintRunner = {
43
44
  return { status: "skipped", diagnostics: [], semantic: "none" };
44
45
  }
45
46
  let cmd = null;
46
- if (await (hadolint.isAvailableAsync(cwd))) {
47
+ if (await hadolint.isAvailableAsync(cwd)) {
47
48
  cmd = hadolint.getCommand(cwd);
48
49
  }
49
50
  else {
@@ -53,20 +54,26 @@ const hadolintRunner = {
53
54
  return { status: "skipped", diagnostics: [], semantic: "none" };
54
55
  }
55
56
  const result = await safeSpawnAsync(cmd, ["--format", "json", "--no-fail", path.resolve(cwd, ctx.filePath)], { cwd });
56
- if (spawnFailedWithNoOutput(result)) {
57
- return { status: "skipped", diagnostics: [], semantic: "none" };
58
- }
59
- const output = result.stdout || "";
60
- const diagnostics = parseHadolintOutput(output, ctx.filePath);
61
- if (diagnostics.length === 0) {
62
- return { status: "succeeded", diagnostics: [], semantic: "none" };
63
- }
64
- const hasErrors = diagnostics.some((d) => d.severity === "error");
65
- return {
66
- status: "failed",
57
+ // #1948: hadolint runs with `--no-fail`, so it exits 0 even when it finds
58
+ // something. A nonzero exit therefore means hadolint itself failed, and
59
+ // zero parsed diagnostics out of whatever it printed is a parser break.
60
+ const run = parseToolRun("hadolint", { result }, (out) => parseHadolintOutput(out, ctx.filePath));
61
+ if (run.skipped)
62
+ return run.skipped;
63
+ const diagnostics = run.diagnostics;
64
+ return finishParsedRun({
65
+ tool: "hadolint",
66
+ ctx,
67
+ result,
67
68
  diagnostics,
68
- semantic: hasErrors ? "blocking" : "warning",
69
- };
69
+ classify: (diagnostics) => {
70
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
71
+ return {
72
+ status: "failed",
73
+ semantic: hasErrors ? "blocking" : "warning",
74
+ };
75
+ },
76
+ });
70
77
  },
71
78
  };
72
79
  export default hadolintRunner;
@@ -15,7 +15,10 @@ const SKIPPED = {
15
15
  };
16
16
  function isWithin(root, candidate) {
17
17
  const relative = path.relative(normalizeMapKey(root), normalizeMapKey(candidate));
18
- return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
18
+ return (relative === "" ||
19
+ (!relative.startsWith(`..${path.sep}`) &&
20
+ relative !== ".." &&
21
+ !path.isAbsolute(relative)));
19
22
  }
20
23
  function diagnosticLocation(message, chartRoot) {
21
24
  const location = message.match(/(?:^|\s)([^:\r\n]+\.(?:ya?ml|tpl|json))(?::(\d+))?(?::\d+)?(?::|\s)/i);
@@ -3,7 +3,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
4
4
  import { PRIORITY } from "../priorities.js";
5
5
  import { createAvailabilityChecker, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
6
- import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
6
+ import { parseToolRun } from "./utils/tool-failure.js";
7
+ import { finishParsedRun } from "./utils/tool-failure.js";
7
8
  const htmlhint = createAvailabilityChecker("htmlhint");
8
9
  const HTMLHINT_RULES = {
9
10
  "tag-pair": true,
@@ -55,7 +56,7 @@ const htmlhintRunner = {
55
56
  return { status: "skipped", diagnostics: [], semantic: "none" };
56
57
  }
57
58
  let cmd = null;
58
- if (await (htmlhint.isAvailableAsync(cwd))) {
59
+ if (await htmlhint.isAvailableAsync(cwd)) {
59
60
  cmd = htmlhint.getCommand(cwd);
60
61
  }
61
62
  else {
@@ -64,28 +65,44 @@ const htmlhintRunner = {
64
65
  if (!cmd) {
65
66
  return { status: "skipped", diagnostics: [], semantic: "none" };
66
67
  }
67
- const rulesJson = JSON.stringify(HTMLHINT_RULES);
68
+ // htmlhint's --rules flag takes a comma-separated ruleid list
69
+ // ("tag-pair,id-class-value=underline"), NOT a JSON object. The old JSON
70
+ // blob parsed as one bogus rule id, leaving ZERO rules enabled — every
71
+ // file read CLEAN (exit 0, no output) no matter what it contained.
72
+ // Root-caused live against htmlhint 1.x after the tier-1 parser smoke
73
+ // caught htmlhint reporting 0 findings on the planted tag-pair defect.
74
+ // Entries set to false are omitted: under --rules semantics an unlisted
75
+ // rule is off, which is exactly what `false` means here.
76
+ const rulesArg = Object.entries(HTMLHINT_RULES)
77
+ .flatMap(([rule, enabled]) => (enabled === true ? [rule] : []))
78
+ .join(",");
68
79
  const result = await safeSpawnAsync(cmd, [
69
80
  "--rules",
70
- rulesJson,
81
+ rulesArg,
71
82
  "--format",
72
83
  "unix",
73
84
  path.resolve(cwd, ctx.filePath),
74
85
  ], { cwd });
86
+ // #1948: htmlhint exits 1 when it finds errors and prints them in `unix`
87
+ // format on stdout. Zero parsed out of a nonzero exit is a parser break.
75
88
  const output = result.stdout || result.stderr || "";
76
- if (spawnFailedWithNoOutput(result, output)) {
77
- return { status: "skipped", diagnostics: [], semantic: "none" };
78
- }
79
- const diagnostics = parseHtmlhintOutput(output, ctx.filePath);
80
- if (diagnostics.length === 0) {
81
- return { status: "succeeded", diagnostics: [], semantic: "none" };
82
- }
83
- const hasErrors = diagnostics.some((d) => d.severity === "error");
84
- return {
85
- status: "failed",
89
+ const run = parseToolRun("htmlhint", { result, output }, (out) => parseHtmlhintOutput(out, ctx.filePath));
90
+ if (run.skipped)
91
+ return run.skipped;
92
+ const diagnostics = run.diagnostics;
93
+ return finishParsedRun({
94
+ tool: "htmlhint",
95
+ ctx,
96
+ result,
86
97
  diagnostics,
87
- semantic: hasErrors ? "blocking" : "warning",
88
- };
98
+ classify: (diagnostics) => {
99
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
100
+ return {
101
+ status: "failed",
102
+ semantic: hasErrors ? "blocking" : "warning",
103
+ };
104
+ },
105
+ });
89
106
  },
90
107
  };
91
108
  export default htmlhintRunner;
@@ -54,7 +54,7 @@ const javacRunner = {
54
54
  ctx.log?.("javac: skipped — file is inside a Maven/Gradle project; a classpath-less compile would emit false positives");
55
55
  return { status: "skipped", diagnostics: [], semantic: "none" };
56
56
  }
57
- if (!(await (javac.isAvailableAsync(cwd)))) {
57
+ if (!(await javac.isAvailableAsync(cwd))) {
58
58
  return { status: "skipped", diagnostics: [], semantic: "none" };
59
59
  }
60
60
  const cmd = javac.getCommand(cwd);
@@ -65,7 +65,7 @@ const ktlintRunner = {
65
65
  return { status: "skipped", diagnostics: [], semantic: "none" };
66
66
  }
67
67
  let cmd = null;
68
- if (await (ktlint.isAvailableAsync(cwd))) {
68
+ if (await ktlint.isAvailableAsync(cwd)) {
69
69
  cmd = ktlint.getCommand(cwd);
70
70
  }
71
71
  else {
@@ -23,7 +23,7 @@ import { demoteInferredProjectDiagnostics } from "../../lsp/inferred-project.js"
23
23
  import { enabledAuxiliaryLspServerIds, retagAuxiliaryDiagnostics, } from "../auxiliary-lsp.js";
24
24
  import { readFileContent } from "./utils.js";
25
25
  import { tryWarmAttachedCodeActions, tryWarmAttachedDiagnostics, } from "../../warm-attach.js";
26
- import { contentHash, WARM_CODE_ACTION_LOOKUP_LIMIT, } from "../../mcp/ipc.js";
26
+ import { contentHash, WARM_CODE_ACTION_LOOKUP_LIMIT } from "../../mcp/ipc.js";
27
27
  const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
28
28
  const LSP_MAX_FILE_LINES = RUNTIME_CONFIG.pipeline.lspMaxFileLines;
29
29
  const LSP_SPAWN_BUDGET_MS = RUNTIME_CONFIG.pipeline.lspSpawnBudgetMs;
@@ -2,7 +2,9 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
2
2
  import { getLinterPolicyForCwd, markdownlintConfigArgs, } 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 { isInSpawnTimeoutCooldown, noteSpawnTimeout, } from "../../spawn-timeout-cooldown.js";
7
+ import { finishParsedRun } from "./utils/tool-failure.js";
6
8
  const markdownlint = createAvailabilityChecker("markdownlint-cli2", ".cmd");
7
9
  // markdownlint-cli2 exit codes (its README's "Exit Codes" section): 0 = no
8
10
  // findings, 1 = lint findings, 2 = unexpected error (unreadable config, bad
@@ -98,7 +100,7 @@ const markdownlintRunner = {
98
100
  return { status: "skipped", diagnostics: [], semantic: "none" };
99
101
  }
100
102
  let cmd = null;
101
- if (await (markdownlint.isAvailableAsync(cwd))) {
103
+ if (await markdownlint.isAvailableAsync(cwd)) {
102
104
  cmd = markdownlint.getCommand(cwd);
103
105
  }
104
106
  else {
@@ -106,6 +108,13 @@ const markdownlintRunner = {
106
108
  }
107
109
  if (!cmd)
108
110
  return { status: "skipped", diagnostics: [], semantic: "none" };
111
+ // #1995: a command cooling down after a spawn timeout already consumed
112
+ // its budget in another lane (availability verify, autofix --fix). Skip
113
+ // without spawning — "not checked", never re-reported as clean.
114
+ if (isInSpawnTimeoutCooldown(cmd)) {
115
+ ctx.log(`markdownlint: ${cmd} is cooling down after a spawn timeout — skipping (one bounded failure budget per edit)`);
116
+ return { status: "skipped", diagnostics: [], semantic: "none" };
117
+ }
109
118
  // Shared config-args seam (#1247): the autofix path consumes the same
110
119
  // builder, so the package-owned fallback config can never drift.
111
120
  const configArgs = markdownlintConfigArgs(cwd);
@@ -113,23 +122,36 @@ const markdownlintRunner = {
113
122
  timeout: 15000,
114
123
  cwd,
115
124
  });
125
+ // #1995: a timeout is negative runtime evidence. Arm the cooldown so
126
+ // the autofix and availability lanes cannot hand this command a second
127
+ // budget within the same session.
128
+ if (result.failure === "timeout") {
129
+ noteSpawnTimeout({
130
+ tool: "markdownlint",
131
+ command: cmd,
132
+ phase: "lint",
133
+ durationMs: 15000,
134
+ teardown: result.timeoutTeardown,
135
+ });
136
+ }
116
137
  // #1816: this runner read `result.status` zero times, so an exit-2
117
138
  // config error with an empty stdout parsed to zero diagnostics and was
118
139
  // reported as a clean Markdown file. An empty result must distinguish
119
140
  // clean from errored.
120
141
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
121
- const skipped = skipUnlessToolRan("markdownlint", {
142
+ // #1948: one seam for both gates — the tool produced nothing, and the
143
+ // tool produced something the parser could not read.
144
+ const run = parseToolRun("markdownlint", { result, output: raw, exitCodes: MARKDOWNLINT_EXIT_CODES }, (out) => parseMarkdownlintOutput(out, ctx.filePath));
145
+ if (run.skipped)
146
+ return run.skipped;
147
+ const diagnostics = run.diagnostics;
148
+ return finishParsedRun({
149
+ tool: "markdownlint",
150
+ ctx,
122
151
  result,
123
- output: raw,
124
- exitCodes: MARKDOWNLINT_EXIT_CODES,
152
+ diagnostics,
153
+ classify: () => ({ status: "succeeded", semantic: "warning" }),
125
154
  });
126
- if (skipped)
127
- return skipped;
128
- const diagnostics = parseMarkdownlintOutput(raw, ctx.filePath);
129
- if (diagnostics.length === 0) {
130
- return { status: "succeeded", diagnostics: [], semantic: "none" };
131
- }
132
- return { status: "succeeded", diagnostics, semantic: "warning" };
133
155
  },
134
156
  };
135
157
  export default markdownlintRunner;
@@ -3,7 +3,8 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
3
3
  import { hasMypyConfig } 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 mypy = createAvailabilityChecker("mypy", "");
8
9
  // mypy exit codes: 0 = no type errors, 1 = type errors found, 2 = a blocking
9
10
  // error. 2 is NOT a rejected invocation on its own — mypy reports a source
@@ -67,7 +68,7 @@ const mypyRunner = {
67
68
  return { status: "skipped", diagnostics: [], semantic: "none" };
68
69
  }
69
70
  let cmd = null;
70
- if (await (mypy.isAvailableAsync(cwd))) {
71
+ if (await mypy.isAvailableAsync(cwd)) {
71
72
  cmd = mypy.getCommand(cwd);
72
73
  }
73
74
  else {
@@ -81,23 +82,20 @@ const mypyRunner = {
81
82
  // stdout only (see MYPY_EXIT_CODES) so an exit-2 SYNTAX error, which
82
83
  // does write a diagnostic there, still reaches the parser.
83
84
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
84
- const skipped = skipUnlessToolRan("mypy", {
85
+ // #1948: `parseOutput` keeps the split this runner already had — the gate
86
+ // judges "did it run" on stdout alone, the parser still reads both
87
+ // streams — so adding the parsed-nothing record does not widen the
88
+ // did-it-run verdict.
89
+ const run = parseToolRun("mypy", { result, output: result.stdout, exitCodes: MYPY_EXIT_CODES }, (out) => parseMypyOutput(out, ctx.filePath, cwd), { parseOutput: raw });
90
+ if (run.skipped)
91
+ return run.skipped;
92
+ const diagnostics = run.diagnostics;
93
+ return finishParsedRun({
94
+ tool: "mypy",
95
+ ctx,
85
96
  result,
86
- output: result.stdout,
87
- exitCodes: MYPY_EXIT_CODES,
88
- });
89
- if (skipped)
90
- return skipped;
91
- const diagnostics = parseMypyOutput(raw, ctx.filePath, cwd);
92
- if (diagnostics.length === 0) {
93
- return { status: "succeeded", diagnostics: [], semantic: "none" };
94
- }
95
- const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
96
- return {
97
- status: hasBlocking ? "failed" : "succeeded",
98
97
  diagnostics,
99
- semantic: hasBlocking ? "blocking" : "warning",
100
- };
98
+ });
101
99
  },
102
100
  };
103
101
  export default mypyRunner;