@herbertgao/pi-extensions 2026.10.0 → 2026.10.2

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 (769) hide show
  1. package/README.md +16 -12
  2. package/THIRD_PARTY_NOTICES.md +3 -3
  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/.opencode/plugins/index.js +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  11. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  12. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  13. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  14. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  15. package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
  16. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  17. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  18. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  24. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  25. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  26. package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
  27. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  28. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  29. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
  34. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
  35. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
  36. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  37. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  38. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  39. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  45. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  46. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  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 +1064 -0
  67. package/node_modules/pi-lens/README.md +115 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  70. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  71. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  72. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  73. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  74. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  75. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  76. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  77. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  78. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  79. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  80. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  81. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  82. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  86. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  87. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  88. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  89. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  90. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  91. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  92. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  93. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  94. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  95. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  96. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  97. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  98. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  99. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  100. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  103. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  104. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  106. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  107. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  108. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  109. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  110. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  111. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  112. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  113. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  114. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  115. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  116. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  117. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  118. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  119. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  120. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  121. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  122. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  123. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  124. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  125. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  126. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  127. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  128. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  129. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  130. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  131. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  132. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  133. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  134. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  135. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  136. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  137. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  138. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  139. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  140. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  142. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  143. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  144. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  145. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  146. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  147. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  148. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  149. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  150. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  151. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  152. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  153. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  154. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  155. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  156. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  157. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  158. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  159. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  160. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  161. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  162. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  163. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  164. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  165. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  166. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  167. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  168. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  169. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  170. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  171. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  172. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  173. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  174. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  175. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  176. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  177. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  178. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  181. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  182. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  183. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  184. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  185. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  186. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  187. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  188. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  189. package/node_modules/pi-lens/dist/index.js +16456 -8335
  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 +22589 -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 +3 -2
  200. package/node_modules/pi-lens/docs/configuration.md +20 -1
  201. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  202. package/node_modules/pi-lens/docs/features.md +8 -4
  203. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  204. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  205. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  206. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  207. package/node_modules/pi-lens/docs/mcp.md +21 -5
  208. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  209. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  210. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  211. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  212. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  213. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  214. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  215. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  216. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  217. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  218. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  219. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  220. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  221. package/node_modules/pi-lens/docs/settings.md +1 -0
  222. package/node_modules/pi-lens/docs/usage.md +5 -1
  223. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  224. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  225. package/node_modules/pi-lens/package.json +18 -6
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  228. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  229. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  230. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  231. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  232. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  233. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  234. package/node_modules/pi-typesafe/README.md +1 -1
  235. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  236. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  237. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  238. package/node_modules/pi-typesafe/dist/client.js +22 -7
  239. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  240. package/node_modules/pi-typesafe/dist/index.js +1 -1
  241. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  242. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  243. package/node_modules/pi-typesafe/package.json +1 -1
  244. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  245. package/node_modules/pi-web-access/README.md +57 -11
  246. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  247. package/node_modules/pi-web-access/curator-page.ts +5 -1
  248. package/node_modules/pi-web-access/curator-server.ts +2 -0
  249. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  250. package/node_modules/pi-web-access/degoog.ts +323 -0
  251. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  252. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  253. package/node_modules/pi-web-access/exa.ts +5 -1
  254. package/node_modules/pi-web-access/extract.ts +41 -3
  255. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  256. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  257. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  258. package/node_modules/pi-web-access/index.ts +153 -1202
  259. package/node_modules/pi-web-access/keenable.ts +221 -0
  260. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  261. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  262. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  263. package/node_modules/pi-web-access/openai-search.ts +34 -1
  264. package/node_modules/pi-web-access/package.json +23 -5
  265. package/node_modules/pi-web-access/perplexity.ts +64 -56
  266. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  267. package/node_modules/pi-web-access/video-extract.ts +1 -1
  268. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  269. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  270. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  271. package/package.json +17 -12
  272. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
  273. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
  274. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  275. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  276. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  277. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  278. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  279. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  280. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  281. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  282. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  283. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  284. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  285. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  286. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  287. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  288. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  289. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  290. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  291. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  292. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  293. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  294. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  295. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  296. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  297. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  298. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  299. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  300. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  301. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  302. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  303. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  304. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  305. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  306. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  307. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  308. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  309. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  310. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  311. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  312. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  313. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  314. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  315. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  316. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  317. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  318. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  319. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  320. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  321. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  322. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  323. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  324. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  325. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  326. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  327. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  328. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  329. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  330. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  331. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  332. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  333. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  334. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  335. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  336. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  337. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  338. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  339. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  340. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  341. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  342. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  343. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  344. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  345. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  346. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  347. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  348. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  349. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  350. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  351. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  352. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  353. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  354. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  355. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  356. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  357. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  358. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  359. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  360. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  361. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  362. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  363. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  364. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  365. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  366. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  367. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  368. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  369. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  370. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  371. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  372. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  373. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  374. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  375. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  376. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  377. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  378. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  379. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  380. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  381. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  382. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  383. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  384. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  385. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  386. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  387. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  388. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  389. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  390. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  391. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  392. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  393. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  394. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  395. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  396. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  397. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  398. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  399. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  400. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  401. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  402. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  403. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  404. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  405. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  406. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  407. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  447. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  448. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  449. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  450. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  451. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  452. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  453. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  454. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  455. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  456. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  457. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  458. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  459. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  460. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  461. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  462. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  463. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  464. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  465. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  466. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  467. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  468. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  469. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  470. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  471. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  472. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  473. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  474. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  475. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  476. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  477. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  478. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  479. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  480. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  481. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  482. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  483. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  484. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  485. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  486. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  487. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  488. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  489. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  490. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  491. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  492. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  493. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  494. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  495. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  496. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  498. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  499. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  500. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  501. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  502. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  503. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  504. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  505. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  506. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  507. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  508. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  509. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  510. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  511. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  512. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  513. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  514. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  515. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  516. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  517. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  518. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  519. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  520. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  521. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  522. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  523. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  524. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  525. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  526. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  527. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  528. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  529. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  530. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  531. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  532. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  533. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  534. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  535. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  536. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  537. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  538. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  539. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  540. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  541. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  542. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  543. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  544. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  545. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  546. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  547. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  548. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  549. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  550. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  551. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  552. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  553. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  554. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  555. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  556. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  557. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  558. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  559. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  560. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  561. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  562. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  563. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  564. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  565. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  566. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  567. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  568. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  569. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  570. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  571. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  572. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  573. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  574. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  575. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  576. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  577. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  578. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  579. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  580. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  581. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  582. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  583. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  584. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  585. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  586. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  587. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  588. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  589. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  590. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  591. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  592. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  593. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  594. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  595. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  596. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  597. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  598. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  599. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  600. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  601. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  602. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  603. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  604. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  605. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  606. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  607. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  608. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  609. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  610. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  611. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  612. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  613. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  614. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  615. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  616. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  617. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  618. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  619. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  620. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  621. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  623. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  624. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  625. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  626. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  627. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  628. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  629. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  630. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  631. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  632. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  633. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  634. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  635. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  636. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  637. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  638. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  639. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  640. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  641. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  642. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  643. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  644. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  645. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  646. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  647. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  648. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  649. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  650. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  651. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  652. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  653. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  654. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  655. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  656. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  657. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  658. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  659. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  660. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  661. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  662. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  663. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  664. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  665. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  666. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  667. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  668. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  669. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  670. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  671. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  672. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  673. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  674. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  675. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  676. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  677. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  678. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  679. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  680. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  681. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  682. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  683. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  684. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  685. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  686. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  687. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  688. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  689. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  690. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  691. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  692. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  693. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  694. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  695. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  696. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  697. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  698. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  699. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  700. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  701. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  702. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  703. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  704. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  705. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  706. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  707. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  708. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  709. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  710. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  711. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  712. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  713. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  714. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  715. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  716. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  717. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  718. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  719. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  720. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  721. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  722. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  723. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  724. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  725. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  726. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  727. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  728. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  729. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  730. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  731. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  732. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  733. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  734. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  735. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  736. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  737. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  738. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  739. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  740. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  741. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  742. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  743. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  744. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  745. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  746. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  747. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  748. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  749. package/node_modules/pi-lens/dist/i18n.js +0 -69
  750. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  751. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  752. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  753. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  754. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  755. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  756. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  757. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  758. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  759. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  760. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  761. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  762. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  763. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  764. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  765. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  766. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  767. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  768. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  769. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
@@ -1,2080 +0,0 @@
1
- /**
2
- * Identifier-aware inverted word index + BM25 ranking.
3
- *
4
- * The lexical half of the "codebase mental model + hybrid ranking" ask (#162):
5
- * a deterministic, zero-dep index over source identifiers that answers
6
- * "which files are most relevant to <query>" with BM25 relevance plus a small
7
- * set of priors (demote tests/vendor and doc files) and an optional graph
8
- * centrality boost (importedBy count from the reverse-dependency index). It
9
- * complements LSP/symbol navigation rather than duplicating the host's grep:
10
- * grep finds raw substrings; this ranks files by identifier relevance.
11
- *
12
- * Built from file contents during the session scan, persisted in the project
13
- * snapshot (serialize/deserialize below), and queried via an MCP tool. No
14
- * embeddings, no native deps, no daemon — pure in-process TypeScript.
15
- */
16
- import * as fs from "node:fs";
17
- import * as path from "node:path";
18
- import { createDeadline, forEachCooperatively, yieldIfOverBudget, } from "./cooperative-budget.js";
19
- import { incrementDegradationCount } from "./degradation-ledger.js";
20
- import { KIND_EXTENSIONS } from "./file-kinds.js";
21
- import { isTestFileName } from "./file-role.js";
22
- import { PathKeyedMap } from "./path-keyed-map.js";
23
- import { isAtOrAboveHomeDir, normalizeEphemeralMapKey } from "./path-utils.js";
24
- import { createSingleFlight } from "./single-flight.js";
25
- import { createDebounceScheduler, } from "./persist-debounce.js";
26
- import { getWordIndexMaxFilesDerived } from "./project-scale.js";
27
- import { compactPostingsIntoArena, compactPostingsIntoArenaCooperatively, countPostingBackingStores, countPostingEntries, estimateWordIndexStoreBytes, WordForwardEntry, WordIndexFileTable, WordPostingList, } from "./word-index-store.js";
28
- import { logWordIndex } from "./word-index-logger.js";
29
- /**
30
- * The single normalizer every word-index path key folds through (#1025). The
31
- * CHEAP form — slash-fold + win32-lowercase, NO filesystem I/O — because the
32
- * index is a hot, single-process, in-memory structure whose keys this process
33
- * produces itself (`collectSourceFilesAsync`'s walk, `path.resolve` at the
34
- * per-edit seams). `normalizeMapKey`'s `realpathSync` per call would be wrong on
35
- * the BM25 path. Applied at BOTH the build seam and the per-edit update seam so
36
- * a file's on-disk casing (walk) and its tool-input casing (edit) can no longer
37
- * diverge into a duplicate doc entry with stale postings (the #1025 item #2
38
- * bug). Kept as a named alias so every seam is grep-visible and provably shares
39
- * ONE normalizer with the {@link PathKeyedMap}s below.
40
- */
41
- export const wordIndexKey = normalizeEphemeralMapKey;
42
- // Common language keywords / boilerplate — indexing them adds noise and bloats
43
- // postings without improving relevance. Kept deliberately small and
44
- // language-agnostic.
45
- const STOPWORDS = new Set([
46
- "the",
47
- "and",
48
- "for",
49
- "let",
50
- "var",
51
- "const",
52
- "function",
53
- "return",
54
- "if",
55
- "else",
56
- "import",
57
- "export",
58
- "from",
59
- "class",
60
- "interface",
61
- "type",
62
- "enum",
63
- "new",
64
- "this",
65
- "self",
66
- "void",
67
- "null",
68
- "true",
69
- "false",
70
- "async",
71
- "await",
72
- "public",
73
- "private",
74
- "protected",
75
- "static",
76
- "def",
77
- "fn",
78
- "func",
79
- "struct",
80
- "impl",
81
- "pub",
82
- "use",
83
- "mod",
84
- "in",
85
- "of",
86
- "as",
87
- "is",
88
- "not",
89
- "with",
90
- ]);
91
- // Retain the old directory alternatives and anchors; filename classification
92
- // belongs to file-role (#2928). The relevance multiplier remains 0.3.
93
- const VENDOR_DIR_RE = /(^|[\\/])(?:tests?|__tests__|spec|specs|__mocks__|vendor|node_modules|examples?|fixtures?|\.git|dist|build|coverage)([\\/]|$)/i;
94
- const DOC_FILE_RE = /\.(?:md|mdx|markdown|json|json5|jsonc|txt|rst|lock|ya?ml|toml|csv)$/i;
95
- const TEST_VENDOR_PENALTY = 0.3;
96
- const DOC_FILE_PENALTY = 0.5;
97
- const BM25_K1 = 1.2;
98
- const BM25_B = 0.75;
99
- function isTestOrVendor(file) {
100
- return isTestFileName(file) || VENDOR_DIR_RE.test(file);
101
- }
102
- function isDocFile(file) {
103
- return DOC_FILE_RE.test(file);
104
- }
105
- /**
106
- * Split an identifier into lowercased sub-tokens across camelCase, PascalCase,
107
- * snake_case, kebab-case, dotted, and digit boundaries — and keep the whole
108
- * lowercased identifier too. `getUserByID` → [getuserbyid, get, user, by, id];
109
- * `MAX_RETRY_2` → [max_retry_2, max, retry, 2] (whole kept, plus parts).
110
- */
111
- export function splitIdentifier(identifier) {
112
- const parts = new Set();
113
- const whole = identifier.toLowerCase();
114
- if (whole.length >= 2 && !STOPWORDS.has(whole))
115
- parts.add(whole);
116
- for (const chunk of identifier.split(/[^A-Za-z0-9]+/)) {
117
- if (!chunk)
118
- continue;
119
- const spaced = chunk
120
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2") // camelCase → camel Case
121
- .replace(/[A-Z](?=[A-Z][a-z])/g, "$& ") // HTTPServer → HTTP Server (linear; lookahead avoids super-linear backtracking, S5852)
122
- .replace(/([A-Za-z])([0-9])/g, "$1 $2") // retry2 → retry 2
123
- .replace(/([0-9])([A-Za-z])/g, "$1 $2"); // 2fa → 2 fa
124
- for (const sub of spaced.split(/\s+/)) {
125
- const token = sub.toLowerCase();
126
- if (token.length >= 2 && !STOPWORDS.has(token))
127
- parts.add(token);
128
- }
129
- }
130
- return [...parts];
131
- }
132
- /** Extract identifier-like tokens from a line and split each into sub-tokens. */
133
- export function tokenizeLine(line) {
134
- const tokens = [];
135
- const matches = line.match(/[A-Za-z_$][A-Za-z0-9_$]*/g);
136
- if (!matches)
137
- return tokens;
138
- for (const match of matches) {
139
- for (const token of splitIdentifier(match))
140
- tokens.push(token);
141
- }
142
- return tokens;
143
- }
144
- const WORD_INDEX_BUILD_YIELD_BUDGET_MS = 8;
145
- /**
146
- * A line at or above this length is treated as a yield checkpoint on its own.
147
- * The per-50-lines cadence is a proxy for work that only holds on hand-written
148
- * source: a minified/bundled single-line file up to {@link WORD_INDEX_MAX_BYTES}
149
- * that the source filter did not exclude would otherwise get ZERO in-document
150
- * yields — one unbroken tokenize+push burst far above the budget (#1197 review
151
- * finding 4). Checking the clock after any long line makes the cooperativeness
152
- * claim true per BYTE, not just per line count.
153
- */
154
- const WORD_INDEX_LONG_LINE_YIELD_CHARS = 4096;
155
- /**
156
- * Shared cooperative time-slicer for every bulk word-index path (#1197).
157
- *
158
- * Time-based, not count-based: "yield every N items" is only a bound when the
159
- * per-item cost is bounded too, and the incremental refresh's per-document cost
160
- * grows with the corpus (each replacement filters shared posting arrays). The
161
- * #1197 outage was exactly that shape — 239 stale documents held pi's event loop
162
- * for seconds between two "every 100 files" checkpoints. Callers combine this
163
- * The shared deadline is checked at every bounded work unit so abort latency
164
- * and occupancy are governed by the same monotonic budget.
165
- */
166
- function createEmptyWordIndex(truncated) {
167
- return {
168
- postings: new Map(),
169
- fileTable: new WordIndexFileTable(),
170
- docLengths: new PathKeyedMap(wordIndexKey),
171
- totalTokens: 0,
172
- docCount: 0,
173
- truncated,
174
- forward: new PathKeyedMap(wordIndexKey),
175
- fileMtimes: new PathKeyedMap(wordIndexKey),
176
- fileSizes: new PathKeyedMap(wordIndexKey),
177
- replacementStats: { count: 0, totalMs: 0, maxMs: 0 },
178
- dirtyFiles: new Set(),
179
- postingStoreCount: 0,
180
- recompactFlight: createSingleFlight(),
181
- };
182
- }
183
- export function countWordIndexPostingEntries(index) {
184
- return countPostingEntries(index);
185
- }
186
- /**
187
- * Estimated resident bytes of the index's two packed stores (#2069).
188
- * Reported on the `full_rebuild`, `incremental_refresh`, and `cold_build`
189
- * word-index log records and on the `memory_sample` word-index subsystem, so
190
- * a heap census can be reconciled against the log without taking a snapshot —
191
- * the gap that let #1999 under-count this subsystem by a factor of sixty.
192
- */
193
- export function estimateWordIndexResidentBytes(index) {
194
- return estimateWordIndexStoreBytes(index);
195
- }
196
- /**
197
- * Record a posting whose file id the file table cannot resolve (#2069).
198
- *
199
- * Unreachable by construction: {@link WordIndexFileTable.release} only runs
200
- * after the forward index has enumerated and removed every posting naming that
201
- * id. If it fires, that invariant broke, and the visible symptom is a query
202
- * returning FEWER results with nothing to distinguish it from a smaller match
203
- * set — the clean-versus-errored ambiguity AGENTS.md catalogs as shape 10.
204
- *
205
- * `incrementDegradationCount` rather than `recordDegradationOnce` because
206
- * ranking is a hot loop: the tally stays exact for every occurrence while only
207
- * the first per (kind, orphaned id) writes a durable row. Subject is the id
208
- * itself, so aggregation keeps the discriminating identity.
209
- */
210
- function recordOrphanWordIndexFileId(fileId, token, seam) {
211
- incrementDegradationCount({
212
- kind: "word-index-orphan-file-id",
213
- subject: `fileId:${fileId}`,
214
- reason: `${seam} dropped a posting for token "${token}": the file table has no path for this id`,
215
- });
216
- }
217
- /**
218
- * Decode one token’s postings into `{ file, line }` objects. Allocates, so it
219
- * is for callers that genuinely need the display form (tests, diagnostics) —
220
- * never for the hot ranking loop, which reads the packed lanes directly.
221
- */
222
- export function wordIndexPostingHits(index, token) {
223
- const list = index.postings.get(token);
224
- if (!list)
225
- return [];
226
- const hits = [];
227
- for (let i = 0; i < list.length; i += 1) {
228
- const fileId = list.fileIdAt(i);
229
- const file = index.fileTable.pathFor(fileId);
230
- if (file === undefined) {
231
- recordOrphanWordIndexFileId(fileId, token, "decode");
232
- continue;
233
- }
234
- hits.push({ file, line: list.lineAt(i) });
235
- }
236
- return hits;
237
- }
238
- /**
239
- * Settle a freshly built index's posting store into its compact resident form
240
- * (#2069): re-home every list into one shared arena, sized from the live entry
241
- * counts. That both releases the doubling growth slack a build leaves behind
242
- * and stops the long tail of rare tokens paying for an `ArrayBuffer` header
243
- * each.
244
- */
245
- function compactWordIndexPostings(index) {
246
- compactPostingsIntoArena(index.postings);
247
- index.postingStoreCount = countPostingBackingStores(index.postings);
248
- }
249
- /** Bump the running distinct-backing-store tally by `count` (#2117). */
250
- function notePostingStoresAllocated(index, count) {
251
- index.postingStoreCount = (index.postingStoreCount ?? 0) + count;
252
- }
253
- /**
254
- * Floor for the recompaction gate: below this many backing stores the arena is
255
- * never repacked, so a small index never pays an O(vocab) walk it cannot amortize
256
- * (#2117). This is also the whole gate for indexes whose vocabulary is under
257
- * `WORD_INDEX_RECOMPACT_STORE_FLOOR / WORD_INDEX_RECOMPACT_STORE_FRACTION`
258
- * tokens, keeping the pre-#2246 behaviour for small corpora bit-identical.
259
- */
260
- const WORD_INDEX_RECOMPACT_STORE_FLOOR = 64;
261
- /**
262
- * Recompaction gate as a share of the live vocabulary (#2246). The flat 64-store
263
- * threshold #2117 shipped was crossed by every single edit: one document
264
- * replacement raises `postingStoreCount` by roughly the edited document's
265
- * distinct-token count (hundreds), so the whole O(vocab) arena was rebuilt after
266
- * every edit. Gating on a fraction of `postings.size` instead lets fragmentation
267
- * accumulate proportionally to the index, so recompaction fires once per many
268
- * edits rather than once per edit.
269
- *
270
- * Memory ceiling: the store count can reach a tenth of the vocabulary in private
271
- * stores before a repack, each carrying a fixed header plus growth slack. #2117
272
- * measured the fully-churned ceiling (every token private, store count =
273
- * vocabulary) at +22% resident. The cost does not scale with the store count
274
- * alone, because the tokens that churn most are also the ones carrying the
275
- * longest posting lists. PR #2246's adversarial hot-token probe measured this
276
- * gate at +13.7% resident at rest and +19.9% peak over base — inside the +22%
277
- * #2117 accepted, but not by the wide margin a naive store-count ratio would
278
- * predict. Raising the fraction further would cross that bound.
279
- */
280
- const WORD_INDEX_RECOMPACT_STORE_FRACTION = 0.1;
281
- /**
282
- * Backing-store count that triggers a repack for `index`: a fixed floor, or a
283
- * share of the live vocabulary once the index is large enough for the share to
284
- * exceed the floor. O(1) — reads `postings.size`, never walks the vocabulary.
285
- */
286
- function wordIndexRecompactThreshold(index) {
287
- return Math.max(WORD_INDEX_RECOMPACT_STORE_FLOOR, Math.floor(index.postings.size * WORD_INDEX_RECOMPACT_STORE_FRACTION));
288
- }
289
- /**
290
- * Pack the arena if churn has spread the postings across more than the
291
- * threshold of backing stores (#2117, #2246). Runs the exact O(vocab) store
292
- * count as the authoritative gate — the per-edit hot path uses the O(1)
293
- * `postingStoreCount` estimate, so this expensive walk only happens inside the
294
- * bounded, off-hot-path recompaction it guards. Resets the estimate to the
295
- * true post-compaction count.
296
- */
297
- async function recompactWordIndexPostingsIfNeeded(index, root) {
298
- const threshold = wordIndexRecompactThreshold(index);
299
- const beforeStores = countPostingBackingStores(index.postings);
300
- if (beforeStores <= threshold) {
301
- index.postingStoreCount = beforeStores;
302
- return;
303
- }
304
- const beforeBytes = estimateWordIndexResidentBytes(index);
305
- await compactPostingsIntoArenaCooperatively(index.postings);
306
- const afterStores = countPostingBackingStores(index.postings);
307
- index.postingStoreCount = afterStores;
308
- const afterBytes = estimateWordIndexResidentBytes(index);
309
- const firstForRoot = incrementDegradationCount({
310
- kind: "word-index-arena-recompact",
311
- subject: path.resolve(root),
312
- reason: `arena store threshold ${threshold} exceeded`,
313
- metadata: { beforeBytes, afterBytes, beforeStores, afterStores },
314
- });
315
- // Keep the detailed record bounded per root. The ledger still counts every
316
- // repeated recompaction and emits its own power-of-two summaries.
317
- if (firstForRoot) {
318
- logWordIndex({
319
- phase: "incremental_refresh",
320
- cwd: path.resolve(root),
321
- trigger: "incremental_refresh",
322
- reason: `arena_recompact beforeBytes=${beforeBytes} afterBytes=${afterBytes} beforeStores=${beforeStores} afterStores=${afterStores}`,
323
- });
324
- }
325
- }
326
- /**
327
- * Serialize a recompaction through the per-index async operation queue so no
328
- * async edit or refresh interleaves it (#2117). The queue plus the corruption-
329
- * proof cooperative compactor together close the review-F2 data race: the queue
330
- * excludes async operations, and the compactor's snapshot-and-skip publish
331
- * excludes the synchronous per-edit path the queue cannot see. The per-index
332
- * `recompactFlight` single-flight (#1753) keeps a burst of edits from stacking
333
- * duplicates: a caller that arrives while one recompaction runs joins it rather
334
- * than starting a second. The single slot uses a constant key.
335
- */
336
- function enqueueWordIndexRecompact(index, root) {
337
- if ((index.postingStoreCount ?? 0) <= wordIndexRecompactThreshold(index)) {
338
- return Promise.resolve();
339
- }
340
- const flight = (index.recompactFlight ??= createSingleFlight());
341
- return flight.run("recompact", () => enqueueAsyncWordIndexOperation(index, () => recompactWordIndexPostingsIfNeeded(index, root)));
342
- }
343
- /**
344
- * O(1) hot-path gate for the synchronous per-edit seam. Reads the running
345
- * store-count estimate, never a fresh `Set` over the vocabulary, then defers
346
- * the actual work onto the async queue so it cannot stall the edit (#2117).
347
- */
348
- function scheduleWordIndexRecompact(index, filePath) {
349
- void enqueueWordIndexRecompact(index, path.dirname(filePath));
350
- }
351
- /**
352
- * Test hook: settle any queued arena recompaction for `index`. Draining the
353
- * index's own async operation queue is enough because the recompaction runs
354
- * through it; there is no module-level scheduler to flush.
355
- */
356
- export async function flushWordIndexRecompactionsForTests(index) {
357
- await (asyncWordIndexOperations.get(index) ?? Promise.resolve());
358
- }
359
- function recordWordIndexReplacement(index, startedAt) {
360
- const stats = index.replacementStats ?? { count: 0, totalMs: 0, maxMs: 0 };
361
- const durationMs = Math.max(0, Date.now() - startedAt);
362
- stats.count += 1;
363
- stats.totalMs += durationMs;
364
- stats.maxMs = Math.max(stats.maxMs, durationMs);
365
- index.replacementStats = stats;
366
- }
367
- /** Intern a document path once per index; posting removal compares this id. */
368
- function internWordIndexFile(index, filePath) {
369
- return index.fileTable.intern(wordIndexKey(filePath), filePath);
370
- }
371
- function appendWordIndexPostings(index, filePath, perTokenHits) {
372
- const fileId = internWordIndexFile(index, filePath);
373
- const tokenLineCounts = new Map();
374
- for (const [token, lineNumbers] of perTokenHits) {
375
- let list = index.postings.get(token);
376
- if (list) {
377
- // A grow allocates a fresh private store; count it so the O(1) gate
378
- // tracks fragmentation without re-walking the vocabulary (#2117).
379
- if (list.reserve(list.length + lineNumbers.length)) {
380
- notePostingStoresAllocated(index, 1);
381
- }
382
- }
383
- else {
384
- list = new WordPostingList(token, lineNumbers.length);
385
- index.postings.set(token, list);
386
- notePostingStoresAllocated(index, 1);
387
- }
388
- // `list.token` is the canonical instance, so the forward entry points at
389
- // the same string the postings map is keyed by instead of retaining this
390
- // document’s own tokenizer allocation (#2069).
391
- tokenLineCounts.set(list.token, lineNumbers.length);
392
- for (const line of lineNumbers)
393
- list.push(fileId, line);
394
- }
395
- return tokenLineCounts;
396
- }
397
- function commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt) {
398
- const tokenLineCounts = appendWordIndexPostings(index, doc.path, perTokenHits);
399
- index.docLengths.set(doc.path, docLength);
400
- index.forward.set(doc.path, WordForwardEntry.fromTally(tokenLineCounts));
401
- index.fileMtimes.set(doc.path, -1);
402
- index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
403
- index.dirtyFiles?.add(wordIndexKey(doc.path));
404
- index.totalTokens += docLength;
405
- index.docCount += 1;
406
- recordWordIndexReplacement(index, startedAt);
407
- }
408
- function indexWordLine(index, fileId, line, lineNumber, tokenLineCounts) {
409
- const lineTokens = tokenizeLine(line);
410
- const seenOnLine = new Set();
411
- for (const token of lineTokens) {
412
- if (seenOnLine.has(token))
413
- continue;
414
- seenOnLine.add(token);
415
- let list = index.postings.get(token);
416
- if (!list) {
417
- list = new WordPostingList(token, 1);
418
- index.postings.set(token, list);
419
- }
420
- list.push(fileId, lineNumber);
421
- // Canonical instance, not this line’s fresh tokenizer allocation (#2069).
422
- const canonical = list.token;
423
- tokenLineCounts.set(canonical, (tokenLineCounts.get(canonical) ?? 0) + 1);
424
- }
425
- return lineTokens.length;
426
- }
427
- function finishWordIndexDocument(index, doc, docLength, tokenLineCounts) {
428
- index.docLengths.set(doc.path, docLength);
429
- index.forward?.set(doc.path, WordForwardEntry.fromTally(tokenLineCounts));
430
- index.fileMtimes.set(doc.path, doc.mtimeMs ?? 0);
431
- index.fileSizes.set(doc.path, doc.size ?? Buffer.byteLength(doc.content, "utf-8"));
432
- // A build has no prior wire cache. Keeping the marker makes a future
433
- // serializer cache explicit and gives incremental refresh one uniform seam.
434
- index.dirtyFiles?.add(wordIndexKey(doc.path));
435
- index.totalTokens += docLength;
436
- index.docCount += 1;
437
- }
438
- function indexWordDocument(index, doc) {
439
- const fileId = internWordIndexFile(index, doc.path);
440
- const lines = doc.content.split(/\r?\n/);
441
- const tokenLineCounts = new Map();
442
- let docLength = 0;
443
- for (let i = 0; i < lines.length; i += 1) {
444
- docLength += indexWordLine(index, fileId, lines[i], i + 1, tokenLineCounts);
445
- }
446
- finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
447
- }
448
- export function buildWordIndex(files) {
449
- const index = createEmptyWordIndex(files.truncated ?? false);
450
- for (const doc of files)
451
- indexWordDocument(index, doc);
452
- compactWordIndexPostings(index);
453
- return index;
454
- }
455
- /**
456
- * Cooperative production builder for bulk/cold paths. It produces the exact
457
- * same index as {@link buildWordIndex}, but time-slices both between documents
458
- * and within a large document so startup warmup cannot monopolize pi's TUI
459
- * event loop (#1197). The index is private until this resolves, so a superseded
460
- * build never publishes a partial replacement.
461
- */
462
- export async function buildWordIndexAsync(files, shouldContinue = () => true) {
463
- const index = createEmptyWordIndex(files.truncated ?? false);
464
- const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
465
- for (const doc of files) {
466
- if (!shouldContinue())
467
- throw new Error("word index build superseded");
468
- const fileId = internWordIndexFile(index, doc.path);
469
- const lines = doc.content.split(/\r?\n/);
470
- const tokenLineCounts = new Map();
471
- let docLength = 0;
472
- for (let i = 0; i < lines.length; i += 1) {
473
- const line = lines[i];
474
- docLength += indexWordLine(index, fileId, line, i + 1, tokenLineCounts);
475
- if (line.length >= WORD_INDEX_LONG_LINE_YIELD_CHARS ||
476
- deadline.expired()) {
477
- await yieldIfOverBudget(deadline);
478
- if (!shouldContinue())
479
- throw new Error("word index build superseded");
480
- }
481
- }
482
- finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
483
- if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
484
- if (!shouldContinue())
485
- throw new Error("word index build superseded");
486
- }
487
- }
488
- compactWordIndexPostings(index);
489
- return index;
490
- }
491
- /**
492
- * Remove `filePath`'s postings/docLength/forward entry from `index` in place,
493
- * using the forward index to know exactly which tokens to touch (no scan of
494
- * unrelated postings). No-op (returns false) if the index has no forward
495
- * index yet (pre-phase-2 / deserialized-old-shape) or the file isn't present —
496
- * callers must treat `false` as "fall back to a full rebuild", never as
497
- * silent success.
498
- */
499
- export function removeWordIndexDocument(index, filePath) {
500
- if (!index.forward)
501
- return false;
502
- const tokenLineCounts = index.forward.get(filePath);
503
- if (!tokenLineCounts)
504
- return false;
505
- // `postings` is token-keyed (not a PathKeyedMap), so the file identity it
506
- // carries must be resolved through the SAME normalizer the path maps use —
507
- // otherwise a build-form hit (`SUB/a.ts`) survives an edit-form removal
508
- // (`sub/a.ts`) on a case-insensitive FS and lingers as a stale posting
509
- // (the #1025 item #2 bug this fix closes). Since #2069 that identity is the
510
- // file table’s integer id, so the comparison below is an int compare rather
511
- // than a per-element string compare.
512
- const removedKey = wordIndexKey(filePath);
513
- const removedId = index.fileTable.idFor(removedKey);
514
- if (removedId === undefined)
515
- return false;
516
- for (const token of tokenLineCounts.keys()) {
517
- const list = index.postings.get(token);
518
- if (!list)
519
- continue;
520
- const next = list.withoutFile(removedId);
521
- // `withoutFile` always returns a freshly allocated private store (#2117).
522
- if (next.length > 0) {
523
- index.postings.set(token, next);
524
- notePostingStoresAllocated(index, 1);
525
- }
526
- else
527
- index.postings.delete(token);
528
- }
529
- const docLength = index.docLengths.get(filePath) ?? 0;
530
- index.docLengths.delete(filePath);
531
- index.forward.delete(filePath);
532
- index.fileMtimes.delete(filePath);
533
- index.fileSizes.delete(filePath);
534
- index.dirtyFiles?.add(wordIndexKey(filePath));
535
- index.fileTable.release(removedKey);
536
- index.totalTokens -= docLength;
537
- index.docCount = Math.max(0, index.docCount - 1);
538
- return true;
539
- }
540
- /**
541
- * Add or replace `filePath`'s document in `index` in place: removes the prior
542
- * postings for this file (if any, via {@link removeWordIndexDocument}'s
543
- * forward-index lookup) then re-tokenizes `content` and adds the new
544
- * postings/docLength/forward entry. df/N/totalTokens (avgdl) are updated as
545
- * running stats — no full recompute over other documents.
546
- *
547
- * Returns `false` (no-op on `index`) when the index has no forward index —
548
- * the caller must fall back to a full {@link buildWordIndex} rebuild in that
549
- * case (documented at the `forward` field and enforced by callers, not
550
- * silently patched here: a partially-forward-consistent index would corrupt
551
- * future incremental updates).
552
- */
553
- export function updateWordIndexDocument(index, doc) {
554
- if (!index.forward)
555
- return false;
556
- const startedAt = Date.now();
557
- // Remove the old contribution first (no-op if this is a brand new doc).
558
- if (index.forward.has(doc.path)) {
559
- removeWordIndexDocument(index, doc.path);
560
- }
561
- // Tokenize with line numbers attached (needed for WordHit.line) — this also
562
- // yields the forward-index entry (distinct-line count per token) so the
563
- // tokenization work happens exactly once for this document.
564
- const lines = doc.content.split(/\r?\n/);
565
- const perTokenHits = new Map();
566
- let docLength = 0;
567
- for (let i = 0; i < lines.length; i += 1) {
568
- const lineTokens = tokenizeLine(lines[i]);
569
- docLength += lineTokens.length;
570
- const seenOnLine = new Set();
571
- for (const token of lineTokens) {
572
- if (seenOnLine.has(token))
573
- continue;
574
- seenOnLine.add(token);
575
- const arr = perTokenHits.get(token);
576
- if (arr)
577
- arr.push(i + 1);
578
- else
579
- perTokenHits.set(token, [i + 1]);
580
- }
581
- }
582
- // Per-edit callers generally already have content but not a stat. -1 is an
583
- // impossible real mtime, so it deliberately makes the document stale at the
584
- // next startup refresh (`-1 !== realMtime` always) — unlike 0, which is a
585
- // legal on-disk mtime (SOURCE_DATE_EPOCH=0, archive extraction) and would
586
- // collide, leaving such a file never re-tokenized (#958 review F2).
587
- commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt);
588
- scheduleWordIndexRecompact(index, doc.path);
589
- return true;
590
- }
591
- const asyncWordIndexOperations = new WeakMap();
592
- function enqueueAsyncWordIndexOperation(index, operation) {
593
- const previous = asyncWordIndexOperations.get(index) ?? Promise.resolve();
594
- const run = previous.catch(() => undefined).then(operation);
595
- const settled = run.then(() => undefined, () => undefined);
596
- asyncWordIndexOperations.set(index, settled);
597
- return run.finally(() => {
598
- if (asyncWordIndexOperations.get(index) === settled) {
599
- asyncWordIndexOperations.delete(index);
600
- }
601
- });
602
- }
603
- async function stageWordIndexDocumentRemoval(index, filePath, shouldContinue) {
604
- if (!index.forward)
605
- return undefined;
606
- const tokenLineCounts = index.forward.get(filePath);
607
- if (!tokenLineCounts)
608
- return undefined;
609
- const removedKey = wordIndexKey(filePath);
610
- const removedId = index.fileTable.idFor(removedKey);
611
- if (removedId === undefined)
612
- return undefined;
613
- const postings = new Map();
614
- const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
615
- for (const token of tokenLineCounts.keys()) {
616
- if (!shouldContinue())
617
- throw new Error("word index refresh superseded");
618
- const list = index.postings.get(token);
619
- if (!list)
620
- continue;
621
- // One token's postings, filtered by the SAME packed primitive the
622
- // synchronous path uses, so the staged result is identical rather than
623
- // merely equivalent. The work unit is the token, not the posting element:
624
- // a per-element deadline check cost one `performance.now()` per entry —
625
- // on the order of a million per edit on a 2.2M-posting corpus — which is
626
- // the tax that made this path unusable from the per-edit seam (#2067).
627
- // One list's scan is a bounded, non-yieldable unit; the deadline is
628
- // checked between lists.
629
- const survivors = list.withoutFile(removedId);
630
- postings.set(token, survivors.length > 0 ? survivors : undefined);
631
- if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
632
- if (!shouldContinue())
633
- throw new Error("word index refresh superseded");
634
- }
635
- }
636
- return { postings, docLength: index.docLengths.get(filePath) ?? 0 };
637
- }
638
- function commitWordIndexDocumentRemoval(index, filePath, staged) {
639
- for (const [token, list] of staged.postings) {
640
- // Staged survivors come from `WordPostingList.withoutFile` — a fresh
641
- // private store each, so the O(1) fragmentation gate must count them
642
- // (#2117).
643
- if (list) {
644
- index.postings.set(token, list);
645
- notePostingStoresAllocated(index, 1);
646
- }
647
- else
648
- index.postings.delete(token);
649
- }
650
- index.docLengths.delete(filePath);
651
- index.forward?.delete(filePath);
652
- index.fileMtimes.delete(filePath);
653
- index.fileSizes.delete(filePath);
654
- index.dirtyFiles?.add(wordIndexKey(filePath));
655
- index.fileTable.release(wordIndexKey(filePath));
656
- index.totalTokens -= staged.docLength;
657
- index.docCount = Math.max(0, index.docCount - 1);
658
- }
659
- /** Cooperative, atomically-published removal used only by bulk refresh. */
660
- export async function removeWordIndexDocumentAsync(index, filePath, shouldContinue = () => true) {
661
- return enqueueAsyncWordIndexOperation(index, async () => {
662
- const staged = await stageWordIndexDocumentRemoval(index, filePath, shouldContinue);
663
- if (!staged)
664
- return false;
665
- if (!shouldContinue())
666
- throw new Error("word index refresh superseded");
667
- commitWordIndexDocumentRemoval(index, filePath, staged);
668
- return true;
669
- });
670
- }
671
- /**
672
- * The per-edit replacement primitive the cascade seam calls (#2067).
673
- *
674
- * `updateWordIndexDocumentAsync` plus the arena-recompaction gate the
675
- * synchronous variant carries, which is what makes this the per-edit entry
676
- * point rather than a rename. Bulk refresh must NOT use it: one refreshed
677
- * document raises `postingStoreCount` by roughly its distinct-token count, so a
678
- * per-document schedule would drive an O(vocabulary) recompaction between
679
- * nearly every pair of documents. `refreshWordIndexIncrementally` therefore
680
- * drives `updateWordIndexDocumentAsync` directly and recompacts ONCE after its
681
- * loop; the per-edit seam has no such "after the loop" and schedules here.
682
- */
683
- export async function updateWordIndexDocumentForEdit(index, doc) {
684
- const updated = await updateWordIndexDocumentAsync(index, doc);
685
- if (updated)
686
- scheduleWordIndexRecompact(index, doc.path);
687
- return updated;
688
- }
689
- /** Cooperative replacement whose old/new state is committed without an await. */
690
- export async function updateWordIndexDocumentAsync(index, doc, shouldContinue = () => true) {
691
- return enqueueAsyncWordIndexOperation(index, () => updateWordIndexDocumentAsyncUnsafe(index, doc, shouldContinue));
692
- }
693
- async function updateWordIndexDocumentAsyncUnsafe(index, doc, shouldContinue) {
694
- if (!index.forward)
695
- return false;
696
- const startedAt = Date.now();
697
- const removal = index.forward.has(doc.path)
698
- ? await stageWordIndexDocumentRemoval(index, doc.path, shouldContinue)
699
- : undefined;
700
- if (index.forward.has(doc.path) && !removal)
701
- return false;
702
- const perTokenHits = new Map();
703
- let docLength = 0;
704
- const lines = doc.content.split(/\r?\n/);
705
- await forEachCooperatively(lines, (line, i) => {
706
- const lineTokens = tokenizeLine(line);
707
- docLength += lineTokens.length;
708
- const seenOnLine = new Set();
709
- for (const token of lineTokens) {
710
- if (seenOnLine.has(token))
711
- continue;
712
- seenOnLine.add(token);
713
- const arr = perTokenHits.get(token);
714
- if (arr)
715
- arr.push(i + 1);
716
- else
717
- perTokenHits.set(token, [i + 1]);
718
- }
719
- }, {
720
- budgetMs: WORD_INDEX_BUILD_YIELD_BUDGET_MS,
721
- shouldContinue,
722
- abortMessage: "word index refresh superseded",
723
- });
724
- if (!shouldContinue())
725
- throw new Error("word index refresh superseded");
726
- if (removal)
727
- commitWordIndexDocumentRemoval(index, doc.path, removal);
728
- commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt);
729
- return true;
730
- }
731
- export const WORD_INDEX_MAX_BYTES = 512 * 1024;
732
- /**
733
- * Collect the bounded `{path, content}` doc set `buildWordIndex` consumes —
734
- * the ONE file-walk-and-read implementation shared by every build path
735
- * (session-start task, quick-mode warmup, cold-query background trigger),
736
- * so a bound/skip-rule change lands in one place instead of three copies.
737
- * `shouldContinue` lets a session-scoped caller abort early (session
738
- * superseded) without this module knowing about RuntimeCoordinator.
739
- */
740
- export async function collectWordIndexDocs(root, shouldContinue = () => true, preflightFiles) {
741
- const { collectSourceFilesAsync } = await import("./source-filter.js");
742
- // #747 hardening: pass the cap INTO the walk — without it,
743
- // `collectSourceFilesAsync` defaults to an unbounded traversal and any
744
- // slice below would only trim the result AFTER the whole tree (all of
745
- // $HOME, on a misrooted cwd) has already been enumerated.
746
- // #760: the walk is additionally bounded by source-filter's default
747
- // visited-entry budget (DEFAULT_MAX_SCAN_ENTRIES), so a mixed tree with few
748
- // source files among a huge pile of non-source files can't force a
749
- // full-tree walk either; an index over the truncated list is acceptable.
750
- const maxFiles = preflightFiles?.length ?? getWordIndexMaxFilesDerived(root);
751
- // #894 review: prioritize code kinds within the cap — with broadened
752
- // enumeration, thousands of data/doc files (locale JSON, fixtures, …)
753
- // ahead of the code dirs in walk order could exhaust `maxFiles` and evict
754
- // real source files from the index entirely (DOC_FILE_PENALTY can't
755
- // rescue a file that never made the slice).
756
- const files = preflightFiles
757
- ? preflightFiles.map((file) => file.path)
758
- : await collectSourceFilesAsync(root, {
759
- maxFiles,
760
- prioritizeCodeKinds: true,
761
- });
762
- const truncated = preflightFiles?.truncated ?? files.length === maxFiles;
763
- const docs = Object.assign([], { truncated, skipped: 0 });
764
- if (!shouldContinue())
765
- return docs;
766
- const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
767
- for (const file of files.slice(0, maxFiles)) {
768
- if (!shouldContinue())
769
- return docs;
770
- try {
771
- // A preflight list saves the second full WALK, but its metadata may be
772
- // stale by the time this rebuild reads the file. Re-stat each document at
773
- // content-read time so the stored freshness stamp describes the bytes we
774
- // actually index and the next incremental refresh cannot miss a change in
775
- // the walk-to-read window (#1302).
776
- const stat = fs.statSync(file);
777
- if (stat.size <= WORD_INDEX_MAX_BYTES) {
778
- docs.push({
779
- path: file,
780
- content: fs.readFileSync(file, "utf-8"),
781
- mtimeMs: stat.mtimeMs,
782
- size: stat.size,
783
- });
784
- }
785
- else {
786
- // Over the byte cap — enumerated but deliberately not indexed. Count
787
- // it (L1, #958) so callers can keep coverage honest instead of the
788
- // old silent drop.
789
- docs.skipped += 1;
790
- }
791
- }
792
- catch {
793
- // unreadable / vanished file — skip, but count it (see above).
794
- docs.skipped += 1;
795
- }
796
- if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
797
- if (!shouldContinue())
798
- return docs;
799
- }
800
- }
801
- return docs;
802
- }
803
- // Node's fs.promises.stat runs on libuv's threadpool (default 4 slots), so
804
- // real parallelism tops out there; the surplus workers are queue depth that
805
- // keeps the pool saturated without starving other threadpool consumers.
806
- const WORD_INDEX_STAT_CONCURRENCY = 8;
807
- const WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD = 0.3;
808
- // A ratio alone misclassifies one stale file in a three-file project as dense.
809
- // Below this absolute floor, per-document replacement is bounded and cheaper
810
- // than a second full walk/read even when the percentage is high (#1197).
811
- const WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS = 32;
812
- /**
813
- * Cost of one file's stat+read+decode expressed in tokenizer-token units, so
814
- * the work model below can compare a path that re-reads EVERY file (full
815
- * rebuild) against one that re-reads only the stale files (incremental) in a
816
- * single currency. One read ≈ 300 tokens at the ~1 µs/token measured below.
817
- * Order-of-magnitude by design — it only has to keep a tiny-vocabulary corpus
818
- * (where posting scans are nearly free) from being sent to a full rebuild that
819
- * re-reads everything to save a handful of array filters.
820
- */
821
- const WORD_INDEX_FILE_READ_TOKEN_COST = 300;
822
- /**
823
- * Cost of scanning ONE posting-array element (the `wordIndexKey` compare in
824
- * {@link removeWordIndexDocument}'s filter) relative to tokenizing one token,
825
- * so both sides of the model below are denominated in tokens. Measured on this
826
- * repository's own 2,062-document corpus: 213 ns per element scan against 979 ns
827
- * per token — and the resulting predicted crossover (23 stale documents) lands
828
- * on the directly measured one (~21, from 97.8 ms per document replacement
829
- * against a 1,436 ms rebuild + re-read). The synthetic high-df corpus in the
830
- * #1197 probe gives 0.09, so this is the conservative (incremental-favouring)
831
- * end of the observed range.
832
- *
833
- * #2069 made the scan an Int32 compare over packed lanes instead of a string
834
- * compare over boxed objects, so the real per-element cost is now BELOW this
835
- * constant. The constant is left as measured: over-stating incremental cost
836
- * only ever routes work to a full rebuild, which is the bounded side of the
837
- * decision. Re-measuring it is #2067/#2068 territory, not this change.
838
- */
839
- const WORD_INDEX_POSTING_SCAN_TOKEN_COST = 0.2;
840
- /**
841
- * Refresh a serializer-v2 index from the current bounded source-file set.
842
- * The walk/stat pass is cheap; only sparse stale/new documents are read and
843
- * tokenized. Expected dense/legacy states return `full-required` BEFORE any
844
- * mutation; unexpected corruption or supersession still throws.
845
- */
846
- export async function refreshWordIndexIncrementally(index, root, shouldContinue = () => true, options = {}) {
847
- if (!index.forward || !index.fileMtimes || !index.fileSizes) {
848
- return {
849
- mode: "full-required",
850
- reason: "missing-incremental-metadata",
851
- timings: { sourceWalkMs: 0, statWalkMs: 0, refreshReadsMs: 0 },
852
- };
853
- }
854
- const { collectSourceFilesAsync } = await import("./source-filter.js");
855
- const maxFiles = getWordIndexMaxFilesDerived(root);
856
- const sourceWalkStartMs = Date.now();
857
- const walked = await collectSourceFilesAsync(root, {
858
- maxFiles,
859
- prioritizeCodeKinds: true,
860
- });
861
- const sourceWalkMs = Date.now() - sourceWalkStartMs;
862
- if (!shouldContinue())
863
- throw new Error("word index refresh superseded");
864
- // This set-difference must run in the SAME normalized key space the path
865
- // maps use (#1025 review). Otherwise a file whose stored displayPath became
866
- // the edit form (last-writer-wins after a case/separator-divergent per-edit
867
- // update) no longer string-matches its walk form here — even though
868
- // `fileMtimes.get(walkForm)` folds and hits — which would (1) double-count
869
- // churn (the same file scored as both "dropped" and "added"), possibly
870
- // crossing the threshold and forcing a needless full rebuild, and (2) drop
871
- // then re-add an unchanged file, opening a regression window because the drop
872
- // loop runs BEFORE the re-read: a transient read failure (skipped++) would
873
- // strand the file with no postings. Keying `current` and `oldSet` through
874
- // `wordIndexKey` keeps build/edit/refresh convergent; `current`'s value
875
- // retains the raw walk path for the stat/read and for the display key.
876
- const current = new Map();
877
- const statWalkStartMs = Date.now();
878
- const statFile = options.statFile ?? ((file) => fs.promises.stat(file));
879
- const requestedConcurrency = options.statConcurrency ?? WORD_INDEX_STAT_CONCURRENCY;
880
- const statConcurrency = Number.isFinite(requestedConcurrency) && requestedConcurrency > 0
881
- ? Math.max(1, Math.floor(requestedConcurrency))
882
- : WORD_INDEX_STAT_CONCURRENCY;
883
- const statResults = [];
884
- statResults.length = walked.length;
885
- let cursor = 0;
886
- let superseded = false;
887
- const worker = async () => {
888
- while (true) {
889
- if (!shouldContinue()) {
890
- superseded = true;
891
- return;
892
- }
893
- const slot = cursor++;
894
- if (slot >= walked.length)
895
- return;
896
- const file = walked[slot];
897
- try {
898
- const stat = await statFile(file);
899
- if (stat.size <= WORD_INDEX_MAX_BYTES) {
900
- statResults[slot] = {
901
- path: file,
902
- mtimeMs: stat.mtimeMs,
903
- size: stat.size,
904
- };
905
- }
906
- }
907
- catch {
908
- // A rejection has statSync parity: the file is simply absent.
909
- }
910
- }
911
- };
912
- await Promise.all(Array.from({ length: Math.min(statConcurrency, walked.length) }, () => worker()));
913
- if (superseded || !shouldContinue()) {
914
- throw new Error("word index refresh superseded");
915
- }
916
- const statWalkMs = Date.now() - statWalkStartMs;
917
- for (const result of statResults) {
918
- if (result)
919
- current.set(wordIndexKey(result.path), result);
920
- }
921
- const timings = { sourceWalkMs, statWalkMs, refreshReadsMs: 0 };
922
- const preflightFiles = Object.assign([...current.values()], {
923
- truncated: walked.length === maxFiles,
924
- });
925
- const oldSet = new Set([...index.docLengths.keys()].map(wordIndexKey));
926
- let changedSet = 0;
927
- for (const key of oldSet)
928
- if (!current.has(key))
929
- changedSet++;
930
- for (const key of current.keys())
931
- if (!oldSet.has(key))
932
- changedSet++;
933
- const denominator = Math.max(oldSet.size, current.size, 1);
934
- if (changedSet / denominator > WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) {
935
- return {
936
- mode: "full-required",
937
- reason: "file-set-churn",
938
- preflightFiles,
939
- timings,
940
- };
941
- }
942
- // Replacing one document filters every shared posting array for that
943
- // document's tokens. That is excellent for a sparse edit, but repeating it
944
- // for most of the corpus becomes effectively quadratic. Decide the dense
945
- // transition BEFORE dropping or editing anything so the old index remains a
946
- // valid fallback until a separately-built full replacement is ready (#1197).
947
- let staleDocuments = 0;
948
- for (const { path: file, mtimeMs, size } of current.values()) {
949
- if (index.fileMtimes.get(file) !== mtimeMs ||
950
- (index.fileSizes.get(file) ?? -1) !== size) {
951
- staleDocuments += 1;
952
- }
953
- }
954
- // Density alone is NOT a bound (#1197 review finding 1): the per-document cost
955
- // GROWS with the corpus, so a stale set comfortably under any fixed ratio (or
956
- // any fixed absolute ceiling) still costs orders of magnitude more than a
957
- // rebuild on a big enough repository. 800 documents with 239 stale — 29.875%,
958
- // just under the ratio — measured 90.6 s with a 39.6 s synchronous block
959
- // against 1.3 s / 10.6 ms for a full build, and at the 6,000-file cap up to
960
- // 1,799 stale documents would have stayed on that path.
961
- //
962
- // So compare estimated WORK, both sides denominated in tokenizer tokens:
963
- // incremental ≈ stale x (docTokens x postingLength x scanCost + readCost)
964
- // full ≈ totalTokens + corpusSize x readCost
965
- // This is self-bounding in a way no constant is: the incremental path is taken
966
- // only while its total estimated cost stays under ONE full rebuild, so the
967
- // worst case is "about as expensive as rebuilding", whatever the corpus size.
968
- //
969
- // Both statistics come from cheap metadata passes — one over `postings`
970
- // (distinct tokens, reading only `.length`) and one over `forward` (documents,
971
- // reading only `.size`). Neither touches a posting ELEMENT, which is the work
972
- // this decision exists to avoid.
973
- let postingEntries = 0;
974
- let weightedPostingEntries = 0;
975
- for (const list of index.postings.values()) {
976
- postingEntries += list.length;
977
- weightedPostingEntries += list.length * list.length;
978
- }
979
- // The mean posting-array length weighted by OCCURRENCES, not the plain mean:
980
- // a document's tokens are drawn from the frequency distribution, so the arrays
981
- // a removal actually filters skew hard toward the high-document-frequency
982
- // tokens. The unweighted mean (dominated by the long tail of df=1 tokens)
983
- // underestimates the real cost by an order of magnitude.
984
- const expectedPostingLength = postingEntries > 0 ? weightedPostingEntries / postingEntries : 0;
985
- let distinctTokenEntries = 0;
986
- for (const tokenLineCounts of index.forward.values()) {
987
- distinctTokenEntries += tokenLineCounts.size;
988
- }
989
- const expectedDocumentTokens = index.docCount > 0 ? distinctTokenEntries / index.docCount : 0;
990
- const estimatedIncrementalWork = staleDocuments *
991
- (expectedDocumentTokens *
992
- expectedPostingLength *
993
- WORD_INDEX_POSTING_SCAN_TOKEN_COST +
994
- WORD_INDEX_FILE_READ_TOKEN_COST);
995
- const estimatedFullRebuildWork = index.totalTokens + current.size * WORD_INDEX_FILE_READ_TOKEN_COST;
996
- // The document-count floor guards the RATIO test only — its documented job is
997
- // that "one stale file in a three-file project" is 33% but not dense. It must
998
- // NOT gate the work test: a floor is another constant, and letting one
999
- // suppress the work comparison would reintroduce exactly the unbounded-on-the
1000
- // -other-axis shape (31 documents at 98 ms each on this repository's own
1001
- // corpus, and worse as the corpus grows). The work test needs no floor — on a
1002
- // small project a full rebuild is cheap, so it never fires there.
1003
- if ((staleDocuments >= WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS &&
1004
- staleDocuments / Math.max(current.size, 1) >
1005
- WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) ||
1006
- estimatedIncrementalWork > estimatedFullRebuildWork) {
1007
- return {
1008
- mode: "full-required",
1009
- reason: "stale-document-churn",
1010
- preflightFiles,
1011
- timings,
1012
- };
1013
- }
1014
- const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
1015
- let dropped = 0;
1016
- for (const key of oldSet) {
1017
- if (!current.has(key)) {
1018
- // `key` is already folded; removeWordIndexDocument re-folds it
1019
- // idempotently via the PathKeyedMap, so the drop hits the right entry.
1020
- if (!(await removeWordIndexDocumentAsync(index, key, shouldContinue))) {
1021
- throw new Error(`failed to drop word-index document: ${key}`);
1022
- }
1023
- dropped++;
1024
- }
1025
- }
1026
- let refreshed = 0;
1027
- let skipped = 0;
1028
- const refreshReadsStartMs = Date.now();
1029
- for (const { path: file, mtimeMs, size } of current.values()) {
1030
- // #1105: mtime-first, size-second freshness — mtime alone is a stale
1031
- // signal (mtime-preserving content changes serve stale identifiers to
1032
- // symbol_search). Size is free (the stat above already read it) and
1033
- // catches every content change that alters byte length. Both come from the
1034
- // SAME stat, so a mismatch on EITHER axis re-reads. `?? -1` makes a file
1035
- // absent from the (possibly legacy, pre-#1105) size map always count as
1036
- // changed → one-time re-read that repopulates the size, the safe direction.
1037
- if (index.fileMtimes.get(file) !== mtimeMs ||
1038
- (index.fileSizes.get(file) ?? -1) !== size) {
1039
- // A file the walk/stat pass saw can still fail to read here — a
1040
- // transient exclusive lock (antivirus, an editor, a build step) or a
1041
- // file that vanished in the interim. Match collectWordIndexDocs'
1042
- // tolerance: skip this one file and leave its existing posting (and
1043
- // its old mtime, so it is retried next session) rather than aborting
1044
- // the whole incremental pass into a full rebuild (#958 review F1).
1045
- let content;
1046
- try {
1047
- content = fs.readFileSync(file, "utf-8");
1048
- }
1049
- catch {
1050
- skipped++;
1051
- continue;
1052
- }
1053
- if (!(await updateWordIndexDocumentAsync(index, { path: file, content }, shouldContinue))) {
1054
- throw new Error(`failed to refresh word-index document: ${file}`);
1055
- }
1056
- // updateWordIndexDocument stamps mtime=-1 (per-edit convention); the
1057
- // refresh path KNOWS the real on-disk stat, so overwrite both axes with
1058
- // the true values so this document is not needlessly re-read next pass.
1059
- index.fileMtimes.set(file, mtimeMs);
1060
- index.fileSizes.set(file, size);
1061
- refreshed++;
1062
- }
1063
- // Same OR as the stat loop. This is the loop the #1197 outage actually
1064
- // blocked in: replacing one document costs more the larger the corpus is,
1065
- // so a count-only checkpoint let 100 replacements run back to back for
1066
- // seconds. The gate above keeps the stale set sparse; this keeps even a
1067
- // sparse-but-expensive set off the event loop.
1068
- if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
1069
- if (!shouldContinue())
1070
- throw new Error("word index refresh superseded");
1071
- }
1072
- }
1073
- timings.refreshReadsMs = Date.now() - refreshReadsStartMs;
1074
- // Route through the same guarded queue the per-edit seam uses, so a refresh
1075
- // and a concurrent cascade edit cannot both drive a recompaction (#2117).
1076
- await enqueueWordIndexRecompact(index, root);
1077
- index.truncated = walked.length === maxFiles;
1078
- return {
1079
- mode: "incremental",
1080
- refreshed,
1081
- dropped,
1082
- skipped,
1083
- reused: current.size - refreshed - skipped,
1084
- timings,
1085
- };
1086
- }
1087
- const WORD_INDEX_QUERY_FILTER_KEYS = [
1088
- "lang",
1089
- "file",
1090
- "ext",
1091
- ];
1092
- /** Thrown by {@link parseWordIndexQuery}/{@link buildWordIndexQueryFilter} for
1093
- * an unrecognized `key:` prefix or an unrecognized `lang:` value — a query
1094
- * typo fails loudly with the supported list instead of silently degrading
1095
- * into a literal search term (#1450 acceptance criterion). */
1096
- export class WordIndexQueryError extends Error {
1097
- constructor(message) {
1098
- super(message);
1099
- this.name = "WordIndexQueryError";
1100
- }
1101
- }
1102
- const WORD_INDEX_FILTER_TOKEN_RE = /^(-)?([A-Za-z][A-Za-z0-9_-]*):(.+)$/;
1103
- /**
1104
- * Split a word-index query string into plain search terms and `key:value`
1105
- * prefix filters (#1450). Whitespace-delimited, one regex per token — no
1106
- * quoting scheme invented here, matching the pre-existing query path (which
1107
- * never supported quoting either). A `-` immediately before a recognized
1108
- * `key:` negates that filter; a bare `-term` (no colon) is left as an
1109
- * ordinary token, unchanged from prior behavior — `tokenizeLine` already
1110
- * strips the leading `-` when it extracts identifiers.
1111
- *
1112
- * An unrecognized `key:` (e.g. `type:foo`) throws {@link WordIndexQueryError}
1113
- * naming the supported prefixes, rather than falling through as a literal
1114
- * term — a typo must fail loudly, never silently rank as if the filter never
1115
- * existed. An empty value (`lang:`) is not treated as a filter token at all
1116
- * (kept as a term) since there is nothing to filter by.
1117
- */
1118
- export function parseWordIndexQuery(query) {
1119
- const filters = [];
1120
- const termParts = [];
1121
- for (const raw of query.split(/\s+/)) {
1122
- if (!raw)
1123
- continue;
1124
- const match = raw.match(WORD_INDEX_FILTER_TOKEN_RE);
1125
- if (!match) {
1126
- termParts.push(raw);
1127
- continue;
1128
- }
1129
- const [, negation, keyRaw, value] = match;
1130
- const key = keyRaw.toLowerCase();
1131
- if (!value) {
1132
- termParts.push(raw);
1133
- continue;
1134
- }
1135
- if (!WORD_INDEX_QUERY_FILTER_KEYS.includes(key)) {
1136
- // Not a recognized filter key. Ordinary search terms legitimately
1137
- // contain colons (std::vector, error:foo, http://…, C:\ paths, a
1138
- // TODO:tag), and the old tokenizer searched them fine — a hard
1139
- // error here is a usability regression, not typo protection. The
1140
- // token passes through as a plain term; the loud-failure contract
1141
- // survives where it is unambiguous: a KNOWN key with a bad value
1142
- // (lang:notalang) still throws.
1143
- termParts.push(raw);
1144
- continue;
1145
- }
1146
- filters.push({
1147
- key: key,
1148
- value,
1149
- negated: negation === "-",
1150
- });
1151
- }
1152
- return { terms: termParts.join(" "), filters };
1153
- }
1154
- /** `lang:X` → the extension set for FileKind X (case-insensitive), drawn
1155
- * exclusively from `KIND_EXTENSIONS` (clients/file-kinds.ts) — the single
1156
- * source of truth (#894 invariant: no second, hand-maintained language list).
1157
- * An unrecognized kind throws {@link WordIndexQueryError} listing the accepted
1158
- * kinds (KIND_EXTENSIONS' own keys), the same loud-failure contract as an
1159
- * unsupported `key:` prefix. */
1160
- function resolveLangExtensions(value) {
1161
- const kind = value.toLowerCase();
1162
- const extensions = KIND_EXTENSIONS[kind];
1163
- if (!extensions) {
1164
- const known = Object.keys(KIND_EXTENSIONS)
1165
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
1166
- .join(", ");
1167
- throw new WordIndexQueryError(`Unknown lang: "${value}" in word-index query — supported languages: ${known}.`);
1168
- }
1169
- return extensions;
1170
- }
1171
- /** `ext:ts` / `ext:.ts` → normalized to a single leading-dot, lowercased form for extname comparison. */
1172
- function normalizeExtFilterValue(value) {
1173
- return `.${value.replace(/^\.+/, "").toLowerCase()}`;
1174
- }
1175
- function resolveWordIndexFilter(filter) {
1176
- if (filter.key === "lang") {
1177
- const extensions = resolveLangExtensions(filter.value);
1178
- return {
1179
- key: filter.key,
1180
- negated: filter.negated,
1181
- test: (file) => extensions.includes(path.extname(file).toLowerCase()),
1182
- };
1183
- }
1184
- if (filter.key === "ext") {
1185
- const normalized = normalizeExtFilterValue(filter.value);
1186
- return {
1187
- key: filter.key,
1188
- negated: filter.negated,
1189
- test: (file) => path.extname(file).toLowerCase() === normalized,
1190
- };
1191
- }
1192
- // "file": substring match against the index's own normalized display path
1193
- // (#1450 — see the doc comment on {@link buildWordIndexQueryFilter} for the
1194
- // case-behavior choice). Both sides fold through `wordIndexKey` so the
1195
- // comparison matches the SAME normalization the index's own path keys use.
1196
- const needle = wordIndexKey(filter.value);
1197
- return {
1198
- key: filter.key,
1199
- negated: filter.negated,
1200
- test: (_file, displayPath) => displayPath.includes(needle),
1201
- };
1202
- }
1203
- /**
1204
- * Build a pre-ranking predicate from parsed query filters (#1450). Composes
1205
- * (AND) with `searchWordIndex`'s pre-existing `fileFilter` option (#771) —
1206
- * `symbol_search`'s structured `paths`/`lang` params and this query's inline
1207
- * filters both apply when both are present.
1208
- *
1209
- * Semantics: multiple positive filters of the SAME key OR together
1210
- * (`lang:ts lang:go` matches either); filters of DIFFERENT keys AND
1211
- * (`lang:ts file:clients/` requires both); a negated filter always subtracts,
1212
- * regardless of any positive filter sharing its key.
1213
- *
1214
- * All filters are RESOLVED EAGERLY (extension lookups, `WordIndexQueryError`
1215
- * thrown) here at build time — before any candidate file is tested — so a bad
1216
- * `lang:` value fails once, loudly, rather than per-file during the scoring
1217
- * loop.
1218
- *
1219
- * `file:` case behavior: matched via `wordIndexKey` (this module's single
1220
- * path-key normalizer, `normalizeEphemeralMapKey`) on BOTH the candidate path
1221
- * and the filter value — slash-folded everywhere, and case-folded ONLY on
1222
- * win32 (Windows' case-insensitive filesystem), preserved on POSIX. This
1223
- * mirrors the SAME normalization the word index's own path-keyed maps already
1224
- * apply (#1025), rather than inventing a second, divergent case rule for this
1225
- * one filter. The candidate path matched is the RAW index-stored path (not
1226
- * cwd-relativized) — the index already stores paths in the form the caller
1227
- * built it with (relative in tests, absolute in the real project walk), and
1228
- * an absolute path's tail always contains its project-relative suffix, so
1229
- * `file:clients/word-index.ts` still matches naturally either way.
1230
- */
1231
- export function buildWordIndexQueryFilter(filters) {
1232
- if (filters.length === 0)
1233
- return undefined;
1234
- const resolved = filters.map(resolveWordIndexFilter);
1235
- const positivesByKey = new Map();
1236
- const negatives = [];
1237
- for (const r of resolved) {
1238
- if (r.negated) {
1239
- negatives.push(r);
1240
- continue;
1241
- }
1242
- const group = positivesByKey.get(r.key) ?? [];
1243
- group.push(r);
1244
- positivesByKey.set(r.key, group);
1245
- }
1246
- return (file) => {
1247
- const displayPath = wordIndexKey(file);
1248
- for (const negative of negatives) {
1249
- if (negative.test(file, displayPath))
1250
- return false;
1251
- }
1252
- for (const group of positivesByKey.values()) {
1253
- if (!group.some((r) => r.test(file, displayPath)))
1254
- return false;
1255
- }
1256
- return true;
1257
- };
1258
- }
1259
- function combineFileFilters(a, b) {
1260
- if (!a)
1261
- return b;
1262
- if (!b)
1263
- return a;
1264
- return (file) => a(file) && b(file);
1265
- }
1266
- /**
1267
- * Rank files for a query by BM25 over the query's identifier tokens, then apply
1268
- * priors: demote test/vendor and doc/data files, and boost by graph centrality
1269
- * when supplied. Returns the top {@link RankOptions.limit} files, highest first.
1270
- *
1271
- * The query string may mix plain terms with `lang:`/`file:`/`ext:` prefix
1272
- * filters (+ `-` negation, #1450) — parsed by {@link parseWordIndexQuery} and
1273
- * applied as a `fileFilter` (composed, AND, with any caller-supplied one)
1274
- * BEFORE scoring, same as the pre-existing `paths`/`lang` options (#771): a
1275
- * surviving file's score is unaffected by filtering. A query with no filter
1276
- * tokens reproduces prior output byte-for-byte.
1277
- *
1278
- * DF-normalization note: BM25's per-token `idf` is computed from `docFrequency`
1279
- * over the FULL (unfiltered) postings for that token — `fileFilter` (whether
1280
- * from `options.fileFilter` or from this query's inline filters) is applied
1281
- * AFTER `idf` is computed, per file, inside the same loop. This means a
1282
- * filtered query reuses the GLOBAL, corpus-wide document frequency rather than
1283
- * recomputing it over just the filtered subset. That is the pre-existing #771
1284
- * behavior this change does not alter; it is an acceptable approximation
1285
- * (idf reflects true corpus rarity, not an artifact of the filter) and keeps
1286
- * filtered/unfiltered scores for the SAME file directly comparable, which is
1287
- * the property #1450 asks for ("BM25 and centrality stay comparable within
1288
- * the filtered set").
1289
- */
1290
- export function searchWordIndex(index, query, options = {}) {
1291
- const { demoteTestVendor = true, demoteDocs = true, centrality, limit = 20, fileFilter, } = options;
1292
- const parsedQuery = parseWordIndexQuery(query);
1293
- const queryFilter = buildWordIndexQueryFilter(parsedQuery.filters);
1294
- const combinedFilter = combineFileFilters(fileFilter, queryFilter);
1295
- const queryTokens = [...new Set(tokenizeLine(parsedQuery.terms))];
1296
- if (queryTokens.length === 0)
1297
- return [];
1298
- const docCount = index.docCount || 1;
1299
- const avgDocLength = index.totalTokens / docCount || 1;
1300
- const scores = new Map();
1301
- for (const token of queryTokens) {
1302
- const posting = index.postings.get(token);
1303
- if (!posting)
1304
- continue;
1305
- // Grouped by the packed file id, then resolved to the display path once
1306
- // per (token, file) instead of once per posting (#2069). Insertion order,
1307
- // document frequency, and the per-file line lists are identical to the
1308
- // boxed representation, so scores and ordering are unchanged.
1309
- const linesByFileId = new Map();
1310
- for (let i = 0; i < posting.length; i += 1) {
1311
- const fileId = posting.fileIdAt(i);
1312
- const arr = linesByFileId.get(fileId);
1313
- if (arr)
1314
- arr.push(posting.lineAt(i));
1315
- else
1316
- linesByFileId.set(fileId, [posting.lineAt(i)]);
1317
- }
1318
- const docFrequency = linesByFileId.size;
1319
- const idf = Math.log(1 + (docCount - docFrequency + 0.5) / (docFrequency + 0.5));
1320
- for (const [fileId, lines] of linesByFileId) {
1321
- const file = index.fileTable.pathFor(fileId);
1322
- if (file === undefined) {
1323
- // Dropping this silently would shorten the result list with nothing to
1324
- // tell it apart from a smaller match set. Record, then drop.
1325
- recordOrphanWordIndexFileId(fileId, token, "search");
1326
- continue;
1327
- }
1328
- if (combinedFilter && !combinedFilter(file))
1329
- continue;
1330
- const termFrequency = lines.length;
1331
- const docLength = index.docLengths.get(file) ?? avgDocLength;
1332
- const denominator = termFrequency +
1333
- BM25_K1 * (1 - BM25_B + BM25_B * (docLength / avgDocLength));
1334
- const termScore = idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
1335
- const entry = scores.get(file) ?? {
1336
- score: 0,
1337
- hits: 0,
1338
- lines: new Set(),
1339
- };
1340
- entry.score += termScore;
1341
- entry.hits += termFrequency;
1342
- for (const line of lines)
1343
- entry.lines.add(line);
1344
- scores.set(file, entry);
1345
- }
1346
- }
1347
- const results = [];
1348
- for (const [file, entry] of scores) {
1349
- let score = entry.score;
1350
- if (demoteTestVendor && isTestOrVendor(file))
1351
- score *= TEST_VENDOR_PENALTY;
1352
- if (demoteDocs && isDocFile(file))
1353
- score *= DOC_FILE_PENALTY;
1354
- const connections = centrality?.get(file);
1355
- if (connections && connections > 0) {
1356
- score *= 1 + Math.log(1 + connections) / 4;
1357
- }
1358
- results.push({
1359
- file,
1360
- score,
1361
- hits: entry.hits,
1362
- lines: [...entry.lines].sort((a, b) => a - b),
1363
- });
1364
- }
1365
- results.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
1366
- return results.slice(0, Math.max(0, limit));
1367
- }
1368
- /**
1369
- * Build a centrality map (file → importedBy count) keyed by THIS index's file
1370
- * paths, from the project snapshot's `reverseDeps` (importedBy). The snapshot
1371
- * keys are normalized (`normalizeMapKey(resolve(...))`) while the index keys are
1372
- * the raw scanned paths, so the caller injects a `normalizeKey` bridge; it
1373
- * defaults to identity for testing. Pass the result to {@link searchWordIndex}
1374
- * as `centrality` to boost well-connected files. Kept here (not in the engine)
1375
- * so it stays pure + unit-testable without the normalizer dependency.
1376
- */
1377
- export function centralityFromReverseDeps(index, reverseDeps, normalizeKey = (file) => file) {
1378
- const centrality = new Map();
1379
- if (!reverseDeps)
1380
- return centrality;
1381
- for (const file of index.docLengths.keys()) {
1382
- const importers = reverseDeps[normalizeKey(file)];
1383
- if (importers && importers.length > 0) {
1384
- centrality.set(file, importers.length);
1385
- }
1386
- }
1387
- return centrality;
1388
- }
1389
- /** Persisted word-index serialization format version. Bump on breaking format changes. */
1390
- export const WORD_INDEX_FORMAT_VERSION = 2;
1391
- const WORD_INDEX_INT32_MAX = 2_147_483_647;
1392
- // A file id and line fit in one exact Number key through this boundary.
1393
- const WORD_INDEX_PACKED_FILE_ID_MAX = 4_194_303;
1394
- function isCanonicalWordIndexNumber(value) {
1395
- return (typeof value === "number" &&
1396
- Number.isFinite(value) &&
1397
- Number.isSafeInteger(value) &&
1398
- !Object.is(value, -0));
1399
- }
1400
- function isCanonicalWordIndexToken(token) {
1401
- return (typeof token === "string" &&
1402
- token.length >= 2 &&
1403
- token === token.toLowerCase() &&
1404
- /^[a-z0-9_$]+$/.test(token) &&
1405
- !STOPWORDS.has(token));
1406
- }
1407
- const serializedWordIndexCaches = new WeakMap();
1408
- let _lastSerializeWork;
1409
- /** Test-only: work stats for the most recent {@link serializeWordIndex} call. */
1410
- export function getLastWordIndexSerializeWork() {
1411
- return _lastSerializeWork;
1412
- }
1413
- function serializeWordIndexFull(index) {
1414
- const files = [...index.docLengths.keys()];
1415
- // `files` carries whatever spelling `docLengths` last stored, which can
1416
- // differ from the spelling the file table interned. Both fold through
1417
- // `wordIndexKey`, so the slot lookup runs in the folded key space and a
1418
- // separator- or case-divergent spelling still finds its postings (#1025).
1419
- const slotByFileId = new Map();
1420
- files.forEach((file, i) => {
1421
- const fileId = index.fileTable.idFor(wordIndexKey(file));
1422
- if (fileId !== undefined)
1423
- slotByFileId.set(fileId, i);
1424
- });
1425
- const postings = [];
1426
- for (const [token, list] of index.postings) {
1427
- const flat = [];
1428
- for (let i = 0; i < list.length; i += 1) {
1429
- const slot = slotByFileId.get(list.fileIdAt(i));
1430
- if (slot === undefined)
1431
- continue;
1432
- flat.push(slot, list.lineAt(i));
1433
- }
1434
- if (flat.length > 0)
1435
- postings.push([token, flat]);
1436
- }
1437
- const forward = index.forward
1438
- ? files.map((file, i) => [
1439
- i,
1440
- [...(index.forward.get(file)?.entries() ?? [])],
1441
- ])
1442
- : undefined;
1443
- const serialized = {
1444
- version: WORD_INDEX_FORMAT_VERSION,
1445
- files,
1446
- postings,
1447
- docLengths: files.map((file) => index.docLengths.get(file) ?? 0),
1448
- totalTokens: index.totalTokens,
1449
- indexedFileCount: index.docCount,
1450
- truncated: index.truncated,
1451
- fileMtimes: files.map((file) => index.fileMtimes.get(file) ?? 0),
1452
- fileSizes: files.map((file) => index.fileSizes.get(file) ?? 0),
1453
- forward,
1454
- };
1455
- const tokensByFile = new Map();
1456
- if (index.forward) {
1457
- for (const file of files) {
1458
- tokensByFile.set(wordIndexKey(file), new Set(index.forward.get(file)?.keys() ?? []));
1459
- }
1460
- }
1461
- _lastSerializeWork = {
1462
- affectedTokenCount: postings.length,
1463
- tookFullPath: true,
1464
- };
1465
- return { serialized, slotByFileId, tokensByFile };
1466
- }
1467
- /**
1468
- * Refresh the cached wire view without walking untouched posting lanes.
1469
- * Existing files keep their slots, so a document replacement only rebuilds
1470
- * the token lists named by that document's old or new forward entry. A file
1471
- * removal or reordering changes slot identity and deliberately takes the
1472
- * bounded full path.
1473
- */
1474
- function serializeWordIndexIncrementally(index, cache) {
1475
- const files = [...index.docLengths.keys()];
1476
- const priorFiles = cache.serialized.files;
1477
- const currentKeys = new Set(files.map(wordIndexKey));
1478
- const priorKeys = new Set(priorFiles.map(wordIndexKey));
1479
- if ([...priorKeys].some((key) => !currentKeys.has(key))) {
1480
- return serializeWordIndexFull(index);
1481
- }
1482
- // Re-flattening most lanes costs more than rebuilding the compact wire view.
1483
- // The review fixture crosses over between 10% and 100% dirty, so use the
1484
- // midpoint as a conservative bound until a workload-specific model exists.
1485
- const dirty = index.dirtyFiles;
1486
- if (dirty && dirty.size * 2 > files.length) {
1487
- return serializeWordIndexFull(index);
1488
- }
1489
- // A replacement deletes and re-adds a PathKeyedMap entry, which changes Map
1490
- // insertion order. Keep the cached wire slots stable instead of rewriting
1491
- // every posting just because one document changed order.
1492
- const filesInWireOrder = [
1493
- ...priorFiles,
1494
- ...files.filter((file) => !priorKeys.has(wordIndexKey(file))),
1495
- ];
1496
- if (!dirty || dirty.size === 0) {
1497
- _lastSerializeWork = { affectedTokenCount: 0, tookFullPath: false };
1498
- return cache;
1499
- }
1500
- const slotByFileId = new Map(cache.slotByFileId);
1501
- for (let i = priorFiles.length; i < filesInWireOrder.length; i += 1) {
1502
- const fileId = index.fileTable.idFor(wordIndexKey(filesInWireOrder[i]));
1503
- if (fileId !== undefined)
1504
- slotByFileId.set(fileId, i);
1505
- }
1506
- const postings = cache.serialized.postings.slice();
1507
- const postingAt = new Map();
1508
- postings.forEach(([token], i) => postingAt.set(token, i));
1509
- const removedTokens = new Set();
1510
- const tokensByFile = new Map(cache.tokensByFile);
1511
- const fileByKey = new Map(filesInWireOrder.map((file) => [wordIndexKey(file), file]));
1512
- const affectedTokens = new Set();
1513
- for (const fileKey of dirty) {
1514
- const file = fileByKey.get(fileKey);
1515
- const oldTokens = tokensByFile.get(fileKey) ?? new Set();
1516
- const currentEntry = file === undefined ? undefined : index.forward?.get(file);
1517
- const currentTokens = new Set(currentEntry?.keys() ?? []);
1518
- for (const token of oldTokens)
1519
- affectedTokens.add(token);
1520
- for (const token of currentTokens)
1521
- affectedTokens.add(token);
1522
- if (file === undefined)
1523
- tokensByFile.delete(fileKey);
1524
- else
1525
- tokensByFile.set(fileKey, currentTokens);
1526
- }
1527
- for (const token of affectedTokens) {
1528
- const list = index.postings.get(token);
1529
- const flat = [];
1530
- if (list) {
1531
- for (let i = 0; i < list.length; i += 1) {
1532
- const slot = slotByFileId.get(list.fileIdAt(i));
1533
- if (slot !== undefined)
1534
- flat.push(slot, list.lineAt(i));
1535
- }
1536
- }
1537
- const at = postingAt.get(token);
1538
- if (flat.length === 0) {
1539
- if (at !== undefined)
1540
- removedTokens.add(token);
1541
- }
1542
- else if (at === undefined) {
1543
- postings.push([token, flat]);
1544
- postingAt.set(token, postings.length - 1);
1545
- }
1546
- else {
1547
- postings[at] = [postings[at][0], flat];
1548
- }
1549
- }
1550
- const compactedPostings = removedTokens.size
1551
- ? postings.filter(([token]) => !removedTokens.has(token))
1552
- : postings;
1553
- const forward = index.forward
1554
- ? (cache.serialized.forward
1555
- ?.slice()
1556
- .map((entry) => [entry[0], entry[1]]) ?? [])
1557
- : undefined;
1558
- for (const fileKey of dirty) {
1559
- const file = fileByKey.get(fileKey);
1560
- const fileId = file === undefined
1561
- ? undefined
1562
- : index.fileTable.idFor(wordIndexKey(file));
1563
- const slot = fileId === undefined ? undefined : slotByFileId.get(fileId);
1564
- if (slot === undefined || !forward || file === undefined)
1565
- continue;
1566
- forward[slot] = [slot, [...(index.forward?.get(file)?.entries() ?? [])]];
1567
- }
1568
- const serialized = {
1569
- ...cache.serialized,
1570
- files: filesInWireOrder,
1571
- postings: compactedPostings,
1572
- docLengths: filesInWireOrder.map((file) => index.docLengths.get(file) ?? 0),
1573
- fileMtimes: filesInWireOrder.map((file) => index.fileMtimes.get(file) ?? 0),
1574
- fileSizes: filesInWireOrder.map((file) => index.fileSizes.get(file) ?? 0),
1575
- forward,
1576
- totalTokens: index.totalTokens,
1577
- indexedFileCount: index.docCount,
1578
- truncated: index.truncated,
1579
- };
1580
- _lastSerializeWork = {
1581
- affectedTokenCount: affectedTokens.size,
1582
- tookFullPath: false,
1583
- };
1584
- return { serialized, slotByFileId, tokensByFile };
1585
- }
1586
- export function serializeWordIndex(index) {
1587
- const prior = serializedWordIndexCaches.get(index);
1588
- const cache = prior
1589
- ? serializeWordIndexIncrementally(index, prior)
1590
- : serializeWordIndexFull(index);
1591
- serializedWordIndexCaches.set(index, cache);
1592
- index.dirtyFiles?.clear();
1593
- return cache.serialized;
1594
- }
1595
- export function deserializeWordIndex(data) {
1596
- if (!data ||
1597
- data.version !== WORD_INDEX_FORMAT_VERSION ||
1598
- !Array.isArray(data.files) ||
1599
- !Array.isArray(data.postings) ||
1600
- !Array.isArray(data.docLengths) ||
1601
- !Array.isArray(data.fileMtimes)) {
1602
- return null;
1603
- }
1604
- let canonical = data.docLengths.length === data.files.length &&
1605
- data.fileMtimes.length === data.files.length;
1606
- let needsDeepSanitize = !Array.isArray(data.forward);
1607
- if (needsDeepSanitize)
1608
- canonical = false;
1609
- const fileKeys = new Set();
1610
- for (const file of data.files) {
1611
- if (typeof file !== "string")
1612
- return null;
1613
- const key = wordIndexKey(file);
1614
- if (fileKeys.has(key)) {
1615
- canonical = false;
1616
- needsDeepSanitize = true;
1617
- }
1618
- fileKeys.add(key);
1619
- }
1620
- const docLengths = new PathKeyedMap(wordIndexKey);
1621
- // Slot i of the wire `files` array becomes file id `fileIdBySlot[i]`. Two
1622
- // slots whose spellings fold to one key share one id, exactly as a build
1623
- // would intern them, so a divergent snapshot cannot deserialize into two
1624
- // posting identities for one document.
1625
- const fileTable = new WordIndexFileTable();
1626
- const fileIdBySlot = data.files.map((file) => fileTable.intern(wordIndexKey(file), file));
1627
- const slotByFileId = new Map();
1628
- fileIdBySlot.forEach((fileId, slot) => {
1629
- if (!slotByFileId.has(fileId))
1630
- slotByFileId.set(fileId, slot);
1631
- });
1632
- const fileMtimes = new PathKeyedMap(wordIndexKey);
1633
- const fileSizes = new PathKeyedMap(wordIndexKey);
1634
- data.files.forEach((file, i) => {
1635
- const value = data.docLengths[i];
1636
- if (!isCanonicalWordIndexNumber(value) || value < 0)
1637
- canonical = false;
1638
- docLengths.set(file, isCanonicalWordIndexNumber(value) && value >= 0 ? value : 0);
1639
- });
1640
- data.files.forEach((file, i) => {
1641
- const value = data.fileMtimes[i];
1642
- if (typeof value !== "number" || !Number.isFinite(value))
1643
- canonical = false;
1644
- fileMtimes.set(file, typeof value === "number" && Number.isFinite(value) ? value : 0);
1645
- });
1646
- // #1105: `fileSizes` is optional on the wire (pre-#1105 snapshots omit it).
1647
- // Only populate when the array is present AND parallel to `files`; otherwise
1648
- // leave it empty so the refresh gate re-reads every file once to repopulate,
1649
- // rather than trusting a bogus/misaligned size. Never treated as "current".
1650
- if (Array.isArray(data.fileSizes) &&
1651
- data.fileSizes.length === data.files.length) {
1652
- data.files.forEach((file, i) => {
1653
- const value = data.fileSizes?.[i];
1654
- if (!isCanonicalWordIndexNumber(value) || value < 0)
1655
- canonical = false;
1656
- fileSizes.set(file, isCanonicalWordIndexNumber(value) && value >= 0 ? value : 0);
1657
- });
1658
- }
1659
- else {
1660
- canonical = false;
1661
- }
1662
- const postings = new Map();
1663
- const postingTokens = new Set();
1664
- const postingFileCounts = new Map();
1665
- for (const posting of data.postings) {
1666
- if (!Array.isArray(posting) || posting.length !== 2) {
1667
- canonical = false;
1668
- needsDeepSanitize = true;
1669
- continue;
1670
- }
1671
- const [token, flat] = posting;
1672
- if (!isCanonicalWordIndexToken(token) ||
1673
- postingTokens.has(token) ||
1674
- !Array.isArray(flat) ||
1675
- flat.length === 0 ||
1676
- flat.length % 2 !== 0) {
1677
- canonical = false;
1678
- needsDeepSanitize = true;
1679
- continue;
1680
- }
1681
- postingTokens.add(token);
1682
- const lanes = [];
1683
- const seenPairs = new Set();
1684
- const observedCounts = new Map();
1685
- for (let i = 0; i < flat.length; i += 2) {
1686
- const slot = flat[i];
1687
- const line = flat[i + 1];
1688
- if (!isCanonicalWordIndexNumber(slot) ||
1689
- slot < 0 ||
1690
- slot >= fileIdBySlot.length ||
1691
- !isCanonicalWordIndexNumber(line) ||
1692
- line < 1 ||
1693
- line > WORD_INDEX_INT32_MAX) {
1694
- canonical = false;
1695
- needsDeepSanitize = true;
1696
- continue;
1697
- }
1698
- const fileId = fileIdBySlot[slot];
1699
- const pairKey = fileId <= WORD_INDEX_PACKED_FILE_ID_MAX
1700
- ? fileId * (WORD_INDEX_INT32_MAX + 1) + line
1701
- : `${fileId}:${line}`;
1702
- if (seenPairs.has(pairKey)) {
1703
- canonical = false;
1704
- needsDeepSanitize = true;
1705
- }
1706
- seenPairs.add(pairKey);
1707
- lanes.push(fileId, line);
1708
- observedCounts.set(fileId, (observedCounts.get(fileId) ?? 0) + 1);
1709
- }
1710
- if (lanes.length === 0)
1711
- continue;
1712
- const list = WordPostingList.fromLanes(token, lanes);
1713
- postings.set(token, list);
1714
- postingFileCounts.set(token, observedCounts);
1715
- }
1716
- // Every list above was sized exactly from the wire payload, so there is no
1717
- // growth slack to release — but the per-list `ArrayBuffer` headers are the
1718
- // same third-of-the-store cost a fresh build pays, and a deserialized index
1719
- // is the LONG-lived one (a warm session reuses it across turns).
1720
- compactPostingsIntoArena(postings);
1721
- let forward;
1722
- if (Array.isArray(data.forward)) {
1723
- const candidate = data.files.map(() => new Map());
1724
- const forwardTotals = new Map();
1725
- if (data.forward.length !== data.files.length)
1726
- canonical = false;
1727
- if (data.forward.length !== data.files.length)
1728
- needsDeepSanitize = true;
1729
- for (const [entryIndex, entry] of data.forward.entries()) {
1730
- if (!Array.isArray(entry) || entry.length !== 2) {
1731
- canonical = false;
1732
- needsDeepSanitize = true;
1733
- continue;
1734
- }
1735
- const [fileIdx, tokenCounts] = entry;
1736
- if (!isCanonicalWordIndexNumber(fileIdx) ||
1737
- fileIdx !== entryIndex ||
1738
- fileIdx < 0 ||
1739
- fileIdx >= data.files.length ||
1740
- !Array.isArray(tokenCounts)) {
1741
- canonical = false;
1742
- needsDeepSanitize = true;
1743
- continue;
1744
- }
1745
- for (const pair of tokenCounts) {
1746
- if (!Array.isArray(pair) ||
1747
- pair.length !== 2 ||
1748
- !isCanonicalWordIndexToken(pair[0]) ||
1749
- !isCanonicalWordIndexNumber(pair[1]) ||
1750
- pair[1] < 1 ||
1751
- pair[1] > WORD_INDEX_INT32_MAX ||
1752
- candidate[fileIdx].has(pair[0])) {
1753
- canonical = false;
1754
- needsDeepSanitize = true;
1755
- continue;
1756
- }
1757
- candidate[fileIdx].set(pair[0], pair[1]);
1758
- forwardTotals.set(pair[0], (forwardTotals.get(pair[0]) ?? 0) + pair[1]);
1759
- }
1760
- }
1761
- let forwardMatchesPostings = true;
1762
- for (const [token, list] of postings) {
1763
- if (forwardTotals.get(token) !== list.length) {
1764
- forwardMatchesPostings = false;
1765
- }
1766
- const observed = postingFileCounts.get(token);
1767
- if (!observed) {
1768
- forwardMatchesPostings = false;
1769
- continue;
1770
- }
1771
- for (const [fileId, count] of observed) {
1772
- const slot = slotByFileId.get(fileId);
1773
- if (slot === undefined || candidate[slot].get(token) !== count) {
1774
- forwardMatchesPostings = false;
1775
- }
1776
- }
1777
- }
1778
- for (const token of forwardTotals.keys()) {
1779
- if (!postingTokens.has(token))
1780
- forwardMatchesPostings = false;
1781
- }
1782
- if (!forwardMatchesPostings)
1783
- canonical = false;
1784
- if (!forwardMatchesPostings)
1785
- needsDeepSanitize = true;
1786
- forward = new PathKeyedMap(wordIndexKey);
1787
- for (let i = 0; i < data.files.length; i += 1) {
1788
- forward.set(data.files[i], WordForwardEntry.fromTally(candidate[i]));
1789
- }
1790
- }
1791
- else if (Object.prototype.hasOwnProperty.call(data, "forward")) {
1792
- canonical = false;
1793
- needsDeepSanitize = true;
1794
- }
1795
- if (needsDeepSanitize) {
1796
- const actualForwardCounts = data.files.map(() => new Map());
1797
- for (const [token, list] of postings) {
1798
- const seenPairs = new Set();
1799
- const sanitized = new WordPostingList(token, list.length);
1800
- for (let i = 0; i < list.length; i += 1) {
1801
- const fileId = list.fileIdAt(i);
1802
- const line = list.lineAt(i);
1803
- const pairKey = `${fileId}:${line}`;
1804
- if (seenPairs.has(pairKey))
1805
- continue;
1806
- seenPairs.add(pairKey);
1807
- sanitized.push(fileId, line);
1808
- const slot = slotByFileId.get(fileId);
1809
- if (slot !== undefined) {
1810
- const counts = actualForwardCounts[slot];
1811
- counts.set(token, (counts.get(token) ?? 0) + 1);
1812
- }
1813
- }
1814
- postings.set(token, sanitized);
1815
- }
1816
- compactPostingsIntoArena(postings);
1817
- if (Array.isArray(data.forward)) {
1818
- forward = new PathKeyedMap(wordIndexKey);
1819
- for (let i = 0; i < data.files.length; i += 1) {
1820
- const canonicalSlot = slotByFileId.get(fileIdBySlot[i]) ?? i;
1821
- forward.set(data.files[i], WordForwardEntry.fromTally(actualForwardCounts[canonicalSlot]));
1822
- }
1823
- }
1824
- }
1825
- const expectedTotalTokens = [...docLengths.values()].reduce((total, length) => total + length, 0);
1826
- const totalTokens = isCanonicalWordIndexNumber(expectedTotalTokens)
1827
- ? expectedTotalTokens
1828
- : 0;
1829
- if (!isCanonicalWordIndexNumber(data.totalTokens) ||
1830
- data.totalTokens !== totalTokens) {
1831
- canonical = false;
1832
- }
1833
- if (typeof data.indexedFileCount !== "number" ||
1834
- data.indexedFileCount !== docLengths.size) {
1835
- canonical = false;
1836
- }
1837
- if (typeof data.truncated !== "boolean")
1838
- canonical = false;
1839
- const index = {
1840
- postings,
1841
- fileTable,
1842
- docLengths,
1843
- totalTokens,
1844
- docCount: docLengths.size,
1845
- truncated: data.truncated === true,
1846
- forward,
1847
- fileMtimes,
1848
- fileSizes,
1849
- // Postings were just packed into one arena above, so the running gate
1850
- // starts from the exact post-compaction store count (#2117).
1851
- postingStoreCount: countPostingBackingStores(postings),
1852
- recompactFlight: createSingleFlight(),
1853
- // A deserialized index is a fresh object distinct from whichever index
1854
- // produced `data`. Without this, every per-edit dirty mark on a
1855
- // session-start reload (#2068) is a no-op against `undefined`, and
1856
- // serializeWordIndexIncrementally's `!dirty` branch then returns the
1857
- // stale cached wire view forever, silently dropping every later edit
1858
- // from the persisted snapshot.
1859
- dirtyFiles: new Set(),
1860
- };
1861
- // Seed the wire cache from the snapshot just loaded so the FIRST persist
1862
- // of a reloaded session (session-start's `snapshotSaveSyncMs`, #2068) also
1863
- // takes the bounded incremental path instead of paying a full rebuild —
1864
- // mirrors exactly what one priming `serializeWordIndex` call would cache.
1865
- const tokensByFile = new Map();
1866
- if (forward) {
1867
- for (const file of data.files) {
1868
- tokensByFile.set(wordIndexKey(file), new Set(forward.get(file)?.keys() ?? []));
1869
- }
1870
- }
1871
- if (canonical) {
1872
- serializedWordIndexCaches.set(index, {
1873
- serialized: data,
1874
- slotByFileId,
1875
- tokensByFile,
1876
- });
1877
- }
1878
- return index;
1879
- }
1880
- const buildStatuses = new Map();
1881
- export function getWordIndexBuildStatus(cwd) {
1882
- return buildStatuses.get(path.resolve(cwd));
1883
- }
1884
- /** Test-only: reset the in-flight-build guard between test files/cases. */
1885
- export function _resetWordIndexBuildGuardForTests() {
1886
- buildStatuses.clear();
1887
- }
1888
- /**
1889
- * Fire a one-time bounded background build for `cwd` if one isn't already
1890
- * running. Persists into the existing project snapshot (preserving its other
1891
- * fields) so the next query — or the next real session — picks it up. Errors
1892
- * are swallowed (this is best-effort warmth, not a request the caller is
1893
- * waiting on); the guard always clears in a `finally` so a failed build can be
1894
- * retried by a later query.
1895
- */
1896
- export function triggerBackgroundWordIndexBuild(cwd, dbg, options = {}) {
1897
- const key = path.resolve(cwd);
1898
- // #747 hardening: this trigger is the one word-index build path with NO
1899
- // session lifecycle in front of it (cold `symbol_search` queries, in-process
1900
- // and via MCP where cwd can be a raw tool argument) — the session-start and
1901
- // quick-mode-warmup builds sit behind `canWarmCaches`, which already refuses
1902
- // a home-rooted cwd. Apply the same `isAtOrAboveHomeDir` ceiling here so a
1903
- // cold query from $HOME never starts a whole-home walk-and-read.
1904
- if (isAtOrAboveHomeDir(key, options.homeDir)) {
1905
- const reason = `root at/above home directory (${key})`;
1906
- dbg?.(`word-index cold-build: skipped — ${reason}`);
1907
- logWordIndex({
1908
- phase: "cold_build_refused",
1909
- cwd: key,
1910
- trigger: "cold_query",
1911
- reason,
1912
- });
1913
- const status = { state: "refused", reason };
1914
- buildStatuses.set(key, status);
1915
- return status;
1916
- }
1917
- const current = buildStatuses.get(key);
1918
- if (current?.state === "building")
1919
- return current;
1920
- const status = { state: "building" };
1921
- buildStatuses.set(key, status);
1922
- void (async () => {
1923
- const startMs = Date.now();
1924
- try {
1925
- const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION, } = await import("./project-snapshot.js");
1926
- const docs = await collectWordIndexDocs(key);
1927
- const index = await buildWordIndexAsync(docs);
1928
- const existing = loadProjectSnapshot(key);
1929
- const snapshot = existing ?? {
1930
- version: PROJECT_SNAPSHOT_VERSION,
1931
- projectRoot: key,
1932
- generatedAt: new Date().toISOString(),
1933
- seq: 0,
1934
- files: {},
1935
- symbols: {},
1936
- reverseDeps: {},
1937
- cachedExports: [],
1938
- };
1939
- snapshot.generatedAt = new Date().toISOString();
1940
- snapshot.wordIndex = serializeWordIndex(index);
1941
- saveProjectSnapshot(key, snapshot);
1942
- dbg?.(`word-index cold-build: ${index.docCount} files, ${index.postings.size} tokens (${Date.now() - startMs}ms)`);
1943
- // M2, #958: durable decision/coverage record for the MCP-critical cold
1944
- // path (this is where symbol_search reads the index and dbg is a no-op).
1945
- logWordIndex({
1946
- phase: "cold_build",
1947
- cwd: key,
1948
- trigger: "cold_query",
1949
- durationMs: Date.now() - startMs,
1950
- indexedFileCount: index.docCount,
1951
- tokens: index.postings.size,
1952
- postingEntries: countWordIndexPostingEntries(index),
1953
- residentBytes: estimateWordIndexResidentBytes(index),
1954
- truncated: index.truncated,
1955
- skipped: docs.skipped,
1956
- });
1957
- buildStatuses.delete(key);
1958
- }
1959
- catch (err) {
1960
- const reason = err instanceof Error ? err.message : String(err);
1961
- buildStatuses.set(key, { state: "failed", reason });
1962
- dbg?.(`word-index cold-build: failed: ${reason}`);
1963
- logWordIndex({
1964
- phase: "cold_build_failed",
1965
- cwd: key,
1966
- trigger: "cold_query",
1967
- durationMs: Date.now() - startMs,
1968
- error: reason,
1969
- });
1970
- }
1971
- })();
1972
- return status;
1973
- }
1974
- // --- Debounced per-edit persist (#348 phase 2) --------------------------------
1975
- //
1976
- // The per-edit seam (dispatch/integration.ts) updates `runtime.wordIndex` in
1977
- // memory on every write, same as the review graph's per-edit rebuild. Without
1978
- // coalescing, persisting that in-memory index on every single edit would mean
1979
- // one full-snapshot JSON.stringify+write per keystroke-adjacent edit — the
1980
- // same OOM-risking spike the graph's #260 circuit-breaker exists to prevent.
1981
- // This reuses `createDebounceScheduler` (persist-debounce.ts) rather than
1982
- // growing a second copy of the graph's bespoke pending-map+timer bookkeeping;
1983
- // only the "write" callback differs, because the target differs: the graph
1984
- // owns its own cache file, but the word index must merge into the SHARED
1985
- // project-snapshot file via `saveRuntimeProjectSnapshot`/`saveProjectSnapshot`
1986
- // (preserving unrelated snapshot fields, and honoring the seq-laundering guard
1987
- // in project-snapshot.ts — see saveRuntimeProjectSnapshot's comment).
1988
- const WORD_INDEX_PERSIST_DEBOUNCE_MS_DEFAULT = 1500;
1989
- function wordIndexPersistDebounceMs() {
1990
- const raw = Number(process.env.PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS);
1991
- return Number.isFinite(raw) && raw >= 0
1992
- ? raw
1993
- : WORD_INDEX_PERSIST_DEBOUNCE_MS_DEFAULT;
1994
- }
1995
- let wordIndexPersistScheduler;
1996
- function getWordIndexPersistScheduler() {
1997
- if (wordIndexPersistScheduler)
1998
- return wordIndexPersistScheduler;
1999
- wordIndexPersistScheduler = createDebounceScheduler({
2000
- debounceMs: wordIndexPersistDebounceMs,
2001
- write(_key, pending) {
2002
- void writeWordIndexSnapshot(pending.cwd, pending.index, pending.dbg);
2003
- },
2004
- });
2005
- return wordIndexPersistScheduler;
2006
- }
2007
- async function writeWordIndexSnapshot(cwd, index, dbg) {
2008
- const persistStartedAt = Date.now();
2009
- try {
2010
- const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION, } = await import("./project-snapshot.js");
2011
- const existing = loadProjectSnapshot(cwd);
2012
- const snapshot = existing ?? {
2013
- version: PROJECT_SNAPSHOT_VERSION,
2014
- projectRoot: path.resolve(cwd),
2015
- generatedAt: new Date().toISOString(),
2016
- seq: 0,
2017
- files: {},
2018
- symbols: {},
2019
- reverseDeps: {},
2020
- cachedExports: [],
2021
- };
2022
- snapshot.generatedAt = new Date().toISOString();
2023
- const serializeStartedAt = performance.now();
2024
- snapshot.wordIndex = serializeWordIndex(index);
2025
- const serializeMs = performance.now() - serializeStartedAt;
2026
- saveProjectSnapshot(cwd, snapshot);
2027
- const writeMs = performance.now() - serializeStartedAt - serializeMs;
2028
- dbg?.(`word-index persist: ${index.docCount} files, ${index.postings.size} tokens`);
2029
- // #958 review F1: durably record persist SUCCESS too, not just failures —
2030
- // in the MCP host (`dbg` is a no-op) this is the only signal that the index
2031
- // is actually being kept fresh across edits. Debounced (~1.5s), so not
2032
- // spammy. Mirrors the review graph's persist_succeeded.
2033
- logWordIndex({
2034
- phase: "persist_succeeded",
2035
- cwd: path.resolve(cwd),
2036
- trigger: "per_edit",
2037
- durationMs: Date.now() - persistStartedAt,
2038
- serializeMs,
2039
- writeMs,
2040
- indexedFileCount: index.docCount,
2041
- tokens: index.postings.size,
2042
- postingEntries: countWordIndexPostingEntries(index),
2043
- residentBytes: estimateWordIndexResidentBytes(index),
2044
- replacementCount: index.replacementStats?.count ?? 0,
2045
- totalReplacementMs: index.replacementStats?.totalMs ?? 0,
2046
- maxReplacementMs: index.replacementStats?.maxMs ?? 0,
2047
- });
2048
- index.replacementStats = { count: 0, totalMs: 0, maxMs: 0 };
2049
- }
2050
- catch (err) {
2051
- dbg?.(`word-index persist: failed: ${err}`);
2052
- // M3, #958: a swallowed persist means every LATER symbol_search reads a
2053
- // stale index with no trace — the exact silent-failure this durable log
2054
- // exists to surface (dbg is a no-op in the MCP host).
2055
- logWordIndex({
2056
- phase: "persist_failed",
2057
- cwd: path.resolve(cwd),
2058
- trigger: "per_edit",
2059
- indexedFileCount: index.docCount,
2060
- error: err instanceof Error ? err.message : String(err),
2061
- });
2062
- }
2063
- }
2064
- /**
2065
- * Schedule a debounced persist of `index` for `cwd`, coalescing a burst of
2066
- * per-edit updates into one write after a quiet window (default 1500ms,
2067
- * override via `PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS`, mirroring the review
2068
- * graph's `PI_LENS_GRAPH_PERSIST_DEBOUNCE_MS`). Merges through the same
2069
- * `saveProjectSnapshot` path phase 1 uses — preserves unrelated snapshot
2070
- * fields and respects the seq-laundering guard (only ever writes wordIndex
2071
- * for the CURRENT in-memory index, never re-stamps a stale one).
2072
- */
2073
- export function scheduleWordIndexPersist(cwd, index, dbg) {
2074
- const key = path.resolve(cwd);
2075
- getWordIndexPersistScheduler().schedule(key, { cwd: key, index, dbg });
2076
- }
2077
- /** Test hook: force any pending debounced word-index persist to write immediately. */
2078
- export function flushWordIndexPersistsForTests() {
2079
- getWordIndexPersistScheduler().flushAll();
2080
- }