@herbertgao/pi-extensions 2026.10.1 → 2026.10.3

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 (934) hide show
  1. package/README.md +14 -17
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  10. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  11. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  12. package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  15. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  16. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  17. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  18. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
  19. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  20. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
  21. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
  22. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
  23. package/node_modules/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
  24. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  25. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  26. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  33. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
  34. package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
  35. package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
  36. package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
  37. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
  38. package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
  39. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
  40. package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
  41. package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
  42. package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
  43. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
  44. package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
  45. package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
  46. package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
  47. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  48. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  49. package/node_modules/@tifan/pi-recap/package.json +1 -1
  50. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  51. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  52. package/node_modules/pi-antigravity/README.md +23 -2
  53. package/node_modules/pi-antigravity/package.json +5 -3
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  55. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  56. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  58. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  59. package/node_modules/pi-antigravity/src/index.ts +52 -137
  60. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  61. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  62. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  63. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  64. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  65. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  66. package/node_modules/pi-lens/CHANGELOG.md +1159 -0
  67. package/node_modules/pi-lens/README.md +116 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  70. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  71. package/node_modules/pi-lens/dist/chunk-2FQ62SPM.js +83 -0
  72. package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
  73. package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
  74. package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
  75. package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
  76. package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
  77. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  78. package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
  79. package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
  80. package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
  81. package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
  82. package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
  86. package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
  87. package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
  88. package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
  89. package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
  90. package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
  91. package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
  92. package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -0
  93. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  94. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  95. package/node_modules/pi-lens/dist/chunk-AFZYMAIV.js +756 -0
  96. package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
  97. package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
  98. package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
  99. package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
  100. package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
  103. package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
  104. package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
  106. package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
  107. package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
  108. package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
  109. package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
  110. package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
  111. package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
  112. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  113. package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
  114. package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
  115. package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
  116. package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
  117. package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
  118. package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
  119. package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
  120. package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
  121. package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
  122. package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
  123. package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
  124. package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
  125. package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
  126. package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
  127. package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
  128. package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
  129. package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
  130. package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
  131. package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
  132. package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
  133. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  134. package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
  135. package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
  136. package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
  137. package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
  138. package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
  139. package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
  140. package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  142. package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
  143. package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
  144. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  145. package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
  146. package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
  147. package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
  148. package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
  149. package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
  150. package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
  151. package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -0
  152. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  153. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  154. package/node_modules/pi-lens/dist/chunk-RJFED73B.js +79 -0
  155. package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
  156. package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
  157. package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
  158. package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
  159. package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
  160. package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
  161. package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
  162. package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
  163. package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
  164. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  165. package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
  166. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  167. package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
  168. package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
  169. package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
  170. package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
  171. package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
  172. package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
  173. package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
  174. package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
  175. package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
  176. package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
  177. package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
  178. package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -0
  181. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  182. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  183. package/node_modules/pi-lens/dist/chunk-Z6BB4JLQ.js +478 -0
  184. package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
  185. package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
  186. package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
  187. package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
  188. package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
  189. package/node_modules/pi-lens/dist/index.js +39712 -29876
  190. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  191. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  192. package/node_modules/pi-lens/dist/mcp/server.js +22688 -1573
  193. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  194. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  195. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  196. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  197. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  198. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  199. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -4
  200. package/node_modules/pi-lens/docs/configuration.md +28 -2
  201. package/node_modules/pi-lens/docs/custom-rules.md +25 -2
  202. package/node_modules/pi-lens/docs/environment-variables.md +86 -8
  203. package/node_modules/pi-lens/docs/features.md +45 -5
  204. package/node_modules/pi-lens/docs/globalconfig.md +2 -1
  205. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  206. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -4
  207. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  208. package/node_modules/pi-lens/docs/mcp.md +21 -5
  209. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  210. package/node_modules/pi-lens/docs/pi-lens-fixer.md +160 -88
  211. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  212. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
  213. package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
  214. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  215. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -48
  216. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  217. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  218. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  219. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  220. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  221. package/node_modules/pi-lens/docs/schema/pi-lens-config-v1.json +530 -0
  222. package/node_modules/pi-lens/docs/servercapabilities.md +56 -49
  223. package/node_modules/pi-lens/docs/settings.md +1 -0
  224. package/node_modules/pi-lens/docs/usage.md +5 -1
  225. package/node_modules/pi-lens/docs/word-index.md +6 -0
  226. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  227. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  228. package/node_modules/pi-lens/package.json +18 -6
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
  237. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  238. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  239. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  240. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  241. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  242. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  243. package/node_modules/pi-web-access/CHANGELOG.md +28 -0
  244. package/node_modules/pi-web-access/README.md +10 -6
  245. package/node_modules/pi-web-access/ceramic.ts +219 -0
  246. package/node_modules/pi-web-access/curator-page.ts +3 -1
  247. package/node_modules/pi-web-access/curator-server.ts +1 -0
  248. package/node_modules/pi-web-access/dist/index.js +846 -588
  249. package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
  250. package/node_modules/pi-web-access/gemini-search.ts +8 -4
  251. package/node_modules/pi-web-access/github-extract.ts +34 -2
  252. package/node_modules/pi-web-access/index.ts +72 -30
  253. package/node_modules/pi-web-access/openai-search.ts +3 -3
  254. package/node_modules/pi-web-access/package.json +2 -2
  255. package/node_modules/pi-web-access/page-query.ts +6 -9
  256. package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
  257. package/node_modules/pi-web-access/query-rewrite.ts +2 -2
  258. package/node_modules/pi-web-access/storage.ts +49 -0
  259. package/node_modules/pi-web-access/summary-review.ts +7 -6
  260. package/node_modules/pi-web-access/utils.ts +21 -6
  261. package/node_modules/pi-web-access/web-tool-core.ts +1 -0
  262. package/package.json +19 -19
  263. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
  264. package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
  265. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
  266. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
  267. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  268. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  269. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  270. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  271. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  272. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  273. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  274. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  275. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  276. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  277. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  278. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  279. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  280. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  281. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  282. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  283. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  284. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  285. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  286. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  287. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  288. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  289. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  290. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  291. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  292. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  293. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  294. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  295. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  296. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  297. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  298. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  299. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  300. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  301. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  302. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  303. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  304. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  305. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  306. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  307. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  308. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  309. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  310. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  311. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  312. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  313. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  314. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  315. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  316. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  317. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  318. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  319. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  320. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  321. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  322. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  323. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  324. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  325. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  326. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  327. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  328. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  329. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  330. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  331. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  332. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  333. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  334. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  335. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  336. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  337. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  338. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  339. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  340. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  341. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  342. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  343. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  344. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  345. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  346. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  347. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  348. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  349. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  350. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  351. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  352. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  353. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  354. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  355. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  356. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  357. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  358. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  359. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  360. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  361. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  362. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  363. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  364. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  365. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  366. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  367. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  368. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  369. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  370. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  371. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  372. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  373. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  374. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  375. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  376. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  377. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  378. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  379. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  380. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  381. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  382. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  383. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  384. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  385. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  386. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  387. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  388. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  389. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  390. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  391. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  392. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  393. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  394. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  395. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  396. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  397. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  398. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  399. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  400. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  401. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  402. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  403. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  404. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  405. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  406. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  407. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  447. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  448. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  449. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  450. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  451. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  452. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  453. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  454. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  455. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  456. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  457. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  458. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  459. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  460. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  461. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  462. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  463. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  464. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  465. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  466. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  467. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  468. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  469. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  470. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  471. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  472. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  473. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  474. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  475. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  476. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  477. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  478. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  479. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  480. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  481. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  482. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  483. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  484. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  485. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  486. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  487. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  488. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  489. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  490. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  491. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  492. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  493. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  494. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  495. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  496. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  497. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  498. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  499. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  500. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  501. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  502. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  503. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  504. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  505. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  506. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  507. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  508. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  509. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  510. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  511. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  512. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  513. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  514. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  515. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  516. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  517. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  518. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  519. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  520. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  521. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  522. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  523. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  524. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  525. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  526. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  527. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  528. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  529. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  530. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  531. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  532. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  533. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  534. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  535. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  536. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  537. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  538. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  539. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  540. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  541. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  542. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  543. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  544. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  545. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  546. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  547. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  548. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  549. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  550. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  551. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  552. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  553. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  554. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  555. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  556. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  557. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  558. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  559. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  560. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  561. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  562. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  563. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  564. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  565. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  566. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  567. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  568. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  569. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  570. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  571. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  572. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  573. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  574. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  575. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  576. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  577. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  578. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  579. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  580. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  581. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  582. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  583. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  584. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  585. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  586. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  587. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  588. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  589. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  590. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  591. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  592. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  593. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  594. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  595. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  596. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  597. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  598. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  599. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  600. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  601. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  602. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  603. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  604. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  605. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  606. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  607. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  608. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  609. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  610. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  611. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  612. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  613. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  614. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  615. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  616. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  617. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  618. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  619. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  620. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  621. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  623. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  624. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  625. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  626. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  627. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  628. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  629. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  630. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  631. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  632. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  633. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  634. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  635. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  636. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  637. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  638. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  639. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  640. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  641. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  642. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  643. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  644. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  645. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  646. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  647. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  648. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  649. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  650. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  651. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  652. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  653. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  654. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  655. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  656. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  657. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  658. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  659. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  660. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  661. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  662. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  663. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  664. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  665. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  666. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  667. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  668. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  669. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  670. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  671. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  672. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  673. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  674. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  675. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  676. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  677. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  678. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  679. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  680. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  681. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  682. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  683. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  684. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  685. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  686. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  687. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  688. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  689. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  690. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  691. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  692. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  693. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  694. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  695. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  696. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  697. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  698. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  699. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  700. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  701. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  702. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  703. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  704. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  705. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  706. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  707. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  708. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  709. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  710. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  711. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  712. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  713. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  714. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  715. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  716. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  717. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  718. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  719. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  720. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  721. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  722. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  723. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  724. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  725. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  726. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  727. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  728. package/node_modules/pi-lens/dist/i18n.js +0 -69
  729. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  730. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  731. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  732. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  733. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  734. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  735. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  736. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  737. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  738. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  739. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  740. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  741. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  742. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  743. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  744. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  745. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  746. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  747. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  748. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
  749. package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
  750. package/node_modules/remote-pi/README.md +0 -675
  751. package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
  752. package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
  753. package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
  754. package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
  755. package/node_modules/remote-pi/dist/actions/registry.js +0 -58
  756. package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
  757. package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
  758. package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
  759. package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
  760. package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
  761. package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
  762. package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
  763. package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
  764. package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
  765. package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
  766. package/node_modules/remote-pi/dist/config.d.ts +0 -62
  767. package/node_modules/remote-pi/dist/config.js +0 -115
  768. package/node_modules/remote-pi/dist/config.js.map +0 -1
  769. package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
  770. package/node_modules/remote-pi/dist/daemon/client.js +0 -131
  771. package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
  772. package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
  773. package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
  774. package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
  775. package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
  776. package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
  777. package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
  778. package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
  779. package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
  780. package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
  781. package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
  782. package/node_modules/remote-pi/dist/daemon/id.js +0 -36
  783. package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
  784. package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
  785. package/node_modules/remote-pi/dist/daemon/install.js +0 -547
  786. package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
  787. package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
  788. package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
  789. package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
  790. package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
  791. package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
  792. package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
  793. package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
  794. package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
  795. package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
  796. package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
  797. package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
  798. package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
  799. package/node_modules/remote-pi/dist/index.d.ts +0 -213
  800. package/node_modules/remote-pi/dist/index.js +0 -4996
  801. package/node_modules/remote-pi/dist/index.js.map +0 -1
  802. package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
  803. package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
  804. package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
  805. package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
  806. package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
  807. package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
  808. package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
  809. package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
  810. package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
  811. package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
  812. package/node_modules/remote-pi/dist/mesh/client.js +0 -125
  813. package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
  814. package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
  815. package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
  816. package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
  817. package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
  818. package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
  819. package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
  820. package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
  821. package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
  822. package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
  823. package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
  824. package/node_modules/remote-pi/dist/mesh/types.js +0 -11
  825. package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
  826. package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
  827. package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
  828. package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
  829. package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
  830. package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
  831. package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
  832. package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
  833. package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
  834. package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
  835. package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
  836. package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
  837. package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
  838. package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
  839. package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
  840. package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
  841. package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
  842. package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
  843. package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
  844. package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
  845. package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
  846. package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
  847. package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
  848. package/node_modules/remote-pi/dist/protocol/types.js +0 -2
  849. package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
  850. package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
  851. package/node_modules/remote-pi/dist/rooms.js +0 -57
  852. package/node_modules/remote-pi/dist/rooms.js.map +0 -1
  853. package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
  854. package/node_modules/remote-pi/dist/session/address.js +0 -58
  855. package/node_modules/remote-pi/dist/session/address.js.map +0 -1
  856. package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
  857. package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
  858. package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
  859. package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
  860. package/node_modules/remote-pi/dist/session/bridge.js +0 -130
  861. package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
  862. package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
  863. package/node_modules/remote-pi/dist/session/broker.js +0 -507
  864. package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
  865. package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
  866. package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
  867. package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
  868. package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
  869. package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
  870. package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
  871. package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
  872. package/node_modules/remote-pi/dist/session/envelope.js +0 -117
  873. package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
  874. package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
  875. package/node_modules/remote-pi/dist/session/global_config.js +0 -73
  876. package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
  877. package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
  878. package/node_modules/remote-pi/dist/session/ipc.js +0 -22
  879. package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
  880. package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
  881. package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
  882. package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
  883. package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
  884. package/node_modules/remote-pi/dist/session/local_config.js +0 -157
  885. package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
  886. package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
  887. package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
  888. package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
  889. package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
  890. package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
  891. package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
  892. package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
  893. package/node_modules/remote-pi/dist/session/peer.js +0 -451
  894. package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
  895. package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
  896. package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
  897. package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
  898. package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
  899. package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
  900. package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
  901. package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
  902. package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
  903. package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
  904. package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
  905. package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
  906. package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
  907. package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
  908. package/node_modules/remote-pi/dist/session/tools.js +0 -252
  909. package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
  910. package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
  911. package/node_modules/remote-pi/dist/session/wizard.js +0 -20
  912. package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
  913. package/node_modules/remote-pi/dist/settings.d.ts +0 -15
  914. package/node_modules/remote-pi/dist/settings.js +0 -52
  915. package/node_modules/remote-pi/dist/settings.js.map +0 -1
  916. package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
  917. package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
  918. package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
  919. package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
  920. package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
  921. package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
  922. package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
  923. package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
  924. package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
  925. package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
  926. package/node_modules/remote-pi/dist/ui/footer.js +0 -34
  927. package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
  928. package/node_modules/remote-pi/docs/daemon.md +0 -289
  929. package/node_modules/remote-pi/package.json +0 -92
  930. package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
  931. package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
  932. package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
  933. package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
  934. package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
@@ -1,4195 +0,0 @@
1
- /**
2
- * Tree-sitter Structural Search Client for pi-lens
3
- *
4
- * Inspired by pi-lsp-extension's search-engine.ts and pattern-compiler.ts
5
- * Provides AST-aware structural search with metavariable capture.
6
- *
7
- * Uses web-tree-sitter (WASM) for parsing - no native compilation needed.
8
- *
9
- * Pattern syntax:
10
- * $NAME - Matches any single AST node, captures as NAME
11
- * $$$NAME - Matches zero or more sibling nodes (variadic)
12
- *
13
- * Example:
14
- * "console.log($MSG)" matches any console.log call, captures argument as MSG
15
- * "function $NAME($$$PARAMS) { $BODY }" matches function declarations
16
- */
17
- import { AsyncLocalStorage } from "node:async_hooks";
18
- import * as crypto from "node:crypto";
19
- import * as fs from "node:fs";
20
- import { createRequire } from "node:module";
21
- import * as path from "node:path";
22
- import { getDegradationLedgerGeneration, incrementDegradationCount, recordDegradation, recordDegradationOnce, } from "./degradation-ledger.js";
23
- import { BoundedFifoMap } from "./bounded-cache.js";
24
- import { loadWebTreeSitter } from "./deps/web-tree-sitter.js";
25
- import { transientRetryDelayMs } from "./dispatch/runners/utils/availability-policy.js";
26
- import { getProjectIgnoreMatcher, isExcludedDirName } from "./file-utils.js";
27
- import { downloadGrammarDetailed, fileHasWasmMagic, grammarBlockReason, grammarFileSha256, isVendoredGrammar, LANGUAGE_TO_GRAMMAR, pinnedGrammarHash, vendoredGrammarRefusal, vendoredGrammarsDir, } from "./grammar-source.js";
28
- import { getPackageRoot, resolvePackagePath } from "./package-root.js";
29
- import { resolveWebTreeSitterPackageDir } from "../scripts/lib/web-tree-sitter-dir.mjs";
30
- import { assertInstallAllowed, getProjectTrustGeneration, } from "./project-trust.js";
31
- import { escapeRegExp } from "./string-utils.js";
32
- import { logTreeSitterDiagnostic } from "./tree-sitter-logger.js";
33
- import { notifyUserDegradation } from "./user-notify.js";
34
- const _require = createRequire(import.meta.url);
35
- import { createTreeCacheCounters, deriveScanTreeCacheCapacity, TreeCache, } from "./tree-sitter-cache.js";
36
- import { TreeSitterNavigator } from "./tree-sitter-navigator.js";
37
- import { isProvenSqlAlchemySessionReceiver, isSafePsycopgIdentifierComposition, isSqlAlchemyEntityQueryArgument, isSqlAlchemyStatementArgument, PYTHON_SQLALCHEMY_RECEIVER_NAMES, PYTHON_SQLALCHEMY_STATEMENT_BUILDERS, } from "./python-provenance.js";
38
- import { TreeSitterQueryLoader, } from "./tree-sitter-query-loader.js";
39
- // Hard cap on a single structural-search file walk. Bounds a misrooted scan so
40
- // it can't enumerate an unbounded tree synchronously before result collection
41
- // short-circuits (#262).
42
- const TREE_SITTER_MAX_SCAN_FILES = 20_000;
43
- // Consecutive grammar-load failures after which a batch cache key gives up and
44
- // caches null like any other deterministic failure. Bounds the retry loop when
45
- // a grammar simply never loads, while still letting a transient loadLanguage()
46
- // failure (offline lazy fetch, mid-scan load error) recover on a later scan
47
- // instead of paying the 3.3x per-rule fallback for the process lifetime (#889).
48
- const QUERY_BATCH_MAX_LOAD_FAILURES = 3;
49
- // Keep post-filter tree walks bounded. A malformed or unexpectedly huge tree
50
- // must not stall the batched query walk; the filter fails open when this cap
51
- // is reached so unrelated diagnostics and the current match are preserved.
52
- const NO_NESTED_ANCHOR_VISIT_CAP = 10_000;
53
- // The execute-style Python DB-API/ORM methods this rule treats as SQL sinks.
54
- // Hoisted so the post-filter does not rebuild the set per match.
55
- const PYTHON_SQL_SINK_METHODS = new Set([
56
- "execute",
57
- "executemany",
58
- "query",
59
- "raw",
60
- ]);
61
- const TYPESCRIPT_SQL_KNOWN_PACKAGES = new Set([
62
- "pg",
63
- "mysql2",
64
- "better-sqlite3",
65
- "knex",
66
- "@prisma/client",
67
- ]);
68
- const TYPESCRIPT_SQL_IMPORTS = new WeakMap();
69
- const NOT_PARSED = { parsed: false };
70
- function createParserCounters() {
71
- return {
72
- parserInvocations: 0,
73
- parserDurationMs: 0,
74
- parserFailures: 0,
75
- };
76
- }
77
- /**
78
- * The errno that blocks writing into `dir`, or undefined when a write can be
79
- * attempted (#3409). The directory itself need not exist yet — the lazy fetch
80
- * creates it — so the nearest EXISTING ancestor is what gets probed.
81
- */
82
- function grammarWriteBlockedReason(dir) {
83
- let probe = dir;
84
- while (!fs.existsSync(probe)) {
85
- const parent = path.dirname(probe);
86
- if (parent === probe)
87
- return "ENOENT";
88
- probe = parent;
89
- }
90
- try {
91
- fs.accessSync(probe, fs.constants.W_OK);
92
- return undefined;
93
- }
94
- catch (err) {
95
- return err?.code ?? "EACCES";
96
- }
97
- }
98
- /**
99
- * `size:mtimeMs` identity for a grammar candidate, or undefined when the path
100
- * is absent or not a regular file. Doubles as the existence check on the
101
- * resolve path and as the invalidation key for the verified-preamble memo
102
- * (#1548) — `size` + `mtimeMs` is the codebase's standard cheap stamp.
103
- */
104
- function grammarFileStamp(filePath) {
105
- try {
106
- const stat = fs.statSync(filePath);
107
- return stat.isFile() ? `${stat.size}:${stat.mtimeMs}` : undefined;
108
- }
109
- catch {
110
- return undefined;
111
- }
112
- }
113
- function isTreeSitterWasmAbortError(error) {
114
- const message = error instanceof Error ? error.message : String(error);
115
- return message.includes("Aborted") || message.includes("abort()");
116
- }
117
- /**
118
- * Positively-identified RESOLUTION-failure codes for a dynamic `import()`:
119
- * the specifier couldn't be found/resolved, or a transient fs error hit
120
- * before `import()` got as far as evaluating the target module. All of
121
- * these recover once whatever was missing/busy appears — they say nothing
122
- * durable about the module itself (mirrors the errno allowlist in
123
- * clients/dispatch/runners/ast-grep-napi.ts's `classifyAstGrepLoadFailure`,
124
- * kept local here rather than shared because that file is owned by an
125
- * in-flight PR, #1701, at the time of writing).
126
- */
127
- const RESOLUTION_ERROR_CODES = new Set([
128
- "ERR_MODULE_NOT_FOUND",
129
- "MODULE_NOT_FOUND",
130
- "ERR_INVALID_MODULE_SPECIFIER",
131
- "ERR_UNSUPPORTED_DIR_IMPORT",
132
- "ENOENT",
133
- "EMFILE",
134
- "EBUSY",
135
- "EAGAIN",
136
- "EPERM",
137
- "ETXTBSY",
138
- ]);
139
- /** Walk an error's `.cause` chain, collecting every `code` seen along the
140
- * way — a resolution failure's `code` can be wrapped in a cause rather than
141
- * sitting on the top-level thrown Error. */
142
- function collectErrorCodes(err) {
143
- const codes = [];
144
- let current = err;
145
- const seen = new Set();
146
- while (current instanceof Error && !seen.has(current)) {
147
- seen.add(current);
148
- const code = current.code;
149
- if (typeof code === "string")
150
- codes.push(code);
151
- current = current.cause;
152
- }
153
- return codes;
154
- }
155
- /**
156
- * Classify a `loadWebTreeSitter()` rejection (#1592). RESOLUTION failures
157
- * (a positively-identified code above) are recoverable: the module gets a
158
- * fresh `import()` attempt on the very next `init()` call, same as before
159
- * this fix, because whatever blocked resolution can plausibly clear on its
160
- * own. Everything else is treated as EVALUATION-shaped and latches — an
161
- * unrecognized error is far more likely to be the target module's own
162
- * top-level code throwing (which Node's ESM loader then memoizes as a
163
- * permanently-rejected module record for that URL) than an unclassified
164
- * resolution hiccup, so "unknown" defaults to the case a retry cannot fix.
165
- */
166
- function classifyWebTreeSitterLoadFailure(err) {
167
- const codes = collectErrorCodes(err);
168
- return codes.some((code) => RESOLUTION_ERROR_CODES.has(code))
169
- ? "resolution"
170
- : "evaluation";
171
- }
172
- // --- Parser Manager ---
173
- export class TreeSitterClient {
174
- initialized = false;
175
- initPromise = null;
176
- /**
177
- * Set when `loadWebTreeSitter()` itself rejects with an EVALUATION-shaped
178
- * error (#1592, review round 2 F1/F2) — `classifyWebTreeSitterLoadFailure`
179
- * above draws that line. That call is a dynamic `import()` of a fixed
180
- * resolved URL, and Node's ESM loader permanently memoizes a module
181
- * record that threw during evaluation — a later `import()` of the SAME
182
- * URL from a later `init()` call would just replay the cached rejection,
183
- * not re-attempt the load. Without this latch, every `withTreeSitterRoot()`
184
- * call (one per file parse) would re-invoke `init()`, see `initPromise`
185
- * cleared by the previous attempt's `finally`, and dynamically re-import —
186
- * a dead retry on the hot path. A RESOLUTION-shaped rejection does NOT
187
- * set this: it leaves `initPromise` cleared as before, so the next
188
- * `init()` call retries for real, because that class of failure can
189
- * plausibly clear before the next call arrives.
190
- *
191
- * SESSION-scoped, not process-lifetime (round 2 F2, the #1567/#1575
192
- * `sgSessionHold` precedent): re-armed by `resetLoadStateForSession()`,
193
- * wired into `resetDispatchBaselines()` via `tree-sitter-shared.ts`'s
194
- * `resetTreeSitterClientLoadState()`. Re-arming does not guarantee the
195
- * next attempt succeeds — if the process itself didn't restart between
196
- * sessions, Node's module cache is unchanged and the replay will just
197
- * fail fast again — but it gives a fresh degradation record for the new
198
- * session (the ledger's own `onceKeys` are cleared at session_start too)
199
- * instead of silently reusing a stale verdict forever, and it does let a
200
- * genuinely fixed install (process WAS restarted) recover.
201
- *
202
- * Distinct from `wasmAborted`, which stays process-lifetime and is
203
- * deliberately NOT included in the session reset: that flag means the
204
- * Emscripten WASM heap itself aborted mid-use — a runtime that DID load
205
- * and then corrupted its own memory. Reusing that heap after a "session"
206
- * boundary that isn't an actual process restart is not a retry, it's
207
- * operating on data already documented (tree-sitter-shared.ts) as
208
- * requiring a real restart to recover. `webTreeSitterLoadFailed` only
209
- * ever covers a runtime that never loaded in the first place, so nothing
210
- * corrupted survives a re-arm.
211
- */
212
- webTreeSitterLoadFailed = false;
213
- languages = new Map();
214
- parsers = new Map();
215
- treeCache;
216
- navigator = new TreeSitterNavigator();
217
- grammarsDir;
218
- /** In-flight lazy grammar fetches, keyed by wasm filename. Evicted on
219
- * settle (#1536) — a rejected/false attempt must not be remembered as the
220
- * permanent answer for the session; only concurrent demands during the
221
- * SAME probe share it. */
222
- grammarEnsurePromises = new Map();
223
- /** Epoch ms before which a failed grammar download is not retried
224
- * (#1536). A transient download failure — offline, DNS hiccup, CDN blip —
225
- * says nothing durable about the grammar, so it gets a bounded cooldown
226
- * (the `transientRetryDelayMs` shape from availability-policy.ts) instead
227
- * of latching for the process lifetime. */
228
- grammarRetryAtMs = new Map();
229
- /**
230
- * Grammar path → the `size:mtimeMs` stamp at which its wasm preamble was
231
- * verified (#1548). A POSITIVE-only memo, so the steady-state resolve costs
232
- * one `stat` (which the old `existsSync` already paid) and re-reads the
233
- * header only when the stamp moves. Stamped rather than a bare `Set`
234
- * because a memo that never expires is defect-shape 6: the file it vouches
235
- * for can be replaced under it. Failures are deliberately NOT memoized — a
236
- * poisoned path must be re-examined on the next resolve so a successful
237
- * re-download over it is picked up immediately.
238
- */
239
- verifiedGrammarPaths = new Map();
240
- /**
241
- * Grammar path → the `size:mtimeMs` stamp at which `Language.load` failed
242
- * on it despite passing the wasm-preamble check (#1564) — e.g. a truncated
243
- * download whose first four bytes are a genuine `\0asm` preamble but whose
244
- * body decodes short ("Code section extends past end of the module"). The
245
- * preamble check alone can't see this; only a real decode attempt can.
246
- * `resolveGrammarFile` treats a path recorded here (at the SAME stamp) as
247
- * absent, so the next demand re-fetches instead of reusing the same broken
248
- * file forever. Stamped so a fresh download (new mtime) is re-examined
249
- * rather than permanently distrusted — same shape as `verifiedGrammarPaths`.
250
- */
251
- decodeFailedGrammarPaths = new Map();
252
- /**
253
- * Grammar path → the `size:mtimeMs` stamp at which its bytes were verified
254
- * to match the CURRENTLY PINNED sha256 manifest (#1760). Positive-only,
255
- * same discipline as `verifiedGrammarPaths`: the hash is computed once per
256
- * stamp (not per parse), and a stamp change — a fresh download — forces a
257
- * re-check rather than trusting a memo made for different bytes.
258
- */
259
- verifiedGrammarVersionAt = new Map();
260
- /**
261
- * Grammar path → the `size:mtimeMs` stamp at which it was found to no
262
- * longer match the pinned manifest hash — a version bump
263
- * (`TREE_SITTER_WASMS_VERSION`, or a `SOURCE_OVERRIDES` entry), or on-disk
264
- * corruption (#1760). `resolveGrammarFile` treats a path recorded here (at
265
- * the SAME stamp) as absent, exactly like `decodeFailedGrammarPaths`, so
266
- * the next demand re-fetches instead of serving the stale/corrupt file for
267
- * the rest of the process's life. A fresh download (new stamp) is
268
- * re-examined rather than permanently distrusted.
269
- */
270
- staleGrammarVersionAt = new Map();
271
- /**
272
- * Paths already reported as version-stale THIS SESSION, to log/record them
273
- * once each (#1801 review F1). Deliberately separate from
274
- * `staleGrammarVersionAt`: that map is a pure hash-verdict memo whose whole
275
- * point is to persist for the process's life (re-hashing an unchanged file
276
- * is exactly what it exists to avoid), so it must never double as a report
277
- * gate — a gate riding on process-lifetime state can't re-arm at a session
278
- * boundary. Cleared in `refreshGrammarSessionLatches`, mirroring
279
- * `poisonedGrammarPaths`.
280
- */
281
- staleReportedGrammarPaths = new Set();
282
- /** Paths already reported as non-wasm, to log/record them once each. */
283
- poisonedGrammarPaths = new Set();
284
- /** Consecutive download failures per grammar, for the exponential
285
- * cooldown; reset on success. */
286
- grammarFailureAttempts = new Map();
287
- /**
288
- * Retry delay (ms) last SHOWN to the user for this grammar, or `-1` for a
289
- * durable (non-retryable) failure already announced. The cooldown lets
290
- * the ensure loop retry silently in the background — only the delay's
291
- * FIRST appearance, and any later escalation the user was never told
292
- * about, needs to interrupt them (#1536 review F6): a fresh 30s cooldown
293
- * re-notifies the same as an escalated 300s one once it's a genuinely new
294
- * number, but two consecutive 30s cooldowns (identical, nothing new to
295
- * say) do not. Absence means "not yet notified this streak."
296
- *
297
- * Session-scoped (#1536 review F5): cleared whenever the degradation
298
- * ledger's own generation moves past `grammarNotificationsLedgerGen`,
299
- * mirroring `trustBlockedGrammarNotifications` below — a lazy
300
- * compare-at-use-time against a monotonic counter, not a listener. Tied to
301
- * the LEDGER's generation (bumped by `resetDegradationLedger`, which
302
- * `handleSessionStart` calls first thing) rather than trust, since this
303
- * is a session boundary, not a trust transition.
304
- */
305
- grammarLastNotifiedDelayMs = new Map();
306
- grammarNotificationsLedgerGen = getDegradationLedgerGeneration();
307
- trustBlockedGrammarNotifications = new Set();
308
- trustNotificationsGeneration = getProjectTrustGeneration();
309
- // biome-ignore lint/suspicious/noExplicitAny: Optional dependency loaded dynamically
310
- ParserClass = null;
311
- // biome-ignore lint/suspicious/noExplicitAny: Language loader from module
312
- LanguageLoader = null;
313
- // Declared BEFORE the two caches below: a static read by an instance field
314
- // initializer must already be initialized (TS2729).
315
- static QUERY_CACHE_MAX_ENTRIES = 256;
316
- static QUERY_BATCH_CACHE_MAX_ENTRIES = 256;
317
- // biome-ignore lint/suspicious/noExplicitAny: Compiled query cache by language+pattern hash
318
- // BoundedFifoMap, not BoundedLruCache: recency here is refreshed by this
319
- // class's own explicit delete+set on both the read and the write path (see
320
- // cacheQuery and the two lookup sites) — exactly the raw-`Map` discipline
321
- // the FIFO map documents. A get() that promoted on its own would change
322
- // which entry eviction targets (#2442 review F5/F7).
323
- queryCache = new BoundedFifoMap(TreeSitterClient.QUERY_CACHE_MAX_ENTRIES);
324
- /** Combined multi-rule queries by language + rule-set identity (null = don't retry). */
325
- queryBatchCache = new BoundedFifoMap(TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES);
326
- queryCacheCap() {
327
- const value = Number.parseInt(process.env.PI_LENS_TREE_SITTER_QUERY_CACHE_CAP ?? "", 10);
328
- return Number.isSafeInteger(value) && value > 0
329
- ? value
330
- : TreeSitterClient.QUERY_CACHE_MAX_ENTRIES;
331
- }
332
- queryBatchCacheCap() {
333
- const value = Number.parseInt(process.env.PI_LENS_TREE_SITTER_QUERY_BATCH_CACHE_CAP ?? "", 10);
334
- return Number.isSafeInteger(value) && value > 0
335
- ? value
336
- : TreeSitterClient.QUERY_BATCH_CACHE_MAX_ENTRIES;
337
- }
338
- // biome-ignore lint/suspicious/noExplicitAny: compiled query objects
339
- cacheQuery(key, value) {
340
- this.queryCache.delete(key); // write-refresh: this write is the newest
341
- // The cap is env-readable, so re-apply it before every write — a raised
342
- // or lowered ceiling has to take effect on the next insert, not on the
343
- // next restart. Both calls hand back the dropped [key, value] pairs so
344
- // the evicted compiled query's native handle is still freed (#2442
345
- // review F5/F7 — this is why set() returns the VALUE, not just the key).
346
- const evicted = this.queryCache.setMaxEntries(this.queryCacheCap());
347
- evicted.push(...this.queryCache.set(key, value));
348
- for (const [, dropped] of evicted)
349
- dropped?.query?.delete?.();
350
- }
351
- cacheQueryBatch(key, value) {
352
- this.queryBatchCache.delete(key);
353
- const evicted = this.queryBatchCache.setMaxEntries(this.queryBatchCacheCap());
354
- evicted.push(...this.queryBatchCache.set(key, value));
355
- for (const [, dropped] of evicted)
356
- dropped?.query?.delete?.();
357
- }
358
- /** Consecutive grammar-load failures per batch key — bounds load retries (#889). */
359
- queryBatchLoadFailures = new Map();
360
- queryLoader = new TreeSitterQueryLoader();
361
- verbose;
362
- parserCounters = createParserCounters();
363
- parseCacheMeasurement = new AsyncLocalStorage();
364
- activeMeasurements = 0;
365
- onWasmAbort;
366
- wasmAborted = false;
367
- grammarDirResolutionDeps;
368
- constructor(verbose = false, onWasmAbort, grammarDirResolutionDeps) {
369
- this.grammarDirResolutionDeps = grammarDirResolutionDeps ?? {
370
- resolveAsset: (asset) => this.resolveWebTreeSitterAsset(asset),
371
- resolvePackage: (specifier) => _require.resolve(specifier),
372
- packageRoot: () => getPackageRoot(import.meta.url),
373
- cwd: () => process.cwd(),
374
- };
375
- this.grammarsDir = this.findGrammarsDir();
376
- this.verbose = verbose;
377
- this.onWasmAbort = onWasmAbort;
378
- this.treeCache = new TreeCache(50, verbose, 4096, (key, amount) => {
379
- const measurement = this.parseCacheMeasurement.getStore();
380
- if (measurement)
381
- measurement[key] += amount;
382
- }, (error) => this.reportWasmAbort(error));
383
- }
384
- recordParserCounter(key, amount = 1) {
385
- this.parserCounters[key] += amount;
386
- const measurement = this.parseCacheMeasurement.getStore();
387
- if (measurement)
388
- measurement[key] += amount;
389
- }
390
- reportWasmAbort(error) {
391
- if (!isTreeSitterWasmAbortError(error))
392
- return false;
393
- if (!this.wasmAborted) {
394
- this.wasmAborted = true;
395
- recordDegradation({
396
- kind: "wasm-abort",
397
- subject: "web-tree-sitter",
398
- reason: error instanceof Error ? error.message : String(error),
399
- });
400
- this.onWasmAbort?.();
401
- }
402
- return true;
403
- }
404
- /**
405
- * O(1) runtime footprint counters (#1123 item 2 memory attribution): every
406
- * field is a `Map.size` read, never an iteration over the maps' contents.
407
- * `wasmMemoryBytes` is deliberately NOT included here — web-tree-sitter
408
- * 0.25.10's Emscripten `Module` (which owns the WASM linear memory /
409
- * `HEAPU8.buffer`) is a private closure variable in the package's
410
- * `bindings.ts` and is not exposed through any public export (`Parser`,
411
- * `Language`, `Query`, ...); reaching it would require either reflecting
412
- * into the package's internal module state (brittle across web-tree-sitter
413
- * versions/bundling) or overriding Emscripten's `wasmMemory` module option
414
- * at `Parser.init()` time with a hand-constructed `WebAssembly.Memory`
415
- * matching the library's own default page-count math (risks a memory-import
416
- * mismatch that would break ALL structural analysis, for an
417
- * observability-only feature). `process.memoryUsage().arrayBuffers` is the
418
- * process-wide proxy used instead (WASM linear memory backs an ArrayBuffer,
419
- * so it is included there) — see clients/memory-sampler.ts.
420
- */
421
- getRuntimeStats() {
422
- return {
423
- languagesLoaded: this.languages.size,
424
- parsersLoaded: this.parsers.size,
425
- queryCacheSize: this.queryCache.size,
426
- queryBatchCacheSize: this.queryBatchCache.size,
427
- };
428
- }
429
- getParseCacheStats() {
430
- return {
431
- ...this.treeCache.getStats(),
432
- ...this.parserCounters,
433
- };
434
- }
435
- /**
436
- * Grow the tree cache to span a full-project scan's working set (#1715).
437
- * The interactive default (50 entries) can't hold a mid/large project's
438
- * file count, so a second scan re-parses everything the first scan's LRU
439
- * evicted — live dogfood evidence showed every miss on a second 110-file
440
- * scan was a `capacityMisses` one. Bounded by
441
- * `TREE_CACHE_SCAN_CAPACITY_CEILING` (see `deriveScanTreeCacheCapacity`'s
442
- * heap-cost note) and monotonic — a smaller scan never shrinks a capacity
443
- * an earlier, larger one already grew.
444
- */
445
- ensureTreeCacheCapacity(fileCount) {
446
- this.treeCache.setMaxSize(deriveScanTreeCacheCapacity(fileCount, this.treeCache.getMaxSize()));
447
- }
448
- async withParseCacheMeasurement(work, onComplete) {
449
- const measurement = {
450
- ...createTreeCacheCounters(),
451
- ...createParserCounters(),
452
- };
453
- this.activeMeasurements++;
454
- try {
455
- return await this.parseCacheMeasurement.run(measurement, async () => {
456
- try {
457
- return await work();
458
- }
459
- finally {
460
- const cacheStats = this.treeCache.getStats();
461
- try {
462
- onComplete({
463
- ...cacheStats,
464
- ...measurement,
465
- misses: measurement.lookups - measurement.hits,
466
- });
467
- }
468
- catch {
469
- // Telemetry must not fail the measured work.
470
- }
471
- }
472
- });
473
- }
474
- finally {
475
- // On Node 22 an active AsyncLocalStorage keeps an async_hooks init hook
476
- // enabled for every promise in the process, so release it once the last
477
- // overlapping measurement is done rather than taxing the whole session.
478
- if (--this.activeMeasurements === 0)
479
- this.parseCacheMeasurement.disable();
480
- }
481
- }
482
- /** Debug logging helper */
483
- dbg(msg) {
484
- if (this.verbose) {
485
- // #1333: verbose gating is preserved, but the SINK is tree-sitter.log —
486
- // a raw write would corrupt pi's frame the moment verbose is enabled.
487
- logTreeSitterDiagnostic({
488
- subsystem: "tree-sitter-client",
489
- level: "debug",
490
- message: msg,
491
- });
492
- }
493
- }
494
- /**
495
- * web-tree-sitter's installed package directory, through the one shared
496
- * ladder in `scripts/lib/web-tree-sitter-dir.mjs` (#3409). Routed through
497
- * `grammarDirResolutionDeps` so the resolver context — the thing that
498
- * differs on a compiled host — stays the injected seam (#2112/#2136).
499
- */
500
- webTreeSitterPackageDir() {
501
- return resolveWebTreeSitterPackageDir({
502
- resolve: (specifier) => this.grammarDirResolutionDeps.resolvePackage(specifier),
503
- packageRoot: () => this.grammarDirResolutionDeps.packageRoot(),
504
- cwd: () => this.grammarDirResolutionDeps.cwd(),
505
- });
506
- }
507
- /**
508
- * Resolve a web-tree-sitter asset path:
509
- * 1. the asset's own subpath, authoritative when the package `exports` it
510
- * (`tree-sitter.wasm` is exported; `grammars` is NOT in 0.25.10, so this
511
- * rung is dead for the grammars dir — #3409's second symptom);
512
- * 2. the asset inside the package directory the shared ladder resolves. That
513
- * ladder's own last two rungs are the package-root walk (#20, for pi's
514
- * temp-dir compile) and the cwd fallback this method used to spell out.
515
- */
516
- resolveWebTreeSitterAsset(asset) {
517
- try {
518
- const resolved = this.grammarDirResolutionDeps.resolvePackage(`web-tree-sitter/${asset}`);
519
- if (fs.existsSync(resolved))
520
- return resolved;
521
- }
522
- catch {
523
- /* fall through */
524
- }
525
- const pkgDir = this.webTreeSitterPackageDir();
526
- if (pkgDir) {
527
- const candidate = path.join(pkgDir, asset);
528
- if (fs.existsSync(candidate))
529
- return candidate;
530
- }
531
- return undefined;
532
- }
533
- /**
534
- * The `grammars/` dir bundled inside the pi-lens package (the core grammars
535
- * shipped in the tarball, so common languages parse offline on every package
536
- * manager). Resolved from the package root; cached. Absent in a source
537
- * checkout where `prepare` hasn't populated it.
538
- */
539
- _bundledGrammarsDir;
540
- bundledGrammarsDir() {
541
- // Cache only a positive hit; keep re-checking until it exists (prepare may
542
- // not have populated it yet at first probe).
543
- if (this._bundledGrammarsDir)
544
- return this._bundledGrammarsDir;
545
- try {
546
- const dir = resolvePackagePath(import.meta.url, "grammars");
547
- if (fs.existsSync(dir))
548
- this._bundledGrammarsDir = dir;
549
- return this._bundledGrammarsDir;
550
- }
551
- catch {
552
- return undefined;
553
- }
554
- }
555
- _vendoredGrammarsDir;
556
- /**
557
- * The committed `vendor/grammars` dir, if it exists. Cached only on a hit,
558
- * mirroring `bundledGrammarsDir`.
559
- */
560
- vendoredGrammarsDir() {
561
- if (this._vendoredGrammarsDir)
562
- return this._vendoredGrammarsDir;
563
- try {
564
- const dir = vendoredGrammarsDir();
565
- if (fs.existsSync(dir))
566
- this._vendoredGrammarsDir = dir;
567
- return this._vendoredGrammarsDir;
568
- }
569
- catch {
570
- return undefined;
571
- }
572
- }
573
- /**
574
- * All directories that may hold grammar wasms, in precedence order: the
575
- * committed vendor dir, the bundled core dir, the resolved
576
- * `this.grammarsDir`, and the web-tree-sitter grammars dir (the lazy-fetch
577
- * write target). Deduped.
578
- *
579
- * `vendor/grammars` comes first because it is the ONLY source for a grammar
580
- * we build ourselves (`VENDORED_GRAMMARS`) — nothing downloads into the
581
- * later dirs for it, so a miss here is a miss everywhere.
582
- */
583
- grammarSourceDirs() {
584
- const dirs = [];
585
- const push = (d) => {
586
- if (d && !dirs.includes(d))
587
- dirs.push(d);
588
- };
589
- push(this.vendoredGrammarsDir());
590
- push(this.bundledGrammarsDir());
591
- push(this.grammarsDir || undefined);
592
- push(this.resolveWebTreeSitterAsset("grammars"));
593
- return dirs;
594
- }
595
- /**
596
- * Absolute path to `grammarFile` across all source dirs, else undefined.
597
- *
598
- * Existence is not enough (#1548): a file whose first four bytes aren't the
599
- * wasm preamble is not a grammar, and returning it would report the language
600
- * as available while every `Language.load` fails — the permanent-poisoning
601
- * shape this issue is about. #1548 stops NEW poisoned files from being
602
- * written, but a file poisoned before that shipped is already on disk, so
603
- * the resolve path has to reject it too: a rejected candidate is treated as
604
- * absent, which lets `ensureGrammar` re-download over it.
605
- */
606
- resolveGrammarFile(grammarFile) {
607
- for (const dir of this.grammarSourceDirs()) {
608
- const candidate = path.join(dir, grammarFile);
609
- const stamp = grammarFileStamp(candidate);
610
- if (!stamp)
611
- continue;
612
- // #1564: a file that passed the preamble check but failed
613
- // `Language.load` at this exact stamp is known-bad — treat it as
614
- // absent so `ensureGrammar` re-fetches over it, same as the
615
- // non-wasm-magic case below. A stamp mismatch (the file changed —
616
- // re-downloaded) falls through to re-examine it fresh.
617
- if (this.decodeFailedGrammarPaths.get(candidate) === stamp)
618
- continue;
619
- if (this.verifiedGrammarPaths.get(candidate) === stamp) {
620
- if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
621
- return candidate;
622
- }
623
- continue;
624
- }
625
- if (fileHasWasmMagic(candidate)) {
626
- this.verifiedGrammarPaths.set(candidate, stamp);
627
- if (this.isGrammarVersionCurrent(candidate, grammarFile, stamp)) {
628
- return candidate;
629
- }
630
- continue;
631
- }
632
- this.verifiedGrammarPaths.delete(candidate);
633
- this.reportPoisonedGrammarFile(candidate, grammarFile);
634
- }
635
- return undefined;
636
- }
637
- /**
638
- * Clear the once-per-session grammar report gates when the degradation
639
- * ledger's own generation has moved (#1536 review F5). Lazy
640
- * compare-at-use-time against a monotonic counter, bumped by
641
- * `resetDegradationLedger`, which `handleSessionStart` calls first thing —
642
- * no listener, no retention. Every gate keyed to a SESSION rather than to
643
- * the process lives here, so a new one cannot forget to re-arm: a gate that
644
- * outlives the ledger it guards silently swallows the record it was only
645
- * ever meant to de-duplicate (#1560 review F1).
646
- */
647
- refreshGrammarSessionLatches() {
648
- const ledgerGen = getDegradationLedgerGeneration();
649
- if (ledgerGen === this.grammarNotificationsLedgerGen)
650
- return;
651
- this.grammarNotificationsLedgerGen = ledgerGen;
652
- this.grammarLastNotifiedDelayMs.clear();
653
- this.poisonedGrammarPaths.clear();
654
- this.staleReportedGrammarPaths.clear();
655
- }
656
- /**
657
- * Log + record a grammar file on disk that isn't a wasm module, once per
658
- * path per session. No user notification here: the caller goes on to attempt
659
- * a re-download, and `recordGrammarFailure` is what interrupts the user if
660
- * that also fails. A silent recovery should stay silent.
661
- */
662
- reportPoisonedGrammarFile(candidate, grammarFile) {
663
- // Per SESSION, not per process (#1560 review F1). The file is still
664
- // poisoned after a session boundary and still degrades the language, so
665
- // the new session's ledger has to carry the entry — a client-lifetime
666
- // gate would leave `resetDegradationLedger` with a permanently empty
667
- // grammar-blocked group and no record of why the language is degraded.
668
- this.refreshGrammarSessionLatches();
669
- if (this.poisonedGrammarPaths.has(candidate))
670
- return;
671
- this.poisonedGrammarPaths.add(candidate);
672
- logTreeSitterDiagnostic({
673
- subsystem: "tree-sitter-client",
674
- level: "warn",
675
- message: `ignoring ${candidate}: the file is not a wasm module (missing the \\0asm preamble) — ` +
676
- `most likely a captive-portal or proxy page written by an earlier download (#1548). ` +
677
- `pi-lens will treat the grammar as missing and try to fetch it again.`,
678
- metadata: { grammarFile, path: candidate, outcome: "not-wasm" },
679
- });
680
- incrementDegradationCount({
681
- kind: "grammar-blocked",
682
- subject: grammarFile,
683
- reason: "on-disk grammar file is not a wasm module — ignored, re-fetching",
684
- });
685
- }
686
- /**
687
- * Is the cached grammar at `candidate` (whose wasm preamble already passed)
688
- * still current against the pinned sha256 manifest (#1760)?
689
- *
690
- * A grammar downloaded once is never revisited when this repo bumps
691
- * `TREE_SITTER_WASMS_VERSION` or changes a `SOURCE_OVERRIDES` entry — the
692
- * cached file's name carries no version, so a stale build serves forever.
693
- * The check is cheap and NEVER touches the network: it compares the
694
- * on-disk sha256 (hashed once per `size:mtimeMs` stamp, memoized exactly
695
- * like `verifiedGrammarPaths` above so a hot parse loop never re-hashes an
696
- * unchanged file) against `pinnedGrammarHash`, which itself is a pure
697
- * in-memory manifest lookup. A mismatch also catches on-disk corruption,
698
- * which nothing detected before this.
699
- *
700
- * Vendored grammars (`VENDORED_GRAMMARS`) are skipped: they have no CDN
701
- * pin to drift against, and their bytes are already guarded by the
702
- * separate build-provenance check (`scripts/check-grammar-provenance.mjs`).
703
- * No pinned hash for this filename (manifest missing, or a grammar added
704
- * before `--write-manifest` was re-run) is treated as "can't verify" and
705
- * trusted, the same fallback `downloadGrammarDetailed`'s own hash check
706
- * already uses — never as a forced, unbounded refetch loop.
707
- */
708
- isGrammarVersionCurrent(candidate, grammarFile, stamp) {
709
- if (isVendoredGrammar(grammarFile))
710
- return true;
711
- if (this.staleGrammarVersionAt.get(candidate) === stamp) {
712
- // #1801 review F1: the hash-verdict memo above is process-lifetime by
713
- // design (re-hashing an unchanged file on every resolve is the exact
714
- // cost this whole check exists to avoid), so an early return here must
715
- // NOT skip the report — a still-stale grammar has to keep showing up
716
- // in every NEW session's ledger, not just the session that first
717
- // discovered the mismatch.
718
- this.reportStaleGrammarVersion(candidate, grammarFile);
719
- return false;
720
- }
721
- if (this.verifiedGrammarVersionAt.get(candidate) === stamp)
722
- return true;
723
- const pinnedHash = pinnedGrammarHash(grammarFile);
724
- if (!pinnedHash) {
725
- this.verifiedGrammarVersionAt.set(candidate, stamp);
726
- return true;
727
- }
728
- const actualHash = grammarFileSha256(candidate);
729
- if (actualHash === pinnedHash) {
730
- this.verifiedGrammarVersionAt.set(candidate, stamp);
731
- return true;
732
- }
733
- this.verifiedGrammarVersionAt.delete(candidate);
734
- this.staleGrammarVersionAt.set(candidate, stamp);
735
- this.reportStaleGrammarVersion(candidate, grammarFile);
736
- return false;
737
- }
738
- /**
739
- * Log + record a version-stale (or corrupt) cached grammar, once per path
740
- * per SESSION (#1801 review F1). Called from BOTH the memoized-stale path
741
- * and the fresh-mismatch path in `isGrammarVersionCurrent`, so the report
742
- * gate is independent of `staleGrammarVersionAt`'s own process-lifetime
743
- * memo — a grammar that is STILL stale in a later session must still emit
744
- * a fresh record into that session's ledger, exactly like
745
- * `reportPoisonedGrammarFile` already does for the non-wasm case.
746
- */
747
- reportStaleGrammarVersion(candidate, grammarFile) {
748
- this.refreshGrammarSessionLatches();
749
- if (this.staleReportedGrammarPaths.has(candidate))
750
- return;
751
- this.staleReportedGrammarPaths.add(candidate);
752
- logTreeSitterDiagnostic({
753
- subsystem: "tree-sitter-client",
754
- level: "warn",
755
- message: `ignoring ${candidate}: its sha256 no longer matches the pinned grammar ` +
756
- `manifest — the pinned tree-sitter-wasms version (or a SOURCE_OVERRIDES entry) ` +
757
- `moved since this file was downloaded. pi-lens will treat the grammar as ` +
758
- `missing and re-fetch the current build (#1760).`,
759
- metadata: { grammarFile, path: candidate, outcome: "stale-version" },
760
- });
761
- incrementDegradationCount({
762
- kind: "grammar-blocked",
763
- subject: grammarFile,
764
- reason: "cached grammar no longer matches the pinned manifest hash — ignored, re-fetching",
765
- });
766
- }
767
- /** Find tree-sitter grammar directory */
768
- findGrammarsDir(deps = this.grammarDirResolutionDeps) {
769
- const grammarsDir = deps.resolveAsset("grammars");
770
- if (grammarsDir &&
771
- fs.existsSync(path.join(grammarsDir, "tree-sitter-typescript.wasm"))) {
772
- return grammarsDir;
773
- }
774
- // Fallback: a real `tree-sitter-wasms` package, if the user installed one
775
- // (it is not a pi-lens dependency — grammars ship bundled / lazy-fetched).
776
- try {
777
- const wasmsOut = path.join(path.dirname(deps.resolvePackage("tree-sitter-wasms/package.json")), "out");
778
- if (fs.existsSync(wasmsOut))
779
- return wasmsOut;
780
- }
781
- catch {
782
- /* fall through */
783
- }
784
- const cwdWasms = path.join(deps.cwd(), "node_modules", "tree-sitter-wasms", "out");
785
- if (fs.existsSync(cwdWasms))
786
- return cwdWasms;
787
- return "";
788
- }
789
- /**
790
- * The directory where grammars SHOULD live (web-tree-sitter/grammars),
791
- * whether or not it exists yet — so we can create + populate it when the
792
- * postinstall download was skipped (pnpm/bun). Returns undefined if
793
- * web-tree-sitter itself can't be located.
794
- *
795
- * #3409: this used to resolve the BARE `web-tree-sitter` specifier and
796
- * nothing else, which throws MODULE_NOT_FOUND inside the `bun
797
- * build --compile` binary pi ships — so on every such host it returned
798
- * undefined and NO non-core grammar could ever be fetched. It now shares the
799
- * read path's ladder, whose first rung is a subpath that does resolve there.
800
- */
801
- grammarsWriteDir() {
802
- const pkgDir = this.webTreeSitterPackageDir();
803
- return pkgDir ? path.join(pkgDir, "grammars") : undefined;
804
- }
805
- /**
806
- * Ensure a single grammar wasm is on disk, fetching it at runtime if the
807
- * postinstall didn't (pnpm/bun skip lifecycle scripts — the documented
808
- * build-scripts gap). Idempotent and de-duplicated per file. Best-effort:
809
- * a failed fetch (e.g. offline) degrades to "grammar unavailable", never
810
- * throws.
811
- */
812
- async ensureGrammar(grammarFile) {
813
- if (this.resolveGrammarFile(grammarFile)) {
814
- return true;
815
- }
816
- if (!assertInstallAllowed(`tree-sitter grammar fetch: ${grammarFile}`)) {
817
- const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable because the project is not trusted; ` +
818
- `runtime grammar downloads are disabled until trust is granted.`;
819
- logTreeSitterDiagnostic({
820
- subsystem: "tree-sitter-client",
821
- message: unavailable,
822
- metadata: { grammarFile, outcome: "trust-gated" },
823
- });
824
- recordDegradation({
825
- kind: "grammar-blocked",
826
- subject: grammarFile,
827
- reason: "runtime grammar download blocked because project is untrusted",
828
- });
829
- // Lazy clear-on-transition (#1363 review): compare the trust
830
- // generation at use time -- no listener registration, no retention.
831
- const generation = getProjectTrustGeneration();
832
- if (generation !== this.trustNotificationsGeneration) {
833
- this.trustNotificationsGeneration = generation;
834
- this.trustBlockedGrammarNotifications.clear();
835
- }
836
- if (!this.trustBlockedGrammarNotifications.has(grammarFile)) {
837
- this.trustBlockedGrammarNotifications.add(grammarFile);
838
- notifyUserDegradation(`pi-lens: ${unavailable}`);
839
- }
840
- return false;
841
- }
842
- const inflight = this.grammarEnsurePromises.get(grammarFile);
843
- if (inflight)
844
- return inflight;
845
- // A failed download only stays refused for its cooldown window (#1536):
846
- // past that, fall through and retry rather than remembering an offline
847
- // moment for the life of the process. No download is attempted while the
848
- // cooldown is live, so a hard-down CDN is not re-hit on every parse. A
849
- // DURABLE failure (grammarBlockDurable below) sets this to +Infinity,
850
- // which reads the same as "still cooling down" here — it just never
851
- // expires, matching the issue's "a genuinely unavailable grammar may
852
- // still latch" allowance.
853
- const retryAt = this.grammarRetryAtMs.get(grammarFile);
854
- if (retryAt !== undefined && Date.now() < retryAt) {
855
- return false;
856
- }
857
- const task = (async () => {
858
- try {
859
- return await this.fetchGrammar(grammarFile);
860
- }
861
- catch (err) {
862
- // Defensive (#1548): nothing in `fetchGrammar` throws today —
863
- // `downloadGrammarDetailed` catches internally. But a thrown error
864
- // here used to skip the whole `else` arm, so NO cooldown was armed
865
- // and the rejection escaped through `task.finally` — the exact
866
- // pre-#1536 shape (no cooldown, possible unhandled rejection). Funnel
867
- // it into the same retryable failure path instead, so a future
868
- // refactor that lets an exception out cannot regress to it.
869
- const message = err instanceof Error ? err.message : String(err);
870
- this.recordGrammarFailure(grammarFile, `The runtime grammar fetch threw an unexpected error (${message}).`,
871
- /* retryable */ true, "download-failed");
872
- return false;
873
- }
874
- })();
875
- this.grammarEnsurePromises.set(grammarFile, task);
876
- // Evict on settle regardless of outcome (#1536): a resolved TRUE is
877
- // superseded by resolveGrammarFile() finding the file on disk on the
878
- // next call, and a resolved FALSE must not be remembered as the
879
- // permanent verdict — only concurrent callers during the SAME in-flight
880
- // attempt should ever observe this promise.
881
- //
882
- // The trailing `.catch` is UNFALSIFIABLE scaffolding, not verified
883
- // defence (#1560 review F3): `finally` returns a DERIVED promise that
884
- // rejects whenever `task` does, but the task's own try/catch above means
885
- // it cannot reject, so no test can make this handler fire and removing it
886
- // changes nothing today. It is here for the day the try/catch above stops
887
- // being exhaustive — or the eviction callback itself throws — because at
888
- // that point the fork becomes an unhandled rejection nobody is awaiting.
889
- void task
890
- .finally(() => {
891
- if (this.grammarEnsurePromises.get(grammarFile) === task) {
892
- this.grammarEnsurePromises.delete(grammarFile);
893
- }
894
- })
895
- .catch(() => {
896
- /* see above: the task's own catch is the real handler */
897
- });
898
- return task;
899
- }
900
- /**
901
- * One grammar-fetch attempt: locate a writable dir, download, and record the
902
- * outcome. Split out of `ensureGrammar` so the whole body sits under one
903
- * try/catch that funnels any throw into `recordGrammarFailure` (#1548).
904
- */
905
- async fetchGrammar(grammarFile) {
906
- // BEFORE resolving a write directory: a vendored grammar is never
907
- // downloaded, so the write dir has no bearing on its verdict. Resolving
908
- // first let the "no writable grammars directory" branch below answer
909
- // RETRYABLE for a missing vendored wasm on any host where
910
- // web-tree-sitter isn't locatable — reporting a packaging fault as a
911
- // transient download failure, and arming a cooldown for a fetch that can
912
- // never happen.
913
- if (isVendoredGrammar(grammarFile)) {
914
- const { reason } = vendoredGrammarRefusal(grammarFile);
915
- this.recordGrammarFailure(grammarFile, reason ?? `${grammarFile} is missing from vendor/grammars/.`,
916
- /* retryable */ false);
917
- return false;
918
- }
919
- const dir = this.grammarsDir && fs.existsSync(this.grammarsDir)
920
- ? this.grammarsDir
921
- : this.grammarsWriteDir();
922
- if (!dir) {
923
- // Nothing to write into: web-tree-sitter is not locatable by ANY rung
924
- // of the shared ladder (this runtime's resolver, pi-lens's own package
925
- // root, the working directory), so the package is missing from this
926
- // install. Still an environment condition, not a CDN verdict — #1536
927
- // review F2: it gets the same cooldown + notification treatment as a
928
- // download failure rather than a silent unmemoized `false` that re-does
929
- // the same failing resolution sweep on every demand.
930
- //
931
- // #3409: before the shared ladder, this arm ALSO caught every host
932
- // whose runtime cannot resolve a bare specifier — where the package was
933
- // present and the wasm downloadable — and told the user to approve
934
- // build scripts or restore network access. Hence the cause.
935
- this.recordGrammarFailure(grammarFile, "No grammars directory could be resolved for the runtime fetch: the web-tree-sitter package is not locatable from this runtime, from pi-lens's own package root, or from the working directory.",
936
- /* retryable */ true, "write-dir-unresolvable");
937
- return false;
938
- }
939
- // A destination that cannot be written to is not a download problem
940
- // either (#3409): a root-owned `node_modules` under a global install
941
- // reaches `downloadGrammarDetailed`'s catch-all, which reports no reason
942
- // at all, so the user was sent to inspect their package manager and
943
- // network for an EACCES.
944
- const writeBlock = grammarWriteBlockedReason(dir);
945
- if (writeBlock) {
946
- this.recordGrammarFailure(grammarFile, `The grammars directory ${dir} cannot be written to (${writeBlock}), so the runtime fetch has nowhere to land.`,
947
- /* retryable */ true, "write-dir-unwritable");
948
- return false;
949
- }
950
- // Reuse the shared single-file downloader (same CDN/source as the
951
- // postinstall) — see clients/grammar-source.ts.
952
- const { ok, retryable, reason } = await downloadGrammarDetailed(dir, grammarFile);
953
- if (ok) {
954
- if (!this.grammarsDir)
955
- this.grammarsDir = dir;
956
- this.grammarRetryAtMs.delete(grammarFile);
957
- this.grammarFailureAttempts.delete(grammarFile);
958
- this.grammarLastNotifiedDelayMs.delete(grammarFile);
959
- logTreeSitterDiagnostic({
960
- subsystem: "tree-sitter-client",
961
- level: "warn",
962
- message: `fetched missing tree-sitter grammar ${grammarFile} at runtime (install scripts were skipped by the package manager)`,
963
- metadata: { grammarFile, outcome: "fetched" },
964
- });
965
- }
966
- else {
967
- // `retryable` distinguishes a durable CDN verdict (404/410 — a
968
- // retry hits the same answer) from everything else (offline, DNS,
969
- // a down CDN, a 5xx, a timeout), which says nothing durable about
970
- // the grammar (#1536 review F4). Only the retryable case gets the
971
- // bounded cooldown; a durable failure keeps the pre-#1536 latched
972
- // behavior, matching the issue's own allowance.
973
- //
974
- // `reason`, when the downloader supplies one, names the actual failure
975
- // shape (#1548: "returned an HTML page, not a wasm module") instead of
976
- // the generic guess — the difference between a diagnosable log line
977
- // and one that sends the user to inspect their package manager while a
978
- // captive portal is the real cause.
979
- this.recordGrammarFailure(grammarFile, reason ??
980
- "The package manager skipped install scripts and the runtime download failed.", retryable, "download-failed");
981
- }
982
- return ok;
983
- }
984
- /**
985
- * Record one failed grammar-ensure attempt: arm (or extend) the retry
986
- * cooldown, record the degradation, and notify the user at most once per
987
- * session per DISTINCT retry delay (#1536 review F1/F2/F4/F5/F6). Shared
988
- * by the "no writable directory" and "download failed" arms of
989
- * `ensureGrammar` — both are failures of the SAME ensure attempt, just
990
- * with a different point of failure.
991
- *
992
- * `cause` is that point of failure (#3409). It picks the REMEDY the user is
993
- * told and lands in the durable record, so `~/.pi-lens` forensics can tell
994
- * "this host has nowhere to write" from "this download failed" — the two
995
- * were one string, and the one it carried named build scripts and the
996
- * network for a cause that is neither.
997
- */
998
- recordGrammarFailure(grammarFile, detail, retryable, cause) {
999
- const attempts = (this.grammarFailureAttempts.get(grammarFile) ?? 0) + 1;
1000
- this.grammarFailureAttempts.set(grammarFile, attempts);
1001
- // A durable verdict (404/410) never expires -- matches the issue's "a
1002
- // genuinely unavailable grammar may still latch" allowance. Everything
1003
- // else gets the bounded exponential cooldown.
1004
- const retryDelayMs = retryable
1005
- ? transientRetryDelayMs(attempts, "probe-timeout")
1006
- : undefined;
1007
- this.grammarRetryAtMs.set(grammarFile, retryDelayMs === undefined
1008
- ? Number.POSITIVE_INFINITY
1009
- : Date.now() + retryDelayMs);
1010
- // The remedy the user is given, keyed to WHERE the attempt failed. The
1011
- // last two arms are the pre-existing strings, byte for byte; the first
1012
- // two exist because sending someone to `pnpm approve-builds` and their
1013
- // network for a resolution or permission fault is the #3409 complaint.
1014
- const remedy = cause === "write-dir-unresolvable"
1015
- ? `This is a packaging/module-resolution fault, not a download problem: reinstall pi-lens so its ` +
1016
- `web-tree-sitter dependency is present, or drop the wasm into pi-lens's own grammars/ directory.`
1017
- : cause === "write-dir-unwritable"
1018
- ? `This is a filesystem-permission fault, not a download problem: make that directory writable ` +
1019
- `for the user pi-lens runs as, or reinstall pi-lens as that user.`
1020
- : retryable
1021
- ? `if the problem persists, allow the package manager's build scripts ` +
1022
- `(pnpm approve-builds / bun trustedDependencies) or restore network access.`
1023
- : `Fix: reinstall with a manager that runs postinstall, allow its build scripts ` +
1024
- `(pnpm approve-builds / bun trustedDependencies), or restore network access.`;
1025
- const unavailable = `tree-sitter grammar '${grammarFile}' is unavailable — symbol search, ` +
1026
- `module reports and structural rules for this language will be degraded. ` +
1027
- `${detail} ` +
1028
- (retryable
1029
- ? `pi-lens will retry automatically in ${Math.round(retryDelayMs / 1000)}s; `
1030
- : `The grammar source reports it does not exist (not a network problem), so this will not ` +
1031
- `resolve on retry. `) +
1032
- remedy;
1033
- logTreeSitterDiagnostic({
1034
- subsystem: "tree-sitter-client",
1035
- message: unavailable,
1036
- metadata: {
1037
- grammarFile,
1038
- outcome: "unavailable",
1039
- retryable,
1040
- retryDelayMs,
1041
- attempts,
1042
- // Omitted, not defaulted, for the arms that are neither a fetch
1043
- // destination nor a download verdict (a vendored grammar is never
1044
- // downloaded; a decode failure happened AFTER a successful one), so
1045
- // those records keep their pre-#3409 shape rather than carrying a
1046
- // cause that is not true of them.
1047
- ...(cause ? { cause } : {}),
1048
- },
1049
- });
1050
- // #1536 review F1: incrementDegradationCount keeps ONE ring-buffer slot
1051
- // per subject (bumping its count in place) instead of recordDegradation's
1052
- // one-new-entry-per-call — a grammar retrying every cooldown window would
1053
- // otherwise flood the shared "grammar-blocked" kind's 20-slot ring and
1054
- // evict unrelated entries (e.g. a different grammar's V8-crash block
1055
- // reason from loadLanguage/grammarBlockReason) after ~20 cycles.
1056
- incrementDegradationCount({
1057
- kind: "grammar-blocked",
1058
- subject: grammarFile,
1059
- // #1548: carry `detail` into the RETRYABLE reason too. All retryable
1060
- // failures used to share one string, so the ledger could not tell an
1061
- // offline laptop from a captive portal serving HTML — the two need
1062
- // different user actions, and the ledger is what a bug report shows.
1063
- reason: retryable
1064
- ? `runtime grammar download failed — retryable (${detail.trim()})`
1065
- : `runtime grammar download failed — durable (${detail.trim()})`,
1066
- });
1067
- this.refreshGrammarSessionLatches();
1068
- // HUMAN-audience: an offline grammar fetch silently degrades this
1069
- // language's features, so it reaches the user through the HOST's render
1070
- // path (#1333) rather than a raw terminal write. Notify once per DISTINCT
1071
- // retry delay this session (#1536 review F6) -- a fresh failure streak or
1072
- // an escalated backoff is new information worth surfacing; an unchanged
1073
- // repeat during the same cooldown tier is not.
1074
- const notifyKey = retryDelayMs ?? -1;
1075
- if (this.grammarLastNotifiedDelayMs.get(grammarFile) !== notifyKey) {
1076
- this.grammarLastNotifiedDelayMs.set(grammarFile, notifyKey);
1077
- notifyUserDegradation(`pi-lens: ${unavailable}`);
1078
- }
1079
- }
1080
- /**
1081
- * Record a `Language.load` failure on a file `resolveGrammarFile` had just
1082
- * vouched for (#1564) — the diagnosis gap #1548 left open: the preamble
1083
- * check proves the first four bytes are `\0asm`, not that the whole body
1084
- * decodes. A decode error here (a truncated download, on-disk corruption)
1085
- * is evidence the FILE is bad, so it gets the exact same treatment as an
1086
- * ensure-time download failure: invalidate the resolve memo so the next
1087
- * demand re-fetches instead of reusing the same broken file forever, and
1088
- * reuse `recordGrammarFailure`'s cooldown/degradation/notification
1089
- * machinery rather than hand-rolling a parallel one — a persistently
1090
- * truncated CDN response would otherwise re-download and re-fail on every
1091
- * single parse.
1092
- *
1093
- * A DURABLE decode failure (the bytes are complete and correct, but ABI-
1094
- * incompatible with the installed `web-tree-sitter` — a version drift, not
1095
- * a truncation) is classified `retryable` here too, same as everything
1096
- * else this method sees: there is no way to tell "truncated" from "wrong
1097
- * ABI" from the error message alone, so it re-downloads and re-fails once
1098
- * per cooldown tier instead of latching forever. That is bounded by the
1099
- * cooldown ladder (this never re-fetches faster than #1536's backoff) and
1100
- * by `BLOCKED_GRAMMARS` for the narrower case that's fatal to the process
1101
- * rather than just wrong (`grammar-source.ts`). The redownload only helps
1102
- * if a fresh fetch could actually be a DIFFERENT (compatible) build,
1103
- * which is exactly what the `grammar-source.test.ts` version/lock sync
1104
- * guard (`lock.version === TREE_SITTER_WASMS_VERSION`) exists to keep
1105
- * true — without it, a version bump with a stale lock would make every
1106
- * redownload land the identical bytes and spin the ladder against a file
1107
- * that can never pass.
1108
- */
1109
- recordGrammarLoadFailure(grammarPath, grammarFile, err) {
1110
- const stamp = grammarFileStamp(grammarPath);
1111
- this.verifiedGrammarPaths.delete(grammarPath);
1112
- if (stamp)
1113
- this.decodeFailedGrammarPaths.set(grammarPath, stamp);
1114
- const message = err instanceof Error ? err.message : String(err);
1115
- logTreeSitterDiagnostic({
1116
- subsystem: "tree-sitter-client",
1117
- level: "warn",
1118
- message: `Language.load failed for ${grammarPath} even though the wasm preamble check ` +
1119
- `passed (${message}) — most likely a truncated download (#1564). Treating the ` +
1120
- `grammar as missing so the next demand re-fetches it.`,
1121
- metadata: { grammarFile, path: grammarPath, outcome: "load-failed" },
1122
- });
1123
- this.recordGrammarFailure(grammarFile, `Language.load failed on a resolved grammar file (${message}).`,
1124
- /* retryable */ true);
1125
- }
1126
- /** Initialize tree-sitter WASM runtime */
1127
- async init() {
1128
- if (this.wasmAborted)
1129
- return false;
1130
- if (this.initialized)
1131
- return true;
1132
- if (this.webTreeSitterLoadFailed)
1133
- return false;
1134
- if (this.initPromise)
1135
- return this.initPromise;
1136
- this.initPromise = (async () => {
1137
- try {
1138
- let mod;
1139
- try {
1140
- mod = await loadWebTreeSitter();
1141
- }
1142
- catch (err) {
1143
- // See webTreeSitterLoadFailed's doc: an EVALUATION-shaped
1144
- // rejection is a dead retry, so latch instead of leaving
1145
- // initPromise clearable (the finally below would otherwise let
1146
- // the very next parse call re-import the same doomed URL). A
1147
- // RESOLUTION-shaped rejection is left alone — initPromise still
1148
- // clears below, so the next init() call retries for real.
1149
- const classification = classifyWebTreeSitterLoadFailure(err);
1150
- if (classification === "evaluation") {
1151
- this.webTreeSitterLoadFailed = true;
1152
- }
1153
- recordDegradationOnce({
1154
- kind: "web-tree-sitter-load-failed",
1155
- subject: "web-tree-sitter",
1156
- reason: (err instanceof Error ? err.message : String(err)) +
1157
- (classification === "resolution"
1158
- ? " (resolution failure — retryable)"
1159
- : " (evaluation failure — latched for the session)"),
1160
- });
1161
- throw err;
1162
- }
1163
- // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter module shape varies (Parser direct / default-wrapped)
1164
- const anyMod = mod;
1165
- const ParserClass = anyMod.Parser || anyMod.default || anyMod;
1166
- if (!ParserClass || typeof ParserClass.init !== "function") {
1167
- this.dbg("Parser class not found or missing init method");
1168
- return false;
1169
- }
1170
- // biome-ignore lint/suspicious/noExplicitAny: Parser class type
1171
- this.ParserClass = ParserClass;
1172
- // Store Language loader from module (not from Parser)
1173
- this.LanguageLoader = mod.Language;
1174
- // Resolve WASM path using same multi-strategy helper (hoisted installs +
1175
- // on-the-fly compilation by pi).
1176
- const wasmPath = this.resolveWebTreeSitterAsset("tree-sitter.wasm");
1177
- if (!wasmPath) {
1178
- this.dbg("Could not resolve tree-sitter.wasm");
1179
- return false;
1180
- }
1181
- const wasmDir = path.dirname(wasmPath);
1182
- this.dbg(`Looking for WASM at: ${wasmPath}, exists: ${fs.existsSync(wasmPath)}`);
1183
- await ParserClass.init({
1184
- locateFile: (scriptName) => {
1185
- const fullPath = path.join(wasmDir, scriptName);
1186
- this.dbg(`locateFile: ${scriptName} -> ${fullPath}`);
1187
- return fullPath;
1188
- },
1189
- });
1190
- this.initialized = true;
1191
- return true;
1192
- }
1193
- catch (err) {
1194
- this.reportWasmAbort(err);
1195
- this.dbg(`Init error: ${err}`);
1196
- return false;
1197
- }
1198
- finally {
1199
- this.initPromise = null;
1200
- }
1201
- })();
1202
- return this.initPromise;
1203
- }
1204
- /**
1205
- * Re-arm the web-tree-sitter load latch for a new session (#1592 review
1206
- * round 2 F2). Deliberately does NOT touch `wasmAborted` or `initialized`
1207
- * — see `webTreeSitterLoadFailed`'s doc for why the abort case stays
1208
- * process-lifetime, and a successful init has nothing to re-arm. Called
1209
- * from `resetTreeSitterClientLoadState()` (tree-sitter-shared.ts), wired
1210
- * into `resetDispatchBaselines()` beside `resetAstGrepNapiLoadState()`.
1211
- */
1212
- resetLoadStateForSession() {
1213
- this.webTreeSitterLoadFailed = false;
1214
- }
1215
- /** Load language grammar */
1216
- async loadLanguage(languageId) {
1217
- if (this.wasmAborted)
1218
- return null;
1219
- this.dbg(`Loading language: ${languageId}`);
1220
- if (this.languages.has(languageId)) {
1221
- this.dbg(`Language ${languageId} already loaded`);
1222
- return this.languages.get(languageId);
1223
- }
1224
- if (!this.ParserClass) {
1225
- this.dbg(`ParserClass not initialized`);
1226
- return null;
1227
- }
1228
- const grammarFile = LANGUAGE_TO_GRAMMAR[languageId];
1229
- if (!grammarFile) {
1230
- this.dbg(`No grammar file for ${languageId}`);
1231
- return null;
1232
- }
1233
- // A grammar that fatally crashes this runtime (uncatchable V8 abort) must
1234
- // never be loaded — skip it and degrade to "unavailable" (#423/#432). The
1235
- // grammar-health nightly is what decides membership of BLOCKED_GRAMMARS.
1236
- const blockReason = grammarBlockReason(grammarFile);
1237
- if (blockReason) {
1238
- this.dbg(`Grammar ${grammarFile} blocked on this runtime — ${blockReason}`);
1239
- recordDegradation({
1240
- kind: "grammar-blocked",
1241
- subject: grammarFile,
1242
- reason: blockReason,
1243
- });
1244
- return null;
1245
- }
1246
- // Look across the bundled core `grammars/` dir and the postinstall/lazy
1247
- // dir. Lazily fetch only if the grammar is in neither (pnpm/bun skip
1248
- // postinstall; the long-tail grammars aren't bundled). Only the language
1249
- // actually being parsed is fetched.
1250
- let grammarPath = this.resolveGrammarFile(grammarFile);
1251
- if (!grammarPath) {
1252
- if (await this.ensureGrammar(grammarFile)) {
1253
- grammarPath = this.resolveGrammarFile(grammarFile);
1254
- }
1255
- }
1256
- this.dbg(`Grammar path: ${grammarPath}, exists: ${grammarPath && fs.existsSync(grammarPath)}`);
1257
- if (!grammarPath || !fs.existsSync(grammarPath)) {
1258
- this.dbg(`Grammar file not found: ${grammarPath}`);
1259
- return null;
1260
- }
1261
- try {
1262
- if (!this.LanguageLoader?.load) {
1263
- this.dbg(`LanguageLoader.load not available`);
1264
- return null;
1265
- }
1266
- this.dbg(`Calling Language.load...`);
1267
- const language = await this.LanguageLoader.load(grammarPath);
1268
- this.dbg(`Language loaded: ${language?.name || "unknown"}`);
1269
- if (language) {
1270
- this.languages.set(languageId, language);
1271
- }
1272
- return language;
1273
- }
1274
- catch (err) {
1275
- // An uncatchable-shaped WASM abort poisons the whole process (#402) —
1276
- // `reportWasmAbort` already records + handles it. Everything else
1277
- // (#1564) is a DECODE failure on a file `resolveGrammarFile` just
1278
- // vouched for — e.g. "Code section extends past end of the module" on
1279
- // a truncated-but-magic-valid download. That is evidence the file
1280
- // itself is bad, not the runtime, so record it and stop vouching for
1281
- // this exact file so the next demand can re-fetch it.
1282
- if (!this.reportWasmAbort(err)) {
1283
- this.recordGrammarLoadFailure(grammarPath, grammarFile, err);
1284
- }
1285
- this.dbg(`Language load error: ${err}`);
1286
- return null;
1287
- }
1288
- }
1289
- /** Get or create parser for a language */
1290
- async getParser(languageId) {
1291
- if (this.wasmAborted)
1292
- return null;
1293
- if (this.parsers.has(languageId)) {
1294
- return this.parsers.get(languageId);
1295
- }
1296
- const language = await this.loadLanguage(languageId);
1297
- if (!language || !this.ParserClass)
1298
- return null;
1299
- const parser = new this.ParserClass();
1300
- parser.setLanguage(language);
1301
- this.parsers.set(languageId, parser);
1302
- return parser;
1303
- }
1304
- /**
1305
- * Parse a file and return the AST tree. The tree stays valid only until the
1306
- * caller's next `await` — prefer `withParsedTree`, which extracts inside the
1307
- * cache-safe window (#417/#675).
1308
- */
1309
- async parseFile(filePath, languageId, contentOverride) {
1310
- const outcome = await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => tree);
1311
- return outcome.parsed ? outcome.value : null;
1312
- }
1313
- async withParsedTree(filePath, languageId, contentOverride, consume) {
1314
- return this.parseFileAndUse(filePath, languageId, contentOverride, consume);
1315
- }
1316
- async parseFileAndUse(filePath, languageId, contentOverride, consume) {
1317
- this.dbg(`Parsing ${filePath} with language ${languageId}`);
1318
- const parser = await this.getParser(languageId);
1319
- if (!parser) {
1320
- this.dbg(`Failed to get parser for ${languageId}`);
1321
- return NOT_PARSED;
1322
- }
1323
- let tree;
1324
- try {
1325
- const content = contentOverride ?? fs.readFileSync(filePath, "utf-8");
1326
- this.dbg(`File content length: ${content.length}`);
1327
- const cachedTree = this.treeCache.get(filePath, content, languageId);
1328
- if (cachedTree) {
1329
- this.dbg(`Using cached tree for ${filePath}`);
1330
- tree = cachedTree;
1331
- }
1332
- else {
1333
- const parseStartedAt = performance.now();
1334
- this.recordParserCounter("parserInvocations");
1335
- try {
1336
- tree = parser.parse(content);
1337
- }
1338
- catch (err) {
1339
- this.recordParserCounter("parserFailures");
1340
- throw err;
1341
- }
1342
- finally {
1343
- this.recordParserCounter("parserDurationMs", performance.now() - parseStartedAt);
1344
- }
1345
- this.dbg(`Parsed, root node type: ${tree.rootNode.type}`);
1346
- this.treeCache.set(filePath, content, languageId, tree);
1347
- }
1348
- }
1349
- catch (err) {
1350
- this.reportWasmAbort(err);
1351
- this.dbg(`Parse error: ${err}`);
1352
- return NOT_PARSED;
1353
- }
1354
- try {
1355
- return { parsed: true, value: consume(tree) };
1356
- }
1357
- catch (error) {
1358
- this.reportWasmAbort(error);
1359
- throw error;
1360
- }
1361
- }
1362
- /**
1363
- * Detect and extract injected content from template literals
1364
- * Used for security analysis (SQL injection, unsafe regex, etc.)
1365
- */
1366
- extractInjections(filePath, content) {
1367
- const injections = [];
1368
- // Pattern: sql`SELECT * FROM users` or query`...`
1369
- const sqlPattern = /\b(sql|query|execute)\s*`([^`]+)`/gi;
1370
- let match;
1371
- while ((match = sqlPattern.exec(content)) !== null) {
1372
- const lines = content.slice(0, match.index).split("\n");
1373
- injections.push({
1374
- type: "sql",
1375
- content: match[2],
1376
- line: lines.length,
1377
- column: lines[lines.length - 1].length,
1378
- });
1379
- }
1380
- // Pattern: styled.div`color: red;` or css`...`
1381
- const cssPattern = /\b(styled(?:\.\w+)?|css)\s*`([^`]+)`/gi;
1382
- while ((match = cssPattern.exec(content)) !== null) {
1383
- const lines = content.slice(0, match.index).split("\n");
1384
- injections.push({
1385
- type: "css",
1386
- content: match[2],
1387
- line: lines.length,
1388
- column: lines[lines.length - 1].length,
1389
- });
1390
- }
1391
- // Pattern: new RegExp(`pattern`)
1392
- const regexPattern = /new\s+RegExp\s*\(\s*`([^`]+)`/gi;
1393
- while ((match = regexPattern.exec(content)) !== null) {
1394
- const lines = content.slice(0, match.index).split("\n");
1395
- injections.push({
1396
- type: "regex",
1397
- content: match[1],
1398
- line: lines.length,
1399
- column: lines[lines.length - 1].length,
1400
- });
1401
- }
1402
- this.dbg(`Found ${injections.length} injections in ${filePath}`);
1403
- return injections;
1404
- }
1405
- /** Check if tree-sitter is available (a core grammar resolves somewhere). */
1406
- isAvailable() {
1407
- if (this.wasmAborted)
1408
- return false;
1409
- // Available if the core TS grammar resolves in ANY source dir — the bundled
1410
- // `grammars/` counts even when web-tree-sitter/grammars is empty (no
1411
- // postinstall on pnpm/bun, or a fresh CI checkout).
1412
- if (this.resolveGrammarFile("tree-sitter-typescript.wasm"))
1413
- return true;
1414
- // Re-evaluate the legacy dir in case grammars were installed after start.
1415
- const dir = this.findGrammarsDir();
1416
- this.grammarsDir = dir;
1417
- return !!dir && fs.existsSync(dir);
1418
- }
1419
- /** Check if specific language is supported */
1420
- async isLanguageSupported(languageId) {
1421
- if (this.wasmAborted)
1422
- return false;
1423
- if (!this.initialized)
1424
- await this.init();
1425
- const language = await this.loadLanguage(languageId);
1426
- return language !== null;
1427
- }
1428
- /** Get loaded language for symbol extraction */
1429
- getLanguage(languageId) {
1430
- if (this.wasmAborted)
1431
- return null;
1432
- return this.languages.get(languageId) || null;
1433
- }
1434
- // --- Structural Search ---
1435
- /**
1436
- * Search for a structural pattern in files
1437
- *
1438
- * @param pattern - Pattern with metavariables (e.g., "console.log($MSG)")
1439
- * @param languageId - Language ID (typescript, python, etc.)
1440
- * @param rootDir - Directory to search
1441
- * @param options - Search options
1442
- * @returns Array of matches with captures
1443
- */
1444
- async structuralSearch(pattern, languageId, rootDir, options = {}) {
1445
- if (!this.initialized) {
1446
- const ok = await this.init();
1447
- if (!ok)
1448
- return [];
1449
- }
1450
- try {
1451
- await this.queryLoader.loadQueries(rootDir);
1452
- }
1453
- catch (err) {
1454
- this.dbg(`Failed to load queries for ${rootDir}: ${err}`);
1455
- }
1456
- // Compile pattern into tree-sitter query
1457
- this.dbg(`Compiling pattern: ${pattern.slice(0, 50)}...`);
1458
- const compiled = await this.compileQuery(pattern, languageId);
1459
- if (!compiled) {
1460
- this.dbg(`Pattern compilation failed`);
1461
- return [];
1462
- }
1463
- this.dbg(`Pattern compiled, metavars: ${compiled.metavars.join(", ")}`);
1464
- // Collect source files
1465
- const files = this.collectFiles(rootDir, languageId, options.fileFilter);
1466
- this.dbg(`Scanning ${files.length} files...`);
1467
- const matches = [];
1468
- const maxResults = options.maxResults ?? 50;
1469
- for (const file of files) {
1470
- if (matches.length >= maxResults)
1471
- break;
1472
- const fileMatches = await this.searchFileWithQuery(file, compiled.query, compiled.metavars, languageId, pattern, compiled.postFilter, compiled.postFilterParams);
1473
- matches.push(...fileMatches);
1474
- }
1475
- return matches.slice(0, maxResults);
1476
- }
1477
- /**
1478
- * Run a preloaded query definition against a single file.
1479
- *
1480
- * Optimized for dispatch runner usage to avoid per-query directory scans.
1481
- */
1482
- async runQueryOnFile(queryDef, filePath, languageId, options = {}, contentOverride) {
1483
- if (!this.initialized) {
1484
- const ok = await this.init();
1485
- if (!ok)
1486
- return [];
1487
- }
1488
- // Compile against the language the FILE is parsed as, never the query's
1489
- // own `language:` key. A Query is bound to the grammar it compiled
1490
- // against; run it on a tree from another grammar and tree-sitter returns
1491
- // zero matches forever — which is what the javascript→typescript rule
1492
- // merge had been doing (31 queries per JS file, structurally unable to
1493
- // fire). `getQueryCacheKey` already namespaces by language.
1494
- const compiled = await this.compileRawQuery(queryDef.id, queryDef.query, queryDef.metavars, languageId, queryDef.post_filter, queryDef.post_filter_params);
1495
- if (!compiled)
1496
- return [];
1497
- const matches = await this.searchFileWithQuery(filePath, compiled.query, compiled.metavars, languageId, queryDef.id, compiled.postFilter, compiled.postFilterParams, contentOverride);
1498
- const maxResults = options.maxResults ?? 50;
1499
- return matches.slice(0, maxResults);
1500
- }
1501
- /**
1502
- * Run a whole rule set against one file in a SINGLE tree walk (#675).
1503
- *
1504
- * Calling `runQueryOnFile` per rule re-walks the tree once per rule — ~34
1505
- * walks per file on a project scan, measured at 3.3× the cost of one
1506
- * combined query for byte-identical matches. Patterns are concatenated in
1507
- * rule order and `match.patternIndex` maps back to the owning rule, so
1508
- * per-rule metavars, predicates, post-filters and caps still apply and
1509
- * results stay grouped in rule order. Rules that don't compile against this
1510
- * language are dropped individually (never poisoning the batch), and a
1511
- * combined query that fails to compile falls back to per-rule execution.
1512
- */
1513
- async runQueriesOnFile(queryDefs, filePath, languageId, options = {}, contentOverride) {
1514
- if (queryDefs.length === 0)
1515
- return [];
1516
- if (!this.initialized) {
1517
- const ok = await this.init();
1518
- if (!ok)
1519
- return [];
1520
- }
1521
- const maxResults = options.maxResults ?? 50;
1522
- const batch = await this.compileQueryBatch(queryDefs, languageId);
1523
- if (!batch) {
1524
- // Fallback: one walk per rule, preserving rule order.
1525
- const results = [];
1526
- for (const queryDef of queryDefs) {
1527
- const matches = await this.runQueryOnFile(queryDef, filePath, languageId, options, contentOverride);
1528
- for (const match of matches)
1529
- results.push({ queryDef, match });
1530
- }
1531
- return results;
1532
- }
1533
- const perQuery = new Map();
1534
- await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
1535
- try {
1536
- const rootNode = tree.rootNode;
1537
- for (const match of batch.query.matches(rootNode)) {
1538
- const owner = batch.ownerOfPattern[match.patternIndex];
1539
- if (owner === undefined)
1540
- continue;
1541
- const bucket = perQuery.get(owner) ?? [];
1542
- if (bucket.length >= maxResults)
1543
- continue;
1544
- const entry = batch.entries[owner];
1545
- const captures = {};
1546
- for (const capture of match.captures) {
1547
- if (entry.metavars.includes(capture.name)) {
1548
- captures[capture.name] = capture.node;
1549
- }
1550
- }
1551
- if (!this.evaluatePredicates(batch.query, match))
1552
- continue;
1553
- if (entry.postFilter &&
1554
- !this.applyPostFilter(entry.postFilter, entry.postFilterParams, captures, rootNode)) {
1555
- continue;
1556
- }
1557
- if (match.captures.length === 0)
1558
- continue;
1559
- const firstNode = match.captures[0].node;
1560
- const textCaptures = {};
1561
- for (const [name, node] of Object.entries(captures)) {
1562
- textCaptures[name] = node.text;
1563
- }
1564
- bucket.push({
1565
- file: filePath,
1566
- line: firstNode.startPosition.row + 1,
1567
- column: firstNode.startPosition.column + 1,
1568
- matchedText: firstNode.text,
1569
- nodeType: firstNode.type,
1570
- captures: textCaptures,
1571
- });
1572
- perQuery.set(owner, bucket);
1573
- }
1574
- }
1575
- catch (err) {
1576
- this.reportWasmAbort(err);
1577
- this.dbg(`Batched query matching error: ${err}`);
1578
- }
1579
- });
1580
- const results = [];
1581
- for (let i = 0; i < batch.entries.length; i++) {
1582
- for (const match of perQuery.get(i) ?? []) {
1583
- results.push({ queryDef: batch.entries[i].queryDef, match });
1584
- }
1585
- }
1586
- return results;
1587
- }
1588
- /**
1589
- * Compile `queryDefs` into one multi-pattern Query for `languageId`, with a
1590
- * pattern-index → rule map. Cached per language + rule-set identity.
1591
- */
1592
- async compileQueryBatch(queryDefs, languageId) {
1593
- // Key on rule CONTENT, not just ids: the batch stores each queryDef (its
1594
- // message reaches diagnostics) and the compiled patterns. Rule ids are
1595
- // stable across edits, so an id-only key kept serving the pre-edit batch
1596
- // for the process lifetime even after the runner reloaded the rules from
1597
- // disk on a RuleCache miss (#878). sha256 keeps the key bounded without
1598
- // #889's 32-bit collision risk.
1599
- const identity = crypto
1600
- .createHash("sha256")
1601
- .update(JSON.stringify(queryDefs))
1602
- .digest("hex");
1603
- const cacheKey = this.getQueryCacheKey(`batch:${identity}`, languageId);
1604
- const cached = this.queryBatchCache.get(cacheKey);
1605
- if (cached !== undefined) {
1606
- this.queryBatchCache.delete(cacheKey);
1607
- this.queryBatchCache.set(cacheKey, cached);
1608
- return cached;
1609
- }
1610
- // A loadLanguage() failure is transient (offline lazy grammar fetch,
1611
- // transient mid-scan load error) — do NOT cache null for it, or every
1612
- // later scan pays the ~3.3x per-rule fallback for the process lifetime
1613
- // (#889). Retry on the next call, bounded so a grammar that never loads
1614
- // doesn't re-attempt every scan.
1615
- const language = await this.loadLanguage(languageId);
1616
- if (!language) {
1617
- const failures = (this.queryBatchLoadFailures.get(cacheKey) ?? 0) + 1;
1618
- if (failures >= QUERY_BATCH_MAX_LOAD_FAILURES) {
1619
- this.dbg(`Batch: grammar for ${languageId} failed to load ${failures} times — caching miss`);
1620
- this.queryBatchLoadFailures.delete(cacheKey);
1621
- this.cacheQueryBatch(cacheKey, null);
1622
- }
1623
- else {
1624
- this.queryBatchLoadFailures.set(cacheKey, failures);
1625
- }
1626
- return null;
1627
- }
1628
- this.queryBatchLoadFailures.delete(cacheKey);
1629
- const build = async () => {
1630
- const Query = (await loadWebTreeSitter()).Query;
1631
- const entries = [];
1632
- const sources = [];
1633
- const ownerOfPattern = [];
1634
- for (const queryDef of queryDefs) {
1635
- let patternCount;
1636
- try {
1637
- // biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
1638
- const probe = new Query(language, queryDef.query);
1639
- patternCount = probe.patternCount();
1640
- probe.delete?.();
1641
- }
1642
- catch (err) {
1643
- if (this.reportWasmAbort(err))
1644
- return null;
1645
- // A rule that can't compile against THIS grammar is simply not
1646
- // applicable here (e.g. a `type_annotation` pattern on javascript).
1647
- this.dbg(`Batch: skipping ${queryDef.id} for ${languageId}: ${err}`);
1648
- this.reportQueryCompileFailure(queryDef.id, languageId, err);
1649
- continue;
1650
- }
1651
- const owner = entries.length;
1652
- entries.push({
1653
- queryDef,
1654
- metavars: queryDef.metavars ?? [],
1655
- postFilter: queryDef.post_filter,
1656
- postFilterParams: queryDef.post_filter_params,
1657
- });
1658
- sources.push(queryDef.query);
1659
- for (let i = 0; i < patternCount; i++)
1660
- ownerOfPattern.push(owner);
1661
- }
1662
- if (entries.length === 0)
1663
- return null;
1664
- try {
1665
- // biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
1666
- const query = new Query(language, sources.join("\n"));
1667
- if (query.patternCount() !== ownerOfPattern.length) {
1668
- this.dbg(`Batch pattern count mismatch for ${languageId} (${query.patternCount()} vs ${ownerOfPattern.length}) — falling back`);
1669
- return null;
1670
- }
1671
- return { query, entries, ownerOfPattern };
1672
- }
1673
- catch (err) {
1674
- if (this.reportWasmAbort(err))
1675
- return null;
1676
- this.dbg(`Batch compile failed for ${languageId}: ${err}`);
1677
- return null;
1678
- }
1679
- };
1680
- const batch = await build();
1681
- this.cacheQueryBatch(cacheKey, batch);
1682
- return batch;
1683
- }
1684
- /**
1685
- * Convert pattern to tree-sitter query
1686
- * First tries to load from query files, then falls back to inline patterns
1687
- */
1688
- patternToQuery(pattern, languageId) {
1689
- // Try to find matching query from loaded files
1690
- const loadedQuery = this.queryLoader.findMatchingQuery(pattern, languageId);
1691
- if (loadedQuery) {
1692
- this.dbg(`Using loaded query: ${loadedQuery.id}`);
1693
- return {
1694
- query: loadedQuery.query,
1695
- metavars: loadedQuery.metavars,
1696
- postFilter: loadedQuery.post_filter,
1697
- postFilterParams: loadedQuery.post_filter_params,
1698
- queryDef: loadedQuery,
1699
- };
1700
- }
1701
- // Fallback to inline patterns
1702
- return this.getInlinePattern(pattern);
1703
- }
1704
- /**
1705
- * Inline patterns as fallback when no query file matches
1706
- */
1707
- getInlinePattern(pattern) {
1708
- // Pattern: async function $NAME($$$PARAMS) { $BODY }
1709
- if (pattern.includes("async function") && pattern.includes("$NAME")) {
1710
- return {
1711
- query: `(function_declaration
1712
- "async"
1713
- name: (identifier) @NAME
1714
- parameters: (formal_parameters) @PARAMS
1715
- body: (statement_block) @BODY)`,
1716
- metavars: ["NAME", "PARAMS", "BODY"],
1717
- };
1718
- }
1719
- // Pattern: console.$METHOD($MSG)
1720
- if (pattern.includes("console")) {
1721
- return {
1722
- query: `(call_expression
1723
- function: (member_expression
1724
- object: (identifier) @OBJ (#eq? @OBJ "console")
1725
- property: (property_identifier) @METHOD)
1726
- arguments: (arguments) @ARGS)`,
1727
- metavars: ["OBJ", "METHOD", "ARGS"],
1728
- };
1729
- }
1730
- // Pattern: function $NAME($$$PARAMS) { $BODY } - match long parameter lists
1731
- if (pattern.includes("function $NAME") && pattern.includes("PARAMS")) {
1732
- return {
1733
- query: `(function_declaration
1734
- name: (identifier) @NAME
1735
- parameters: (formal_parameters) @PARAMS
1736
- body: (statement_block) @BODY)`,
1737
- metavars: ["NAME", "PARAMS", "BODY"],
1738
- postFilter: "count_params",
1739
- postFilterParams: { min_params: 6 },
1740
- };
1741
- }
1742
- // Pattern: promise chains with .then().catch().then() - 3+ levels
1743
- if (pattern.includes(".then") && pattern.includes(".catch")) {
1744
- return {
1745
- query: `(call_expression
1746
- function: (member_expression
1747
- object: (call_expression
1748
- function: (member_expression
1749
- object: (call_expression
1750
- function: (member_expression
1751
- property: (property_identifier) @M1)
1752
- arguments: (arguments))
1753
- property: (property_identifier) @M2)
1754
- arguments: (arguments))
1755
- property: (property_identifier) @M3)
1756
- arguments: (arguments))
1757
- (#match? @M1 "^(then|catch)$")
1758
- (#match? @M2 "^(then|catch)$")
1759
- (#match? @M3 "^(then|catch)$")`,
1760
- metavars: ["M1", "M2", "M3"],
1761
- };
1762
- }
1763
- // Fallback: try to create a simple identifier capture
1764
- const simpleMatch = pattern.match(/\$([A-Z_][A-Z0-9_]*)/);
1765
- if (simpleMatch) {
1766
- const name = simpleMatch[1];
1767
- return {
1768
- query: `(identifier) @${name}`,
1769
- metavars: [name],
1770
- };
1771
- }
1772
- // If we can't convert, return empty to trigger fallback
1773
- return { query: "", metavars: [] };
1774
- }
1775
- /**
1776
- * Inject native tree-sitter predicates into S-expression query
1777
- * This moves text filtering to WASM for better performance
1778
- */
1779
- /** Generate cache key for compiled query */
1780
- getQueryCacheKey(pattern, languageId) {
1781
- // Full pattern text as the key. The previous 32-bit hash could collide
1782
- // across rules and permanently poison a cache slot with the wrong
1783
- // compiled query (or a null meant for a different rule set) (#889).
1784
- return `${languageId}:${pattern}`;
1785
- }
1786
- /** Compile a pattern into a tree-sitter Query with caching */
1787
- async compileQuery(pattern, languageId) {
1788
- const cacheKey = this.getQueryCacheKey(pattern, languageId);
1789
- // Check cache first
1790
- if (this.queryCache.has(cacheKey)) {
1791
- this.dbg(`Query cache hit: ${cacheKey}`);
1792
- const cached = this.queryCache.get(cacheKey);
1793
- this.queryCache.delete(cacheKey);
1794
- this.queryCache.set(cacheKey, cached);
1795
- return cached;
1796
- }
1797
- const language = await this.loadLanguage(languageId);
1798
- if (!language) {
1799
- this.dbg(`Could not load language ${languageId}`);
1800
- return null;
1801
- }
1802
- const { query: queryStr, metavars, postFilter, postFilterParams, } = this.patternToQuery(pattern, languageId);
1803
- this.dbg(`Query string: ${queryStr.slice(0, 100)}...`);
1804
- try {
1805
- // biome-ignore lint/suspicious/noExplicitAny: Query constructor
1806
- const Query = (await loadWebTreeSitter()).Query;
1807
- // biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
1808
- const query = new Query(language, queryStr);
1809
- this.dbg(`Query compiled with ${query.patternCount()} patterns`);
1810
- const result = { query, metavars, postFilter, postFilterParams };
1811
- // Cache the compiled query
1812
- this.cacheQuery(cacheKey, result);
1813
- return result;
1814
- }
1815
- catch (err) {
1816
- this.reportWasmAbort(err);
1817
- this.dbg(`Query compilation failed: ${err}`);
1818
- return null;
1819
- }
1820
- }
1821
- /** Compile a raw tree-sitter query string with caching */
1822
- async compileRawQuery(queryId, queryStr, metavars, languageId, postFilter, postFilterParams) {
1823
- const cacheKey = this.getQueryCacheKey(`raw:${queryId}:${queryStr}`, languageId);
1824
- if (this.queryCache.has(cacheKey)) {
1825
- const cached = this.queryCache.get(cacheKey);
1826
- this.queryCache.delete(cacheKey);
1827
- this.queryCache.set(cacheKey, cached);
1828
- return cached;
1829
- }
1830
- const language = await this.loadLanguage(languageId);
1831
- if (!language)
1832
- return null;
1833
- try {
1834
- // biome-ignore lint/suspicious/noExplicitAny: Query constructor from web-tree-sitter
1835
- const Query = (await loadWebTreeSitter()).Query;
1836
- // biome-ignore lint/suspicious/noExplicitAny: Language type compatibility
1837
- const query = new Query(language, queryStr);
1838
- const result = { query, metavars, postFilter, postFilterParams };
1839
- this.cacheQuery(cacheKey, result);
1840
- return result;
1841
- }
1842
- catch (err) {
1843
- this.reportWasmAbort(err);
1844
- this.dbg(`Raw query compilation failed (${queryId}): ${err}`);
1845
- this.reportQueryCompileFailure(queryId, languageId, err);
1846
- return null;
1847
- }
1848
- }
1849
- reportedCompileFailures = new Set();
1850
- /** Warn once per rule+grammar pair whose query fails to compile — a silently-dead rule needs a trail. */
1851
- reportQueryCompileFailure(ruleId, languageId, err) {
1852
- // Keyed by rule AND language: a rule can be dispatched against more than one
1853
- // grammar (`queriesForLanguage` hands the typescript set to tsx too), and
1854
- // failing on one must not mute the report for the others.
1855
- const key = `${ruleId}:${languageId}`;
1856
- if (this.reportedCompileFailures.has(key))
1857
- return;
1858
- this.reportedCompileFailures.add(key);
1859
- logTreeSitterDiagnostic({
1860
- subsystem: "tree-sitter-client",
1861
- languageId,
1862
- message: `tree-sitter rule '${ruleId}' failed to compile against '${languageId}' — ` +
1863
- `matches for this rule are silently dropped rather than reported. ` +
1864
- `Fix the query in the rule definition to re-enable it. (${err})`,
1865
- metadata: { ruleId },
1866
- });
1867
- }
1868
- hasChildToken(node, token) {
1869
- return node.children?.some((child) => child.type === token || child.text === token);
1870
- }
1871
- /**
1872
- * Collect names introduced by a parameter binding pattern.
1873
- *
1874
- * Binding-aware: only identifiers in a *binding* position are counted.
1875
- * Two node shapes hold reference expressions rather than bindings and
1876
- * must not be descended into:
1877
- * - `assignment_pattern` / `object_assignment_pattern` (`pattern = default`,
1878
- * e.g. `[a = b]` or `{a = b}`) — the `value`/`right` side is a default
1879
- * *expression*, not a new name (`b` in `{a = b}` is a reference).
1880
- * - `computed_property_name` (`[expr]:` in a destructuring pattern,
1881
- * e.g. `{[key]: a}`) — `expr` is a reference, not a binding.
1882
- * Nested binding positions (destructured sub-patterns) are still walked
1883
- * so renamed/duplicate collisions inside them are found.
1884
- */
1885
- bindingNames(node) {
1886
- const names = new Set();
1887
- if (!node)
1888
- return names;
1889
- const stack = [node];
1890
- while (stack.length > 0) {
1891
- const current = stack.pop();
1892
- if (current.type === "identifier")
1893
- names.add(current.text);
1894
- else if (current.type === "shorthand_property_identifier_pattern") {
1895
- names.add(current.text);
1896
- }
1897
- if (current.type === "property_identifier" ||
1898
- current.type === "type_identifier" ||
1899
- current.type === "computed_property_name")
1900
- continue;
1901
- if (current.type === "assignment_pattern" ||
1902
- current.type === "object_assignment_pattern") {
1903
- const left = current.childForFieldName?.("left");
1904
- if (left)
1905
- stack.push(left);
1906
- continue;
1907
- }
1908
- stack.push(...(current.children ?? []));
1909
- }
1910
- return names;
1911
- }
1912
- containsYieldInFunctionBody(node, root = node) {
1913
- for (const child of node.children ?? []) {
1914
- if (child.type === "yield")
1915
- return true;
1916
- if (child !== root &&
1917
- ["function_definition", "class_definition", "lambda"].includes(child.type)) {
1918
- continue;
1919
- }
1920
- if (this.containsYieldInFunctionBody(child, root))
1921
- return true;
1922
- }
1923
- return false;
1924
- }
1925
- /**
1926
- * SQLAlchemy sessions are conventionally bound to one of a handful of
1927
- * receiver names. Structural provenance (`python-provenance.ts`) proves the
1928
- * annotated case; this name check keeps the far more common unannotated
1929
- * `session.execute(stmt)` quiet, as it has been since the exemption was
1930
- * added. Removing it regresses every codebase that never annotates.
1931
- */
1932
- isLikelySqlAlchemyReceiver(text) {
1933
- const tail = text.split(".").pop() ?? text;
1934
- return PYTHON_SQLALCHEMY_RECEIVER_NAMES.has(tail.toLowerCase());
1935
- }
1936
- /**
1937
- * The body statements of a `switch_case`, in order. A switch_case's named
1938
- * children are `[value, ...statements]` (its statements are direct children,
1939
- * not a wrapping statement_block), so drop the leading `case <value>` and any
1940
- * comments.
1941
- */
1942
- switchCaseBodyStatements(caseNode) {
1943
- // biome-ignore lint/suspicious/noExplicitAny: AST child iteration
1944
- const named = (caseNode.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
1945
- // First named child is the case's value expression (`case <value>`).
1946
- return named.slice(1);
1947
- }
1948
- /**
1949
- * Whether a case body carries an intentional fallthrough marker comment
1950
- * (`// fallthrough`, `// falls through`, …). Only a *trailing* marker is
1951
- * honored: a comment attached to the case node after its body statements
1952
- * (TypeScript), the case's next sibling in the switch body (JavaScript), or
1953
- * one inside the trailing body statement's block. A comment in a nested
1954
- * function or an earlier statement is not a marker for this case and must
1955
- * not suppress a genuine fall-through. Only comment nodes are checked, never
1956
- * the case value or statement text.
1957
- */
1958
- hasFallthroughMarker(caseNode) {
1959
- const comments = [];
1960
- const body = this.switchCaseBodyStatements(caseNode);
1961
- const last = body[body.length - 1];
1962
- const lastEnd = last?.endIndex ?? caseNode.startIndex;
1963
- // TypeScript attaches a trailing `// fallthrough` as a direct child of the
1964
- // case node, after the body statements.
1965
- for (const c of caseNode.children ?? []) {
1966
- if (c.type.includes("comment") && c.startIndex >= lastEnd) {
1967
- comments.push(c);
1968
- }
1969
- }
1970
- // JavaScript attaches it to the switch body as the case's next sibling.
1971
- const siblings = (caseNode.parent?.children ?? []).filter((c) => c.isNamed);
1972
- const index = siblings.findIndex((c) => c.startIndex === caseNode.startIndex);
1973
- const next = index >= 0 ? siblings[index + 1] : undefined;
1974
- if (next?.type.includes("comment"))
1975
- comments.push(next);
1976
- // A marker inside the trailing body statement's block (e.g. `{ work();
1977
- // /* fallthrough */ }`). Only the trailing statement is walked, and nested
1978
- // functions are not descended into, so a comment in a nested function or
1979
- // an earlier statement can't suppress a genuine fall-through.
1980
- if (last) {
1981
- const NESTED_FN = new Set([
1982
- "function_declaration",
1983
- "function_expression",
1984
- "arrow_function",
1985
- "method_definition",
1986
- "generator_function",
1987
- "generator_function_declaration",
1988
- "class_declaration",
1989
- ]);
1990
- const stack = [last];
1991
- for (let visited = 0; stack.length > 0 && visited < 500; visited++) {
1992
- const node = stack.pop();
1993
- if (!node)
1994
- break;
1995
- if (node.type.includes("comment"))
1996
- comments.push(node);
1997
- if (!NESTED_FN.has(node.type)) {
1998
- stack.push(...(node.children ?? []));
1999
- }
2000
- }
2001
- }
2002
- return comments.some((c) => /falls?\s?through/i.test(c.text));
2003
- }
2004
- /**
2005
- * Whether a statement terminates control flow (does not fall through to the
2006
- * next statement). Handles terminators, trailing blocks, try/catch/finally,
2007
- * and exhaustive if/else. Fail-safe: any unrecognized shape returns false
2008
- * (falls through), so a blocking rule never under-reports. Depth-bounded so a
2009
- * pathological nesting cannot blow the stack.
2010
- */
2011
- statementTerminates(node, depth = 0) {
2012
- if (depth > 20)
2013
- return false;
2014
- const t = node.type;
2015
- const TERMINATORS = new Set([
2016
- "break_statement",
2017
- "return_statement",
2018
- "throw_statement",
2019
- "continue_statement",
2020
- ]);
2021
- if (TERMINATORS.has(t))
2022
- return true;
2023
- // Wrapper nodes whose trailing statement decides termination: a block, an
2024
- // `else` clause, a `catch` clause, or a `finally` clause.
2025
- if (t === "statement_block" ||
2026
- t === "else_clause" ||
2027
- t === "catch_clause" ||
2028
- t === "finally_clause") {
2029
- const inner = (node.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
2030
- const last = inner[inner.length - 1];
2031
- if (!last)
2032
- return false; // empty wrapper falls through
2033
- return this.statementTerminates(last, depth + 1);
2034
- }
2035
- if (t === "try_statement") {
2036
- // The try body is the first statement_block child. A returning/throwing
2037
- // try completes after finally, so only a catch that handles a thrown try
2038
- // and falls through can still reach the end. A try body that completes
2039
- // normally reaches the end unless a finally terminates it.
2040
- const tryBody = (node.children ?? []).find((c) => c.type === "statement_block");
2041
- const tryTerminates = tryBody
2042
- ? this.statementTerminates(tryBody, depth + 1)
2043
- : false;
2044
- const catches = (node.children ?? []).filter((c) => c.type === "catch_clause");
2045
- const fin = (node.children ?? []).find((c) => c.type === "finally_clause");
2046
- if (tryTerminates) {
2047
- return catches.every((c) => this.statementTerminates(c, depth + 1));
2048
- }
2049
- return !!fin && this.statementTerminates(fin, depth + 1);
2050
- }
2051
- if (t === "if_statement") {
2052
- // An if only terminates when it has an else and both branches do.
2053
- const named = (node.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
2054
- // named = [condition, consequence, (else_clause)]
2055
- const consequence = named[1];
2056
- const alternative = named[2];
2057
- if (!consequence || !alternative)
2058
- return false;
2059
- return (this.statementTerminates(consequence, depth + 1) &&
2060
- this.statementTerminates(alternative, depth + 1));
2061
- }
2062
- return false;
2063
- }
2064
- /**
2065
- * Whether a `switch_case`'s next sibling is another label — `case "a": case
2066
- * "b": handle()` groups two labels onto one body, which is idiomatic, not a
2067
- * dead case.
2068
- */
2069
- nextSiblingIsSwitchLabel(caseNode) {
2070
- const siblings = (caseNode.parent?.children ?? []).filter((c) => c.isNamed && !c.type.includes("comment"));
2071
- // Match by source offset: web-tree-sitter hands out fresh node wrappers per
2072
- // access, so identity comparison against `caseNode` is not reliable.
2073
- const index = siblings.findIndex((c) => c.startIndex === caseNode.startIndex);
2074
- const next = index >= 0 ? siblings[index + 1] : undefined;
2075
- return next?.type === "switch_case" || next?.type === "switch_default";
2076
- }
2077
- /**
2078
- * Whether a loop body contains a statement that can terminate the loop:
2079
- * `return`/`throw` anywhere (they unwind past the loop), or a `break` that is
2080
- * not swallowed by a nested loop/switch. Does not descend into nested
2081
- * functions, whose `return` exits the function rather than the loop.
2082
- */
2083
- bodyHasLoopExit(node, insideNestedLoop) {
2084
- const NESTS_BREAK = new Set([
2085
- "for_statement",
2086
- "for_in_statement",
2087
- "while_statement",
2088
- "do_statement",
2089
- "switch_statement",
2090
- ]);
2091
- const NESTED_FN = new Set([
2092
- "function_declaration",
2093
- "function_expression",
2094
- "arrow_function",
2095
- "method_definition",
2096
- "generator_function",
2097
- "generator_function_declaration",
2098
- "class_declaration",
2099
- ]);
2100
- for (const child of node.children ?? []) {
2101
- const t = child.type;
2102
- if (NESTED_FN.has(t))
2103
- continue;
2104
- if (t === "return_statement" || t === "throw_statement")
2105
- return true;
2106
- // A labeled `break outer;` is not swallowed by the nested loop it sits
2107
- // in. Which label it targets isn't resolved here — counting any labeled
2108
- // break as an exit errs toward not flagging, the safe direction for a
2109
- // blocking rule.
2110
- if (t === "break_statement") {
2111
- const labeled = (child.children ?? []).some((c) => c.isNamed);
2112
- if (!insideNestedLoop || labeled)
2113
- return true;
2114
- }
2115
- if (this.bodyHasLoopExit(child, insideNestedLoop || NESTS_BREAK.has(t))) {
2116
- return true;
2117
- }
2118
- }
2119
- return false;
2120
- }
2121
- /**
2122
- * Name of the binding a node's value flows into: the declared name of an
2123
- * enclosing `variable_declarator`, or the left-hand side of an enclosing
2124
- * `assignment_expression`. Returns "" if the node is not part of a binding
2125
- * (the walk stops at function/block/program boundaries so it never reaches
2126
- * out to an unrelated outer binding).
2127
- */
2128
- enclosingBindingName(node) {
2129
- let cur = node?.parent;
2130
- for (let depth = 0; cur && depth < 12; depth++) {
2131
- const t = cur.type;
2132
- if (t === "variable_declarator") {
2133
- const name = cur.children?.find((c) => c.isNamed);
2134
- return name?.text ?? "";
2135
- }
2136
- if (t === "assignment_expression") {
2137
- return cur.children?.[0]?.text ?? "";
2138
- }
2139
- if (t === "statement_block" ||
2140
- t === "program" ||
2141
- t === "function_declaration" ||
2142
- t === "function_expression" ||
2143
- t === "arrow_function" ||
2144
- t === "method_definition") {
2145
- return "";
2146
- }
2147
- cur = cur.parent;
2148
- }
2149
- return "";
2150
- }
2151
- /**
2152
- * Resolves `name` (as used in the *same file*) to a provably fixed URL:
2153
- * a `const` declarator whose initializer is a string literal, or a
2154
- * template literal with no `${...}` substitutions.
2155
- *
2156
- * This is deliberately conservative — naming convention (e.g.
2157
- * SCREAMING_SNAKE_CASE) proves nothing about provenance, so it is never
2158
- * consulted here. Any of the following makes resolution fail (and the
2159
- * caller must then treat the identifier as potentially tainted):
2160
- * - no declarator found for `name` in this file;
2161
- * - more than one declarator for `name` (ambiguous/shadowed — refuse
2162
- * rather than guess which one applies at the use site);
2163
- * - declared with `let`/`var` (not `const`);
2164
- * - the identifier is reassigned anywhere in the file
2165
- * (`name = ...`), even if the declaration itself is `const`-like in
2166
- * spirit — this also catches destructuring/compound-assignment
2167
- * edge cases conservatively since we only special-case a clean
2168
- * assignment_expression;
2169
- * - the initializer is not a plain string/no-substitution template
2170
- * literal (e.g. `process.env.X`, a function call, a member
2171
- * expression, another identifier).
2172
- */
2173
- resolvesToFileLiteralConst(name, rootNode) {
2174
- const valueNode = this.resolveFileConstValueNode(name, rootNode);
2175
- if (!valueNode)
2176
- return false;
2177
- return this.isFixedUrlLiteralExpr(valueNode);
2178
- }
2179
- /**
2180
- * Resolves `name` to the initializer value node of its *single, clean*
2181
- * file-local `const` declarator, or `null` when resolution must be refused.
2182
- *
2183
- * Refusal (returns `null`) on any of: no declarator; more than one
2184
- * declarator (shadowed/ambiguous — don't guess); a `let`/`var` binding for
2185
- * the same name anywhere; or a reassignment (`name = ...`) anywhere in the
2186
- * file. This is the shared, provenance-safe gate used by every "provably
2187
- * fixed value" check; callers inspect the returned value node themselves.
2188
- */
2189
- resolveFileConstValueNode(name, rootNode) {
2190
- const constDeclarators = [];
2191
- let hasNonConstBinding = false;
2192
- let hasReassignment = false;
2193
- const stack = [rootNode];
2194
- while (stack.length > 0) {
2195
- const node = stack.pop();
2196
- if (!node)
2197
- continue;
2198
- if (node.type === "variable_declarator") {
2199
- const nameNode = node.childForFieldName?.("name");
2200
- if (nameNode?.type === "identifier" && nameNode.text === name) {
2201
- const decl = node.parent;
2202
- const isConst = decl?.type === "lexical_declaration" &&
2203
- (decl.children ?? []).some((c) => c.type === "const");
2204
- if (isConst) {
2205
- constDeclarators.push(node);
2206
- }
2207
- else {
2208
- hasNonConstBinding = true;
2209
- }
2210
- }
2211
- }
2212
- else if (node.type === "assignment_expression" ||
2213
- node.type === "augmented_assignment_expression") {
2214
- const left = node.childForFieldName?.("left");
2215
- if (left?.type === "identifier" && left.text === name) {
2216
- hasReassignment = true;
2217
- }
2218
- else if ((left?.type === "member_expression" ||
2219
- left?.type === "subscript_expression") &&
2220
- left.childForFieldName?.("object")?.type === "identifier" &&
2221
- left.childForFieldName?.("object")?.text === name) {
2222
- // Property/subscript write to the bound receiver
2223
- // (`name.<prop> = …` / `name[…] = …`). Any origin/host/path/
2224
- // protocol/port/href mutation re-taints the destination after
2225
- // construction, so fail closed and treat it as a reassignment
2226
- // (#1008). The ONE safe exception is a query-string-only write
2227
- // (`name.search = …`), which never alters the origin;
2228
- // `name.searchParams.<method>(…)` is a call_expression (not an
2229
- // assignment) and so never reaches this branch, staying exempt.
2230
- const searchOnly = left.type === "member_expression" &&
2231
- left.childForFieldName?.("property")?.text === "search";
2232
- if (!searchOnly) {
2233
- hasReassignment = true;
2234
- }
2235
- }
2236
- }
2237
- for (const child of node.children ?? [])
2238
- stack.push(child);
2239
- }
2240
- // Ambiguous (shadowed/duplicated), reassigned anywhere, or backed by a
2241
- // non-const binding somewhere in the file: refuse to resolve.
2242
- if (hasNonConstBinding ||
2243
- hasReassignment ||
2244
- constDeclarators.length !== 1) {
2245
- return null;
2246
- }
2247
- return constDeclarators[0].childForFieldName?.("value") ?? null;
2248
- }
2249
- /**
2250
- * True when `node` is a self-contained fixed URL string: a plain string
2251
- * literal, or a template literal with no `${...}` substitutions.
2252
- */
2253
- isFixedUrlLiteralExpr(node) {
2254
- if (node.type === "string")
2255
- return true;
2256
- if (node.type === "template_string") {
2257
- return !(node.children ?? []).some((c) => c.type === "template_substitution");
2258
- }
2259
- return false;
2260
- }
2261
- /**
2262
- * True when `name` is a binding introduced by an `import` in this file
2263
- * (named/aliased/default/namespace). An import binding is immutable and its
2264
- * value is fixed at module-load time from source — it is never request- or
2265
- * attacker-scoped, so an imported base URL is treated as fixed. (Limitation:
2266
- * we cannot see the exporting module, so an imported value that is itself
2267
- * `process.env.X` in another file is not distinguished — an accepted, bounded
2268
- * gap; direct env/param/request taint at the sink still fires.)
2269
- */
2270
- isImportedBinding(name, rootNode) {
2271
- return this.importedAs(name, rootNode) !== null;
2272
- }
2273
- /**
2274
- * If `name` is an import binding, returns the *imported* name (e.g. `URL`
2275
- * for `import { URL as NodeURL }` → `importedAs("NodeURL") === "URL"`, and
2276
- * `importedAs("URL") === "URL"`). Returns `null` when `name` is not imported.
2277
- */
2278
- importedAs(name, rootNode) {
2279
- const stack = [rootNode];
2280
- while (stack.length > 0) {
2281
- const node = stack.pop();
2282
- if (!node)
2283
- continue;
2284
- if (node.type === "import_specifier") {
2285
- const nameNode = node.childForFieldName?.("name");
2286
- const aliasNode = node.childForFieldName?.("alias");
2287
- const local = (aliasNode ?? nameNode)?.text;
2288
- if (local === name)
2289
- return nameNode?.text ?? null;
2290
- }
2291
- else if (node.type === "namespace_import" ||
2292
- node.type === "import_clause") {
2293
- // `import X from …` / `import * as X from …` — default/namespace
2294
- // local binding is a direct identifier child.
2295
- for (const c of node.children ?? []) {
2296
- if (c.type === "identifier" && c.text === name)
2297
- return name;
2298
- }
2299
- }
2300
- for (const child of node.children ?? [])
2301
- stack.push(child);
2302
- }
2303
- return null;
2304
- }
2305
- /**
2306
- * True when `base` (the second argument of `new URL(path, base)`) is a
2307
- * provably fixed origin: a literal/substitution-free template, an identifier
2308
- * resolving to a file-local literal `const` or an import binding, or a
2309
- * template whose every `${…}` substitution is such an identifier. Anything
2310
- * else (function params, `process.env.X`, member expressions, calls) fails.
2311
- */
2312
- isFixedUrlBaseExpr(base, rootNode) {
2313
- if (base.type === "string")
2314
- return true;
2315
- if (base.type === "identifier") {
2316
- return this.isFixedBaseIdentifier(base, rootNode);
2317
- }
2318
- if (base.type === "template_string") {
2319
- for (const child of base.children ?? []) {
2320
- if (child.type !== "template_substitution")
2321
- continue;
2322
- const inner = (child.children ?? []).find((c) => c.isNamed);
2323
- if (!inner ||
2324
- inner.type !== "identifier" ||
2325
- !this.isFixedBaseIdentifier(inner, rootNode)) {
2326
- return false;
2327
- }
2328
- }
2329
- return true;
2330
- }
2331
- return false;
2332
- }
2333
- /**
2334
- * True when the identifier `ident` (used as, or inside, a `new URL` base) is
2335
- * a provably fixed origin AT ITS USE SITE. A file-local literal `const` is
2336
- * only trusted when no nearer binding shadows it: `resolveFileConstValueNode`
2337
- * already fails closed on any `let`/`var` of the same name and on multiple
2338
- * `const` declarators, but function/method PARAMETERS are not variable
2339
- * declarators and so slip past that gate — a request-tainted parameter base
2340
- * would otherwise be exempted merely because an unrelated same-named
2341
- * module-level `const` literal exists (#1008). So we additionally refuse the
2342
- * file-const path when an enclosing function on the path from the use site to
2343
- * the module root binds a parameter of the same name. Imported bindings stay
2344
- * trusted unconditionally (imported-base-as-fixed is sound; see #1000).
2345
- */
2346
- isFixedBaseIdentifier(ident, rootNode) {
2347
- const name = ident.text;
2348
- if (!this.isShadowedByEnclosingParam(ident, name) &&
2349
- this.resolvesToFileLiteralConst(name, rootNode)) {
2350
- return true;
2351
- }
2352
- return this.isImportedBinding(name, rootNode);
2353
- }
2354
- /**
2355
- * True when some function/method/arrow on the ancestor chain of `node` (up to
2356
- * the module root) binds a PARAMETER named `name` — i.e. `name` at `node`'s
2357
- * location resolves to a parameter, not to an outer `const`. Only binding
2358
- * positions are inspected (a parameter's `pattern`, including destructured
2359
- * bindings); default-value expressions (`= expr`) are uses, not bindings, and
2360
- * are skipped so an outer const referenced in a default is not mistaken for a
2361
- * shadow. Fail-closed bias: unknown parameter shapes that surface a matching
2362
- * identifier in a binding position are treated as a shadow.
2363
- */
2364
- isShadowedByEnclosingParam(node, name) {
2365
- const FUNCTION_TYPES = new Set([
2366
- "function_declaration",
2367
- "function_expression",
2368
- "generator_function",
2369
- "generator_function_declaration",
2370
- "arrow_function",
2371
- "method_definition",
2372
- ]);
2373
- let current = node.parent;
2374
- while (current) {
2375
- if (FUNCTION_TYPES.has(current.type)) {
2376
- // Bare-identifier arrow parameter: `name => …`.
2377
- const bare = current.childForFieldName?.("parameter");
2378
- if (bare?.type === "identifier" && bare.text === name)
2379
- return true;
2380
- const params = current.childForFieldName?.("parameters");
2381
- if (params && this.paramsBindName(params, name))
2382
- return true;
2383
- }
2384
- current = current.parent;
2385
- }
2386
- return false;
2387
- }
2388
- /** True when a `formal_parameters` node binds `name` in any binding position. */
2389
- paramsBindName(params, name) {
2390
- for (const param of params.children ?? []) {
2391
- if (!param.isNamed)
2392
- continue;
2393
- // required_parameter / optional_parameter carry the binding in `pattern`
2394
- // and the default (a use, not a binding) in `value`.
2395
- const pattern = param.type === "required_parameter" ||
2396
- param.type === "optional_parameter"
2397
- ? param.childForFieldName?.("pattern")
2398
- : param;
2399
- if (pattern && this.patternBindsName(pattern, name))
2400
- return true;
2401
- }
2402
- return false;
2403
- }
2404
- /**
2405
- * True when a binding pattern (`identifier`, or a destructuring
2406
- * object/array/rest pattern) introduces `name`. Walks the pattern but skips
2407
- * `assignment_pattern` default values (`= expr`), which are uses.
2408
- */
2409
- patternBindsName(pattern, name) {
2410
- if (pattern.type === "identifier")
2411
- return pattern.text === name;
2412
- const stack = [pattern];
2413
- while (stack.length > 0) {
2414
- const n = stack.pop();
2415
- if (!n)
2416
- continue;
2417
- if ((n.type === "identifier" ||
2418
- n.type === "shorthand_property_identifier_pattern") &&
2419
- n.text === name) {
2420
- return true;
2421
- }
2422
- if (n.type === "assignment_pattern") {
2423
- // Only the left (binding) side introduces names; skip the default.
2424
- const left = n.childForFieldName?.("left") ?? n.children?.[0];
2425
- if (left)
2426
- stack.push(left);
2427
- continue;
2428
- }
2429
- for (const c of n.children ?? [])
2430
- stack.push(c);
2431
- }
2432
- return false;
2433
- }
2434
- /**
2435
- * True when `name` resolves (same file) to a `const` initialized with
2436
- * `new URL(<literalPath>, <fixedBase>)` — a fully fixed destination origin
2437
- * and path. Query parameters added later via `url.searchParams.set(...)` do
2438
- * not alter origin/path, so they never taint the destination. The `URL`
2439
- * constructor may be imported under an alias (e.g. `NodeURL`).
2440
- */
2441
- resolvesToFixedNewUrlConst(name, rootNode) {
2442
- const value = this.resolveFileConstValueNode(name, rootNode);
2443
- if (!value || value.type !== "new_expression")
2444
- return false;
2445
- const ctor = value.childForFieldName?.("constructor");
2446
- const ctorName = ctor?.text ?? "";
2447
- const isUrlCtor = ctorName === "URL" ||
2448
- (ctor?.type === "identifier" &&
2449
- this.importedAs(ctorName, rootNode) === "URL");
2450
- if (!isUrlCtor)
2451
- return false;
2452
- const args = (value.childForFieldName?.("arguments")?.children ?? []).filter((c) => c.isNamed && c.type !== "comment");
2453
- if (args.length === 0)
2454
- return false;
2455
- // First arg (relative path / full URL) must be a literal.
2456
- if (!this.isFixedUrlLiteralExpr(args[0]))
2457
- return false;
2458
- // Single-arg form: `new URL("https://host/path")` — already fully fixed.
2459
- if (args.length === 1)
2460
- return true;
2461
- // Two-arg form: base must resolve to a fixed origin.
2462
- return this.isFixedUrlBaseExpr(args[1], rootNode);
2463
- }
2464
- /**
2465
- * For a fetch URL argument of the form `u.toString()` (call_expression) or
2466
- * `u.href` (member_expression), returns the receiver identifier name `u`
2467
- * (only for the `toString`/`href` accessors a `URL` yields). Returns `null`
2468
- * for any other shape so the caller falls through to taint heuristics.
2469
- */
2470
- newUrlBaseVarName(urlNode) {
2471
- let member;
2472
- if (urlNode.type === "call_expression") {
2473
- member = urlNode.childForFieldName?.("function");
2474
- }
2475
- else if (urlNode.type === "member_expression") {
2476
- member = urlNode;
2477
- }
2478
- if (!member || member.type !== "member_expression")
2479
- return null;
2480
- const prop = member.childForFieldName?.("property")?.text;
2481
- if (prop !== "toString" && prop !== "href")
2482
- return null;
2483
- const object = member.childForFieldName?.("object");
2484
- if (object?.type !== "identifier")
2485
- return null;
2486
- return object.text;
2487
- }
2488
- /**
2489
- * `session.execute(select(...))` and friends pass a statement OBJECT, not a
2490
- * SQL string: parameterized by construction, and far too noisy as blockers.
2491
- */
2492
- isSafeSqlAlchemyExpressionCall(node) {
2493
- if (node.type !== "call")
2494
- return false;
2495
- const callee = node.children?.[0]?.text ?? "";
2496
- const expression = node.text;
2497
- for (const name of PYTHON_SQLALCHEMY_STATEMENT_BUILDERS) {
2498
- if (callee === name || expression.startsWith(`${name}(`))
2499
- return true;
2500
- }
2501
- return false;
2502
- }
2503
- /**
2504
- * Post-filter predicate: returns true if the match should be kept, false to skip.
2505
- * Each branch is an independent filter identified by name — flat dispatch, no nesting.
2506
- */
2507
- // biome-ignore lint/suspicious/noExplicitAny: postFilterParams is untyped per-filter config
2508
- applyPostFilter(postFilter, postFilterParams, captures, rootNode) {
2509
- /**
2510
- * Extract the list of declared slot names from a class_definition's
2511
- * `__slots__` assignment. Returns:
2512
- * - `null` if the class has no `__slots__` declaration
2513
- * - an array of slot names (strings) otherwise
2514
- *
2515
- * Handles both common shapes:
2516
- * - string tuple/list: `__slots__ = ("a", "b")` or `['a', 'b']`
2517
- * - single string: `__slots__ = "a"` (Python's quirky single-slot form)
2518
- * - parent inheritance: returns null (we don't follow MRO)
2519
- */
2520
- function extractSlots(classNode) {
2521
- const classText = classNode.text ?? "";
2522
- if (!classText.includes("__slots__"))
2523
- return null;
2524
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
2525
- const body = classNode.children?.find((c) => c.type === "block");
2526
- if (!body)
2527
- return null;
2528
- const slots = [];
2529
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
2530
- for (const stmt of body.children ?? []) {
2531
- if (stmt.type !== "expression_statement")
2532
- continue;
2533
- // biome-ignore lint/suspicious/noExplicitAny: AST traversal
2534
- const assignment = stmt.children?.find((c) => c.type === "assignment");
2535
- if (!assignment)
2536
- continue;
2537
- // biome-ignore lint/suspicious/noExplicitAny: LHS check
2538
- // LHS text may include a leading whitespace token from the AST
2539
- // (tree-sitter separates the space before the LHS identifier).
2540
- const lhsText = (assignment.children?.[0]?.text ?? "").trim();
2541
- if (lhsText !== "__slots__")
2542
- continue;
2543
- // biome-ignore lint/suspicious/noExplicitAny: RHS extraction
2544
- // children layout: [LHS identifier, `=` operator, RHS expression]
2545
- const rhs = assignment.children?.[2];
2546
- if (!rhs)
2547
- continue;
2548
- if (rhs.type === "string") {
2549
- // __slots__ = "a" — single string form (Python quirk)
2550
- const s = (rhs.text ?? "").replace(/^["']|["']$/g, "");
2551
- if (s)
2552
- slots.push(s);
2553
- }
2554
- else if (rhs.type === "tuple" || rhs.type === "list") {
2555
- // biome-ignore lint/suspicious/noExplicitAny: list element extraction
2556
- for (const el of rhs.children ?? []) {
2557
- if (!el.isNamed)
2558
- continue; // skip "," punctuation
2559
- if (el.type === "string") {
2560
- slots.push((el.text ?? "").replace(/^["']|["']$/g, ""));
2561
- }
2562
- }
2563
- }
2564
- break; // first __slots__ wins
2565
- }
2566
- return slots;
2567
- }
2568
- switch (postFilter) {
2569
- case "no_nested_anchor_chain": {
2570
- // Tree-sitter queries can express the opening-tag shape, but they
2571
- // cannot express both an arbitrary-depth descendant relationship and
2572
- // an ancestor exclusion. Walk the captured JSX element here so the
2573
- // TSX path agrees with the ast-grep rule: keep exactly the outermost
2574
- // anchor that contains another anchor, including through wrappers.
2575
- // This post-filter runs inside a batched query walk. It must never
2576
- // abort that walk or turn a malformed tree into a false negative.
2577
- try {
2578
- const outer = captures.OUTER_ELEMENT;
2579
- if (!outer) {
2580
- this.reportPostFilterFailure(postFilter, "missing OUTER_ELEMENT capture");
2581
- return true;
2582
- }
2583
- const isAnchorElement = (node) => {
2584
- if (node.type !== "jsx_element")
2585
- return false;
2586
- const opening = node.childForFieldName?.("open_tag") ??
2587
- node.children?.find((child) => child.type === "jsx_opening_element");
2588
- const name = opening?.childForFieldName?.("name") ??
2589
- opening?.children?.find((child) => child.type === "identifier");
2590
- return name?.text === "a";
2591
- };
2592
- let visited = 0;
2593
- let ancestor = outer.parent;
2594
- while (ancestor) {
2595
- if (++visited > NO_NESTED_ANCHOR_VISIT_CAP) {
2596
- this.reportPostFilterFailure(postFilter, `ancestor walk exceeded ${NO_NESTED_ANCHOR_VISIT_CAP} nodes`);
2597
- return true;
2598
- }
2599
- if (isAnchorElement(ancestor))
2600
- return false;
2601
- ancestor = ancestor.parent;
2602
- }
2603
- const pending = [...(outer.children ?? [])];
2604
- while (pending.length > 0) {
2605
- const node = pending.pop();
2606
- if (!node)
2607
- continue;
2608
- if (++visited > NO_NESTED_ANCHOR_VISIT_CAP) {
2609
- this.reportPostFilterFailure(postFilter, `descendant walk exceeded ${NO_NESTED_ANCHOR_VISIT_CAP} nodes`);
2610
- return true;
2611
- }
2612
- if (isAnchorElement(node))
2613
- return true;
2614
- pending.push(...(node.children ?? []));
2615
- }
2616
- return false;
2617
- }
2618
- catch (error) {
2619
- const reason = error instanceof Error ? error.message : String(error);
2620
- this.reportPostFilterFailure(postFilter, `tree walk failed${reason ? `: ${reason}` : ""}`);
2621
- return true;
2622
- }
2623
- }
2624
- case "differs_only_by_case": {
2625
- try {
2626
- const field = captures.FIELD?.text ?? "";
2627
- const method = captures.METHOD?.text ?? "";
2628
- return (field !== method &&
2629
- field.toLocaleLowerCase() === method.toLocaleLowerCase());
2630
- }
2631
- catch {
2632
- return true;
2633
- }
2634
- }
2635
- case "in_default_package": {
2636
- try {
2637
- if (!rootNode)
2638
- return true;
2639
- return !(rootNode.children ?? []).some((node) => node.type === "package_declaration");
2640
- }
2641
- catch {
2642
- return true;
2643
- }
2644
- }
2645
- case "missing_mimetype_and_download_name": {
2646
- try {
2647
- const firstArg = captures.FIRST_ARG;
2648
- if (!firstArg)
2649
- return true;
2650
- // Flask can infer a MIME type from a filesystem path. The warning
2651
- // is for file-like objects, where it needs an explicit MIME type or
2652
- // download name.
2653
- if (firstArg.type === "string")
2654
- return false;
2655
- const args = firstArg.parent;
2656
- if (!args)
2657
- return true;
2658
- return !(args.children ?? []).some((node) => {
2659
- if (node.type !== "keyword_argument")
2660
- return false;
2661
- const name = node.childForFieldName?.("name")?.text;
2662
- return name === "mimetype" || name === "download_name";
2663
- });
2664
- }
2665
- catch {
2666
- return true;
2667
- }
2668
- }
2669
- case "missing_super_call": {
2670
- try {
2671
- const body = captures.BODY;
2672
- const method = captures.METHOD?.text ?? "";
2673
- if (!body || !method)
2674
- return true;
2675
- const stack = [body];
2676
- for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
2677
- const node = stack.pop();
2678
- if (!node)
2679
- break;
2680
- if (node.type === "method_invocation" &&
2681
- node.childForFieldName?.("object")?.text === "super" &&
2682
- node.childForFieldName?.("name")?.text === method) {
2683
- return false;
2684
- }
2685
- stack.push(...(node.children ?? []));
2686
- }
2687
- return true;
2688
- }
2689
- catch {
2690
- return true;
2691
- }
2692
- }
2693
- case "no_assertion_call": {
2694
- try {
2695
- const body = captures.BODY;
2696
- if (!body)
2697
- return true;
2698
- const assertionNames = /^(?:assert\w*|fail|verify|expect|check|assume\w*)$/i;
2699
- const stack = [body];
2700
- for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
2701
- const node = stack.pop();
2702
- if (!node)
2703
- break;
2704
- if (node.type === "method_invocation") {
2705
- const name = node.childForFieldName?.("name")?.text ?? "";
2706
- if (assertionNames.test(name))
2707
- return false;
2708
- }
2709
- stack.push(...(node.children ?? []));
2710
- }
2711
- return true;
2712
- }
2713
- catch {
2714
- return true;
2715
- }
2716
- }
2717
- case "not_closed_or_try_with_resources": {
2718
- try {
2719
- const declaration = captures.DECL;
2720
- const resource = captures.RESOURCE?.text ?? "";
2721
- if (!declaration || !resource)
2722
- return true;
2723
- // (#956 review: the old resource_specification ANCESTOR walk was
2724
- // dead code — the query only matches local_variable_declaration,
2725
- // which the Java grammar never places inside a try header. The
2726
- // real try-with-resources shapes are handled in the scope scan
2727
- // below: a `resource` node naming this variable covers both
2728
- // `try (Type x = …)` and the Java 9 `try (x)` form.)
2729
- const scope = this.navigator.findParent(declaration, [
2730
- "method_declaration",
2731
- "constructor_declaration",
2732
- "block",
2733
- ]) ?? rootNode;
2734
- if (!scope)
2735
- return true;
2736
- // `\b` in a template literal is the BACKSPACE control char
2737
- // (U+0008), not a regex word-boundary escape — that requires
2738
- // the literal two-character sequence `\\b`. The identifier
2739
- // itself must also be regex-escaped (#1089 P2).
2740
- const resourceWord = new RegExp(`\\b${escapeRegExp(resource)}\\b`);
2741
- const stack = [scope];
2742
- for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
2743
- const node = stack.pop();
2744
- if (!node)
2745
- break;
2746
- if (node.type === "method_invocation" &&
2747
- node.childForFieldName?.("object")?.text === resource &&
2748
- node.childForFieldName?.("name")?.text === "close") {
2749
- return false;
2750
- }
2751
- if (node.type === "resource" &&
2752
- resourceWord.test(node.text ?? "")) {
2753
- return false;
2754
- }
2755
- stack.push(...(node.children ?? []));
2756
- }
2757
- return true;
2758
- }
2759
- catch {
2760
- return true;
2761
- }
2762
- }
2763
- case "same_method_no_base_case": {
2764
- try {
2765
- const method = captures.NAME?.text ?? "";
2766
- if (!method || captures.RECURSE?.text !== method)
2767
- return false;
2768
- const call = captures.CALL;
2769
- const declaration = call
2770
- ? this.navigator.findParent(call, ["method_declaration"])
2771
- : undefined;
2772
- if (!declaration)
2773
- return true;
2774
- const stack = [declaration];
2775
- for (let visited = 0; stack.length > 0 && visited < 10_000; visited++) {
2776
- const node = stack.pop();
2777
- if (!node)
2778
- break;
2779
- // Any conditional or loop construct is a plausible base-case
2780
- // guard (#956 review: while/for-guarded recursion and ternary
2781
- // base cases are common). This errs toward suppressing a
2782
- // blocking diagnostic, never inventing one.
2783
- if (node.type === "if_statement" ||
2784
- node.type === "switch_expression" ||
2785
- node.type === "switch_statement" ||
2786
- node.type === "while_statement" ||
2787
- node.type === "do_statement" ||
2788
- node.type === "for_statement" ||
2789
- node.type === "enhanced_for_statement" ||
2790
- node.type === "ternary_expression") {
2791
- return false;
2792
- }
2793
- stack.push(...(node.children ?? []));
2794
- }
2795
- // Cap exhausted without a verdict: this rule is BLOCKING, so a
2796
- // >10k-node method whose guard sits beyond the budget must not
2797
- // become a silent blocking FP — suppress instead (#956 review;
2798
- // the advisory filters keep their keep-the-diagnostic default).
2799
- return stack.length > 0 ? false : true;
2800
- }
2801
- catch {
2802
- return true;
2803
- }
2804
- }
2805
- case "memset_for_sensitive_data": {
2806
- try {
2807
- const destination = captures.DEST?.text ?? "";
2808
- const value = captures.VALUE?.text?.trim() ?? "";
2809
- if (!/^(?:0|0x0+|NULL|nullptr)$/.test(value))
2810
- return false;
2811
- return /(?:pass(?:word)?|passwd|secret|token|api_?key|private_?key|credential|auth|pin)/i.test(destination);
2812
- }
2813
- catch {
2814
- return true;
2815
- }
2816
- }
2817
- case "unsafe_regex_dynamic_identifier": {
2818
- // unsafe-regex is intentionally a coarse advisory heuristic. When the
2819
- // interpolation is a plain identifier, recover the common safe-before-
2820
- // assignment pattern without pretending to perform whole-program
2821
- // dataflow. #949 review hardening: the LAST write (declarator or
2822
- // assignment) textually BEFORE the use decides — a later reassignment
2823
- // to user input un-suppresses, and an unrelated declaration after the
2824
- // use never suppresses. Initializers come from the "value" field (a
2825
- // type annotation is not an initializer), and bare .replace("a","b")
2826
- // is not escaping — only escape-named callees or .replace(/regex/
2827
- // count. Any internal error keeps the diagnostic: never silently
2828
- // suppress via a filter crash, and a throw here would abort the whole
2829
- // file's batch.
2830
- try {
2831
- const interpolationNode = captures.INTERPOLATION;
2832
- const interpolation = interpolationNode?.text ?? "";
2833
- const identifier = interpolation.match(/^\$\{\s*([A-Za-z_$][\w$]*)\s*\}$/)?.[1];
2834
- if (!identifier || !rootNode || !interpolationNode)
2835
- return true;
2836
- const useRow = interpolationNode.startPosition.row;
2837
- const hasEscapeSignal = (text) => /\b(?:escape|Escape)\w*\s*\(/.test(text) ||
2838
- /\.\s*replace\s*\(\s*\//.test(text);
2839
- // Track the last write to the identifier before the use site.
2840
- let lastWriteRow = -1;
2841
- let lastWriteSafe = false;
2842
- const consider = (row, valueText) => {
2843
- if (row >= useRow || row < lastWriteRow)
2844
- return;
2845
- lastWriteRow = row;
2846
- lastWriteSafe =
2847
- valueText !== undefined && hasEscapeSignal(valueText);
2848
- };
2849
- const stack = [rootNode];
2850
- while (stack.length > 0) {
2851
- const node = stack.pop();
2852
- if (!node)
2853
- continue;
2854
- if (node.type === "variable_declarator") {
2855
- const name = node.childForFieldName?.("name");
2856
- if (name?.text === identifier) {
2857
- const initializer = node.childForFieldName?.("value");
2858
- consider(node.startPosition.row, initializer?.text);
2859
- }
2860
- }
2861
- else if (node.type === "assignment_expression") {
2862
- const left = node.childForFieldName?.("left");
2863
- if (left?.type === "identifier" && left.text === identifier) {
2864
- const right = node.childForFieldName?.("right");
2865
- consider(node.startPosition.row, right?.text);
2866
- }
2867
- }
2868
- stack.push(...node.children);
2869
- }
2870
- return lastWriteRow >= 0 && lastWriteSafe ? false : true;
2871
- }
2872
- catch {
2873
- return true;
2874
- }
2875
- }
2876
- case "is_generator_with_valued_return": {
2877
- const returnNode = captures.RETURN;
2878
- const functionNode = captures.FUNCTION ??
2879
- (returnNode
2880
- ? this.navigator.findParent(returnNode, ["function_definition"])
2881
- : undefined);
2882
- if (!functionNode)
2883
- return false;
2884
- // In the Python grammar, `async def` is also a function_definition with
2885
- // an anonymous `async` child. Coroutines may return values normally;
2886
- // only synchronous generator functions should be flagged.
2887
- if (this.hasChildToken(functionNode, "async"))
2888
- return false;
2889
- return this.containsYieldInFunctionBody(functionNode);
2890
- }
2891
- case "count_params": {
2892
- const paramsNode = captures.PARAMS;
2893
- if (!paramsNode)
2894
- return true;
2895
- // Count only truly required params — exclude:
2896
- // • optional_parameter nodes (foo?: T) if the grammar uses that type
2897
- // • required_parameter nodes that have a "?" child (same semantic,
2898
- // different grammar version — web-tree-sitter-typescript collapses
2899
- // both into required_parameter with a "?" token child)
2900
- // • params with a default value (text contains "=")
2901
- // biome-ignore lint/suspicious/noExplicitAny: Count parameter nodes
2902
- const paramCount = paramsNode.children.filter((c) => {
2903
- if (c.type !== "required_parameter")
2904
- return false;
2905
- if (c.text.includes("="))
2906
- return false;
2907
- // biome-ignore lint/suspicious/noExplicitAny: child node check
2908
- if (c.children?.some((ch) => ch.text === "?"))
2909
- return false;
2910
- return true;
2911
- }).length;
2912
- return paramCount >= (postFilterParams?.min_params ?? 6);
2913
- }
2914
- case "empty_body": {
2915
- const bodyNode = captures.BODY;
2916
- if (!bodyNode)
2917
- return true;
2918
- // biome-ignore lint/suspicious/noExplicitAny: Check for meaningful statements
2919
- const meaningful = bodyNode.children.filter((c) => c.isNamed &&
2920
- c.type !== "comment" &&
2921
- c.type !== "line_comment" &&
2922
- c.type !== "block_comment");
2923
- return meaningful.length === 0;
2924
- }
2925
- case "is_empty_block": {
2926
- // empty-switch-case: keep a `switch_case` that has no body statements.
2927
- // A switch_case's named children are `[value, ...body statements]`
2928
- // (the case's statements are direct children, not a statement_block),
2929
- // so an empty case is one whose only named child is its `case <value>`.
2930
- // An empty case followed by another label is a fall-through group, not
2931
- // a dead case — only the last label of a group carries the body.
2932
- const caseNode = captures.CASE;
2933
- if (!caseNode)
2934
- return false;
2935
- if (this.switchCaseBodyStatements(caseNode).length > 0)
2936
- return false;
2937
- return !this.nextSiblingIsSwitchLabel(caseNode);
2938
- }
2939
- case "no_break_or_return_in_body": {
2940
- // infinite-loop: keep a `while(true)`/`for(;;)` whose body contains no
2941
- // statement that can terminate the loop (break/return/throw). `continue`
2942
- // does not count — it keeps the loop running.
2943
- const bodyNode = captures.BODY;
2944
- if (!bodyNode)
2945
- return false;
2946
- return !this.bodyHasLoopExit(bodyNode, false);
2947
- }
2948
- case "same_param_name": {
2949
- // duplicate-function-arg: keep the pair when the captured binding
2950
- // patterns share any name.
2951
- const first = this.bindingNames(captures.PARAM1);
2952
- const second = this.bindingNames(captures.NAME);
2953
- return [...first].some((name) => second.has(name));
2954
- }
2955
- case "no_terminating_statement": {
2956
- // switch-case-termination: keep a non-empty case (already known to be
2957
- // followed by another case via the query) whose last body statement
2958
- // does not terminate, i.e. it falls through. Empty cases are handled
2959
- // by empty-switch-case, so require at least one body statement here.
2960
- // An intentional `// fallthrough` marker suppresses the finding.
2961
- const caseNode = captures.CASE;
2962
- if (!caseNode)
2963
- return false;
2964
- if (this.hasFallthroughMarker(caseNode))
2965
- return false;
2966
- const body = this.switchCaseBodyStatements(caseNode);
2967
- if (body.length === 0)
2968
- return false;
2969
- return !this.statementTerminates(body[body.length - 1]);
2970
- }
2971
- case "no_break_or_return": {
2972
- // infinite-loop-java: keep a `while(true)`/`for(;;)` whose body has no
2973
- // break/return/throw reachable in this loop's scope.
2974
- const bodyNode = captures.BODY;
2975
- if (!bodyNode)
2976
- return false;
2977
- return !this.bodyHasLoopExit(bodyNode, false);
2978
- }
2979
- case "is_double_checked_locking": {
2980
- // no-double-checked-locking: the query pins the if→synchronized→if
2981
- // shape; keep it only when both null-checks test the same field.
2982
- const outer = captures.FIELD?.text;
2983
- const inner = captures.FIELD2?.text;
2984
- return !!outer && outer === inner;
2985
- }
2986
- case "shadows_parent_field": {
2987
- // no-field-shadowing: keep only when the named parent class is
2988
- // declared in the same file and it declares a field with the same
2989
- // name. Cross-file inheritance can't be resolved here — fail closed.
2990
- const parentName = captures.PARENT?.text;
2991
- const fieldName = captures.NAME?.text;
2992
- if (!parentName || !fieldName)
2993
- return false;
2994
- // biome-ignore lint/suspicious/noExplicitAny: AST traversal
2995
- let root = captures.NAME;
2996
- while (root.parent)
2997
- root = root.parent;
2998
- // biome-ignore lint/suspicious/noExplicitAny: AST traversal
2999
- const stack = [root];
3000
- while (stack.length) {
3001
- const node = stack.pop();
3002
- if (node.type === "class_declaration" &&
3003
- node.childForFieldName?.("name")?.text === parentName) {
3004
- const body = node.childForFieldName?.("body");
3005
- for (const member of body?.children ?? []) {
3006
- if (member.type !== "field_declaration")
3007
- continue;
3008
- for (const declarator of member.children ?? []) {
3009
- if (declarator.type === "variable_declarator" &&
3010
- declarator.childForFieldName?.("name")?.text === fieldName) {
3011
- return true;
3012
- }
3013
- }
3014
- }
3015
- }
3016
- for (const child of node.children ?? [])
3017
- stack.push(child);
3018
- }
3019
- return false;
3020
- }
3021
- case "missing_break_between_cases": {
3022
- // switch-fall-through: keep when the labeled statement group contains
3023
- // no terminating statement at all — its control flow reaches the next
3024
- // case. The query already requires a following group.
3025
- const label = captures.LABEL;
3026
- const group = label?.parent;
3027
- if (!group)
3028
- return false;
3029
- const TERMINATORS = new Set([
3030
- "break_statement",
3031
- "return_statement",
3032
- "throw_statement",
3033
- "continue_statement",
3034
- "yield_statement",
3035
- ]);
3036
- for (const child of group.children ?? []) {
3037
- if (TERMINATORS.has(child.type))
3038
- return false;
3039
- }
3040
- return true;
3041
- }
3042
- case "scoped_lock_empty_args": {
3043
- // no-scoped-lock-without-args: the query only matches a bare
3044
- // `declarator: (identifier)` — an arg-taking declaration parses as an
3045
- // init/function declarator instead — so a match IS the defect. Keep it
3046
- // as long as the captured declarator has no sibling argument list.
3047
- const decl = captures.DECL;
3048
- if (!decl)
3049
- return false;
3050
- for (const sibling of decl.parent?.children ?? []) {
3051
- if (sibling.type === "argument_list")
3052
- return false;
3053
- }
3054
- return true;
3055
- }
3056
- case "calc_missing_spaces": {
3057
- // calc-spacing: keep a calc() whose +/- has an operand directly on
3058
- // both sides (e.g. `100%-20px`). A leading sign after `(` or a comma
3059
- // is legitimate and stays allowed.
3060
- const text = captures.EXPR?.text ?? "";
3061
- return /[\w%)][+-][\w.(]/.test(text);
3062
- }
3063
- case "bare_except_only": {
3064
- const clauseNode = captures.CLAUSE;
3065
- if (!clauseNode)
3066
- return true;
3067
- // A typed `except` clause has a named child for the exception
3068
- // spec — one of: identifier (e.g. `except ValueError`),
3069
- // attribute (e.g. `except asyncio.TimeoutError` — dotted name),
3070
- // tuple (e.g. `except (E, F)`), as_pattern (e.g. `except E as e`),
3071
- // parenthesized_expression (e.g. `except (E)`), or subscript
3072
- // (e.g. `except dict[str, int]` — #1244, mirrors the ast-grep
3073
- // rule's shape space).
3074
- // Bare `except:` has NO named children (just the `except` keyword,
3075
- // the `:` colon, and the body block).
3076
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
3077
- const hasExceptionSpec = clauseNode.children.some((c) => {
3078
- if (!c.isNamed)
3079
- return false;
3080
- return c.type !== "block";
3081
- });
3082
- // Fire ONLY when bare (no exception spec)
3083
- return !hasExceptionSpec;
3084
- }
3085
- case "eq_mod_fn": {
3086
- // Belt-and-braces re-check of the #eq? predicates that
3087
- // evaluatePredicates already enforces (clients/tree-sitter-client.ts).
3088
- // This filter re-applies the #eq? checks at post_filter time.
3089
- const mod = captures.MOD?.text ?? "";
3090
- const fn = captures.FN?.text ?? "";
3091
- return mod === "threading" && fn === "Thread";
3092
- }
3093
- case "regex_first_arg_identifier": {
3094
- // Belt-and-braces re-check of the #eq?/#match? predicates that
3095
- // evaluatePredicates already enforces, plus the first-argument
3096
- // check that predicates can't express. This post_filter re-applies
3097
- // both predicate checks AND the first-argument check:
3098
- // 1. MOD must be "re" (would-be #eq? @MOD "re")
3099
- // 2. FUNC must match the regex method pattern (#match? @FUNC ...)
3100
- // 3. First arg must be an identifier (dynamic pattern)
3101
- // String literals (r"...", "...") are safe static patterns.
3102
- const mod = captures.MOD?.text ?? "";
3103
- if (mod !== "re")
3104
- return false;
3105
- const func = captures.FUNC?.text ?? "";
3106
- if (!/^(compile|match|search|fullmatch|findall|finditer|sub|subn|split)$/.test(func)) {
3107
- return false;
3108
- }
3109
- const argsNode = captures.ARGS;
3110
- if (!argsNode)
3111
- return false;
3112
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
3113
- const firstNamed = (argsNode.children ?? []).find((c) => c.isNamed);
3114
- if (!firstNamed)
3115
- return false;
3116
- return firstNamed.type === "identifier";
3117
- }
3118
- case "open_mode_invalid": {
3119
- const modeNode = captures.MODE;
3120
- if (!modeNode)
3121
- return false;
3122
- // Python's open() mode accepts: r, w, a, x (basic), b/t/+ (suffix).
3123
- // Strip surrounding quotes from the string literal text.
3124
- const text = modeNode.text ?? "";
3125
- const stripped = text.replace(/^["']|["']$/g, "");
3126
- // Skip empty mode (defaults to 'r')
3127
- if (stripped.length === 0)
3128
- return false;
3129
- // Skip single-char modes (r/w/a/x — always valid)
3130
- if (stripped.length === 1)
3131
- return false;
3132
- // Must contain only valid characters
3133
- if (!/^[rwxabt+]+$/.test(stripped))
3134
- return true;
3135
- // Multi-char must be exactly: basic + optional (b|t) + optional +
3136
- // Examples valid: "rb", "rb+", "r+", "ab", "rt"
3137
- // Examples invalid: "rwb", "rrr", "rw", "rbb" (no + between r and w is invalid)
3138
- // The "rw" case (basic mode followed by another basic mode without +) is invalid
3139
- // Allow: [basic][bt]?[+]
3140
- const validShape = /^[rwax][bt]?\+?$/;
3141
- if (!validShape.test(stripped))
3142
- return true;
3143
- return false;
3144
- }
3145
- case "status_204_with_value_return": {
3146
- const funcNode = captures.FUNC;
3147
- const valNode = captures.VAL;
3148
- if (!funcNode || !valNode)
3149
- return false;
3150
- // Only fire if status_code=204
3151
- if (Number(valNode.text ?? 0) !== 204)
3152
- return false;
3153
- // Walk the function subtree looking for return_statement nodes.
3154
- // Manual BFS because web-tree-sitter doesn't expose
3155
- // descendantsOfType directly.
3156
- // biome-ignore lint/suspicious/noExplicitAny: tree-sitter node iteration
3157
- const queue = [funcNode];
3158
- while (queue.length > 0) {
3159
- const node = queue.shift();
3160
- if (node.type === "return_statement") {
3161
- // Has a value child (not just the `return` keyword)
3162
- // biome-ignore lint/suspicious/noExplicitAny: child check
3163
- const hasValue = node.children.some((c) => c.isNamed && c.type !== "comment");
3164
- if (hasValue)
3165
- return true;
3166
- }
3167
- // biome-ignore lint/suspicious/noExplicitAny: child queue
3168
- if (node.children)
3169
- queue.push(...node.children);
3170
- }
3171
- return false;
3172
- }
3173
- case "has_mixed_async": {
3174
- const bodyNode = captures.BODY;
3175
- if (!bodyNode)
3176
- return true;
3177
- const bodyText = bodyNode.text;
3178
- return (bodyText.includes("await") && /\.\s*(then|catch)\s*\(/.test(bodyText));
3179
- }
3180
- case "format_arity_mismatch": {
3181
- const formatNode = captures.FORMAT;
3182
- const argsNode = captures.ARGS;
3183
- if (!formatNode || !argsNode)
3184
- return false;
3185
- // Strip quotes from format string
3186
- const fmtText = (formatNode.text ?? "").replace(/^["']|["']$/g, "");
3187
- // Don't strip a leading "%" — the format string's contents are
3188
- // intact after stripping only the surrounding quotes. The original
3189
- // code stripped the first "%" thinking it was the operator, but
3190
- // the operator is a separate binary_operator node, not part of
3191
- // the string literal's text.
3192
- const fmt = fmtText;
3193
- // Count placeholders: %s, %d, %f, %(name)s, %i, etc.
3194
- // The simple %s/%d style: each %X counts as 1
3195
- // The %(name)s style: counts as 1 with name
3196
- // The %% escape: doesn't count
3197
- let placeholderCount = 0;
3198
- const namedKeys = [];
3199
- // biome-ignore lint/suspicious/noExplicitAny: regex match
3200
- const positionalRegex = /%(?:\([^)]+\))?[#0\- +]*\d*(?:\.\d+)?[hlL]?[diouxXeEfFgGcrs%]/g;
3201
- // biome-ignore lint/suspicious/noExplicitAny: regex match
3202
- const positionalMatches = fmt.match(positionalRegex) ?? [];
3203
- for (const m of positionalMatches) {
3204
- if (m === "%%")
3205
- continue;
3206
- placeholderCount++;
3207
- // biome-ignore lint/suspicious/noExplicitAny: capture group
3208
- const namedMatch = m.match(/^%\(([^)]+)\)/);
3209
- if (namedMatch)
3210
- namedKeys.push(namedMatch[1]);
3211
- }
3212
- // If format uses named placeholders, RHS should be a dict
3213
- if (namedKeys.length > 0) {
3214
- // Check if dict contains all named keys
3215
- if (argsNode.type === "dictionary") {
3216
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
3217
- const dictKeys = [];
3218
- for (const child of argsNode.children ?? []) {
3219
- // biome-ignore lint/suspicious/noExplicitAny: child check
3220
- if (child.type === "pair" && child.children?.[0]) {
3221
- // biome-ignore lint/suspicious/noExplicitAny: child text
3222
- // Strip quotes — child is a string literal node,
3223
- // text includes the surrounding "...".
3224
- dictKeys.push((child.children[0].text ?? "").replace(/^["']|["']$/g, ""));
3225
- }
3226
- }
3227
- const missing = namedKeys.filter((k) => !dictKeys.includes(k));
3228
- return missing.length > 0;
3229
- }
3230
- // Format uses named but RHS isn't a dict — definitely wrong
3231
- return true;
3232
- }
3233
- // Positional: count tuple args
3234
- if (argsNode.type === "tuple") {
3235
- const argCount = (argsNode.children ?? []).filter((c) => c.isNamed).length;
3236
- if (argCount !== placeholderCount)
3237
- return true;
3238
- }
3239
- return false;
3240
- }
3241
- case "aws_policy_public": {
3242
- const policyNode = captures.POLICY;
3243
- if (!policyNode)
3244
- return false;
3245
- const text = policyNode.text ?? "";
3246
- // Match patterns indicating public access
3247
- const patterns = [
3248
- /"Principal"\s*:\s*"\*"/, // direct wildcard
3249
- /"Principal"\s*:\s*\{\s*"AWS"\s*:\s*"\*"\s*\}/, // AWS wildcard
3250
- /"Effect"\s*:\s*"Allow"[\s\S]*?"Action"\s*:\s*"\*"[\s\S]*?"Resource"\s*:\s*"\*"/, // full admin
3251
- /"Principal"\s*:\s*"\*"/,
3252
- ];
3253
- return patterns.some((p) => p.test(text));
3254
- }
3255
- case "slots_attribute_mismatch": {
3256
- const selfNode = captures.SELF;
3257
- const attrNode = captures.ATTR;
3258
- const methodNode = captures.METHOD;
3259
- if (!selfNode || !attrNode || !methodNode)
3260
- return false;
3261
- // Only consider self.X = (not other.X)
3262
- if (selfNode.text !== "self")
3263
- return false;
3264
- const attrName = attrNode.text ?? "";
3265
- // Find parent class_definition
3266
- // biome-ignore lint/suspicious/noExplicitAny: AST navigation
3267
- let parent = methodNode.parent;
3268
- while (parent && parent.type !== "class_definition") {
3269
- parent = parent.parent;
3270
- }
3271
- if (!parent)
3272
- return false;
3273
- // Parse the class's __slots__ list and check if attrName is in it.
3274
- // Fires ONLY when self.X = ... assigns to an attribute NOT in __slots__
3275
- // (a real S8494 violation — the assignment will raise AttributeError).
3276
- const slots = extractSlots(parent);
3277
- // null = no __slots__ declared in this class. [] = __slots__ declared
3278
- // but we couldn't parse it (treat as null to avoid FPs on inner-class
3279
- // parent walks where the parent text mentions __slots__ but the
3280
- // direct children don't contain the assignment).
3281
- if (slots === null || slots.length === 0)
3282
- return false;
3283
- return !slots.includes(attrName);
3284
- }
3285
- case "special_method_arity": {
3286
- const nameNode = captures.NAME;
3287
- const paramsNode = captures.PARAMS;
3288
- if (!nameNode || !paramsNode)
3289
- return false;
3290
- const name = nameNode.text ?? "";
3291
- // Expected arities: {method_name: expected_arg_count}
3292
- // (excluding `self`/`cls` which is always 1)
3293
- const expected = {
3294
- __del__: 0,
3295
- __repr__: 0,
3296
- __str__: 0,
3297
- __hash__: 0,
3298
- __bool__: 0,
3299
- __len__: 0,
3300
- __eq__: 1,
3301
- __lt__: 1,
3302
- __le__: 1,
3303
- __gt__: 1,
3304
- __ge__: 1,
3305
- __ne__: 1,
3306
- };
3307
- const expectedCount = expected[name];
3308
- if (expectedCount === undefined)
3309
- return false; // not in our list
3310
- // Count required params (excluding defaults)
3311
- // biome-ignore lint/suspicious/noExplicitAny: AST iteration
3312
- const paramCount = (paramsNode.children ?? []).filter((c) => {
3313
- if (c.type !== "identifier" && c.type !== "typed_parameter")
3314
- return false;
3315
- if (c.text.includes("="))
3316
- return false;
3317
- return true;
3318
- }).length;
3319
- // Expected total = expectedCount + 1 (for self/cls)
3320
- return paramCount !== expectedCount + 1;
3321
- }
3322
- case "no_super_call": {
3323
- const bodyNode = captures.BODY;
3324
- if (!bodyNode)
3325
- return true;
3326
- return !/(?<!\/\/.*)super\s*\(/.test(bodyNode.text);
3327
- }
3328
- case "in_test_block": {
3329
- const first = Object.values(captures)[0];
3330
- return !!first && this.navigator.isInTestBlock(first);
3331
- }
3332
- case "not_in_test_block": {
3333
- const first = Object.values(captures)[0];
3334
- return !first || !this.navigator.isInTestBlock(first);
3335
- }
3336
- case "not_in_try_catch": {
3337
- const first = Object.values(captures)[0];
3338
- return !first || !this.navigator.isInTryCatch(first);
3339
- }
3340
- case "in_try_catch": {
3341
- const first = Object.values(captures)[0];
3342
- return !!first && this.navigator.isInTryCatch(first);
3343
- }
3344
- case "name_matches_param": {
3345
- const nameNode = captures.NAME;
3346
- const paramNode = captures.PARAM;
3347
- return !!nameNode && !!paramNode && nameNode.text === paramNode.text;
3348
- }
3349
- case "not_in_function": {
3350
- const first = Object.values(captures)[0];
3351
- return (!first ||
3352
- !this.navigator.isInside(first, [
3353
- "function_definition",
3354
- "function_declaration",
3355
- "method_definition",
3356
- "arrow_function",
3357
- ]));
3358
- }
3359
- case "check_secret_pattern": {
3360
- const varName = captures.VARNAME?.text ?? "";
3361
- const varNameLower = varName.toLowerCase();
3362
- // Skip UPPER_CASE constants — they're module-level constants
3363
- // (e.g. `GITHUB_TYPE_FOR_PERSONAL_API_KEY = "..."`), not secrets.
3364
- // A constant has no lowercase letters in its name.
3365
- if (varName === varName.toUpperCase() && /[A-Z]/.test(varName)) {
3366
- return false;
3367
- }
3368
- return [
3369
- /api[_-]?key/,
3370
- /api[_-]?secret/,
3371
- /password/,
3372
- /passwd/,
3373
- /secret/,
3374
- /token/,
3375
- /auth/,
3376
- /private[_-]?key/,
3377
- /access[_-]?token/,
3378
- /credentials/,
3379
- /aws[_-]?secret/,
3380
- /github[_-]?token/,
3381
- /client[_-]?secret/,
3382
- ].some((p) => p.test(varNameLower));
3383
- }
3384
- case "returns_error": {
3385
- const first = Object.values(captures)[0];
3386
- if (!first)
3387
- return false;
3388
- const funcNode = this.navigator.findParent(first, [
3389
- "function_declaration",
3390
- "method_declaration",
3391
- ]);
3392
- if (!funcNode)
3393
- return false;
3394
- const signature = String(funcNode.text ?? "")
3395
- .split("{", 1)[0]
3396
- ?.trim() ?? "";
3397
- const returnPart = signature
3398
- .match(/func\s*(?:\([^)]*\)\s*)?[A-Za-z_]\w*\s*\([^)]*\)\s*(.*)$/s)?.[1]
3399
- ?.trim() ?? "";
3400
- return returnPart.length > 0 && /\berror\b/.test(returnPart);
3401
- }
3402
- case "python_empty_except": {
3403
- const bodyNode = captures.BODY;
3404
- if (!bodyNode)
3405
- return true;
3406
- // biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
3407
- return !bodyNode.children.some((c) => c.isNamed && c.type !== "pass_statement" && c.type !== "comment");
3408
- }
3409
- case "check_in_operator_types": {
3410
- // `in`/`not in` require __contains__, __iter__ or __getitem__. We can't
3411
- // do real type inference from a bare identifier, so only flag when the
3412
- // right-hand side is a literal of a type known NOT to support
3413
- // containment (None, bool, int, float) — identifiers, strings, lists,
3414
- // dicts, sets, tuples etc. are left alone to avoid false positives.
3415
- const target = captures.TARGET;
3416
- if (!target)
3417
- return false;
3418
- return ["none", "true", "false", "integer", "float"].includes(target.type);
3419
- }
3420
- case "torchscript_super_call": {
3421
- // The query only anchors on the `super()` call itself (so it can find
3422
- // it at any nesting depth inside the method body); this filter walks
3423
- // back up to confirm the enclosing method OR its class actually carries
3424
- // a @torch.jit.script / @jit.script decorator.
3425
- const call = captures.CALL;
3426
- if (!call)
3427
- return false;
3428
- const isTorchScriptDecorated = (node) => {
3429
- const decorated = node?.parent;
3430
- if (!decorated || decorated.type !== "decorated_definition")
3431
- return false;
3432
- // biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
3433
- return (decorated.children ?? []).some((c) => c.type === "decorator" &&
3434
- /^@(torch\.jit\.script|jit\.script)$/.test(c.text ?? ""));
3435
- };
3436
- const methodNode = this.navigator.findParent(call, [
3437
- "function_definition",
3438
- ]);
3439
- if (!methodNode)
3440
- return false;
3441
- const classNode = this.navigator.findParent(methodNode, [
3442
- "class_definition",
3443
- ]);
3444
- return (isTorchScriptDecorated(methodNode) ||
3445
- isTorchScriptDecorated(classNode));
3446
- }
3447
- case "exit_params_insufficient": {
3448
- // __exit__ must accept (self, exc_type, exc_value, traceback) — 4 named
3449
- // parameters total. The query captures the whole `parameters` node
3450
- // rather than binding each slot individually: tree-sitter's optional
3451
- // (`?`) quantifiers on consecutive anchored siblings match every valid
3452
- // sub-alignment (e.g. self+exc_type alone, or self+exc_type+exc_value),
3453
- // not just the maximal one, so per-slot captures produce spurious
3454
- // duplicate/partial matches for a single, fully-correct signature.
3455
- // Counting named children of the whole node sidesteps that entirely.
3456
- const params = captures.PARAMS;
3457
- if (!params)
3458
- return true;
3459
- // biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
3460
- const named = (params.children ?? []).filter((c) => c.isNamed);
3461
- // `def __exit__(self, *args)` / `(self, *exc_info)` are valid: the splat
3462
- // absorbs the whole exception triple, so it satisfies every remaining
3463
- // slot no matter how few named children the node has.
3464
- if (named.some((c) => c.type === "list_splat_pattern" ||
3465
- c.type === "dictionary_splat_pattern")) {
3466
- return false;
3467
- }
3468
- return named.length < 4;
3469
- }
3470
- case "ruby_empty_rescue": {
3471
- const bodyNode = captures.BODY;
3472
- if (!bodyNode)
3473
- return true;
3474
- // biome-ignore lint/suspicious/noExplicitAny: tree-sitter node
3475
- return !bodyNode.children.some((c) => c.isNamed && !["comment", "nil", "nil_literal"].includes(c.type));
3476
- }
3477
- case "ts_command_injection_sink":
3478
- return (captures.MOD?.text === "child_process" &&
3479
- /^(exec|execSync)$/.test(captures.FN?.text ?? ""));
3480
- case "ts_sql_injection_sink": {
3481
- const template = captures.TEMPLATE?.text ?? "";
3482
- // Inspect only the template's raw prefix. Leading SQL comments are
3483
- // ignored; comments and unrelated strings elsewhere cannot satisfy it.
3484
- const rawPrefix = template
3485
- .replace(/^`/, "")
3486
- .replace(/`$/, "")
3487
- .replace(/^(?:\s|\/\*[\s\S]*?\*\/|--[^\n]*(?:\n|$))*/, "");
3488
- if (/^(?:SELECT[\s\S]*(?:\bFROM\b|\$\{)|INSERT\s+INTO\b|UPDATE\s+[\s\S]*\bSET\b|DELETE\s+FROM\b|CREATE\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|DROP\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|ALTER\s+(?:OR\s+REPLACE\s+|TEMP(?:ORARY)?\s+|UNIQUE\s+|MATERIALIZED\s+)*(?:TABLE|INDEX|VIEW|SCHEMA|DATABASE|TRIGGER|FUNCTION|PROCEDURE|SEQUENCE|TYPE|EXTENSION)\b|GRANT\b|REVOKE\b|WITH[\s\S]*\bSELECT\b|MERGE\s+INTO\b|TRUNCATE\s+TABLE\b|REPLACE\s+INTO\b)/i.test(rawPrefix))
3489
- return true;
3490
- if (!rootNode || !captures.OBJ)
3491
- return false;
3492
- let imported = TYPESCRIPT_SQL_IMPORTS.get(rootNode);
3493
- if (!imported) {
3494
- const names = new Set();
3495
- const stack = [rootNode];
3496
- while (stack.length > 0) {
3497
- const node = stack.pop();
3498
- if (!node)
3499
- continue;
3500
- if (node.type === "import_statement") {
3501
- const source = node.childForFieldName?.("source")?.text ?? "";
3502
- const packageName = source.replace(/^['"]|['"]$/g, "");
3503
- const isKnown = [...TYPESCRIPT_SQL_KNOWN_PACKAGES].some((pkg) => packageName === pkg || packageName.startsWith(`${pkg}/`));
3504
- if (isKnown) {
3505
- const importStack = [node];
3506
- while (importStack.length > 0) {
3507
- const importNode = importStack.pop();
3508
- if (!importNode)
3509
- continue;
3510
- if (importNode.type === "identifier" ||
3511
- importNode.type === "import_specifier") {
3512
- names.add(importNode.text);
3513
- }
3514
- importStack.push(...importNode.children);
3515
- }
3516
- }
3517
- }
3518
- stack.push(...node.children);
3519
- }
3520
- const declarations = new Map();
3521
- const declarationStack = [rootNode];
3522
- while (declarationStack.length > 0) {
3523
- const node = declarationStack.pop();
3524
- if (!node)
3525
- continue;
3526
- if (node.type === "variable_declarator") {
3527
- const name = node.childForFieldName?.("name")?.text;
3528
- const value = node.childForFieldName?.("value");
3529
- if (name && value)
3530
- declarations.set(name, value);
3531
- }
3532
- declarationStack.push(...node.children);
3533
- }
3534
- const resolvesKnownValue = (node, seen = new Set()) => {
3535
- if (node.type === "identifier") {
3536
- if (names.has(node.text))
3537
- return true;
3538
- if (seen.has(node.text))
3539
- return false;
3540
- const value = declarations.get(node.text);
3541
- if (!value)
3542
- return false;
3543
- seen.add(node.text);
3544
- return resolvesKnownValue(value, seen);
3545
- }
3546
- if (node.type === "await_expression") {
3547
- const value = node.children.find((child) => child.isNamed);
3548
- return value ? resolvesKnownValue(value, seen) : false;
3549
- }
3550
- if (node.type === "new_expression") {
3551
- const ctor = node.childForFieldName?.("constructor");
3552
- return ctor ? resolvesKnownValue(ctor, seen) : false;
3553
- }
3554
- if (node.type === "call_expression") {
3555
- const fn = node.childForFieldName?.("function");
3556
- return fn ? resolvesKnownValue(fn, seen) : false;
3557
- }
3558
- return false;
3559
- };
3560
- let changed = true;
3561
- while (changed) {
3562
- changed = false;
3563
- for (const [name, value] of declarations) {
3564
- if (!names.has(name) && resolvesKnownValue(value)) {
3565
- names.add(name);
3566
- changed = true;
3567
- }
3568
- }
3569
- }
3570
- imported = names;
3571
- TYPESCRIPT_SQL_IMPORTS.set(rootNode, imported);
3572
- }
3573
- const resolvesToKnownClient = (node) => {
3574
- if (node.type === "identifier")
3575
- return imported.has(node.text);
3576
- if (node.type === "await_expression") {
3577
- const value = node.children.find((child) => child.isNamed);
3578
- return value ? resolvesToKnownClient(value) : false;
3579
- }
3580
- if (node.type === "new_expression") {
3581
- const ctor = node.childForFieldName?.("constructor");
3582
- return ctor ? resolvesToKnownClient(ctor) : false;
3583
- }
3584
- if (node.type === "call_expression") {
3585
- const fn = node.childForFieldName?.("function");
3586
- return fn ? resolvesToKnownClient(fn) : false;
3587
- }
3588
- return false;
3589
- };
3590
- return resolvesToKnownClient(captures.OBJ);
3591
- }
3592
- case "ts_ssrf_sink": {
3593
- const fn = captures.FN?.text ?? "";
3594
- const obj = captures.OBJ?.text ?? "";
3595
- const urlText = captures.URL?.text ?? "";
3596
- const allowedFns = new Set([
3597
- "fetch",
3598
- "request",
3599
- "get",
3600
- "post",
3601
- "put",
3602
- "patch",
3603
- "delete",
3604
- ]);
3605
- if (!allowedFns.has(fn))
3606
- return false;
3607
- // A bare identifier that provably resolves (in this file) to a
3608
- // `const` initialized with a fixed string/template literal is a
3609
- // genuinely fixed URL — exempt it regardless of naming
3610
- // convention (SCREAMING_SNAKE_CASE proves nothing about
3611
- // provenance; see #963). Anything we cannot definitively prove
3612
- // falls through to the existing broad heuristic below, so
3613
- // member expressions, non-literal consts, reassigned
3614
- // bindings, and unresolved identifiers all keep being
3615
- // evaluated for taint as before — a false positive here is
3616
- // acceptable, a missed SSRF is not.
3617
- if (rootNode &&
3618
- captures.URL?.type === "identifier" &&
3619
- this.resolvesToFileLiteralConst(urlText, rootNode)) {
3620
- return false;
3621
- }
3622
- // `fetch(u.toString())` / `fetch(u.href)` where `u` is a file-local
3623
- // `const u = new URL(<literalPath>, <fixedBase>)`. The origin+path are
3624
- // fully fixed; dynamic query params set later via
3625
- // `u.searchParams.set(...)` don't control the destination. Anything we
3626
- // cannot prove fixed (param/env/member/tainted base) falls through to
3627
- // the broad heuristic and still fires. See #1000.
3628
- if (rootNode && captures.URL) {
3629
- const urlVar = this.newUrlBaseVarName(captures.URL);
3630
- if (urlVar && this.resolvesToFixedNewUrlConst(urlVar, rootNode)) {
3631
- return false;
3632
- }
3633
- }
3634
- // Only flag when the URL argument looks like it could carry external
3635
- // input: member expressions (req.url, ctx.query.x) or identifiers
3636
- // whose names suggest user/external provenance. Plain generic names
3637
- // like `url` or `path` in internal download utilities produce too
3638
- // many false positives — those need data-flow analysis to resolve.
3639
- const looksLikeExternalInput = urlText.includes(".") ||
3640
- /user|external|remote|input|target|webhook|callback|redirect|untrusted|arbitrary/i.test(urlText);
3641
- if (!looksLikeExternalInput)
3642
- return false;
3643
- if (!obj)
3644
- return fn === "fetch";
3645
- return new Set([
3646
- "axios",
3647
- "http",
3648
- "https",
3649
- "got",
3650
- "request",
3651
- "superagent",
3652
- "undici",
3653
- ]).has(obj);
3654
- }
3655
- case "ts_weak_hash_algorithm":
3656
- return (captures.FN?.text === "createHash" &&
3657
- /^(md5|sha1)$/i.test(captures.ALG?.text ?? ""));
3658
- case "ts_insecure_random_source": {
3659
- if (captures.OBJ?.text !== "Math" || captures.FN?.text !== "random")
3660
- return false;
3661
- // Only flag when the result flows into a security-sensitive binding.
3662
- // Walk up from the `Math.random()` call so chained forms such as
3663
- // `Math.random().toString(36)` are still attributed to their binding.
3664
- const varName = this.enclosingBindingName(captures.CALL ?? captures.VAR);
3665
- return /token|secret|password|key|nonce|salt|csrf|auth|session|credential|hash|otp|pin/i.test(varName);
3666
- }
3667
- case "ts_detached_async_call":
3668
- return /(Async$|fetch$|request$)/.test(captures.FN?.text ?? "");
3669
- case "incomplete_assertion": {
3670
- const expectNode = captures.EXPECT;
3671
- if (!expectNode)
3672
- return false;
3673
- const CHAI_PROPERTY_ASSERTIONS = new Set([
3674
- "true",
3675
- "false",
3676
- "null",
3677
- "undefined",
3678
- "empty",
3679
- "NaN",
3680
- "finite",
3681
- "exist",
3682
- "arguments",
3683
- "extensible",
3684
- "sealed",
3685
- "frozen",
3686
- "locked",
3687
- ]);
3688
- // The expect identifier is inside a call_expression. Walk up past that
3689
- // call_expression to the container that determines if it's a complete
3690
- // assertion or an incomplete one.
3691
- let current = expectNode.parent;
3692
- if (!current)
3693
- return false;
3694
- current = current.parent; // skip the expect(...) call_expression
3695
- if (!current)
3696
- return false;
3697
- // Bare expect(foo); or return expect(foo);
3698
- if (current.type === "expression_statement" ||
3699
- current.type === "return_statement")
3700
- return true;
3701
- let lastPropertyName = null;
3702
- while (current && current.type === "member_expression") {
3703
- const propNode = current.children?.find((c) => c.type === "property_identifier");
3704
- if (propNode)
3705
- lastPropertyName = propNode.text;
3706
- const parent = current.parent;
3707
- if (!parent)
3708
- return false;
3709
- if (parent.type === "expression_statement" ||
3710
- parent.type === "return_statement") {
3711
- if (lastPropertyName &&
3712
- CHAI_PROPERTY_ASSERTIONS.has(lastPropertyName))
3713
- return false;
3714
- return true;
3715
- }
3716
- if (parent.type === "call_expression")
3717
- return false;
3718
- current = parent;
3719
- }
3720
- return false;
3721
- }
3722
- case "py_command_injection_sink": {
3723
- const mod = captures.MOD?.text ?? "";
3724
- const fn = captures.FN?.text ?? "";
3725
- const kw = captures.KW?.text ?? "";
3726
- return ((mod === "os" && /^(system|popen)$/.test(fn)) ||
3727
- (mod === "subprocess" &&
3728
- /^(run|Popen|call|check_output|check_call)$/.test(fn) &&
3729
- kw === "shell"));
3730
- }
3731
- case "go_command_injection_sink":
3732
- return (captures.PKG?.text === "exec" &&
3733
- /^(Command|CommandContext)$/.test(captures.FN?.text ?? "") &&
3734
- /^"(sh|bash|zsh|cmd|powershell|pwsh)"$/.test(captures.SHELL?.text ?? "") &&
3735
- /^"(-c|\/c)"$/.test(captures.FLAG?.text ?? ""));
3736
- case "ruby_command_injection_sink":
3737
- return /^(system|exec|spawn|popen|capture3|capture2|capture2e)$/.test(captures.FN?.text ?? "");
3738
- case "py_ssrf_sink":
3739
- return (captures.MOD?.text === "requests" &&
3740
- /^(get|post|put|patch|delete|request|head|options)$/.test(captures.FN?.text ?? ""));
3741
- case "py_path_traversal_sink":
3742
- return /^(open|read_text|read_bytes|write_text|write_bytes|remove|unlink|rmdir)$/.test(captures.FN?.text ?? "");
3743
- case "go_path_traversal_sink":
3744
- return (/^(os|ioutil)$/.test(captures.PKG?.text ?? "") &&
3745
- /^(Open|OpenFile|ReadFile|WriteFile|Create|Remove|RemoveAll)$/.test(captures.FN?.text ?? ""));
3746
- case "py_sql_injection_sink": {
3747
- const fn = captures.FN?.text ?? "";
3748
- if (!PYTHON_SQL_SINK_METHODS.has(fn))
3749
- return false;
3750
- const sqlNode = captures.SQL;
3751
- const receiver = captures.OBJ;
3752
- // Pre-existing exemptions — kept verbatim so no current user
3753
- // regresses (#2577 review): an unannotated session receiver, and a
3754
- // statement-builder call in argument position.
3755
- if (fn === "execute" &&
3756
- this.isLikelySqlAlchemyReceiver(receiver?.text ?? "")) {
3757
- return false;
3758
- }
3759
- if (sqlNode && this.isSafeSqlAlchemyExpressionCall(sqlNode)) {
3760
- return false;
3761
- }
3762
- // #2576: a receiver PROVEN to be a sqlalchemy Session/AsyncSession.
3763
- // `Session.query` takes entity classes and `Session.execute` a
3764
- // statement object (a builder call, or a name bound to one —
3765
- // `stmt = select(User)`, which the check above cannot see). Neither
3766
- // suppression may swallow composed SQL text (#2577 review F1/F2).
3767
- if (isProvenSqlAlchemySessionReceiver(receiver, rootNode)) {
3768
- if (fn === "query" &&
3769
- isSqlAlchemyEntityQueryArgument(sqlNode, rootNode)) {
3770
- return false;
3771
- }
3772
- if (fn === "execute" &&
3773
- isSqlAlchemyStatementArgument(sqlNode, rootNode)) {
3774
- return false;
3775
- }
3776
- }
3777
- // #2576: psycopg `sql.SQL("...").format(sql.Identifier(...))`.
3778
- if (isSafePsycopgIdentifierComposition(sqlNode, rootNode)) {
3779
- return false;
3780
- }
3781
- return true;
3782
- }
3783
- case "go_sql_injection_sink":
3784
- return (/^(Query|QueryContext|QueryRow|QueryRowContext|Exec|ExecContext)$/.test(captures.DBFN?.text ?? "") &&
3785
- captures.FMTPKG?.text === "fmt" &&
3786
- captures.FMTFN?.text === "Sprintf");
3787
- case "py_insecure_deserialization_sink":
3788
- return (/^(pickle|yaml)$/.test(captures.MOD?.text ?? "") &&
3789
- /^(load|loads|unsafe_load)$/.test(captures.FN?.text ?? ""));
3790
- case "ruby_insecure_deserialization_sink":
3791
- return (/^(Marshal|YAML|Psych)$/.test(captures.MOD?.text ?? "") &&
3792
- /^(load|unsafe_load)$/.test(captures.FN?.text ?? ""));
3793
- case "match_captures": {
3794
- // Generic filter: each key in postFilterParams is a capture name,
3795
- // value is a regex string. All must match.
3796
- for (const [captureName, pattern] of Object.entries(postFilterParams ?? {})) {
3797
- const node = captures[captureName];
3798
- if (!node)
3799
- return false;
3800
- if (!new RegExp(pattern).test(node.text))
3801
- return false;
3802
- }
3803
- return true;
3804
- }
3805
- case "case_range_single_value": {
3806
- const start = captures.START?.text ?? "";
3807
- const end = captures.END?.text ?? "";
3808
- return start === end;
3809
- }
3810
- case "goto_jumps_backward": {
3811
- const label = captures.LABEL;
3812
- const gotoNode = captures.GOTO;
3813
- if (!label || !gotoNode)
3814
- return false;
3815
- return label.startIndex < gotoNode.startIndex;
3816
- }
3817
- case "goto_targets_inner_block": {
3818
- const target = captures.TARGET;
3819
- if (!target)
3820
- return false;
3821
- // A goto targets an inner block if its label is inside a
3822
- // compound_statement that is nested inside another compound_statement.
3823
- let depth = 0;
3824
- let node = target.parent;
3825
- while (node) {
3826
- if (node.type === "compound_statement")
3827
- depth++;
3828
- node = node.parent;
3829
- }
3830
- return depth >= 2;
3831
- }
3832
- case "c_memset_sensitive_arg": {
3833
- const callNode = captures.CALL;
3834
- if (!callNode || callNode.type !== "call_expression")
3835
- return false;
3836
- // Find the first argument in the argument_list
3837
- const argList = callNode.children?.find((c) => c.type === "argument_list");
3838
- if (!argList)
3839
- return false;
3840
- // First named child after the opening paren is the first arg
3841
- const firstArg = argList.children?.find((c) => c.isNamed);
3842
- if (!firstArg)
3843
- return false;
3844
- const argName = firstArg.text ?? "";
3845
- return /password|secret|key|token|credential|auth|private|passwd|pin|salt|nonce|iv|seed/i.test(argName);
3846
- }
3847
- case "c_stdlib_name": {
3848
- const name = captures.NAME?.text ?? "";
3849
- const STDLIB_NAMES = new Set([
3850
- "malloc",
3851
- "calloc",
3852
- "realloc",
3853
- "free",
3854
- "alloca",
3855
- "printf",
3856
- "fprintf",
3857
- "sprintf",
3858
- "snprintf",
3859
- "vprintf",
3860
- "vfprintf",
3861
- "vsprintf",
3862
- "vsnprintf",
3863
- "scanf",
3864
- "fscanf",
3865
- "sscanf",
3866
- "vscanf",
3867
- "vfscanf",
3868
- "vsscanf",
3869
- "strcpy",
3870
- "strncpy",
3871
- "strcat",
3872
- "strncat",
3873
- "strcmp",
3874
- "strncmp",
3875
- "strlen",
3876
- "strchr",
3877
- "strrchr",
3878
- "strstr",
3879
- "strerror",
3880
- "memcpy",
3881
- "memmove",
3882
- "memset",
3883
- "memcmp",
3884
- "memchr",
3885
- "fopen",
3886
- "fclose",
3887
- "fread",
3888
- "fwrite",
3889
- "fgets",
3890
- "fputs",
3891
- "getc",
3892
- "putc",
3893
- "getchar",
3894
- "putchar",
3895
- "exit",
3896
- "abort",
3897
- "assert",
3898
- "errno",
3899
- "abs",
3900
- "labs",
3901
- "llabs",
3902
- "div",
3903
- "ldiv",
3904
- "lldiv",
3905
- "atoi",
3906
- "atol",
3907
- "atoll",
3908
- "strtol",
3909
- "strtoll",
3910
- "strtoul",
3911
- "strtoull",
3912
- "strtod",
3913
- "strtof",
3914
- "strtold",
3915
- "qsort",
3916
- "bsearch",
3917
- "time",
3918
- "clock",
3919
- "difftime",
3920
- "mktime",
3921
- "strftime",
3922
- "getenv",
3923
- "setenv",
3924
- "putenv",
3925
- "system",
3926
- "isalpha",
3927
- "isdigit",
3928
- "isalnum",
3929
- "isspace",
3930
- "isupper",
3931
- "islower",
3932
- "toupper",
3933
- "tolower",
3934
- "sizeof",
3935
- "offsetof",
3936
- "NULL",
3937
- "EXIT_SUCCESS",
3938
- "EXIT_FAILURE",
3939
- ]);
3940
- return STDLIB_NAMES.has(name);
3941
- }
3942
- case "c_octal_literal": {
3943
- const num = captures.NUM?.text ?? "";
3944
- return /^0[0-7]+$/.test(num);
3945
- }
3946
- case "c_noreturn_attr": {
3947
- const attr = captures.ATTR?.text ?? "";
3948
- return attr === "noreturn";
3949
- }
3950
- case "c_label_in_switch": {
3951
- const stmt = captures.STMT;
3952
- if (!stmt)
3953
- return false;
3954
- let node = stmt.parent;
3955
- while (node) {
3956
- if (node.type === "switch_statement")
3957
- return true;
3958
- node = node.parent;
3959
- }
3960
- return false;
3961
- }
3962
- default:
3963
- // A rule whose post_filter has no implementation cannot honour its
3964
- // own definition, so DROP the match instead of reporting every raw
3965
- // structural hit. Failing open here made `duplicate-function-arg`
3966
- // (`same_param_name`, unimplemented) fire on 59 of 60 files that
3967
- // contain no duplicate parameter at all.
3968
- this.reportMissingPostFilter(postFilter);
3969
- return false;
3970
- }
3971
- }
3972
- reportedMissingPostFilters = new Set();
3973
- reportedPostFilterFailures = new Set();
3974
- /** Keep a failed post-filter match and leave a bounded diagnostic trail. */
3975
- reportPostFilterFailure(postFilter, reason) {
3976
- if (this.reportedPostFilterFailures.has(postFilter))
3977
- return;
3978
- this.reportedPostFilterFailures.add(postFilter);
3979
- logTreeSitterDiagnostic({
3980
- subsystem: "tree-sitter-client",
3981
- message: `tree-sitter rule post_filter '${postFilter}' failed — ` +
3982
- `keeping the diagnostic (fail-open): ${reason}.`,
3983
- metadata: { postFilter },
3984
- });
3985
- }
3986
- /** Warn once per unimplemented post_filter — a silent rule needs a trail. */
3987
- reportMissingPostFilter(postFilter) {
3988
- if (this.reportedMissingPostFilters.has(postFilter))
3989
- return;
3990
- this.reportedMissingPostFilters.add(postFilter);
3991
- logTreeSitterDiagnostic({
3992
- subsystem: "tree-sitter-client",
3993
- message: `tree-sitter rule post_filter '${postFilter}' is not implemented — ` +
3994
- `matches for the rules using it are suppressed rather than reported unfiltered. ` +
3995
- `Implement it in applyPostFilter (clients/tree-sitter-client.ts) to re-enable them.`,
3996
- metadata: { postFilter },
3997
- });
3998
- }
3999
- /**
4000
- * Latch so the "textPredicates is malformed" diagnostic log line fires once
4001
- * per `TreeSitterClient` instance for the life of the process (this client is
4002
- * a process-wide singleton in production — see clients/tree-sitter-shared.ts),
4003
- * not once per call. This gates ONLY the log line: process-wide log volume is
4004
- * the concern here, not the user-facing degradation record below, which must
4005
- * re-arm every session (see recordDegradationOnce) — including for a Query
4006
- * object the LRU query cache (queryCache/queryBatchCache above) kept alive
4007
- * across a session boundary, since the cache is evicted, never cleared.
4008
- */
4009
- textPredicatesInvalidLogged = false;
4010
- /**
4011
- * Typed accessor for `query.textPredicates`. If the property is missing or the
4012
- * wrong shape — e.g. a future web-tree-sitter upgrade renames or removes it —
4013
- * `query.textPredicates?.[i] ?? []` would silently mean "no predicates for this
4014
- * pattern," passing every match through unfiltered. That's a silent fail-open on
4015
- * #match?/#eq? predicates that rules rely on for correctness, and because this
4016
- * runs for every match of every query, it would zero out ALL structural matches
4017
- * across every language while pi-lens reports "no issues found." Fail loud (log
4018
- * once per client instance, record a degradation so it reaches the user) and
4019
- * closed (report the query has no usable predicates) instead of guessing.
4020
- *
4021
- * No WeakSet short-circuit for already-known-bad queries here (#1523 review
4022
- * R1): `Array.isArray` is O(1), so memoizing it buys nothing, and a
4023
- * process-lifetime WeakSet would make the SAME cached Query object (the query
4024
- * cache is LRU-evicted, never cleared) skip straight past
4025
- * `recordDegradationOnce` on every call after the first — which is exactly
4026
- * what made the degradation record fail to re-arm across a session boundary
4027
- * for a query the cache kept warm. `recordDegradationOnce` itself is called
4028
- * unconditionally on every invalid check; the ledger's own once-per-kind/
4029
- * subject dedupe (cleared by resetDegradationLedger, which handleSessionStart
4030
- * calls first thing — clients/runtime-session.ts) is the single source of
4031
- * truth for "how often does this actually get recorded."
4032
- */
4033
- // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter Query instances
4034
- hasValidTextPredicates(query) {
4035
- const valid = Array.isArray(query?.textPredicates);
4036
- if (!valid) {
4037
- if (!this.textPredicatesInvalidLogged) {
4038
- this.textPredicatesInvalidLogged = true;
4039
- logTreeSitterDiagnostic({
4040
- subsystem: "tree-sitter-client",
4041
- message: "web-tree-sitter Query.textPredicates is missing or not an array — " +
4042
- "#match?/#eq? predicates cannot be evaluated. Failing CLOSED: matches " +
4043
- "for this query are dropped rather than reported unfiltered.",
4044
- metadata: { textPredicatesType: typeof query?.textPredicates },
4045
- });
4046
- }
4047
- // User-facing signal (#1523 review F1). Called unconditionally on every
4048
- // invalid check (not gated by a client-lifetime latch) so it re-arms
4049
- // every session, per the R1 fix above.
4050
- recordDegradationOnce({
4051
- kind: "query-predicates-invalid",
4052
- subject: "web-tree-sitter",
4053
- reason: "Query.textPredicates is missing or not an array — #match?/#eq? " +
4054
- "predicates cannot be evaluated; structural matches relying on them " +
4055
- "are dropped fail-closed",
4056
- });
4057
- }
4058
- return valid;
4059
- }
4060
- /**
4061
- * Evaluate text predicates (#match?, #eq?) for a query match. web-tree-sitter
4062
- * 0.25's `Query.matches()` DOES apply these predicates itself (probed on the
4063
- * shipped grammars — see clients/tree-sitter-symbol-extractor.ts), so this is a
4064
- * belt-and-braces re-filter, not a workaround for missing enforcement. It also
4065
- * doubles as the fail-closed guard (#1523): if `query.textPredicates` is ever
4066
- * missing or malformed, this drops the match instead of assuming it already
4067
- * passed upstream.
4068
- */
4069
- // biome-ignore lint/suspicious/noExplicitAny: web-tree-sitter types
4070
- evaluatePredicates(query, match) {
4071
- if (!this.hasValidTextPredicates(query))
4072
- return false;
4073
- // `?.` guards a post-validation removal of the property (e.g. a later
4074
- // mutation strips it after the first successful check) so the read can't
4075
- // throw here — an uncaught throw would be swallowed by the outer try/catch
4076
- // in searchFileWithQuery and silently zero out matches for the whole file.
4077
- const predicates = query.textPredicates?.[match.patternIndex] ?? [];
4078
- return predicates.every((fn) => fn(match.captures));
4079
- }
4080
- /** Search a single file using tree-sitter Query */
4081
- async searchFileWithQuery(filePath,
4082
- // biome-ignore lint/suspicious/noExplicitAny: Query type from web-tree-sitter
4083
- query, metavars, languageId, _originalPattern, postFilter,
4084
- // biome-ignore lint/suspicious/noExplicitAny: Post filter params
4085
- postFilterParams, contentOverride) {
4086
- const matches = [];
4087
- await this.parseFileAndUse(filePath, languageId, contentOverride, (tree) => {
4088
- try {
4089
- const rootNode = tree.rootNode;
4090
- const queryMatches = query.matches(rootNode);
4091
- for (const match of queryMatches) {
4092
- const captures = {};
4093
- for (const capture of match.captures) {
4094
- if (metavars.includes(capture.name)) {
4095
- captures[capture.name] = capture.node;
4096
- }
4097
- }
4098
- // Belt-and-braces re-check of #match?/#eq? predicates (web-tree-sitter
4099
- // 0.25's Query.matches() already applies them) plus the fail-closed
4100
- // guard for a malformed query.textPredicates (see evaluatePredicates).
4101
- if (!this.evaluatePredicates(query, match)) {
4102
- continue;
4103
- }
4104
- if (postFilter &&
4105
- !this.applyPostFilter(postFilter, postFilterParams, captures, rootNode)) {
4106
- continue;
4107
- }
4108
- if (match.captures.length > 0) {
4109
- const firstNode = match.captures[0].node;
4110
- const textCaptures = {};
4111
- for (const [name, node] of Object.entries(captures)) {
4112
- textCaptures[name] = node.text;
4113
- }
4114
- matches.push({
4115
- file: filePath,
4116
- line: firstNode.startPosition.row + 1,
4117
- column: firstNode.startPosition.column + 1,
4118
- matchedText: firstNode.text,
4119
- nodeType: firstNode.type,
4120
- captures: textCaptures,
4121
- });
4122
- }
4123
- }
4124
- if (matches.length > 0) {
4125
- this.dbg(`Found ${matches.length} matches in ${path.basename(filePath)}`);
4126
- }
4127
- }
4128
- catch (err) {
4129
- this.reportWasmAbort(err);
4130
- this.dbg(`Query matching error: ${err}`);
4131
- }
4132
- });
4133
- return matches;
4134
- }
4135
- /** Collect source files for a language */
4136
- collectFiles(dir, languageId, fileFilter) {
4137
- const files = [];
4138
- const extensions = this.getExtensionsForLanguage(languageId);
4139
- const rootDir = path.resolve(dir);
4140
- const ignoreMatcher = getProjectIgnoreMatcher(rootDir);
4141
- // Hard cap on the walk itself (not just result collection). The per-file
4142
- // `maxResults` break upstream only stops gathering matches *after* the walk
4143
- // has already enumerated the whole tree — so a misrooted structuralSearch
4144
- // would still synchronously read every directory. Bound the walk (#262).
4145
- const scan = (d) => {
4146
- if (files.length >= TREE_SITTER_MAX_SCAN_FILES)
4147
- return;
4148
- try {
4149
- const entries = fs.readdirSync(d, { withFileTypes: true });
4150
- for (const entry of entries) {
4151
- if (files.length >= TREE_SITTER_MAX_SCAN_FILES)
4152
- return;
4153
- const full = path.join(d, entry.name);
4154
- if (entry.isDirectory()) {
4155
- if (isExcludedDirName(entry.name))
4156
- continue;
4157
- if (ignoreMatcher.isIgnored(full, true))
4158
- continue;
4159
- scan(full);
4160
- }
4161
- else if (extensions.some((ext) => entry.name.endsWith(ext))) {
4162
- if (ignoreMatcher.isIgnored(full, false))
4163
- continue;
4164
- if (!fileFilter || fileFilter(full)) {
4165
- files.push(full);
4166
- }
4167
- }
4168
- }
4169
- }
4170
- catch { }
4171
- };
4172
- scan(rootDir);
4173
- return files;
4174
- }
4175
- /** Get file extensions for a language */
4176
- getExtensionsForLanguage(languageId) {
4177
- const mapping = {
4178
- typescript: [".ts", ".mts", ".cts"],
4179
- tsx: [".tsx"],
4180
- javascript: [".js", ".mjs", ".cjs"],
4181
- python: [".py"],
4182
- rust: [".rs"],
4183
- go: [".go"],
4184
- java: [".java"],
4185
- kotlin: [".kt", ".kts"],
4186
- dart: [".dart"],
4187
- c: [".c", ".h"],
4188
- cpp: [".cpp", ".hpp", ".cc", ".hh"],
4189
- elixir: [".ex", ".exs"],
4190
- ruby: [".rb"],
4191
- };
4192
- return mapping[languageId] || [];
4193
- }
4194
- }
4195
- // --- Simplified Pattern Search (regex fallback) ---