pi-lens 4.1.2 → 4.1.4
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/CHANGELOG.md +1308 -0
- package/README.md +3 -0
- package/config/biome/core.jsonc +11 -2
- package/config/dependency-cruiser-eager-allowlist.json +91 -0
- package/dist/clients/actionable-warnings-logger.js +18 -3
- package/dist/clients/actionable-warnings.js +927 -80
- package/dist/clients/agent-behavior-client.js +13 -4
- package/dist/clients/ast-grep-client.js +136 -9
- package/dist/clients/ast-grep-rule-manager.js +60 -5
- package/dist/clients/ast-grep-tool-logger.js +2 -2
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +1 -3
- package/dist/clients/biome-client.js +16 -2
- package/dist/clients/blocker-freshness.js +34 -0
- package/dist/clients/bootstrap.js +509 -73
- package/dist/clients/bounded-cache.js +152 -12
- package/dist/clients/bounded-telemetry.js +115 -23
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bundled-resource-health.js +113 -0
- package/dist/clients/bus-events-logger.js +2 -2
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +33 -5
- package/dist/clients/cache-manager.js +105 -6
- package/dist/clients/cache-observability.js +46 -19
- package/dist/clients/cargo-manifest.js +422 -0
- package/dist/clients/cascade-logger.js +17 -3
- package/dist/clients/code-quality-warnings.js +13 -3
- package/dist/clients/complexity-client.js +16 -5
- package/dist/clients/config-core/deny.js +221 -0
- package/dist/clients/config-core/index.js +47 -0
- package/dist/clients/config-core/merge.js +357 -0
- package/dist/clients/config-core/normalize.js +340 -0
- package/dist/clients/config-core/process-spec.js +248 -0
- package/dist/clients/config-core/provenance.js +171 -0
- package/dist/clients/config-core/records.js +218 -0
- package/dist/clients/config-core/resolve.js +125 -0
- package/dist/clients/config-core/safe-object.js +78 -0
- package/dist/clients/config-core/schema.js +167 -0
- package/dist/clients/config-diagnostic-codes.js +281 -0
- package/dist/clients/config-locations.js +160 -0
- package/dist/clients/config-resolve.js +789 -0
- package/dist/clients/config-schema.js +197 -0
- package/dist/clients/config-warn.js +407 -0
- package/dist/clients/dead-code-client.js +33 -25
- package/dist/clients/dead-code-logger.js +2 -2
- package/dist/clients/deadline-utils.js +178 -0
- package/dist/clients/debug-handles.js +2 -2
- package/dist/clients/debug-heap.js +3 -3
- package/dist/clients/deferred-lsp-work.js +106 -0
- package/dist/clients/degradation-ledger.js +166 -8
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +28 -22
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-line-freshness.js +12 -9
- package/dist/clients/diagnostic-logger.js +14 -4
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +304 -19
- package/dist/clients/dispatch/fact-store.js +328 -9
- package/dist/clients/dispatch/integration.js +214 -107
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
- package/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/dist/clients/dispatch/runners/credo.js +0 -1
- package/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/dist/clients/dispatch/runners/detekt.js +0 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/dist/clients/dispatch/runners/eslint.js +5 -2
- package/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/dist/clients/dispatch/runners/helm-lint.js +16 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -10
- package/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +0 -1
- package/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/dist/clients/dispatch/runners/lsp.js +37 -2
- package/dist/clients/dispatch/runners/markdownlint.js +4 -2
- package/dist/clients/dispatch/runners/mypy.js +0 -1
- package/dist/clients/dispatch/runners/oxlint.js +24 -22
- package/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
- package/dist/clients/dispatch/runners/pyright.js +7 -4
- package/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/dist/clients/dispatch/runners/ruff.js +0 -1
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
- package/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/dist/clients/dispatch/runners/taplo.js +0 -1
- package/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/dist/clients/dispatch/runners/tflint.js +0 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
- package/dist/clients/dispatch/runners/vale.js +0 -1
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/dist/clients/disposition-logger.js +3 -3
- package/dist/clients/effective-config.js +403 -0
- package/dist/clients/error-class.js +23 -0
- package/dist/clients/event-loop-hold.js +274 -0
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +2 -2
- package/dist/clients/feature-hints.js +2 -1
- package/dist/clients/file-time.js +6 -1
- package/dist/clients/file-utils.js +266 -13
- package/dist/clients/finding-delivery-gate.js +55 -13
- package/dist/clients/format-service.js +6 -1
- package/dist/clients/formatters.js +303 -104
- package/dist/clients/freshness-cadence.js +17 -0
- package/dist/clients/generated-artifacts.js +114 -19
- package/dist/clients/generation-guard.js +4 -9
- package/dist/clients/git-guard.js +198 -48
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +39 -0
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/gradle-ktfmt-style.js +252 -0
- package/dist/clients/gzip-stage-write.js +7 -0
- package/dist/clients/hashline-anchor.js +424 -0
- package/dist/clients/hook-budgets.js +76 -0
- package/dist/clients/host-edit-normalize.js +5 -2
- package/dist/clients/host-ports.js +1 -1
- package/dist/clients/installer/index.js +200 -84
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +108 -243
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +435 -122
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/language-profile.js +4 -2
- package/dist/clients/language-registry.js +595 -0
- package/dist/clients/latency-logger.js +179 -3
- package/dist/clients/ledger-bounds.js +13 -0
- package/dist/clients/lens-config.js +141 -30
- package/dist/clients/lens-engine.js +30 -37
- package/dist/clients/lens-flag-registry.js +67 -6
- package/dist/clients/lens-map.js +3 -1
- package/dist/clients/log-cleanup.js +2 -2
- package/dist/clients/lsp/aggregation.js +3 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +672 -49
- package/dist/clients/lsp/config.js +504 -156
- package/dist/clients/lsp/diagnostic-binding.js +10 -5
- package/dist/clients/lsp/edits.js +15 -2
- package/dist/clients/lsp/index.js +1179 -247
- package/dist/clients/lsp/jvm-runtime.js +5 -0
- package/dist/clients/lsp/language.js +18 -169
- package/dist/clients/lsp/launch.js +38 -9
- package/dist/clients/lsp/pending-aux-coverage.js +113 -12
- package/dist/clients/lsp/server.js +426 -160
- package/dist/clients/lsp/session-roots.js +192 -0
- package/dist/clients/lsp/spawn-history.js +2 -7
- package/dist/clients/lsp/tsserver-sync.js +9 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-mutation.js +179 -23
- package/dist/clients/map-with-concurrency.js +36 -0
- package/dist/clients/mcp/analyze.js +45 -10
- package/dist/clients/mcp/session.js +19 -17
- package/dist/clients/memory-sampler.js +28 -1
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +2 -4
- package/dist/clients/module-report.js +20 -45
- package/dist/clients/mutating-tool.js +651 -0
- package/dist/clients/mutation-attribution.js +368 -0
- package/dist/clients/mutation-bridge.js +240 -0
- package/dist/clients/ndjson-logger.js +247 -20
- package/dist/clients/observed-mutation-sources.js +101 -0
- package/dist/clients/observed-mutation.js +1215 -0
- package/dist/clients/opaque-mutation-scan.js +300 -40
- package/dist/clients/opengrep-config.js +4 -0
- package/dist/clients/package-manager.js +201 -44
- package/dist/clients/partial-edit-apply.js +359 -80
- package/dist/clients/path-attribution-telemetry.js +23 -7
- package/dist/clients/path-keyed-map.js +35 -3
- package/dist/clients/path-utils.js +460 -13
- package/dist/clients/persist-debounce.js +8 -1
- package/dist/clients/php-cs-fixer-config.js +114 -0
- package/dist/clients/pipeline.js +77 -24
- package/dist/clients/probe-home-state.js +228 -0
- package/dist/clients/process-bridge.js +66 -0
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/process-snapshot.js +68 -0
- package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/dist/clients/project-diagnostics/scanner.js +61 -24
- package/dist/clients/project-lens-config.js +393 -122
- package/dist/clients/project-snapshot.js +85 -35
- package/dist/clients/python-environment.js +217 -0
- package/dist/clients/python-provenance.js +639 -0
- package/dist/clients/quiet-window.js +6 -1
- package/dist/clients/read-bridge.js +13 -21
- package/dist/clients/read-expansion.js +9 -46
- package/dist/clients/read-guard-logger.js +39 -6
- package/dist/clients/read-guard-tool-lines.js +364 -212
- package/dist/clients/read-guard.js +95 -7
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +195 -96
- package/dist/clients/review-graph/builder.js +752 -132
- package/dist/clients/review-graph/service.js +55 -3
- package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/dist/clients/review-graph/workspace-modules.js +10 -74
- package/dist/clients/review-graph-logger.js +30 -4
- package/dist/clients/runtime-agent-end.js +18 -2
- package/dist/clients/runtime-context.js +49 -16
- package/dist/clients/runtime-coordinator.js +93 -6
- package/dist/clients/runtime-session.js +169 -34
- package/dist/clients/runtime-tool-call.js +183 -35
- package/dist/clients/runtime-tool-result.js +894 -241
- package/dist/clients/runtime-turn.js +1264 -74
- package/dist/clients/rust-client.js +31 -0
- package/dist/clients/safe-spawn.js +191 -28
- package/dist/clients/sanitize.js +7 -1
- package/dist/clients/scratch-tree-policy.js +1 -3
- package/dist/clients/security-scan-client.js +51 -17
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-start-observability.js +79 -0
- package/dist/clients/sessionstart-logger.js +12 -3
- package/dist/clients/sg-runner.js +27 -15
- package/dist/clients/sgconfig.js +6 -1
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/skills-resolver.js +105 -0
- package/dist/clients/smells-rollup.js +2 -2
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/dist/clients/startup-scan.js +6 -7
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +33 -0
- package/dist/clients/subagent-mode.js +17 -4
- package/dist/clients/test-runner-client.js +586 -118
- package/dist/clients/test-runner-delivery.js +239 -0
- package/dist/clients/tool-definition.js +41 -1
- package/dist/clients/tool-policy.js +92 -22
- package/dist/clients/tree-sitter-cache.js +40 -40
- package/dist/clients/tree-sitter-client.js +97 -53
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-query-loader.js +81 -1
- package/dist/clients/tree-sitter-shared.js +51 -46
- package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/dist/clients/typos-config.js +5 -0
- package/dist/clients/user-notify.js +6 -2
- package/dist/clients/warm-attach.js +8 -10
- package/dist/clients/widget-state.js +191 -26
- package/dist/clients/word-index-logger.js +15 -3
- package/dist/clients/word-index-store.js +463 -0
- package/dist/clients/word-index.js +815 -125
- package/dist/clients/workspace-topology.js +25 -1
- package/dist/clients/zizmor-config.js +29 -6
- package/dist/index.js +68577 -53950
- package/dist/mcp/analyze-cli.js +3 -1
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +133 -6
- package/dist/scripts/lib/process-scan.mjs +583 -0
- package/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/dist/tools/effective-config.js +89 -0
- package/dist/tools/lens-diagnostics.js +99 -24
- package/dist/tools/lsp-diagnostics.js +81 -85
- package/dist/tools/lsp-navigation.js +40 -17
- package/dist/tools/shared.js +0 -1
- package/docs/agent-guide.md +68 -3
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/configuration.md +222 -0
- package/docs/dependencies.md +3 -3
- package/docs/features.md +44 -5
- package/docs/language-coverage.md +2 -2
- package/docs/pi-lens-fixer.md +25 -0
- package/docs/pi-lens-investigator.md +25 -0
- package/docs/pi-lens-reviewer.md +27 -0
- package/docs/pi-lens-subagent.md +38 -0
- package/docs/pi-lens-warden.md +55 -0
- package/docs/public-api-stability.md +359 -0
- package/docs/release-qa-baseline.md +182 -0
- package/docs/subagent-compat.md +110 -30
- package/docs/tree-sitter_rules_catalog.md +2 -2
- package/package.json +17 -21
- package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/scripts/install-selftest.mjs +58 -2
- package/scripts/lib/skills-predicate.mjs +129 -0
- package/scripts/rpc-load-check.mjs +3 -1
|
@@ -4,6 +4,7 @@ import { fileURLToPath } from "node:url";
|
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
5
|
import { gunzipSync, gzipSync } from "node:zlib";
|
|
6
6
|
import { writeFileAtomic } from "./atomic-write.js";
|
|
7
|
+
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
7
8
|
import { getProjectDataDir } from "./file-utils.js";
|
|
8
9
|
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
9
10
|
import { readJsonCache } from "./json-cache-read.js";
|
|
@@ -130,14 +131,14 @@ export function readProjectSnapshotMeta(cwd) {
|
|
|
130
131
|
export function isProjectSnapshotMetaStale(meta, currentProjectSeq) {
|
|
131
132
|
return (meta.version !== PROJECT_SNAPSHOT_VERSION || meta.seq !== currentProjectSeq);
|
|
132
133
|
}
|
|
133
|
-
const SNAPSHOT_PARSE_CACHE_MAX = 4;
|
|
134
|
+
export const SNAPSHOT_PARSE_CACHE_MAX = 4;
|
|
134
135
|
// #957 review: the cache exists to avoid re-parsing NORMAL snapshots within a
|
|
135
136
|
// session. A 112MB-class body parses to hundreds of MB of heap — pinning that
|
|
136
137
|
// for process lifetime inverts the win, so oversized bodies are simply never
|
|
137
138
|
// cached (they re-parse per read, exactly the pre-#947 behavior). Measured
|
|
138
139
|
// against the UNCOMPRESSED body size, never the (much smaller) gz file size.
|
|
139
140
|
const SNAPSHOT_PARSE_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
140
|
-
const snapshotParseCache = new
|
|
141
|
+
const snapshotParseCache = new BoundedFifoMap(SNAPSHOT_PARSE_CACHE_MAX);
|
|
141
142
|
function withoutWordIndex(snapshot) {
|
|
142
143
|
if (!snapshot?.wordIndex)
|
|
143
144
|
return snapshot;
|
|
@@ -145,18 +146,13 @@ function withoutWordIndex(snapshot) {
|
|
|
145
146
|
return stripped;
|
|
146
147
|
}
|
|
147
148
|
function cacheParsedSnapshot(snapshotPath, entry) {
|
|
148
|
-
// Refresh recency (
|
|
149
|
+
// Refresh recency (delete then set moves the key to the newest position;
|
|
150
|
+
// BoundedFifoMap itself never reorders an already-present key on set()).
|
|
149
151
|
snapshotParseCache.delete(snapshotPath);
|
|
150
152
|
snapshotParseCache.set(snapshotPath, entry);
|
|
151
|
-
while (snapshotParseCache.size > SNAPSHOT_PARSE_CACHE_MAX) {
|
|
152
|
-
const oldest = snapshotParseCache.keys().next().value;
|
|
153
|
-
if (oldest === undefined)
|
|
154
|
-
break;
|
|
155
|
-
snapshotParseCache.delete(oldest);
|
|
156
|
-
}
|
|
157
153
|
}
|
|
158
154
|
const authoritativeSnapshots = new Map();
|
|
159
|
-
const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
|
|
155
|
+
export const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
|
|
160
156
|
const PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
|
|
161
157
|
function projectSnapshotIdleEvictMs() {
|
|
162
158
|
const value = Number.parseInt(process.env.PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS ?? "", 10);
|
|
@@ -533,11 +529,22 @@ export function loadProjectSnapshotExportsAndRules(cwd) {
|
|
|
533
529
|
const body = resolveSnapshotBodyPath(cwd);
|
|
534
530
|
const authoritative = authoritativeSnapshots.get(key);
|
|
535
531
|
if (authoritative) {
|
|
536
|
-
|
|
537
|
-
|
|
532
|
+
// `body === null` means nothing is on disk — that is never "the body
|
|
533
|
+
// changed size", so it must not be read as superseding our own write.
|
|
534
|
+
// See the matching comment in loadProjectSnapshotInternal.
|
|
535
|
+
const notSuperseded = body === null ||
|
|
536
|
+
(body.mtimeMs <= authoritative.knownMtime &&
|
|
537
|
+
body.size === authoritative.knownSize);
|
|
538
|
+
if (notSuperseded) {
|
|
538
539
|
const { version, seq, cachedExports, projectRulesScan } = authoritative.snapshot;
|
|
539
540
|
return { version, seq, cachedExports, projectRulesScan };
|
|
540
541
|
}
|
|
542
|
+
// On mismatch, unlike the full loader, this narrow loader neither
|
|
543
|
+
// deletes the entry nor touches its idle timer (it never calls
|
|
544
|
+
// touchAuthoritativeSnapshot even on a hit). The stale entry is safe to
|
|
545
|
+
// leave in place: nothing here re-arms its eviction, so it is still
|
|
546
|
+
// bounded by whatever idle window the full loader (or the original
|
|
547
|
+
// write) last set, not left to live indefinitely.
|
|
541
548
|
}
|
|
542
549
|
if (!body)
|
|
543
550
|
return null;
|
|
@@ -547,18 +554,22 @@ function loadProjectSnapshotInternal(cwd, requireWordIndex) {
|
|
|
547
554
|
const key = normalizeMapKey(cwd);
|
|
548
555
|
const body = resolveSnapshotBodyPath(cwd);
|
|
549
556
|
// Authoritative in-process write wins while our own (possibly still
|
|
550
|
-
// in-flight) write has not been superseded on disk by a newer external
|
|
551
|
-
//
|
|
552
|
-
//
|
|
557
|
+
// in-flight) write has not been superseded on disk by a newer external mtime
|
|
558
|
+
// or a different-size body in the same/coarser bucket. `body === null` means
|
|
559
|
+
// nothing is on disk — either our just-scheduled write hasn't landed yet, or
|
|
560
|
+
// something removed the body after we wrote it (e.g. a cleared cache dir).
|
|
561
|
+
// Neither case is "the body changed size", so serve the in-process object.
|
|
553
562
|
const authoritative = authoritativeSnapshots.get(key);
|
|
554
563
|
if (authoritative) {
|
|
555
|
-
const
|
|
556
|
-
|
|
564
|
+
const notSuperseded = body === null ||
|
|
565
|
+
(body.mtimeMs <= authoritative.knownMtime &&
|
|
566
|
+
body.size === authoritative.knownSize);
|
|
567
|
+
if (notSuperseded) {
|
|
557
568
|
touchAuthoritativeSnapshot(key, authoritative);
|
|
558
569
|
return authoritative.snapshot;
|
|
559
570
|
}
|
|
560
|
-
// An external writer
|
|
561
|
-
// serving the now-stale in-memory object.
|
|
571
|
+
// An external writer changed the body beyond our metadata stamp — honor
|
|
572
|
+
// disk and stop serving the now-stale in-memory object.
|
|
562
573
|
deleteAuthoritativeSnapshot(key);
|
|
563
574
|
}
|
|
564
575
|
if (!body) {
|
|
@@ -605,8 +616,8 @@ export function loadProjectSnapshotWithoutWordIndex(cwd) {
|
|
|
605
616
|
return loadProjectSnapshotInternal(cwd, false);
|
|
606
617
|
}
|
|
607
618
|
const _snapshotGenerationStates = new Map();
|
|
608
|
-
const _successfulSnapshotPersists = new
|
|
609
|
-
const _failedSnapshotPersists = new
|
|
619
|
+
const _successfulSnapshotPersists = new BoundedFifoMap(PROJECT_SNAPSHOT_MAX_WARM_ROOTS);
|
|
620
|
+
const _failedSnapshotPersists = new BoundedFifoMap(PROJECT_SNAPSHOT_MAX_WARM_ROOTS);
|
|
610
621
|
const _activeSnapshotPersists = new Map();
|
|
611
622
|
const _queuedSnapshotPersists = new Map();
|
|
612
623
|
const _snapshotWorkerRequests = new Map();
|
|
@@ -633,12 +644,6 @@ function pendingSnapshotIsCurrent(pending) {
|
|
|
633
644
|
function rememberSuccessfulSnapshotPersist(key, record) {
|
|
634
645
|
_successfulSnapshotPersists.delete(key);
|
|
635
646
|
_successfulSnapshotPersists.set(key, record);
|
|
636
|
-
while (_successfulSnapshotPersists.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
|
|
637
|
-
const oldest = _successfulSnapshotPersists.keys().next().value;
|
|
638
|
-
if (oldest === undefined)
|
|
639
|
-
break;
|
|
640
|
-
_successfulSnapshotPersists.delete(oldest);
|
|
641
|
-
}
|
|
642
647
|
}
|
|
643
648
|
function assessSnapshotBodyIntegrity(meta, body) {
|
|
644
649
|
if (meta.gzBytes === undefined) {
|
|
@@ -793,12 +798,6 @@ function recordSnapshotPersistFailure(pending, error, bodyPersisted = false) {
|
|
|
793
798
|
seq: pending.snapshot.seq,
|
|
794
799
|
generation: pending.generation,
|
|
795
800
|
});
|
|
796
|
-
while (_failedSnapshotPersists.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
|
|
797
|
-
const oldest = _failedSnapshotPersists.keys().next().value;
|
|
798
|
-
if (oldest === undefined)
|
|
799
|
-
break;
|
|
800
|
-
_failedSnapshotPersists.delete(oldest);
|
|
801
|
-
}
|
|
802
801
|
deleteAuthoritativeSnapshot(pending.key);
|
|
803
802
|
logLatency({
|
|
804
803
|
type: "phase",
|
|
@@ -907,10 +906,12 @@ function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
|
|
|
907
906
|
return;
|
|
908
907
|
}
|
|
909
908
|
try {
|
|
910
|
-
|
|
909
|
+
const stat = fs.statSync(pending.gzPath);
|
|
910
|
+
entry.knownMtime = stat.mtimeMs;
|
|
911
|
+
entry.knownSize = stat.size;
|
|
911
912
|
}
|
|
912
913
|
catch {
|
|
913
|
-
// If we can't stat our own write, leave
|
|
914
|
+
// If we can't stat our own write, leave the metadata as-is; the worst case
|
|
914
915
|
// is one extra disk re-parse on the next load.
|
|
915
916
|
}
|
|
916
917
|
}
|
|
@@ -1368,11 +1369,16 @@ export function saveProjectSnapshot(cwd, snapshot) {
|
|
|
1368
1369
|
// legacy body to a merge-consumer, silently dropping this snapshot's fields.
|
|
1369
1370
|
const priorBody = resolveSnapshotBodyPath(cwd);
|
|
1370
1371
|
const knownMtime = priorBody ? priorBody.mtimeMs : Number.NEGATIVE_INFINITY;
|
|
1372
|
+
const knownSize = priorBody ? priorBody.size : Number.NEGATIVE_INFINITY;
|
|
1371
1373
|
const authoritativeEntry = {
|
|
1372
1374
|
snapshot,
|
|
1373
1375
|
knownMtime,
|
|
1376
|
+
knownSize,
|
|
1374
1377
|
lastUsedAt: Date.now(),
|
|
1375
1378
|
};
|
|
1379
|
+
const previousAuthoritativeEntry = authoritativeSnapshots.get(key);
|
|
1380
|
+
if (previousAuthoritativeEntry)
|
|
1381
|
+
clearAuthoritativeSnapshotTimer(previousAuthoritativeEntry);
|
|
1376
1382
|
authoritativeSnapshots.set(key, authoritativeEntry);
|
|
1377
1383
|
scheduleAuthoritativeSnapshotEviction(key, authoritativeEntry);
|
|
1378
1384
|
enforceAuthoritativeSnapshotCap();
|
|
@@ -1470,6 +1476,50 @@ export function setProjectSnapshotPromotionSeamForTests(seam) {
|
|
|
1470
1476
|
export function getProjectSnapshotPersistErrorForTests() {
|
|
1471
1477
|
return _lastSnapshotPersistErrorForTests;
|
|
1472
1478
|
}
|
|
1479
|
+
// ── #2442 test-only seams: exercise the three BoundedFifoMap-backed caches ──
|
|
1480
|
+
// through the exact same write path production uses (cacheParsedSnapshot /
|
|
1481
|
+
// rememberSuccessfulSnapshotPersist's delete+set refresh, and a direct write
|
|
1482
|
+
// mirroring recordSnapshotPersistFailure's), without the real gzip/worker
|
|
1483
|
+
// persistence pipeline or a full ProjectSnapshot fixture.
|
|
1484
|
+
/** Test-only: seed the parse cache exactly as `cacheParsedSnapshot` would. */
|
|
1485
|
+
export function _seedSnapshotParseCacheForTests(key, entry) {
|
|
1486
|
+
cacheParsedSnapshot(key, entry);
|
|
1487
|
+
}
|
|
1488
|
+
export function _snapshotParseCacheKeysForTests() {
|
|
1489
|
+
return [...snapshotParseCache.keys()];
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* #2442 test-only: the exact `snapshotParseCache.get(key)` the production
|
|
1493
|
+
* read at {@link loadProjectSnapshot}'s parse-cache hit performs. A `.has()`
|
|
1494
|
+
* would not do — `.has()` reorders nothing on either bounded class, so a
|
|
1495
|
+
* has-based test cannot tell FIFO from LRU, which is the axis these bounded
|
|
1496
|
+
* tests exist to pin (#2442 review F4).
|
|
1497
|
+
*/
|
|
1498
|
+
export function _snapshotParseCacheGetForTests(key) {
|
|
1499
|
+
return snapshotParseCache.get(key) !== undefined;
|
|
1500
|
+
}
|
|
1501
|
+
/** Test-only: seed the successful-persist cache exactly as production would. */
|
|
1502
|
+
export function _seedSuccessfulSnapshotPersistForTests(key, record) {
|
|
1503
|
+
rememberSuccessfulSnapshotPersist(key, record);
|
|
1504
|
+
}
|
|
1505
|
+
export function _successfulSnapshotPersistKeysForTests() {
|
|
1506
|
+
return [..._successfulSnapshotPersists.keys()];
|
|
1507
|
+
}
|
|
1508
|
+
/** #2442 test-only: the same `_successfulSnapshotPersists.get(key)` the
|
|
1509
|
+
* persist-dedupe read and `getProjectSnapshotPersistStateForTests` perform.
|
|
1510
|
+
* See {@link _snapshotParseCacheGetForTests} for why `.has()` will not do. */
|
|
1511
|
+
export function _successfulSnapshotPersistGetForTests(key) {
|
|
1512
|
+
return _successfulSnapshotPersists.get(key) !== undefined;
|
|
1513
|
+
}
|
|
1514
|
+
/** Test-only: seed the failed-persist cache with the same write
|
|
1515
|
+
* `recordSnapshotPersistFailure` performs, without its side effects
|
|
1516
|
+
* (deleteAuthoritativeSnapshot / logLatency) or a full PendingSnapshotBody. */
|
|
1517
|
+
export function _seedFailedSnapshotPersistForTests(key, record) {
|
|
1518
|
+
_failedSnapshotPersists.set(key, record);
|
|
1519
|
+
}
|
|
1520
|
+
export function _failedSnapshotPersistKeysForTests() {
|
|
1521
|
+
return [..._failedSnapshotPersists.keys()];
|
|
1522
|
+
}
|
|
1473
1523
|
export function buildProjectSnapshotFromRuntime(args) {
|
|
1474
1524
|
return {
|
|
1475
1525
|
version: PROJECT_SNAPSHOT_VERSION,
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { extractTomlTableSection, hasTomlTable, parseTomlStringArray, } from "./cargo-manifest.js";
|
|
5
|
+
import { isAtOrAboveHomeDir, matchesWorkspaceMemberPattern, toPosix, UV_WORKSPACE_EXCLUDE_DIALECT, UV_WORKSPACE_MEMBERS_DIALECT, walkUpDirs, } from "./path-utils.js";
|
|
6
|
+
const UV_WORKSPACE_TABLE = "tool\\.uv\\.workspace";
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the uv workspace root using the same discovery shape as uv:
|
|
9
|
+
* start at the nearest pyproject, then continue upward for an explicit
|
|
10
|
+
* `[tool.uv.workspace]` declaration. A nearest pyproject without that table
|
|
11
|
+
* is an implicit single-project workspace.
|
|
12
|
+
*
|
|
13
|
+
* The walk stops AT `homeDir` (`isAtOrAboveHomeDir`, the shared ceiling
|
|
14
|
+
* primitive from #625) so a `pyproject.toml` sitting in `/tmp`, `/home`, or
|
|
15
|
+
* `$HOME` itself cannot supply the environment for every project beneath it.
|
|
16
|
+
* `homeDir` is injected rather than read from `os.homedir()` inside the walk
|
|
17
|
+
* for the same reason every sibling walker takes it (#2536, #2544 F2): the
|
|
18
|
+
* ceiling is otherwise untestable.
|
|
19
|
+
*/
|
|
20
|
+
async function findUvWorkspace(startDir, homeDir) {
|
|
21
|
+
const resolvedStart = path.resolve(startDir);
|
|
22
|
+
let nearestProject;
|
|
23
|
+
for (const dir of walkUpDirs(startDir)) {
|
|
24
|
+
if (isAtOrAboveHomeDir(dir, homeDir))
|
|
25
|
+
break;
|
|
26
|
+
let content;
|
|
27
|
+
try {
|
|
28
|
+
content = await readFile(path.join(dir, "pyproject.toml"), "utf8");
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (!nearestProject)
|
|
34
|
+
nearestProject = dir;
|
|
35
|
+
if (hasTomlTable(content, UV_WORKSPACE_TABLE)) {
|
|
36
|
+
const workspaceTable = extractTomlTableSection(content, UV_WORKSPACE_TABLE);
|
|
37
|
+
return {
|
|
38
|
+
root: dir,
|
|
39
|
+
explicit: true,
|
|
40
|
+
projectRoot: nearestProject,
|
|
41
|
+
isStartDir: nearestProject === resolvedStart,
|
|
42
|
+
members: parseTomlStringArray(workspaceTable, "members"),
|
|
43
|
+
exclude: parseTomlStringArray(workspaceTable, "exclude"),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return nearestProject
|
|
48
|
+
? {
|
|
49
|
+
root: nearestProject,
|
|
50
|
+
explicit: false,
|
|
51
|
+
projectRoot: nearestProject,
|
|
52
|
+
isStartDir: nearestProject === resolvedStart,
|
|
53
|
+
members: [],
|
|
54
|
+
exclude: [],
|
|
55
|
+
}
|
|
56
|
+
: undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Apply uv's explicit-workspace membership rules before inheriting its root
|
|
60
|
+
* environment. The workspace root is always a member; descendants must match
|
|
61
|
+
* a declared member glob and must not match an exclusion glob.
|
|
62
|
+
*
|
|
63
|
+
* The glob dialects are pinned to uv 3c979abda4530fe9bf3d92e9bcf5c5575e3b3126,
|
|
64
|
+
* `crates/uv-workspace/src/workspace.rs`, and live beside the ONE shared
|
|
65
|
+
* workspace-member matcher (#2591) instead of as a minimatch options block
|
|
66
|
+
* here: `UV_WORKSPACE_MEMBERS_DIALECT` for `members`
|
|
67
|
+
* (`is_included_in_workspace` — `MatchOptions { require_literal_separator:
|
|
68
|
+
* true, ..MatchOptions::new() }`) and `UV_WORKSPACE_EXCLUDE_DIALECT` for
|
|
69
|
+
* `exclude` (`WorkspaceExclusions::matches` — `Pattern::matches_path`, i.e.
|
|
70
|
+
* `MatchOptions::new()` defaults, where `require_literal_separator` is FALSE
|
|
71
|
+
* and a `*` therefore crosses `/`). Both normalize the pattern first
|
|
72
|
+
* (`normalize_path`, so a leading `./` is not part of the pattern) and are
|
|
73
|
+
* case-SENSITIVE on every platform with no literal-leading-dot requirement.
|
|
74
|
+
*
|
|
75
|
+
* That `exclude` option set used to be a documented limitation rather than
|
|
76
|
+
* behavior: minimatch cannot express a separator-crossing `*`, so
|
|
77
|
+
* `exclude = ['packages/a*c']` did not exclude `packages/a/b/c`. A dialect
|
|
78
|
+
* object can, so #2591 implements it — the one intentional answer change in
|
|
79
|
+
* that fold.
|
|
80
|
+
*/
|
|
81
|
+
function isUvWorkspaceMember(workspace, projectRoot) {
|
|
82
|
+
// No `projectRoot === workspace.root` shortcut. uv treats the workspace
|
|
83
|
+
// root as a member of its own workspace, but only in its capacity as a
|
|
84
|
+
// PROJECT — and a project already gets uv's settings through `isStartDir`,
|
|
85
|
+
// with its own `<root>/.venv` reached by the ordinary project candidate at
|
|
86
|
+
// the very same path. Answering "member" on directory equality made every
|
|
87
|
+
// non-project subdirectory of the workspace root inherit the workspace
|
|
88
|
+
// `.venv`, `exclude` included, because equality was tested before the
|
|
89
|
+
// walk result was consulted (review round 3, S2). The relative-path guard
|
|
90
|
+
// below returns false for the root itself, which is the intended answer.
|
|
91
|
+
const relative = toPosix(path.relative(workspace.root, projectRoot));
|
|
92
|
+
if (relative.length === 0 ||
|
|
93
|
+
relative === ".." ||
|
|
94
|
+
relative.startsWith("../") ||
|
|
95
|
+
path.isAbsolute(relative)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const matches = (dialect) => (pattern) => matchesWorkspaceMemberPattern(pattern, relative, dialect);
|
|
99
|
+
return (!workspace.exclude.some(matches(UV_WORKSPACE_EXCLUDE_DIALECT)) &&
|
|
100
|
+
workspace.members.some(matches(UV_WORKSPACE_MEMBERS_DIALECT)));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Resolve the interpreter and executable directory for the project's Python
|
|
104
|
+
* environment without activating it or invoking a package manager.
|
|
105
|
+
*/
|
|
106
|
+
export async function detectPythonEnvironment(projectRoot, homeDir = os.homedir()) {
|
|
107
|
+
// `path.resolve` once at the seam entry so `isStartDir` below compares like
|
|
108
|
+
// with like: `walkUpDirs` resolves its input, a caller's argument need not
|
|
109
|
+
// be normalized.
|
|
110
|
+
const root = path.resolve(projectRoot);
|
|
111
|
+
const uvWorkspace = await findUvWorkspace(root, homeDir);
|
|
112
|
+
// `UV_PROJECT_ENVIRONMENT` and the workspace `.venv` are uv PROJECT
|
|
113
|
+
// settings. uv applies them to a directory only when that directory IS the
|
|
114
|
+
// project — a `pyproject.toml` merely sitting somewhere ABOVE it makes it a
|
|
115
|
+
// subdirectory of a project, not a project, and nothing below a project
|
|
116
|
+
// root inherits from it (the F3 decision, applied consistently). Exporting
|
|
117
|
+
// `UV_PROJECT_ENVIRONMENT` process-wide is uv's own documented CI/Docker
|
|
118
|
+
// recipe, so a gate on "a pyproject exists at or above root" lets one
|
|
119
|
+
// image-level variable capture every subdirectory on the box that happens
|
|
120
|
+
// to sit under some Python project.
|
|
121
|
+
const isProjectRoot = uvWorkspace?.isStartDir === true;
|
|
122
|
+
// A declared, non-excluded member of an EXPLICIT workspace is itself a
|
|
123
|
+
// project root, so this is a strict refinement of `isProjectRoot`: it adds
|
|
124
|
+
// the workspace `.venv` candidate and re-anchors a relative
|
|
125
|
+
// `UV_PROJECT_ENVIRONMENT` at the workspace root.
|
|
126
|
+
const memberWorkspaceRoot = isProjectRoot &&
|
|
127
|
+
uvWorkspace.explicit &&
|
|
128
|
+
isUvWorkspaceMember(uvWorkspace, root)
|
|
129
|
+
? uvWorkspace.root
|
|
130
|
+
: undefined;
|
|
131
|
+
const uvEnvironmentRoot = isProjectRoot
|
|
132
|
+
? (memberWorkspaceRoot ?? uvWorkspace.projectRoot)
|
|
133
|
+
: undefined;
|
|
134
|
+
const uvProjectEnvironment = process.env.UV_PROJECT_ENVIRONMENT;
|
|
135
|
+
// PEP 723 `uv run --script` environments are cache-keyed by script content;
|
|
136
|
+
// without a stable project marker or explicit path, they remain undiscoverable.
|
|
137
|
+
const candidates = [
|
|
138
|
+
...(uvEnvironmentRoot !== undefined && uvProjectEnvironment
|
|
139
|
+
? [
|
|
140
|
+
{
|
|
141
|
+
// `path.resolve` leaves an absolute value untouched and
|
|
142
|
+
// anchors a relative one at the project's workspace root.
|
|
143
|
+
root: path.resolve(uvEnvironmentRoot, uvProjectEnvironment),
|
|
144
|
+
source: "uv-project-environment",
|
|
145
|
+
},
|
|
146
|
+
]
|
|
147
|
+
: []),
|
|
148
|
+
...(memberWorkspaceRoot !== undefined
|
|
149
|
+
? [
|
|
150
|
+
{
|
|
151
|
+
root: path.join(memberWorkspaceRoot, ".venv"),
|
|
152
|
+
source: "uv-workspace",
|
|
153
|
+
},
|
|
154
|
+
]
|
|
155
|
+
: []),
|
|
156
|
+
{ root: process.env.VIRTUAL_ENV, source: "virtual-env" },
|
|
157
|
+
{ root: process.env.CONDA_PREFIX, source: "conda" },
|
|
158
|
+
{ root: path.join(root, ".venv"), source: "project-dot-venv" },
|
|
159
|
+
{ root: path.join(root, "venv"), source: "project-venv" },
|
|
160
|
+
];
|
|
161
|
+
for (const candidate of candidates) {
|
|
162
|
+
if (!candidate.root)
|
|
163
|
+
continue;
|
|
164
|
+
const binDir = path.join(candidate.root, process.platform === "win32" ? "Scripts" : "bin");
|
|
165
|
+
const pythonPath = path.join(binDir, process.platform === "win32" ? "python.exe" : "python");
|
|
166
|
+
try {
|
|
167
|
+
await access(pythonPath);
|
|
168
|
+
return {
|
|
169
|
+
root: candidate.root,
|
|
170
|
+
binDir,
|
|
171
|
+
pythonPath,
|
|
172
|
+
source: candidate.source,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
// The marker can outlive its environment. Continue to the next candidate.
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
/** Preserve the existing interpreter-only API used by LSP initialization. */
|
|
182
|
+
export async function detectPythonVenv(projectRoot) {
|
|
183
|
+
return (await detectPythonEnvironment(projectRoot))?.pythonPath;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Build a child-only environment for Python tools. The host process remains
|
|
187
|
+
* unchanged, so another project can resolve a different environment.
|
|
188
|
+
*/
|
|
189
|
+
export function augmentPythonEnvironment(baseEnvironment, environment) {
|
|
190
|
+
if (!environment)
|
|
191
|
+
return baseEnvironment;
|
|
192
|
+
const inheritedPath = baseEnvironment.PATH ?? baseEnvironment.Path ?? baseEnvironment.path ?? "";
|
|
193
|
+
const augmentedPath = inheritedPath
|
|
194
|
+
? `${environment.binDir}${path.delimiter}${inheritedPath}`
|
|
195
|
+
: environment.binDir;
|
|
196
|
+
const childEnvironment = {
|
|
197
|
+
...baseEnvironment,
|
|
198
|
+
PATH: augmentedPath,
|
|
199
|
+
VIRTUAL_ENV: environment.root,
|
|
200
|
+
};
|
|
201
|
+
if (process.platform === "win32")
|
|
202
|
+
childEnvironment.Path = augmentedPath;
|
|
203
|
+
return childEnvironment;
|
|
204
|
+
}
|
|
205
|
+
/** Return explicit project-environment candidates before a bare PATH fallback. */
|
|
206
|
+
export function pythonEnvironmentToolCandidates(environment, command) {
|
|
207
|
+
if (!environment)
|
|
208
|
+
return [];
|
|
209
|
+
if (process.platform !== "win32") {
|
|
210
|
+
return [path.join(environment.binDir, command)];
|
|
211
|
+
}
|
|
212
|
+
return [
|
|
213
|
+
path.join(environment.binDir, `${command}.exe`),
|
|
214
|
+
path.join(environment.binDir, `${command}.cmd`),
|
|
215
|
+
path.join(environment.binDir, command),
|
|
216
|
+
];
|
|
217
|
+
}
|