@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
@@ -19,16 +19,17 @@ import * as crypto from "node:crypto";
19
19
  import * as fs from "node:fs";
20
20
  import { createRequire } from "node:module";
21
21
  import * as path from "node:path";
22
+ import { getDegradationLedgerGeneration, incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
22
23
  import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
24
+ import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
23
25
  import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
24
- import { downloadGrammar, grammarBlockReason, LANGUAGE_TO_GRAMMAR, } from "./grammar-source.js";
26
+ import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
25
27
  import { resolvePackagePath } from "./package-root.js";
26
- import { recordDegradation } from "./degradation-ledger.js";
27
28
  import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
28
29
  import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
29
30
  import { notifyUserDegradation } from "./user-notify.js";
30
31
  const _require = createRequire(import.meta.url);
31
- import { createTreeCacheCounters, TreeCache, } from "./tree-sitter-cache.js";
32
+ import { createTreeCacheCounters, deriveScanTreeCacheCapacity, TreeCache, } from "./tree-sitter-cache.js";
32
33
  import { TreeSitterNavigator } from "./tree-sitter-navigator.js";
33
34
  import { TreeSitterQueryLoader, } from "./tree-sitter-query-loader.js";
34
35
  // Hard cap on a single structural-search file walk. Bounds a misrooted scan so
@@ -53,21 +54,215 @@ function createParserCounters() {
53
54
  parserFailures: 0,
54
55
  };
55
56
  }
57
+ /**
58
+ * `size:mtimeMs` identity for a grammar candidate, or undefined when the path
59
+ * is absent or not a regular file. Doubles as the existence check on the
60
+ * resolve path and as the invalidation key for the verified-preamble memo
61
+ * (#1548) — `size` + `mtimeMs` is the codebase's standard cheap stamp.
62
+ */
63
+ function grammarFileStamp(filePath) {
64
+ try {
65
+ const stat = fs.statSync(filePath);
66
+ return stat.isFile() ? `${stat.size}:${stat.mtimeMs}` : undefined;
67
+ }
68
+ catch {
69
+ return undefined;
70
+ }
71
+ }
56
72
  export function isTreeSitterWasmAbortError(error) {
57
73
  const message = error instanceof Error ? error.message : String(error);
58
74
  return message.includes("Aborted") || message.includes("abort()");
59
75
  }
76
+ /**
77
+ * Positively-identified RESOLUTION-failure codes for a dynamic `import()`:
78
+ * the specifier couldn't be found/resolved, or a transient fs error hit
79
+ * before `import()` got as far as evaluating the target module. All of
80
+ * these recover once whatever was missing/busy appears — they say nothing
81
+ * durable about the module itself (mirrors the errno allowlist in
82
+ * clients/dispatch/runners/ast-grep-napi.ts's `classifyAstGrepLoadFailure`,
83
+ * kept local here rather than shared because that file is owned by an
84
+ * in-flight PR, #1701, at the time of writing).
85
+ */
86
+ const RESOLUTION_ERROR_CODES = new Set([
87
+ "ERR_MODULE_NOT_FOUND",
88
+ "MODULE_NOT_FOUND",
89
+ "ERR_INVALID_MODULE_SPECIFIER",
90
+ "ERR_UNSUPPORTED_DIR_IMPORT",
91
+ "ENOENT",
92
+ "EMFILE",
93
+ "EBUSY",
94
+ "EAGAIN",
95
+ "EPERM",
96
+ "ETXTBSY",
97
+ ]);
98
+ /** Walk an error's `.cause` chain, collecting every `code` seen along the
99
+ * way — a resolution failure's `code` can be wrapped in a cause rather than
100
+ * sitting on the top-level thrown Error. */
101
+ function collectErrorCodes(err) {
102
+ const codes = [];
103
+ let current = err;
104
+ const seen = new Set();
105
+ while (current instanceof Error && !seen.has(current)) {
106
+ seen.add(current);
107
+ const code = current.code;
108
+ if (typeof code === "string")
109
+ codes.push(code);
110
+ current = current.cause;
111
+ }
112
+ return codes;
113
+ }
114
+ /**
115
+ * Classify a `loadWebTreeSitter()` rejection (#1592). RESOLUTION failures
116
+ * (a positively-identified code above) are recoverable: the module gets a
117
+ * fresh `import()` attempt on the very next `init()` call, same as before
118
+ * this fix, because whatever blocked resolution can plausibly clear on its
119
+ * own. Everything else is treated as EVALUATION-shaped and latches — an
120
+ * unrecognized error is far more likely to be the target module's own
121
+ * top-level code throwing (which Node's ESM loader then memoizes as a
122
+ * permanently-rejected module record for that URL) than an unclassified
123
+ * resolution hiccup, so "unknown" defaults to the case a retry cannot fix.
124
+ */
125
+ function classifyWebTreeSitterLoadFailure(err) {
126
+ const codes = collectErrorCodes(err);
127
+ return codes.some((code) => RESOLUTION_ERROR_CODES.has(code))
128
+ ? "resolution"
129
+ : "evaluation";
130
+ }
60
131
  // --- Parser Manager ---
61
132
  export class TreeSitterClient {
62
133
  initialized = false;
63
134
  initPromise = null;
135
+ /**
136
+ * Set when `loadWebTreeSitter()` itself rejects with an EVALUATION-shaped
137
+ * error (#1592, review round 2 F1/F2) — `classifyWebTreeSitterLoadFailure`
138
+ * above draws that line. That call is a dynamic `import()` of a fixed
139
+ * resolved URL, and Node's ESM loader permanently memoizes a module
140
+ * record that threw during evaluation — a later `import()` of the SAME
141
+ * URL from a later `init()` call would just replay the cached rejection,
142
+ * not re-attempt the load. Without this latch, every `withTreeSitterRoot()`
143
+ * call (one per file parse) would re-invoke `init()`, see `initPromise`
144
+ * cleared by the previous attempt's `finally`, and dynamically re-import —
145
+ * a dead retry on the hot path. A RESOLUTION-shaped rejection does NOT
146
+ * set this: it leaves `initPromise` cleared as before, so the next
147
+ * `init()` call retries for real, because that class of failure can
148
+ * plausibly clear before the next call arrives.
149
+ *
150
+ * SESSION-scoped, not process-lifetime (round 2 F2, the #1567/#1575
151
+ * `sgSessionHold` precedent): re-armed by `resetLoadStateForSession()`,
152
+ * wired into `resetDispatchBaselines()` via `tree-sitter-shared.ts`'s
153
+ * `resetTreeSitterClientLoadState()`. Re-arming does not guarantee the
154
+ * next attempt succeeds — if the process itself didn't restart between
155
+ * sessions, Node's module cache is unchanged and the replay will just
156
+ * fail fast again — but it gives a fresh degradation record for the new
157
+ * session (the ledger's own `onceKeys` are cleared at session_start too)
158
+ * instead of silently reusing a stale verdict forever, and it does let a
159
+ * genuinely fixed install (process WAS restarted) recover.
160
+ *
161
+ * Distinct from `wasmAborted`, which stays process-lifetime and is
162
+ * deliberately NOT included in the session reset: that flag means the
163
+ * Emscripten WASM heap itself aborted mid-use — a runtime that DID load
164
+ * and then corrupted its own memory. Reusing that heap after a "session"
165
+ * boundary that isn't an actual process restart is not a retry, it's
166
+ * operating on data already documented (tree-sitter-shared.ts) as
167
+ * requiring a real restart to recover. `webTreeSitterLoadFailed` only
168
+ * ever covers a runtime that never loaded in the first place, so nothing
169
+ * corrupted survives a re-arm.
170
+ */
171
+ webTreeSitterLoadFailed = false;
64
172
  languages = new Map();
65
173
  parsers = new Map();
66
174
  treeCache;
67
175
  navigator = new TreeSitterNavigator();
68
176
  grammarsDir;
69
- /** In-flight/settled lazy grammar fetches, keyed by wasm filename. */
177
+ /** In-flight lazy grammar fetches, keyed by wasm filename. Evicted on
178
+ * settle (#1536) — a rejected/false attempt must not be remembered as the
179
+ * permanent answer for the session; only concurrent demands during the
180
+ * SAME probe share it. */
70
181
  grammarEnsurePromises = new Map();
182
+ /** Epoch ms before which a failed grammar download is not retried
183
+ * (#1536). A transient download failure — offline, DNS hiccup, CDN blip —
184
+ * says nothing durable about the grammar, so it gets a bounded cooldown
185
+ * (the `transientRetryDelayMs` shape from availability-policy.ts) instead
186
+ * of latching for the process lifetime. */
187
+ grammarRetryAtMs = new Map();
188
+ /**
189
+ * Grammar path → the `size:mtimeMs` stamp at which its wasm preamble was
190
+ * verified (#1548). A POSITIVE-only memo, so the steady-state resolve costs
191
+ * one `stat` (which the old `existsSync` already paid) and re-reads the
192
+ * header only when the stamp moves. Stamped rather than a bare `Set`
193
+ * because a memo that never expires is defect-shape 6: the file it vouches
194
+ * for can be replaced under it. Failures are deliberately NOT memoized — a
195
+ * poisoned path must be re-examined on the next resolve so a successful
196
+ * re-download over it is picked up immediately.
197
+ */
198
+ verifiedGrammarPaths = new Map();
199
+ /**
200
+ * Grammar path → the `size:mtimeMs` stamp at which `Language.load` failed
201
+ * on it despite passing the wasm-preamble check (#1564) — e.g. a truncated
202
+ * download whose first four bytes are a genuine `\0asm` preamble but whose
203
+ * body decodes short ("Code section extends past end of the module"). The
204
+ * preamble check alone can't see this; only a real decode attempt can.
205
+ * `resolveGrammarFile` treats a path recorded here (at the SAME stamp) as
206
+ * absent, so the next demand re-fetches instead of reusing the same broken
207
+ * file forever. Stamped so a fresh download (new mtime) is re-examined
208
+ * rather than permanently distrusted — same shape as `verifiedGrammarPaths`.
209
+ */
210
+ decodeFailedGrammarPaths = new Map();
211
+ /**
212
+ * Grammar path → the `size:mtimeMs` stamp at which its bytes were verified
213
+ * to match the CURRENTLY PINNED sha256 manifest (#1760). Positive-only,
214
+ * same discipline as `verifiedGrammarPaths`: the hash is computed once per
215
+ * stamp (not per parse), and a stamp change — a fresh download — forces a
216
+ * re-check rather than trusting a memo made for different bytes.
217
+ */
218
+ verifiedGrammarVersionAt = new Map();
219
+ /**
220
+ * Grammar path → the `size:mtimeMs` stamp at which it was found to no
221
+ * longer match the pinned manifest hash — a version bump
222
+ * (`TREE_SITTER_WASMS_VERSION`, or a `SOURCE_OVERRIDES` entry), or on-disk
223
+ * corruption (#1760). `resolveGrammarFile` treats a path recorded here (at
224
+ * the SAME stamp) as absent, exactly like `decodeFailedGrammarPaths`, so
225
+ * the next demand re-fetches instead of serving the stale/corrupt file for
226
+ * the rest of the process's life. A fresh download (new stamp) is
227
+ * re-examined rather than permanently distrusted.
228
+ */
229
+ staleGrammarVersionAt = new Map();
230
+ /**
231
+ * Paths already reported as version-stale THIS SESSION, to log/record them
232
+ * once each (#1801 review F1). Deliberately separate from
233
+ * `staleGrammarVersionAt`: that map is a pure hash-verdict memo whose whole
234
+ * point is to persist for the process's life (re-hashing an unchanged file
235
+ * is exactly what it exists to avoid), so it must never double as a report
236
+ * gate — a gate riding on process-lifetime state can't re-arm at a session
237
+ * boundary. Cleared in `refreshGrammarSessionLatches`, mirroring
238
+ * `poisonedGrammarPaths`.
239
+ */
240
+ staleReportedGrammarPaths = new Set();
241
+ /** Paths already reported as non-wasm, to log/record them once each. */
242
+ poisonedGrammarPaths = new Set();
243
+ /** Consecutive download failures per grammar, for the exponential
244
+ * cooldown; reset on success. */
245
+ grammarFailureAttempts = new Map();
246
+ /**
247
+ * Retry delay (ms) last SHOWN to the user for this grammar, or `-1` for a
248
+ * durable (non-retryable) failure already announced. The cooldown lets
249
+ * the ensure loop retry silently in the background — only the delay's
250
+ * FIRST appearance, and any later escalation the user was never told
251
+ * about, needs to interrupt them (#1536 review F6): a fresh 30s cooldown
252
+ * re-notifies the same as an escalated 300s one once it's a genuinely new
253
+ * number, but two consecutive 30s cooldowns (identical, nothing new to
254
+ * say) do not. Absence means "not yet notified this streak."
255
+ *
256
+ * Session-scoped (#1536 review F5): cleared whenever the degradation
257
+ * ledger's own generation moves past `grammarNotificationsLedgerGen`,
258
+ * mirroring `trustBlockedGrammarNotifications` below — a lazy
259
+ * compare-at-use-time against a monotonic counter, not a listener. Tied to
260
+ * the LEDGER's generation (bumped by `resetDegradationLedger`, which
261
+ * `handleSessionStart` calls first thing) rather than trust, since this
262
+ * is a session boundary, not a trust transition.
263
+ */
264
+ grammarLastNotifiedDelayMs = new Map();
265
+ grammarNotificationsLedgerGen = getDegradationLedgerGeneration();
71
266
  trustBlockedGrammarNotifications = new Set();
72
267
  trustNotificationsGeneration = getProjectTrustGeneration();
73
268
  // biome-ignore lint/suspicious/noExplicitAny: Optional dependency loaded dynamically
@@ -180,6 +375,19 @@ export class TreeSitterClient {
180
375
  ...this.parserCounters,
181
376
  };
182
377
  }
378
+ /**
379
+ * Grow the tree cache to span a full-project scan's working set (#1715).
380
+ * The interactive default (50 entries) can't hold a mid/large project's
381
+ * file count, so a second scan re-parses everything the first scan's LRU
382
+ * evicted — live dogfood evidence showed every miss on a second 110-file
383
+ * scan was a `capacityMisses` one. Bounded by
384
+ * `TREE_CACHE_SCAN_CAPACITY_CEILING` (see `deriveScanTreeCacheCapacity`'s
385
+ * heap-cost note) and monotonic — a smaller scan never shrinks a capacity
386
+ * an earlier, larger one already grew.
387
+ */
388
+ ensureTreeCacheCapacity(fileCount) {
389
+ this.treeCache.setMaxSize(deriveScanTreeCacheCapacity(fileCount, this.treeCache.getMaxSize()));
390
+ }
183
391
  async withParseCacheMeasurement(work, onComplete) {
184
392
  const measurement = {
185
393
  ...createTreeCacheCounters(),
@@ -283,10 +491,33 @@ export class TreeSitterClient {
283
491
  return undefined;
284
492
  }
285
493
  }
494
+ _vendoredGrammarsDir;
495
+ /**
496
+ * The committed `vendor/grammars` dir, if it exists. Cached only on a hit,
497
+ * mirroring `bundledGrammarsDir`.
498
+ */
499
+ vendoredGrammarsDir() {
500
+ if (this._vendoredGrammarsDir)
501
+ return this._vendoredGrammarsDir;
502
+ try {
503
+ const dir = vendoredGrammarsDir();
504
+ if (fs.existsSync(dir))
505
+ this._vendoredGrammarsDir = dir;
506
+ return this._vendoredGrammarsDir;
507
+ }
508
+ catch {
509
+ return undefined;
510
+ }
511
+ }
286
512
  /**
287
513
  * All directories that may hold grammar wasms, in precedence order: the
288
- * bundled core dir, the resolved `this.grammarsDir`, and the web-tree-sitter
289
- * grammars dir (the lazy-fetch write target). Deduped.
514
+ * committed vendor dir, the bundled core dir, the resolved
515
+ * `this.grammarsDir`, and the web-tree-sitter grammars dir (the lazy-fetch
516
+ * write target). Deduped.
517
+ *
518
+ * `vendor/grammars` comes first because it is the ONLY source for a grammar
519
+ * we build ourselves (`VENDORED_GRAMMARS`) — nothing downloads into the
520
+ * later dirs for it, so a miss here is a miss everywhere.
290
521
  */
291
522
  grammarSourceDirs() {
292
523
  const dirs = [];
@@ -294,20 +525,184 @@ export class TreeSitterClient {
294
525
  if (d && !dirs.includes(d))
295
526
  dirs.push(d);
296
527
  };
528
+ push(this.vendoredGrammarsDir());
297
529
  push(this.bundledGrammarsDir());
298
530
  push(this.grammarsDir || undefined);
299
531
  push(this.resolveWebTreeSitterAsset("grammars"));
300
532
  return dirs;
301
533
  }
302
- /** Absolute path to `grammarFile` across all source dirs, else undefined. */
534
+ /**
535
+ * Absolute path to `grammarFile` across all source dirs, else undefined.
536
+ *
537
+ * Existence is not enough (#1548): a file whose first four bytes aren't the
538
+ * wasm preamble is not a grammar, and returning it would report the language
539
+ * as available while every `Language.load` fails — the permanent-poisoning
540
+ * shape this issue is about. #1548 stops NEW poisoned files from being
541
+ * written, but a file poisoned before that shipped is already on disk, so
542
+ * the resolve path has to reject it too: a rejected candidate is treated as
543
+ * absent, which lets `ensureGrammar` re-download over it.
544
+ */
303
545
  resolveGrammarFile(grammarFile) {
304
546
  for (const dir of this.grammarSourceDirs()) {
305
547
  const candidate = path.join(dir, grammarFile);
306
- if (fs.existsSync(candidate))
307
- return candidate;
548
+ const stamp = grammarFileStamp(candidate);
549
+ if (!stamp)
550
+ continue;
551
+ // #1564: a file that passed the preamble check but failed
552
+ // `Language.load` at this exact stamp is known-bad — treat it as
553
+ // absent so `ensureGrammar` re-fetches over it, same as the
554
+ // non-wasm-magic case below. A stamp mismatch (the file changed —
555
+ // re-downloaded) falls through to re-examine it fresh.
556
+ if (this.decodeFailedGrammarPaths.get(candidate) === stamp)
557
+ continue;
558
+ if (this.verifiedGrammarPaths.get(candidate) === stamp) {
559
+ if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
560
+ return candidate;
561
+ }
562
+ continue;
563
+ }
564
+ if (fileHasWasmMagic(candidate)) {
565
+ this.verifiedGrammarPaths.set(candidate, stamp);
566
+ if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
567
+ return candidate;
568
+ }
569
+ continue;
570
+ }
571
+ this.verifiedGrammarPaths.delete(candidate);
572
+ this.reportPoisonedGrammarFile(candidate, grammarFile);
308
573
  }
309
574
  return undefined;
310
575
  }
576
+ /**
577
+ * Clear the once-per-session grammar report gates when the degradation
578
+ * ledger's own generation has moved (#1536 review F5). Lazy
579
+ * compare-at-use-time against a monotonic counter, bumped by
580
+ * `resetDegradationLedger`, which `handleSessionStart` calls first thing —
581
+ * no listener, no retention. Every gate keyed to a SESSION rather than to
582
+ * the process lives here, so a new one cannot forget to re-arm: a gate that
583
+ * outlives the ledger it guards silently swallows the record it was only
584
+ * ever meant to de-duplicate (#1560 review F1).
585
+ */
586
+ refreshGrammarSessionLatches() {
587
+ const ledgerGen = getDegradationLedgerGeneration();
588
+ if (ledgerGen === this.grammarNotificationsLedgerGen)
589
+ return;
590
+ this.grammarNotificationsLedgerGen = ledgerGen;
591
+ this.grammarLastNotifiedDelayMs.clear();
592
+ this.poisonedGrammarPaths.clear();
593
+ this.staleReportedGrammarPaths.clear();
594
+ }
595
+ /**
596
+ * Log + record a grammar file on disk that isn't a wasm module, once per
597
+ * path per session. No user notification here: the caller goes on to attempt
598
+ * a re-download, and `recordGrammarFailure` is what interrupts the user if
599
+ * that also fails. A silent recovery should stay silent.
600
+ */
601
+ reportPoisonedGrammarFile(candidate, grammarFile) {
602
+ // Per SESSION, not per process (#1560 review F1). The file is still
603
+ // poisoned after a session boundary and still degrades the language, so
604
+ // the new session's ledger has to carry the entry — a client-lifetime
605
+ // gate would leave `resetDegradationLedger` with a permanently empty
606
+ // grammar-blocked group and no record of why the language is degraded.
607
+ this.refreshGrammarSessionLatches();
608
+ if (this.poisonedGrammarPaths.has(candidate))
609
+ return;
610
+ this.poisonedGrammarPaths.add(candidate);
611
+ logTreeSitterDiagnostic({
612
+ subsystem: "tree-sitter-client",
613
+ level: "warn",
614
+ message: `ignoring ${candidate}: the file is not a wasm module (missing the \\0asm preamble) — ` +
615
+ `most likely a captive-portal or proxy page written by an earlier download (#1548). ` +
616
+ `pi-lens will treat the grammar as missing and try to fetch it again.`,
617
+ metadata: { grammarFile, path: candidate, outcome: "not-wasm" },
618
+ });
619
+ incrementDegradationCount({
620
+ kind: "grammar-blocked",
621
+ subject: grammarFile,
622
+ reason: "on-disk grammar file is not a wasm module — ignored, re-fetching",
623
+ });
624
+ }
625
+ /**
626
+ * Is the cached grammar at `candidate` (whose wasm preamble already passed)
627
+ * still current against the pinned sha256 manifest (#1760)?
628
+ *
629
+ * A grammar downloaded once is never revisited when this repo bumps
630
+ * `TREE_SITTER_WASMS_VERSION` or changes a `SOURCE_OVERRIDES` entry — the
631
+ * cached file's name carries no version, so a stale build serves forever.
632
+ * The check is cheap and NEVER touches the network: it compares the
633
+ * on-disk sha256 (hashed once per `size:mtimeMs` stamp, memoized exactly
634
+ * like `verifiedGrammarPaths` above so a hot parse loop never re-hashes an
635
+ * unchanged file) against `pinnedGrammarHash`, which itself is a pure
636
+ * in-memory manifest lookup. A mismatch also catches on-disk corruption,
637
+ * which nothing detected before this.
638
+ *
639
+ * Vendored grammars (`VENDORED_GRAMMARS`) are skipped: they have no CDN
640
+ * pin to drift against, and their bytes are already guarded by the
641
+ * separate build-provenance check (`scripts/check-grammar-provenance.mjs`).
642
+ * No pinned hash for this filename (manifest missing, or a grammar added
643
+ * before `--write-manifest` was re-run) is treated as "can't verify" and
644
+ * trusted, the same fallback `downloadGrammarDetailed`'s own hash check
645
+ * already uses — never as a forced, unbounded refetch loop.
646
+ */
647
+ isGrammarVersionCurrent(candidate, grammarFile, stamp) {
648
+ if (isVendoredGrammar(grammarFile))
649
+ return true;
650
+ if (this.staleGrammarVersionAt.get(candidate) === stamp) {
651
+ // #1801 review F1: the hash-verdict memo above is process-lifetime by
652
+ // design (re-hashing an unchanged file on every resolve is the exact
653
+ // cost this whole check exists to avoid), so an early return here must
654
+ // NOT skip the report — a still-stale grammar has to keep showing up
655
+ // in every NEW session's ledger, not just the session that first
656
+ // discovered the mismatch.
657
+ this.reportStaleGrammarVersion(candidate, grammarFile);
658
+ return false;
659
+ }
660
+ if (this.verifiedGrammarVersionAt.get(candidate) === stamp)
661
+ return true;
662
+ const pinnedHash = pinnedGrammarHash(grammarFile);
663
+ if (!pinnedHash) {
664
+ this.verifiedGrammarVersionAt.set(candidate, stamp);
665
+ return true;
666
+ }
667
+ const actualHash = grammarFileSha256(candidate);
668
+ if (actualHash === pinnedHash) {
669
+ this.verifiedGrammarVersionAt.set(candidate, stamp);
670
+ return true;
671
+ }
672
+ this.verifiedGrammarVersionAt.delete(candidate);
673
+ this.staleGrammarVersionAt.set(candidate, stamp);
674
+ this.reportStaleGrammarVersion(candidate, grammarFile);
675
+ return false;
676
+ }
677
+ /**
678
+ * Log + record a version-stale (or corrupt) cached grammar, once per path
679
+ * per SESSION (#1801 review F1). Called from BOTH the memoized-stale path
680
+ * and the fresh-mismatch path in `isGrammarVersionCurrent`, so the report
681
+ * gate is independent of `staleGrammarVersionAt`'s own process-lifetime
682
+ * memo — a grammar that is STILL stale in a later session must still emit
683
+ * a fresh record into that session's ledger, exactly like
684
+ * `reportPoisonedGrammarFile` already does for the non-wasm case.
685
+ */
686
+ reportStaleGrammarVersion(candidate, grammarFile) {
687
+ this.refreshGrammarSessionLatches();
688
+ if (this.staleReportedGrammarPaths.has(candidate))
689
+ return;
690
+ this.staleReportedGrammarPaths.add(candidate);
691
+ logTreeSitterDiagnostic({
692
+ subsystem: "tree-sitter-client",
693
+ level: "warn",
694
+ message: `ignoring ${candidate}: its sha256 no longer matches the pinned grammar ` +
695
+ `manifest — the pinned tree-sitter-wasms version (or a SOURCE_OVERRIDES entry) ` +
696
+ `moved since this file was downloaded. pi-lens will treat the grammar as ` +
697
+ `missing and re-fetch the current build (#1760).`,
698
+ metadata: { grammarFile, path: candidate, outcome: "stale-version" },
699
+ });
700
+ incrementDegradationCount({
701
+ kind: "grammar-blocked",
702
+ subject: grammarFile,
703
+ reason: "cached grammar no longer matches the pinned manifest hash — ignored, re-fetching",
704
+ });
705
+ }
311
706
  /** Find tree-sitter grammar directory */
312
707
  findGrammarsDir() {
313
708
  const grammarsDir = this.resolveWebTreeSitterAsset("grammars");
@@ -392,65 +787,284 @@ export class TreeSitterClient {
392
787
  const inflight = this.grammarEnsurePromises.get(grammarFile);
393
788
  if (inflight)
394
789
  return inflight;
790
+ // A failed download only stays refused for its cooldown window (#1536):
791
+ // past that, fall through and retry rather than remembering an offline
792
+ // moment for the life of the process. No download is attempted while the
793
+ // cooldown is live, so a hard-down CDN is not re-hit on every parse. A
794
+ // DURABLE failure (grammarBlockDurable below) sets this to +Infinity,
795
+ // which reads the same as "still cooling down" here — it just never
796
+ // expires, matching the issue's "a genuinely unavailable grammar may
797
+ // still latch" allowance.
798
+ const retryAt = this.grammarRetryAtMs.get(grammarFile);
799
+ if (retryAt !== undefined && Date.now() < retryAt) {
800
+ return false;
801
+ }
395
802
  const task = (async () => {
396
- const dir = this.grammarsDir && fs.existsSync(this.grammarsDir)
397
- ? this.grammarsDir
398
- : this.grammarsWriteDir();
399
- if (!dir)
400
- return false;
401
- // Reuse the shared single-file downloader (same CDN/source as the
402
- // postinstall) — see clients/grammar-source.ts.
403
- const ok = await downloadGrammar(dir, grammarFile);
404
- if (ok) {
405
- if (!this.grammarsDir)
406
- this.grammarsDir = dir;
407
- logTreeSitterDiagnostic({
408
- subsystem: "tree-sitter-client",
409
- level: "warn",
410
- message: `fetched missing tree-sitter grammar ${grammarFile} at runtime (install scripts were skipped by the package manager)`,
411
- metadata: { grammarFile, outcome: "fetched" },
412
- });
803
+ try {
804
+ return await this.fetchGrammar(grammarFile);
413
805
  }
414
- else {
415
- // Surface the degradation once per grammar (the promise cache dedupes)
416
- // instead of failing silently otherwise pnpm/bun users offline get
417
- // no signal that a language's tree-sitter features are unavailable.
418
- const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable ` +
419
- `symbol search, module reports and structural rules for this language will be degraded. ` +
420
- `The package manager skipped install scripts and the runtime download failed (offline or CDN unreachable). ` +
421
- `Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
422
- `(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
423
- logTreeSitterDiagnostic({
424
- subsystem: "tree-sitter-client",
425
- message: unavailable,
426
- metadata: { grammarFile, outcome: "unavailable" },
427
- });
428
- recordDegradation({
429
- kind: "grammar-blocked",
430
- subject: grammarFile,
431
- reason: "runtime grammar download failed",
432
- });
433
- // HUMAN-audience: an offline grammar fetch silently degrades this
434
- // language's features, so it reaches the user through the HOST's
435
- // render path (#1333) rather than a raw terminal write.
436
- notifyUserDegradation(`pi-lens: ${unavailable}`);
806
+ catch (err) {
807
+ // Defensive (#1548): nothing in `fetchGrammar` throws today
808
+ // `downloadGrammarDetailed` catches internally. But a thrown error
809
+ // here used to skip the whole `else` arm, so NO cooldown was armed
810
+ // and the rejection escaped through `task.finally`the exact
811
+ // pre-#1536 shape (no cooldown, possible unhandled rejection). Funnel
812
+ // it into the same retryable failure path instead, so a future
813
+ // refactor that lets an exception out cannot regress to it.
814
+ const message = err instanceof Error ? err.message : String(err);
815
+ this.recordGrammarFailure(grammarFile, `The runtime grammar fetch threw an unexpected error (${message}).`,
816
+ /* retryable */ true);
817
+ return false;
437
818
  }
438
- return ok;
439
819
  })();
440
820
  this.grammarEnsurePromises.set(grammarFile, task);
821
+ // Evict on settle regardless of outcome (#1536): a resolved TRUE is
822
+ // superseded by resolveGrammarFile() finding the file on disk on the
823
+ // next call, and a resolved FALSE must not be remembered as the
824
+ // permanent verdict — only concurrent callers during the SAME in-flight
825
+ // attempt should ever observe this promise.
826
+ //
827
+ // The trailing `.catch` is UNFALSIFIABLE scaffolding, not verified
828
+ // defence (#1560 review F3): `finally` returns a DERIVED promise that
829
+ // rejects whenever `task` does, but the task's own try/catch above means
830
+ // it cannot reject, so no test can make this handler fire and removing it
831
+ // changes nothing today. It is here for the day the try/catch above stops
832
+ // being exhaustive — or the eviction callback itself throws — because at
833
+ // that point the fork becomes an unhandled rejection nobody is awaiting.
834
+ void task
835
+ .finally(() => {
836
+ if (this.grammarEnsurePromises.get(grammarFile) === task) {
837
+ this.grammarEnsurePromises.delete(grammarFile);
838
+ }
839
+ })
840
+ .catch(() => {
841
+ /* see above: the task's own catch is the real handler */
842
+ });
441
843
  return task;
442
844
  }
845
+ /**
846
+ * One grammar-fetch attempt: locate a writable dir, download, and record the
847
+ * outcome. Split out of `ensureGrammar` so the whole body sits under one
848
+ * try/catch that funnels any throw into `recordGrammarFailure` (#1548).
849
+ */
850
+ async fetchGrammar(grammarFile) {
851
+ // BEFORE resolving a write directory: a vendored grammar is never
852
+ // downloaded, so the write dir has no bearing on its verdict. Resolving
853
+ // first let the "no writable grammars directory" branch below answer
854
+ // RETRYABLE for a missing vendored wasm on any host where
855
+ // web-tree-sitter isn't locatable — reporting a packaging fault as a
856
+ // transient download failure, and arming a cooldown for a fetch that can
857
+ // never happen.
858
+ if (isVendoredGrammar(grammarFile)) {
859
+ const { reason } = vendoredGrammarRefusal(grammarFile);
860
+ this.recordGrammarFailure(grammarFile, reason ?? `${grammarFile} is missing from vendor/grammars/.`,
861
+ /* retryable */ false);
862
+ return false;
863
+ }
864
+ const dir = this.grammarsDir && fs.existsSync(this.grammarsDir)
865
+ ? this.grammarsDir
866
+ : this.grammarsWriteDir();
867
+ if (!dir) {
868
+ // No writable grammars directory could be located (e.g. pi compiled
869
+ // this module to a temp dir and web-tree-sitter isn't resolvable
870
+ // from there yet). This is an environment condition, not a CDN
871
+ // verdict — #1536 review F2: it must get the same cooldown +
872
+ // notification treatment as a download failure, not a silent
873
+ // unmemoized `false` that re-does the same failing resolution
874
+ // sweep on every single demand.
875
+ this.recordGrammarFailure(grammarFile, "No writable grammars directory could be located for the runtime fetch.",
876
+ /* retryable */ true);
877
+ return false;
878
+ }
879
+ // Reuse the shared single-file downloader (same CDN/source as the
880
+ // postinstall) — see clients/grammar-source.ts.
881
+ const { ok, retryable, reason } = await downloadGrammarDetailed(dir, grammarFile);
882
+ if (ok) {
883
+ if (!this.grammarsDir)
884
+ this.grammarsDir = dir;
885
+ this.grammarRetryAtMs.delete(grammarFile);
886
+ this.grammarFailureAttempts.delete(grammarFile);
887
+ this.grammarLastNotifiedDelayMs.delete(grammarFile);
888
+ logTreeSitterDiagnostic({
889
+ subsystem: "tree-sitter-client",
890
+ level: "warn",
891
+ message: `fetched missing tree-sitter grammar ${grammarFile} at runtime (install scripts were skipped by the package manager)`,
892
+ metadata: { grammarFile, outcome: "fetched" },
893
+ });
894
+ }
895
+ else {
896
+ // `retryable` distinguishes a durable CDN verdict (404/410 — a
897
+ // retry hits the same answer) from everything else (offline, DNS,
898
+ // a down CDN, a 5xx, a timeout), which says nothing durable about
899
+ // the grammar (#1536 review F4). Only the retryable case gets the
900
+ // bounded cooldown; a durable failure keeps the pre-#1536 latched
901
+ // behavior, matching the issue's own allowance.
902
+ //
903
+ // `reason`, when the downloader supplies one, names the actual failure
904
+ // shape (#1548: "returned an HTML page, not a wasm module") instead of
905
+ // the generic guess — the difference between a diagnosable log line
906
+ // and one that sends the user to inspect their package manager while a
907
+ // captive portal is the real cause.
908
+ this.recordGrammarFailure(grammarFile, reason ??
909
+ "The package manager skipped install scripts and the runtime download failed.", retryable);
910
+ }
911
+ return ok;
912
+ }
913
+ /**
914
+ * Record one failed grammar-ensure attempt: arm (or extend) the retry
915
+ * cooldown, record the degradation, and notify the user at most once per
916
+ * session per DISTINCT retry delay (#1536 review F1/F2/F4/F5/F6). Shared
917
+ * by the "no writable directory" and "download failed" arms of
918
+ * `ensureGrammar` — both are failures of the SAME ensure attempt, just
919
+ * with a different point of failure.
920
+ */
921
+ recordGrammarFailure(grammarFile, detail, retryable) {
922
+ const attempts = (this.grammarFailureAttempts.get(grammarFile) ?? 0) + 1;
923
+ this.grammarFailureAttempts.set(grammarFile, attempts);
924
+ // A durable verdict (404/410) never expires -- matches the issue's "a
925
+ // genuinely unavailable grammar may still latch" allowance. Everything
926
+ // else gets the bounded exponential cooldown.
927
+ const retryDelayMs = retryable
928
+ ? transientRetryDelayMs(attempts, "probe-timeout")
929
+ : undefined;
930
+ this.grammarRetryAtMs.set(grammarFile, retryDelayMs === undefined
931
+ ? Number.POSITIVE_INFINITY
932
+ : Date.now() + retryDelayMs);
933
+ const unavailable = retryable
934
+ ? `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
935
+ `module reports and structural rules for this language will be degraded. ` +
936
+ `${detail} pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; ` +
937
+ `if the problem persists, allow the package manager's build scripts ` +
938
+ `(pnpm approve-builds / bun trustedDependencies) or restore network access.`
939
+ : `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
940
+ `module reports and structural rules for this language will be degraded. ` +
941
+ `${detail} The grammar source reports it does not exist (not a network problem), so this will not ` +
942
+ `resolve on retry. Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
943
+ `(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
944
+ logTreeSitterDiagnostic({
945
+ subsystem: "tree-sitter-client",
946
+ message: unavailable,
947
+ metadata: {
948
+ grammarFile,
949
+ outcome: "unavailable",
950
+ retryable,
951
+ retryDelayMs,
952
+ attempts,
953
+ },
954
+ });
955
+ // #1536 review F1: incrementDegradationCount keeps ONE ring-buffer slot
956
+ // per subject (bumping its count in place) instead of recordDegradation's
957
+ // one-new-entry-per-call — a grammar retrying every cooldown window would
958
+ // otherwise flood the shared "grammar-blocked" kind's 20-slot ring and
959
+ // evict unrelated entries (e.g. a different grammar's V8-crash block
960
+ // reason from loadLanguage/grammarBlockReason) after ~20 cycles.
961
+ incrementDegradationCount({
962
+ kind: "grammar-blocked",
963
+ subject: grammarFile,
964
+ // #1548: carry `detail` into the RETRYABLE reason too. All retryable
965
+ // failures used to share one string, so the ledger could not tell an
966
+ // offline laptop from a captive portal serving HTML — the two need
967
+ // different user actions, and the ledger is what a bug report shows.
968
+ reason: retryable
969
+ ? `runtime grammar download failed — retryable (${detail.trim()})`
970
+ : `runtime grammar download failed — durable (${detail.trim()})`,
971
+ });
972
+ this.refreshGrammarSessionLatches();
973
+ // HUMAN-audience: an offline grammar fetch silently degrades this
974
+ // language's features, so it reaches the user through the HOST's render
975
+ // path (#1333) rather than a raw terminal write. Notify once per DISTINCT
976
+ // retry delay this session (#1536 review F6) -- a fresh failure streak or
977
+ // an escalated backoff is new information worth surfacing; an unchanged
978
+ // repeat during the same cooldown tier is not.
979
+ const notifyKey = retryDelayMs ?? -1;
980
+ if (this.grammarLastNotifiedDelayMs.get(grammarFile) !== notifyKey) {
981
+ this.grammarLastNotifiedDelayMs.set(grammarFile, notifyKey);
982
+ notifyUserDegradation(`pi-lens: ${unavailable}`);
983
+ }
984
+ }
985
+ /**
986
+ * Record a `Language.load` failure on a file `resolveGrammarFile` had just
987
+ * vouched for (#1564) — the diagnosis gap #1548 left open: the preamble
988
+ * check proves the first four bytes are `\0asm`, not that the whole body
989
+ * decodes. A decode error here (a truncated download, on-disk corruption)
990
+ * is evidence the FILE is bad, so it gets the exact same treatment as an
991
+ * ensure-time download failure: invalidate the resolve memo so the next
992
+ * demand re-fetches instead of reusing the same broken file forever, and
993
+ * reuse `recordGrammarFailure`'s cooldown/degradation/notification
994
+ * machinery rather than hand-rolling a parallel one — a persistently
995
+ * truncated CDN response would otherwise re-download and re-fail on every
996
+ * single parse.
997
+ *
998
+ * A DURABLE decode failure (the bytes are complete and correct, but ABI-
999
+ * incompatible with the installed `web-tree-sitter` — a version drift, not
1000
+ * a truncation) is classified `retryable` here too, same as everything
1001
+ * else this method sees: there is no way to tell "truncated" from "wrong
1002
+ * ABI" from the error message alone, so it re-downloads and re-fails once
1003
+ * per cooldown tier instead of latching forever. That is bounded by the
1004
+ * cooldown ladder (this never re-fetches faster than #1536's backoff) and
1005
+ * by `BLOCKED_GRAMMARS` for the narrower case that's fatal to the process
1006
+ * rather than just wrong (`grammar-source.ts`). The redownload only helps
1007
+ * if a fresh fetch could actually be a DIFFERENT (compatible) build,
1008
+ * which is exactly what the `grammar-source.test.ts` version/lock sync
1009
+ * guard (`lock.version === TREE_SITTER_WASMS_VERSION`) exists to keep
1010
+ * true — without it, a version bump with a stale lock would make every
1011
+ * redownload land the identical bytes and spin the ladder against a file
1012
+ * that can never pass.
1013
+ */
1014
+ recordGrammarLoadFailure(grammarPath, grammarFile, err) {
1015
+ const stamp = grammarFileStamp(grammarPath);
1016
+ this.verifiedGrammarPaths.delete(grammarPath);
1017
+ if (stamp)
1018
+ this.decodeFailedGrammarPaths.set(grammarPath, stamp);
1019
+ const message = err instanceof Error ? err.message : String(err);
1020
+ logTreeSitterDiagnostic({
1021
+ subsystem: "tree-sitter-client",
1022
+ level: "warn",
1023
+ message: `Language.load failed for ${grammarPath} even though the wasm preamble check ` +
1024
+ `passed (${message}) — most likely a truncated download (#1564). Treating the ` +
1025
+ `grammar as missing so the next demand re-fetches it.`,
1026
+ metadata: { grammarFile, path: grammarPath, outcome: "load-failed" },
1027
+ });
1028
+ this.recordGrammarFailure(grammarFile, `Language.load failed on a resolved grammar file (${message}).`,
1029
+ /* retryable */ true);
1030
+ }
443
1031
  /** Initialize tree-sitter WASM runtime */
444
1032
  async init() {
445
1033
  if (this.wasmAborted)
446
1034
  return false;
447
1035
  if (this.initialized)
448
1036
  return true;
1037
+ if (this.webTreeSitterLoadFailed)
1038
+ return false;
449
1039
  if (this.initPromise)
450
1040
  return this.initPromise;
451
1041
  this.initPromise = (async () => {
452
1042
  try {
453
- const mod = await loadWebTreeSitter();
1043
+ let mod;
1044
+ try {
1045
+ mod = await loadWebTreeSitter();
1046
+ }
1047
+ catch (err) {
1048
+ // See webTreeSitterLoadFailed's doc: an EVALUATION-shaped
1049
+ // rejection is a dead retry, so latch instead of leaving
1050
+ // initPromise clearable (the finally below would otherwise let
1051
+ // the very next parse call re-import the same doomed URL). A
1052
+ // RESOLUTION-shaped rejection is left alone — initPromise still
1053
+ // clears below, so the next init() call retries for real.
1054
+ const classification = classifyWebTreeSitterLoadFailure(err);
1055
+ if (classification === "evaluation") {
1056
+ this.webTreeSitterLoadFailed = true;
1057
+ }
1058
+ recordDegradationOnce({
1059
+ kind: "web-tree-sitter-load-failed",
1060
+ subject: "web-tree-sitter",
1061
+ reason: (err instanceof Error ? err.message : String(err)) +
1062
+ (classification === "resolution"
1063
+ ? " (resolution failure — retryable)"
1064
+ : " (evaluation failure — latched for the session)"),
1065
+ });
1066
+ throw err;
1067
+ }
454
1068
  // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter module shape varies (Parser direct / default-wrapped)
455
1069
  const anyMod = mod;
456
1070
  const ParserClass = anyMod.Parser || anyMod.default || anyMod;
@@ -492,6 +1106,17 @@ export class TreeSitterClient {
492
1106
  })();
493
1107
  return this.initPromise;
494
1108
  }
1109
+ /**
1110
+ * Re-arm the web-tree-sitter load latch for a new session (#1592 review
1111
+ * round 2 F2). Deliberately does NOT touch `wasmAborted` or `initialized`
1112
+ * — see `webTreeSitterLoadFailed`'s doc for why the abort case stays
1113
+ * process-lifetime, and a successful init has nothing to re-arm. Called
1114
+ * from `resetTreeSitterClientLoadState()` (tree-sitter-shared.ts), wired
1115
+ * into `resetDispatchBaselines()` beside `resetAstGrepNapiLoadState()`.
1116
+ */
1117
+ resetLoadStateForSession() {
1118
+ this.webTreeSitterLoadFailed = false;
1119
+ }
495
1120
  /** Load language grammar */
496
1121
  async loadLanguage(languageId) {
497
1122
  if (this.wasmAborted)
@@ -552,7 +1177,16 @@ export class TreeSitterClient {
552
1177
  return language;
553
1178
  }
554
1179
  catch (err) {
555
- this.reportWasmAbort(err);
1180
+ // An uncatchable-shaped WASM abort poisons the whole process (#402)
1181
+ // `reportWasmAbort` already records + handles it. Everything else
1182
+ // (#1564) is a DECODE failure on a file `resolveGrammarFile` just
1183
+ // vouched for — e.g. "Code section extends past end of the module" on
1184
+ // a truncated-but-magic-valid download. That is evidence the file
1185
+ // itself is bad, not the runtime, so record it and stop vouching for
1186
+ // this exact file so the next demand can re-fetch it.
1187
+ if (!this.reportWasmAbort(err)) {
1188
+ this.recordGrammarLoadFailure(grammarPath, grammarFile, err);
1189
+ }
556
1190
  this.dbg(`Language load error: ${err}`);
557
1191
  return null;
558
1192
  }
@@ -2350,20 +2984,18 @@ export class TreeSitterClient {
2350
2984
  return !hasExceptionSpec;
2351
2985
  }
2352
2986
  case "eq_mod_fn": {
2353
- // Workaround for web-tree-sitter not auto-applying #eq? predicates
2354
- // on the structural pattern of a query that has predicates. The
2355
- // query captures @MOD, @FN but the predicates aren't enforced
2356
- // (see evaluatePredicates in clients/tree-sitter-client.ts).
2987
+ // Belt-and-braces re-check of the #eq? predicates that
2988
+ // evaluatePredicates already enforces (clients/tree-sitter-client.ts).
2357
2989
  // This filter re-applies the #eq? checks at post_filter time.
2358
2990
  const mod = captures.MOD?.text ?? "";
2359
2991
  const fn = captures.FN?.text ?? "";
2360
2992
  return mod === "threading" && fn === "Thread";
2361
2993
  }
2362
2994
  case "regex_first_arg_identifier": {
2363
- // Workaround for web-tree-sitter not auto-applying #eq?/#match?
2364
- // predicates on the structural pattern (see evaluatePredicates).
2365
- // This post_filter re-applies both predicate checks AND
2366
- // the first-argument check:
2995
+ // Belt-and-braces re-check of the #eq?/#match? predicates that
2996
+ // evaluatePredicates already enforces, plus the first-argument
2997
+ // check that predicates can't express. This post_filter re-applies
2998
+ // both predicate checks AND the first-argument check:
2367
2999
  // 1. MOD must be "re" (would-be #eq? @MOD "re")
2368
3000
  // 2. FUNC must match the regex method pattern (#match? @FUNC ...)
2369
3001
  // 3. First arg must be an identifier (dynamic pattern)
@@ -3135,12 +3767,83 @@ export class TreeSitterClient {
3135
3767
  });
3136
3768
  }
3137
3769
  /**
3138
- * Evaluate text predicates (#match?, #eq?) for a query match.
3139
- * web-tree-sitter stores these as compiled functions in query.textPredicates[patternIndex]
3140
- * and does NOT apply them automatically via .matches().
3770
+ * Latch so the "textPredicates is malformed" diagnostic log line fires once
3771
+ * per `TreeSitterClient` instance for the life of the process (this client is
3772
+ * a process-wide singleton in production see clients/tree-sitter-shared.ts),
3773
+ * not once per call. This gates ONLY the log line: process-wide log volume is
3774
+ * the concern here, not the user-facing degradation record below, which must
3775
+ * re-arm every session (see recordDegradationOnce) — including for a Query
3776
+ * object the LRU query cache (queryCache/queryBatchCache above) kept alive
3777
+ * across a session boundary, since the cache is evicted, never cleared.
3778
+ */
3779
+ textPredicatesInvalidLogged = false;
3780
+ /**
3781
+ * Typed accessor for `query.textPredicates`. If the property is missing or the
3782
+ * wrong shape — e.g. a future web-tree-sitter upgrade renames or removes it —
3783
+ * `query.textPredicates?.[i] ?? []` would silently mean "no predicates for this
3784
+ * pattern," passing every match through unfiltered. That's a silent fail-open on
3785
+ * #match?/#eq? predicates that rules rely on for correctness, and because this
3786
+ * runs for every match of every query, it would zero out ALL structural matches
3787
+ * across every language while pi-lens reports "no issues found." Fail loud (log
3788
+ * once per client instance, record a degradation so it reaches the user) and
3789
+ * closed (report the query has no usable predicates) instead of guessing.
3790
+ *
3791
+ * No WeakSet short-circuit for already-known-bad queries here (#1523 review
3792
+ * R1): `Array.isArray` is O(1), so memoizing it buys nothing, and a
3793
+ * process-lifetime WeakSet would make the SAME cached Query object (the query
3794
+ * cache is LRU-evicted, never cleared) skip straight past
3795
+ * `recordDegradationOnce` on every call after the first — which is exactly
3796
+ * what made the degradation record fail to re-arm across a session boundary
3797
+ * for a query the cache kept warm. `recordDegradationOnce` itself is called
3798
+ * unconditionally on every invalid check; the ledger's own once-per-kind/
3799
+ * subject dedupe (cleared by resetDegradationLedger, which handleSessionStart
3800
+ * calls first thing — clients/runtime-session.ts) is the single source of
3801
+ * truth for "how often does this actually get recorded."
3802
+ */
3803
+ // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter Query instances
3804
+ hasValidTextPredicates(query) {
3805
+ const valid = Array.isArray(query?.textPredicates);
3806
+ if (!valid) {
3807
+ if (!this.textPredicatesInvalidLogged) {
3808
+ this.textPredicatesInvalidLogged = true;
3809
+ logTreeSitterDiagnostic({
3810
+ subsystem: "tree-sitter-client",
3811
+ message: "web-tree-sitter Query.textPredicates is missing or not an array — " +
3812
+ "#match?/#eq? predicates cannot be evaluated. Failing CLOSED: matches " +
3813
+ "for this query are dropped rather than reported unfiltered.",
3814
+ metadata: { textPredicatesType: typeof query?.textPredicates },
3815
+ });
3816
+ }
3817
+ // User-facing signal (#1523 review F1). Called unconditionally on every
3818
+ // invalid check (not gated by a client-lifetime latch) so it re-arms
3819
+ // every session, per the R1 fix above.
3820
+ recordDegradationOnce({
3821
+ kind: "query-predicates-invalid",
3822
+ subject: "web-tree-sitter",
3823
+ reason: "Query.textPredicates is missing or not an array — #match?/#eq? " +
3824
+ "predicates cannot be evaluated; structural matches relying on them " +
3825
+ "are dropped fail-closed",
3826
+ });
3827
+ }
3828
+ return valid;
3829
+ }
3830
+ /**
3831
+ * Evaluate text predicates (#match?, #eq?) for a query match. web-tree-sitter
3832
+ * 0.25's `Query.matches()` DOES apply these predicates itself (probed on the
3833
+ * shipped grammars — see clients/tree-sitter-symbol-extractor.ts), so this is a
3834
+ * belt-and-braces re-filter, not a workaround for missing enforcement. It also
3835
+ * doubles as the fail-closed guard (#1523): if `query.textPredicates` is ever
3836
+ * missing or malformed, this drops the match instead of assuming it already
3837
+ * passed upstream.
3141
3838
  */
3142
3839
  // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter types
3143
3840
  evaluatePredicates(query, match) {
3841
+ if (!this.hasValidTextPredicates(query))
3842
+ return false;
3843
+ // `?.` guards a post-validation removal of the property (e.g. a later
3844
+ // mutation strips it after the first successful check) so the read can't
3845
+ // throw here — an uncaught throw would be swallowed by the outer try/catch
3846
+ // in searchFileWithQuery and silently zero out matches for the whole file.
3144
3847
  const predicates = query.textPredicates?.[match.patternIndex] ?? [];
3145
3848
  return predicates.every((fn) => fn(match.captures));
3146
3849
  }
@@ -3161,7 +3864,9 @@ export class TreeSitterClient {
3161
3864
  captures[capture.name] = capture.node;
3162
3865
  }
3163
3866
  }
3164
- // Evaluate #match? and #eq? predicates that web-tree-sitter doesn't enforce automatically
3867
+ // Belt-and-braces re-check of #match?/#eq? predicates (web-tree-sitter
3868
+ // 0.25's Query.matches() already applies them) plus the fail-closed
3869
+ // guard for a malformed query.textPredicates (see evaluatePredicates).
3165
3870
  if (!this.evaluatePredicates(query, match)) {
3166
3871
  continue;
3167
3872
  }