@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.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +1 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +2 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/pi-lens/CHANGELOG.md +909 -0
- package/node_modules/pi-lens/README.md +11 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +67738 -55739
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +10 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +23 -0
- package/node_modules/pi-web-access/README.md +27 -9
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +28 -3
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/index.ts +41 -57
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- 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
|
|
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
|
+
}
|