pi-lens 4.2.0 → 4.3.0

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 (157) hide show
  1. package/CHANGELOG.md +543 -0
  2. package/README.md +45 -2
  3. package/dist/clients/actionable-warnings.js +44 -16
  4. package/dist/clients/advisory-provenance.js +187 -51
  5. package/dist/clients/cache-manager.js +64 -5
  6. package/dist/clients/cascade-format.js +24 -0
  7. package/dist/clients/child-unref.js +17 -8
  8. package/dist/clients/config-diagnostic-codes.js +2 -0
  9. package/dist/clients/config-locations.js +237 -9
  10. package/dist/clients/config-resolve.js +7 -1
  11. package/dist/clients/degradation-ledger.js +33 -3
  12. package/dist/clients/dependency-checker.js +87 -64
  13. package/dist/clients/diagnostic-dispositions.js +39 -3
  14. package/dist/clients/dispatch/finding-policy.js +118 -1
  15. package/dist/clients/dispatch/indent-detect.js +8 -2
  16. package/dist/clients/dispatch/inline-suppressions.js +23 -4
  17. package/dist/clients/dispatch/rule-ignores.js +14 -7
  18. package/dist/clients/dispatch/runners/actionlint.js +26 -28
  19. package/dist/clients/dispatch/runners/biome-check.js +38 -25
  20. package/dist/clients/dispatch/runners/cpp-check.js +32 -49
  21. package/dist/clients/dispatch/runners/credo.js +16 -14
  22. package/dist/clients/dispatch/runners/cue-vet.js +25 -7
  23. package/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  24. package/dist/clients/dispatch/runners/detekt.js +11 -4
  25. package/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  26. package/dist/clients/dispatch/runners/elixir-check.js +31 -46
  27. package/dist/clients/dispatch/runners/eslint.js +29 -46
  28. package/dist/clients/dispatch/runners/gleam-check.js +91 -11
  29. package/dist/clients/dispatch/runners/go-vet.js +16 -2
  30. package/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  31. package/dist/clients/dispatch/runners/hadolint.js +27 -15
  32. package/dist/clients/dispatch/runners/htmlhint.js +17 -8
  33. package/dist/clients/dispatch/runners/javac.js +31 -21
  34. package/dist/clients/dispatch/runners/ktlint.js +34 -60
  35. package/dist/clients/dispatch/runners/lsp.js +6 -5
  36. package/dist/clients/dispatch/runners/markdownlint.js +12 -5
  37. package/dist/clients/dispatch/runners/mypy.js +2 -1
  38. package/dist/clients/dispatch/runners/oxlint.js +22 -6
  39. package/dist/clients/dispatch/runners/php-lint.js +39 -16
  40. package/dist/clients/dispatch/runners/phpstan.js +1 -1
  41. package/dist/clients/dispatch/runners/rubocop.js +27 -18
  42. package/dist/clients/dispatch/runners/ruff.js +18 -19
  43. package/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  44. package/dist/clients/dispatch/runners/shellcheck.js +6 -2
  45. package/dist/clients/dispatch/runners/spellcheck.js +10 -3
  46. package/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  47. package/dist/clients/dispatch/runners/stylelint.js +10 -2
  48. package/dist/clients/dispatch/runners/swiftlint.js +11 -2
  49. package/dist/clients/dispatch/runners/taplo.js +12 -6
  50. package/dist/clients/dispatch/runners/tflint.js +30 -15
  51. package/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  52. package/dist/clients/dispatch/runners/trivy-config.js +8 -2
  53. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  54. package/dist/clients/dispatch/runners/vale.js +15 -3
  55. package/dist/clients/dispatch/runners/yamllint.js +12 -2
  56. package/dist/clients/dispatch/runners/zig-check.js +27 -39
  57. package/dist/clients/dispatch/suppress-writer.js +13 -3
  58. package/dist/clients/effective-config.js +3 -1
  59. package/dist/clients/extension-log.js +48 -1
  60. package/dist/clients/finding-delivery-gate.js +53 -23
  61. package/dist/clients/formatters.js +49 -11
  62. package/dist/clients/git-guard.js +247 -34
  63. package/dist/clients/gradle-ktfmt-style.js +121 -25
  64. package/dist/clients/inline-blocker-dispositions.js +186 -0
  65. package/dist/clients/install-diagnostics.js +19 -6
  66. package/dist/clients/installer/index.js +281 -32
  67. package/dist/clients/jscpd-client.js +8 -0
  68. package/dist/clients/latency-logger.js +8 -0
  69. package/dist/clients/lens-config.js +66 -8
  70. package/dist/clients/lsp/cascade-tier.js +3 -0
  71. package/dist/clients/lsp/client.js +139 -11
  72. package/dist/clients/lsp/config.js +9 -7
  73. package/dist/clients/lsp/content-limits.js +41 -0
  74. package/dist/clients/lsp/index.js +42 -8
  75. package/dist/clients/lsp/launch.js +23 -2
  76. package/dist/clients/lsp/server.js +106 -25
  77. package/dist/clients/lsp/sync-kind.js +14 -0
  78. package/dist/clients/lsp/wait-policy/strategies.js +105 -16
  79. package/dist/clients/mcp/analyze.js +9 -2
  80. package/dist/clients/mcp/ipc.js +232 -44
  81. package/dist/clients/mcp/review.js +53 -7
  82. package/dist/clients/opengrep-client.js +11 -6
  83. package/dist/clients/path-utils.js +51 -9
  84. package/dist/clients/persistent-reverify.js +271 -0
  85. package/dist/clients/pipeline.js +136 -67
  86. package/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  87. package/dist/clients/project-lens-config.js +11 -3
  88. package/dist/clients/review-graph/builder.js +34 -26
  89. package/dist/clients/review-graph/git-identity.js +16 -10
  90. package/dist/clients/ruff-client.js +31 -4
  91. package/dist/clients/runtime-coordinator.js +38 -4
  92. package/dist/clients/runtime-tool-call.js +16 -5
  93. package/dist/clients/runtime-tool-result.js +27 -12
  94. package/dist/clients/runtime-turn.js +571 -287
  95. package/dist/clients/safe-spawn.js +256 -65
  96. package/dist/clients/skills-resolver.js +38 -2
  97. package/dist/clients/spawn-output-cap.js +84 -1
  98. package/dist/clients/tree-sitter-client.js +119 -58
  99. package/dist/clients/turn-end/lane.js +51 -0
  100. package/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  101. package/dist/clients/turn-end/lanes/secrets.js +204 -0
  102. package/dist/clients/warm-attach.js +59 -0
  103. package/dist/clients/widget-state.js +294 -17
  104. package/dist/clients/workspace-topology.js +57 -3
  105. package/dist/clients/zizmor-config.js +26 -13
  106. package/dist/index.js +6342 -3995
  107. package/dist/mcp/analyze-cli.js +4 -0
  108. package/dist/mcp/server.js +33 -21
  109. package/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  110. package/dist/tools/lens-diagnostic-mark.js +14 -2
  111. package/dist/tools/lens-diagnostics.js +127 -33
  112. package/dist/tools/lsp-diagnostics.js +150 -12
  113. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  114. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  115. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  116. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  117. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  118. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  119. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  120. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  121. package/docs/adr/0009-reported-path-attribution.md +114 -0
  122. package/docs/adr/README.md +26 -0
  123. package/docs/agent-tools.md +18 -1
  124. package/docs/ast-grep_rules_catalog.md +4 -3
  125. package/docs/configuration.md +55 -1
  126. package/docs/engineering-principles.md +202 -0
  127. package/docs/environment-variables.md +61 -4
  128. package/docs/features.md +2 -2
  129. package/docs/globalconfig.md +6 -1
  130. package/docs/language-coverage.md +17 -0
  131. package/docs/lsp-capability-matrix.md +98 -31
  132. package/docs/pi-lens-fixer.md +57 -3
  133. package/docs/pi-lens-investigator.md +9 -0
  134. package/docs/pi-lens-reviewer.md +16 -0
  135. package/docs/pi-lens-subagent.md +9 -0
  136. package/docs/public-api-stability.md +1 -0
  137. package/docs/release-qa-baseline.md +2 -0
  138. package/docs/servercapabilities.md +21 -11
  139. package/docs/settings.md +12 -3
  140. package/docs/subagent-compat.md +1 -1
  141. package/package.json +5 -3
  142. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  143. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  144. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  145. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  146. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  147. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  148. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  149. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  150. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  151. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  152. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  153. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  154. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  155. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  156. package/scripts/install-selftest.mjs +21 -17
  157. package/scripts/lib/web-tree-sitter-dir.mjs +123 -0
@@ -5,7 +5,9 @@
5
5
  * Autofix is handled earlier by the post-write pipeline to avoid
6
6
  * mutating files mid-dispatch after LSP sync has already happened.
7
7
  */
8
+ import * as path from "node:path";
8
9
  import { incrementDegradationCount } from "../../degradation-ledger.js";
10
+ import { pathsEqual } from "../../path-utils.js";
9
11
  import { mapWithConcurrency } from "../../dependency-checker.js";
10
12
  import { safeSpawnAsync } from "../../safe-spawn.js";
11
13
  import { biomeConfigArgs, getAutofixCapability, getJstsLintPolicyForCwd, } from "../../tool-policy.js";
@@ -50,35 +52,46 @@ export function biomeRuleNameFromCategory(category) {
50
52
  const parts = category.split("/");
51
53
  return parts[parts.length - 1] || undefined;
52
54
  }
53
- export function parseBiomeJson(raw, filePath, fixKindByRule) {
55
+ export function parseBiomeJson(raw, filePath, cwd, fixKindByRule) {
54
56
  try {
55
57
  const result = JSON.parse(raw);
56
58
  const diagnostics = result.diagnostics || [];
57
59
  const autofix = getAutofixCapability("biome");
60
+ const absTarget = path.resolve(cwd, filePath);
58
61
  return {
59
- diagnostics: diagnostics.map((d) => {
60
- const ruleName = biomeRuleNameFromCategory(d.category);
61
- const fixKind = ruleName ? fixKindByRule?.get(ruleName) : undefined;
62
- const isFixable = fixKind === "safe" || fixKind === "unsafe";
63
- return {
64
- id: `biome:${d.category}:${d.location.start.line}`,
65
- message: d.message,
66
- filePath,
67
- line: d.location.start.line,
68
- column: d.location.start.column,
69
- severity: normalizeBiomeSeverity(d.severity),
70
- semantic: d.severity === "error" ? "blocking" : "warning",
71
- tool: "biome",
72
- rule: d.category,
73
- fixable: isFixable,
74
- // Mirrors `biomeClient.fixFileAsync`'s own `lint --write` call
75
- // (no `--unsafe`, clients/biome-client.ts): the pipeline only
76
- // ever applies SAFE fixes, so only "safe" earns autoFixAvailable.
77
- autoFixAvailable: fixKind === "safe" && (autofix?.safePipelineAutofix ?? false),
78
- fixKind: isFixable && autofix?.fixKind !== "none"
79
- ? autofix?.fixKind
80
- : undefined,
81
- };
62
+ diagnostics: diagnostics.flatMap((d) => {
63
+ // #3295: `location.path` is biome's own name for the file each
64
+ // diagnostic is about; `biome.json` `files.includes` can widen the
65
+ // run past the argv.
66
+ if (d.location?.path &&
67
+ !pathsEqual(path.resolve(cwd, d.location.path), absTarget))
68
+ return [];
69
+ {
70
+ const ruleName = biomeRuleNameFromCategory(d.category);
71
+ const fixKind = ruleName ? fixKindByRule?.get(ruleName) : undefined;
72
+ const isFixable = fixKind === "safe" || fixKind === "unsafe";
73
+ return [
74
+ {
75
+ id: `biome:${d.category}:${d.location.start.line}`,
76
+ message: d.message,
77
+ filePath,
78
+ line: d.location.start.line,
79
+ column: d.location.start.column,
80
+ severity: normalizeBiomeSeverity(d.severity),
81
+ semantic: d.severity === "error" ? "blocking" : "warning",
82
+ tool: "biome",
83
+ rule: d.category,
84
+ fixable: isFixable,
85
+ // Mirrors `biomeClient.fixFileAsync`'s own `lint --write` call
86
+ // (no `--unsafe`, clients/biome-client.ts): the pipeline only
87
+ // ever applies SAFE fixes, so only "safe" earns autoFixAvailable.
88
+ autoFixAvailable: fixKind === "safe" && (autofix?.safePipelineAutofix ?? false),
89
+ fixKind: isFixable && autofix?.fixKind !== "none"
90
+ ? autofix?.fixKind
91
+ : undefined,
92
+ },
93
+ ];
94
+ }
82
95
  }),
83
96
  };
84
97
  }
@@ -248,7 +261,7 @@ const biomeCheckJsonRunner = {
248
261
  fixKindByRule = await resolveBiomeFixKinds(cmd, cwd, categories);
249
262
  }
250
263
  const parsed = checkResult.status === 0 || checkResult.status === 1
251
- ? parseBiomeJson(checkResult.stdout || "", ctx.filePath, fixKindByRule)
264
+ ? parseBiomeJson(checkResult.stdout || "", ctx.filePath, cwd, fixKindByRule)
252
265
  : { diagnostics: [] };
253
266
  if (parsed.parseError) {
254
267
  const raw = checkResult.stdout || checkResult.stderr || "";
@@ -1,10 +1,12 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
+ import { pathsEqual } from "../../path-utils.js";
3
4
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
5
  import { probeToolAsync } from "../../tool-probe.js";
5
6
  import { resolveRunnerCwd } from "../../tool-cwd.js";
6
7
  import { PRIORITY } from "../priorities.js";
7
8
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
9
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
8
10
  // Per-compiler availability checkers — each one caches per-cwd and
9
11
  // dedupes concurrent --version probes, the same protections every other
10
12
  // runner gets via createAvailabilityChecker. cpp-check previously
@@ -112,16 +114,16 @@ async function resolveCompiler(absPath, cwd) {
112
114
  }
113
115
  return undefined;
114
116
  }
115
- function parseGccLikeOutput(raw, filePath) {
117
+ function parseGccLikeOutput(raw, filePath, cwd) {
116
118
  const diagnostics = [];
119
+ const absTarget = path.resolve(cwd, filePath);
117
120
  for (const line of raw.split(/\r?\n/)) {
118
121
  const match = line.match(/^(.*?):(\d+):(?:(\d+):)?\s*(fatal error|error|warning|note):\s+(.+)$/i);
119
122
  if (!match)
120
123
  continue;
121
124
  const [, sourcePath, lineStr, colStr, severityLabel, message] = match;
122
- const resolvedSource = path.resolve(sourcePath.trim());
123
- const resolvedTarget = path.resolve(filePath);
124
- if (resolvedSource !== resolvedTarget)
125
+ // #3278: one seam for reported-path attribution — see javac.ts.
126
+ if (!pathsEqual(path.resolve(cwd, sourcePath.trim()), absTarget))
125
127
  continue;
126
128
  const severity = severityLabel.toLowerCase().includes("error")
127
129
  ? "error"
@@ -143,16 +145,16 @@ function parseGccLikeOutput(raw, filePath) {
143
145
  }
144
146
  return diagnostics;
145
147
  }
146
- function parseMsvcOutput(raw, filePath) {
148
+ function parseMsvcOutput(raw, filePath, cwd) {
147
149
  const diagnostics = [];
150
+ const absTarget = path.resolve(cwd, filePath);
148
151
  for (const line of raw.split(/\r?\n/)) {
149
152
  const match = line.match(/^(.*)\((\d+)(?:,(\d+))?\):\s*(fatal error|error|warning)\s+([A-Z]+\d+):\s+(.+)$/i);
150
153
  if (!match)
151
154
  continue;
152
155
  const [, sourcePath, lineStr, colStr, severityLabel, rule, message] = match;
153
- const resolvedSource = path.resolve(sourcePath.trim());
154
- const resolvedTarget = path.resolve(filePath);
155
- if (resolvedSource !== resolvedTarget)
156
+ // #3278: one seam for reported-path attribution — see javac.ts.
157
+ if (!pathsEqual(path.resolve(cwd, sourcePath.trim()), absTarget))
156
158
  continue;
157
159
  const severity = severityLabel.toLowerCase().includes("error")
158
160
  ? "error"
@@ -173,9 +175,6 @@ function parseMsvcOutput(raw, filePath) {
173
175
  }
174
176
  return diagnostics;
175
177
  }
176
- function firstOutputLine(raw) {
177
- return raw.trim().split(/\r?\n/, 1)[0]?.slice(0, 200) ?? "";
178
- }
179
178
  const cppCheckRunner = {
180
179
  id: "cpp-check",
181
180
  appliesTo: ["cxx"],
@@ -193,44 +192,28 @@ const cppCheckRunner = {
193
192
  timeout: 30000,
194
193
  });
195
194
  const raw = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
196
- const diagnostics = compiler.flavor === "msvc"
197
- ? parseMsvcOutput(raw, ctx.filePath)
198
- : parseGccLikeOutput(raw, ctx.filePath);
199
- if (diagnostics.length === 0) {
200
- if (result.status && result.status !== 0) {
201
- return {
202
- status: "failed",
203
- diagnostics: [
204
- {
205
- id: "cpp-check-nonzero-no-diagnostics",
206
- message: firstOutputLine(raw) ||
207
- `${compiler.command} exited non-zero without structured diagnostics`,
208
- filePath: ctx.filePath,
209
- severity: "warning",
210
- semantic: "warning",
211
- tool: "cpp-check",
212
- rule: compiler.command,
213
- fixable: false,
214
- },
215
- ],
216
- semantic: "warning",
217
- rawOutput: raw,
218
- };
219
- }
220
- return {
221
- status: "succeeded",
222
- diagnostics: [],
223
- semantic: "none",
224
- rawOutput: raw,
225
- };
226
- }
227
- const hasErrors = diagnostics.some((d) => d.severity === "error");
228
- return {
229
- status: hasErrors ? "failed" : "succeeded",
230
- diagnostics,
231
- semantic: "warning",
232
- rawOutput: raw,
233
- };
195
+ const parsed = parseToolRun("cpp-check", {
196
+ result,
197
+ output: raw,
198
+ // EXIT TABLE (gcc/clang 13 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
199
+ exitCodes: { ran: [1, 2] },
200
+ }, (output) => compiler.flavor === "msvc"
201
+ ? parseMsvcOutput(output, ctx.filePath, cwd)
202
+ : parseGccLikeOutput(output, ctx.filePath, cwd));
203
+ if (parsed.skipped)
204
+ return parsed.skipped;
205
+ return finishParsedRun({
206
+ tool: "cpp-check",
207
+ ctx,
208
+ result,
209
+ diagnostics: parsed.diagnostics,
210
+ classify: (diagnostics) => ({
211
+ status: diagnostics.some((d) => d.severity === "error")
212
+ ? "failed"
213
+ : "succeeded",
214
+ semantic: "warning",
215
+ }),
216
+ });
234
217
  },
235
218
  };
236
219
  export default cppCheckRunner;
@@ -3,6 +3,7 @@ import * as fs from "node:fs";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
5
  import { PRIORITY } from "../priorities.js";
6
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
6
7
  import { createCwdCachedProbe } from "./utils/runner-helpers.js";
7
8
  const CREDO_PROBE_BUDGET_MS = 10_000;
8
9
  // Per-cwd cached `mix credo --version` probe (#120), governed by the shared
@@ -61,20 +62,21 @@ const credoRunner = {
61
62
  }
62
63
  const absPath = path.resolve(cwd, ctx.filePath);
63
64
  const result = await safeSpawnAsync("mix", ["credo", "--format", "json", "--strict", absPath], { timeout: 30000, cwd });
64
- // credo exits 1 when issues found, 0 when clean
65
- if (result.status === null || result.status > 1) {
66
- return { status: "skipped", diagnostics: [], semantic: "none" };
67
- }
68
- const diagnostics = parseCredoJson(result.stdout ?? "", ctx.filePath, cwd);
69
- if (diagnostics.length === 0) {
70
- return { status: "succeeded", diagnostics: [], semantic: "none" };
71
- }
72
- const hasBlocking = diagnostics.some((d) => d.semantic === "blocking");
73
- return {
74
- status: hasBlocking ? "failed" : "succeeded",
75
- diagnostics,
76
- semantic: hasBlocking ? "blocking" : "warning",
77
- };
65
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
66
+ const parsed = parseToolRun("credo", {
67
+ result,
68
+ output,
69
+ // EXIT TABLE (Credo 1.7 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
70
+ exitCodes: { ran: [1, 2] },
71
+ }, (raw) => parseCredoJson(raw, ctx.filePath, cwd));
72
+ if (parsed.skipped)
73
+ return parsed.skipped;
74
+ return finishParsedRun({
75
+ tool: "credo",
76
+ ctx,
77
+ result,
78
+ diagnostics: parsed.diagnostics,
79
+ });
78
80
  },
79
81
  };
80
82
  export default credoRunner;
@@ -104,6 +104,7 @@
104
104
  import * as fs from "node:fs";
105
105
  import * as path from "node:path";
106
106
  import { safeSpawnAsync } from "../../safe-spawn.js";
107
+ import { pathsEqual } from "../../path-utils.js";
107
108
  import { resolveRunnerCwd } from "../../tool-cwd.js";
108
109
  import { createAvailabilityChecker, resolveAvailableOrInstall, } from "./utils/runner-helpers.js";
109
110
  import { spawnFailedWithNoOutput } from "./utils/spawn-outcome.js";
@@ -205,10 +206,24 @@ export function hasPackageClause(content) {
205
206
  }
206
207
  return false;
207
208
  }
208
- /** `.\bad.cue`, `./bad.cue`, and `bad.cue` all name the same file. */
209
- function locationMatchesFile(location, fileName) {
210
- const normalized = location.file.replace(/\\/g, "/").replace(/^\.\//, "");
211
- return path.posix.basename(normalized) === fileName;
209
+ /**
210
+ * Is this reported location about the file we dispatched for? ONE seam answers
211
+ * that for every runner (#3278): resolve the tool's spelling against the cwd
212
+ * the tool RAN in and compare through `pathsEqual`.
213
+ *
214
+ * `cue` prints every position relative to its own cwd and prefixes a `./` when
215
+ * the result does not already start with a dot (`cue/errors/errors.go:586-596`
216
+ * at v0.11.0: `filepath.Rel(cfg.Cwd, s)`), which is why `.\bad.cue`,
217
+ * `./bad.cue` and `bad.cue` all name the vetted file — `pathsEqual` folds the
218
+ * separator and the `.` segment, so all three still match.
219
+ *
220
+ * What the basename compare this replaces could not do is tell those apart
221
+ * from `./sub/bad.cue` or `../lib/bad.cue`: an imported package's file that
222
+ * merely SHARES the touched file's basename was reported as the touched file's
223
+ * own failure (the over-merge direction, #3278 sub-shape C).
224
+ */
225
+ function locationMatchesFile(location, absTarget, vetCwd) {
226
+ return pathsEqual(path.resolve(vetCwd, location.file), absTarget);
212
227
  }
213
228
  function toDiagnostic(id, message, fileName, line, column) {
214
229
  return {
@@ -252,16 +267,19 @@ function toDiagnostic(id, message, fileName, line, column) {
252
267
  * whole-output fallback message is used instead of one row per parsed
253
268
  * header.
254
269
  */
255
- export function filterToTouchedFile(errors, fileName) {
270
+ export function filterToTouchedFile(errors, fileName, vetCwd) {
256
271
  if (errors.length === 0)
257
272
  return undefined;
273
+ // `cue vet` always runs in the touched file's own directory, so the vet cwd
274
+ // joined with the bare file name IS the dispatched file.
275
+ const absTarget = path.resolve(vetCwd, fileName);
258
276
  const withLocation = errors.filter((error) => error.locations.length > 0);
259
277
  const unattributable = errors.filter((error) => error.locations.length === 0);
260
278
  if (withLocation.length === 0)
261
279
  return undefined;
262
280
  const diagnostics = [];
263
281
  for (const error of withLocation) {
264
- const match = error.locations.find((location) => locationMatchesFile(location, fileName));
282
+ const match = error.locations.find((location) => locationMatchesFile(location, absTarget, vetCwd));
265
283
  if (!match)
266
284
  continue; // sibling-only finding — surfaces when that file is touched
267
285
  diagnostics.push(toDiagnostic(`cue-vet-${diagnostics.length + 1}-${match.line}`, error.message, fileName, match.line, match.column));
@@ -364,7 +382,7 @@ const cueVetRunner = {
364
382
  // the directory-scoped path — one code path, and a single-file run
365
383
  // that somehow fails with no location at all still gets the same
366
384
  // never-silently-clean `undefined` fallback as the directory path.
367
- const filtered = filterToTouchedFile(errors, fileName);
385
+ const filtered = filterToTouchedFile(errors, fileName, fileDir);
368
386
  if (filtered === undefined) {
369
387
  // Nonzero exit, some output, but nothing in it could be attributed to
370
388
  // ANY file — a real failure must not present as zero findings.
@@ -1,7 +1,9 @@
1
1
  import * as path from "node:path";
2
2
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
+ import { pathsEqual } from "../../path-utils.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
6
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
5
7
  import { PRIORITY } from "../priorities.js";
6
8
  const dart = createAvailabilityChecker("dart", ".exe");
7
9
  const flutter = createAvailabilityChecker("flutter", ".bat");
@@ -94,8 +96,9 @@ const DART_FIXABLE_RULES = new Set([
94
96
  ]);
95
97
  // dart analyze --format=machine output:
96
98
  // severity|type|code|file|line|col|length|message
97
- function parseDartMachineOutput(raw, filePath) {
99
+ function parseDartMachineOutput(raw, filePath, cwd) {
98
100
  const diagnostics = [];
101
+ const absTarget = path.resolve(cwd, filePath);
99
102
  for (const line of raw.split(/\r?\n/)) {
100
103
  if (!line.trim())
101
104
  continue;
@@ -106,14 +109,16 @@ function parseDartMachineOutput(raw, filePath) {
106
109
  const message = messageParts.join("|").trim();
107
110
  const lineNum = parseInt(lineStr, 10);
108
111
  const colNum = parseInt(colStr, 10);
109
- // Only include diagnostics for the target file
110
- if (file &&
111
- !path.resolve(file).endsWith(path.resolve(filePath).replace(/\\/g, "/"))) {
112
- const resolvedFile = path.resolve(file.trim());
113
- const resolvedTarget = path.resolve(filePath);
114
- if (resolvedFile !== resolvedTarget)
115
- continue;
116
- }
112
+ // #3278: one seam for reported-path attribution — see javac.ts.
113
+ // The `endsWith` outer arm this replaces was inert on the target (a
114
+ // string ends with itself) and an over-merge risk everywhere else: any
115
+ // reported path whose TAIL spelled the absolute target attached to it.
116
+ // `file` is `parts[3]`, which `tsconfig.strict-indexed.json`
117
+ // (`noUncheckedIndexedAccess`) types as `string | undefined` even behind the
118
+ // `parts.length < 8` guard above — the strictness ratchet pins that, so the
119
+ // optional chain stays.
120
+ if (!pathsEqual(path.resolve(cwd, file?.trim() ?? ""), absTarget))
121
+ continue;
117
122
  const severity = severityStr?.trim().toLowerCase() === "error" ? "error" : "warning";
118
123
  const ruleId = code?.trim() ?? "dart";
119
124
  const fixable = DART_FIXABLE_RULES.has(ruleId);
@@ -136,12 +141,6 @@ function parseDartMachineOutput(raw, filePath) {
136
141
  }
137
142
  return diagnostics;
138
143
  }
139
- function firstOutputLine(result) {
140
- return `${result.stderr || ""}\n${result.stdout || ""}`
141
- .trim()
142
- .split(/\r?\n/, 1)[0]
143
- .slice(0, 200);
144
- }
145
144
  const dartAnalyzeRunner = {
146
145
  id: "dart-analyze",
147
146
  appliesTo: ["dart"],
@@ -162,40 +161,22 @@ const dartAnalyzeRunner = {
162
161
  ? ["analyze", "--format=machine", absPath]
163
162
  : ["analyze", "--machine", absPath];
164
163
  const result = await safeSpawnAsync(cmd, args, { cwd, timeout: 30000 });
165
- if (result.error && !result.stdout && !result.stderr) {
166
- return { status: "skipped", diagnostics: [], semantic: "none" };
167
- }
168
164
  // dart analyze writes diagnostics to stderr in machine format
169
165
  const raw = (result.stderr || "") + (result.stdout || "");
170
- const diagnostics = parseDartMachineOutput(raw, ctx.filePath);
171
- if (diagnostics.length === 0) {
172
- if (result.status && result.status !== 0) {
173
- return {
174
- status: "failed",
175
- diagnostics: [
176
- {
177
- id: "dart-analyze-nonzero-no-diagnostics",
178
- message: firstOutputLine(result) ||
179
- "dart analyze exited non-zero without machine diagnostics",
180
- filePath: ctx.filePath,
181
- severity: "warning",
182
- semantic: "warning",
183
- tool: "dart",
184
- rule: "dart-analyze",
185
- fixable: false,
186
- },
187
- ],
188
- semantic: "warning",
189
- };
190
- }
191
- return { status: "succeeded", diagnostics: [], semantic: "none" };
192
- }
193
- const hasErrors = diagnostics.some((d) => d.severity === "error");
194
- return {
195
- status: hasErrors ? "failed" : "succeeded",
196
- diagnostics,
197
- semantic: hasErrors ? "blocking" : "warning",
198
- };
166
+ const parsed = parseToolRun("dart-analyze", {
167
+ result,
168
+ output: raw,
169
+ // EXIT TABLE (Dart 3.5 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
170
+ exitCodes: { ran: [1, 2] },
171
+ }, (out) => parseDartMachineOutput(out, ctx.filePath, cwd));
172
+ if (parsed.skipped)
173
+ return parsed.skipped;
174
+ return finishParsedRun({
175
+ tool: "dart-analyze",
176
+ ctx,
177
+ result,
178
+ diagnostics: parsed.diagnostics,
179
+ });
199
180
  },
200
181
  };
201
182
  export default dartAnalyzeRunner;
@@ -1,6 +1,7 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { pathsEqual } from "../../path-utils.js";
4
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { getLinterPolicyForCwd } from "../../tool-policy.js";
6
7
  import { PRIORITY } from "../priorities.js";
@@ -91,13 +92,14 @@ export function findDetektConfig(cwd) {
91
92
  return undefined;
92
93
  }
93
94
  // detekt text output: /path/file.kt:10:5: error: Message [RuleId]
94
- function parseDetektOutput(raw, filePath) {
95
+ function parseDetektOutput(raw, filePath, cwd) {
95
96
  const diagnostics = [];
96
97
  const pattern = /^(.+?):(\d+):(\d+): (error|warning): (.+?)(?:\s+\[([^\]]+)\])?$/gm;
97
- const absTarget = path.resolve(filePath);
98
+ const absTarget = path.resolve(cwd, filePath);
98
99
  for (const match of raw.matchAll(pattern)) {
99
100
  const [, file, lineStr, colStr, level, message, rule] = match;
100
- if (path.resolve(file.trim()) !== absTarget)
101
+ // #3278: one seam for reported-path attribution — see javac.ts.
102
+ if (!pathsEqual(path.resolve(cwd, file.trim()), absTarget))
101
103
  continue;
102
104
  const severity = level === "error" ? "error" : "warning";
103
105
  const lineNum = Number.parseInt(lineStr, 10);
@@ -150,7 +152,12 @@ const detektRunner = {
150
152
  // read the same concatenated string. A nonzero exit that yields zero
151
153
  // findings out of real output is a parser break, not clean Kotlin.
152
154
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
153
- const run = parseToolRun("detekt", { result, output: raw }, (out) => parseDetektOutput(out, ctx.filePath));
155
+ const run = parseToolRun("detekt", {
156
+ result,
157
+ output: raw,
158
+ // EXIT TABLE (detekt 1.23 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
159
+ exitCodes: { ran: [1, 2] },
160
+ }, (out) => parseDetektOutput(out, ctx.filePath, cwd));
154
161
  if (run.skipped)
155
162
  return run.skipped;
156
163
  const diagnostics = run.diagnostics;
@@ -1,9 +1,11 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
4
+ import { pathsEqual } from "../../path-utils.js";
4
5
  import { resolveRunnerCwd } from "../../tool-cwd.js";
5
6
  import { PRIORITY } from "../priorities.js";
6
7
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
8
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
7
9
  const dotnet = createAvailabilityChecker("dotnet", ".exe");
8
10
  function findProjectTarget(cwd) {
9
11
  const entries = fs.readdirSync(cwd);
@@ -83,16 +85,16 @@ function parseDotnetDiagnosticLine(line) {
83
85
  }
84
86
  return { reportedFile, lineStr, colStr, severityLabel, rule, message };
85
87
  }
86
- function parseDotnetBuildOutput(raw, filePath) {
88
+ function parseDotnetBuildOutput(raw, filePath, cwd) {
87
89
  const diagnostics = [];
90
+ const absTarget = path.resolve(cwd, filePath);
88
91
  for (const line of raw.split(/\r?\n/)) {
89
92
  const parsed = parseDotnetDiagnosticLine(line);
90
93
  if (!parsed)
91
94
  continue;
92
95
  const { reportedFile, lineStr, colStr, severityLabel, rule, message } = parsed;
93
- const resolvedReported = path.resolve(reportedFile);
94
- const resolvedTarget = path.resolve(filePath);
95
- if (resolvedReported !== resolvedTarget)
96
+ // #3278: one seam for reported-path attribution — see javac.ts.
97
+ if (!pathsEqual(path.resolve(cwd, reportedFile), absTarget))
96
98
  continue;
97
99
  const severity = severityLabel.toLowerCase() === "error" ? "error" : "warning";
98
100
  const lineNum = Number.parseInt(lineStr, 10) || 1;
@@ -136,24 +138,27 @@ const dotnetBuildRunner = {
136
138
  timeout: 60000,
137
139
  });
138
140
  const raw = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
139
- if (result.status === 0 && !raw) {
140
- return { status: "succeeded", diagnostics: [], semantic: "none" };
141
- }
142
- const diagnostics = parseDotnetBuildOutput(raw, ctx.filePath);
143
- if (diagnostics.length === 0) {
144
- return {
145
- status: result.status === 0 ? "succeeded" : "failed",
146
- diagnostics: [],
147
- semantic: "warning",
148
- rawOutput: raw,
149
- };
150
- }
151
- const hasErrors = diagnostics.some((d) => d.severity === "error");
152
- return {
153
- status: hasErrors ? "failed" : "succeeded",
154
- diagnostics,
155
- semantic: hasErrors ? "blocking" : "warning",
156
- };
141
+ const parsed = parseToolRun("dotnet-build", {
142
+ result,
143
+ output: raw,
144
+ // EXIT TABLE (.NET SDK 8 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
145
+ exitCodes: { ran: [1, 2] },
146
+ }, (output) => parseDotnetBuildOutput(output, ctx.filePath, cwd));
147
+ if (parsed.skipped)
148
+ return parsed.skipped;
149
+ return finishParsedRun({
150
+ tool: "dotnet-build",
151
+ ctx,
152
+ result,
153
+ diagnostics: parsed.diagnostics,
154
+ classify: (diagnostics) => {
155
+ const hasErrors = diagnostics.some((d) => d.severity === "error");
156
+ return {
157
+ status: hasErrors ? "failed" : "succeeded",
158
+ semantic: hasErrors ? "blocking" : "warning",
159
+ };
160
+ },
161
+ });
157
162
  },
158
163
  };
159
164
  export default dotnetBuildRunner;