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.
Files changed (359) hide show
  1. package/CHANGELOG.md +1308 -0
  2. package/README.md +3 -0
  3. package/config/biome/core.jsonc +11 -2
  4. package/config/dependency-cruiser-eager-allowlist.json +91 -0
  5. package/dist/clients/actionable-warnings-logger.js +18 -3
  6. package/dist/clients/actionable-warnings.js +927 -80
  7. package/dist/clients/agent-behavior-client.js +13 -4
  8. package/dist/clients/ast-grep-client.js +136 -9
  9. package/dist/clients/ast-grep-rule-manager.js +60 -5
  10. package/dist/clients/ast-grep-tool-logger.js +2 -2
  11. package/dist/clients/availability-probe-flight.js +12 -0
  12. package/dist/clients/bash-file-access.js +1 -3
  13. package/dist/clients/biome-client.js +16 -2
  14. package/dist/clients/blocker-freshness.js +34 -0
  15. package/dist/clients/bootstrap.js +509 -73
  16. package/dist/clients/bounded-cache.js +152 -12
  17. package/dist/clients/bounded-telemetry.js +115 -23
  18. package/dist/clients/build-identity.js +95 -0
  19. package/dist/clients/bundled-resource-health.js +113 -0
  20. package/dist/clients/bus-events-logger.js +2 -2
  21. package/dist/clients/bus-publish.js +3 -0
  22. package/dist/clients/cache/rule-cache.js +33 -5
  23. package/dist/clients/cache-manager.js +105 -6
  24. package/dist/clients/cache-observability.js +46 -19
  25. package/dist/clients/cargo-manifest.js +422 -0
  26. package/dist/clients/cascade-logger.js +17 -3
  27. package/dist/clients/code-quality-warnings.js +13 -3
  28. package/dist/clients/complexity-client.js +16 -5
  29. package/dist/clients/config-core/deny.js +221 -0
  30. package/dist/clients/config-core/index.js +47 -0
  31. package/dist/clients/config-core/merge.js +357 -0
  32. package/dist/clients/config-core/normalize.js +340 -0
  33. package/dist/clients/config-core/process-spec.js +248 -0
  34. package/dist/clients/config-core/provenance.js +171 -0
  35. package/dist/clients/config-core/records.js +218 -0
  36. package/dist/clients/config-core/resolve.js +125 -0
  37. package/dist/clients/config-core/safe-object.js +78 -0
  38. package/dist/clients/config-core/schema.js +167 -0
  39. package/dist/clients/config-diagnostic-codes.js +281 -0
  40. package/dist/clients/config-locations.js +160 -0
  41. package/dist/clients/config-resolve.js +789 -0
  42. package/dist/clients/config-schema.js +197 -0
  43. package/dist/clients/config-warn.js +407 -0
  44. package/dist/clients/dead-code-client.js +33 -25
  45. package/dist/clients/dead-code-logger.js +2 -2
  46. package/dist/clients/deadline-utils.js +178 -0
  47. package/dist/clients/debug-handles.js +2 -2
  48. package/dist/clients/debug-heap.js +3 -3
  49. package/dist/clients/deferred-lsp-work.js +106 -0
  50. package/dist/clients/degradation-ledger.js +166 -8
  51. package/dist/clients/demoted-finding-render.js +14 -0
  52. package/dist/clients/dependency-checker.js +28 -22
  53. package/dist/clients/deps/minimatch.js +1 -1
  54. package/dist/clients/diagnostic-line-freshness.js +12 -9
  55. package/dist/clients/diagnostic-logger.js +14 -4
  56. package/dist/clients/dispatch/collect-later-tier.js +39 -0
  57. package/dist/clients/dispatch/dispatcher.js +304 -19
  58. package/dist/clients/dispatch/fact-store.js +328 -9
  59. package/dist/clients/dispatch/integration.js +214 -107
  60. package/dist/clients/dispatch/pending-runner-findings.js +87 -0
  61. package/dist/clients/dispatch/plan.js +21 -5
  62. package/dist/clients/dispatch/runners/actionlint.js +0 -1
  63. package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
  64. package/dist/clients/dispatch/runners/biome-check.js +0 -1
  65. package/dist/clients/dispatch/runners/cpp-check.js +0 -1
  66. package/dist/clients/dispatch/runners/credo.js +0 -1
  67. package/dist/clients/dispatch/runners/cue-vet.js +0 -1
  68. package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
  69. package/dist/clients/dispatch/runners/detekt.js +0 -1
  70. package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
  71. package/dist/clients/dispatch/runners/elixir-check.js +0 -1
  72. package/dist/clients/dispatch/runners/eslint.js +5 -2
  73. package/dist/clients/dispatch/runners/fact-rules.js +0 -1
  74. package/dist/clients/dispatch/runners/fish-indent.js +0 -1
  75. package/dist/clients/dispatch/runners/gleam-check.js +0 -1
  76. package/dist/clients/dispatch/runners/go-vet.js +1 -3
  77. package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
  78. package/dist/clients/dispatch/runners/hadolint.js +0 -1
  79. package/dist/clients/dispatch/runners/helm-lint.js +16 -4
  80. package/dist/clients/dispatch/runners/helm-render.js +54 -10
  81. package/dist/clients/dispatch/runners/htmlhint.js +0 -1
  82. package/dist/clients/dispatch/runners/index.js +1 -3
  83. package/dist/clients/dispatch/runners/javac.js +0 -1
  84. package/dist/clients/dispatch/runners/ktlint.js +0 -1
  85. package/dist/clients/dispatch/runners/lsp.js +37 -2
  86. package/dist/clients/dispatch/runners/markdownlint.js +4 -2
  87. package/dist/clients/dispatch/runners/mypy.js +0 -1
  88. package/dist/clients/dispatch/runners/oxlint.js +24 -22
  89. package/dist/clients/dispatch/runners/php-lint.js +0 -1
  90. package/dist/clients/dispatch/runners/phpstan.js +0 -1
  91. package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
  92. package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
  93. package/dist/clients/dispatch/runners/pyright.js +7 -4
  94. package/dist/clients/dispatch/runners/rubocop.js +0 -1
  95. package/dist/clients/dispatch/runners/ruff.js +0 -1
  96. package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
  97. package/dist/clients/dispatch/runners/shellcheck.js +0 -1
  98. package/dist/clients/dispatch/runners/shfmt.js +0 -1
  99. package/dist/clients/dispatch/runners/spellcheck.js +0 -1
  100. package/dist/clients/dispatch/runners/spotbugs.js +0 -1
  101. package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
  102. package/dist/clients/dispatch/runners/stylelint.js +0 -1
  103. package/dist/clients/dispatch/runners/swiftlint.js +0 -1
  104. package/dist/clients/dispatch/runners/taplo.js +0 -1
  105. package/dist/clients/dispatch/runners/terragrunt.js +0 -1
  106. package/dist/clients/dispatch/runners/tflint.js +0 -1
  107. package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
  108. package/dist/clients/dispatch/runners/trivy-config.js +0 -1
  109. package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
  110. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
  111. package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
  112. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
  113. package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
  114. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
  115. package/dist/clients/dispatch/runners/vale.js +0 -1
  116. package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
  117. package/dist/clients/dispatch/runners/yamllint.js +0 -1
  118. package/dist/clients/dispatch/runners/zig-check.js +0 -1
  119. package/dist/clients/disposition-logger.js +3 -3
  120. package/dist/clients/effective-config.js +403 -0
  121. package/dist/clients/error-class.js +23 -0
  122. package/dist/clients/event-loop-hold.js +274 -0
  123. package/dist/clients/event-loop-monitor.js +66 -1
  124. package/dist/clients/extension-log.js +2 -2
  125. package/dist/clients/feature-hints.js +2 -1
  126. package/dist/clients/file-time.js +6 -1
  127. package/dist/clients/file-utils.js +266 -13
  128. package/dist/clients/finding-delivery-gate.js +55 -13
  129. package/dist/clients/format-service.js +6 -1
  130. package/dist/clients/formatters.js +303 -104
  131. package/dist/clients/freshness-cadence.js +17 -0
  132. package/dist/clients/generated-artifacts.js +114 -19
  133. package/dist/clients/generation-guard.js +4 -9
  134. package/dist/clients/git-guard.js +198 -48
  135. package/dist/clients/git-tracked-ignore.js +32 -1
  136. package/dist/clients/go-client.js +39 -0
  137. package/dist/clients/govulncheck-client.js +5 -1
  138. package/dist/clients/gradle-ktfmt-style.js +252 -0
  139. package/dist/clients/gzip-stage-write.js +7 -0
  140. package/dist/clients/hashline-anchor.js +424 -0
  141. package/dist/clients/hook-budgets.js +76 -0
  142. package/dist/clients/host-edit-normalize.js +5 -2
  143. package/dist/clients/host-ports.js +1 -1
  144. package/dist/clients/installer/index.js +200 -84
  145. package/dist/clients/installer/managed-tool-refresh.js +3 -1
  146. package/dist/clients/instance-reaper.js +108 -243
  147. package/dist/clients/instance-registry-lock.js +145 -0
  148. package/dist/clients/instance-registry.js +435 -122
  149. package/dist/clients/jscpd-client.js +6 -1
  150. package/dist/clients/language-profile.js +4 -2
  151. package/dist/clients/language-registry.js +595 -0
  152. package/dist/clients/latency-logger.js +179 -3
  153. package/dist/clients/ledger-bounds.js +13 -0
  154. package/dist/clients/lens-config.js +141 -30
  155. package/dist/clients/lens-engine.js +30 -37
  156. package/dist/clients/lens-flag-registry.js +67 -6
  157. package/dist/clients/lens-map.js +3 -1
  158. package/dist/clients/log-cleanup.js +2 -2
  159. package/dist/clients/lsp/aggregation.js +3 -1
  160. package/dist/clients/lsp/cascade-tier.js +4 -0
  161. package/dist/clients/lsp/client.js +672 -49
  162. package/dist/clients/lsp/config.js +504 -156
  163. package/dist/clients/lsp/diagnostic-binding.js +10 -5
  164. package/dist/clients/lsp/edits.js +15 -2
  165. package/dist/clients/lsp/index.js +1179 -247
  166. package/dist/clients/lsp/jvm-runtime.js +5 -0
  167. package/dist/clients/lsp/language.js +18 -169
  168. package/dist/clients/lsp/launch.js +38 -9
  169. package/dist/clients/lsp/pending-aux-coverage.js +113 -12
  170. package/dist/clients/lsp/server.js +426 -160
  171. package/dist/clients/lsp/session-roots.js +192 -0
  172. package/dist/clients/lsp/spawn-history.js +2 -7
  173. package/dist/clients/lsp/tsserver-sync.js +9 -2
  174. package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
  175. package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
  176. package/dist/clients/lsp-mutation.js +179 -23
  177. package/dist/clients/map-with-concurrency.js +36 -0
  178. package/dist/clients/mcp/analyze.js +45 -10
  179. package/dist/clients/mcp/session.js +19 -17
  180. package/dist/clients/memory-sampler.js +28 -1
  181. package/dist/clients/message-end-attribution.js +30 -0
  182. package/dist/clients/metrics-history.js +21 -4
  183. package/dist/clients/middle-man-analysis.js +2 -4
  184. package/dist/clients/module-report.js +20 -45
  185. package/dist/clients/mutating-tool.js +651 -0
  186. package/dist/clients/mutation-attribution.js +368 -0
  187. package/dist/clients/mutation-bridge.js +240 -0
  188. package/dist/clients/ndjson-logger.js +247 -20
  189. package/dist/clients/observed-mutation-sources.js +101 -0
  190. package/dist/clients/observed-mutation.js +1215 -0
  191. package/dist/clients/opaque-mutation-scan.js +300 -40
  192. package/dist/clients/opengrep-config.js +4 -0
  193. package/dist/clients/package-manager.js +201 -44
  194. package/dist/clients/partial-edit-apply.js +359 -80
  195. package/dist/clients/path-attribution-telemetry.js +23 -7
  196. package/dist/clients/path-keyed-map.js +35 -3
  197. package/dist/clients/path-utils.js +460 -13
  198. package/dist/clients/persist-debounce.js +8 -1
  199. package/dist/clients/php-cs-fixer-config.js +114 -0
  200. package/dist/clients/pipeline.js +77 -24
  201. package/dist/clients/probe-home-state.js +228 -0
  202. package/dist/clients/process-bridge.js +66 -0
  203. package/dist/clients/process-singletons.js +210 -0
  204. package/dist/clients/process-snapshot.js +68 -0
  205. package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
  206. package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
  207. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
  208. package/dist/clients/project-diagnostics/scanner.js +61 -24
  209. package/dist/clients/project-lens-config.js +393 -122
  210. package/dist/clients/project-snapshot.js +85 -35
  211. package/dist/clients/python-environment.js +217 -0
  212. package/dist/clients/python-provenance.js +639 -0
  213. package/dist/clients/quiet-window.js +6 -1
  214. package/dist/clients/read-bridge.js +13 -21
  215. package/dist/clients/read-expansion.js +9 -46
  216. package/dist/clients/read-guard-logger.js +39 -6
  217. package/dist/clients/read-guard-tool-lines.js +364 -212
  218. package/dist/clients/read-guard.js +95 -7
  219. package/dist/clients/recent-touches.js +17 -5
  220. package/dist/clients/resource-sampler.js +195 -96
  221. package/dist/clients/review-graph/builder.js +752 -132
  222. package/dist/clients/review-graph/service.js +55 -3
  223. package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
  224. package/dist/clients/review-graph/tsconfig-paths.js +5 -1
  225. package/dist/clients/review-graph/workspace-modules.js +10 -74
  226. package/dist/clients/review-graph-logger.js +30 -4
  227. package/dist/clients/runtime-agent-end.js +18 -2
  228. package/dist/clients/runtime-context.js +49 -16
  229. package/dist/clients/runtime-coordinator.js +93 -6
  230. package/dist/clients/runtime-session.js +169 -34
  231. package/dist/clients/runtime-tool-call.js +183 -35
  232. package/dist/clients/runtime-tool-result.js +894 -241
  233. package/dist/clients/runtime-turn.js +1264 -74
  234. package/dist/clients/rust-client.js +31 -0
  235. package/dist/clients/safe-spawn.js +191 -28
  236. package/dist/clients/sanitize.js +7 -1
  237. package/dist/clients/scratch-tree-policy.js +1 -3
  238. package/dist/clients/security-scan-client.js +51 -17
  239. package/dist/clients/session-lifecycle.js +238 -42
  240. package/dist/clients/session-start-observability.js +79 -0
  241. package/dist/clients/sessionstart-logger.js +12 -3
  242. package/dist/clients/sg-runner.js +27 -15
  243. package/dist/clients/sgconfig.js +6 -1
  244. package/dist/clients/shared-checkout-guard.js +338 -0
  245. package/dist/clients/skills-resolver.js +105 -0
  246. package/dist/clients/smells-rollup.js +2 -2
  247. package/dist/clients/spawn-output-cap.js +37 -0
  248. package/dist/clients/spawn-timeout-cooldown.js +15 -1
  249. package/dist/clients/startup-scan.js +6 -7
  250. package/dist/clients/startup-timing.js +69 -4
  251. package/dist/clients/string-utils.js +33 -0
  252. package/dist/clients/subagent-mode.js +17 -4
  253. package/dist/clients/test-runner-client.js +586 -118
  254. package/dist/clients/test-runner-delivery.js +239 -0
  255. package/dist/clients/tool-definition.js +41 -1
  256. package/dist/clients/tool-policy.js +92 -22
  257. package/dist/clients/tree-sitter-cache.js +40 -40
  258. package/dist/clients/tree-sitter-client.js +97 -53
  259. package/dist/clients/tree-sitter-logger.js +19 -3
  260. package/dist/clients/tree-sitter-query-loader.js +81 -1
  261. package/dist/clients/tree-sitter-shared.js +51 -46
  262. package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
  263. package/dist/clients/typos-config.js +5 -0
  264. package/dist/clients/user-notify.js +6 -2
  265. package/dist/clients/warm-attach.js +8 -10
  266. package/dist/clients/widget-state.js +191 -26
  267. package/dist/clients/word-index-logger.js +15 -3
  268. package/dist/clients/word-index-store.js +463 -0
  269. package/dist/clients/word-index.js +815 -125
  270. package/dist/clients/workspace-topology.js +25 -1
  271. package/dist/clients/zizmor-config.js +29 -6
  272. package/dist/index.js +68577 -53950
  273. package/dist/mcp/analyze-cli.js +3 -1
  274. package/dist/mcp/build-staleness.js +8 -7
  275. package/dist/mcp/cli.js +3 -1
  276. package/dist/mcp/server.js +133 -6
  277. package/dist/scripts/lib/process-scan.mjs +583 -0
  278. package/dist/scripts/lib/skills-predicate.mjs +129 -0
  279. package/dist/tools/effective-config.js +89 -0
  280. package/dist/tools/lens-diagnostics.js +99 -24
  281. package/dist/tools/lsp-diagnostics.js +81 -85
  282. package/dist/tools/lsp-navigation.js +40 -17
  283. package/dist/tools/shared.js +0 -1
  284. package/docs/agent-guide.md +68 -3
  285. package/docs/ast-grep_rules_catalog.md +41 -10
  286. package/docs/astplayground.md +26 -17
  287. package/docs/configuration.md +222 -0
  288. package/docs/dependencies.md +3 -3
  289. package/docs/features.md +44 -5
  290. package/docs/language-coverage.md +2 -2
  291. package/docs/pi-lens-fixer.md +25 -0
  292. package/docs/pi-lens-investigator.md +25 -0
  293. package/docs/pi-lens-reviewer.md +27 -0
  294. package/docs/pi-lens-subagent.md +38 -0
  295. package/docs/pi-lens-warden.md +55 -0
  296. package/docs/public-api-stability.md +359 -0
  297. package/docs/release-qa-baseline.md +182 -0
  298. package/docs/subagent-compat.md +110 -30
  299. package/docs/tree-sitter_rules_catalog.md +2 -2
  300. package/package.json +17 -21
  301. package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
  302. package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
  303. package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
  304. package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
  305. package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
  306. package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
  307. package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
  308. package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
  309. package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
  310. package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
  311. package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
  312. package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
  313. package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
  314. package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
  315. package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
  316. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
  317. package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
  318. package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
  319. package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
  320. package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
  321. package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
  322. package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
  323. package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
  324. package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
  325. package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
  326. package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
  327. package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
  328. package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
  329. package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
  330. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
  331. package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
  332. package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
  333. package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
  334. package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
  335. package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
  336. package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
  337. package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
  338. package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
  339. package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
  340. package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
  341. package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
  342. package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
  343. package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
  344. package/rules/ast-grep-rules/rules/no-important.yml +21 -0
  345. package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
  346. package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
  347. package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
  348. package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
  349. package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
  350. package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
  351. package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
  352. package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
  353. package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
  354. package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
  355. package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
  356. package/scripts/analyze-pi-lens-logs.mjs +192 -1
  357. package/scripts/install-selftest.mjs +58 -2
  358. package/scripts/lib/skills-predicate.mjs +129 -0
  359. 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 Map();
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 (Map preserves insertion order).
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
- const diskMtime = body ? body.mtimeMs : Number.NEGATIVE_INFINITY;
537
- if (diskMtime <= authoritative.knownMtime) {
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
- // mtime. `body === null` means nothing is on disk yet — our just-scheduled
552
- // write is the only truth, so serve it.
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 diskMtime = body ? body.mtimeMs : Number.NEGATIVE_INFINITY;
556
- if (diskMtime <= authoritative.knownMtime) {
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 moved past our write — honor disk and stop
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 Map();
609
- const _failedSnapshotPersists = new Map();
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
- entry.knownMtime = fs.statSync(pending.gzPath).mtimeMs;
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 knownMtime as-is; the worst case
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
+ }