@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
@@ -18,7 +18,7 @@
18
18
  * hold dispatch `Diagnostic`s, `applyLspFindingPolicy` for the probe lane,
19
19
  * which holds raw `LSPDiagnostic`s.
20
20
  */
21
- import { applyDispositions, hasAnyDispositionMarks, } from "../diagnostic-dispositions.js";
21
+ import { applyDispositions, applyDispositionsMultiFile, hasAnyDispositionMarks, } from "../diagnostic-dispositions.js";
22
22
  import { loadPiLensProjectConfig } from "../project-lens-config.js";
23
23
  import { retagAuxiliaryDiagnostics } from "./auxiliary-lsp.js";
24
24
  import { applyInlineSuppressions } from "./inline-suppressions.js";
@@ -129,6 +129,57 @@ function probeIdentities(canonical, raw) {
129
129
  export function renderedRuleIdentities(diagnostic) {
130
130
  return expandRenderedIdentities([[diagnostic.tool, diagnostic.rule]]);
131
131
  }
132
+ /**
133
+ * #3246: the identity spellings a mark against a surface that renders NEITHER
134
+ * the tool NOR the rule can carry — the turn-end unresolved-inline-blocker
135
+ * body, whose every line is `formatDiagnostics`' ` L<n>: <message>` (plus an
136
+ * optional fix hint). An agent marking from that text has only the message and
137
+ * the line to give `lens_diagnostic_mark`, whose `tool` AND `rule` parameters
138
+ * are both optional, so the mark's anchor carries neither; a mark made from the
139
+ * widget or `mode=full` carries the full canonical pair for the same finding.
140
+ * Honoring one spelling and not the other is exactly the non-convergence #3088
141
+ * reported, so both go through the SAME expansion `probeIdentities` uses. The
142
+ * bare spelling stays safe for this STOP-tier surface because a `"blocking"`
143
+ * diagnostic can only ever be dropped by the STRICT, content-bound
144
+ * false-positive anchor (`applyDispositions`' F1 rule), which still binds the
145
+ * normalized message and the flagged line's content hash.
146
+ */
147
+ export function inlineBlockerIdentities(diagnostic) {
148
+ return expandRenderedIdentities([
149
+ [diagnostic.tool, diagnostic.rule],
150
+ [undefined, undefined],
151
+ ]);
152
+ }
153
+ /**
154
+ * The whole stack for a turn-end PUSH surface that renders dispatch
155
+ * diagnostics for ONE file — the late-auxiliary drain (#3102) and the
156
+ * late-runner drain (#3248), which are the same shape: a post-freshness-gate
157
+ * `Diagnostic[]`, a `<file>:<line>:<col> [<rule>] <message>` rendering that
158
+ * prints the rule but never the tool, and a suppressed count the caller
159
+ * surfaces once per delivery (#1616).
160
+ *
161
+ * It exists so those two lanes cannot drift apart: this used to be open-coded
162
+ * at the late-auxiliary site, and #3248 would have made a second copy of the
163
+ * identical four arguments — the single-source rule's exact failure shape. The
164
+ * caller still owns its own content read, because the two lanes read on
165
+ * different schedules (the auxiliary drain reads once per FILE across several
166
+ * server pairs; the runner drain reads once per pending runner).
167
+ *
168
+ * `content` is `undefined` when the file could not be read: it becomes `""`,
169
+ * `FindingPolicyOptions.content`'s documented fail-open input, so the STRICT
170
+ * anchor cannot match and nothing is hidden over an I/O error (shape 48).
171
+ */
172
+ export function applyPushedFindingPolicy(findings, options) {
173
+ const { kept } = applyFindingPolicy(findings, {
174
+ cwd: options.cwd,
175
+ filePath: options.filePath,
176
+ content: options.content ?? "",
177
+ // mtime-cached, so several files in one drain cost one stat.
178
+ policyMap: loadProjectRulePolicyMap(options.cwd),
179
+ identities: renderedRuleIdentities,
180
+ });
181
+ return { kept, suppressed: findings.length - kept.length };
182
+ }
132
183
  /** Every rendered `(tool, rule)` spelling plus the `tool`-omitted form of
133
184
  * each, de-duplicated, in order. One derivation for both callers. */
134
185
  function expandRenderedIdentities(rendered) {
@@ -220,3 +271,69 @@ export function applyLspFindingPolicy(diagnostics, options) {
220
271
  };
221
272
  return { kept: survivorsOf(kept), inlineKept: survivorsOf(inlineKept) };
222
273
  }
274
+ /**
275
+ * #1617: turn_end reads gitleaks/govulncheck/trivy straight from their
276
+ * session-scan caches and formats them into advisory/blocker text — a
277
+ * reporting lane parallel to (and, before this fix, entirely bypassing)
278
+ * `dispatcher.ts:924`'s `applyDispositions` filter. An agent-marked
279
+ * false-positive/won't-fix on one of these findings never suppressed it
280
+ * here, so it re-reported on every turn.
281
+ *
282
+ * Filters `findings` through the SAME anchor derivation the dispatch path
283
+ * and `lens_diagnostics mode=full` use (`applyDispositionsMultiFile` in
284
+ * `diagnostic-dispositions.ts`), keyed off each lane's own canonical
285
+ * `ProjectDiagnostic` adapter (`toDiagnostic`) — the exact tool/rule/message
286
+ * identity `lens_diagnostics` already surfaces and `lens_diagnostic_mark`
287
+ * already anchors a mark against, not a second, cloned identity that would
288
+ * silently diverge from what the agent actually marked.
289
+ *
290
+ * Returns the surviving findings plus how many were dropped, so a caller can
291
+ * still surface a "suppressed by disposition: N" trace (the #1616
292
+ * suppressed-bucket rule — a security finding must never vanish with no
293
+ * trace, even when the disposition that dropped it is working as intended).
294
+ */
295
+ export function filterFindingsByDisposition(findings, cwd, toDiagnostic) {
296
+ if (findings.length === 0)
297
+ return { kept: findings, suppressed: 0 };
298
+ // #3248: every `(tool, rule)` spelling a mark against these findings can
299
+ // carry, not just the canonical one. NONE of these turn-end surfaces prints
300
+ // a tool — knip renders `<file>:<line> — <type>: <name>`, dead-code
301
+ // `unused <kind> <name>`, the security lanes `Potential secret: <rule>` —
302
+ // so an agent marking from what it was SHOWN has no tool to pass, and
303
+ // `lens_diagnostic_mark`'s `tool` parameter is optional. Honouring only the
304
+ // spelling `lens_diagnostics` happens to show is the #3088 non-convergence,
305
+ // and it is fixed HERE, once, for every caller of this helper rather than
306
+ // per lane. Widening is safe for the blocking members because a
307
+ // `semantic: "blocking"` finding can still only be dropped by the STRICT,
308
+ // content-bound false-positive anchor, which keeps binding the normalized
309
+ // message and the flagged line's content.
310
+ // The expanded candidate is a `DispositionCandidate` with a path, NOT a
311
+ // `ProjectDiagnostic`: the tool-omitted spelling has no `tool`, which that
312
+ // type requires. Only the anchor derivation reads it.
313
+ const candidates = findings.flatMap((finding) => {
314
+ const diagnostic = toDiagnostic(finding);
315
+ return renderedRuleIdentities(diagnostic).map((identity) => ({
316
+ finding,
317
+ candidate: {
318
+ filePath: diagnostic.filePath,
319
+ message: diagnostic.message,
320
+ ...(diagnostic.line !== undefined && { line: diagnostic.line }),
321
+ ...(diagnostic.semantic !== undefined && {
322
+ semantic: diagnostic.semantic,
323
+ }),
324
+ ...(identity.tool !== undefined && { tool: identity.tool }),
325
+ ...(identity.rule !== undefined && { rule: identity.rule }),
326
+ },
327
+ }));
328
+ });
329
+ const survivors = new Set(applyDispositionsMultiFile(candidates.map((c) => c.candidate), cwd, (d) => d.filePath));
330
+ const disposed = new Set();
331
+ for (const candidate of candidates) {
332
+ if (!survivors.has(candidate.candidate))
333
+ disposed.add(candidate.finding);
334
+ }
335
+ const kept = disposed.size === 0
336
+ ? findings
337
+ : findings.filter((finding) => !disposed.has(finding));
338
+ return { kept, suppressed: findings.length - kept.length };
339
+ }
@@ -12,8 +12,14 @@ export function detectIndentation(content) {
12
12
  const spaceCounts = lines
13
13
  .map((line) => line.match(/^ +(?=\S)/)?.[0].length ?? 0)
14
14
  .filter((count) => count > 0);
15
- if (tabs === 0 && spaceCounts.length === 0)
16
- return DEFAULT_INDENTATION;
15
+ if (tabs === 0 && spaceCounts.length === 0) {
16
+ // The formatter gate uses the shared hasDetectableIndentation predicate. If
17
+ // that predicate fired but structural masking removed every candidate line,
18
+ // there is no style to pin and the caller must take the #3038 skip valve
19
+ // instead of imposing the default width. Keep the historical default for
20
+ // content with no raw indentation, whose callers never enter this path.
21
+ return hasDetectableIndentation(content) ? undefined : DEFAULT_INDENTATION;
22
+ }
17
23
  if (tabs > spaceCounts.length)
18
24
  return { style: "tab", width: 1 };
19
25
  if (spaceCounts.length > tabs) {
@@ -11,6 +11,25 @@
11
11
  */
12
12
  import { normalizeRuleId } from "./rule-id-normalize.js";
13
13
  const SUPPRESS_RE = /(?:\/\/|#)\s*pi-lens-ignore:\s*(.+)/;
14
+ const TRAILING_REASON_RE = /(?:\s+--\s+|\s+—\s+|:\s+).+$/u;
15
+ export function splitInlineSuppressionPayload(payload) {
16
+ const reasonMatch = TRAILING_REASON_RE.exec(payload);
17
+ const trailingReason = reasonMatch?.[0];
18
+ if (reasonMatch === null || trailingReason === undefined) {
19
+ return { ruleList: payload, trailingReason: "" };
20
+ }
21
+ return {
22
+ ruleList: payload.slice(0, reasonMatch.index),
23
+ trailingReason,
24
+ };
25
+ }
26
+ export function parseInlineSuppressionRuleIds(payload) {
27
+ const { ruleList } = splitInlineSuppressionPayload(payload);
28
+ return ruleList
29
+ .split(",")
30
+ .map((rule) => rule.trim())
31
+ .filter(Boolean);
32
+ }
14
33
  /**
15
34
  * Normalize a rule id to the form a user writes in a `pi-lens-ignore` comment.
16
35
  * The napi scan and the ast-grep LSP tag the same rule as `ast-grep:<id>` /
@@ -37,10 +56,10 @@ export function applyInlineSuppressions(diagnostics, content) {
37
56
  const m = SUPPRESS_RE.exec(lines[i]);
38
57
  if (!m)
39
58
  continue;
40
- const rules = m[1]
41
- .split(",")
42
- .map((r) => r.trim())
43
- .filter(Boolean);
59
+ const payload = m[1];
60
+ if (payload === undefined)
61
+ continue;
62
+ const rules = parseInlineSuppressionRuleIds(payload);
44
63
  const suppressedLine = i + 1; // same line (1-based)
45
64
  const nextLine = i + 2; // next line (1-based)
46
65
  for (const ruleId of rules) {
@@ -15,22 +15,29 @@
15
15
  import * as path from "node:path";
16
16
  import { minimatch } from "../deps/minimatch.js";
17
17
  import { buildEffectiveAstGrepCatalog } from "./ast-grep-catalog.js";
18
+ import { isWindowsPath, toPosix } from "../path-utils.js";
18
19
  /**
19
20
  * True when `filePath` is carved out of a rule by one of its glob `patterns`.
20
21
  *
21
22
  * The glob is matched against `filePath` relative to `root`, forward-slashed.
22
23
  * Falls back to the absolute (slash-normalized) path when `filePath` isn't
23
- * under `root` (e.g. an out-of-tree temp file), so a glob like `scripts/**`
24
- * simply never matches rather than throwing.
24
+ * under `root` (e.g. an out-of-tree temp file). Leaf-file patterns retain that
25
+ * fallback for sinks such as a double-star logger leaf; directory carve-outs
26
+ * ending in a slash plus double-star remain root-contained so a project's CLI
27
+ * exemption cannot suppress a finding in an unrelated out-of-tree file.
25
28
  */
26
29
  export function isRuleIgnoredForPath(filePath, root, patterns) {
27
30
  if (!patterns || patterns.length === 0)
28
31
  return false;
29
- const relative = path.relative(root, filePath);
30
- const displayPath = (relative.startsWith("..") ? filePath : relative)
31
- .split(path.sep)
32
- .join("/");
33
- return patterns.some((pattern) => minimatch(displayPath, pattern, { dot: true }));
32
+ const pathApi = isWindowsPath(root) || isWindowsPath(filePath) ? path.win32 : path.posix;
33
+ const relative = pathApi.relative(root, filePath);
34
+ const outsideRoot = relative !== "" &&
35
+ (relative === ".." ||
36
+ relative.startsWith(`..${pathApi.sep}`) ||
37
+ pathApi.isAbsolute(relative));
38
+ const displayPath = toPosix(outsideRoot ? filePath : relative);
39
+ return patterns.some((pattern) => (!outsideRoot || !pattern.replaceAll("\\", "/").endsWith("/**")) &&
40
+ minimatch(displayPath, pattern, { dot: true }));
34
41
  }
35
42
  /**
36
43
  * Rule id → its `ignores` globs, for the effective ast-grep catalog at `root`.
@@ -1,8 +1,10 @@
1
1
  import 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, resolveToolCommandWithInstallFallback, } from "./utils/runner-helpers.js";
7
+ import { finishParsedRun, parseToolRun } from "./utils/tool-failure.js";
6
8
  const actionlint = createAvailabilityChecker("actionlint", ".exe");
7
9
  export function isGitHubWorkflowFile(filePath) {
8
10
  const normalized = filePath.replace(/\\/g, "/");
@@ -31,14 +33,18 @@ function toDiagnostic(issue, filePath) {
31
33
  matchedText: issue.snippet,
32
34
  };
33
35
  }
34
- export function parseActionlintJson(raw, filePath) {
36
+ export function parseActionlintJson(raw, filePath, cwd) {
35
37
  const trimmed = raw.trim();
36
38
  if (!trimmed)
37
39
  return [];
40
+ // #3295: actionlint resolves reusable-workflow and composite-action refs, so
41
+ // its `filepath` is not always the workflow we asked about.
42
+ const absTarget = path.resolve(cwd, filePath);
43
+ const isTarget = (issue) => !issue.filepath || pathsEqual(path.resolve(cwd, issue.filepath), absTarget);
38
44
  try {
39
45
  const parsed = JSON.parse(trimmed);
40
46
  const issues = Array.isArray(parsed) ? parsed : [parsed];
41
- return issues.map((issue) => toDiagnostic(issue, filePath));
47
+ return issues.flatMap((issue) => isTarget(issue) ? [toDiagnostic(issue, filePath)] : []);
42
48
  }
43
49
  catch {
44
50
  // Some actionlint versions or wrappers may emit one JSON object per line.
@@ -48,6 +54,8 @@ export function parseActionlintJson(raw, filePath) {
48
54
  continue;
49
55
  try {
50
56
  const parsed = JSON.parse(line);
57
+ if (!isTarget(parsed))
58
+ continue;
51
59
  diagnostics.push(toDiagnostic(parsed, filePath));
52
60
  }
53
61
  catch {
@@ -82,32 +90,22 @@ const actionlintRunner = {
82
90
  cwd,
83
91
  timeout: 15000,
84
92
  });
85
- const raw = `${result.stdout ?? ""}${result.stderr ?? ""}`;
86
- let diagnostics = parseActionlintJson(raw, ctx.filePath);
87
- if (diagnostics.length === 0 && result.status !== 0 && raw.trim()) {
88
- diagnostics = [
89
- {
90
- id: `actionlint-${path.basename(ctx.filePath)}-failure`,
91
- message: raw.trim().split(/\r?\n/)[0] || "actionlint failed",
92
- filePath: ctx.filePath,
93
- line: 1,
94
- column: 1,
95
- severity: "error",
96
- semantic: "blocking",
97
- tool: "actionlint",
98
- rule: "actionlint",
99
- defectClass: "correctness",
100
- },
101
- ];
102
- }
103
- if (diagnostics.length === 0) {
104
- return { status: "succeeded", diagnostics: [], semantic: "none" };
105
- }
106
- return {
107
- status: "failed",
108
- diagnostics,
109
- semantic: "blocking",
110
- };
93
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
94
+ const parsed = parseToolRun("actionlint", {
95
+ result,
96
+ output,
97
+ // EXIT TABLE (actionlint 1.7.7 measured fixture): 0 clean; 1 findings; 2 error; other nonzero rejected.
98
+ exitCodes: { ran: [1, 2] },
99
+ }, (raw) => parseActionlintJson(raw, ctx.filePath, cwd));
100
+ if (parsed.skipped)
101
+ return parsed.skipped;
102
+ return finishParsedRun({
103
+ tool: "actionlint",
104
+ ctx,
105
+ result,
106
+ diagnostics: parsed.diagnostics,
107
+ classify: () => ({ status: "failed", semantic: "blocking" }),
108
+ });
111
109
  },
112
110
  };
113
111
  export default actionlintRunner;
@@ -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;