@herbertgao/pi-extensions 2026.8.6 → 2026.8.8
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 +8 -6
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +999 -0
- package/node_modules/pi-lens/README.md +117 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
- package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +61407 -48577
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +27 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +37 -0
- package/node_modules/pi-web-access/README.md +32 -13
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +40 -5
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +60 -61
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +14 -14
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Falls back safely when LSP is unavailable.
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
|
+
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
13
14
|
import { createFileTime } from "./file-time.js";
|
|
14
15
|
import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
|
|
15
|
-
import { normalizeFilePath } from "./path-utils.js";
|
|
16
|
+
import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
|
|
16
17
|
import { logReadGuardEvent } from "./read-guard-logger.js";
|
|
17
18
|
export const READ_GUARD_STATE_VERSION = 1;
|
|
18
19
|
// --- Constants ---
|
|
@@ -35,6 +36,58 @@ const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ
|
|
|
35
36
|
*/
|
|
36
37
|
const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
|
|
37
38
|
const READ_GUARD_MAX_FILES = 256;
|
|
39
|
+
/**
|
|
40
|
+
* #1904 item 3: `enforceFileCap` bounds how many FILES the store holds, not how
|
|
41
|
+
* many records each file holds. One hot file reached 268 hash-bearing records in
|
|
42
|
+
* 75 minutes, and every record carries a `lineHashes` map, so a long session on
|
|
43
|
+
* a few files grows without bound. Cap records per file and drop oldest-first.
|
|
44
|
+
*
|
|
45
|
+
* Eviction is safe in the blocking direction: it narrows the staleness-rescue
|
|
46
|
+
* path — a stale edit rescued by an older snapshot that still matches — and can
|
|
47
|
+
* turn an allow into a "re-read and retry" block. It never turns a block into
|
|
48
|
+
* an allow. See `enforceRecordCapForFile` for which records go first, and why
|
|
49
|
+
* age alone is the wrong order.
|
|
50
|
+
*/
|
|
51
|
+
const READ_GUARD_MAX_RECORDS_PER_FILE = 128;
|
|
52
|
+
const NO_TRIM = {
|
|
53
|
+
evictedCount: 0,
|
|
54
|
+
evictedCreditCount: 0,
|
|
55
|
+
evictedGenuineCount: 0,
|
|
56
|
+
};
|
|
57
|
+
function enforceRecordCapForFile(records) {
|
|
58
|
+
if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
|
|
59
|
+
return NO_TRIM;
|
|
60
|
+
const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
|
|
61
|
+
const evicted = new Set();
|
|
62
|
+
let evictedCreditCount = 0;
|
|
63
|
+
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
64
|
+
if (records[i].searchCredit !== undefined) {
|
|
65
|
+
evicted.add(i);
|
|
66
|
+
evictedCreditCount++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
70
|
+
evicted.add(i);
|
|
71
|
+
}
|
|
72
|
+
const kept = records.filter((_, i) => !evicted.has(i));
|
|
73
|
+
records.length = 0;
|
|
74
|
+
for (const record of kept)
|
|
75
|
+
records.push(record);
|
|
76
|
+
return {
|
|
77
|
+
evictedCount: excess,
|
|
78
|
+
evictedCreditCount,
|
|
79
|
+
evictedGenuineCount: excess - evictedCreditCount,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* #1904 class sweep: `this.edits` is the same shape as `this.reads` — a
|
|
84
|
+
* per-file array that only ever grows. Its cap sits far above every consumer's
|
|
85
|
+
* reach, so trimming is inert: `canTreatStalenessAsOwnPriorEdit` reads only the
|
|
86
|
+
* last record, and `findRelocation`'s window saturates at
|
|
87
|
+
* RELOCATION_WINDOW_MAX / RELOCATION_WINDOW_PER_EDIT (20) applied edits. Only
|
|
88
|
+
* `getStats`, a debug surface, sees the older records at all.
|
|
89
|
+
*/
|
|
90
|
+
const READ_GUARD_MAX_EDITS_PER_FILE = 256;
|
|
38
91
|
// Unconsumed reads remain valid until edit or session end, but this high
|
|
39
92
|
// sanity cap prevents a read-only session from growing without bound.
|
|
40
93
|
const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
|
|
@@ -213,6 +266,25 @@ export class ReadGuard {
|
|
|
213
266
|
// pi Write tool authored, independent of filesystem mtime granularity
|
|
214
267
|
// or clock skew (NFS, FAT32, etc.).
|
|
215
268
|
writtenThisSession = new Set();
|
|
269
|
+
// Existence-independent index for hasKnownPath/forgetPath (#1668 review
|
|
270
|
+
// F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
|
|
271
|
+
// currently exists on disk: an existing file resolves to realpathSync
|
|
272
|
+
// canonical casing, a missing one to a lowercased tail. recordRead/
|
|
273
|
+
// recordWritten always key while the file is still on disk (real
|
|
274
|
+
// casing); hasKnownPath/forgetPath are queried AFTER an external delete
|
|
275
|
+
// already landed, when the path no longer exists — recomputing
|
|
276
|
+
// `this.key()` at that point returns a DIFFERENT string for any
|
|
277
|
+
// mixed-case basename (`MyModule.ts` → `mymodule.ts`), so a lookup
|
|
278
|
+
// against `reads`/`writtenThisSession` silently misses. This index maps
|
|
279
|
+
// a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
|
|
280
|
+
// lowercase, no filesystem access, so it never depends on current disk
|
|
281
|
+
// state) to the REAL key `reads`/`writtenThisSession` used at record
|
|
282
|
+
// time, so a post-delete lookup finds the same entry regardless of what
|
|
283
|
+
// happened to the file since. Pruned inside `evictFile` so it never
|
|
284
|
+
// outlives the record it points at.
|
|
285
|
+
knownPathIndex = new Map();
|
|
286
|
+
/** Running per-file record-cap trim totals for this session (#1913 F1). */
|
|
287
|
+
trimAccumulators = new Map();
|
|
216
288
|
sessionId;
|
|
217
289
|
sessionStartMs;
|
|
218
290
|
constructor(sessionId, config = {}) {
|
|
@@ -236,7 +308,9 @@ export class ReadGuard {
|
|
|
236
308
|
}
|
|
237
309
|
idleEvictMs() {
|
|
238
310
|
const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
|
|
239
|
-
return Number.isSafeInteger(value) && value > 0
|
|
311
|
+
return Number.isSafeInteger(value) && value > 0
|
|
312
|
+
? value
|
|
313
|
+
: READ_GUARD_IDLE_EVICT_MS_DEFAULT;
|
|
240
314
|
}
|
|
241
315
|
clearFileTimer(filePath) {
|
|
242
316
|
const timer = this.fileIdleTimers.get(filePath);
|
|
@@ -251,6 +325,12 @@ export class ReadGuard {
|
|
|
251
325
|
this.fileLastUsed.delete(filePath);
|
|
252
326
|
this.consumedReadFiles.delete(filePath);
|
|
253
327
|
this.writtenThisSession.delete(filePath);
|
|
328
|
+
// #1668 review F1: prune the reverse-pointing knownPathIndex entries
|
|
329
|
+
// too, so it never outlives the record it points at.
|
|
330
|
+
for (const [syntacticKey, stored] of this.knownPathIndex) {
|
|
331
|
+
if (stored === filePath)
|
|
332
|
+
this.knownPathIndex.delete(syntacticKey);
|
|
333
|
+
}
|
|
254
334
|
}
|
|
255
335
|
touchFile(filePath) {
|
|
256
336
|
const now = Date.now();
|
|
@@ -285,8 +365,7 @@ export class ReadGuard {
|
|
|
285
365
|
while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
|
|
286
366
|
const victim = [...this.reads.keys()]
|
|
287
367
|
.filter((filePath) => !this.consumedReadFiles.has(filePath))
|
|
288
|
-
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) -
|
|
289
|
-
(this.fileLastUsed.get(b) ?? 0))[0];
|
|
368
|
+
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
|
|
290
369
|
if (!victim)
|
|
291
370
|
break;
|
|
292
371
|
// This is a normal read miss: a later edit must require a fresh read,
|
|
@@ -301,6 +380,11 @@ export class ReadGuard {
|
|
|
301
380
|
*/
|
|
302
381
|
recordRead(record) {
|
|
303
382
|
const filePath = this.key(record.filePath);
|
|
383
|
+
// #1668 review F1: index by the existence-independent syntactic key
|
|
384
|
+
// while the file is (presumably) still on disk, so a later
|
|
385
|
+
// hasKnownPath/forgetPath lookup after an external delete can still
|
|
386
|
+
// find this entry's real key.
|
|
387
|
+
this.knownPathIndex.set(normalizeEphemeralMapKey(record.filePath), filePath);
|
|
304
388
|
const storedRecord = {
|
|
305
389
|
...record,
|
|
306
390
|
filePath,
|
|
@@ -311,6 +395,11 @@ export class ReadGuard {
|
|
|
311
395
|
this.consumedReadFiles.delete(storedRecord.filePath);
|
|
312
396
|
arr.push(storedRecord);
|
|
313
397
|
this.reads.set(storedRecord.filePath, arr);
|
|
398
|
+
// Capture BEFORE the trim: `readCountForFile` saturates at the cap once
|
|
399
|
+
// eviction starts, so it stops showing growth. `rawReadCountForFile` keeps
|
|
400
|
+
// the real arrival count observable alongside `evictedRecordCount`.
|
|
401
|
+
const rawReadCountForFile = arr.length;
|
|
402
|
+
const { evictedCount: evictedRecordCount, evictedCreditCount, evictedGenuineCount } = enforceRecordCapForFile(arr);
|
|
314
403
|
this.touchFile(storedRecord.filePath);
|
|
315
404
|
this.enforceFileCap();
|
|
316
405
|
logReadGuardEvent({
|
|
@@ -334,8 +423,74 @@ export class ReadGuard {
|
|
|
334
423
|
...(storedRecord.source !== undefined && {
|
|
335
424
|
source: storedRecord.source,
|
|
336
425
|
}),
|
|
426
|
+
...(storedRecord.searchCredit !== undefined && {
|
|
427
|
+
searchCreditReason: storedRecord.searchCredit.reason,
|
|
428
|
+
searchCreditMarginBefore: storedRecord.searchCredit.marginBefore,
|
|
429
|
+
searchCreditMarginAfter: storedRecord.searchCredit.marginAfter,
|
|
430
|
+
}),
|
|
431
|
+
...(evictedRecordCount > 0 && {
|
|
432
|
+
evictedRecordCount,
|
|
433
|
+
rawReadCountForFile,
|
|
434
|
+
}),
|
|
337
435
|
},
|
|
338
436
|
});
|
|
437
|
+
// #1913: the eviction counters above ride `read_recorded`, which is
|
|
438
|
+
// gated behind PI_LENS_READ_GUARD_VERBOSE — off by default. That left a
|
|
439
|
+
// live eviction regression with no trace at default verbosity.
|
|
440
|
+
//
|
|
441
|
+
// review F1: a naive "emit every trim" fix floods read-guard.log once a
|
|
442
|
+
// hot file sits past the cap — every later push trims exactly 1 record
|
|
443
|
+
// (the array is always AT the cap before the push, so `excess` is
|
|
444
|
+
// always exactly 1), so 300 recordRead calls on one file is ~172
|
|
445
|
+
// identical always-on lines, and can rotate `edit_blocked` records out
|
|
446
|
+
// of the 1MB cap. `rawReadCountForFile` also freezes at cap+1 forever,
|
|
447
|
+
// so a raw per-trim record can't even discriminate thrash severity.
|
|
448
|
+
//
|
|
449
|
+
// Fix: emit the read-guard.log line ONCE per file per session, on the
|
|
450
|
+
// FIRST trim only. `trimAccumulators` still updates on every trim,
|
|
451
|
+
// queryable via `getTrimStats` — the running totals a health surface or
|
|
452
|
+
// a future emission point would need are never lost, even though
|
|
453
|
+
// read-guard.log stops re-announcing them. "Have we already logged
|
|
454
|
+
// this file's first trim" routes through the degradation ledger's own
|
|
455
|
+
// rising-edge tally (`incrementDegradationCount` — the pattern
|
|
456
|
+
// CLAUDE.md names for repeated degradations) instead of a hand-rolled
|
|
457
|
+
// per-file Set; subsequent trims still call it, so the ledger's own
|
|
458
|
+
// entry for this (kind, subject) keeps its reason text and count
|
|
459
|
+
// current for the health/degradation summary even after read-guard.log
|
|
460
|
+
// goes quiet.
|
|
461
|
+
if (evictedRecordCount > 0) {
|
|
462
|
+
const key = storedRecord.filePath;
|
|
463
|
+
const acc = this.trimAccumulators.get(key) ?? {
|
|
464
|
+
totalEvicted: 0,
|
|
465
|
+
evictedCreditCount: 0,
|
|
466
|
+
evictedGenuineCount: 0,
|
|
467
|
+
trimEventCount: 0,
|
|
468
|
+
};
|
|
469
|
+
acc.totalEvicted += evictedRecordCount;
|
|
470
|
+
acc.evictedCreditCount += evictedCreditCount;
|
|
471
|
+
acc.evictedGenuineCount += evictedGenuineCount;
|
|
472
|
+
acc.trimEventCount += 1;
|
|
473
|
+
this.trimAccumulators.set(key, acc);
|
|
474
|
+
const isRisingEdge = incrementDegradationCount({
|
|
475
|
+
kind: "read-guard-record-cap-trim",
|
|
476
|
+
subject: key,
|
|
477
|
+
reason: `trim #${acc.trimEventCount}: evicted ${evictedRecordCount} (credit ${evictedCreditCount}, genuine ${evictedGenuineCount})`,
|
|
478
|
+
});
|
|
479
|
+
if (isRisingEdge) {
|
|
480
|
+
logReadGuardEvent({
|
|
481
|
+
event: "read_cap_trimmed",
|
|
482
|
+
sessionId: this.sessionId,
|
|
483
|
+
filePath: storedRecord.filePath,
|
|
484
|
+
metadata: {
|
|
485
|
+
trimEventCount: acc.trimEventCount,
|
|
486
|
+
evictedRecordCount: acc.totalEvicted,
|
|
487
|
+
evictedCreditCount: acc.evictedCreditCount,
|
|
488
|
+
evictedGenuineCount: acc.evictedGenuineCount,
|
|
489
|
+
rawReadCountForFile,
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
}
|
|
339
494
|
// Also update FileTime stamp for this file
|
|
340
495
|
this.fileTime.read(storedRecord.filePath);
|
|
341
496
|
}
|
|
@@ -442,9 +597,17 @@ export class ReadGuard {
|
|
|
442
597
|
// 2. FileTime check (actual staleness)
|
|
443
598
|
let ignoredOwnEditStaleness = false;
|
|
444
599
|
let ignoredHashStaleness = false;
|
|
600
|
+
let ignoredOldTextResolvedStaleness = false;
|
|
445
601
|
if (this.fileTime.hasChanged(filePath)) {
|
|
446
602
|
const lastRead = fileReads[fileReads.length - 1];
|
|
447
|
-
if (
|
|
603
|
+
if (options?.oldTextResolved === true) {
|
|
604
|
+
// The host-facing preflight sets this only after oldText resolves to
|
|
605
|
+
// exactly one span in the live bytes. That content evidence is newer
|
|
606
|
+
// and stronger than FileTime's coarse external-write signal, matching
|
|
607
|
+
// the skipSnapshotCheck exception at the later range-stale gate.
|
|
608
|
+
ignoredOldTextResolvedStaleness = true;
|
|
609
|
+
}
|
|
610
|
+
else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
|
|
448
611
|
ignoredOwnEditStaleness = true;
|
|
449
612
|
}
|
|
450
613
|
else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
|
|
@@ -474,7 +637,7 @@ export class ReadGuard {
|
|
|
474
637
|
const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
|
|
475
638
|
let viaSymbol = false;
|
|
476
639
|
for (const range of rangesToCheck) {
|
|
477
|
-
const snapshotValidation = this.validateRangeSnapshot(filePath, range);
|
|
640
|
+
const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
|
|
478
641
|
const coverage = this.checkCoverage(filePath, range);
|
|
479
642
|
if (!coverage.covered) {
|
|
480
643
|
const lastRead = fileReads[fileReads.length - 1];
|
|
@@ -568,10 +731,15 @@ export class ReadGuard {
|
|
|
568
731
|
}
|
|
569
732
|
const verdict = this.allow();
|
|
570
733
|
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
571
|
-
reasonKind:
|
|
734
|
+
reasonKind: ignoredOldTextResolvedStaleness
|
|
735
|
+
? "file_modified_oldtext_unique"
|
|
736
|
+
: viaSymbol
|
|
737
|
+
? "symbol_coverage"
|
|
738
|
+
: "range_coverage",
|
|
572
739
|
viaSymbol,
|
|
573
740
|
ignoredOwnEditStaleness,
|
|
574
741
|
ignoredHashStaleness,
|
|
742
|
+
oldTextResolved: ignoredOldTextResolvedStaleness,
|
|
575
743
|
});
|
|
576
744
|
return verdict;
|
|
577
745
|
}
|
|
@@ -587,6 +755,44 @@ export class ReadGuard {
|
|
|
587
755
|
return true; // Assume new if we can't stat
|
|
588
756
|
}
|
|
589
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Whether pi-lens has any record of this path from a read or write this
|
|
760
|
+
* session (#1668). Gates external-delete detection: a path pi-lens never
|
|
761
|
+
* read or wrote is one no LSP server's cache was ever told about through
|
|
762
|
+
* us, so an `rm` naming it carries no signal worth checking disk for —
|
|
763
|
+
* this is a lookup against state already being tracked, never a fresh
|
|
764
|
+
* filesystem stat over an unbounded path set.
|
|
765
|
+
*
|
|
766
|
+
* MUST go through `knownPathIndex`, not `this.key(filePath)` directly
|
|
767
|
+
* (#1668 review F1): this is called AFTER a bash delete has already
|
|
768
|
+
* landed, when `filePath` no longer exists — `this.key()` at that point
|
|
769
|
+
* returns a lowercased-tail key, not the real-casing key `recordRead`/
|
|
770
|
+
* `recordWritten` stored while the file was still on disk. For a
|
|
771
|
+
* mixed-case basename (`MyModule.ts`) the two keys differ and a direct
|
|
772
|
+
* `this.key()` lookup always misses.
|
|
773
|
+
*/
|
|
774
|
+
hasKnownPath(filePath) {
|
|
775
|
+
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
776
|
+
return (stored !== undefined &&
|
|
777
|
+
(this.reads.has(stored) || this.writtenThisSession.has(stored)));
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* Drop all record of a path pi-lens confirmed no longer exists on disk
|
|
781
|
+
* (#1668, external delete). Without this a later write reusing the same
|
|
782
|
+
* path would inherit a stale writtenThisSession/reads entry from before
|
|
783
|
+
* the delete, and a repeat `rm` of the same already-gone path would keep
|
|
784
|
+
* matching {@link hasKnownPath} and re-emitting a type-3 notification.
|
|
785
|
+
*
|
|
786
|
+
* MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
|
|
787
|
+
* (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
|
|
788
|
+
* is already gone from disk by the time this runs, so recomputing
|
|
789
|
+
* `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
|
|
790
|
+
* basename and leaves the real entry behind un-evicted.
|
|
791
|
+
*/
|
|
792
|
+
forgetPath(filePath) {
|
|
793
|
+
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
794
|
+
this.evictFile(stored ?? this.key(filePath));
|
|
795
|
+
}
|
|
590
796
|
/**
|
|
591
797
|
* Mark a file as pending creation (Write tool to a non-existing file).
|
|
592
798
|
* Must be called from the tool_call handler before the write lands so
|
|
@@ -601,8 +807,12 @@ export class ReadGuard {
|
|
|
601
807
|
* Call this from the tool_result handler so the next checkEdit on the same
|
|
602
808
|
* file doesn't see "file_modified" caused by our own previous edit.
|
|
603
809
|
*/
|
|
604
|
-
recordWritten(
|
|
605
|
-
filePath = this.key(
|
|
810
|
+
recordWritten(rawFilePath) {
|
|
811
|
+
const filePath = this.key(rawFilePath);
|
|
812
|
+
// #1668 review F1: index by the existence-independent syntactic key
|
|
813
|
+
// (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
|
|
814
|
+
// after an external delete can still find this entry's real key.
|
|
815
|
+
this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
|
|
606
816
|
this.fileTime.read(filePath);
|
|
607
817
|
this.writtenThisSession.add(filePath);
|
|
608
818
|
if (this.reads.has(filePath))
|
|
@@ -675,6 +885,18 @@ export class ReadGuard {
|
|
|
675
885
|
this.touchFile(key);
|
|
676
886
|
return this.reads.get(key) ?? [];
|
|
677
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Session-lifetime record-cap trim totals for a file (#1913 review F1).
|
|
890
|
+
* `recordRead` only writes ONE `read_cap_trimmed` read-guard.log line per
|
|
891
|
+
* file per session (on the first trim), so this is the running-totals
|
|
892
|
+
* surface for everything after that — the exact split this class already
|
|
893
|
+
* computes on every trim, not re-derivable from `getReadHistory` alone
|
|
894
|
+
* (which only shows the SURVIVING records, not what was evicted).
|
|
895
|
+
*/
|
|
896
|
+
getTrimStats(filePath) {
|
|
897
|
+
const stats = this.trimAccumulators.get(this.key(filePath));
|
|
898
|
+
return stats ? { ...stats } : undefined;
|
|
899
|
+
}
|
|
678
900
|
/**
|
|
679
901
|
* Snapshot the read-set for persistence across a session resume (#1041).
|
|
680
902
|
* Mirrors widget-state's `exportWidgetState`: the Map is emitted as
|
|
@@ -838,7 +1060,13 @@ export class ReadGuard {
|
|
|
838
1060
|
return (read.enclosingSymbol.startLine <= editStart &&
|
|
839
1061
|
read.enclosingSymbol.endLine >= editEnd);
|
|
840
1062
|
}
|
|
841
|
-
validateRangeSnapshot(filePath, range
|
|
1063
|
+
validateRangeSnapshot(filePath, range,
|
|
1064
|
+
/**
|
|
1065
|
+
* What the CALLER will do with the verdict. `checkEdit` honours
|
|
1066
|
+
* `skipSnapshotCheck` for content-validated (oldText) edits, so a
|
|
1067
|
+
* `shouldBlock` verdict may never reach the gate (#1904 item 1).
|
|
1068
|
+
*/
|
|
1069
|
+
snapshotCheckSkipped) {
|
|
842
1070
|
const reads = this.reads.get(filePath) ?? [];
|
|
843
1071
|
const candidates = reads.filter((read) => this.readCoversRange(read, range));
|
|
844
1072
|
let status = "unavailable";
|
|
@@ -886,6 +1114,17 @@ export class ReadGuard {
|
|
|
886
1114
|
lastUnavailableTimestamp <= lastMismatchTimestamp &&
|
|
887
1115
|
checkedCandidateCount > 0 &&
|
|
888
1116
|
hashUnavailableCandidateCount === 0;
|
|
1117
|
+
// `enforced` below states INTENT — whether this validation reached a
|
|
1118
|
+
// decidable verdict. It says nothing about what the caller did with it.
|
|
1119
|
+
// `outcome` states the caller's actual behaviour, which is what an
|
|
1120
|
+
// enforcement rate must be computed from (#1904 item 1).
|
|
1121
|
+
const outcome = shouldBlock
|
|
1122
|
+
? snapshotCheckSkipped
|
|
1123
|
+
? "bypassed-content-match"
|
|
1124
|
+
: "enforced-block"
|
|
1125
|
+
: status === "match"
|
|
1126
|
+
? "enforced-pass"
|
|
1127
|
+
: "not-decidable";
|
|
889
1128
|
logReadGuardEvent({
|
|
890
1129
|
event: "range_snapshot_validation",
|
|
891
1130
|
sessionId: this.sessionId,
|
|
@@ -903,6 +1142,7 @@ export class ReadGuard {
|
|
|
903
1142
|
missingLines: missingLines.slice(0, 20),
|
|
904
1143
|
mismatchedLines: mismatchedLines.slice(0, 20),
|
|
905
1144
|
enforced: shouldBlock || status === "match",
|
|
1145
|
+
outcome,
|
|
906
1146
|
},
|
|
907
1147
|
});
|
|
908
1148
|
return {
|
|
@@ -1117,6 +1357,9 @@ export class ReadGuard {
|
|
|
1117
1357
|
reason: verdict.reason,
|
|
1118
1358
|
timestamp: Date.now(),
|
|
1119
1359
|
});
|
|
1360
|
+
if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
|
|
1361
|
+
arr.splice(0, arr.length - READ_GUARD_MAX_EDITS_PER_FILE);
|
|
1362
|
+
}
|
|
1120
1363
|
this.edits.set(filePath, arr);
|
|
1121
1364
|
}
|
|
1122
1365
|
recordVerdict(filePath, tool, touchedLines, verdict, metadata = {}) {
|
|
@@ -45,7 +45,18 @@
|
|
|
45
45
|
*/
|
|
46
46
|
import * as path from "node:path";
|
|
47
47
|
import pidusage from "pidusage";
|
|
48
|
-
import {
|
|
48
|
+
import { spawnCollectStdoutResult } from "./child-unref.js";
|
|
49
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
50
|
+
import { terminateScannerChild } from "./instance-reaper.js";
|
|
51
|
+
export const RESOURCE_SAMPLE_QUERY_TIMEOUT_MS = 2_000;
|
|
52
|
+
function recordQueryFailure(subject, status, exitCode) {
|
|
53
|
+
const exitReason = status === "exit-error" ? ` (exit code ${exitCode ?? "unknown"})` : "";
|
|
54
|
+
recordDegradationOnce({
|
|
55
|
+
kind: "resource-sampler-query-failed",
|
|
56
|
+
subject,
|
|
57
|
+
reason: `process-table query ${status}${exitReason}`,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
49
60
|
// Read the platform live (not a module-load const) so both the Windows and the
|
|
50
61
|
// POSIX sampling paths are exercisable in unit tests regardless of the host OS.
|
|
51
62
|
function runningOnWindows() {
|
|
@@ -113,12 +124,18 @@ async function findDescendantPidsWindows(rootPid) {
|
|
|
113
124
|
// reaper's identical spawn→collect plumbing (#1153/#1160). Sampling still
|
|
114
125
|
// works normally in an interactive/long-lived session: unref only means
|
|
115
126
|
// "don't hold the loop open FOR this alone," the collected stdout is still
|
|
116
|
-
// delivered whenever `close` fires.
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
127
|
+
// delivered whenever `close` fires. The result status keeps a failed query
|
|
128
|
+
// distinct from a successful empty process table.
|
|
129
|
+
const result = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, {
|
|
130
|
+
timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
|
|
131
|
+
onTimeout: (child) => terminateScannerChild(child, {}),
|
|
132
|
+
});
|
|
133
|
+
if (result.status !== "ok") {
|
|
134
|
+
recordQueryFailure("windows-descendant-process-table", result.status, result.exitCode);
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
120
137
|
const pairs = [];
|
|
121
|
-
for (const line of
|
|
138
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
122
139
|
const [pidStr, ppidStr] = line.split(",");
|
|
123
140
|
const pid = Number(pidStr);
|
|
124
141
|
const ppid = Number(ppidStr);
|
|
@@ -142,7 +159,7 @@ export function __resetWindowsCpuHistoryForTests() {
|
|
|
142
159
|
* Windows-only CPU%/RSS sampling via a FULLY GUARDED `Get-CimInstance
|
|
143
160
|
* Win32_Process` query (mirrors `findDescendantPidsWindows`): a synchronous
|
|
144
161
|
* throw from `spawn` (the `spawn UNKNOWN` crash vector, #620), a `child`
|
|
145
|
-
* `error` event, or a non-zero/garbage exit all resolve to
|
|
162
|
+
* `error` event, or a non-zero/garbage exit all resolve to an errored/absent
|
|
146
163
|
* map — this function can NEVER throw or reject. Deliberately does NOT call
|
|
147
164
|
* `pidusage`, whose unguarded internal `gwmi` spawn is the crash we're fixing.
|
|
148
165
|
*
|
|
@@ -153,9 +170,9 @@ export function __resetWindowsCpuHistoryForTests() {
|
|
|
153
170
|
* reported as 0 for that tick and a real rate lands on the next one.
|
|
154
171
|
*/
|
|
155
172
|
async function sampleProcessesWindows(valid) {
|
|
156
|
-
const
|
|
173
|
+
const samples = new Map();
|
|
157
174
|
if (valid.length === 0)
|
|
158
|
-
return
|
|
175
|
+
return samples;
|
|
159
176
|
// pids are pre-validated finite positive integers, so this WQL filter is
|
|
160
177
|
// injection-safe. One line per pid: "pid,workingSet,kernel100ns,user100ns".
|
|
161
178
|
const filter = valid.map((p) => `ProcessId=${p}`).join(" or ");
|
|
@@ -169,15 +186,21 @@ async function sampleProcessesWindows(valid) {
|
|
|
169
186
|
// reaper's identical spawn→collect plumbing (#1153/#1160). It also absorbs
|
|
170
187
|
// both failure modes this function used to guard inline — a synchronous
|
|
171
188
|
// `spawn` throw (the `spawn UNKNOWN` crash vector, #620) and an async
|
|
172
|
-
// `error` event
|
|
173
|
-
//
|
|
174
|
-
// produced. Sampling still works normally in an interactive/long-lived
|
|
189
|
+
// `error` event, or a non-zero exit — the result status keeps failure distinct
|
|
190
|
+
// map. Sampling still works normally in an interactive/long-lived
|
|
175
191
|
// session: unref only means "don't hold the loop open FOR this alone."
|
|
176
|
-
const
|
|
192
|
+
const query = await spawnCollectStdoutResult(powershell, ["-NoProfile", "-NonInteractive", "-Command", psScript], { shell: false, windowsHide: true, stdio: ["ignore", "pipe", "ignore"] }, {
|
|
193
|
+
timeoutMs: RESOURCE_SAMPLE_QUERY_TIMEOUT_MS,
|
|
194
|
+
onTimeout: (child) => terminateScannerChild(child, {}),
|
|
195
|
+
});
|
|
196
|
+
if (query.status !== "ok") {
|
|
197
|
+
recordQueryFailure("windows-process-table", query.status, query.exitCode);
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
177
200
|
try {
|
|
178
201
|
const now = Date.now();
|
|
179
202
|
const seen = new Set();
|
|
180
|
-
for (const line of
|
|
203
|
+
for (const line of query.stdout.split(/\r?\n/)) {
|
|
181
204
|
const parts = line.split(",");
|
|
182
205
|
if (parts.length < 4)
|
|
183
206
|
continue;
|
|
@@ -202,7 +225,7 @@ async function sampleProcessesWindows(valid) {
|
|
|
202
225
|
}
|
|
203
226
|
windowsCpuHistory.set(pid, { cpuMs, ts: now });
|
|
204
227
|
seen.add(pid);
|
|
205
|
-
|
|
228
|
+
samples.set(pid, { rssBytes: workingSet, cpuPercent });
|
|
206
229
|
}
|
|
207
230
|
// Prune stale history so pids that have gone away don't accumulate.
|
|
208
231
|
for (const [pid, entry] of windowsCpuHistory) {
|
|
@@ -214,7 +237,7 @@ async function sampleProcessesWindows(valid) {
|
|
|
214
237
|
catch {
|
|
215
238
|
// Parsing must never throw into the caller; best-effort.
|
|
216
239
|
}
|
|
217
|
-
return
|
|
240
|
+
return samples;
|
|
218
241
|
}
|
|
219
242
|
/**
|
|
220
243
|
* Sample CPU%/RSS for a set of pids. Best-effort: a pid that can't be resolved
|
|
@@ -248,8 +271,10 @@ export async function sampleProcesses(pids) {
|
|
|
248
271
|
}
|
|
249
272
|
}
|
|
250
273
|
catch {
|
|
274
|
+
recordQueryFailure("posix-pidusage-process-table", "spawn-error");
|
|
251
275
|
// Best-effort: sampling failure loses this tick's data for every pid in
|
|
252
276
|
// the batch, but must never throw into the heartbeat/spawn path.
|
|
277
|
+
return null;
|
|
253
278
|
}
|
|
254
279
|
return result;
|
|
255
280
|
}
|
|
@@ -321,10 +346,17 @@ export function startSpawnUsageSampler(pid, intervalMs = 750) {
|
|
|
321
346
|
if (stopped)
|
|
322
347
|
return;
|
|
323
348
|
try {
|
|
349
|
+
const descendants = runningOnWindows()
|
|
350
|
+
? await findDescendantPidsWindows(targetPid)
|
|
351
|
+
: [];
|
|
352
|
+
if (descendants === null)
|
|
353
|
+
return;
|
|
324
354
|
const pids = runningOnWindows()
|
|
325
|
-
? [targetPid, ...
|
|
355
|
+
? [targetPid, ...descendants]
|
|
326
356
|
: [targetPid];
|
|
327
357
|
const usageByPid = await sampleProcesses(pids);
|
|
358
|
+
if (usageByPid === null)
|
|
359
|
+
return;
|
|
328
360
|
if (stopped || usageByPid.size === 0)
|
|
329
361
|
return;
|
|
330
362
|
let rssBytes = 0;
|
|
@@ -33,14 +33,30 @@ function addEdge(index, fromFile, toFile) {
|
|
|
33
33
|
(index.importedBy[to] ??= []).push(from);
|
|
34
34
|
}
|
|
35
35
|
function normalizeIndex(index) {
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// #1814 F2: `imports[file]` asserts "we know file's complete import
|
|
37
|
+
// list" — true only when `index.imports` already has a raw entry for
|
|
38
|
+
// `file` (from a real per-file scan). `importedBy[file]` asserts "we
|
|
39
|
+
// know SOME importer of file", which holds regardless of whether `file`
|
|
40
|
+
// itself was ever scanned. Materializing `imports[file] = []` for every
|
|
41
|
+
// key in their UNION (the pre-fix behavior) fabricated the first claim
|
|
42
|
+
// for a file that is only an `importedBy` target — imported by
|
|
43
|
+
// something, never itself scanned — indistinguishable downstream from a
|
|
44
|
+
// file the scan confirmed has zero imports.
|
|
45
|
+
// `clients/lsp/workspace-diagnostics-cache.ts`'s `getImports` reads this
|
|
46
|
+
// map directly and relies on `undefined` meaning "not covered". Only
|
|
47
|
+
// `importedBy` keeps the union treatment: "who imports me" has no
|
|
48
|
+
// equivalent "was I scanned" precondition to fabricate.
|
|
49
|
+
const importFileKeys = [...new Set(Object.keys(index.imports))].sort((a, b) => a.localeCompare(b));
|
|
50
|
+
const allFileKeys = new Set([
|
|
51
|
+
...importFileKeys,
|
|
38
52
|
...Object.keys(index.importedBy),
|
|
39
53
|
]);
|
|
40
54
|
const imports = {};
|
|
41
|
-
const
|
|
42
|
-
for (const file of [...fileKeys].sort((a, b) => a.localeCompare(b))) {
|
|
55
|
+
for (const file of importFileKeys) {
|
|
43
56
|
imports[file] = sortedUnique(index.imports[file] ?? []);
|
|
57
|
+
}
|
|
58
|
+
const importedBy = {};
|
|
59
|
+
for (const file of [...allFileKeys].sort((a, b) => a.localeCompare(b))) {
|
|
44
60
|
importedBy[file] = sortedUnique(index.importedBy[file] ?? []);
|
|
45
61
|
}
|
|
46
62
|
return { ...index, imports, importedBy };
|
|
@@ -120,12 +136,24 @@ export function buildReverseDependencyIndexFromSnapshot(snapshot) {
|
|
|
120
136
|
};
|
|
121
137
|
for (const [filePath, file] of fileEntries) {
|
|
122
138
|
const normalized = normalizeMapKey(file.path || filePath);
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
139
|
+
// #1814 F2: only materialize `imports[normalized]` when this
|
|
140
|
+
// snapshot entry genuinely recorded an import list. `file.imports`
|
|
141
|
+
// absent means this file was captured in the snapshot (e.g. a
|
|
142
|
+
// lightweight touch) but never had its own imports scanned —
|
|
143
|
+
// defaulting to `[]` here fabricated the "covered, zero imports"
|
|
144
|
+
// claim `getImports` (workspace-diagnostics-cache.ts) distinguishes
|
|
145
|
+
// from "not covered". `hasFileImports` above already anticipates a
|
|
146
|
+
// mixed snapshot with entries in both states; this closes the gap
|
|
147
|
+
// per entry.
|
|
148
|
+
if (Array.isArray(file.imports)) {
|
|
149
|
+
const imports = sortedUnique(file.imports);
|
|
150
|
+
index.imports[normalized] = imports;
|
|
151
|
+
for (const imported of imports) {
|
|
152
|
+
index.importedBy[imported] ??= [];
|
|
153
|
+
index.importedBy[imported].push(normalized);
|
|
154
|
+
}
|
|
128
155
|
}
|
|
156
|
+
index.importedBy[normalized] ??= [];
|
|
129
157
|
}
|
|
130
158
|
for (const [filePath, importers] of Object.entries(reverseDeps)) {
|
|
131
159
|
const normalized = normalizeMapKey(filePath);
|
|
@@ -133,9 +161,20 @@ export function buildReverseDependencyIndexFromSnapshot(snapshot) {
|
|
|
133
161
|
...(index.importedBy[normalized] ?? []),
|
|
134
162
|
...importers,
|
|
135
163
|
]);
|
|
136
|
-
|
|
164
|
+
// #1814 F2 (class sweep): do NOT fabricate `imports[normalized] = []`
|
|
165
|
+
// here — `reverseDeps` records who imports `normalized`, not what
|
|
166
|
+
// `normalized` itself imports. Leave `imports[normalized]` exactly
|
|
167
|
+
// as the fileEntries loop above established it: present and real
|
|
168
|
+
// from a scan, or absent because this file was never scanned.
|
|
137
169
|
for (const importer of index.importedBy[normalized]) {
|
|
138
|
-
|
|
170
|
+
// #1814 F2: only record this edge on `importer`'s own imports
|
|
171
|
+
// list when that list is already genuinely known (a real
|
|
172
|
+
// per-file scan from the fileEntries loop above) — appending to
|
|
173
|
+
// an absent list would silently promote "we know ONE edge from
|
|
174
|
+
// reverseDeps" into "we know importer's COMPLETE import list",
|
|
175
|
+
// the same false-coverage claim closed above.
|
|
176
|
+
if (index.imports[importer] === undefined)
|
|
177
|
+
continue;
|
|
139
178
|
if (!index.imports[importer].includes(normalized)) {
|
|
140
179
|
index.imports[importer].push(normalized);
|
|
141
180
|
}
|