@herbertgao/pi-extensions 2026.8.3 → 2026.8.5
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 +45 -4
- package/examples/pi-footer.json +275 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +8 -7
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
- package/node_modules/pi-footer/CHANGELOG.md +145 -0
- package/node_modules/pi-footer/LICENSE +21 -0
- package/node_modules/pi-footer/README.md +354 -0
- package/node_modules/pi-footer/package.json +68 -0
- package/node_modules/pi-footer/src/cache.ts +106 -0
- package/node_modules/pi-footer/src/colors.ts +237 -0
- package/node_modules/pi-footer/src/config.ts +202 -0
- package/node_modules/pi-footer/src/event-widgets.ts +41 -0
- package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
- package/node_modules/pi-footer/src/git.ts +135 -0
- package/node_modules/pi-footer/src/index.ts +268 -0
- package/node_modules/pi-footer/src/metrics.ts +141 -0
- package/node_modules/pi-footer/src/presets.ts +383 -0
- package/node_modules/pi-footer/src/render.ts +114 -0
- package/node_modules/pi-footer/src/separators.ts +52 -0
- package/node_modules/pi-footer/src/types.ts +122 -0
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
- package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
- package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
- package/node_modules/pi-footer/src/ui/events.ts +35 -0
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
- package/node_modules/pi-footer/src/ui/fields.ts +252 -0
- package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
- package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
- package/node_modules/pi-footer/src/ui/layout.ts +17 -0
- package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
- package/node_modules/pi-footer/src/ui/model.ts +64 -0
- package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
- package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
- package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
- package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
- package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
- package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
- package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
- package/node_modules/pi-footer/src/ui/screen.ts +243 -0
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
- package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
- package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
- package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
- package/node_modules/pi-footer/src/ui/theme.ts +48 -0
- package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
- package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
- package/node_modules/pi-footer/src/ui.ts +66 -0
- package/node_modules/pi-footer/src/widgets/context.ts +28 -0
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
- package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
- package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
- package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
- package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
- package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
- package/node_modules/pi-footer/src/widgets/options.ts +122 -0
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
- package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/store.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/types.ts +181 -0
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
- package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
- package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
- package/node_modules/pi-lens/CHANGELOG.md +2440 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
- package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
- package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
- package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +49361 -36888
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +211 -44
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/docs/agent-guide.md +2 -2
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +31 -2
- package/node_modules/pi-lens/docs/globalconfig.md +45 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +12 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +18 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
- package/node_modules/pi-mcp-adapter/README.md +34 -3
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +93 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/package.json +17 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -20,6 +20,7 @@ import { isAtOrAboveHomeDir, normalizeEphemeralMapKey } from "./path-utils.js";
|
|
|
20
20
|
import { createDebounceScheduler, } from "./persist-debounce.js";
|
|
21
21
|
import { getWordIndexMaxFilesDerived } from "./project-scale.js";
|
|
22
22
|
import { logWordIndex } from "./word-index-logger.js";
|
|
23
|
+
import { createDeadline, forEachCooperatively, yieldIfOverBudget, } from "./cooperative-budget.js";
|
|
23
24
|
/**
|
|
24
25
|
* The single normalizer every word-index path key folds through (#1025). The
|
|
25
26
|
* CHEAP form — slash-fold + win32-lowercase, NO filesystem I/O — because the
|
|
@@ -94,52 +95,112 @@ export function tokenizeLine(line) {
|
|
|
94
95
|
}
|
|
95
96
|
return tokens;
|
|
96
97
|
}
|
|
98
|
+
const WORD_INDEX_BUILD_YIELD_BUDGET_MS = 8;
|
|
97
99
|
/**
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* that
|
|
100
|
+
* A line at or above this length is treated as a yield checkpoint on its own.
|
|
101
|
+
* The per-50-lines cadence is a proxy for work that only holds on hand-written
|
|
102
|
+
* source: a minified/bundled single-line file up to {@link WORD_INDEX_MAX_BYTES}
|
|
103
|
+
* that the source filter did not exclude would otherwise get ZERO in-document
|
|
104
|
+
* yields — one unbroken tokenize+push burst far above the budget (#1197 review
|
|
105
|
+
* finding 4). Checking the clock after any long line makes the cooperativeness
|
|
106
|
+
* claim true per BYTE, not just per line count.
|
|
102
107
|
*/
|
|
108
|
+
const WORD_INDEX_LONG_LINE_YIELD_CHARS = 4096;
|
|
109
|
+
/**
|
|
110
|
+
* Shared cooperative time-slicer for every bulk word-index path (#1197).
|
|
111
|
+
*
|
|
112
|
+
* Time-based, not count-based: "yield every N items" is only a bound when the
|
|
113
|
+
* per-item cost is bounded too, and the incremental refresh's per-document cost
|
|
114
|
+
* grows with the corpus (each replacement filters shared posting arrays). The
|
|
115
|
+
* #1197 outage was exactly that shape — 239 stale documents held pi's event loop
|
|
116
|
+
* for seconds between two "every 100 files" checkpoints. Callers combine this
|
|
117
|
+
* The shared deadline is checked at every bounded work unit so abort latency
|
|
118
|
+
* and occupancy are governed by the same monotonic budget.
|
|
119
|
+
*/
|
|
120
|
+
function createEmptyWordIndex(truncated) {
|
|
121
|
+
return {
|
|
122
|
+
postings: new Map(),
|
|
123
|
+
docLengths: new PathKeyedMap(wordIndexKey),
|
|
124
|
+
totalTokens: 0,
|
|
125
|
+
docCount: 0,
|
|
126
|
+
truncated,
|
|
127
|
+
forward: new PathKeyedMap(wordIndexKey),
|
|
128
|
+
fileMtimes: new PathKeyedMap(wordIndexKey),
|
|
129
|
+
fileSizes: new PathKeyedMap(wordIndexKey),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function indexWordLine(index, filePath, line, lineNumber, tokenLineCounts) {
|
|
133
|
+
const lineTokens = tokenizeLine(line);
|
|
134
|
+
const seenOnLine = new Set();
|
|
135
|
+
for (const token of lineTokens) {
|
|
136
|
+
if (seenOnLine.has(token))
|
|
137
|
+
continue;
|
|
138
|
+
seenOnLine.add(token);
|
|
139
|
+
const arr = index.postings.get(token);
|
|
140
|
+
if (arr)
|
|
141
|
+
arr.push({ file: filePath, line: lineNumber });
|
|
142
|
+
else
|
|
143
|
+
index.postings.set(token, [{ file: filePath, line: lineNumber }]);
|
|
144
|
+
tokenLineCounts.set(token, (tokenLineCounts.get(token) ?? 0) + 1);
|
|
145
|
+
}
|
|
146
|
+
return lineTokens.length;
|
|
147
|
+
}
|
|
148
|
+
function finishWordIndexDocument(index, doc, docLength, tokenLineCounts) {
|
|
149
|
+
index.docLengths.set(doc.path, docLength);
|
|
150
|
+
index.forward?.set(doc.path, tokenLineCounts);
|
|
151
|
+
index.fileMtimes.set(doc.path, doc.mtimeMs ?? 0);
|
|
152
|
+
index.fileSizes.set(doc.path, doc.size ?? Buffer.byteLength(doc.content, "utf-8"));
|
|
153
|
+
index.totalTokens += docLength;
|
|
154
|
+
index.docCount += 1;
|
|
155
|
+
}
|
|
156
|
+
function indexWordDocument(index, doc) {
|
|
157
|
+
const lines = doc.content.split(/\r?\n/);
|
|
158
|
+
const tokenLineCounts = new Map();
|
|
159
|
+
let docLength = 0;
|
|
160
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
161
|
+
docLength += indexWordLine(index, doc.path, lines[i], i + 1, tokenLineCounts);
|
|
162
|
+
}
|
|
163
|
+
finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
|
|
164
|
+
}
|
|
103
165
|
export function buildWordIndex(files) {
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
166
|
+
const index = createEmptyWordIndex(files.truncated ?? false);
|
|
167
|
+
for (const doc of files)
|
|
168
|
+
indexWordDocument(index, doc);
|
|
169
|
+
return index;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Cooperative production builder for bulk/cold paths. It produces the exact
|
|
173
|
+
* same index as {@link buildWordIndex}, but time-slices both between documents
|
|
174
|
+
* and within a large document so startup warmup cannot monopolize pi's TUI
|
|
175
|
+
* event loop (#1197). The index is private until this resolves, so a superseded
|
|
176
|
+
* build never publishes a partial replacement.
|
|
177
|
+
*/
|
|
178
|
+
export async function buildWordIndexAsync(files, shouldContinue = () => true) {
|
|
179
|
+
const index = createEmptyWordIndex(files.truncated ?? false);
|
|
180
|
+
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
181
|
+
for (const doc of files) {
|
|
182
|
+
if (!shouldContinue())
|
|
183
|
+
throw new Error("word index build superseded");
|
|
184
|
+
const lines = doc.content.split(/\r?\n/);
|
|
112
185
|
const tokenLineCounts = new Map();
|
|
186
|
+
let docLength = 0;
|
|
113
187
|
for (let i = 0; i < lines.length; i += 1) {
|
|
114
|
-
const
|
|
115
|
-
docLength +=
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const arr = postings.get(token);
|
|
122
|
-
if (arr)
|
|
123
|
-
arr.push({ file: filePath, line: i + 1 });
|
|
124
|
-
else
|
|
125
|
-
postings.set(token, [{ file: filePath, line: i + 1 }]);
|
|
126
|
-
tokenLineCounts.set(token, (tokenLineCounts.get(token) ?? 0) + 1);
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
docLength += indexWordLine(index, doc.path, line, i + 1, tokenLineCounts);
|
|
190
|
+
if (line.length >= WORD_INDEX_LONG_LINE_YIELD_CHARS ||
|
|
191
|
+
deadline.expired()) {
|
|
192
|
+
await yieldIfOverBudget(deadline);
|
|
193
|
+
if (!shouldContinue())
|
|
194
|
+
throw new Error("word index build superseded");
|
|
127
195
|
}
|
|
128
196
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
197
|
+
finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
|
|
198
|
+
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
199
|
+
if (!shouldContinue())
|
|
200
|
+
throw new Error("word index build superseded");
|
|
201
|
+
}
|
|
133
202
|
}
|
|
134
|
-
return
|
|
135
|
-
postings,
|
|
136
|
-
docLengths,
|
|
137
|
-
totalTokens,
|
|
138
|
-
docCount: files.length,
|
|
139
|
-
truncated: files.truncated ?? false,
|
|
140
|
-
forward,
|
|
141
|
-
fileMtimes,
|
|
142
|
-
};
|
|
203
|
+
return index;
|
|
143
204
|
}
|
|
144
205
|
/**
|
|
145
206
|
* Remove `filePath`'s postings/docLength/forward entry from `index` in place,
|
|
@@ -175,6 +236,7 @@ export function removeWordIndexDocument(index, filePath) {
|
|
|
175
236
|
index.docLengths.delete(filePath);
|
|
176
237
|
index.forward.delete(filePath);
|
|
177
238
|
index.fileMtimes.delete(filePath);
|
|
239
|
+
index.fileSizes.delete(filePath);
|
|
178
240
|
index.totalTokens -= docLength;
|
|
179
241
|
index.docCount = Math.max(0, index.docCount - 1);
|
|
180
242
|
return true;
|
|
@@ -238,6 +300,117 @@ export function updateWordIndexDocument(index, doc) {
|
|
|
238
300
|
// legal on-disk mtime (SOURCE_DATE_EPOCH=0, archive extraction) and would
|
|
239
301
|
// collide, leaving such a file never re-tokenized (#958 review F2).
|
|
240
302
|
index.fileMtimes.set(doc.path, -1);
|
|
303
|
+
// Size is likewise recorded for the #1105 mtime+size refresh gate. The -1
|
|
304
|
+
// mtime already forces a re-read next session, so this value only keeps the
|
|
305
|
+
// map dense (parallel to fileMtimes); store the real byte length so a
|
|
306
|
+
// deserialize→reserialize round-trip before any refresh carries a truthful
|
|
307
|
+
// size rather than a placeholder.
|
|
308
|
+
index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
|
|
309
|
+
index.totalTokens += docLength;
|
|
310
|
+
index.docCount += 1;
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
async function stageWordIndexDocumentRemoval(index, filePath, shouldContinue) {
|
|
314
|
+
if (!index.forward)
|
|
315
|
+
return undefined;
|
|
316
|
+
const tokenLineCounts = index.forward.get(filePath);
|
|
317
|
+
if (!tokenLineCounts)
|
|
318
|
+
return undefined;
|
|
319
|
+
const removedKey = wordIndexKey(filePath);
|
|
320
|
+
const postings = new Map();
|
|
321
|
+
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
322
|
+
for (const token of tokenLineCounts.keys()) {
|
|
323
|
+
if (!shouldContinue())
|
|
324
|
+
throw new Error("word index refresh superseded");
|
|
325
|
+
const arr = index.postings.get(token);
|
|
326
|
+
if (!arr)
|
|
327
|
+
continue;
|
|
328
|
+
const next = [];
|
|
329
|
+
for (const hit of arr) {
|
|
330
|
+
if (wordIndexKey(hit.file) !== removedKey)
|
|
331
|
+
next.push(hit);
|
|
332
|
+
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
333
|
+
if (!shouldContinue())
|
|
334
|
+
throw new Error("word index refresh superseded");
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
postings.set(token, next.length > 0 ? next : undefined);
|
|
338
|
+
}
|
|
339
|
+
return { postings, docLength: index.docLengths.get(filePath) ?? 0 };
|
|
340
|
+
}
|
|
341
|
+
function commitWordIndexDocumentRemoval(index, filePath, staged) {
|
|
342
|
+
for (const [token, hits] of staged.postings) {
|
|
343
|
+
if (hits)
|
|
344
|
+
index.postings.set(token, hits);
|
|
345
|
+
else
|
|
346
|
+
index.postings.delete(token);
|
|
347
|
+
}
|
|
348
|
+
index.docLengths.delete(filePath);
|
|
349
|
+
index.forward?.delete(filePath);
|
|
350
|
+
index.fileMtimes.delete(filePath);
|
|
351
|
+
index.fileSizes.delete(filePath);
|
|
352
|
+
index.totalTokens -= staged.docLength;
|
|
353
|
+
index.docCount = Math.max(0, index.docCount - 1);
|
|
354
|
+
}
|
|
355
|
+
/** Cooperative, atomically-published removal used only by bulk refresh. */
|
|
356
|
+
export async function removeWordIndexDocumentAsync(index, filePath, shouldContinue = () => true) {
|
|
357
|
+
const staged = await stageWordIndexDocumentRemoval(index, filePath, shouldContinue);
|
|
358
|
+
if (!staged)
|
|
359
|
+
return false;
|
|
360
|
+
if (!shouldContinue())
|
|
361
|
+
throw new Error("word index refresh superseded");
|
|
362
|
+
commitWordIndexDocumentRemoval(index, filePath, staged);
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
/** Cooperative replacement whose old/new state is committed without an await. */
|
|
366
|
+
export async function updateWordIndexDocumentAsync(index, doc, shouldContinue = () => true) {
|
|
367
|
+
if (!index.forward)
|
|
368
|
+
return false;
|
|
369
|
+
const removal = index.forward.has(doc.path)
|
|
370
|
+
? await stageWordIndexDocumentRemoval(index, doc.path, shouldContinue)
|
|
371
|
+
: undefined;
|
|
372
|
+
if (index.forward.has(doc.path) && !removal)
|
|
373
|
+
return false;
|
|
374
|
+
const perTokenHits = new Map();
|
|
375
|
+
let docLength = 0;
|
|
376
|
+
const lines = doc.content.split(/\r?\n/);
|
|
377
|
+
await forEachCooperatively(lines, (line, i) => {
|
|
378
|
+
const lineTokens = tokenizeLine(line);
|
|
379
|
+
docLength += lineTokens.length;
|
|
380
|
+
const seenOnLine = new Set();
|
|
381
|
+
for (const token of lineTokens) {
|
|
382
|
+
if (seenOnLine.has(token))
|
|
383
|
+
continue;
|
|
384
|
+
seenOnLine.add(token);
|
|
385
|
+
const arr = perTokenHits.get(token);
|
|
386
|
+
if (arr)
|
|
387
|
+
arr.push(i + 1);
|
|
388
|
+
else
|
|
389
|
+
perTokenHits.set(token, [i + 1]);
|
|
390
|
+
}
|
|
391
|
+
}, {
|
|
392
|
+
budgetMs: WORD_INDEX_BUILD_YIELD_BUDGET_MS,
|
|
393
|
+
shouldContinue,
|
|
394
|
+
abortMessage: "word index refresh superseded",
|
|
395
|
+
});
|
|
396
|
+
if (!shouldContinue())
|
|
397
|
+
throw new Error("word index refresh superseded");
|
|
398
|
+
if (removal)
|
|
399
|
+
commitWordIndexDocumentRemoval(index, doc.path, removal);
|
|
400
|
+
const tokenLineCounts = new Map();
|
|
401
|
+
for (const [token, lineNumbers] of perTokenHits) {
|
|
402
|
+
tokenLineCounts.set(token, lineNumbers.length);
|
|
403
|
+
const hits = lineNumbers.map((line) => ({ file: doc.path, line }));
|
|
404
|
+
const arr = index.postings.get(token);
|
|
405
|
+
if (arr)
|
|
406
|
+
arr.push(...hits);
|
|
407
|
+
else
|
|
408
|
+
index.postings.set(token, hits);
|
|
409
|
+
}
|
|
410
|
+
index.docLengths.set(doc.path, docLength);
|
|
411
|
+
index.forward.set(doc.path, tokenLineCounts);
|
|
412
|
+
index.fileMtimes.set(doc.path, -1);
|
|
413
|
+
index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
|
|
241
414
|
index.totalTokens += docLength;
|
|
242
415
|
index.docCount += 1;
|
|
243
416
|
return true;
|
|
@@ -262,7 +435,7 @@ export const WORD_INDEX_MAX_BYTES = 512 * 1024;
|
|
|
262
435
|
* `shouldContinue` lets a session-scoped caller abort early (session
|
|
263
436
|
* superseded) without this module knowing about RuntimeCoordinator.
|
|
264
437
|
*/
|
|
265
|
-
export async function collectWordIndexDocs(root, shouldContinue = () => true) {
|
|
438
|
+
export async function collectWordIndexDocs(root, shouldContinue = () => true, preflightFiles) {
|
|
266
439
|
const { collectSourceFilesAsync } = await import("./source-filter.js");
|
|
267
440
|
// #747 hardening: pass the cap INTO the walk — without it,
|
|
268
441
|
// `collectSourceFilesAsync` defaults to an unbounded traversal and the
|
|
@@ -272,29 +445,39 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
|
|
|
272
445
|
// visited-entry budget (DEFAULT_MAX_SCAN_ENTRIES), so a mixed tree with few
|
|
273
446
|
// source files among a huge pile of non-source files can't force a
|
|
274
447
|
// full-tree walk either; an index over the truncated list is acceptable.
|
|
275
|
-
const maxFiles = getWordIndexMaxFilesDerived(root);
|
|
448
|
+
const maxFiles = preflightFiles?.length ?? getWordIndexMaxFilesDerived(root);
|
|
276
449
|
// #894 review: prioritize code kinds within the cap — with broadened
|
|
277
450
|
// enumeration, thousands of data/doc files (locale JSON, fixtures, …)
|
|
278
451
|
// ahead of the code dirs in walk order could exhaust `maxFiles` and evict
|
|
279
452
|
// real source files from the index entirely (DOC_FILE_PENALTY can't
|
|
280
453
|
// rescue a file that never made the slice).
|
|
281
|
-
const files =
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
454
|
+
const files = preflightFiles
|
|
455
|
+
? preflightFiles.map((file) => file.path)
|
|
456
|
+
: await collectSourceFilesAsync(root, {
|
|
457
|
+
maxFiles,
|
|
458
|
+
prioritizeCodeKinds: true,
|
|
459
|
+
});
|
|
460
|
+
const truncated = preflightFiles?.truncated ?? files.length === maxFiles;
|
|
286
461
|
const docs = Object.assign([], { truncated, skipped: 0 });
|
|
287
462
|
if (!shouldContinue())
|
|
288
463
|
return docs;
|
|
289
|
-
|
|
464
|
+
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
290
465
|
for (const file of files.slice(0, maxFiles)) {
|
|
466
|
+
if (!shouldContinue())
|
|
467
|
+
return docs;
|
|
291
468
|
try {
|
|
469
|
+
// A preflight list saves the second full WALK, but its metadata may be
|
|
470
|
+
// stale by the time this rebuild reads the file. Re-stat each document at
|
|
471
|
+
// content-read time so the stored freshness stamp describes the bytes we
|
|
472
|
+
// actually index and the next incremental refresh cannot miss a change in
|
|
473
|
+
// the walk-to-read window (#1302).
|
|
292
474
|
const stat = fs.statSync(file);
|
|
293
475
|
if (stat.size <= WORD_INDEX_MAX_BYTES) {
|
|
294
476
|
docs.push({
|
|
295
477
|
path: file,
|
|
296
478
|
content: fs.readFileSync(file, "utf-8"),
|
|
297
479
|
mtimeMs: stat.mtimeMs,
|
|
480
|
+
size: stat.size,
|
|
298
481
|
});
|
|
299
482
|
}
|
|
300
483
|
else {
|
|
@@ -308,8 +491,7 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
|
|
|
308
491
|
// unreadable / vanished file — skip, but count it (see above).
|
|
309
492
|
docs.skipped += 1;
|
|
310
493
|
}
|
|
311
|
-
if (
|
|
312
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
494
|
+
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
313
495
|
if (!shouldContinue())
|
|
314
496
|
return docs;
|
|
315
497
|
}
|
|
@@ -317,14 +499,44 @@ export async function collectWordIndexDocs(root, shouldContinue = () => true) {
|
|
|
317
499
|
return docs;
|
|
318
500
|
}
|
|
319
501
|
const WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD = 0.3;
|
|
502
|
+
// A ratio alone misclassifies one stale file in a three-file project as dense.
|
|
503
|
+
// Below this absolute floor, per-document replacement is bounded and cheaper
|
|
504
|
+
// than a second full walk/read even when the percentage is high (#1197).
|
|
505
|
+
const WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS = 32;
|
|
506
|
+
/**
|
|
507
|
+
* Cost of one file's stat+read+decode expressed in tokenizer-token units, so
|
|
508
|
+
* the work model below can compare a path that re-reads EVERY file (full
|
|
509
|
+
* rebuild) against one that re-reads only the stale files (incremental) in a
|
|
510
|
+
* single currency. One read ≈ 300 tokens at the ~1 µs/token measured below.
|
|
511
|
+
* Order-of-magnitude by design — it only has to keep a tiny-vocabulary corpus
|
|
512
|
+
* (where posting scans are nearly free) from being sent to a full rebuild that
|
|
513
|
+
* re-reads everything to save a handful of array filters.
|
|
514
|
+
*/
|
|
515
|
+
const WORD_INDEX_FILE_READ_TOKEN_COST = 300;
|
|
516
|
+
/**
|
|
517
|
+
* Cost of scanning ONE posting-array element (the `wordIndexKey` compare in
|
|
518
|
+
* {@link removeWordIndexDocument}'s filter) relative to tokenizing one token,
|
|
519
|
+
* so both sides of the model below are denominated in tokens. Measured on this
|
|
520
|
+
* repository's own 2,062-document corpus: 213 ns per element scan against 979 ns
|
|
521
|
+
* per token — and the resulting predicted crossover (23 stale documents) lands
|
|
522
|
+
* on the directly measured one (~21, from 97.8 ms per document replacement
|
|
523
|
+
* against a 1,436 ms rebuild + re-read). The synthetic high-df corpus in the
|
|
524
|
+
* #1197 probe gives 0.09, so this is the conservative (incremental-favouring)
|
|
525
|
+
* end of the observed range.
|
|
526
|
+
*/
|
|
527
|
+
const WORD_INDEX_POSTING_SCAN_TOKEN_COST = 0.2;
|
|
320
528
|
/**
|
|
321
529
|
* Refresh a serializer-v2 index from the current bounded source-file set.
|
|
322
|
-
* The walk/stat pass is cheap; only stale/new documents are read and
|
|
323
|
-
*
|
|
530
|
+
* The walk/stat pass is cheap; only sparse stale/new documents are read and
|
|
531
|
+
* tokenized. Expected dense/legacy states return `full-required` BEFORE any
|
|
532
|
+
* mutation; unexpected corruption or supersession still throws.
|
|
324
533
|
*/
|
|
325
534
|
export async function refreshWordIndexIncrementally(index, root, shouldContinue = () => true) {
|
|
326
|
-
if (!index.forward || !index.fileMtimes) {
|
|
327
|
-
|
|
535
|
+
if (!index.forward || !index.fileMtimes || !index.fileSizes) {
|
|
536
|
+
return {
|
|
537
|
+
mode: "full-required",
|
|
538
|
+
reason: "missing-incremental-metadata",
|
|
539
|
+
};
|
|
328
540
|
}
|
|
329
541
|
const { collectSourceFilesAsync } = await import("./source-filter.js");
|
|
330
542
|
const maxFiles = getWordIndexMaxFilesDerived(root);
|
|
@@ -347,17 +559,33 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
|
|
|
347
559
|
// `wordIndexKey` keeps build/edit/refresh convergent; `current`'s value
|
|
348
560
|
// retains the raw walk path for the stat/read and for the display key.
|
|
349
561
|
const current = new Map();
|
|
562
|
+
// Check both supersession and the monotonic budget at every stat unit. Unlike
|
|
563
|
+
// the former modulus cadence, this remains bounded as per-file work changes.
|
|
564
|
+
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
350
565
|
for (const file of walked) {
|
|
566
|
+
if (!shouldContinue())
|
|
567
|
+
throw new Error("word index refresh superseded");
|
|
351
568
|
try {
|
|
352
569
|
const stat = fs.statSync(file);
|
|
353
570
|
if (stat.size <= WORD_INDEX_MAX_BYTES) {
|
|
354
|
-
current.set(wordIndexKey(file), {
|
|
571
|
+
current.set(wordIndexKey(file), {
|
|
572
|
+
path: file,
|
|
573
|
+
mtimeMs: stat.mtimeMs,
|
|
574
|
+
size: stat.size,
|
|
575
|
+
});
|
|
355
576
|
}
|
|
356
577
|
}
|
|
357
578
|
catch {
|
|
358
579
|
// A file vanishing between walk and stat is simply absent.
|
|
359
580
|
}
|
|
581
|
+
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
582
|
+
if (!shouldContinue())
|
|
583
|
+
throw new Error("word index refresh superseded");
|
|
584
|
+
}
|
|
360
585
|
}
|
|
586
|
+
const preflightFiles = Object.assign([...current.values()], {
|
|
587
|
+
truncated: walked.length === maxFiles,
|
|
588
|
+
});
|
|
361
589
|
const oldSet = new Set([...index.docLengths.keys()].map(wordIndexKey));
|
|
362
590
|
let changedSet = 0;
|
|
363
591
|
for (const key of oldSet)
|
|
@@ -368,14 +596,85 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
|
|
|
368
596
|
changedSet++;
|
|
369
597
|
const denominator = Math.max(oldSet.size, current.size, 1);
|
|
370
598
|
if (changedSet / denominator > WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) {
|
|
371
|
-
|
|
599
|
+
return { mode: "full-required", reason: "file-set-churn", preflightFiles };
|
|
600
|
+
}
|
|
601
|
+
// Replacing one document filters every shared posting array for that
|
|
602
|
+
// document's tokens. That is excellent for a sparse edit, but repeating it
|
|
603
|
+
// for most of the corpus becomes effectively quadratic. Decide the dense
|
|
604
|
+
// transition BEFORE dropping or editing anything so the old index remains a
|
|
605
|
+
// valid fallback until a separately-built full replacement is ready (#1197).
|
|
606
|
+
let staleDocuments = 0;
|
|
607
|
+
for (const { path: file, mtimeMs, size } of current.values()) {
|
|
608
|
+
if (index.fileMtimes.get(file) !== mtimeMs ||
|
|
609
|
+
(index.fileSizes.get(file) ?? -1) !== size) {
|
|
610
|
+
staleDocuments += 1;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
// Density alone is NOT a bound (#1197 review finding 1): the per-document cost
|
|
614
|
+
// GROWS with the corpus, so a stale set comfortably under any fixed ratio (or
|
|
615
|
+
// any fixed absolute ceiling) still costs orders of magnitude more than a
|
|
616
|
+
// rebuild on a big enough repository. 800 documents with 239 stale — 29.875%,
|
|
617
|
+
// just under the ratio — measured 90.6 s with a 39.6 s synchronous block
|
|
618
|
+
// against 1.3 s / 10.6 ms for a full build, and at the 6,000-file cap up to
|
|
619
|
+
// 1,799 stale documents would have stayed on that path.
|
|
620
|
+
//
|
|
621
|
+
// So compare estimated WORK, both sides denominated in tokenizer tokens:
|
|
622
|
+
// incremental ≈ stale x (docTokens x postingLength x scanCost + readCost)
|
|
623
|
+
// full ≈ totalTokens + corpusSize x readCost
|
|
624
|
+
// This is self-bounding in a way no constant is: the incremental path is taken
|
|
625
|
+
// only while its total estimated cost stays under ONE full rebuild, so the
|
|
626
|
+
// worst case is "about as expensive as rebuilding", whatever the corpus size.
|
|
627
|
+
//
|
|
628
|
+
// Both statistics come from cheap metadata passes — one over `postings`
|
|
629
|
+
// (distinct tokens, reading only `.length`) and one over `forward` (documents,
|
|
630
|
+
// reading only `.size`). Neither touches a posting ELEMENT, which is the work
|
|
631
|
+
// this decision exists to avoid.
|
|
632
|
+
let postingEntries = 0;
|
|
633
|
+
let weightedPostingEntries = 0;
|
|
634
|
+
for (const hits of index.postings.values()) {
|
|
635
|
+
postingEntries += hits.length;
|
|
636
|
+
weightedPostingEntries += hits.length * hits.length;
|
|
637
|
+
}
|
|
638
|
+
// The mean posting-array length weighted by OCCURRENCES, not the plain mean:
|
|
639
|
+
// a document's tokens are drawn from the frequency distribution, so the arrays
|
|
640
|
+
// a removal actually filters skew hard toward the high-document-frequency
|
|
641
|
+
// tokens. The unweighted mean (dominated by the long tail of df=1 tokens)
|
|
642
|
+
// underestimates the real cost by an order of magnitude.
|
|
643
|
+
const expectedPostingLength = postingEntries > 0 ? weightedPostingEntries / postingEntries : 0;
|
|
644
|
+
let distinctTokenEntries = 0;
|
|
645
|
+
for (const tokenLineCounts of index.forward.values()) {
|
|
646
|
+
distinctTokenEntries += tokenLineCounts.size;
|
|
647
|
+
}
|
|
648
|
+
const expectedDocumentTokens = index.docCount > 0 ? distinctTokenEntries / index.docCount : 0;
|
|
649
|
+
const estimatedIncrementalWork = staleDocuments *
|
|
650
|
+
(expectedDocumentTokens *
|
|
651
|
+
expectedPostingLength *
|
|
652
|
+
WORD_INDEX_POSTING_SCAN_TOKEN_COST +
|
|
653
|
+
WORD_INDEX_FILE_READ_TOKEN_COST);
|
|
654
|
+
const estimatedFullRebuildWork = index.totalTokens + current.size * WORD_INDEX_FILE_READ_TOKEN_COST;
|
|
655
|
+
// The document-count floor guards the RATIO test only — its documented job is
|
|
656
|
+
// that "one stale file in a three-file project" is 33% but not dense. It must
|
|
657
|
+
// NOT gate the work test: a floor is another constant, and letting one
|
|
658
|
+
// suppress the work comparison would reintroduce exactly the unbounded-on-the
|
|
659
|
+
// -other-axis shape (31 documents at 98 ms each on this repository's own
|
|
660
|
+
// corpus, and worse as the corpus grows). The work test needs no floor — on a
|
|
661
|
+
// small project a full rebuild is cheap, so it never fires there.
|
|
662
|
+
if ((staleDocuments >= WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS &&
|
|
663
|
+
staleDocuments / Math.max(current.size, 1) >
|
|
664
|
+
WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) ||
|
|
665
|
+
estimatedIncrementalWork > estimatedFullRebuildWork) {
|
|
666
|
+
return {
|
|
667
|
+
mode: "full-required",
|
|
668
|
+
reason: "stale-document-churn",
|
|
669
|
+
preflightFiles,
|
|
670
|
+
};
|
|
372
671
|
}
|
|
373
672
|
let dropped = 0;
|
|
374
673
|
for (const key of oldSet) {
|
|
375
674
|
if (!current.has(key)) {
|
|
376
675
|
// `key` is already folded; removeWordIndexDocument re-folds it
|
|
377
676
|
// idempotently via the PathKeyedMap, so the drop hits the right entry.
|
|
378
|
-
if (!
|
|
677
|
+
if (!(await removeWordIndexDocumentAsync(index, key, shouldContinue))) {
|
|
379
678
|
throw new Error(`failed to drop word-index document: ${key}`);
|
|
380
679
|
}
|
|
381
680
|
dropped++;
|
|
@@ -383,9 +682,16 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
|
|
|
383
682
|
}
|
|
384
683
|
let refreshed = 0;
|
|
385
684
|
let skipped = 0;
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
685
|
+
for (const { path: file, mtimeMs, size } of current.values()) {
|
|
686
|
+
// #1105: mtime-first, size-second freshness — mtime alone is a stale
|
|
687
|
+
// signal (mtime-preserving content changes serve stale identifiers to
|
|
688
|
+
// symbol_search). Size is free (the stat above already read it) and
|
|
689
|
+
// catches every content change that alters byte length. Both come from the
|
|
690
|
+
// SAME stat, so a mismatch on EITHER axis re-reads. `?? -1` makes a file
|
|
691
|
+
// absent from the (possibly legacy, pre-#1105) size map always count as
|
|
692
|
+
// changed → one-time re-read that repopulates the size, the safe direction.
|
|
693
|
+
if (index.fileMtimes.get(file) !== mtimeMs ||
|
|
694
|
+
(index.fileSizes.get(file) ?? -1) !== size) {
|
|
389
695
|
// A file the walk/stat pass saw can still fail to read here — a
|
|
390
696
|
// transient exclusive lock (antivirus, an editor, a build step) or a
|
|
391
697
|
// file that vanished in the interim. Match collectWordIndexDocs'
|
|
@@ -400,14 +706,22 @@ export async function refreshWordIndexIncrementally(index, root, shouldContinue
|
|
|
400
706
|
skipped++;
|
|
401
707
|
continue;
|
|
402
708
|
}
|
|
403
|
-
if (!
|
|
709
|
+
if (!(await updateWordIndexDocumentAsync(index, { path: file, content }, shouldContinue))) {
|
|
404
710
|
throw new Error(`failed to refresh word-index document: ${file}`);
|
|
405
711
|
}
|
|
712
|
+
// updateWordIndexDocument stamps mtime=-1 (per-edit convention); the
|
|
713
|
+
// refresh path KNOWS the real on-disk stat, so overwrite both axes with
|
|
714
|
+
// the true values so this document is not needlessly re-read next pass.
|
|
406
715
|
index.fileMtimes.set(file, mtimeMs);
|
|
716
|
+
index.fileSizes.set(file, size);
|
|
407
717
|
refreshed++;
|
|
408
718
|
}
|
|
409
|
-
|
|
410
|
-
|
|
719
|
+
// Same OR as the stat loop. This is the loop the #1197 outage actually
|
|
720
|
+
// blocked in: replacing one document costs more the larger the corpus is,
|
|
721
|
+
// so a count-only checkpoint let 100 replacements run back to back for
|
|
722
|
+
// seconds. The gate above keeps the stale set sparse; this keeps even a
|
|
723
|
+
// sparse-but-expensive set off the event loop.
|
|
724
|
+
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
411
725
|
if (!shouldContinue())
|
|
412
726
|
throw new Error("word index refresh superseded");
|
|
413
727
|
}
|
|
@@ -510,6 +824,8 @@ export function centralityFromReverseDeps(index, reverseDeps, normalizeKey = (fi
|
|
|
510
824
|
}
|
|
511
825
|
return centrality;
|
|
512
826
|
}
|
|
827
|
+
/** Persisted word-index serialization format version. Bump on breaking format changes. */
|
|
828
|
+
export const WORD_INDEX_FORMAT_VERSION = 2;
|
|
513
829
|
export function serializeWordIndex(index) {
|
|
514
830
|
const files = [...index.docLengths.keys()];
|
|
515
831
|
const fileIndex = new Map();
|
|
@@ -533,7 +849,7 @@ export function serializeWordIndex(index) {
|
|
|
533
849
|
])
|
|
534
850
|
: undefined;
|
|
535
851
|
return {
|
|
536
|
-
version:
|
|
852
|
+
version: WORD_INDEX_FORMAT_VERSION,
|
|
537
853
|
files,
|
|
538
854
|
postings,
|
|
539
855
|
docLengths: files.map((file) => index.docLengths.get(file) ?? 0),
|
|
@@ -541,12 +857,13 @@ export function serializeWordIndex(index) {
|
|
|
541
857
|
indexedFileCount: index.docCount,
|
|
542
858
|
truncated: index.truncated,
|
|
543
859
|
fileMtimes: files.map((file) => index.fileMtimes.get(file) ?? 0),
|
|
860
|
+
fileSizes: files.map((file) => index.fileSizes.get(file) ?? 0),
|
|
544
861
|
forward,
|
|
545
862
|
};
|
|
546
863
|
}
|
|
547
864
|
export function deserializeWordIndex(data) {
|
|
548
865
|
if (!data ||
|
|
549
|
-
data.version !==
|
|
866
|
+
data.version !== WORD_INDEX_FORMAT_VERSION ||
|
|
550
867
|
!Array.isArray(data.files) ||
|
|
551
868
|
!Array.isArray(data.postings) ||
|
|
552
869
|
!Array.isArray(data.docLengths) ||
|
|
@@ -556,8 +873,17 @@ export function deserializeWordIndex(data) {
|
|
|
556
873
|
}
|
|
557
874
|
const docLengths = new PathKeyedMap(wordIndexKey);
|
|
558
875
|
const fileMtimes = new PathKeyedMap(wordIndexKey);
|
|
876
|
+
const fileSizes = new PathKeyedMap(wordIndexKey);
|
|
559
877
|
data.files.forEach((file, i) => docLengths.set(file, data.docLengths[i] ?? 0));
|
|
560
878
|
data.files.forEach((file, i) => fileMtimes.set(file, data.fileMtimes[i] ?? 0));
|
|
879
|
+
// #1105: `fileSizes` is optional on the wire (pre-#1105 snapshots omit it).
|
|
880
|
+
// Only populate when the array is present AND parallel to `files`; otherwise
|
|
881
|
+
// leave it empty so the refresh gate re-reads every file once to repopulate,
|
|
882
|
+
// rather than trusting a bogus/misaligned size. Never treated as "current".
|
|
883
|
+
if (Array.isArray(data.fileSizes) &&
|
|
884
|
+
data.fileSizes.length === data.files.length) {
|
|
885
|
+
data.files.forEach((file, i) => fileSizes.set(file, data.fileSizes?.[i] ?? 0));
|
|
886
|
+
}
|
|
561
887
|
const postings = new Map();
|
|
562
888
|
for (const [token, flat] of data.postings) {
|
|
563
889
|
if (typeof token !== "string" || !Array.isArray(flat))
|
|
@@ -603,6 +929,7 @@ export function deserializeWordIndex(data) {
|
|
|
603
929
|
truncated: data.truncated === true,
|
|
604
930
|
forward,
|
|
605
931
|
fileMtimes,
|
|
932
|
+
fileSizes,
|
|
606
933
|
};
|
|
607
934
|
}
|
|
608
935
|
const buildStatuses = new Map();
|
|
@@ -652,7 +979,7 @@ export function triggerBackgroundWordIndexBuild(cwd, dbg, options = {}) {
|
|
|
652
979
|
try {
|
|
653
980
|
const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION } = await import("./project-snapshot.js");
|
|
654
981
|
const docs = await collectWordIndexDocs(key);
|
|
655
|
-
const index =
|
|
982
|
+
const index = await buildWordIndexAsync(docs);
|
|
656
983
|
const existing = loadProjectSnapshot(key);
|
|
657
984
|
const snapshot = existing ?? {
|
|
658
985
|
version: PROJECT_SNAPSHOT_VERSION,
|