@herbertgao/pi-extensions 2026.8.6 → 2026.8.7

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 (343) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  15. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  16. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  17. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  18. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  19. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  26. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  27. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  29. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  30. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
  32. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  33. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +1 -0
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +2 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  49. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  50. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  51. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  52. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  53. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  54. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  55. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  56. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  57. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  58. package/node_modules/pi-lens/CHANGELOG.md +909 -0
  59. package/node_modules/pi-lens/README.md +11 -3
  60. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  61. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  62. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  63. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  64. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  65. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  66. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  67. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  68. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  69. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  70. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  71. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  72. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  73. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  74. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  75. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  76. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  77. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  78. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  79. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  80. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  81. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  84. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  86. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  87. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  122. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  123. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  124. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  125. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  126. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  127. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  128. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  129. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  130. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  131. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  134. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  135. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  136. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  137. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  138. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  139. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  140. package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
  141. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  142. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  144. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  145. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  146. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  147. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  148. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
  149. package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
  150. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  151. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  152. package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
  153. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  154. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  155. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  156. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  157. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  158. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  159. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  160. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  161. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  163. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  164. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  165. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  166. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  167. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  168. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  169. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  170. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  171. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  172. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  173. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  174. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  175. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  176. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  177. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  178. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  179. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  180. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  181. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  182. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  183. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  185. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  186. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  187. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  188. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  189. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  190. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
  191. package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
  192. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  193. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  194. package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
  195. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  196. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  197. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  198. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  199. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  200. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  201. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  202. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  203. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  204. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  205. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  210. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  211. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  212. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  213. package/node_modules/pi-lens/dist/index.js +67738 -55739
  214. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
  215. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  216. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  217. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  218. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  219. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  220. package/node_modules/pi-lens/docs/features.md +19 -5
  221. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  222. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  223. package/node_modules/pi-lens/docs/mcp.md +2 -2
  224. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  225. package/node_modules/pi-lens/docs/settings.md +19 -1
  226. package/node_modules/pi-lens/docs/usage.md +1 -0
  227. package/node_modules/pi-lens/package.json +10 -6
  228. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  237. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  238. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  239. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  240. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  241. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  252. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  253. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  254. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  255. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  256. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  257. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  258. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  288. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  289. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  290. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  291. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  292. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  293. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  294. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  295. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  296. package/node_modules/pi-mcp-adapter/README.md +47 -5
  297. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  298. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  299. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  300. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  301. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  302. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  303. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  304. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  305. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  306. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  307. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  308. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  309. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  310. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  311. package/node_modules/pi-mcp-adapter/package.json +3 -1
  312. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  313. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  314. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  315. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  316. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  317. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  318. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  319. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  320. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  321. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  322. package/node_modules/pi-web-access/CHANGELOG.md +23 -0
  323. package/node_modules/pi-web-access/README.md +27 -9
  324. package/node_modules/pi-web-access/anysearch.ts +4 -2
  325. package/node_modules/pi-web-access/brave.ts +16 -4
  326. package/node_modules/pi-web-access/curator-page.ts +6 -3
  327. package/node_modules/pi-web-access/curator-server.ts +4 -1
  328. package/node_modules/pi-web-access/exa.ts +18 -7
  329. package/node_modules/pi-web-access/extract.ts +28 -3
  330. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  331. package/node_modules/pi-web-access/index.ts +41 -57
  332. package/node_modules/pi-web-access/package.json +1 -1
  333. package/node_modules/pi-web-access/page-query.ts +4 -2
  334. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  335. package/node_modules/pi-web-access/parallel.ts +2 -2
  336. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  337. package/node_modules/pi-web-access/serper.ts +211 -0
  338. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  339. package/node_modules/pi-web-access/summary-review.ts +44 -22
  340. package/node_modules/pi-web-access/tavily.ts +16 -4
  341. package/node_modules/pi-web-access/utils.ts +87 -0
  342. package/node_modules/pi-web-access/valyu.ts +199 -0
  343. package/package.json +13 -13
@@ -15,7 +15,7 @@ import { findNodeToolBinary } from "./package-manager.js";
15
15
  import { isFullyQualified } from "./path-utils.js";
16
16
  import { safeSpawnAsync } from "./safe-spawn.js";
17
17
  import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
18
- import { createAvailabilityLatch } from "./dispatch/runners/utils/availability-policy.js";
18
+ import { createAvailabilityLatch, } from "./dispatch/runners/utils/availability-policy.js";
19
19
  /**
20
20
  * Build madge's argv for a circular-dependency scan.
21
21
  *
@@ -136,8 +136,15 @@ async function resolvedCommandIsStale(resolved, spawnableCache) {
136
136
  spawnableCache.set(resolved.cmd, spawnable);
137
137
  return !spawnable;
138
138
  }
139
- /** Run `mapper` over `items` with at most `concurrency` in flight at once. */
140
- async function mapWithConcurrency(items, concurrency, mapper) {
139
+ /**
140
+ * Run `mapper` over `items` with at most `concurrency` in flight at once.
141
+ * Exported (#1810 review F6) so `clients/dispatch/runners/biome-check.ts`'s
142
+ * `resolveBiomeFixKinds` can reuse this exact worker-pool shape for its
143
+ * `biome explain` fan-out instead of carrying a second copy — this file
144
+ * predates that need but is otherwise unrelated to it; a shared home for
145
+ * this one helper wasn't worth a whole new module for two call sites.
146
+ */
147
+ export async function mapWithConcurrency(items, concurrency, mapper) {
141
148
  if (items.length === 0)
142
149
  return;
143
150
  let nextIndex = 0;
@@ -353,6 +360,21 @@ export class DependencyChecker {
353
360
  this.availabilityLatch.noteUnavailable(verdict.outcome ?? "missing", verdict.cause ?? "not-found");
354
361
  return false;
355
362
  }
363
+ /**
364
+ * #1623: public availability verdict for callers outside the dispatch
365
+ * graph (mode=full's fresh-fetch) that need to say WHY `ensureAvailable()`
366
+ * most recently returned false, using the SAME outcome/cause this class
367
+ * already latched above — not a re-guessed "madge binary unavailable"
368
+ * that can't tell a transient retry-cooldown probe from a durable
369
+ * absence.
370
+ */
371
+ getAvailabilityVerdict() {
372
+ return {
373
+ outcome: this.availabilityLatch.getOutcome(),
374
+ cause: this.availabilityLatch.getCause(),
375
+ retryAtMs: this.availabilityLatch.getRetryAtMs(),
376
+ };
377
+ }
356
378
  /**
357
379
  * Check if a file is part of a circular dependency (from cache)
358
380
  */
@@ -53,26 +53,40 @@
53
53
  * assumption proves wrong in practice, a surrounding-window hash can be
54
54
  * layered on later without changing the store shape.
55
55
  */
56
- import { createHash } from "node:crypto";
57
56
  import * as fs from "node:fs";
58
57
  import * as path from "node:path";
59
58
  import { commitDurableStore } from "./durable-store.js";
60
59
  import { logDispositionEvent } from "./disposition-logger.js";
61
60
  import { publishDisposition } from "./disposition-publish.js";
62
61
  import { getProjectDataDir } from "./file-utils.js";
62
+ import { normalizeMessage as sharedNormalizeMessage, relativeFile as sharedRelativeFile, stableFindingId, } from "./finding-identity.js";
63
63
  import { normalizeMapKey } from "./path-utils.js";
64
64
  import { lineContentHash } from "./read-guard.js";
65
65
  // "defer" is session-ephemeral by design (#690) — held only in memory so it
66
66
  // resurfaces for free on the next process run, with no expiry/pruning logic
67
67
  // needed. Stores WEAK anchors (see module doc) so a deferred finding stays
68
68
  // hidden all session even if the flagged line itself is edited.
69
+ //
70
+ // Review-round F3 (#1625): this Set is process-GLOBAL, but a weak anchor
71
+ // itself only encodes a RELATIVE path (`relativeFile` derives it from
72
+ // `path.relative(cwd, filePath)`, never the cwd's own identity) plus
73
+ // tool/rule/normalizedMessage — two unrelated projects sharing the same
74
+ // relative path/tool/rule/message (e.g. "src/auth.ts" flagged by the same
75
+ // eslint rule in project A and project B) collapse onto the IDENTICAL weak
76
+ // anchor string. A defer in project A therefore silently suppressed the same
77
+ // finding in project B for the rest of the process's life. Every read/write
78
+ // below goes through `deferredKey`, which folds the resolved cwd into the Set
79
+ // key, so the two projects can no longer collide.
69
80
  const deferredThisSession = new Set();
81
+ function deferredKey(cwd, anchor) {
82
+ return `${normalizeMapKey(cwd)}::${anchor}`;
83
+ }
70
84
  /** Exported (#802) so lens-diagnostic-mark's cross-check against live widget
71
85
  * diagnostics matches a message the same way anchor derivation does — a
72
- * second, slightly different normalizer would make a real match invisible. */
73
- export function normalizeMessage(message) {
74
- return message.replace(/\s+/g, " ").trim().toLowerCase();
75
- }
86
+ * second, slightly different normalizer would make a real match invisible.
87
+ * Re-exports the shared `finding-identity.js` normalizer (#1816) so this
88
+ * module keeps its existing public surface. */
89
+ export const normalizeMessage = sharedNormalizeMessage;
76
90
  // Anchor derivation chokepoint (#1024, #210 class): the `dd:`/`ddw:` id builders
77
91
  // (computeStrictAnchor/computeWeakAnchor) both derive their path component here,
78
92
  // so a mark and its later lookup diverge whenever the two callers pass different
@@ -91,34 +105,35 @@ export function normalizeMessage(message) {
91
105
  // widget hot path, and the read side already pays exactly this cost. Semantics
92
106
  // are unchanged: the `..`-escape fallback still returns the canonical filePath,
93
107
  // only now in the same canonical form the non-escape branch uses.
94
- function relativeFile(filePath, cwd) {
95
- const canonicalCwd = normalizeMapKey(cwd);
96
- const canonicalFile = normalizeMapKey(filePath);
97
- const rel = path.relative(canonicalCwd, canonicalFile).replace(/\\/g, "/");
98
- return rel && !rel.startsWith("..") ? rel : canonicalFile;
99
- }
108
+ //
109
+ // #1816: this used to be a local copy; it is now `finding-identity.js`'s
110
+ // `relativeFile` (identical body — canonicalize both inputs through
111
+ // `normalizeMapKey` before relativizing), re-exported so the rest of this
112
+ // file, and the module doc/comments above referencing `relativeFile`, keep
113
+ // working unchanged.
114
+ const relativeFile = sharedRelativeFile;
100
115
  /** Site-specific anchor — see module doc. Used only for false-positive. */
101
116
  export function computeStrictAnchor(args) {
102
117
  const lines = args.content?.split(/\r?\n/);
103
118
  const lineText = args.line !== undefined && lines ? (lines[args.line - 1] ?? "") : "";
104
- const parts = [
105
- relativeFile(args.filePath, args.cwd),
106
- args.tool ?? "",
107
- args.rule ?? "",
108
- normalizeMessage(args.message),
109
- lineContentHash(lineText),
110
- ];
111
- return `dd:${createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12)}`;
119
+ return stableFindingId("dd:", {
120
+ cwd: args.cwd,
121
+ filePath: args.filePath,
122
+ parts: [
123
+ args.tool ?? "",
124
+ args.rule ?? "",
125
+ normalizeMessage(args.message),
126
+ lineContentHash(lineText),
127
+ ],
128
+ });
112
129
  }
113
130
  /** Intent-level anchor — see module doc. Used for defer/flagged/suppress. */
114
131
  export function computeWeakAnchor(args) {
115
- const parts = [
116
- relativeFile(args.filePath, args.cwd),
117
- args.tool ?? "",
118
- args.rule ?? "",
119
- normalizeMessage(args.message),
120
- ];
121
- return `ddw:${createHash("sha256").update(parts.join("|")).digest("hex").slice(0, 12)}`;
132
+ return stableFindingId("ddw:", {
133
+ cwd: args.cwd,
134
+ filePath: args.filePath,
135
+ parts: [args.tool ?? "", args.rule ?? "", normalizeMessage(args.message)],
136
+ });
122
137
  }
123
138
  /** Both anchors a stored/filtered diagnostic would compute — the one shared
124
139
  * derivation both the dispatch-pipeline filter and lens-diagnostics' flagged
@@ -156,6 +171,15 @@ export function _setBeforeDispositionCacheRefreshForTests(hook) {
156
171
  export function _setDispositionStatForTests(statSync) {
157
172
  dispositionStatSync = statSync ?? fs.statSync;
158
173
  }
174
+ // Test seam for `applyDispositionsMultiFile`'s per-group content read (#1625
175
+ // F2) — Vitest can't `vi.spyOn` a bare ESM `node:fs` named export directly
176
+ // ("Module namespace is not configurable in ESM"), so the F2 regression test
177
+ // (proving the empty-store hoist means zero reads, not just fast ones)
178
+ // swaps this indirection instead, mirroring `dispositionStatSync` above.
179
+ let multiFileReadFileSync = fs.readFileSync;
180
+ export function _setMultiFileReadForTests(readFileSync) {
181
+ multiFileReadFileSync = readFileSync ?? fs.readFileSync;
182
+ }
159
183
  function readState(cwd) {
160
184
  const p = statePath(cwd);
161
185
  let stat;
@@ -328,7 +352,7 @@ export function markDisposition(cwd, target, disposition, reason, identity) {
328
352
  // mark) — the log should record what this mark shadowed either way.
329
353
  const existing = readState(cwd).dispositions?.[anchor];
330
354
  if (disposition === "defer") {
331
- deferredThisSession.add(anchor);
355
+ deferredThisSession.add(deferredKey(cwd, anchor));
332
356
  emitMarkTelemetry(cwd, target, disposition, anchor, reason, existing, identity);
333
357
  return anchor;
334
358
  }
@@ -352,8 +376,12 @@ export function markDisposition(cwd, target, disposition, reason, identity) {
352
376
  export function getDisposition(cwd, anchor) {
353
377
  return readState(cwd).dispositions?.[anchor];
354
378
  }
355
- export function isDeferredThisSession(anchor) {
356
- return deferredThisSession.has(anchor);
379
+ /** #1625 F3: `anchor` alone is ambiguous across projects — a weak anchor
380
+ * encodes only a RELATIVE path plus tool/rule/message, never the project's
381
+ * own identity, so `cwd` must be supplied to disambiguate which project's
382
+ * defer is being checked. */
383
+ export function isDeferredThisSession(cwd, anchor) {
384
+ return deferredThisSession.has(deferredKey(cwd, anchor));
357
385
  }
358
386
  /** Test-only escape hatch — defer state is module-level (one process = one
359
387
  * session), so tests need to reset it between cases. */
@@ -369,6 +397,14 @@ export function _resetDeferredForTests() {
369
397
  * Computes both anchors per diagnostic (cheap — same hash primitive, twice)
370
398
  * since false-positive is keyed strict while defer/suppress are keyed weak;
371
399
  * see module doc for why each disposition binds the way it does.
400
+ *
401
+ * Review-round F1 (#1625): a `d.semantic === "blocking"` diagnostic can be
402
+ * dropped ONLY by the STRICT (false-positive) branch — never by a WEAK-
403
+ * anchored suppress or defer. Two distinct secrets sharing the same
404
+ * rule/message (e.g. two different AWS keys on different lines) collapse
405
+ * onto the SAME weak anchor, so a weak suppress/defer on one would silently
406
+ * silence the other too; unacceptable for a STOP-tier finding. `flagged` is
407
+ * unaffected either way — it never drops anything, blocking or not.
372
408
  */
373
409
  export function applyDispositions(diagnostics, cwd, filePath, content) {
374
410
  if (!diagnostics.length)
@@ -378,16 +414,21 @@ export function applyDispositions(diagnostics, cwd, filePath, content) {
378
414
  return diagnostics;
379
415
  return diagnostics.filter((d) => {
380
416
  const { strict, weak } = anchorsForDiagnostic(cwd, filePath, d, content);
381
- if (deferredThisSession.has(weak))
417
+ const isBlocking = d.semantic === "blocking";
418
+ if (!isBlocking && deferredThisSession.has(deferredKey(cwd, weak))) {
382
419
  return false;
420
+ }
383
421
  if (dispositions?.[strict]?.disposition === "false-positive")
384
422
  return false;
385
423
  // Belt-and-braces: the inline `pi-lens-ignore` comment is the real
386
424
  // suppress enforcement (see suppress-writer.ts) and normally already
387
425
  // dropped this finding upstream via applyInlineSuppressions. This is a
388
- // harmless second cover for the store-only audit trail case.
389
- if (dispositions?.[weak]?.disposition === "suppress")
426
+ // harmless second cover for the store-only audit trail case — gated
427
+ // off blocking findings per F1 above (the inline comment, unaffected
428
+ // by this gate, remains the real suppress mechanism for those too).
429
+ if (!isBlocking && dispositions?.[weak]?.disposition === "suppress") {
390
430
  return false;
431
+ }
391
432
  return true;
392
433
  });
393
434
  }
@@ -407,6 +448,13 @@ export function applyDispositions(diagnostics, cwd, filePath, content) {
407
448
  * content-based filter). suppress/defer, being intent-level and weak-anchored,
408
449
  * are the marks that must apply the instant a query re-serves cached findings,
409
450
  * and they do so here without any read.
451
+ *
452
+ * Review-round F1 (#1625): this filter is ENTIRELY weak-anchored — it has no
453
+ * strict-anchor branch to fall back to — so a `d.semantic === "blocking"`
454
+ * diagnostic is never dropped here at all, regardless of any suppress/defer
455
+ * mark. The full `applyDispositions` (content in hand, at the next per-edit
456
+ * dispatch or mode=full merge) is the only place a blocking finding can ever
457
+ * be suppressed, and only via its STRICT branch.
410
458
  */
411
459
  export function applyWeakDispositions(diagnostics, cwd, filePath) {
412
460
  if (!diagnostics.length)
@@ -415,6 +463,8 @@ export function applyWeakDispositions(diagnostics, cwd, filePath) {
415
463
  if (!dispositions && deferredThisSession.size === 0)
416
464
  return diagnostics;
417
465
  return diagnostics.filter((d) => {
466
+ if (d.semantic === "blocking")
467
+ return true;
418
468
  const weak = computeWeakAnchor({
419
469
  cwd,
420
470
  filePath,
@@ -423,10 +473,74 @@ export function applyWeakDispositions(diagnostics, cwd, filePath) {
423
473
  message: d.message,
424
474
  line: d.line,
425
475
  });
426
- if (deferredThisSession.has(weak))
476
+ if (deferredThisSession.has(deferredKey(cwd, weak)))
427
477
  return false;
428
478
  if (dispositions?.[weak]?.disposition === "suppress")
429
479
  return false;
430
480
  return true;
431
481
  });
432
482
  }
483
+ /**
484
+ * Multi-file variant of {@link applyDispositions} for lanes whose findings
485
+ * span many files in ONE report — gitleaks/trivy/govulncheck/opengrep
486
+ * project-wide scans (#1617), unlike the dispatch path's one-`ctx.filePath`-
487
+ * at-a-time shape. Groups by `filePathOf(diagnostic)`, reads each file's
488
+ * CURRENT content once, then delegates to `applyDispositions` per group —
489
+ * same anchor derivation, same strict/weak split, no cloned logic (#1617's
490
+ * single-source-of-truth requirement).
491
+ *
492
+ * Content read is best-effort: a file that no longer exists, or that this
493
+ * process can't read, degrades to `content: ""` for that group rather than
494
+ * dropping its diagnostics outright. `applyDispositions` already treats a
495
+ * missing/empty content argument safely — the STRICT (false-positive) anchor
496
+ * hashes the diagnostic's own line, so an empty content can't collide with a
497
+ * mark made against the file's real content and the finding simply isn't
498
+ * matched (fails OPEN — still reported); the WEAK anchor (suppress/defer)
499
+ * never looks at content at all, so those marks keep applying regardless.
500
+ * This is a deliberate choice, not an oversight: a security finding must
501
+ * never silently vanish just because its file went unreadable at filter
502
+ * time (a security finding staying VISIBLE on a read failure is the safe
503
+ * default; a stale mark quietly reappearing is recoverable, a leaked secret
504
+ * quietly disappearing is not).
505
+ *
506
+ * Review-round F2 (#1625): the empty-store early return is checked HERE,
507
+ * before any per-group file read — not left to `applyDispositions` to notice
508
+ * once already inside the per-group loop. Every mode=full analyzer call
509
+ * (9 lanes) runs through this on every scan, and the overwhelmingly common
510
+ * case is zero marks in the project; measured ~270ms of synchronous
511
+ * `fs.readFileSync` per analyzer per run for that common case before this
512
+ * hoist (reading every finding's file just to discover there was nothing to
513
+ * filter), ~0-1ms after.
514
+ */
515
+ export function applyDispositionsMultiFile(diagnostics, cwd, filePathOf) {
516
+ if (!diagnostics.length)
517
+ return diagnostics;
518
+ const dispositions = readState(cwd).dispositions;
519
+ if (!dispositions && deferredThisSession.size === 0)
520
+ return diagnostics;
521
+ const groups = new Map();
522
+ for (const d of diagnostics) {
523
+ const filePath = filePathOf(d);
524
+ const group = groups.get(filePath);
525
+ if (group)
526
+ group.push(d);
527
+ else
528
+ groups.set(filePath, [d]);
529
+ }
530
+ const kept = new Set();
531
+ for (const [filePath, group] of groups) {
532
+ let content = "";
533
+ try {
534
+ content = multiFileReadFileSync(filePath, "utf-8");
535
+ }
536
+ catch {
537
+ // Unreadable/missing — fail open, see doc above.
538
+ }
539
+ for (const d of applyDispositions(group, cwd, filePath, content)) {
540
+ kept.add(d);
541
+ }
542
+ }
543
+ // Preserve the caller's original order/dedup by reference rather than the
544
+ // per-group insertion order above.
545
+ return diagnostics.filter((d) => kept.has(d));
546
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Past-EOF validity gate for cited diagnostic line numbers (#1641).
3
+ *
4
+ * A live session served diagnostics at lines 407-410 of a file that was 402
5
+ * lines on disk and UNMODIFIED — the LSP's in-memory document had drifted
6
+ * longer than disk without any file write happening (no didChange lost, just
7
+ * an in-memory buffer that grew past what the file actually contains). That
8
+ * makes this a DIFFERENT invariant than #1622/#1627's freshness gate: those
9
+ * compare a cited file's MTIME against a scan timestamp, which says nothing
10
+ * when the file was never touched on disk at all. The cheap, orthogonal check
11
+ * available here is structural: a diagnostic whose start line exceeds the
12
+ * file's CURRENT line count cannot possibly be describing current content,
13
+ * regardless of what any timestamp says.
14
+ *
15
+ * CONTRACT:
16
+ * - A diagnostic citing a line beyond the file's current line count is
17
+ * DEMOTED (`stale: true`), never dropped — the underlying issue may well
18
+ * still be real, just at a coordinate this gate can no longer vouch for.
19
+ * - RE-ARMS, never latches: the verdict is recomputed from the gate's own
20
+ * inputs (current mtime, current line count) on every call, so a
21
+ * TRANSIENT shrink (truncate-then-write, a formatter pass, a checkout)
22
+ * that later restores the line un-demotes on its own next read. A
23
+ * persisted `stale: true` a caller stores between calls is a cache of the
24
+ * last-derived verdict, never an independent source of truth that
25
+ * overrides the next derivation — the #1633-V1 lesson: derive, don't latch.
26
+ * - Fail open: a file that cannot be stat'ed/read, or exceeds the size gate,
27
+ * yields no verdict at all (every diagnostic passes through unchanged).
28
+ * An unreadable/oversized file is not evidence of anything; demoting on
29
+ * it would manufacture false staleness.
30
+ * - The line count matches the LSP's own addressing, not `wc -l`: a
31
+ * document with N newline characters has N+1 ADDRESSABLE lines (the
32
+ * line after the final `\n`, or the single line of an empty document,
33
+ * is a real position a server can anchor a diagnostic to — e.g. TS1005
34
+ * "'}' expected" at EOF). `WidgetDiagnostic.line` is always
35
+ * `range.start.line + 1` from that same addressing, so the gate must
36
+ * use it too or every trailing-newline file demotes its own valid EOF
37
+ * diagnostics.
38
+ * - Cost discipline: one `statSync` per served file; the file's newlines
39
+ * are counted in bounded chunks via a raw fd read (never a full
40
+ * `readFileSync` — no whole-file string materialization, no per-render
41
+ * re-read of unchanged content), gated by a byte-size cap matching
42
+ * `captureReadContentBinding`'s precedent (`read-guard.ts`). The count is
43
+ * memoized in a small SHARED cache (module-scoped, bounded, evicted FIFO
44
+ * past a cap), keyed on BOTH mtime AND size (review round V1) — mtime
45
+ * ALONE is not a reliable cache key on hosts where its resolution is
46
+ * coarser than back-to-back writes: a truncate-then-write, a formatter
47
+ * write-back, or pi-lens's own auto-format immediately followed by the
48
+ * agent's write can land two DIFFERENT contents on the identical
49
+ * `mtimeMs`, and a mtime-only cache serves the first content's count for
50
+ * the second — measured live at 207/300 shrink/restore cycles. Requiring
51
+ * both to match narrows a same-tick collision to same-mtime-AND-same-size
52
+ * (which a shrink/restore cycle never produces, since the whole point is
53
+ * the size changed). A changed file is always recounted. Tests can still
54
+ * inject an isolated {@link LineCountCache} via {@link createLineCountCache}
55
+ * to avoid cross-test bleed.
56
+ * - On a demotion EDGE (a diagnostic that was not already flagged past-EOF
57
+ * and now is), best-effort trigger a document resync (didClose/didOpen or
58
+ * a full-sync didChange) so the desync HEALS instead of re-serving the
59
+ * same stale verdict next time (#1631's resync-before-revalidate rule).
60
+ * Resync is fire-and-forget: a failure here must never block or fail the
61
+ * render path that discovered the drift. Already-demoted entries and the
62
+ * healing direction never re-trigger a resync — only the rising edge does.
63
+ */
64
+ import * as fs from "node:fs";
65
+ import { logLatency } from "./latency-logger.js";
66
+ import { toProjectRelativePath } from "./path-utils.js";
67
+ import { STALE_LINE_MARKER } from "./stale-marker.js";
68
+ /** Marker rendered in place of a demoted diagnostic's now-untrustworthy line.
69
+ * Shares the base `STALE_LINE_MARKER` vocabulary with sibling freshness
70
+ * gates (#1622/#1633) and adds this gate's own specific reason as a suffix. */
71
+ export const PAST_EOF_STALE_MARKER = `${STALE_LINE_MARKER} — line past EOF`;
72
+ /** How many demoted cited lines a single record names before it stops. */
73
+ const MAX_LOGGED_PAST_EOF_LINES = 3;
74
+ /** Byte-size gate matching `captureReadContentBinding`'s precedent
75
+ * (`clients/read-guard.ts`'s `READ_BINDING_MAX_BYTES`) — a hot render/serve
76
+ * path must never pay to scan an unbounded file. */
77
+ const MAX_GATE_BYTES = 4 * 1024 * 1024;
78
+ /** Chunk size for the bounded newline scan — small enough to keep one read's
79
+ * memory footprint negligible, large enough that a multi-MB file finishes in
80
+ * single-digit reads rather than thousands. */
81
+ const NEWLINE_SCAN_CHUNK_BYTES = 64 * 1024;
82
+ const NEWLINE_BYTE = 0x0a;
83
+ /** Test-only isolation seam. Production code uses the shared default cache. */
84
+ export function createLineCountCache() {
85
+ return new Map();
86
+ }
87
+ // Shared across calls/render passes so N files touched once per session cost
88
+ // N recounts total, not N per render — the actual fix for the cost this gate
89
+ // exists to control. Still cannot go stale: every read re-stats first and a
90
+ // mismatched mtime always recomputes (see the module doc comment). Bounded
91
+ // via FIFO eviction (mirrors `READ_GUARD_MAX_FILES`'s precedent) so a long
92
+ // session's file count cannot grow this without limit.
93
+ const MAX_SHARED_CACHE_ENTRIES = 512;
94
+ const sharedLineCountCache = new Map();
95
+ function rememberInSharedCache(cache, filePath, entry) {
96
+ if (cache === sharedLineCountCache &&
97
+ cache.size >= MAX_SHARED_CACHE_ENTRIES &&
98
+ !cache.has(filePath)) {
99
+ const oldest = cache.keys().next().value;
100
+ if (oldest !== undefined)
101
+ cache.delete(oldest);
102
+ }
103
+ cache.set(filePath, entry);
104
+ }
105
+ /** Test-only: drop the shared memo between test files/cases. */
106
+ export function _resetSharedLineCountCacheForTests() {
107
+ sharedLineCountCache.clear();
108
+ }
109
+ /**
110
+ * Test-only: seed the shared memo with a fabricated entry, deterministically
111
+ * proving a cache HIT (and that the default cache used by callers with no
112
+ * explicit `lineCountCache` is genuinely shared across calls) without relying
113
+ * on a real write's mtime round-tripping through `fs.utimesSync` at full
114
+ * precision — a platform-specific hazard on its own, unrelated to what such a
115
+ * test is actually verifying.
116
+ */
117
+ export function _seedSharedLineCountCacheForTests(filePath, entry) {
118
+ sharedLineCountCache.set(filePath, entry);
119
+ }
120
+ /**
121
+ * Count newline bytes in `filePath` via bounded raw reads — never a full
122
+ * `readFileSync`/UTF-8 decode of the whole file. `0x0A` (LF) never appears as
123
+ * a continuation byte in UTF-8, so counting it at the byte level is exact
124
+ * regardless of encoding, and multi-byte characters spanning a chunk boundary
125
+ * cannot produce a false match (a continuation byte's high bits can never
126
+ * equal `0x0A`).
127
+ */
128
+ function countNewlinesChunked(filePath, sizeBytes) {
129
+ const fd = fs.openSync(filePath, "r");
130
+ try {
131
+ const buffer = Buffer.allocUnsafe(Math.min(NEWLINE_SCAN_CHUNK_BYTES, sizeBytes) || 1);
132
+ let newlineCount = 0;
133
+ let position = 0;
134
+ while (position < sizeBytes) {
135
+ const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, position);
136
+ if (bytesRead <= 0)
137
+ break;
138
+ for (let i = 0; i < bytesRead; i++) {
139
+ if (buffer[i] === NEWLINE_BYTE)
140
+ newlineCount++;
141
+ }
142
+ position += bytesRead;
143
+ }
144
+ return newlineCount;
145
+ }
146
+ finally {
147
+ fs.closeSync(fd);
148
+ }
149
+ }
150
+ /**
151
+ * LSP-addressable line count for `filePath`: newline count + 1, matching
152
+ * `range.start.line + 1` (a document with zero newlines still has one
153
+ * addressable line — position 0,0 — and a trailing `\n` adds one more,
154
+ * empty, addressable line after it). Memoized in `cache` (the shared default
155
+ * unless a caller injects its own) and invalidated by mtime AND size (V1 —
156
+ * mtime alone is not a reliable cache key on hosts where its resolution is
157
+ * coarser than back-to-back writes). Returns `undefined` when the file
158
+ * cannot be stat'ed, is zero bytes, exceeds the byte-size gate, or
159
+ * cannot be read (deleted, unreadable, permissions, oversized) — callers MUST
160
+ * treat `undefined` as "no verdict", not as "zero lines" (the empty-result-
161
+ * must-distinguish-clean-from-errored screen).
162
+ */
163
+ export function getCachedLineCount(filePath, cache = sharedLineCountCache) {
164
+ let stat;
165
+ try {
166
+ stat = fs.statSync(filePath);
167
+ }
168
+ catch {
169
+ return undefined;
170
+ }
171
+ // A zero-byte stat can be a truncate-then-write window. It is not evidence
172
+ // that the file has one addressable line, so fail open like an unreadable
173
+ // file rather than manufacturing a verdict from the empty read.
174
+ if (stat.size === 0)
175
+ return undefined;
176
+ const cached = cache.get(filePath);
177
+ if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
178
+ return cached.lineCount;
179
+ }
180
+ if (stat.size > MAX_GATE_BYTES)
181
+ return undefined;
182
+ let newlineCount;
183
+ try {
184
+ newlineCount = countNewlinesChunked(filePath, stat.size);
185
+ }
186
+ catch {
187
+ return undefined;
188
+ }
189
+ const lineCount = newlineCount + 1;
190
+ rememberInSharedCache(cache, filePath, {
191
+ mtimeMs: stat.mtimeMs,
192
+ size: stat.size,
193
+ lineCount,
194
+ });
195
+ return lineCount;
196
+ }
197
+ /**
198
+ * Recompute the past-EOF verdict for every diagnostic in `diagnostics`
199
+ * against `filePath`'s CURRENT line count. Sets `stale: true` on a match and
200
+ * `stale: false` when a previously-past-EOF line is back in bounds (a
201
+ * transient shrink healed) — the verdict is always derived fresh, never
202
+ * latched. Pure apart from the cached stat/read in {@link getCachedLineCount}
203
+ * and the bounded `logLatency`/`resync` calls, which fire only on a RISING
204
+ * edge (a diagnostic that was not already flagged and now is) so an
205
+ * unchanged persistent demotion doesn't re-log or re-resync on every render.
206
+ * Never throws, never mutates the input array.
207
+ */
208
+ export function demotePastEofDiagnostics(args) {
209
+ const { diagnostics } = args;
210
+ if (diagnostics.length === 0) {
211
+ return { diagnostics: [], demotedCount: 0 };
212
+ }
213
+ const lineCount = getCachedLineCount(args.filePath, args.lineCountCache ?? sharedLineCountCache);
214
+ if (lineCount === undefined) {
215
+ // Fail open: no verdict available for this file right now. Leave
216
+ // whatever `stale` value each entry already carries untouched — this
217
+ // gate has nothing to say either direction.
218
+ return { diagnostics: [...diagnostics], demotedCount: 0 };
219
+ }
220
+ const risingEdgeLines = [];
221
+ const out = diagnostics.map((d) => {
222
+ // Another gate's demotion is not this gate's to heal (#1631 drift
223
+ // entries have in-bounds lines; re-deriving here would clear them).
224
+ if (d.stale && d.staleReason !== undefined && d.staleReason !== "past-eof")
225
+ return d;
226
+ const isPastEof = typeof d.line === "number" && d.line > lineCount;
227
+ if (isPastEof === !!d.stale)
228
+ return d; // no transition either direction
229
+ if (isPastEof)
230
+ risingEdgeLines.push(d.line);
231
+ return { ...d, stale: isPastEof, staleReason: isPastEof ? "past-eof" : undefined };
232
+ });
233
+ if (risingEdgeLines.length === 0) {
234
+ return { diagnostics: out, demotedCount: 0 };
235
+ }
236
+ let sizeBytes;
237
+ try {
238
+ sizeBytes = fs.statSync(args.filePath).size;
239
+ }
240
+ catch {
241
+ sizeBytes = 0;
242
+ }
243
+ logLatency({
244
+ type: "phase",
245
+ phase: "diagnostic_past_eof",
246
+ filePath: args.cwd,
247
+ durationMs: 0,
248
+ metadata: {
249
+ store: args.store,
250
+ file: toProjectRelativePath(args.filePath, args.cwd),
251
+ sizeBytes,
252
+ actualLineCount: lineCount,
253
+ demotedCount: risingEdgeLines.length,
254
+ sampleCitedLines: risingEdgeLines.slice(0, MAX_LOGGED_PAST_EOF_LINES),
255
+ },
256
+ });
257
+ if (args.resync) {
258
+ try {
259
+ args.resync(args.filePath);
260
+ }
261
+ catch {
262
+ // Best-effort — a resync failure must never fail the render path.
263
+ }
264
+ }
265
+ return { diagnostics: out, demotedCount: risingEdgeLines.length };
266
+ }
267
+ /**
268
+ * Default resync trigger (#1641 criterion 2): re-open the file with its
269
+ * CURRENT disk content so the LSP's in-memory document is replaced rather
270
+ * than left to re-publish the same divergent diagnostics next time
271
+ * (`LSPService.openFile` sends a full-sync didChange, or a didClose/didOpen
272
+ * pair for servers that only re-scan on a fresh open — see
273
+ * `handleNotifyOpen`'s `reopenOnResync` branch). Dynamic import (mirrors
274
+ * `clients/module-report-lsp.ts`) so this module — used from widget-state.ts
275
+ * and tools/lens-diagnostics.ts — never statically depends on `clients/lsp`,
276
+ * which itself imports widget-state.ts.
277
+ *
278
+ * Fire-and-forget by design: the caller is a synchronous render path and must
279
+ * not block on (or fail because of) a resync. No LSP service, no client for
280
+ * this file's language, or a transient read/spawn failure all degrade to a
281
+ * silent no-op — the next successful touch/dispatch resyncs anyway.
282
+ */
283
+ export function resyncDocumentOnPastEof(filePath) {
284
+ void (async () => {
285
+ try {
286
+ const [{ getLSPService }, content] = await Promise.all([
287
+ import("./lsp/index.js"),
288
+ fs.promises.readFile(filePath, "utf-8"),
289
+ ]);
290
+ await getLSPService().openFile(filePath, content, {
291
+ preserveDiagnostics: false,
292
+ });
293
+ }
294
+ catch {
295
+ // Best-effort heal only.
296
+ }
297
+ })();
298
+ }