@herbertgao/pi-extensions 2026.8.6 → 2026.8.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (343) hide show
  1. package/README.md +5 -5
  2. package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
  3. package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
  4. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
  5. package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
  6. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
  7. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
  8. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
  9. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
  10. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
  11. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
  12. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
  13. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
  14. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
  15. package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
  16. package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
  17. package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
  18. package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
  19. package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
  20. package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
  21. package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
  22. package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
  23. package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
  24. package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
  25. package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
  26. package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
  27. package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
  28. package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
  29. package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
  30. package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
  31. package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
  32. package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
  33. package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
  34. package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +1 -0
  35. package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
  36. package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
  37. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
  38. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +2 -1
  39. package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
  40. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
  41. package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
  42. package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
  43. package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
  44. package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
  45. package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
  46. package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
  47. package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
  48. package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
  49. package/node_modules/@narumitw/pi-btw/README.md +120 -111
  50. package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
  51. package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
  52. package/node_modules/@narumitw/pi-btw/package.json +10 -6
  53. package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
  54. package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
  55. package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
  56. package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
  57. package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
  58. package/node_modules/pi-lens/CHANGELOG.md +909 -0
  59. package/node_modules/pi-lens/README.md +11 -3
  60. package/node_modules/pi-lens/config/ruff/core.toml +42 -0
  61. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
  62. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
  63. package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
  64. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
  65. package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
  66. package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
  67. package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
  68. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
  69. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
  70. package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
  71. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
  72. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
  73. package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
  74. package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
  75. package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
  76. package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
  77. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
  78. package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
  79. package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
  80. package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
  81. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
  82. package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
  83. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
  84. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
  85. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
  86. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
  87. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
  88. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
  89. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
  90. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
  91. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
  92. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
  93. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
  94. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
  95. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
  96. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
  97. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
  98. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
  99. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
  100. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
  101. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
  102. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
  103. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
  104. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
  105. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
  106. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
  107. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
  108. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
  109. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
  110. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
  111. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
  112. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
  113. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
  114. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
  115. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
  116. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
  117. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
  118. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
  119. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
  120. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
  121. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
  122. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
  123. package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
  124. package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
  125. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
  126. package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
  127. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
  128. package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
  129. package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
  130. package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
  131. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
  132. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
  133. package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
  134. package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
  135. package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
  136. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
  137. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
  138. package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
  139. package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
  140. package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
  141. package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
  142. package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
  143. package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
  144. package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
  145. package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
  146. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
  147. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
  148. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
  149. package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
  150. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
  151. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
  152. package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
  153. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
  154. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
  155. package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
  156. package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
  157. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
  158. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
  159. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
  160. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
  161. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
  162. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
  163. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
  164. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
  165. package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
  166. package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
  167. package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
  168. package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
  169. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
  170. package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
  171. package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
  172. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
  173. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
  174. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
  175. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
  176. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
  177. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
  178. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
  179. package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
  180. package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
  181. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
  182. package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
  183. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
  184. package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
  185. package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
  186. package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
  187. package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
  188. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
  189. package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
  190. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
  191. package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
  192. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
  193. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
  194. package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
  195. package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
  196. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
  197. package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
  198. package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
  199. package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
  200. package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
  201. package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
  202. package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
  203. package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
  204. package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
  205. package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
  206. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
  207. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
  208. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
  209. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
  210. package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
  211. package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
  212. package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
  213. package/node_modules/pi-lens/dist/index.js +67738 -55739
  214. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
  215. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
  216. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
  217. package/node_modules/pi-lens/docs/agent-guide.md +6 -1
  218. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
  219. package/node_modules/pi-lens/docs/dispositions.md +21 -0
  220. package/node_modules/pi-lens/docs/features.md +19 -5
  221. package/node_modules/pi-lens/docs/globalconfig.md +3 -0
  222. package/node_modules/pi-lens/docs/language-coverage.md +71 -5
  223. package/node_modules/pi-lens/docs/mcp.md +2 -2
  224. package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
  225. package/node_modules/pi-lens/docs/settings.md +19 -1
  226. package/node_modules/pi-lens/docs/usage.md +1 -0
  227. package/node_modules/pi-lens/package.json +10 -6
  228. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
  229. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
  230. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
  231. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
  232. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
  233. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
  234. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
  235. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
  236. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
  237. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
  238. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
  239. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
  240. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
  241. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
  242. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
  243. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
  244. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
  245. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
  246. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
  247. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
  248. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
  249. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
  250. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
  251. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
  252. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
  253. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
  254. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
  255. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
  256. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
  257. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
  258. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
  259. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
  260. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
  261. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
  262. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
  263. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
  264. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
  265. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
  266. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
  267. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
  268. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
  269. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
  270. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
  271. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
  272. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
  273. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
  274. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
  275. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
  276. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
  277. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
  278. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
  279. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
  280. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
  281. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
  282. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
  283. package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
  284. package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
  285. package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
  286. package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
  287. package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
  288. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
  289. package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
  290. package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
  291. package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
  292. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
  293. package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
  294. package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
  295. package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
  296. package/node_modules/pi-mcp-adapter/README.md +47 -5
  297. package/node_modules/pi-mcp-adapter/cli.js +163 -1
  298. package/node_modules/pi-mcp-adapter/commands.ts +128 -22
  299. package/node_modules/pi-mcp-adapter/config.ts +25 -7
  300. package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
  301. package/node_modules/pi-mcp-adapter/index.ts +219 -32
  302. package/node_modules/pi-mcp-adapter/init.ts +36 -4
  303. package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
  304. package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
  305. package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
  306. package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
  307. package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
  308. package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
  309. package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
  310. package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
  311. package/node_modules/pi-mcp-adapter/package.json +3 -1
  312. package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
  313. package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
  314. package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
  315. package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
  316. package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
  317. package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
  318. package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
  319. package/node_modules/pi-mcp-adapter/types.ts +2 -0
  320. package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
  321. package/node_modules/pi-mcp-adapter/utils.ts +70 -0
  322. package/node_modules/pi-web-access/CHANGELOG.md +23 -0
  323. package/node_modules/pi-web-access/README.md +27 -9
  324. package/node_modules/pi-web-access/anysearch.ts +4 -2
  325. package/node_modules/pi-web-access/brave.ts +16 -4
  326. package/node_modules/pi-web-access/curator-page.ts +6 -3
  327. package/node_modules/pi-web-access/curator-server.ts +4 -1
  328. package/node_modules/pi-web-access/exa.ts +18 -7
  329. package/node_modules/pi-web-access/extract.ts +28 -3
  330. package/node_modules/pi-web-access/gemini-search.ts +16 -4
  331. package/node_modules/pi-web-access/index.ts +41 -57
  332. package/node_modules/pi-web-access/package.json +1 -1
  333. package/node_modules/pi-web-access/page-query.ts +4 -2
  334. package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
  335. package/node_modules/pi-web-access/parallel.ts +2 -2
  336. package/node_modules/pi-web-access/query-rewrite.ts +51 -0
  337. package/node_modules/pi-web-access/serper.ts +211 -0
  338. package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
  339. package/node_modules/pi-web-access/summary-review.ts +44 -22
  340. package/node_modules/pi-web-access/tavily.ts +16 -4
  341. package/node_modules/pi-web-access/utils.ts +87 -0
  342. package/node_modules/pi-web-access/valyu.ts +199 -0
  343. package/package.json +13 -13
@@ -2,6 +2,48 @@ import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import { PathKeyedMap } from "../path-keyed-map.js";
4
4
  import { normalizeEphemeralMapKey } from "../path-utils.js";
5
+ /**
6
+ * #1549: the ONE rule that decides whether a touch is inconclusive, and who is
7
+ * responsible.
8
+ *
9
+ * The rule this replaced was `notifyWriteTimedOut || diagnosticsTimedOut` with
10
+ * both flags TOUCH-WIDE over every spawned server, so one slow auxiliary
11
+ * discarded every good answer in the touch: a clean typescript reply beside an
12
+ * opengrep scan that needed 2s read as "nothing is known about this file". Over
13
+ * 6,079 cascade neighbour sweeps that produced a 97.6% inconclusive rate
14
+ * (ordinary edit-time touches in the same window: 15%).
15
+ *
16
+ * The verdict is therefore decided from the PRIMARY population only. An
17
+ * auxiliary that never reported is a named COVERAGE GAP — `confirmation:
18
+ * "partial"` plus {@link touchCoverageGap} — which withdraws the claim of full
19
+ * coverage while the primary's findings still flow. That is the #533 honesty
20
+ * doctrine cutting both ways: overclaiming ("confirmed" while a scanner was
21
+ * silent) and underclaiming ("inconclusive" while the language server answered)
22
+ * are both dishonest.
23
+ *
24
+ * Keep this a pure function: it is the single place a consumer's reading of
25
+ * `inconclusive` can be audited against, and it is unit-tested directly.
26
+ */
27
+ export function resolveTouchVerdict(input) {
28
+ const notifyIds = [...new Set(input.primaryNotifyWriteTimedOutServerIds)];
29
+ const waitIds = input.diagnosticsTimedOut
30
+ ? [...new Set(input.diagnosticsUnansweredServerIds)]
31
+ : [];
32
+ if (notifyIds.length === 0 && !input.diagnosticsTimedOut) {
33
+ return { inconclusive: false };
34
+ }
35
+ const serverIds = [...new Set([...notifyIds, ...waitIds])];
36
+ const reason = notifyIds.length > 0 && input.diagnosticsTimedOut
37
+ ? "mixed"
38
+ : notifyIds.length > 0
39
+ ? "notify-write"
40
+ : "diagnostics-wait";
41
+ return {
42
+ inconclusive: true,
43
+ ...(serverIds.length > 0 && { inconclusiveServerIds: serverIds }),
44
+ inconclusiveReason: reason,
45
+ };
46
+ }
5
47
  /**
6
48
  * #1470: the single source of truth for "which servers did this touch NOT hear
7
49
  * from". Read this instead of comparing `confirmation` to a string literal — a
@@ -24,13 +66,53 @@ export function touchCoverageGap(result) {
24
66
  * silent-clean gates ran to completion exactly as they do for a full
25
67
  * confirmation. Reading `=== "confirmed"` there looks safely fail-closed but
26
68
  * reports "the language server could not confirm clean" when the truth is "the
27
- * language server confirmed clean and a scanner was cut off" — the same overclaim
69
+ * language server confirmed clean and a scanner never reported" — the same overclaim
28
70
  * pointing the other way. Pair this with {@link touchCoverageGap}, which names
29
71
  * what the touch does not speak for.
30
72
  */
31
73
  export function touchCompletedConfirmationPolicy(result) {
32
74
  return result?.confirmation !== undefined;
33
75
  }
76
+ /**
77
+ * #1493: the single policy deciding which auxiliaries a touch carries NO
78
+ * evidence from. Both no-answer shapes belong here, because they are the same
79
+ * fact about coverage:
80
+ * - `cut_off` (#1470) — our grace timer ended the wait.
81
+ * - `silent` — the auxiliary's own budget lapsed with nothing published.
82
+ * - `deferred` (#1459) — the fan-out gate never sent it these bytes.
83
+ * The rule is written as `outcome !== "answered"`, not as a list, so a new
84
+ * no-answer shape fails closed by default instead of needing to be remembered
85
+ * here — which is how `deferred` joined without touching this filter.
86
+ * An auxiliary that never reported has told us nothing about the file, and
87
+ * whether a SIBLING answered fast enough for the wait to settle early is
88
+ * irrelevant to that. Before this policy, a silent auxiliary was a gap only by
89
+ * accident: it burned the whole touch deadline when it was the only one, which
90
+ * tripped `diagnosticsTimedOut`. With a fast sibling the wait settled early and
91
+ * the silence went unrecorded, so the touch claimed an unqualified
92
+ * `"confirmed"` for a scanner that said nothing (#1493, the #1470 shape on the
93
+ * sibling outcome).
94
+ *
95
+ * NOT an overcorrection into demoting every clean file: `answered` is decided
96
+ * by a publication landing, not by findings existing, so an auxiliary that ran
97
+ * to budget and published an empty set is covered and its touch stays
98
+ * `"confirmed"`. `publishedThisContent` covers the second honest case — an
99
+ * auxiliary whose publication for these exact bytes is already stored (the
100
+ * touch's notify was debounce-skipped, or its late publication was carried in),
101
+ * which has reported even though nothing new landed during this wait.
102
+ *
103
+ * The `publishedThisContent` exemption applies to BOTH no-answer shapes, for the
104
+ * same reason it applies to either: the question is whether this auxiliary has
105
+ * reported on these exact bytes, and how a wait we abandoned would have ended is
106
+ * irrelevant once a verified publication for them exists. Exempting `silent` but
107
+ * not `cut_off` on identical evidence would report the same coverage two ways
108
+ * depending on which timer happened to win. This stays fail-closed — it
109
+ * un-narrows only against a content-hash match, never against a timer.
110
+ */
111
+ export function auxiliaryCoverageGap(evidence) {
112
+ return evidence
113
+ .filter((aux) => aux.outcome !== "answered" && aux.publishedThisContent !== true)
114
+ .map((aux) => aux.serverId);
115
+ }
34
116
  /**
35
117
  * Fingerprint the EXACT text handed to didOpen/didChange. sha256 over the raw
36
118
  * string — no normalization — so the disk comparison (which reads the file with
@@ -0,0 +1,389 @@
1
+ /**
2
+ * #1783 — disk-drift backstop for open LSP documents.
3
+ *
4
+ * An edit made outside the tracked write/edit path (a bash-tool bulk edit)
5
+ * never produces a `didChange`, so the language server keeps answering from
6
+ * the content it was last given. Nothing compared disk against that content,
7
+ * so the stale view survived for as long as the server stayed up: proven live
8
+ * on 2026-08-20, where `src/mcp/stdio.ts` published `diagnosticCount:2` for
9
+ * 40 minutes while `tsc` on disk reported 0.
10
+ *
11
+ * This module records what content actually landed on a server and, on a
12
+ * bounded cadence, compares disk against that record.
13
+ *
14
+ * ## The drift key pairs mtime with size (catalog shape 6)
15
+ *
16
+ * Neither half is sufficient on its own:
17
+ *
18
+ * - Size alone misses a same-length edit (a renamed identifier, a flipped
19
+ * comparison) — the exact shape a bulk sed produces.
20
+ * - An mtime comparison alone misses an edit whose tool preserves the
21
+ * original timestamp (`cp -p`, an archive extract, a `utimes` restore).
22
+ *
23
+ * The mtime half compares against `syncedAt` — the wall clock at which the
24
+ * content landed on the server — not against a stat taken at record time.
25
+ * That keeps the recording path free of a stat: pi-lens always syncs AFTER
26
+ * the write it is reacting to, so `mtimeMs > syncedAt` means "disk moved
27
+ * after we last told the server anything".
28
+ *
29
+ * A stat divergence is a CANDIDATE, not a verdict. The sweep then reads the
30
+ * file and compares the content fingerprint. A touched-but-unchanged file
31
+ * (`touch`, a formatter that rewrote identical bytes) re-stamps its record
32
+ * and sends nothing. That preserves the changed/unchanged distinction rather
33
+ * than laundering both into one "drifted" verdict.
34
+ *
35
+ * Out of scope, stated plainly: an edit that changes content, keeps the
36
+ * length identical, AND preserves mtime is invisible to `stat`. Detecting it
37
+ * needs a read of every tracked document on every pass, which is the cost
38
+ * this design exists to avoid. The disk-binding contentHash on the read path
39
+ * (`diagnostic-binding.ts`) remains the check for that case.
40
+ *
41
+ * ## The record is per FILE, so it must claim only full coverage
42
+ *
43
+ * A file can be open on several servers at once: the primary language server
44
+ * plus the cross-cutting auxiliary scanners. One record per file therefore
45
+ * means one claim about ALL of them. `LSPService.recordFullyCoveredSync` only
46
+ * stamps when every server that currently holds the document was targeted by
47
+ * the sync AND every one of those writes landed. A primary-only touch while an
48
+ * auxiliary holds the document writes nothing, and neither does a touch where
49
+ * one server's write timed out.
50
+ *
51
+ * The resync is the mirror image: it targets every live client holding the
52
+ * document, so a heal cannot leave one view behind and then stamp the file as
53
+ * current. On a partial sync the OLD record survives, with an older timestamp
54
+ * and an older fingerprint, so the document stays eligible instead of going
55
+ * invisible.
56
+ *
57
+ * ## Cost and pacing
58
+ *
59
+ * One `stat` per tracked document per pass, at most {@link DRIFT_CHECK_BATCH}
60
+ * documents per pass, at most one pass per {@link driftCheckIntervalMs}. A
61
+ * read happens only for a document whose stat already diverged.
62
+ *
63
+ * Resyncs are capped at {@link DRIFT_RESYNC_BATCH} per pass and run serially,
64
+ * so a 200-file bulk edit heals in paced rounds instead of firing 200
65
+ * concurrent `didChange` writes at one single-threaded server — the #1714
66
+ * sweep-burst shape. Each resync goes through the caller's normal touch path,
67
+ * so it inherits the existing per-server notify-write budget and
68
+ * backpressure demotion rather than bypassing them.
69
+ */
70
+ import nodeFs from "node:fs";
71
+ import { normalizeMapKey } from "../path-utils.js";
72
+ import { createSingleFlight } from "../single-flight.js";
73
+ /** Cheap content fingerprint. Same shape as the touch-debounce fingerprint. */
74
+ export function fingerprintDocumentContent(content) {
75
+ if (content.length <= 96)
76
+ return `${content.length}:${content}`;
77
+ return `${content.length}:${content.slice(0, 48)}:${content.slice(-48)}`;
78
+ }
79
+ const SKIPPED = {
80
+ checked: 0,
81
+ candidates: 0,
82
+ resynced: 0,
83
+ failed: 0,
84
+ unchanged: 0,
85
+ deferred: 0,
86
+ vanished: 0,
87
+ unheld: 0,
88
+ tracked: 0,
89
+ };
90
+ /** Documents stat'd per pass. Bounds the per-pass cost on a large workspace. */
91
+ export const DRIFT_CHECK_BATCH = 64;
92
+ /** Resyncs issued per pass. Paces the heal so a bulk edit cannot burst. */
93
+ export const DRIFT_RESYNC_BATCH = 4;
94
+ /** Tracked-document ceiling. A workspace sweep can open far more than this. */
95
+ export const DRIFT_TRACK_CAP = 512;
96
+ const DEFAULT_DRIFT_CHECK_INTERVAL_MS = 10_000;
97
+ /** The pass is workspace-wide, so the single-flight registry needs one key. */
98
+ const SWEEP_KEY = "open-document-drift";
99
+ /** Minimum gap between two drift passes. Env-tunable; 0 disables the sweep. */
100
+ export function driftCheckIntervalMs() {
101
+ const raw = Number(process.env.PI_LENS_LSP_DRIFT_CHECK_MS);
102
+ if (!Number.isFinite(raw) || raw < 0)
103
+ return DEFAULT_DRIFT_CHECK_INTERVAL_MS;
104
+ return Math.floor(raw);
105
+ }
106
+ export class DocumentDriftTracker {
107
+ /** Insertion-ordered; the sweep cursor walks it round-robin. */
108
+ synced = new Map();
109
+ lastSweepAt = 0;
110
+ cursor = 0;
111
+ /**
112
+ * At most one pass at a time (#1753's primitive). Callers fire `sweep`
113
+ * without awaiting it from hot paths, so an overlapping call must JOIN the
114
+ * running pass rather than double the stat load. One key: the pass is
115
+ * workspace-wide, not per file. No `generation` hook — `clear()` is called
116
+ * from the service reset, and a pass that settles afterwards writes into a
117
+ * map the reset already emptied, so there is no successor for it to evict.
118
+ */
119
+ flight = createSingleFlight();
120
+ get size() {
121
+ return this.synced.size;
122
+ }
123
+ peek(filePath) {
124
+ return this.synced.get(normalizeMapKey(filePath));
125
+ }
126
+ /**
127
+ * Record content that landed on EVERY server holding this document.
128
+ *
129
+ * The caller owns that coverage judgement — see
130
+ * `LSPService.recordFullyCoveredSync`. `now` is the moment the sync STARTED,
131
+ * not the moment its write landed; see {@link SyncedDocumentRecord.syncedAt}.
132
+ */
133
+ recordSynced(filePath, content, now = Date.now()) {
134
+ const key = normalizeMapKey(filePath);
135
+ // Delete first so a re-record moves the entry to the END of the insertion
136
+ // order. Without this, a hot file keeps an old cursor position and the
137
+ // eviction below would drop it while stale entries survive.
138
+ this.synced.delete(key);
139
+ this.synced.set(key, {
140
+ size: Buffer.byteLength(content, "utf8"),
141
+ fingerprint: fingerprintDocumentContent(content),
142
+ syncedAt: now,
143
+ });
144
+ while (this.synced.size > DRIFT_TRACK_CAP) {
145
+ const oldest = this.synced.keys().next();
146
+ if (oldest.done)
147
+ break;
148
+ this.synced.delete(oldest.value);
149
+ }
150
+ }
151
+ forget(filePath) {
152
+ this.synced.delete(normalizeMapKey(filePath));
153
+ }
154
+ /** Drop every record. Used on shutdown/session reset, where the server's view dies with it. */
155
+ clear() {
156
+ this.synced.clear();
157
+ this.cursor = 0;
158
+ this.lastSweepAt = 0;
159
+ this.flight.clear();
160
+ }
161
+ /**
162
+ * Stat one batch of tracked documents and resync the ones whose disk state
163
+ * no longer matches what the server holds.
164
+ *
165
+ * Rate-limited and single-flight: callers fire this from hot paths without
166
+ * awaiting it, so an overlapping call must join the running pass rather than
167
+ * double the stat load.
168
+ */
169
+ async sweep(deps, options = {}) {
170
+ const now = deps.now ?? Date.now;
171
+ // The rate limit and the empty check only decide whether to START a pass.
172
+ // A caller arriving mid-pass joins it instead, which is the point of the
173
+ // single-flight registry.
174
+ if (!this.flight.has(SWEEP_KEY)) {
175
+ const intervalMs = driftCheckIntervalMs();
176
+ if (!options.force) {
177
+ if (intervalMs === 0)
178
+ return SKIPPED;
179
+ if (now() - this.lastSweepAt < intervalMs)
180
+ return SKIPPED;
181
+ }
182
+ if (this.synced.size === 0) {
183
+ this.lastSweepAt = now();
184
+ return SKIPPED;
185
+ }
186
+ }
187
+ return this.flight.run(SWEEP_KEY, () => this.runSweep(deps, now));
188
+ }
189
+ async runSweep(deps, now) {
190
+ this.lastSweepAt = now();
191
+ const stat = deps.stat ?? defaultStat;
192
+ const read = deps.read ?? defaultRead;
193
+ const keys = [...this.synced.keys()];
194
+ const tracked = keys.length;
195
+ if (this.cursor >= tracked)
196
+ this.cursor = 0;
197
+ const window = [];
198
+ for (let i = 0; i < Math.min(DRIFT_CHECK_BATCH, tracked); i++) {
199
+ window.push(keys[(this.cursor + i) % tracked]);
200
+ }
201
+ this.cursor = (this.cursor + window.length) % tracked;
202
+ // Drop records nobody holds BEFORE spending a stat on them.
203
+ let unheld = 0;
204
+ const batch = [];
205
+ for (const key of window) {
206
+ const record = this.synced.get(key);
207
+ if (!record)
208
+ continue;
209
+ if (deps.holdsDocument && !deps.holdsDocument(key)) {
210
+ this.synced.delete(key);
211
+ unheld += 1;
212
+ deps.onDrift?.({
213
+ filePath: key,
214
+ driftAgeMs: Math.max(0, now() - record.syncedAt),
215
+ disposition: "unheld",
216
+ syncedSize: record.size,
217
+ diskSize: -1,
218
+ });
219
+ continue;
220
+ }
221
+ batch.push(key);
222
+ }
223
+ // The stats run concurrently: they are independent, and this pass is
224
+ // already off the caller's critical path.
225
+ const stats = await Promise.all(batch.map(async (key) => {
226
+ try {
227
+ return { key, stat: await stat(key) };
228
+ }
229
+ catch {
230
+ return { key, stat: undefined };
231
+ }
232
+ }));
233
+ let candidates = 0;
234
+ let vanished = 0;
235
+ const drifted = [];
236
+ for (const entry of stats) {
237
+ const record = this.synced.get(entry.key);
238
+ if (!record)
239
+ continue;
240
+ if (!entry.stat) {
241
+ this.synced.delete(entry.key);
242
+ vanished += 1;
243
+ deps.onDrift?.({
244
+ filePath: entry.key,
245
+ driftAgeMs: Math.max(0, now() - record.syncedAt),
246
+ disposition: "vanished",
247
+ syncedSize: record.size,
248
+ diskSize: -1,
249
+ });
250
+ continue;
251
+ }
252
+ // The paired key. Either half alone leaves a real edit undetected.
253
+ const sizeMoved = entry.stat.size !== record.size;
254
+ // `Math.floor` because the two clocks have different resolutions:
255
+ // `stat.mtimeMs` carries a sub-millisecond fraction (…972.1006) while
256
+ // `Date.now()` is a whole millisecond (…972). Comparing them raw makes
257
+ // EVERY file written in the same millisecond as its sync a permanent
258
+ // candidate — a read of that file on every pass, forever, for a
259
+ // difference of 0.1ms. Flooring costs a blind window of at most one
260
+ // millisecond, which is inside the read-to-sync gap this key already
261
+ // cannot see into. Coarse filesystems (FAT 2s, HFS+ 1s) round mtime
262
+ // DOWN, so they never produce the opposite error.
263
+ const mtimeMoved = Math.floor(entry.stat.mtimeMs) > record.syncedAt;
264
+ if (!sizeMoved && !mtimeMoved)
265
+ continue;
266
+ candidates += 1;
267
+ drifted.push({
268
+ key: entry.key,
269
+ record,
270
+ mtimeMs: entry.stat.mtimeMs,
271
+ });
272
+ }
273
+ let resynced = 0;
274
+ let failed = 0;
275
+ let unchanged = 0;
276
+ let deferred = 0;
277
+ for (const { key, record, mtimeMs } of drifted) {
278
+ const driftAgeMs = Math.max(0, now() - record.syncedAt);
279
+ if (resynced >= DRIFT_RESYNC_BATCH) {
280
+ // Paced, not dropped: the cursor already advanced past this file, but
281
+ // a later pass wraps around to it. Recorded so a backlog is visible.
282
+ deferred += 1;
283
+ deps.onDrift?.({
284
+ filePath: key,
285
+ driftAgeMs,
286
+ disposition: "deferred",
287
+ syncedSize: record.size,
288
+ diskSize: -1,
289
+ });
290
+ continue;
291
+ }
292
+ let content;
293
+ try {
294
+ content = await read(key);
295
+ }
296
+ catch {
297
+ this.synced.delete(key);
298
+ vanished += 1;
299
+ continue;
300
+ }
301
+ if (fingerprintDocumentContent(content) === record.fingerprint) {
302
+ // Touched, not changed. Re-stamp so the next pass stops flagging it,
303
+ // and send nothing: the server's view is already correct.
304
+ unchanged += 1;
305
+ this.recordSynced(key, content, settledStamp(now(), mtimeMs));
306
+ deps.onDrift?.({
307
+ filePath: key,
308
+ driftAgeMs,
309
+ disposition: "unchanged",
310
+ syncedSize: record.size,
311
+ diskSize: Buffer.byteLength(content, "utf8"),
312
+ });
313
+ continue;
314
+ }
315
+ // Serial on purpose. See the pacing note in this module's header.
316
+ // The record is emitted AFTER the await, with the outcome the resync
317
+ // actually had: emitting "resynced" up front logged a completed heal for
318
+ // a push that then threw, which is exactly the lie a bounded record
319
+ // exists to prevent.
320
+ const diskSize = Buffer.byteLength(content, "utf8");
321
+ let landed = false;
322
+ try {
323
+ landed = await deps.resync(key, content, driftAgeMs);
324
+ }
325
+ catch {
326
+ landed = false;
327
+ }
328
+ if (!landed) {
329
+ // A failed resync leaves the OLD record in place, so the next pass
330
+ // retries this file instead of recording a heal that never happened.
331
+ failed += 1;
332
+ deps.onDrift?.({
333
+ filePath: key,
334
+ driftAgeMs,
335
+ disposition: "failed",
336
+ syncedSize: record.size,
337
+ diskSize,
338
+ });
339
+ continue;
340
+ }
341
+ resynced += 1;
342
+ // Stamp AUTHORITATIVELY, over whatever the sync path recorded. The sync
343
+ // path anchors on its own start time, which can sit BEHIND the disk mtime
344
+ // it is reacting to — deliberately, so an edit inside the sync window is
345
+ // still visible. Left alone, that same record makes this file a candidate
346
+ // on every later pass: a read per pass, forever, for a heal that already
347
+ // happened. `settledStamp` closes it against the mtime this pass actually
348
+ // observed, so a converged file stops being re-read and only a NEWER
349
+ // mtime reopens it.
350
+ this.recordSynced(key, content, settledStamp(now(), mtimeMs));
351
+ deps.onDrift?.({
352
+ filePath: key,
353
+ driftAgeMs,
354
+ disposition: "resynced",
355
+ syncedSize: record.size,
356
+ diskSize,
357
+ });
358
+ }
359
+ return {
360
+ checked: batch.length,
361
+ candidates,
362
+ resynced,
363
+ failed,
364
+ unchanged,
365
+ deferred,
366
+ vanished,
367
+ unheld,
368
+ tracked,
369
+ };
370
+ }
371
+ }
372
+ /**
373
+ * The timestamp that settles a record against the disk state just observed.
374
+ *
375
+ * `Math.ceil` because `stat.mtimeMs` carries a sub-millisecond fraction that
376
+ * `Date.now()` does not have, and `Math.max` because the clock may already be
377
+ * past that mtime. The result is the smallest timestamp for which the drift
378
+ * key reads "in sync" for exactly this disk state and nothing newer.
379
+ */
380
+ function settledStamp(now, observedMtimeMs) {
381
+ return Math.max(now, Math.ceil(observedMtimeMs));
382
+ }
383
+ async function defaultStat(filePath) {
384
+ const st = await nodeFs.promises.stat(filePath);
385
+ return { size: st.size, mtimeMs: st.mtimeMs };
386
+ }
387
+ async function defaultRead(filePath) {
388
+ return nodeFs.promises.readFile(filePath, "utf-8");
389
+ }