@herbertgao/pi-extensions 2026.8.4 → 2026.8.6
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 +11 -3
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +11 -3
- 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/feature/context.ts +74 -5
- 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 +53 -14
- 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 +18 -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 +20 -4
- 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/fleet-list.ts +15 -6
- 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/@herbertgao/pi-subagents/src/worktree.ts +9 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +2 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +6 -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 +32 -24
- package/node_modules/@narumitw/pi-btw/package.json +6 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +4 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -13
- package/node_modules/@narumitw/pi-btw/src/settings.ts +22 -2
- package/node_modules/pi-lens/CHANGELOG.md +2535 -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/advisory-provenance.js +314 -0
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +14 -7
- 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 +136 -24
- 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-events-logger.js +62 -6
- package/node_modules/pi-lens/dist/clients/bus-publish.js +19 -4
- 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/cascade-format.js +57 -2
- 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 +26 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +137 -31
- 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 +361 -77
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +65 -13
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +18 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +176 -29
- 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 +1172 -683
- 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 +47 -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/availability-policy.js +226 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +69 -0
- 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 +473 -42
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +97 -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 +18 -4
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +17 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +432 -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/fix-worklog.js +5 -1
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +51 -7
- 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 +678 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +23 -40
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +124 -7
- 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 +395 -100
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +27 -55
- package/node_modules/pi-lens/dist/clients/knip-client.js +58 -42
- 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 +72 -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 +63 -9
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +16 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +73 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +30 -4
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +59 -14
- package/node_modules/pi-lens/dist/clients/lsp/client.js +662 -56
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +115 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2025 -800
- 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 +437 -86
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +96 -0
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +21 -5
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +18 -3
- 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 +127 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +235 -14
- 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/model-provider.js +53 -0
- 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 +76 -15
- 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/fresh-fetch.js +1 -1
- 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/runner-adapters/runner-findings.js +23 -2
- 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/query.js +24 -0
- 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/run-duration.js +55 -0
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +257 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +129 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +242 -26
- package/node_modules/pi-lens/dist/clients/runtime-session.js +428 -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 +342 -33
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +619 -93
- package/node_modules/pi-lens/dist/clients/rust-client.js +23 -38
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +90 -6
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +424 -196
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +231 -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 +7 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +469 -32
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +390 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +76 -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 +50 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +680 -81
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +51785 -36118
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +219 -46
- package/node_modules/pi-lens/dist/tools/activate-tools.js +17 -5
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +9 -4
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +203 -38
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +210 -42
- 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/dist/tools/symbol-search.js +1 -1
- package/node_modules/pi-lens/docs/agent-guide.md +40 -15
- 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 +14 -3
- 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 +45 -3
- package/node_modules/pi-lens/docs/globalconfig.md +56 -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 +18 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +41 -5
- package/node_modules/pi-lens/docs/word-index.md +35 -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-chained-type-assertions-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-js-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-conditional-empty-object-spread-test.yml +9 -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-reflect-apply-js-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-laundering-test.yml +11 -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-chained-type-assertions.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread-js.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-conditional-empty-object-spread.yml +29 -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-reflect-apply-js.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +27 -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 +134 -2
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +10 -11
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +22 -22
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +8 -114
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +129 -0
- package/node_modules/pi-lens/skills/pi-lens-write-tree-sitter-rule/SKILL.md +3 -1
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +38 -0
- package/node_modules/pi-mcp-adapter/README.md +38 -4
- package/node_modules/pi-mcp-adapter/agent-dir.ts +12 -4
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/cli.js +25 -4
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/config.ts +4 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +58 -8
- 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/mcp-setup-panel.ts +2 -1
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +37 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +4 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +336 -0
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +41 -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 +62 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +176 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/node_modules/pi-web-access/CHANGELOG.md +14 -0
- package/node_modules/pi-web-access/README.md +18 -12
- package/node_modules/pi-web-access/auth-fetch.ts +148 -0
- package/node_modules/pi-web-access/chrome-cookies.ts +110 -23
- package/node_modules/pi-web-access/curator-page.ts +5 -3
- package/node_modules/pi-web-access/curator-server.ts +2 -1
- package/node_modules/pi-web-access/extract.ts +106 -34
- package/node_modules/pi-web-access/fetch-params.ts +17 -3
- package/node_modules/pi-web-access/firecrawl.ts +172 -12
- package/node_modules/pi-web-access/gemini-search.ts +18 -4
- package/node_modules/pi-web-access/index.ts +120 -48
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/summary-review.ts +11 -5
- package/node_modules/pi-web-access/youtube-extract.ts +2 -2
- package/package.json +12 -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
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
* patterns apply to files inside that package, in addition to (and with
|
|
58
58
|
* higher precedence than) the root config's `ignore` patterns.
|
|
59
59
|
*/
|
|
60
|
+
import { logExtension } from "./extension-log.js";
|
|
61
|
+
import { notifyUserDegradation } from "./user-notify.js";
|
|
60
62
|
import * as fs from "node:fs";
|
|
61
63
|
import * as os from "node:os";
|
|
62
64
|
import * as path from "node:path";
|
|
@@ -107,11 +109,17 @@ export function loadPiLensProjectConfig(startDir, preloadedInfo = findPiLensProj
|
|
|
107
109
|
if (!configInfo)
|
|
108
110
|
return EMPTY_PROJECT_CONFIG;
|
|
109
111
|
const cached = configCache.get(configInfo.path);
|
|
110
|
-
if (cached &&
|
|
112
|
+
if (cached &&
|
|
113
|
+
cached.mtimeMs === configInfo.mtimeMs &&
|
|
114
|
+
cached.size === configInfo.size) {
|
|
111
115
|
return cached.config;
|
|
112
116
|
}
|
|
113
117
|
const config = parseConfigFile(configInfo.path);
|
|
114
|
-
configCache.set(configInfo.path, {
|
|
118
|
+
configCache.set(configInfo.path, {
|
|
119
|
+
mtimeMs: configInfo.mtimeMs,
|
|
120
|
+
size: configInfo.size,
|
|
121
|
+
config,
|
|
122
|
+
});
|
|
115
123
|
return config;
|
|
116
124
|
}
|
|
117
125
|
/** For tests + callers that need to force a re-read (e.g. config-watcher hooks). */
|
|
@@ -137,7 +145,12 @@ export function findPiLensConfigInDir(dir) {
|
|
|
137
145
|
const marker = findPiLensConfigMarkerInDir(dir);
|
|
138
146
|
if (!marker)
|
|
139
147
|
return undefined;
|
|
140
|
-
return {
|
|
148
|
+
return {
|
|
149
|
+
path: marker.path,
|
|
150
|
+
dir: marker.dir,
|
|
151
|
+
mtimeMs: marker.mtimeMs,
|
|
152
|
+
size: marker.size,
|
|
153
|
+
};
|
|
141
154
|
}
|
|
142
155
|
/**
|
|
143
156
|
* Find the closest config, between an edited file and the project root, that
|
|
@@ -174,11 +187,11 @@ export function loadPiLensConfigInDir(dir) {
|
|
|
174
187
|
if (!info)
|
|
175
188
|
return EMPTY_PROJECT_CONFIG;
|
|
176
189
|
const cached = configCache.get(info.path);
|
|
177
|
-
if (cached && cached.mtimeMs === info.mtimeMs) {
|
|
190
|
+
if (cached && cached.mtimeMs === info.mtimeMs && cached.size === info.size) {
|
|
178
191
|
return cached.config;
|
|
179
192
|
}
|
|
180
193
|
const config = parseConfigFile(info.path);
|
|
181
|
-
configCache.set(info.path, { mtimeMs: info.mtimeMs, config });
|
|
194
|
+
configCache.set(info.path, { mtimeMs: info.mtimeMs, size: info.size, config });
|
|
182
195
|
return config;
|
|
183
196
|
}
|
|
184
197
|
export function findPiLensProjectConfig(startDir) {
|
|
@@ -189,7 +202,7 @@ export function findPiLensProjectConfig(startDir) {
|
|
|
189
202
|
return undefined;
|
|
190
203
|
const stat = safeFileStat(cached.info.path);
|
|
191
204
|
if (stat?.isFile())
|
|
192
|
-
return { ...cached.info, mtimeMs: stat.mtimeMs };
|
|
205
|
+
return { ...cached.info, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
193
206
|
}
|
|
194
207
|
const discovered = discoverPiLensProjectConfig(cacheKey);
|
|
195
208
|
discoveryCache.set(cacheKey, discovered);
|
|
@@ -223,7 +236,7 @@ function discoverPiLensProjectConfig(startDir) {
|
|
|
223
236
|
const stat = safeFileStat(candidate);
|
|
224
237
|
if (stat?.isFile()) {
|
|
225
238
|
return {
|
|
226
|
-
info: { path: candidate, dir, mtimeMs: stat.mtimeMs },
|
|
239
|
+
info: { path: candidate, dir, mtimeMs: stat.mtimeMs, size: stat.size },
|
|
227
240
|
dirMtimes,
|
|
228
241
|
};
|
|
229
242
|
}
|
|
@@ -236,7 +249,41 @@ function warnInvalidConfigOnce(configPath, reason) {
|
|
|
236
249
|
if (warnedInvalidConfigs.has(key))
|
|
237
250
|
return;
|
|
238
251
|
warnedInvalidConfigs.add(key);
|
|
239
|
-
|
|
252
|
+
const message = `ignoring invalid project config ${configPath}: ${reason}`;
|
|
253
|
+
logExtension({
|
|
254
|
+
subsystem: "project-lens-config",
|
|
255
|
+
level: "warn",
|
|
256
|
+
message,
|
|
257
|
+
metadata: { configPath, reason },
|
|
258
|
+
});
|
|
259
|
+
// HUMAN-audience too: the user's own `.pi-lens.json` is being ignored.
|
|
260
|
+
notifyUserDegradation(`pi-lens: ${message}`);
|
|
261
|
+
}
|
|
262
|
+
function parseRulePolicyList(configPath, ruleId, key, value) {
|
|
263
|
+
if (!Array.isArray(value)) {
|
|
264
|
+
warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must be an array of strings`);
|
|
265
|
+
return { list: [], invalid: true };
|
|
266
|
+
}
|
|
267
|
+
const list = [];
|
|
268
|
+
for (const entry of value) {
|
|
269
|
+
if (typeof entry !== "string")
|
|
270
|
+
continue;
|
|
271
|
+
const trimmed = entry.trim();
|
|
272
|
+
if (trimmed.length > 0)
|
|
273
|
+
list.push(trimmed);
|
|
274
|
+
}
|
|
275
|
+
if (list.length === 0) {
|
|
276
|
+
// #1087: an explicitly empty array (`"disable": []`) is a well-formed
|
|
277
|
+
// no-op, not an error — don't warn. Only warn when the array HAD entries
|
|
278
|
+
// but none were usable strings (all blank / non-string), which is a real
|
|
279
|
+
// authoring mistake that must not fail silently.
|
|
280
|
+
if (value.length > 0) {
|
|
281
|
+
warnInvalidConfigOnce(configPath, `rules.${ruleId}.${key} must contain at least one non-empty string`);
|
|
282
|
+
return { list: [], invalid: true };
|
|
283
|
+
}
|
|
284
|
+
return { list: [], invalid: false };
|
|
285
|
+
}
|
|
286
|
+
return { list, invalid: false };
|
|
240
287
|
}
|
|
241
288
|
function parseConfigFile(configPath) {
|
|
242
289
|
let raw;
|
|
@@ -264,18 +311,48 @@ function parseConfigFile(configPath) {
|
|
|
264
311
|
if (obj.rules && typeof obj.rules === "object" && !Array.isArray(obj.rules)) {
|
|
265
312
|
const rawRules = obj.rules;
|
|
266
313
|
for (const [ruleId, ruleCfg] of Object.entries(rawRules)) {
|
|
314
|
+
// #444's own example writes the lists directly under `rules` (`rules.
|
|
315
|
+
// disable`), which lands here as an array and would otherwise be
|
|
316
|
+
// dropped without a word — the one shape a user is most likely to try.
|
|
267
317
|
if (!ruleCfg || typeof ruleCfg !== "object" || Array.isArray(ruleCfg)) {
|
|
318
|
+
warnInvalidConfigOnce(configPath, `rules.${ruleId} must be an object with threshold, disable, or select; ignored`);
|
|
268
319
|
continue;
|
|
269
320
|
}
|
|
270
321
|
const r = ruleCfg;
|
|
322
|
+
const entry = {};
|
|
271
323
|
if (typeof r.threshold === "number" &&
|
|
272
324
|
Number.isFinite(r.threshold) &&
|
|
273
325
|
r.threshold > 0) {
|
|
274
|
-
|
|
326
|
+
entry.threshold = r.threshold;
|
|
275
327
|
}
|
|
276
328
|
else if ("threshold" in r) {
|
|
277
329
|
warnInvalidConfigOnce(configPath, `rules.${ruleId}.threshold must be a positive finite number`);
|
|
278
330
|
}
|
|
331
|
+
if ("disable" in r) {
|
|
332
|
+
const parsed = parseRulePolicyList(configPath, ruleId, "disable", r.disable);
|
|
333
|
+
// #1087: an explicitly empty list is valid-but-empty (no warning);
|
|
334
|
+
// don't store a pointless no-op entry for it.
|
|
335
|
+
if (!parsed.invalid && parsed.list.length > 0)
|
|
336
|
+
entry.disable = parsed.list;
|
|
337
|
+
}
|
|
338
|
+
if ("select" in r) {
|
|
339
|
+
const parsed = parseRulePolicyList(configPath, ruleId, "select", r.select);
|
|
340
|
+
if (!parsed.invalid && parsed.list.length > 0)
|
|
341
|
+
entry.select = parsed.list;
|
|
342
|
+
}
|
|
343
|
+
// Honor both threshold-only and policy-only entries; only drop if
|
|
344
|
+
// the entry had no recognized fields at all (e.g. { unrelated: true }).
|
|
345
|
+
// A recognized-but-malformed field already warned above, so only warn
|
|
346
|
+
// here when nothing recognized was spelled at all — #444 proposed
|
|
347
|
+
// `only` rather than `select`, and that typo must not fail silent.
|
|
348
|
+
if (entry.threshold !== undefined || entry.disable || entry.select) {
|
|
349
|
+
rules[ruleId] = entry;
|
|
350
|
+
}
|
|
351
|
+
else if (!("threshold" in r) &&
|
|
352
|
+
!("disable" in r) &&
|
|
353
|
+
!("select" in r)) {
|
|
354
|
+
warnInvalidConfigOnce(configPath, `rules.${ruleId} has no recognized setting (threshold, disable, select); ignored`);
|
|
355
|
+
}
|
|
279
356
|
}
|
|
280
357
|
}
|
|
281
358
|
let maxProjectFiles;
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
* into symbol-node metadata so this path can read it for free.
|
|
35
35
|
*/
|
|
36
36
|
import * as path from "node:path";
|
|
37
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
38
|
-
import {
|
|
37
|
+
import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
|
|
38
|
+
import { loadProjectSnapshotWithoutWordIndex } from "./project-snapshot.js";
|
|
39
39
|
const DEFAULT_LIMIT = 10;
|
|
40
40
|
const STALE_THRESHOLD_MS = 15 * 60_000; // 15 minutes
|
|
41
41
|
const LOW_COVERAGE_THRESHOLD = 0.8;
|
|
@@ -47,14 +47,19 @@ function clampLimit(limit) {
|
|
|
47
47
|
}
|
|
48
48
|
// Same display-path convention as module-report.ts's toDisplayPath: cwd-relative
|
|
49
49
|
// + forward-slashed under the project root, else the absolute (slash-normalized)
|
|
50
|
-
// path.
|
|
50
|
+
// path. Delegates to the shape-aware toProjectRelativePath (#1163/#1194) instead
|
|
51
|
+
// of hand-rolling with host-default path.isAbsolute/path.relative, which
|
|
52
|
+
// mis-parses a Windows-shaped path (e.g. a persisted graph symbol-key path) on
|
|
53
|
+
// Linux CI: path.isAbsolute("C:\\repo\\x.ts") is false there, short-circuiting
|
|
54
|
+
// to the raw absolute path instead of relativizing (#1024 divergence class).
|
|
51
55
|
function toDisplayPath(p, projectRoot) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
return toProjectRelativePath(p, projectRoot);
|
|
57
|
+
}
|
|
58
|
+
/** Test-only export (refs #1194) so the shape-aware delegation can be verified
|
|
59
|
+
* directly without standing up a full review graph — mirrors the
|
|
60
|
+
* `_reset*ForTests` underscore convention used elsewhere in this file. */
|
|
61
|
+
export function _toDisplayPathForTests(p, projectRoot) {
|
|
62
|
+
return toDisplayPath(p, projectRoot);
|
|
58
63
|
}
|
|
59
64
|
function suggestedNext(displayPath) {
|
|
60
65
|
return { tool: "module_report", path: displayPath };
|
|
@@ -110,9 +115,10 @@ function buildFileDegrees(graph) {
|
|
|
110
115
|
// --- section 1: trust header --------------------------------------------------
|
|
111
116
|
function computeTrust(graph, cwd) {
|
|
112
117
|
const filesCovered = graph.fileNodes.size;
|
|
113
|
-
const snapshot =
|
|
118
|
+
const snapshot = loadProjectSnapshotWithoutWordIndex(cwd);
|
|
114
119
|
const snapshotFileCount = snapshot ? Object.keys(snapshot.files).length : 0;
|
|
115
|
-
const
|
|
120
|
+
const persistedTotal = graph.persistCoverage?.totalFiles ?? 0;
|
|
121
|
+
const filesTotal = Math.max(filesCovered, snapshotFileCount, persistedTotal);
|
|
116
122
|
const coverage = filesTotal > 0 ? filesCovered / filesTotal : 1;
|
|
117
123
|
let exact = 0;
|
|
118
124
|
let imp = 0;
|
|
@@ -142,7 +148,8 @@ function computeTrust(graph, cwd) {
|
|
|
142
148
|
const frac = (n) => (sampleSize > 0 ? n / sampleSize : 0);
|
|
143
149
|
const ageMs = Date.now() - Date.parse(graph.builtAt);
|
|
144
150
|
const stale = Number.isFinite(ageMs) && ageMs > STALE_THRESHOLD_MS;
|
|
145
|
-
const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD ||
|
|
151
|
+
const lowCoverage = coverage < LOW_COVERAGE_THRESHOLD ||
|
|
152
|
+
graph.persistCoverage?.partial === true;
|
|
146
153
|
const notes = [];
|
|
147
154
|
if (stale) {
|
|
148
155
|
const ageMin = Math.round(ageMs / 60_000);
|
|
@@ -151,7 +158,14 @@ function computeTrust(graph, cwd) {
|
|
|
151
158
|
if (lowCoverage) {
|
|
152
159
|
if (graph.persistCoverage?.partial) {
|
|
153
160
|
const p = graph.persistCoverage;
|
|
154
|
-
|
|
161
|
+
const totalFiles = p.totalFiles ?? filesCovered;
|
|
162
|
+
const persistedFiles = p.persistedFiles ?? filesCovered;
|
|
163
|
+
if (p.sourceFilesTruncated) {
|
|
164
|
+
notes.push(`Partial source walk: at least ${totalFiles} source files were represented before the visited-entry budget stopped enumeration; ${persistedFiles} files are in this graph.`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
notes.push(`Partial persisted graph: ${p.persistedNodes}/${p.totalNodes} nodes, ${p.persistedEdges}/${p.totalEdges} edges, and ${persistedFiles}/${totalFiles} files were retained under the ${p.cap}-element cap — whole subsystems may be invisible below.`);
|
|
168
|
+
}
|
|
155
169
|
}
|
|
156
170
|
else {
|
|
157
171
|
notes.push(`Low coverage: only ${filesCovered}/${filesTotal} project files (${Math.round(coverage * 100)}%) are in the graph — whole subsystems may be invisible below.`);
|
|
@@ -127,6 +127,12 @@ const SNAPSHOT_PARSE_CACHE_MAX = 4;
|
|
|
127
127
|
// against the UNCOMPRESSED body size, never the (much smaller) gz file size.
|
|
128
128
|
const SNAPSHOT_PARSE_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
129
129
|
const snapshotParseCache = new Map();
|
|
130
|
+
function withoutWordIndex(snapshot) {
|
|
131
|
+
if (!snapshot?.wordIndex)
|
|
132
|
+
return snapshot;
|
|
133
|
+
const { wordIndex: _releasedPostings, ...stripped } = snapshot;
|
|
134
|
+
return stripped;
|
|
135
|
+
}
|
|
130
136
|
function cacheParsedSnapshot(snapshotPath, entry) {
|
|
131
137
|
// Refresh recency (Map preserves insertion order).
|
|
132
138
|
snapshotParseCache.delete(snapshotPath);
|
|
@@ -139,11 +145,64 @@ function cacheParsedSnapshot(snapshotPath, entry) {
|
|
|
139
145
|
}
|
|
140
146
|
}
|
|
141
147
|
const authoritativeSnapshots = new Map();
|
|
148
|
+
const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
|
|
149
|
+
const PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
|
|
150
|
+
function projectSnapshotIdleEvictMs() {
|
|
151
|
+
const value = Number.parseInt(process.env.PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS ?? "", 10);
|
|
152
|
+
return Number.isSafeInteger(value) && value > 0 ? value : PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT;
|
|
153
|
+
}
|
|
154
|
+
function clearAuthoritativeSnapshotTimer(entry) {
|
|
155
|
+
if (entry.idleTimer)
|
|
156
|
+
clearTimeout(entry.idleTimer);
|
|
157
|
+
entry.idleTimer = undefined;
|
|
158
|
+
}
|
|
159
|
+
function deleteAuthoritativeSnapshot(key) {
|
|
160
|
+
const entry = authoritativeSnapshots.get(key);
|
|
161
|
+
if (entry)
|
|
162
|
+
clearAuthoritativeSnapshotTimer(entry);
|
|
163
|
+
authoritativeSnapshots.delete(key);
|
|
164
|
+
}
|
|
165
|
+
function scheduleAuthoritativeSnapshotEviction(key, entry) {
|
|
166
|
+
clearAuthoritativeSnapshotTimer(entry);
|
|
167
|
+
const generation = entry.lastUsedAt;
|
|
168
|
+
entry.idleTimer = setTimeout(() => {
|
|
169
|
+
entry.idleTimer = undefined;
|
|
170
|
+
if (authoritativeSnapshots.get(key) !== entry || entry.lastUsedAt !== generation)
|
|
171
|
+
return;
|
|
172
|
+
deleteAuthoritativeSnapshot(key);
|
|
173
|
+
}, projectSnapshotIdleEvictMs());
|
|
174
|
+
entry.idleTimer.unref?.();
|
|
175
|
+
}
|
|
176
|
+
function touchAuthoritativeSnapshot(key, entry) {
|
|
177
|
+
entry.lastUsedAt = Date.now();
|
|
178
|
+
scheduleAuthoritativeSnapshotEviction(key, entry);
|
|
179
|
+
}
|
|
180
|
+
function enforceAuthoritativeSnapshotCap() {
|
|
181
|
+
while (authoritativeSnapshots.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
|
|
182
|
+
const victim = [...authoritativeSnapshots.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
|
|
183
|
+
if (!victim)
|
|
184
|
+
return;
|
|
185
|
+
clearAuthoritativeSnapshotTimer(victim[1]);
|
|
186
|
+
deleteAuthoritativeSnapshot(victim[0]);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** Test-only cache keys, in LRU order from oldest to newest. */
|
|
190
|
+
export function _getAuthoritativeSnapshotCacheKeysForTests() {
|
|
191
|
+
return [...authoritativeSnapshots.entries()]
|
|
192
|
+
.sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)
|
|
193
|
+
.map(([key]) => key);
|
|
194
|
+
}
|
|
142
195
|
/** Test hook: drop all cached parses + authoritative writes (per-worker isolation). */
|
|
143
196
|
export function _resetProjectSnapshotParseCacheForTests() {
|
|
144
197
|
snapshotParseCache.clear();
|
|
198
|
+
for (const entry of authoritativeSnapshots.values())
|
|
199
|
+
clearAuthoritativeSnapshotTimer(entry);
|
|
145
200
|
authoritativeSnapshots.clear();
|
|
146
201
|
}
|
|
202
|
+
/** Test hook: prove the parse-cache tier never owns serialized postings. */
|
|
203
|
+
export function _projectSnapshotParseCacheRetainsWordIndexForTests() {
|
|
204
|
+
return [...snapshotParseCache.values()].some((entry) => entry.snapshot?.wordIndex !== undefined);
|
|
205
|
+
}
|
|
147
206
|
/** Resolve which body file is currently on disk: gz canonical, else legacy. */
|
|
148
207
|
function resolveSnapshotBodyPath(cwd) {
|
|
149
208
|
const gzPath = getProjectSnapshotPath(cwd);
|
|
@@ -219,7 +278,7 @@ function readSnapshotBody(bodyPath, gz) {
|
|
|
219
278
|
return { snapshot: null, rawBytes: 0 };
|
|
220
279
|
}
|
|
221
280
|
}
|
|
222
|
-
|
|
281
|
+
function loadProjectSnapshotInternal(cwd, requireWordIndex) {
|
|
223
282
|
const key = normalizeMapKey(cwd);
|
|
224
283
|
const body = resolveSnapshotBodyPath(cwd);
|
|
225
284
|
// Authoritative in-process write wins while our own (possibly still
|
|
@@ -230,11 +289,12 @@ export function loadProjectSnapshot(cwd) {
|
|
|
230
289
|
if (authoritative) {
|
|
231
290
|
const diskMtime = body ? body.mtimeMs : Number.NEGATIVE_INFINITY;
|
|
232
291
|
if (diskMtime <= authoritative.knownMtime) {
|
|
292
|
+
touchAuthoritativeSnapshot(key, authoritative);
|
|
233
293
|
return authoritative.snapshot;
|
|
234
294
|
}
|
|
235
295
|
// An external writer moved past our write — honor disk and stop
|
|
236
296
|
// serving the now-stale in-memory object.
|
|
237
|
-
|
|
297
|
+
deleteAuthoritativeSnapshot(key);
|
|
238
298
|
}
|
|
239
299
|
if (!body) {
|
|
240
300
|
snapshotParseCache.delete(getProjectSnapshotPath(cwd));
|
|
@@ -246,14 +306,21 @@ export function loadProjectSnapshot(cwd) {
|
|
|
246
306
|
// SnapshotParseCacheEntry for why: coarse FAT/exFAT mtime resolution can
|
|
247
307
|
// otherwise alias a just-rewritten file onto a stale cache entry).
|
|
248
308
|
if (cached && cached.mtimeMs === body.mtimeMs && cached.size === body.size) {
|
|
249
|
-
|
|
309
|
+
if (!requireWordIndex || !cached.snapshot || cached.snapshot.wordIndex) {
|
|
310
|
+
return cached.snapshot;
|
|
311
|
+
}
|
|
250
312
|
}
|
|
251
313
|
const { snapshot, rawBytes } = readSnapshotBody(body.path, body.gz);
|
|
252
|
-
|
|
314
|
+
// Serialized postings expand into a much larger object graph. Cache only a
|
|
315
|
+
// shallow postings-stripped body: metadata/report consumers stay warm while
|
|
316
|
+
// the live warm WordIndex remains the sole retained postings graph (#1370).
|
|
317
|
+
const cacheSnapshot = withoutWordIndex(snapshot);
|
|
318
|
+
if (rawBytes > 0 &&
|
|
319
|
+
(rawBytes <= SNAPSHOT_PARSE_CACHE_MAX_BYTES || snapshot?.wordIndex)) {
|
|
253
320
|
cacheParsedSnapshot(cacheKey, {
|
|
254
321
|
mtimeMs: body.mtimeMs,
|
|
255
322
|
size: body.size,
|
|
256
|
-
snapshot,
|
|
323
|
+
snapshot: cacheSnapshot,
|
|
257
324
|
});
|
|
258
325
|
}
|
|
259
326
|
else {
|
|
@@ -261,11 +328,24 @@ export function loadProjectSnapshot(cwd) {
|
|
|
261
328
|
}
|
|
262
329
|
return snapshot;
|
|
263
330
|
}
|
|
331
|
+
/** Load the canonical body, including serialized postings when present. */
|
|
332
|
+
export function loadProjectSnapshot(cwd) {
|
|
333
|
+
return loadProjectSnapshotInternal(cwd, true);
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Load snapshot metadata without retaining or re-reading serialized postings.
|
|
337
|
+
* After publication this is served by the postings-stripped parse cache.
|
|
338
|
+
*/
|
|
339
|
+
export function loadProjectSnapshotWithoutWordIndex(cwd) {
|
|
340
|
+
return loadProjectSnapshotInternal(cwd, false);
|
|
341
|
+
}
|
|
264
342
|
const _snapshotGenerations = new Map();
|
|
265
343
|
const _snapshotWorkerRequests = new Map();
|
|
266
344
|
let _snapshotPersistWorker;
|
|
267
345
|
let _snapshotWorkerRequestId = 0;
|
|
268
346
|
let _snapshotWorkerDisabled = false;
|
|
347
|
+
let _snapshotGenerationGateEnabledForTests = true;
|
|
348
|
+
let _snapshotPromotionSeamForTests;
|
|
269
349
|
let _lastSnapshotPersistErrorForTests;
|
|
270
350
|
function snapshotWorkerEnabled() {
|
|
271
351
|
// The synchronous fallback writer is a legitimate degraded mode (hosts that
|
|
@@ -281,7 +361,7 @@ function recordSnapshotPersistFailure(cwd, error) {
|
|
|
281
361
|
// seq), and dropping the authoritative entry means the next load reflects
|
|
282
362
|
// what is ACTUALLY on disk rather than the object we failed to persist.
|
|
283
363
|
_lastSnapshotPersistErrorForTests = error;
|
|
284
|
-
|
|
364
|
+
deleteAuthoritativeSnapshot(normalizeMapKey(cwd));
|
|
285
365
|
logLatency({
|
|
286
366
|
type: "phase",
|
|
287
367
|
phase: "project_snapshot_persist_failed",
|
|
@@ -289,7 +369,8 @@ function recordSnapshotPersistFailure(cwd, error) {
|
|
|
289
369
|
durationMs: 0,
|
|
290
370
|
metadata: { error },
|
|
291
371
|
});
|
|
292
|
-
|
|
372
|
+
// #1333: the logLatency call above already carries this failure to
|
|
373
|
+
// latency.log — the console.error was a duplicate RAW write into pi's frame.
|
|
293
374
|
}
|
|
294
375
|
function logSnapshotPersistSuccess(pending, stats) {
|
|
295
376
|
logLatency({
|
|
@@ -312,6 +393,33 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
|
|
|
312
393
|
// a superseding save already replaced it with a newer object.
|
|
313
394
|
if (!entry || entry.snapshot !== pending.snapshot)
|
|
314
395
|
return;
|
|
396
|
+
// The worker needs the serialized snapshot until promotion completes, but
|
|
397
|
+
// retaining it afterward duplicates the mutable warm index's postings. A
|
|
398
|
+
// shared reference is unsafe: ProjectSnapshot stores serialized arrays while
|
|
399
|
+
// WordIndex owns mutable Map/PathKeyedMap state. Drop the authoritative copy
|
|
400
|
+
// after publication; later merge-writers rehydrate the canonical disk body.
|
|
401
|
+
if (pending.snapshot.wordIndex) {
|
|
402
|
+
try {
|
|
403
|
+
const stat = fs.statSync(pending.gzPath);
|
|
404
|
+
cacheParsedSnapshot(pending.gzPath, {
|
|
405
|
+
mtimeMs: stat.mtimeMs,
|
|
406
|
+
size: stat.size,
|
|
407
|
+
snapshot: withoutWordIndex(pending.snapshot),
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch {
|
|
411
|
+
// A cache miss is safe: the first metadata consumer reconstructs it.
|
|
412
|
+
}
|
|
413
|
+
deleteAuthoritativeSnapshot(pending.key);
|
|
414
|
+
logLatency({
|
|
415
|
+
type: "phase",
|
|
416
|
+
phase: "project_snapshot_word_index_released",
|
|
417
|
+
filePath: pending.gzPath,
|
|
418
|
+
durationMs: 0,
|
|
419
|
+
metadata: { rawBytes },
|
|
420
|
+
});
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
315
423
|
if (rawBytes > SNAPSHOT_PARSE_CACHE_MAX_BYTES) {
|
|
316
424
|
// Benign but invisible otherwise: the next load will re-parse this body
|
|
317
425
|
// from disk instead of serving the in-process object.
|
|
@@ -322,7 +430,7 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
|
|
|
322
430
|
durationMs: 0,
|
|
323
431
|
metadata: { rawBytes, maxBytes: SNAPSHOT_PARSE_CACHE_MAX_BYTES },
|
|
324
432
|
});
|
|
325
|
-
|
|
433
|
+
deleteAuthoritativeSnapshot(pending.key);
|
|
326
434
|
return;
|
|
327
435
|
}
|
|
328
436
|
try {
|
|
@@ -378,6 +486,19 @@ function writeSnapshotBodyOnMainThread(pending, reason) {
|
|
|
378
486
|
recordSnapshotPersistFailure(pending.cwd, err instanceof Error ? err.message : String(err));
|
|
379
487
|
}
|
|
380
488
|
}
|
|
489
|
+
/**
|
|
490
|
+
* The ForTests promotion seam covers worker-message and every main-thread
|
|
491
|
+
* fallback promotion. It stays sync and seam-free in production, identical to
|
|
492
|
+
* calling the writer directly. The process-exit hook is deliberately the sole
|
|
493
|
+
* direct write because an exit handler cannot await this asynchronous seam.
|
|
494
|
+
*/
|
|
495
|
+
function dispatchMainThreadWriteThroughSeam(pending, reason) {
|
|
496
|
+
if (_snapshotPromotionSeamForTests) {
|
|
497
|
+
void _snapshotPromotionSeamForTests().then(() => writeSnapshotBodyOnMainThread(pending, reason));
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
writeSnapshotBodyOnMainThread(pending, reason);
|
|
501
|
+
}
|
|
381
502
|
function handleSnapshotWorkerResult(result) {
|
|
382
503
|
const pending = _snapshotWorkerRequests.get(result.id);
|
|
383
504
|
if (!pending) {
|
|
@@ -391,12 +512,15 @@ function handleSnapshotWorkerResult(result) {
|
|
|
391
512
|
result.serializeMs === undefined ||
|
|
392
513
|
result.writeMs === undefined) {
|
|
393
514
|
fs.rm(result.stagePath, { force: true }, () => { });
|
|
394
|
-
|
|
515
|
+
dispatchMainThreadWriteThroughSeam(pending, result.error ?? "invalid worker result");
|
|
395
516
|
return;
|
|
396
517
|
}
|
|
397
518
|
// Generation gate: a newer save already superseded this one — discard the
|
|
398
519
|
// stale stage file rather than promote it over the fresher body.
|
|
399
|
-
if (
|
|
520
|
+
if (_snapshotGenerationGateEnabledForTests &&
|
|
521
|
+
_snapshotGenerations.get(pending.key) !== result.generation) {
|
|
522
|
+
// The stale stage is part of the promotion transaction: remove it before
|
|
523
|
+
// returning so a superseded save cannot leave an orphan behind.
|
|
400
524
|
fs.rm(result.stagePath, { force: true }, () => { });
|
|
401
525
|
return;
|
|
402
526
|
}
|
|
@@ -414,7 +538,7 @@ function handleSnapshotWorkerResult(result) {
|
|
|
414
538
|
}
|
|
415
539
|
catch (err) {
|
|
416
540
|
fs.rm(result.stagePath, { force: true }, () => { });
|
|
417
|
-
|
|
541
|
+
dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
|
|
418
542
|
}
|
|
419
543
|
}
|
|
420
544
|
function handleSnapshotWorkerDeath(reason) {
|
|
@@ -423,7 +547,7 @@ function handleSnapshotWorkerDeath(reason) {
|
|
|
423
547
|
const requests = [..._snapshotWorkerRequests.values()];
|
|
424
548
|
_snapshotWorkerRequests.clear();
|
|
425
549
|
for (const pending of requests)
|
|
426
|
-
|
|
550
|
+
dispatchMainThreadWriteThroughSeam(pending, reason);
|
|
427
551
|
}
|
|
428
552
|
function resolveSnapshotPersistWorkerPath() {
|
|
429
553
|
// esbuild does NOT rewrite new URL(...) asset refs, so from the bundled
|
|
@@ -460,8 +584,17 @@ function getSnapshotPersistWorker() {
|
|
|
460
584
|
return undefined;
|
|
461
585
|
}
|
|
462
586
|
const worker = new Worker(workerPath);
|
|
463
|
-
|
|
464
|
-
|
|
587
|
+
// The ForTests promotion seam wraps ONLY when set — the production path
|
|
588
|
+
// binds the sync handler directly, so scheduling is byte-identical when
|
|
589
|
+
// no test seam is installed (the async-handler variant of this shifted
|
|
590
|
+
// promotion timing under full-suite load and flaked the round-trip test).
|
|
591
|
+
worker.on("message", (result) => {
|
|
592
|
+
if (_snapshotPromotionSeamForTests) {
|
|
593
|
+
void _snapshotPromotionSeamForTests().then(() => handleSnapshotWorkerResult(result));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
handleSnapshotWorkerResult(result);
|
|
597
|
+
});
|
|
465
598
|
worker.on("error", (err) => handleSnapshotWorkerDeath(err.message));
|
|
466
599
|
worker.on("exit", (code) => {
|
|
467
600
|
if (_snapshotPersistWorker === worker)
|
|
@@ -475,7 +608,7 @@ function getSnapshotPersistWorker() {
|
|
|
475
608
|
if (stranded.length > 0) {
|
|
476
609
|
_snapshotWorkerRequests.clear();
|
|
477
610
|
for (const pending of stranded) {
|
|
478
|
-
|
|
611
|
+
dispatchMainThreadWriteThroughSeam(pending, `persist worker exited with code ${code}`);
|
|
479
612
|
}
|
|
480
613
|
}
|
|
481
614
|
// Only an ABNORMAL exit disables respawning; a clean idle exit (nothing
|
|
@@ -483,6 +616,9 @@ function getSnapshotPersistWorker() {
|
|
|
483
616
|
if (code !== 0)
|
|
484
617
|
_snapshotWorkerDisabled = true;
|
|
485
618
|
});
|
|
619
|
+
// #1148: adding a message listener refs the Worker's public MessagePort.
|
|
620
|
+
// Unref only after every listener is installed so it stays background-only.
|
|
621
|
+
worker.unref();
|
|
486
622
|
_snapshotPersistWorker = worker;
|
|
487
623
|
return worker;
|
|
488
624
|
}
|
|
@@ -585,7 +721,14 @@ export function saveProjectSnapshot(cwd, snapshot) {
|
|
|
585
721
|
// legacy body to a merge-consumer, silently dropping this snapshot's fields.
|
|
586
722
|
const priorBody = resolveSnapshotBodyPath(cwd);
|
|
587
723
|
const knownMtime = priorBody ? priorBody.mtimeMs : Number.NEGATIVE_INFINITY;
|
|
588
|
-
|
|
724
|
+
const authoritativeEntry = {
|
|
725
|
+
snapshot,
|
|
726
|
+
knownMtime,
|
|
727
|
+
lastUsedAt: Date.now(),
|
|
728
|
+
};
|
|
729
|
+
authoritativeSnapshots.set(key, authoritativeEntry);
|
|
730
|
+
scheduleAuthoritativeSnapshotEviction(key, authoritativeEntry);
|
|
731
|
+
enforceAuthoritativeSnapshotCap();
|
|
589
732
|
// A stale disk-parse-cache entry for this path must not out-vote the fresh
|
|
590
733
|
// authoritative write once the latter is dropped (oversized bodies).
|
|
591
734
|
snapshotParseCache.delete(gzPath);
|
|
@@ -604,12 +747,12 @@ export function saveProjectSnapshot(cwd, snapshot) {
|
|
|
604
747
|
sweepStaleSnapshotStageFiles(cacheDir);
|
|
605
748
|
ensureSnapshotPersistExitHook();
|
|
606
749
|
if (!snapshotWorkerEnabled()) {
|
|
607
|
-
|
|
750
|
+
dispatchMainThreadWriteThroughSeam(pending, undefined);
|
|
608
751
|
return;
|
|
609
752
|
}
|
|
610
753
|
const worker = getSnapshotPersistWorker();
|
|
611
754
|
if (!worker) {
|
|
612
|
-
|
|
755
|
+
dispatchMainThreadWriteThroughSeam(pending, "persist worker unavailable");
|
|
613
756
|
return;
|
|
614
757
|
}
|
|
615
758
|
const id = ++_snapshotWorkerRequestId;
|
|
@@ -640,7 +783,7 @@ export function flushProjectSnapshotPersistsForTests() {
|
|
|
640
783
|
for (const pending of requests) {
|
|
641
784
|
if (_snapshotGenerations.get(pending.key) !== pending.generation)
|
|
642
785
|
continue;
|
|
643
|
-
|
|
786
|
+
dispatchMainThreadWriteThroughSeam(pending, undefined);
|
|
644
787
|
}
|
|
645
788
|
}
|
|
646
789
|
/** Test-only: exercise the degraded worker-death path. */
|
|
@@ -652,11 +795,21 @@ export async function terminateProjectSnapshotPersistWorkerForTests() {
|
|
|
652
795
|
/** Test-only: restore worker creation + clear generation state after a deliberate death. */
|
|
653
796
|
export function resetProjectSnapshotPersistWorkerForTests() {
|
|
654
797
|
_snapshotWorkerDisabled = false;
|
|
798
|
+
_snapshotGenerationGateEnabledForTests = true;
|
|
799
|
+
_snapshotPromotionSeamForTests = undefined;
|
|
655
800
|
_snapshotPersistWorker = undefined;
|
|
656
801
|
_snapshotWorkerRequests.clear();
|
|
657
802
|
_snapshotGenerations.clear();
|
|
658
803
|
_lastSnapshotPersistErrorForTests = undefined;
|
|
659
804
|
}
|
|
805
|
+
/** Test-only mutation switch for proving the supersession invariant. */
|
|
806
|
+
export function setProjectSnapshotGenerationGateForTests(enabled) {
|
|
807
|
+
_snapshotGenerationGateEnabledForTests = enabled;
|
|
808
|
+
}
|
|
809
|
+
/** Test-only seam immediately before generation-gated promotion. */
|
|
810
|
+
export function setProjectSnapshotPromotionSeamForTests(seam) {
|
|
811
|
+
_snapshotPromotionSeamForTests = seam;
|
|
812
|
+
}
|
|
660
813
|
export function getProjectSnapshotPersistErrorForTests() {
|
|
661
814
|
return _lastSnapshotPersistErrorForTests;
|
|
662
815
|
}
|
|
@@ -730,7 +883,9 @@ export function saveRuntimeProjectSnapshot(args) {
|
|
|
730
883
|
// isProjectSnapshotFresh on load, seq mismatch) would get silently
|
|
731
884
|
// re-stamped with the CURRENT seq by this save, "laundering" a stale
|
|
732
885
|
// index into looking fresh before the word-index task even runs.
|
|
733
|
-
if (!snapshot.wordIndex &&
|
|
886
|
+
if (!snapshot.wordIndex &&
|
|
887
|
+
existing.wordIndex &&
|
|
888
|
+
existing.seq === snapshot.seq) {
|
|
734
889
|
snapshot.wordIndex = existing.wordIndex;
|
|
735
890
|
}
|
|
736
891
|
}
|