@herbertgao/pi-extensions 2026.8.6 → 2026.8.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +272 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +14 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +1 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +26 -10
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +2 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +3 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +30 -7
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +30 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +26 -5
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/pi-lens/CHANGELOG.md +909 -0
- package/node_modules/pi-lens/README.md +11 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +285 -8
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +119 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +192 -0
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +62 -13
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +23 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +20 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +10 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +18 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +72 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +294 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +82 -6
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +2 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +102 -7
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1516 -159
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2274 -147
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +8 -0
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +5 -0
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +1 -1
- package/node_modules/pi-lens/dist/clients/read-guard.js +184 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +308 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +332 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +575 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +67738 -55739
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +466 -54
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +10 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +23 -0
- package/node_modules/pi-web-access/README.md +27 -9
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +28 -3
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/index.ts +41 -57
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +13 -13
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quiet-window kill switch and wait budget, split out of `quiet-window.ts` so a
|
|
3
|
+
* caller that only needs the NUMBERS does not have to import the scheduler.
|
|
4
|
+
*
|
|
5
|
+
* `cascade-budget.ts` is the caller that forced it (#1462): it reads both knobs
|
|
6
|
+
* to decide how much drain is really left, and depending on `quiet-window.ts`
|
|
7
|
+
* for that would point a pure arithmetic module at a task registry — module
|
|
8
|
+
* state (`_tasks`, `_inProgress`), a heartbeat sampler, latency logging, none
|
|
9
|
+
* of which it wants. Config belongs below the scheduler, not beside it.
|
|
10
|
+
*
|
|
11
|
+
* NOT a load-time win, despite what the first version of this comment claimed:
|
|
12
|
+
* `cascade-budget.ts` imports `runtime-config.ts` for `cascadeMaxFiles`, and
|
|
13
|
+
* that reaches `pidusage` through `lens-config.ts` regardless — probed, and
|
|
14
|
+
* `dispatch/integration.ts` already imported `runtime-config.ts` before #1462.
|
|
15
|
+
* The split is a dependency-direction fix and nothing more.
|
|
16
|
+
*
|
|
17
|
+
* `quiet-window.ts` re-exports all three, so every existing importer keeps
|
|
18
|
+
* working and there is still exactly one memo behind `isQuietWindowEnabled`.
|
|
19
|
+
*/
|
|
20
|
+
import { toPositiveFinite } from "./env-utils.js";
|
|
21
|
+
let _enabledCache;
|
|
22
|
+
/** `PI_LENS_QUIET_WINDOW=0` disables the whole scheduler (no-op, no logging). */
|
|
23
|
+
export function isQuietWindowEnabled() {
|
|
24
|
+
if (_enabledCache !== undefined)
|
|
25
|
+
return _enabledCache;
|
|
26
|
+
_enabledCache = process.env.PI_LENS_QUIET_WINDOW !== "0";
|
|
27
|
+
return _enabledCache;
|
|
28
|
+
}
|
|
29
|
+
/** Test-only: clear the memoized kill-switch read. */
|
|
30
|
+
export function _resetQuietWindowEnabledForTests() {
|
|
31
|
+
_enabledCache = undefined;
|
|
32
|
+
}
|
|
33
|
+
const DEFAULT_QUIET_WINDOW_WAIT_MS = 15_000;
|
|
34
|
+
/**
|
|
35
|
+
* Bounded wait for the quiet-window's own settle attempts (currently just
|
|
36
|
+
* the carried-over cascade drain). Lazy env read, `Number.isFinite`-guarded
|
|
37
|
+
* so a malformed value falls back to the default instead of poisoning
|
|
38
|
+
* `Math.max`/`setTimeout` with `NaN` (see PR #109).
|
|
39
|
+
*
|
|
40
|
+
* This is the task's BUDGET, not a statement that the task runs — it keeps
|
|
41
|
+
* returning the default when `isQuietWindowEnabled()` is false. Callers asking
|
|
42
|
+
* "how long will the pipeline actually keep working on this" must check both.
|
|
43
|
+
*/
|
|
44
|
+
export function quietWindowWaitMs() {
|
|
45
|
+
const raw = toPositiveFinite(process.env.PI_LENS_QUIET_WINDOW_WAIT_MS);
|
|
46
|
+
return raw > 0 ? raw : DEFAULT_QUIET_WINDOW_WAIT_MS;
|
|
47
|
+
}
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
import { readInstanceRegistry, updateHeartbeat, } from "./instance-registry.js";
|
|
33
33
|
import { logLatency } from "./latency-logger.js";
|
|
34
34
|
import { sampleProcesses } from "./resource-sampler.js";
|
|
35
|
-
import {
|
|
35
|
+
import { _resetQuietWindowEnabledForTests, isQuietWindowEnabled, quietWindowWaitMs, } from "./quiet-window-config.js";
|
|
36
36
|
// Module-level registry so future work (#458 Tier-3 reconcile, #236
|
|
37
37
|
// enrichment) can plug into the same scheduler without touching it.
|
|
38
38
|
const _tasks = [];
|
|
@@ -48,30 +48,14 @@ export function registerQuietWindowTask(name, fn) {
|
|
|
48
48
|
export function _resetQuietWindowTasksForTests() {
|
|
49
49
|
_tasks.length = 0;
|
|
50
50
|
}
|
|
51
|
-
// --- Kill switch
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
/** Test-only: clear the memoized kill-switch read. */
|
|
61
|
-
export function _resetQuietWindowEnabledForTests() {
|
|
62
|
-
_enabledCache = undefined;
|
|
63
|
-
}
|
|
64
|
-
const DEFAULT_QUIET_WINDOW_WAIT_MS = 15_000;
|
|
65
|
-
/**
|
|
66
|
-
* Bounded wait for the quiet-window's own settle attempts (currently just
|
|
67
|
-
* the carried-over cascade drain). Lazy env read, `Number.isFinite`-guarded
|
|
68
|
-
* so a malformed value falls back to the default instead of poisoning
|
|
69
|
-
* `Math.max`/`setTimeout` with `NaN` (see PR #109).
|
|
70
|
-
*/
|
|
71
|
-
export function quietWindowWaitMs() {
|
|
72
|
-
const raw = toPositiveFinite(process.env.PI_LENS_QUIET_WINDOW_WAIT_MS);
|
|
73
|
-
return raw > 0 ? raw : DEFAULT_QUIET_WINDOW_WAIT_MS;
|
|
74
|
-
}
|
|
51
|
+
// --- Kill switch and wait budget ---
|
|
52
|
+
//
|
|
53
|
+
// #1462 review (N4): both live in `quiet-window-config.ts` now, so a caller
|
|
54
|
+
// that needs only the numbers (`cascade-budget.ts`, on the dispatch load path)
|
|
55
|
+
// does not drag this module's `resource-sampler` → `pidusage` import in with
|
|
56
|
+
// them. Re-exported here so every existing importer is unaffected and there is
|
|
57
|
+
// still one memo behind `isQuietWindowEnabled`.
|
|
58
|
+
export { isQuietWindowEnabled, quietWindowWaitMs, _resetQuietWindowEnabledForTests, };
|
|
75
59
|
// Re-entrancy guard: agent_settled can fire multiple times per session
|
|
76
60
|
// (once per completed/aborted run). If a previous quiet-window run is still
|
|
77
61
|
// in flight when the next fires, skip rather than queue/overlap.
|
|
@@ -224,6 +208,8 @@ async function buildHeartbeatResourcePatch() {
|
|
|
224
208
|
const self = instances.find((instance) => instance.pid === process.pid);
|
|
225
209
|
const childPids = self?.lspChildren.map((child) => child.pid) ?? [];
|
|
226
210
|
const usage = await sampleProcesses([process.pid, ...childPids]);
|
|
211
|
+
if (usage === null)
|
|
212
|
+
return {};
|
|
227
213
|
const childUsage = {};
|
|
228
214
|
for (const pid of childPids) {
|
|
229
215
|
const sample = usage.get(pid);
|
|
@@ -19,7 +19,7 @@ const SNAPSHOT_LOG_SETTING = (process.env.PI_LENS_READ_GUARD_LOG_SNAPSHOTS ?? "1
|
|
|
19
19
|
const LOG_SNAPSHOT_VALIDATION = !["0", "false", "off"].includes(SNAPSHOT_LOG_SETTING);
|
|
20
20
|
export const MAX_EDIT_BATCH_ITEMS = 100;
|
|
21
21
|
let generatedCorrelationCounter = 0;
|
|
22
|
-
function sanitizeCorrelationId(value) {
|
|
22
|
+
export function sanitizeCorrelationId(value) {
|
|
23
23
|
if (typeof value !== "string" && typeof value !== "number")
|
|
24
24
|
return undefined;
|
|
25
25
|
const sanitized = String(value)
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import { createFileTime } from "./file-time.js";
|
|
14
14
|
import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
|
|
15
|
-
import { normalizeFilePath } from "./path-utils.js";
|
|
15
|
+
import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
|
|
16
16
|
import { logReadGuardEvent } from "./read-guard-logger.js";
|
|
17
17
|
export const READ_GUARD_STATE_VERSION = 1;
|
|
18
18
|
// --- Constants ---
|
|
@@ -35,6 +35,61 @@ const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ
|
|
|
35
35
|
*/
|
|
36
36
|
const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
|
|
37
37
|
const READ_GUARD_MAX_FILES = 256;
|
|
38
|
+
/**
|
|
39
|
+
* #1904 item 3: `enforceFileCap` bounds how many FILES the store holds, not how
|
|
40
|
+
* many records each file holds. One hot file reached 268 hash-bearing records in
|
|
41
|
+
* 75 minutes, and every record carries a `lineHashes` map, so a long session on
|
|
42
|
+
* a few files grows without bound. Cap records per file and drop oldest-first.
|
|
43
|
+
*
|
|
44
|
+
* Eviction is safe in the blocking direction: it narrows the staleness-rescue
|
|
45
|
+
* path — a stale edit rescued by an older snapshot that still matches — and can
|
|
46
|
+
* turn an allow into a "re-read and retry" block. It never turns a block into
|
|
47
|
+
* an allow. See `enforceRecordCapForFile` for which records go first, and why
|
|
48
|
+
* age alone is the wrong order.
|
|
49
|
+
*/
|
|
50
|
+
const READ_GUARD_MAX_RECORDS_PER_FILE = 128;
|
|
51
|
+
/**
|
|
52
|
+
* Trim a single file's read list to the per-file cap. Returns how many records
|
|
53
|
+
* were dropped so the ledger can show the trim.
|
|
54
|
+
*
|
|
55
|
+
* Eviction is NOT purely by age. The shape that overflows this cap is
|
|
56
|
+
* read-once-then-grep-often: one whole-file read followed by hundreds of cheap
|
|
57
|
+
* search credits. Pure age order evicts that whole-file read first, and it is
|
|
58
|
+
* the record `canIgnoreStalenessByHashes` needs to rescue an edit after an
|
|
59
|
+
* unrelated mtime touch. So spend the search credits first, oldest among them
|
|
60
|
+
* first, and only fall through to genuine reads when the credits run out.
|
|
61
|
+
* Genuine reads are then evicted oldest-first as before.
|
|
62
|
+
*
|
|
63
|
+
* Records are trimmed IN PLACE: `EditRecord.precedingReads` holds a reference
|
|
64
|
+
* to this same array, so a replacement array would silently detach it.
|
|
65
|
+
*/
|
|
66
|
+
function enforceRecordCapForFile(records) {
|
|
67
|
+
if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
|
|
68
|
+
return 0;
|
|
69
|
+
const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
|
|
70
|
+
const evicted = new Set();
|
|
71
|
+
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
72
|
+
if (records[i].searchCredit !== undefined)
|
|
73
|
+
evicted.add(i);
|
|
74
|
+
}
|
|
75
|
+
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
76
|
+
evicted.add(i);
|
|
77
|
+
}
|
|
78
|
+
const kept = records.filter((_, i) => !evicted.has(i));
|
|
79
|
+
records.length = 0;
|
|
80
|
+
for (const record of kept)
|
|
81
|
+
records.push(record);
|
|
82
|
+
return excess;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* #1904 class sweep: `this.edits` is the same shape as `this.reads` — a
|
|
86
|
+
* per-file array that only ever grows. Its cap sits far above every consumer's
|
|
87
|
+
* reach, so trimming is inert: `canTreatStalenessAsOwnPriorEdit` reads only the
|
|
88
|
+
* last record, and `findRelocation`'s window saturates at
|
|
89
|
+
* RELOCATION_WINDOW_MAX / RELOCATION_WINDOW_PER_EDIT (20) applied edits. Only
|
|
90
|
+
* `getStats`, a debug surface, sees the older records at all.
|
|
91
|
+
*/
|
|
92
|
+
const READ_GUARD_MAX_EDITS_PER_FILE = 256;
|
|
38
93
|
// Unconsumed reads remain valid until edit or session end, but this high
|
|
39
94
|
// sanity cap prevents a read-only session from growing without bound.
|
|
40
95
|
const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
|
|
@@ -213,6 +268,23 @@ export class ReadGuard {
|
|
|
213
268
|
// pi Write tool authored, independent of filesystem mtime granularity
|
|
214
269
|
// or clock skew (NFS, FAT32, etc.).
|
|
215
270
|
writtenThisSession = new Set();
|
|
271
|
+
// Existence-independent index for hasKnownPath/forgetPath (#1668 review
|
|
272
|
+
// F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
|
|
273
|
+
// currently exists on disk: an existing file resolves to realpathSync
|
|
274
|
+
// canonical casing, a missing one to a lowercased tail. recordRead/
|
|
275
|
+
// recordWritten always key while the file is still on disk (real
|
|
276
|
+
// casing); hasKnownPath/forgetPath are queried AFTER an external delete
|
|
277
|
+
// already landed, when the path no longer exists — recomputing
|
|
278
|
+
// `this.key()` at that point returns a DIFFERENT string for any
|
|
279
|
+
// mixed-case basename (`MyModule.ts` → `mymodule.ts`), so a lookup
|
|
280
|
+
// against `reads`/`writtenThisSession` silently misses. This index maps
|
|
281
|
+
// a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
|
|
282
|
+
// lowercase, no filesystem access, so it never depends on current disk
|
|
283
|
+
// state) to the REAL key `reads`/`writtenThisSession` used at record
|
|
284
|
+
// time, so a post-delete lookup finds the same entry regardless of what
|
|
285
|
+
// happened to the file since. Pruned inside `evictFile` so it never
|
|
286
|
+
// outlives the record it points at.
|
|
287
|
+
knownPathIndex = 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 evictedRecordCount = enforceRecordCapForFile(arr);
|
|
314
403
|
this.touchFile(storedRecord.filePath);
|
|
315
404
|
this.enforceFileCap();
|
|
316
405
|
logReadGuardEvent({
|
|
@@ -334,6 +423,15 @@ 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
|
});
|
|
339
437
|
// Also update FileTime stamp for this file
|
|
@@ -442,9 +540,17 @@ export class ReadGuard {
|
|
|
442
540
|
// 2. FileTime check (actual staleness)
|
|
443
541
|
let ignoredOwnEditStaleness = false;
|
|
444
542
|
let ignoredHashStaleness = false;
|
|
543
|
+
let ignoredOldTextResolvedStaleness = false;
|
|
445
544
|
if (this.fileTime.hasChanged(filePath)) {
|
|
446
545
|
const lastRead = fileReads[fileReads.length - 1];
|
|
447
|
-
if (
|
|
546
|
+
if (options?.oldTextResolved === true) {
|
|
547
|
+
// The host-facing preflight sets this only after oldText resolves to
|
|
548
|
+
// exactly one span in the live bytes. That content evidence is newer
|
|
549
|
+
// and stronger than FileTime's coarse external-write signal, matching
|
|
550
|
+
// the skipSnapshotCheck exception at the later range-stale gate.
|
|
551
|
+
ignoredOldTextResolvedStaleness = true;
|
|
552
|
+
}
|
|
553
|
+
else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
|
|
448
554
|
ignoredOwnEditStaleness = true;
|
|
449
555
|
}
|
|
450
556
|
else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
|
|
@@ -474,7 +580,7 @@ export class ReadGuard {
|
|
|
474
580
|
const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
|
|
475
581
|
let viaSymbol = false;
|
|
476
582
|
for (const range of rangesToCheck) {
|
|
477
|
-
const snapshotValidation = this.validateRangeSnapshot(filePath, range);
|
|
583
|
+
const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
|
|
478
584
|
const coverage = this.checkCoverage(filePath, range);
|
|
479
585
|
if (!coverage.covered) {
|
|
480
586
|
const lastRead = fileReads[fileReads.length - 1];
|
|
@@ -568,10 +674,15 @@ export class ReadGuard {
|
|
|
568
674
|
}
|
|
569
675
|
const verdict = this.allow();
|
|
570
676
|
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
571
|
-
reasonKind:
|
|
677
|
+
reasonKind: ignoredOldTextResolvedStaleness
|
|
678
|
+
? "file_modified_oldtext_unique"
|
|
679
|
+
: viaSymbol
|
|
680
|
+
? "symbol_coverage"
|
|
681
|
+
: "range_coverage",
|
|
572
682
|
viaSymbol,
|
|
573
683
|
ignoredOwnEditStaleness,
|
|
574
684
|
ignoredHashStaleness,
|
|
685
|
+
oldTextResolved: ignoredOldTextResolvedStaleness,
|
|
575
686
|
});
|
|
576
687
|
return verdict;
|
|
577
688
|
}
|
|
@@ -587,6 +698,44 @@ export class ReadGuard {
|
|
|
587
698
|
return true; // Assume new if we can't stat
|
|
588
699
|
}
|
|
589
700
|
}
|
|
701
|
+
/**
|
|
702
|
+
* Whether pi-lens has any record of this path from a read or write this
|
|
703
|
+
* session (#1668). Gates external-delete detection: a path pi-lens never
|
|
704
|
+
* read or wrote is one no LSP server's cache was ever told about through
|
|
705
|
+
* us, so an `rm` naming it carries no signal worth checking disk for —
|
|
706
|
+
* this is a lookup against state already being tracked, never a fresh
|
|
707
|
+
* filesystem stat over an unbounded path set.
|
|
708
|
+
*
|
|
709
|
+
* MUST go through `knownPathIndex`, not `this.key(filePath)` directly
|
|
710
|
+
* (#1668 review F1): this is called AFTER a bash delete has already
|
|
711
|
+
* landed, when `filePath` no longer exists — `this.key()` at that point
|
|
712
|
+
* returns a lowercased-tail key, not the real-casing key `recordRead`/
|
|
713
|
+
* `recordWritten` stored while the file was still on disk. For a
|
|
714
|
+
* mixed-case basename (`MyModule.ts`) the two keys differ and a direct
|
|
715
|
+
* `this.key()` lookup always misses.
|
|
716
|
+
*/
|
|
717
|
+
hasKnownPath(filePath) {
|
|
718
|
+
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
719
|
+
return (stored !== undefined &&
|
|
720
|
+
(this.reads.has(stored) || this.writtenThisSession.has(stored)));
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Drop all record of a path pi-lens confirmed no longer exists on disk
|
|
724
|
+
* (#1668, external delete). Without this a later write reusing the same
|
|
725
|
+
* path would inherit a stale writtenThisSession/reads entry from before
|
|
726
|
+
* the delete, and a repeat `rm` of the same already-gone path would keep
|
|
727
|
+
* matching {@link hasKnownPath} and re-emitting a type-3 notification.
|
|
728
|
+
*
|
|
729
|
+
* MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
|
|
730
|
+
* (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
|
|
731
|
+
* is already gone from disk by the time this runs, so recomputing
|
|
732
|
+
* `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
|
|
733
|
+
* basename and leaves the real entry behind un-evicted.
|
|
734
|
+
*/
|
|
735
|
+
forgetPath(filePath) {
|
|
736
|
+
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
737
|
+
this.evictFile(stored ?? this.key(filePath));
|
|
738
|
+
}
|
|
590
739
|
/**
|
|
591
740
|
* Mark a file as pending creation (Write tool to a non-existing file).
|
|
592
741
|
* Must be called from the tool_call handler before the write lands so
|
|
@@ -601,8 +750,12 @@ export class ReadGuard {
|
|
|
601
750
|
* Call this from the tool_result handler so the next checkEdit on the same
|
|
602
751
|
* file doesn't see "file_modified" caused by our own previous edit.
|
|
603
752
|
*/
|
|
604
|
-
recordWritten(
|
|
605
|
-
filePath = this.key(
|
|
753
|
+
recordWritten(rawFilePath) {
|
|
754
|
+
const filePath = this.key(rawFilePath);
|
|
755
|
+
// #1668 review F1: index by the existence-independent syntactic key
|
|
756
|
+
// (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
|
|
757
|
+
// after an external delete can still find this entry's real key.
|
|
758
|
+
this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
|
|
606
759
|
this.fileTime.read(filePath);
|
|
607
760
|
this.writtenThisSession.add(filePath);
|
|
608
761
|
if (this.reads.has(filePath))
|
|
@@ -838,7 +991,13 @@ export class ReadGuard {
|
|
|
838
991
|
return (read.enclosingSymbol.startLine <= editStart &&
|
|
839
992
|
read.enclosingSymbol.endLine >= editEnd);
|
|
840
993
|
}
|
|
841
|
-
validateRangeSnapshot(filePath, range
|
|
994
|
+
validateRangeSnapshot(filePath, range,
|
|
995
|
+
/**
|
|
996
|
+
* What the CALLER will do with the verdict. `checkEdit` honours
|
|
997
|
+
* `skipSnapshotCheck` for content-validated (oldText) edits, so a
|
|
998
|
+
* `shouldBlock` verdict may never reach the gate (#1904 item 1).
|
|
999
|
+
*/
|
|
1000
|
+
snapshotCheckSkipped) {
|
|
842
1001
|
const reads = this.reads.get(filePath) ?? [];
|
|
843
1002
|
const candidates = reads.filter((read) => this.readCoversRange(read, range));
|
|
844
1003
|
let status = "unavailable";
|
|
@@ -886,6 +1045,17 @@ export class ReadGuard {
|
|
|
886
1045
|
lastUnavailableTimestamp <= lastMismatchTimestamp &&
|
|
887
1046
|
checkedCandidateCount > 0 &&
|
|
888
1047
|
hashUnavailableCandidateCount === 0;
|
|
1048
|
+
// `enforced` below states INTENT — whether this validation reached a
|
|
1049
|
+
// decidable verdict. It says nothing about what the caller did with it.
|
|
1050
|
+
// `outcome` states the caller's actual behaviour, which is what an
|
|
1051
|
+
// enforcement rate must be computed from (#1904 item 1).
|
|
1052
|
+
const outcome = shouldBlock
|
|
1053
|
+
? snapshotCheckSkipped
|
|
1054
|
+
? "bypassed-content-match"
|
|
1055
|
+
: "enforced-block"
|
|
1056
|
+
: status === "match"
|
|
1057
|
+
? "enforced-pass"
|
|
1058
|
+
: "not-decidable";
|
|
889
1059
|
logReadGuardEvent({
|
|
890
1060
|
event: "range_snapshot_validation",
|
|
891
1061
|
sessionId: this.sessionId,
|
|
@@ -903,6 +1073,7 @@ export class ReadGuard {
|
|
|
903
1073
|
missingLines: missingLines.slice(0, 20),
|
|
904
1074
|
mismatchedLines: mismatchedLines.slice(0, 20),
|
|
905
1075
|
enforced: shouldBlock || status === "match",
|
|
1076
|
+
outcome,
|
|
906
1077
|
},
|
|
907
1078
|
});
|
|
908
1079
|
return {
|
|
@@ -1117,6 +1288,9 @@ export class ReadGuard {
|
|
|
1117
1288
|
reason: verdict.reason,
|
|
1118
1289
|
timestamp: Date.now(),
|
|
1119
1290
|
});
|
|
1291
|
+
if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
|
|
1292
|
+
arr.splice(0, arr.length - READ_GUARD_MAX_EDITS_PER_FILE);
|
|
1293
|
+
}
|
|
1120
1294
|
this.edits.set(filePath, arr);
|
|
1121
1295
|
}
|
|
1122
1296
|
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
|
}
|