@herbertgao/pi-extensions 2026.9.15 → 2026.9.17

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 (197) hide show
  1. package/README.md +5 -0
  2. package/THIRD_PARTY_NOTICES.md +26 -0
  3. package/node_modules/@herbertgao/pi-cc-extensions/package.json +1 -1
  4. package/node_modules/@herbertgao/sol-pi/package.json +1 -1
  5. package/node_modules/@herbertgao/sol-pi/src/sol-pi/extensions/online-context-compact/extension.ts +11 -53
  6. package/node_modules/@narumitw/pi-btw/dist/index.ts +12 -6
  7. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
  8. package/node_modules/@narumitw/pi-btw/package.json +4 -4
  9. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +2 -0
  10. package/node_modules/@narumitw/pi-btw/src/side-thread.ts +16 -4
  11. package/node_modules/pi-lens/CHANGELOG.md +498 -0
  12. package/node_modules/pi-lens/README.md +45 -2
  13. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +23 -7
  14. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +187 -51
  15. package/node_modules/pi-lens/dist/clients/cache-manager.js +64 -5
  16. package/node_modules/pi-lens/dist/clients/child-unref.js +17 -8
  17. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +2 -0
  18. package/node_modules/pi-lens/dist/clients/config-locations.js +237 -9
  19. package/node_modules/pi-lens/dist/clients/config-resolve.js +7 -1
  20. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +33 -3
  21. package/node_modules/pi-lens/dist/clients/dependency-checker.js +87 -64
  22. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +39 -3
  23. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +118 -1
  24. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +8 -2
  25. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +23 -4
  26. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +14 -7
  27. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +26 -28
  28. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +38 -25
  29. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +32 -49
  30. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +16 -14
  31. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +25 -7
  32. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +28 -47
  33. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +11 -4
  34. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +27 -22
  35. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +31 -46
  36. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +29 -46
  37. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +91 -11
  38. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +16 -2
  39. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +38 -23
  40. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +27 -15
  41. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +17 -8
  42. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +31 -21
  43. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +34 -60
  44. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +6 -5
  45. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +12 -5
  46. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +2 -1
  47. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +22 -6
  48. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +39 -16
  49. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +1 -1
  50. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +27 -18
  51. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +18 -19
  52. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +6 -1
  53. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +6 -2
  54. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +10 -3
  55. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +11 -2
  56. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +10 -2
  57. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +11 -2
  58. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +12 -6
  59. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +30 -15
  60. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +23 -0
  61. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +8 -2
  62. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +11 -1
  63. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +15 -3
  64. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +12 -2
  65. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +27 -39
  66. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +13 -3
  67. package/node_modules/pi-lens/dist/clients/effective-config.js +3 -1
  68. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +42 -11
  69. package/node_modules/pi-lens/dist/clients/formatters.js +49 -11
  70. package/node_modules/pi-lens/dist/clients/git-guard.js +247 -34
  71. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +121 -25
  72. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +186 -0
  73. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +19 -6
  74. package/node_modules/pi-lens/dist/clients/installer/index.js +281 -32
  75. package/node_modules/pi-lens/dist/clients/jscpd-client.js +8 -0
  76. package/node_modules/pi-lens/dist/clients/latency-logger.js +8 -0
  77. package/node_modules/pi-lens/dist/clients/lens-config.js +66 -8
  78. package/node_modules/pi-lens/dist/clients/lsp/client.js +139 -11
  79. package/node_modules/pi-lens/dist/clients/lsp/config.js +9 -7
  80. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +41 -0
  81. package/node_modules/pi-lens/dist/clients/lsp/index.js +1 -1
  82. package/node_modules/pi-lens/dist/clients/lsp/launch.js +23 -2
  83. package/node_modules/pi-lens/dist/clients/lsp/server.js +106 -25
  84. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +14 -0
  85. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +105 -16
  86. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +232 -44
  87. package/node_modules/pi-lens/dist/clients/mcp/review.js +53 -7
  88. package/node_modules/pi-lens/dist/clients/opengrep-client.js +11 -6
  89. package/node_modules/pi-lens/dist/clients/pipeline.js +121 -66
  90. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +21 -3
  91. package/node_modules/pi-lens/dist/clients/project-lens-config.js +11 -3
  92. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +34 -26
  93. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +16 -10
  94. package/node_modules/pi-lens/dist/clients/ruff-client.js +31 -4
  95. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +38 -4
  96. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +16 -5
  97. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +27 -12
  98. package/node_modules/pi-lens/dist/clients/runtime-turn.js +447 -280
  99. package/node_modules/pi-lens/dist/clients/safe-spawn.js +256 -65
  100. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +84 -1
  101. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +119 -58
  102. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +51 -0
  103. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +112 -0
  104. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +204 -0
  105. package/node_modules/pi-lens/dist/clients/warm-attach.js +59 -0
  106. package/node_modules/pi-lens/dist/clients/widget-state.js +145 -14
  107. package/node_modules/pi-lens/dist/clients/workspace-topology.js +57 -3
  108. package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -13
  109. package/node_modules/pi-lens/dist/index.js +5695 -3733
  110. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +4 -0
  111. package/node_modules/pi-lens/dist/mcp/server.js +33 -21
  112. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  113. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +43 -16
  114. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +118 -7
  115. package/node_modules/pi-lens/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  116. package/node_modules/pi-lens/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  117. package/node_modules/pi-lens/docs/adr/0003-git-guard-latch-writer.md +203 -0
  118. package/node_modules/pi-lens/docs/adr/0004-disposition-policy-seam.md +38 -0
  119. package/node_modules/pi-lens/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  120. package/node_modules/pi-lens/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  121. package/node_modules/pi-lens/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  122. package/node_modules/pi-lens/docs/adr/0008-turn-end-lane-interface.md +165 -0
  123. package/node_modules/pi-lens/docs/adr/0009-reported-path-attribution.md +114 -0
  124. package/node_modules/pi-lens/docs/adr/README.md +26 -0
  125. package/node_modules/pi-lens/docs/agent-tools.md +18 -1
  126. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +4 -3
  127. package/node_modules/pi-lens/docs/configuration.md +55 -1
  128. package/node_modules/pi-lens/docs/engineering-principles.md +202 -0
  129. package/node_modules/pi-lens/docs/environment-variables.md +61 -4
  130. package/node_modules/pi-lens/docs/features.md +2 -2
  131. package/node_modules/pi-lens/docs/globalconfig.md +6 -1
  132. package/node_modules/pi-lens/docs/language-coverage.md +17 -0
  133. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +98 -31
  134. package/node_modules/pi-lens/docs/pi-lens-fixer.md +57 -3
  135. package/node_modules/pi-lens/docs/pi-lens-investigator.md +9 -0
  136. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +16 -0
  137. package/node_modules/pi-lens/docs/pi-lens-subagent.md +9 -0
  138. package/node_modules/pi-lens/docs/public-api-stability.md +1 -0
  139. package/node_modules/pi-lens/docs/release-qa-baseline.md +2 -0
  140. package/node_modules/pi-lens/docs/servercapabilities.md +21 -11
  141. package/node_modules/pi-lens/docs/settings.md +12 -3
  142. package/node_modules/pi-lens/docs/subagent-compat.md +1 -1
  143. package/node_modules/pi-lens/package.json +5 -3
  144. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  145. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  146. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  147. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  148. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  149. package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  150. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  151. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  152. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  153. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  154. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  155. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  156. package/node_modules/pi-lens/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  157. package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  158. package/node_modules/pi-lens/scripts/install-selftest.mjs +21 -17
  159. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +123 -0
  160. package/node_modules/pi-mcp-adapter/CHANGELOG.md +29 -0
  161. package/node_modules/pi-mcp-adapter/OAUTH.md +369 -0
  162. package/node_modules/pi-mcp-adapter/README.md +12 -5
  163. package/node_modules/pi-mcp-adapter/config.ts +31 -8
  164. package/node_modules/pi-mcp-adapter/direct-tool-surface.ts +1 -1
  165. package/node_modules/pi-mcp-adapter/dist/config.js +33 -8
  166. package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
  167. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js +15 -2
  168. package/node_modules/pi-mcp-adapter/dist/json-schema-validator.js.map +1 -1
  169. package/node_modules/pi-mcp-adapter/dist/mcp-auth.d.ts +2 -0
  170. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js +27 -2
  171. package/node_modules/pi-mcp-adapter/dist/mcp-auth.js.map +1 -1
  172. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js +1 -1
  173. package/node_modules/pi-mcp-adapter/dist/metadata-cache.js.map +1 -1
  174. package/node_modules/pi-mcp-adapter/dist/server-manager.js +6 -4
  175. package/node_modules/pi-mcp-adapter/dist/server-manager.js.map +1 -1
  176. package/node_modules/pi-mcp-adapter/dist/types.d.ts +2 -2
  177. package/node_modules/pi-mcp-adapter/dist/utils.d.ts +2 -0
  178. package/node_modules/pi-mcp-adapter/dist/utils.js +10 -3
  179. package/node_modules/pi-mcp-adapter/dist/utils.js.map +1 -1
  180. package/node_modules/pi-mcp-adapter/index.ts +37 -20
  181. package/node_modules/pi-mcp-adapter/init.ts +14 -15
  182. package/node_modules/pi-mcp-adapter/json-schema-validator.ts +16 -2
  183. package/node_modules/pi-mcp-adapter/mcp-auth.ts +27 -2
  184. package/node_modules/pi-mcp-adapter/metadata-cache.ts +1 -1
  185. package/node_modules/pi-mcp-adapter/package.json +3 -2
  186. package/node_modules/pi-mcp-adapter/proxy-modes.ts +1 -1
  187. package/node_modules/pi-mcp-adapter/server-manager.ts +6 -3
  188. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +99 -6
  189. package/node_modules/pi-mcp-adapter/types.ts +2 -2
  190. package/node_modules/pi-mcp-adapter/ui-session.ts +60 -27
  191. package/node_modules/pi-mcp-adapter/utils.ts +11 -3
  192. package/node_modules/pi-next-cue/.github/workflows/publish.yml +20 -0
  193. package/node_modules/pi-next-cue/LICENSE +21 -0
  194. package/node_modules/pi-next-cue/README.md +81 -0
  195. package/node_modules/pi-next-cue/index.ts +358 -0
  196. package/node_modules/pi-next-cue/package.json +28 -0
  197. package/package.json +9 -6
@@ -1,3 +1,5 @@
1
+ import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
1
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
4
  import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
3
5
  import { getLinterPolicyForCwd, markdownlintConfigArgs, } from "../../tool-policy.js";
@@ -10,7 +12,7 @@ const markdownlint = createAvailabilityChecker("markdownlint-cli2", ".cmd", [
10
12
  "--no-globs",
11
13
  "-",
12
14
  ]);
13
- // markdownlint-cli2 exit codes (its README's "Exit Codes" section): 0 = no
15
+ // EXIT TABLE (markdownlint-cli2 0.17 README https://github.com/DavidAnson/markdownlint-cli2#exit-codes): 0 = no
14
16
  // findings, 1 = lint findings, 2 = unexpected error (unreadable config, bad
15
17
  // glob, crash). Only 2 is a rejected invocation.
16
18
  const MARKDOWNLINT_EXIT_CODES = { ran: [1] };
@@ -59,18 +61,23 @@ const MARKDOWNLINT_FIXABLE_RULES = new Set([
59
61
  // 2. some rules carry MULTIPLE slash-separated names (e.g.
60
62
  // `MD041/first-line-heading/first-line-h1`).
61
63
  // The severity token is optional so older/relative-path output still parses.
62
- function parseMarkdownlintOutput(raw, filePath) {
64
+ function parseMarkdownlintOutput(raw, filePath, cwd) {
63
65
  const diagnostics = [];
66
+ const absTarget = path.resolve(cwd, filePath);
64
67
  for (const line of raw.split(/\r?\n/)) {
65
68
  if (!line.trim())
66
69
  continue;
67
70
  // Rule code is MD### followed by one or more slash-joined names. Use a
68
71
  // single char class (`[\w/-]+`) rather than a nested quantifier
69
72
  // (`(?:/[\w-]+)+`) so there's no super-linear backtracking (S5852).
70
- const match = line.match(/^.*?:(\d+)(?::(\d+))?\s+(?:error|warning)?\s*(MD\d+\/[\w/-]+)\s+(.+)$/);
73
+ const match = line.match(/^(.*?):(\d+)(?::(\d+))?\s+(?:error|warning)?\s*(MD\d+\/[\w/-]+)\s+(.+)$/);
71
74
  if (!match)
72
75
  continue;
73
- const [, lineNum, col, ruleCode, message] = match;
76
+ const [, reported, lineNum, col, ruleCode, message] = match;
77
+ // #3295: the leading `.*?` this parser discarded IS markdownlint-cli2's
78
+ // reported path, and its config globs can widen the run past the argv.
79
+ if (reported && !pathsEqual(path.resolve(cwd, reported), absTarget))
80
+ continue;
74
81
  const ruleName = ruleCode.split("/")[0];
75
82
  const fixable = MARKDOWNLINT_FIXABLE_RULES.has(ruleName);
76
83
  diagnostics.push({
@@ -144,7 +151,7 @@ const markdownlintRunner = {
144
151
  const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
145
152
  // #1948: one seam for both gates — the tool produced nothing, and the
146
153
  // tool produced something the parser could not read.
147
- const run = parseToolRun("markdownlint", { result, output: raw, exitCodes: MARKDOWNLINT_EXIT_CODES }, (out) => parseMarkdownlintOutput(out, ctx.filePath));
154
+ const run = parseToolRun("markdownlint", { result, output: raw, exitCodes: MARKDOWNLINT_EXIT_CODES }, (out) => parseMarkdownlintOutput(out, ctx.filePath, cwd));
148
155
  if (run.skipped)
149
156
  return run.skipped;
150
157
  const diagnostics = run.diagnostics;
@@ -14,7 +14,8 @@ const mypy = createAvailabilityChecker("mypy", "");
14
14
  // [syntax]`). A bad flag or unreadable config also exits 2, but writes its
15
15
  // message to STDERR and leaves stdout empty.
16
16
  //
17
- // So the exit code cannot separate the two, and the STREAM does: the gate below
17
+ // EXIT TABLE (mypy 1.11 docs https://mypy.readthedocs.io/en/stable/command_line.html): 0 clean; 1 findings; 2 findings/error; other
18
+ // nonzero rejected. The exit code cannot separate the two, and the STREAM does:
18
19
  // judges "nothing to parse" on stdout alone, while the parser still reads both.
19
20
  // The table's remaining job is to reject any OTHER nonzero status.
20
21
  const MYPY_EXIT_CODES = { ran: [1, 2] };
@@ -6,7 +6,9 @@
6
6
  *
7
7
  * Requires: oxlint (npm install -g oxlint)
8
8
  */
9
+ import * as path from "node:path";
9
10
  import { findLocalBinUpwards } from "../../package-manager.js";
11
+ import { pathsEqual } from "../../path-utils.js";
10
12
  import { safeSpawnAsync } from "../../safe-spawn.js";
11
13
  import { resolveRunnerCwd } from "../../tool-cwd.js";
12
14
  import { truncatedByOutputCap } from "../../spawn-output-cap.js";
@@ -111,7 +113,12 @@ const oxlintRunner = {
111
113
  // force a status-0 lookalike away from clean. Neither escapes this runner.
112
114
  // parseToolRun checks process failures before invoking this parser, so
113
115
  // spawn/timeout/killed/signal precedence remains owned by #1994.
114
- const parsedRun = parseToolRun("oxlint", { result, output: parsedOutput }, () => {
116
+ const parsedRun = parseToolRun("oxlint", {
117
+ result,
118
+ output: parsedOutput,
119
+ // EXIT TABLE (oxlint 0.8 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
120
+ exitCodes: { ran: [1, 2] },
121
+ }, () => {
115
122
  if (noFilesDecision.kind === "expected-no-files") {
116
123
  return [OXLINT_NO_FILES];
117
124
  }
@@ -119,9 +126,9 @@ const oxlintRunner = {
119
126
  result.status === 0) {
120
127
  return [OXLINT_NO_FILES_UNCONFIRMED];
121
128
  }
122
- let parsed = parseOxlintJson(stdout, ctx.filePath);
129
+ let parsed = parseOxlintJson(stdout, ctx.filePath, cwd);
123
130
  if (parsed.length === 0 && stdout.length > 0) {
124
- parsed = parseOxlintUnix(parsedOutput, ctx.filePath);
131
+ parsed = parseOxlintUnix(parsedOutput, ctx.filePath, cwd);
125
132
  }
126
133
  return parsed;
127
134
  }, { parseOutput: parsedOutput });
@@ -334,7 +341,7 @@ function extractOxlintRule(code) {
334
341
  return code;
335
342
  return code.slice(open + 1, close);
336
343
  }
337
- function parseOxlintJson(raw, filePath) {
344
+ function parseOxlintJson(raw, filePath, cwd) {
338
345
  const trimmed = raw.trim();
339
346
  if (!trimmed.startsWith("{"))
340
347
  return [];
@@ -348,7 +355,11 @@ function parseOxlintJson(raw, filePath) {
348
355
  if (!Array.isArray(parsed.diagnostics))
349
356
  return [];
350
357
  const diagnostics = [];
358
+ const absTarget = path.resolve(cwd, filePath);
351
359
  for (const d of parsed.diagnostics) {
360
+ if (!d.filename || !pathsEqual(path.resolve(cwd, d.filename), absTarget)) {
361
+ continue;
362
+ }
352
363
  const rule = extractOxlintRule(d.code);
353
364
  const label = d.labels?.[0]?.span;
354
365
  const lineNum = label?.line ?? 1;
@@ -374,13 +385,18 @@ function parseOxlintJson(raw, filePath) {
374
385
  }
375
386
  return diagnostics;
376
387
  }
377
- function parseOxlintUnix(raw, filePath) {
388
+ function parseOxlintUnix(raw, filePath, cwd) {
378
389
  const diagnostics = [];
390
+ const absTarget = path.resolve(cwd, filePath);
379
391
  for (const line of raw.split("\n")) {
380
392
  // Parse: file:line:column: message (rule)
381
393
  const match = line.match(/^(.+):(\d+):(\d+):\s*(.+?)\s*\(([^)]+)\)$/);
382
394
  if (match) {
383
- const [, _file, lineStr, _col, message, rule] = match;
395
+ const [, reportedPath, lineStr, _col, message, rule] = match;
396
+ if (!reportedPath || !lineStr || !message || !rule)
397
+ continue;
398
+ if (!pathsEqual(path.resolve(cwd, reportedPath), absTarget))
399
+ continue;
384
400
  diagnostics.push({
385
401
  id: `oxlint-${rule}-${lineStr}`,
386
402
  message: `${message} (${rule})`,
@@ -1,16 +1,33 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { PRIORITY } from "../priorities.js";
5
6
  import { createAvailabilityChecker } from "./utils/runner-helpers.js";
7
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
8
+ // PHP's `-l` exit contract, declared HERE rather than in the shared classifier
9
+ // (#3291 r3): an admission is a property of this tool. The manual documents
10
+ // `-l` as syntax-check-only with a nonzero return on failure; the captured
11
+ // PHP 8.3.32 wire exits 1 in the usual CLI path and 255 (the shell reading of
12
+ // PHP's documented -1) for the parse-error wire. Both are completed analyses
13
+ // whose output must reach the parser; any other nonzero status stays a
14
+ // rejected invocation.
15
+ const PHP_LINT_EXIT_CODES = { ran: [1, 255] };
6
16
  const php = createAvailabilityChecker("php", ".exe");
7
- function parsePhpLintOutput(raw, filePath) {
17
+ function parsePhpLintOutput(raw, filePath, cwd) {
8
18
  const output = raw.trim();
9
- if (!output)
19
+ if (!output || !/(?:PHP )?Parse error:/i.test(output))
10
20
  return [];
11
21
  const lineMatch = output.match(/on line (\d+)/i);
12
- const messageMatch = output.match(/PHP Parse error:\s*(.+?)(?:\s+in\s+.+?\s+on line \d+)?$/im) ??
13
- output.match(/Parse error:\s*(.+?)(?:\s+in\s+.+?\s+on line \d+)?$/im);
22
+ const messageMatch = output.match(/PHP Parse error:\s*(.+?)(?:\s+in\s+(.+?)\s+on line \d+)?$/im) ??
23
+ output.match(/Parse error:\s*(.+?)(?:\s+in\s+(.+?)\s+on line \d+)?$/im);
24
+ // #3295: PHP names the file it could not parse in the same sentence. `php -l`
25
+ // follows no includes today, so this drops nothing under our argv — it pins
26
+ // the attribution the parser was asserting without asking.
27
+ const reported = messageMatch?.[2]?.trim();
28
+ if (reported &&
29
+ !pathsEqual(path.resolve(cwd, reported), path.resolve(cwd, filePath)))
30
+ return [];
14
31
  return [
15
32
  {
16
33
  id: `php-lint:${lineMatch?.[1] ?? "1"}`,
@@ -45,18 +62,24 @@ const phpLintRunner = {
45
62
  timeout: 15000,
46
63
  cwd,
47
64
  });
48
- if (result.status === 0) {
49
- return { status: "succeeded", diagnostics: [], semantic: "none" };
50
- }
51
- const diagnostics = parsePhpLintOutput(`${result.stdout ?? ""}\n${result.stderr ?? ""}`, ctx.filePath);
52
- if (diagnostics.length === 0) {
53
- return { status: "skipped", diagnostics: [], semantic: "none" };
54
- }
55
- return {
56
- status: "failed",
57
- diagnostics,
58
- semantic: "blocking",
59
- };
65
+ // `output` is what the classifier judges AND what the parser reads: the
66
+ // parse error arrives on stderr, so both streams are one wire here. The
67
+ // exit table rides in this same per-runner input, never in a shared
68
+ // option (#3291 r3).
69
+ const run = parseToolRun("php-lint", {
70
+ result,
71
+ output: `${result.stdout ?? ""}\n${result.stderr ?? ""}`,
72
+ exitCodes: PHP_LINT_EXIT_CODES,
73
+ }, (output) => parsePhpLintOutput(output, ctx.filePath, cwd));
74
+ if (run.skipped)
75
+ return run.skipped;
76
+ return finishParsedRun({
77
+ tool: "php-lint",
78
+ ctx,
79
+ result,
80
+ diagnostics: run.diagnostics,
81
+ classify: () => ({ status: "failed", semantic: "blocking" }),
82
+ });
60
83
  },
61
84
  };
62
85
  export default phpLintRunner;
@@ -6,7 +6,7 @@ import { PRIORITY } from "../priorities.js";
6
6
  import { createAvailabilityChecker, resolveVendorToolCommand, } from "./utils/runner-helpers.js";
7
7
  import { parseToolRun } from "./utils/tool-failure.js";
8
8
  import { finishParsedRun } from "./utils/tool-failure.js";
9
- // phpstan's documented exit codes: 0 = no errors, 1 = errors found, 2 = a
9
+ // EXIT TABLE (PHPStan 1.12 docs https://phpstan.org/user-guide/command-line-usage): 0 = no errors, 1 = errors found, 2 = a
10
10
  // fatal/internal error that stopped the analysis. Only 1 is a run that carries
11
11
  // findings.
12
12
  const PHPSTAN_EXIT_CODES = { ran: [1] };
@@ -7,11 +7,14 @@
7
7
  *
8
8
  * Supports bundle exec (preferred in Bundler projects).
9
9
  */
10
+ import * as path from "node:path";
11
+ import { pathsEqual } from "../../path-utils.js";
10
12
  import { safeSpawnAsync } from "../../safe-spawn.js";
11
13
  import { resolveRunnerCwd } from "../../tool-cwd.js";
12
14
  import { getAutofixCapability, getLinterPolicyForCwd, getRubocopCommand, } from "../../tool-policy.js";
13
15
  import { PRIORITY } from "../priorities.js";
14
16
  import { resolveCommandArgsWithInstallFallback } from "./utils/runner-helpers.js";
17
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
15
18
  const SEVERITY_MAP = {
16
19
  fatal: "error",
17
20
  error: "error",
@@ -19,12 +22,17 @@ const SEVERITY_MAP = {
19
22
  convention: "warning",
20
23
  refactor: "info",
21
24
  };
22
- function parseRubocopJson(raw, filePath) {
25
+ function parseRubocopJson(raw, filePath, cwd) {
23
26
  try {
24
27
  const output = JSON.parse(raw);
25
28
  const autofix = getAutofixCapability("rubocop");
26
29
  const diagnostics = [];
30
+ const absTarget = path.resolve(cwd, filePath);
27
31
  for (const file of output.files) {
32
+ // #3295: `.rubocop.yml` `Include:`/`inherit_from` can widen the run past
33
+ // the argv, and each result carries its own `path`.
34
+ if (file.path && !pathsEqual(path.resolve(cwd, file.path), absTarget))
35
+ continue;
28
36
  for (const offense of file.offenses) {
29
37
  const severity = SEVERITY_MAP[offense.severity] ?? "warning";
30
38
  diagnostics.push({
@@ -68,23 +76,24 @@ const rubocopRunner = {
68
76
  const { cmd, args } = resolved;
69
77
  // Lint only — no auto-correct (formatter handles that)
70
78
  const result = await safeSpawnAsync(cmd, [...args, "--format", "json", "--no-color", ctx.filePath], { timeout: 30000, cwd });
71
- // rubocop exits 0 = no offenses, 1 = offenses found, 2 = fatal error
72
- if (result.status === 2) {
73
- return { status: "skipped", diagnostics: [], semantic: "none" };
74
- }
75
- if (result.status === 0) {
76
- return { status: "succeeded", diagnostics: [], semantic: "none" };
77
- }
78
- const diagnostics = parseRubocopJson(result.stdout, ctx.filePath);
79
- if (diagnostics.length === 0) {
80
- return { status: "succeeded", diagnostics: [], semantic: "none" };
81
- }
82
- const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
83
- return {
84
- status: hasErrors ? "failed" : "succeeded",
85
- diagnostics,
86
- semantic: hasErrors ? "blocking" : "warning",
87
- };
79
+ // EXIT TABLE (RuboCop 1.66 docs https://docs.rubocop.org/rubocop/usage/basic_usage.html): 0 = clean, 1 = offenses/findings, 2 = fatal/error or rejected
80
+ // invocation. A nonzero exit with valid JSON remains findings; a nonzero
81
+ // exit with empty or unparsable JSON is never clean (#1816).
82
+ const run = parseToolRun("rubocop", {
83
+ result,
84
+ // Classify both streams so stderr-only failures reach the parse-error
85
+ // path, while parseOutput keeps RuboCop's native JSON parser on stdout.
86
+ output: `${result.stdout}${result.stderr}`,
87
+ exitCodes: { ran: [1, 2] },
88
+ }, (output) => parseRubocopJson(output, ctx.filePath, cwd), { parseOutput: result.stdout });
89
+ if (run.skipped)
90
+ return run.skipped;
91
+ return finishParsedRun({
92
+ tool: "rubocop",
93
+ ctx,
94
+ result,
95
+ diagnostics: run.diagnostics,
96
+ });
88
97
  },
89
98
  };
90
99
  export default rubocopRunner;
@@ -10,6 +10,7 @@ import { safeSpawnAsync } from "../../safe-spawn.js";
10
10
  import { stripAnsi } from "../../sanitize.js";
11
11
  import { getAutofixCapability, getLinterPolicyForCwd, ruffConfigArgs, } from "../../tool-policy.js";
12
12
  import { PRIORITY } from "../priorities.js";
13
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
13
14
  import { parseRuffOutput } from "./utils/diagnostic-parsers.js";
14
15
  import { createAvailabilityChecker, resolveAvailableOrInstall, resolveRunnerCwd, } from "./utils/runner-helpers.js";
15
16
  const ruff = createAvailabilityChecker("ruff", ".exe");
@@ -65,25 +66,23 @@ const ruffRunner = {
65
66
  // Step 1: Capture diagnostics (before fixing) — teaching signal for the agent
66
67
  const checkResult = await safeSpawnAsync(cmd, ["check", "--output-format", "json", ...configArgs, ctx.filePath], { cwd, timeout: 30000 });
67
68
  const raw = stripAnsi(checkResult.stdout + checkResult.stderr);
68
- const diagnostics = parseRuffJson(checkResult.stdout || "", ctx.filePath);
69
- const parsedDiagnostics = diagnostics.length > 0 ? diagnostics : parseRuffOutput(raw, ctx.filePath);
70
- if (checkResult.status === 0 && parsedDiagnostics.length === 0) {
71
- return { status: "succeeded", diagnostics: [], semantic: "none" };
72
- }
73
- if (parsedDiagnostics.length === 0) {
74
- return {
75
- status: "failed",
76
- diagnostics: [],
77
- semantic: "warning",
78
- rawOutput: raw.slice(0, 500),
79
- };
80
- }
81
- const hasErrors = parsedDiagnostics.some((d) => d.severity === "error");
82
- return {
83
- status: hasErrors ? "failed" : "succeeded",
84
- diagnostics: parsedDiagnostics,
85
- semantic: hasErrors ? "blocking" : "warning",
86
- };
69
+ // EXIT TABLE (Ruff 0.6 docs https://docs.astral.sh/ruff/linter/): 0 = clean, 1 = findings, 2 = findings/error or
70
+ // tool error. A nonzero exit with valid findings stays findings; empty or
71
+ // unparsable output must never become clean (#1816).
72
+ const run = parseToolRun("ruff", { result: checkResult, output: raw, exitCodes: { ran: [1, 2] } }, (output) => {
73
+ const diagnostics = parseRuffJson(output, ctx.filePath);
74
+ return diagnostics.length > 0
75
+ ? diagnostics
76
+ : parseRuffOutput(output, ctx.filePath, cwd);
77
+ });
78
+ if (run.skipped)
79
+ return run.skipped;
80
+ return finishParsedRun({
81
+ tool: "ruff",
82
+ ctx,
83
+ result: checkResult,
84
+ diagnostics: run.diagnostics,
85
+ });
87
86
  },
88
87
  };
89
88
  export default ruffRunner;
@@ -4,6 +4,7 @@
4
4
  * Runs `cargo clippy` for Rust files to catch common mistakes.
5
5
  */
6
6
  import { isAbsolute, resolve } from "node:path";
7
+ import { pathsEqual } from "../../path-utils.js";
7
8
  import { rustClient } from "../../rust-client.js";
8
9
  import { safeSpawnAsync } from "../../safe-spawn.js";
9
10
  import { stripAnsi } from "../../sanitize.js";
@@ -125,8 +126,12 @@ const rustClippyRunner = {
125
126
  // a crate-mate's pre-existing diagnostic must NOT fail the edited file's
126
127
  // turn. Filter to the edited file like golangci-lint — if the edited file
127
128
  // is clean, this turn succeeds even when siblings carry warnings/errors.
129
+ // #3278: one seam for reported-path attribution — see javac.ts. This
130
+ // member already had the right BASE (`parseClippyOutput` resolves
131
+ // `span.file` against `cargoDir`); what it lacked was the on-disk
132
+ // identity predicate.
128
133
  const absEdited = resolve(ctx.filePath);
129
- const diagnostics = allDiagnostics.filter((d) => resolve(d.filePath) === absEdited);
134
+ const diagnostics = allDiagnostics.filter((d) => pathsEqual(resolve(cargoDir, d.filePath), absEdited));
130
135
  const hasErrors = diagnostics.some((d) => d.semantic === "blocking");
131
136
  return {
132
137
  status: hasErrors ? "failed" : "succeeded",
@@ -21,6 +21,7 @@
21
21
  */
22
22
  import * as fs from "node:fs";
23
23
  import * as path from "node:path";
24
+ import { pathsEqual } from "../../path-utils.js";
24
25
  import { safeSpawnAsync } from "../../safe-spawn.js";
25
26
  import { resolveRunnerCwd } from "../../tool-cwd.js";
26
27
  import { PRIORITY } from "../priorities.js";
@@ -61,8 +62,9 @@ function findShellcheckConfig(cwd) {
61
62
  *
62
63
  * Levels: "error", "warning", "info", "style"
63
64
  */
64
- function parseShellcheckOutput(raw, filePath) {
65
+ function parseShellcheckOutput(raw, filePath, cwd) {
65
66
  const diagnostics = [];
67
+ const absTarget = path.resolve(cwd, filePath);
66
68
  if (!raw.trim()) {
67
69
  return diagnostics;
68
70
  }
@@ -74,6 +76,8 @@ function parseShellcheckOutput(raw, filePath) {
74
76
  for (const item of parsed) {
75
77
  if (!item.message || !item.line)
76
78
  continue;
79
+ if (!item.file || !pathsEqual(path.resolve(cwd, item.file), absTarget))
80
+ continue;
77
81
  // Map shellcheck levels to our severity
78
82
  const severityMap = {
79
83
  error: "error",
@@ -157,7 +161,7 @@ const shellcheckRunner = {
157
161
  }
158
162
  // Parse diagnostics
159
163
  const raw = result.stdout + result.stderr;
160
- const diagnostics = parseShellcheckOutput(raw, ctx.filePath);
164
+ const diagnostics = parseShellcheckOutput(raw, ctx.filePath, cwd);
161
165
  return finishParsedRun({
162
166
  tool: "shellcheck",
163
167
  ctx,
@@ -18,12 +18,14 @@
18
18
  * Install: cargo install typos-cli
19
19
  * Or: npm install -g typos-cli (if wrapped)
20
20
  */
21
+ import * as path from "node:path";
22
+ import { pathsEqual } from "../../path-utils.js";
21
23
  import { safeSpawnAsync } from "../../safe-spawn.js";
22
24
  import { PRIORITY } from "../priorities.js";
23
25
  import { createAvailabilityChecker, resolveRunnerCwd, } from "./utils/runner-helpers.js";
24
26
  import { parseToolRun } from "./utils/tool-failure.js";
25
27
  const typos = createAvailabilityChecker("typos", ".exe");
26
- // typos-cli exit codes: 0 = no typos, 2 = typos found, 1 = an error that
28
+ // EXIT TABLE (typos-cli 1.29 docs https://github.com/crate-ci/typos): 0 = no typos, 2 = typos found, 1 = an error that
27
29
  // stopped the scan. Anything nonzero outside {2} is a rejected invocation.
28
30
  const TYPOS_EXIT_CODES = { ran: [2] };
29
31
  /**
@@ -38,8 +40,9 @@ const TYPOS_EXIT_CODES = { ran: [2] };
38
40
  * "corrections": ["receive"]
39
41
  * }
40
42
  */
41
- function parseTyposOutput(raw, filePath) {
43
+ function parseTyposOutput(raw, filePath, cwd) {
42
44
  const diagnostics = [];
45
+ const absTarget = path.resolve(cwd, filePath);
43
46
  if (!raw.trim()) {
44
47
  return diagnostics;
45
48
  }
@@ -52,6 +55,10 @@ function parseTyposOutput(raw, filePath) {
52
55
  const parsed = JSON.parse(line);
53
56
  if (!parsed.typo || !parsed.line_num)
54
57
  continue;
58
+ // #3295: every typos JSONL record names its own `path`; a `typos.toml`
59
+ // `files.extend-exclude` or a directory argv makes that a second file.
60
+ if (parsed.path && !pathsEqual(path.resolve(cwd, parsed.path), absTarget))
61
+ continue;
55
62
  const corrections = parsed.corrections?.join(", ") || "no suggestions";
56
63
  const message = `Typo: "${parsed.typo}" → ${corrections}`;
57
64
  diagnostics.push({
@@ -104,7 +111,7 @@ const spellcheckRunner = {
104
111
  // yield nothing leaves a record. `parseOutput` keeps the historical
105
112
  // split: the gate judges "did it run" on stdout, the parser reads both
106
113
  // streams.
107
- const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
114
+ const run = parseToolRun("spellcheck", { result, exitCodes: TYPOS_EXIT_CODES }, (out) => parseTyposOutput(out, ctx.filePath, cwd), { parseOutput: `${result.stdout ?? ""}${result.stderr ?? ""}` });
108
115
  if (run.skipped)
109
116
  return run.skipped;
110
117
  const diagnostics = run.diagnostics;
@@ -1,3 +1,5 @@
1
+ import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
1
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
2
4
  import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
3
5
  import { getLinterPolicyForCwd, hasSqlfluffConfig } from "../../tool-policy.js";
@@ -11,6 +13,7 @@ const sqlfluff = createAvailabilityChecker("sqlfluff", ".exe");
11
13
  // Only 2 is a rejected invocation.
12
14
  //
13
15
  // The exit TABLE is what protects this runner, not the nothing-to-parse rule:
16
+ // EXIT TABLE (SQLFluff 3.1 docs https://docs.sqlfluff.com/en/stable/production.html): 0 clean; 1 findings/error; other nonzero rejected.
14
17
  // sqlfluff writes its user errors to STDOUT, not stderr (live: an unknown
15
18
  // dialect prints the error there), so stdout is non-empty and the fallback
16
19
  // rule would read the failed run as a run that found nothing.
@@ -73,7 +76,7 @@ const SQLFLUFF_FIXABLE_RULES = new Set([
73
76
  "ST01",
74
77
  "ST02",
75
78
  ]);
76
- function parseSqlfluffOutput(raw, filePath) {
79
+ function parseSqlfluffOutput(raw, filePath, cwd) {
77
80
  if (!raw.trim())
78
81
  return [];
79
82
  try {
@@ -81,7 +84,13 @@ function parseSqlfluffOutput(raw, filePath) {
81
84
  if (!Array.isArray(parsed))
82
85
  return [];
83
86
  const diagnostics = [];
87
+ const absTarget = path.resolve(cwd, filePath);
84
88
  for (const item of parsed) {
89
+ // #3295: sqlfluff's JSON is an array of FILES; a jinja `{% include %}`
90
+ // or a directory argv puts a second one in it.
91
+ if (item.filepath &&
92
+ !pathsEqual(path.resolve(cwd, item.filepath), absTarget))
93
+ continue;
85
94
  for (const v of item.violations ?? []) {
86
95
  if (!v.description)
87
96
  continue;
@@ -158,7 +167,7 @@ const sqlfluffRunner = {
158
167
  // reported the SQL file as clean.
159
168
  // #1948: sqlfluff exit 1 with a JSON report we read nothing out of is a
160
169
  // parser break, not a clean file.
161
- const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath), { parseOutput: result.stdout ?? "" });
170
+ const run = parseToolRun("sqlfluff", { result, exitCodes: SQLFLUFF_EXIT_CODES }, (out) => parseSqlfluffOutput(out, ctx.filePath, cwd), { parseOutput: result.stdout ?? "" });
162
171
  if (run.skipped)
163
172
  return run.skipped;
164
173
  const diagnostics = run.diagnostics;
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { logRunnerAdvisoryOnce, resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { getLinterPolicyForCwd, hasStylelintConfig, } from "../../tool-policy.js";
@@ -18,6 +19,7 @@ const stylelint = createAvailabilityChecker("stylelint", ".cmd");
18
19
  // exists to name.
19
20
  //
20
21
  // #1937 corrected the safety argument that used to sit here: it claimed a
22
+ // EXIT TABLE (Stylelint 16 docs https://stylelint.io/user-guide/cli): 0 clean; 1 findings; 2 fatal/error; other nonzero rejected.
21
23
  // fatal stylelint "writes nothing to stdout", which is true but useless,
22
24
  // because a stylelint that DID find problems writes nothing to stdout either.
23
25
  // `stylelintReport` below is what now separates the two.
@@ -80,11 +82,17 @@ const STYLELINT_FIXABLE_RULES = new Set([
80
82
  "string-quotes",
81
83
  "value-no-vendor-prefix",
82
84
  ]);
83
- function parseStylelintJson(raw, filePath) {
85
+ function parseStylelintJson(raw, filePath, cwd) {
84
86
  try {
85
87
  const results = JSON.parse(raw);
86
88
  const diagnostics = [];
89
+ const absTarget = path.resolve(cwd, filePath);
87
90
  for (const result of results) {
91
+ // #3295: stylelint keys each result by its own `source`; a config
92
+ // `files`/`overrides` entry or an `@import` makes that a DIFFERENT file.
93
+ if (result.source &&
94
+ !pathsEqual(path.resolve(cwd, result.source), absTarget))
95
+ continue;
88
96
  for (const w of result.warnings) {
89
97
  const severity = w.severity === "error" ? "error" : "warning";
90
98
  const fixable = STYLELINT_FIXABLE_RULES.has(w.rule);
@@ -143,7 +151,7 @@ const stylelintRunner = {
143
151
  const raw = stylelintReport(result.stdout, result.stderr);
144
152
  // #1948: the stdout-vs-stderr report bug landed here as exit 2 plus a
145
153
  // 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));
154
+ const run = parseToolRun("stylelint", { result, output: raw, exitCodes: STYLELINT_EXIT_CODES }, (out) => parseStylelintJson(out, ctx.filePath, cwd));
147
155
  if (run.skipped)
148
156
  return run.skipped;
149
157
  const diagnostics = run.diagnostics;
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { pathsEqual } from "../../path-utils.js";
2
3
  import { safeSpawnAsync } from "../../safe-spawn.js";
3
4
  import { resolveRunnerCwd } from "../../tool-cwd.js";
4
5
  import { PRIORITY } from "../priorities.js";
@@ -68,10 +69,11 @@ const SWIFTLINT_FIXABLE_RULES = new Set([
68
69
  "void_return",
69
70
  "yoda_condition",
70
71
  ]);
71
- function parseSwiftLintOutput(raw, filePath) {
72
+ function parseSwiftLintOutput(raw, filePath, cwd) {
72
73
  const diagnostics = [];
73
74
  if (!raw.trim())
74
75
  return diagnostics;
76
+ const absTarget = path.resolve(cwd, filePath);
75
77
  try {
76
78
  const parsed = JSON.parse(raw);
77
79
  if (!Array.isArray(parsed))
@@ -79,6 +81,9 @@ function parseSwiftLintOutput(raw, filePath) {
79
81
  for (const item of parsed) {
80
82
  if (!item.reason)
81
83
  continue;
84
+ // #3295: `.swiftlint.yml` `included:` widens the run past the argv.
85
+ if (item.file && !pathsEqual(path.resolve(cwd, item.file), absTarget))
86
+ continue;
82
87
  const severityMap = {
83
88
  error: "error",
84
89
  warning: "warning",
@@ -135,7 +140,11 @@ const swiftlintRunner = {
135
140
  // never ran.
136
141
  // SwiftLint exits non-zero on violations — stdout still has the JSON, so
137
142
  // a nonzero exit whose JSON yields nothing is a parser break (#1948).
138
- const run = parseToolRun("swiftlint", { result }, (out) => parseSwiftLintOutput(out, ctx.filePath));
143
+ const run = parseToolRun("swiftlint", {
144
+ result,
145
+ // EXIT TABLE (SwiftLint 0.56 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
146
+ exitCodes: { ran: [1, 2] },
147
+ }, (out) => parseSwiftLintOutput(out, ctx.filePath, cwd));
139
148
  if (run.skipped)
140
149
  return run.skipped;
141
150
  const diagnostics = run.diagnostics;