@herbertgao/pi-extensions 2026.9.0 → 2026.9.1
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/node_modules/@czottmann/pi-automode/CHANGELOG.md +13 -1
- package/node_modules/@czottmann/pi-automode/README.md +20 -0
- package/node_modules/@czottmann/pi-automode/docs/GLOSSARY.md +1 -1
- package/node_modules/@czottmann/pi-automode/docs/automode-classifier-flow.md +2 -1
- package/node_modules/@czottmann/pi-automode/docs/defaults.md +3 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +38 -2
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +5 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/extension.ts +11 -3
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@pi-plugins/fast-mode/README.md +5 -3
- package/node_modules/@pi-plugins/fast-mode/dist/index.d.mts.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +20 -55
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +1 -1
- package/node_modules/pi-lens/CHANGELOG.md +988 -0
- package/node_modules/pi-lens/README.md +3 -0
- package/node_modules/pi-lens/config/biome/core.jsonc +11 -2
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +13 -1
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +927 -80
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +13 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +123 -3
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +60 -5
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +1 -3
- package/node_modules/pi-lens/dist/clients/biome-client.js +9 -2
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +14 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +509 -73
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +152 -12
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +59 -4
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +113 -0
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +31 -4
- package/node_modules/pi-lens/dist/clients/cache-manager.js +105 -6
- package/node_modules/pi-lens/dist/clients/cache-observability.js +44 -18
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +422 -0
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +13 -3
- package/node_modules/pi-lens/dist/clients/complexity-client.js +16 -5
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +221 -0
- package/node_modules/pi-lens/dist/clients/config-core/index.js +47 -0
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +357 -0
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +340 -0
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +248 -0
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +171 -0
- package/node_modules/pi-lens/dist/clients/config-core/records.js +218 -0
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +125 -0
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +78 -0
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +167 -0
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +281 -0
- package/node_modules/pi-lens/dist/clients/config-locations.js +160 -0
- package/node_modules/pi-lens/dist/clients/config-resolve.js +789 -0
- package/node_modules/pi-lens/dist/clients/config-schema.js +197 -0
- package/node_modules/pi-lens/dist/clients/config-warn.js +407 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +19 -14
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +178 -0
- package/node_modules/pi-lens/dist/clients/debug-handles.js +2 -2
- package/node_modules/pi-lens/dist/clients/debug-heap.js +3 -3
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +106 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +113 -7
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +8 -19
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +12 -9
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +162 -18
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +380 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +13 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +194 -80
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +84 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +3 -3
- package/node_modules/pi-lens/dist/clients/effective-config.js +403 -0
- package/node_modules/pi-lens/dist/clients/error-class.js +23 -0
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +274 -0
- package/node_modules/pi-lens/dist/clients/extension-log.js +2 -2
- package/node_modules/pi-lens/dist/clients/feature-hints.js +2 -1
- package/node_modules/pi-lens/dist/clients/file-time.js +6 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +112 -9
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +50 -13
- package/node_modules/pi-lens/dist/clients/format-service.js +6 -1
- package/node_modules/pi-lens/dist/clients/formatters.js +276 -102
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +17 -0
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +114 -19
- package/node_modules/pi-lens/dist/clients/generation-guard.js +4 -9
- package/node_modules/pi-lens/dist/clients/git-guard.js +1 -1
- package/node_modules/pi-lens/dist/clients/go-client.js +39 -0
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +252 -0
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +424 -0
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +76 -0
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +5 -2
- package/node_modules/pi-lens/dist/clients/host-ports.js +1 -1
- package/node_modules/pi-lens/dist/clients/installer/index.js +71 -52
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +104 -230
- package/node_modules/pi-lens/dist/clients/instance-registry.js +6 -3
- package/node_modules/pi-lens/dist/clients/language-registry.js +595 -0
- package/node_modules/pi-lens/dist/clients/latency-logger.js +160 -2
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +13 -0
- package/node_modules/pi-lens/dist/clients/lens-config.js +141 -30
- package/node_modules/pi-lens/dist/clients/lens-engine.js +30 -37
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +59 -6
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +168 -30
- package/node_modules/pi-lens/dist/clients/lsp/config.js +483 -158
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +15 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +676 -71
- package/node_modules/pi-lens/dist/clients/lsp/language.js +18 -169
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +5 -16
- package/node_modules/pi-lens/dist/clients/lsp/server.js +284 -147
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +116 -23
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +2 -7
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +9 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +163 -21
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +36 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +33 -4
- package/node_modules/pi-lens/dist/clients/mcp/session.js +5 -16
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +8 -3
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +2 -4
- package/node_modules/pi-lens/dist/clients/module-report.js +20 -45
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +651 -0
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +368 -0
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +240 -0
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +247 -20
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +101 -0
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +1215 -0
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +68 -22
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +4 -0
- package/node_modules/pi-lens/dist/clients/package-manager.js +195 -26
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +359 -80
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +23 -7
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +21 -2
- package/node_modules/pi-lens/dist/clients/path-utils.js +428 -13
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +8 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +114 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +77 -16
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +228 -0
- package/node_modules/pi-lens/dist/clients/process-bridge.js +66 -0
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +68 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +51 -20
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +393 -122
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +52 -24
- package/node_modules/pi-lens/dist/clients/python-environment.js +217 -0
- package/node_modules/pi-lens/dist/clients/python-provenance.js +639 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +6 -1
- package/node_modules/pi-lens/dist/clients/read-bridge.js +13 -21
- package/node_modules/pi-lens/dist/clients/read-expansion.js +9 -46
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +15 -4
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +364 -212
- package/node_modules/pi-lens/dist/clients/read-guard.js +21 -0
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +195 -96
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +152 -109
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +55 -3
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +10 -74
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +9 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +18 -2
- package/node_modules/pi-lens/dist/clients/runtime-context.js +49 -16
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +25 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +139 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +168 -35
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +797 -243
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +1028 -53
- package/node_modules/pi-lens/dist/clients/rust-client.js +31 -0
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +33 -10
- package/node_modules/pi-lens/dist/clients/sanitize.js +7 -1
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +1 -3
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +3 -0
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +1 -1
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +79 -0
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +12 -3
- package/node_modules/pi-lens/dist/clients/sgconfig.js +6 -1
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +105 -0
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +2 -2
- package/node_modules/pi-lens/dist/clients/string-utils.js +13 -0
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +17 -4
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +176 -39
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +239 -0
- package/node_modules/pi-lens/dist/clients/tool-definition.js +41 -1
- package/node_modules/pi-lens/dist/clients/tool-policy.js +91 -21
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +40 -40
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +86 -48
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +81 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +51 -46
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/node_modules/pi-lens/dist/clients/typos-config.js +5 -0
- package/node_modules/pi-lens/dist/clients/user-notify.js +6 -2
- package/node_modules/pi-lens/dist/clients/widget-state.js +191 -26
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +2 -2
- package/node_modules/pi-lens/dist/clients/word-index-store.js +20 -5
- package/node_modules/pi-lens/dist/clients/word-index.js +243 -31
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +8 -1
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +3 -0
- package/node_modules/pi-lens/dist/index.js +64335 -54300
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +127 -5
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +583 -0
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/dist/tools/effective-config.js +89 -0
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +95 -24
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +67 -80
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +37 -16
- package/node_modules/pi-lens/dist/tools/shared.js +0 -1
- package/node_modules/pi-lens/docs/agent-guide.md +64 -1
- package/node_modules/pi-lens/docs/configuration.md +222 -0
- package/node_modules/pi-lens/docs/dependencies.md +3 -3
- package/node_modules/pi-lens/docs/features.md +44 -5
- package/node_modules/pi-lens/docs/language-coverage.md +2 -2
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +25 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +27 -0
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +38 -0
- package/node_modules/pi-lens/docs/pi-lens-warden.md +55 -0
- package/node_modules/pi-lens/docs/public-api-stability.md +359 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +182 -0
- package/node_modules/pi-lens/docs/subagent-compat.md +110 -30
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +2 -2
- package/node_modules/pi-lens/package.json +15 -23
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/node_modules/pi-lens/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +58 -2
- package/node_modules/pi-lens/scripts/lib/skills-predicate.mjs +129 -0
- package/node_modules/pi-lens/scripts/rpc-load-check.mjs +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +62 -15
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +5 -1
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +242 -1
- package/node_modules/pi-web-access/index.ts +118 -64
- package/node_modules/pi-web-access/mistral-search.ts +281 -0
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/perplexity.ts +14 -1
- package/node_modules/pi-web-access/utils.ts +23 -6
- package/node_modules/pi-web-access/xai-search.ts +96 -33
- package/package.json +5 -5
|
@@ -13,13 +13,18 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
13
13
|
import * as fs from "node:fs/promises";
|
|
14
14
|
import * as path from "node:path";
|
|
15
15
|
import { BoundedLruCache } from "./bounded-cache.js";
|
|
16
|
+
import { createGenerationSource } from "./generation-guard.js";
|
|
16
17
|
import { normalizeMapKey } from "./path-utils.js";
|
|
18
|
+
import { resolveCargoPackageEdition } from "./cargo-manifest.js";
|
|
19
|
+
import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
|
|
20
|
+
import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
|
|
17
21
|
import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
|
|
22
|
+
import { stripAnsi } from "./sanitize.js";
|
|
18
23
|
import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
|
|
19
24
|
import { logLatency } from "./latency-logger.js";
|
|
20
25
|
import { compareOrdinal } from "./string-utils.js";
|
|
21
26
|
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
22
|
-
import { findGlobalBinary, findLocalBinUpwards } from "./package-manager.js";
|
|
27
|
+
import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
|
|
23
28
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
24
29
|
import { assertInstallAllowed } from "./project-trust.js";
|
|
25
30
|
import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
|
|
@@ -49,6 +54,19 @@ export async function tryLazyInstallFormatterTool(tool, cwd) {
|
|
|
49
54
|
* stock command there is exactly the stock-style imposition the fix bans.
|
|
50
55
|
*/
|
|
51
56
|
export const SKIP_FORMATTING = "skip-formatting";
|
|
57
|
+
/**
|
|
58
|
+
* Sentinel a `resolveCommand` returns when it has PROVEN the selected
|
|
59
|
+
* formatter has no executable — the binary is absent from every place the
|
|
60
|
+
* resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
|
|
61
|
+
* no alternative resolved (#2413). Distinct from `null` ("no override, fall
|
|
62
|
+
* back to the static `command`") and from SKIP_FORMATTING ("style-preserving
|
|
63
|
+
* refusal"): the static fallback for these formatters is the SAME bare binary
|
|
64
|
+
* the resolver just proved missing, so spawning it only re-triggers the ENOENT
|
|
65
|
+
* the resolver already observed — and reporting that as a formatting failure is
|
|
66
|
+
* the exact defect this fixes (recurring defect shape 10: an unavailable
|
|
67
|
+
* producer collapsed into an ordinary failed result).
|
|
68
|
+
*/
|
|
69
|
+
export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
|
|
52
70
|
// --- Utility Functions ---
|
|
53
71
|
async function fileExists(filePath) {
|
|
54
72
|
try {
|
|
@@ -63,12 +81,47 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
|
63
81
|
const found = [];
|
|
64
82
|
let currentDir = startDir;
|
|
65
83
|
while (currentDir !== stopDir) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
// One `readdir` per directory instead of one `access` per target: the
|
|
85
|
+
// per-target probe made this loop's cost O(targets), so a long target
|
|
86
|
+
// list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
|
|
87
|
+
// ancestor directory even when almost none of them exist (#1603).
|
|
88
|
+
let entries;
|
|
89
|
+
try {
|
|
90
|
+
entries = await fs.readdir(currentDir);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
entries = [];
|
|
94
|
+
}
|
|
95
|
+
if (entries.length > 0) {
|
|
96
|
+
// On default-case-insensitive platforms the old `fs.access` probe
|
|
97
|
+
// matched a config file regardless of on-disk casing; fold so the
|
|
98
|
+
// readdir membership check keeps that behavior.
|
|
99
|
+
const foldCase = process.platform === "win32" || process.platform === "darwin";
|
|
100
|
+
const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
|
|
101
|
+
for (const target of targets) {
|
|
102
|
+
// Nested candidates (for example node_modules/.bin/biome) do
|
|
103
|
+
// not appear as direct readdir members. They retain the old
|
|
104
|
+
// bounded candidate probe; direct names use the O(depth + matches)
|
|
105
|
+
// directory-membership path above.
|
|
106
|
+
if (target.includes("/") || target.includes("\\"))
|
|
107
|
+
continue;
|
|
108
|
+
if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
|
|
109
|
+
const candidate = path.join(currentDir, target);
|
|
110
|
+
// Directory membership is only a cheap candidate filter. Keep the
|
|
111
|
+
// old access check for matched entries so dangling links and entries
|
|
112
|
+
// that cannot be read do not become config evidence (#1603 R2).
|
|
113
|
+
if (await fileExists(candidate))
|
|
114
|
+
found.push(candidate);
|
|
115
|
+
}
|
|
70
116
|
}
|
|
71
117
|
}
|
|
118
|
+
for (const target of targets) {
|
|
119
|
+
if (!target.includes("/") && !target.includes("\\"))
|
|
120
|
+
continue;
|
|
121
|
+
const candidate = path.join(currentDir, target);
|
|
122
|
+
if (await fileExists(candidate))
|
|
123
|
+
found.push(candidate);
|
|
124
|
+
}
|
|
72
125
|
const parent = path.dirname(currentDir);
|
|
73
126
|
if (parent === currentDir)
|
|
74
127
|
break;
|
|
@@ -76,6 +129,10 @@ async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
|
|
|
76
129
|
}
|
|
77
130
|
return found;
|
|
78
131
|
}
|
|
132
|
+
/** Test-only access to the real filesystem walker and its candidate filter. */
|
|
133
|
+
export function _findUpForTests(targets, startDir, stopDir) {
|
|
134
|
+
return findUp(targets, startDir, stopDir);
|
|
135
|
+
}
|
|
79
136
|
const WHICH_BUDGET_MS = 5000;
|
|
80
137
|
/**
|
|
81
138
|
* PATH lookups, governed by the shared availability policy (#1495).
|
|
@@ -337,79 +394,58 @@ async function resolveGoFmtBinary() {
|
|
|
337
394
|
/**
|
|
338
395
|
* Walk up from cwd looking for a binary in .venv or venv.
|
|
339
396
|
* Returns the absolute path if found, null otherwise.
|
|
397
|
+
*
|
|
398
|
+
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
399
|
+
* the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
|
|
400
|
+
* bin found at or above HOME can never be THIS project's own virtualenv. The
|
|
401
|
+
* ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
|
|
402
|
+
* that pin a fake HOME drive that walker directly rather than threading a
|
|
403
|
+
* `homeDir` no production caller ever passed (#2544 review F3).
|
|
404
|
+
*
|
|
405
|
+
* On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
|
|
406
|
+
* `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
|
|
407
|
+
* did and what `findLocalBinUpwards` preserves.
|
|
340
408
|
*/
|
|
341
409
|
async function findInVenv(binary, cwd) {
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
`venv/Scripts/${binary}.exe`,
|
|
347
|
-
`.venv/Scripts/${binary}`,
|
|
348
|
-
`venv/Scripts/${binary}`,
|
|
349
|
-
]
|
|
350
|
-
: [`.venv/bin/${binary}`, `venv/bin/${binary}`];
|
|
351
|
-
let dir = cwd;
|
|
352
|
-
const root = path.parse(dir).root;
|
|
353
|
-
while (dir !== root) {
|
|
354
|
-
for (const candidate of candidates) {
|
|
355
|
-
const full = path.join(dir, candidate);
|
|
356
|
-
if (await fileExists(full))
|
|
357
|
-
return full;
|
|
358
|
-
}
|
|
359
|
-
const parent = path.dirname(dir);
|
|
360
|
-
if (parent === dir)
|
|
361
|
-
break;
|
|
362
|
-
dir = parent;
|
|
363
|
-
}
|
|
364
|
-
return null;
|
|
410
|
+
return (findLocalBinUpwards(binary, cwd, {
|
|
411
|
+
windowsExt: ".exe",
|
|
412
|
+
binDirs: VENV_BIN_DIRS,
|
|
413
|
+
}) ?? null);
|
|
365
414
|
}
|
|
366
415
|
/**
|
|
367
416
|
* Check vendor/bin for PHP Composer-managed tools.
|
|
368
417
|
* Walks up from cwd to find vendor/bin/<binary>.
|
|
418
|
+
*
|
|
419
|
+
* Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
|
|
420
|
+
* the project upward past HOME means STOP, not keep reading — a `vendor/bin`
|
|
421
|
+
* match found at or above HOME can never be THIS project's own Composer
|
|
422
|
+
* install. Same single walker as `findInVenv` above; only `binDirs` and the
|
|
423
|
+
* Windows extension differ.
|
|
369
424
|
*/
|
|
370
425
|
async function findInVendorBin(binary, cwd) {
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
while (dir !== root) {
|
|
376
|
-
for (const name of names) {
|
|
377
|
-
const full = path.join(dir, "vendor", "bin", name);
|
|
378
|
-
if (await fileExists(full))
|
|
379
|
-
return full;
|
|
380
|
-
}
|
|
381
|
-
const parent = path.dirname(dir);
|
|
382
|
-
if (parent === dir)
|
|
383
|
-
break;
|
|
384
|
-
dir = parent;
|
|
385
|
-
}
|
|
386
|
-
return null;
|
|
426
|
+
return (findLocalBinUpwards(binary, cwd, {
|
|
427
|
+
windowsExt: ".bat",
|
|
428
|
+
binDirs: VENDOR_BIN_DIRS,
|
|
429
|
+
}) ?? null);
|
|
387
430
|
}
|
|
388
431
|
/**
|
|
389
432
|
* Check node_modules/.bin for locally installed Node tools.
|
|
390
433
|
* Walks up from cwd to find node_modules/.bin/<binary>.
|
|
434
|
+
*
|
|
435
|
+
* #2514: this WAS a private, byte-for-byte duplicate of
|
|
436
|
+
* `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
|
|
437
|
+
* below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
|
|
438
|
+
* `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
|
|
439
|
+
* `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
|
|
440
|
+
* installs its own bins) was picked up as the project's formatter because
|
|
441
|
+
* this copy had no HOME ceiling; fixing only this copy would have left every
|
|
442
|
+
* other caller of `findLocalBinUpwards` with the same defect. Delegating
|
|
443
|
+
* here instead of hand-rolling a second ceiling keeps there being exactly one
|
|
444
|
+
* `node_modules/.bin` walker — see that function for the tool-resolution
|
|
445
|
+
* "escaping the project means STOP" policy (#2517).
|
|
391
446
|
*/
|
|
392
447
|
async function findInNodeModules(binary, cwd) {
|
|
393
|
-
|
|
394
|
-
let dir = cwd;
|
|
395
|
-
const root = path.parse(dir).root;
|
|
396
|
-
while (dir !== root) {
|
|
397
|
-
const candidates = isWin
|
|
398
|
-
? [
|
|
399
|
-
path.join(dir, "node_modules", ".bin", `${binary}.cmd`),
|
|
400
|
-
path.join(dir, "node_modules", ".bin", binary),
|
|
401
|
-
]
|
|
402
|
-
: [path.join(dir, "node_modules", ".bin", binary)];
|
|
403
|
-
for (const full of candidates) {
|
|
404
|
-
if (await fileExists(full))
|
|
405
|
-
return full;
|
|
406
|
-
}
|
|
407
|
-
const parent = path.dirname(dir);
|
|
408
|
-
if (parent === dir)
|
|
409
|
-
break;
|
|
410
|
-
dir = parent;
|
|
411
|
-
}
|
|
412
|
-
return null;
|
|
448
|
+
return findLocalBinUpwards(binary, cwd) ?? null;
|
|
413
449
|
}
|
|
414
450
|
/**
|
|
415
451
|
* Returns true if `bundle exec <gem>` should be used:
|
|
@@ -747,7 +783,11 @@ export const oxfmtFormatter = {
|
|
|
747
783
|
const found = await which("oxfmt");
|
|
748
784
|
if (found)
|
|
749
785
|
return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
|
|
750
|
-
|
|
786
|
+
// #2413: neither node_modules/.bin nor PATH has oxfmt, and `detect()` is
|
|
787
|
+
// config-only (it never probes the binary), so selection can reach here
|
|
788
|
+
// with nothing installed. The static command is bare `oxfmt` — spawning it
|
|
789
|
+
// only reproduces the reported `spawn oxfmt ENOENT`. Prove it unavailable.
|
|
790
|
+
return FORMATTER_UNAVAILABLE;
|
|
751
791
|
},
|
|
752
792
|
// Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
|
|
753
793
|
// Do not hand-maintain a second copy of this list (#1134 — previously two
|
|
@@ -784,7 +824,12 @@ export const ruffFormatter = {
|
|
|
784
824
|
const installed = await ensureTool(toolId);
|
|
785
825
|
if (installed)
|
|
786
826
|
return [installed, ...args, filePath];
|
|
787
|
-
|
|
827
|
+
// #2413: `ensureTool` already resolves PATH, global bins and the managed
|
|
828
|
+
// dir (and attempts an install) before returning falsy, so reaching here
|
|
829
|
+
// means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
|
|
830
|
+
// no binary check, so a config-only project selects ruff with nothing
|
|
831
|
+
// installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
|
|
832
|
+
return FORMATTER_UNAVAILABLE;
|
|
788
833
|
},
|
|
789
834
|
async detect(cwd) {
|
|
790
835
|
if (hasRuffConfig(cwd))
|
|
@@ -847,6 +892,18 @@ export const rustfmtFormatter = {
|
|
|
847
892
|
name: "rustfmt",
|
|
848
893
|
command: ["rustfmt", "$FILE"],
|
|
849
894
|
extensions: [".rs"],
|
|
895
|
+
// #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
|
|
896
|
+
// package when invoked bare, so it can reject valid newer-edition syntax
|
|
897
|
+
// (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
|
|
898
|
+
// `edition.workspace = true` inheritance) through `--edition`; `undefined`
|
|
899
|
+
// (unreadable/unparseable manifest) falls back to the static command
|
|
900
|
+
// above, unchanged from pre-#2466 behavior.
|
|
901
|
+
async resolveCommand(filePath, _cwd) {
|
|
902
|
+
const edition = await resolveCargoPackageEdition(filePath);
|
|
903
|
+
if (edition === undefined)
|
|
904
|
+
return null;
|
|
905
|
+
return ["rustfmt", "--edition", edition, filePath];
|
|
906
|
+
},
|
|
850
907
|
async detect(cwd) {
|
|
851
908
|
if ((await which("rustfmt")) !== null)
|
|
852
909
|
return true;
|
|
@@ -971,13 +1028,27 @@ export const ktfmtFormatter = {
|
|
|
971
1028
|
// ktfmt formats in place when given a file path (no flag needed).
|
|
972
1029
|
command: ["ktfmt", "$FILE"],
|
|
973
1030
|
extensions: [".kt", ".kts"],
|
|
1031
|
+
// #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
|
|
1032
|
+
// | kotlinLangStyle() }` selection — style is CLI-flag-only
|
|
1033
|
+
// (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
|
|
1034
|
+
// own arg parser). Carry the nearest module's declared style through;
|
|
1035
|
+
// `undefined` (no declaration, unreadable/unparseable manifest, or an
|
|
1036
|
+
// unsupported style like the removed `dropboxStyle()`) falls back to the
|
|
1037
|
+
// bare invocation, unchanged from pre-#2468 behavior.
|
|
974
1038
|
async resolveCommand(filePath, _cwd) {
|
|
1039
|
+
const styleFlag = await resolveKtfmtGradleStyle(filePath);
|
|
975
1040
|
const inPath = await which("ktfmt");
|
|
976
1041
|
if (inPath)
|
|
977
|
-
return [inPath, filePath];
|
|
1042
|
+
return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
|
|
978
1043
|
const { ensureTool } = await import("./installer/index.js");
|
|
979
1044
|
const installed = await ensureTool("ktfmt");
|
|
980
|
-
|
|
1045
|
+
// #2413: which() and ensureTool (PATH/global/managed + install) both
|
|
1046
|
+
// failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
|
|
1047
|
+
// so a config-only project reaches here with ktfmt absent. The static
|
|
1048
|
+
// command is bare `ktfmt`; spawning it reproduces the ENOENT class.
|
|
1049
|
+
if (!installed)
|
|
1050
|
+
return FORMATTER_UNAVAILABLE;
|
|
1051
|
+
return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
|
|
981
1052
|
},
|
|
982
1053
|
async detect(cwd) {
|
|
983
1054
|
// Opt-in only: ktfmt becomes the formatter when the project elects it,
|
|
@@ -1063,18 +1134,49 @@ export const phpCsFixerFormatter = {
|
|
|
1063
1134
|
name: "php-cs-fixer",
|
|
1064
1135
|
command: ["php-cs-fixer", "fix", "$FILE"],
|
|
1065
1136
|
extensions: [".php"],
|
|
1137
|
+
// #2472: php-cs-fixer does NOT walk up parent directories looking for its
|
|
1138
|
+
// own config the way prettier/biome/eslint do (verified against upstream
|
|
1139
|
+
// `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
|
|
1140
|
+
// and `formatFile` spawns with cwd = the FILE's own directory, which is
|
|
1141
|
+
// not necessarily where the ancestor config `detect()` found actually
|
|
1142
|
+
// lives. Always resolve the binary explicitly here (vendor/bin first,
|
|
1143
|
+
// then global) rather than falling through to the static `command` above
|
|
1144
|
+
// — that static command can never carry `--config`, so a config found at
|
|
1145
|
+
// an ancestor would silently be dropped whenever the vendor lookup here
|
|
1146
|
+
// missed but a global binary still resolved. `--config` is attached
|
|
1147
|
+
// whenever a config resolves, even when it sits in the file's own
|
|
1148
|
+
// directory (AC3): unlike the pre-#2472 code, correctness no longer
|
|
1149
|
+
// depends on php-cs-fixer's own (nonexistent) upward search.
|
|
1066
1150
|
async resolveCommand(filePath, cwd) {
|
|
1067
|
-
const
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1151
|
+
const configPath = resolvePhpCsFixerConfig(filePath);
|
|
1152
|
+
const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
|
|
1153
|
+
(await which("php-cs-fixer"));
|
|
1154
|
+
// #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
|
|
1155
|
+
// PROVEN the binary is absent — returning `null` here would fall back
|
|
1156
|
+
// to the static `command` above, which is the SAME bare
|
|
1157
|
+
// `php-cs-fixer` this just failed to find, spawning it only to
|
|
1158
|
+
// re-observe the ENOENT already known. Report the proven-missing
|
|
1159
|
+
// state instead so `formatFile` skips the wasted spawn.
|
|
1160
|
+
if (!binary)
|
|
1161
|
+
return FORMATTER_UNAVAILABLE;
|
|
1162
|
+
return configPath
|
|
1163
|
+
? [binary, "fix", "--config", configPath, filePath]
|
|
1164
|
+
: [binary, "fix", filePath];
|
|
1071
1165
|
},
|
|
1072
1166
|
async detect(cwd) {
|
|
1073
1167
|
const vendorBin = await findInVendorBin("php-cs-fixer", cwd);
|
|
1074
1168
|
const globalBin = await which("php-cs-fixer");
|
|
1075
1169
|
if (!vendorBin && !globalBin)
|
|
1076
1170
|
return false;
|
|
1077
|
-
// Only run if project has explicit config
|
|
1171
|
+
// Only run if project has explicit config. This is a presence-only
|
|
1172
|
+
// climb from the project `cwd` (not necessarily the formatted file's
|
|
1173
|
+
// own directory) via this file's own `findUp` — deliberately NOT
|
|
1174
|
+
// merged with `resolvePhpCsFixerConfig` above (#2472 AC4): that
|
|
1175
|
+
// resolver climbs from the FILE's directory and needs the exact
|
|
1176
|
+
// winning path for `--config`, while this only needs a yes/no answer
|
|
1177
|
+
// from whatever `cwd` the caller passed. `rustfmtFormatter.detect`
|
|
1178
|
+
// keeps the same non-merged shape against `resolveCargoPackageEdition`
|
|
1179
|
+
// for the identical reason.
|
|
1078
1180
|
const configs = [".php-cs-fixer.php", ".php-cs-fixer.dist.php"];
|
|
1079
1181
|
const found = await findUp(configs, cwd);
|
|
1080
1182
|
return found.length > 0;
|
|
@@ -1333,9 +1435,15 @@ export const ALL_FORMATTERS = [
|
|
|
1333
1435
|
const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
|
|
1334
1436
|
// Cache for detection results - stores array of enabled formatter names per cwd+ext
|
|
1335
1437
|
const detectionCache = new BoundedLruCache(32);
|
|
1438
|
+
// The signature is immutable for a cache generation. This memo is separate
|
|
1439
|
+
// from detectionCache because a cwd can have several extension entries, and a
|
|
1440
|
+
// warm lookup must not repeat the ancestor walk or stat matched configs.
|
|
1441
|
+
const formatterSignatureFlights = new Map();
|
|
1442
|
+
const formatterCacheGeneration = createGenerationSource("formatter-cache");
|
|
1336
1443
|
// These are the formatter configuration files consulted by the policy helpers
|
|
1337
|
-
// above. Their metadata is
|
|
1338
|
-
//
|
|
1444
|
+
// above. Their metadata is captured by the cold detection signature. The
|
|
1445
|
+
// write-result seam invalidates that signature when a config path changes, so
|
|
1446
|
+
// detection re-runs even when PATH and installed tools are unchanged. A
|
|
1339
1447
|
// filename a `has*Config` check reads but this list omits is invisible to the
|
|
1340
1448
|
// cache: the signature never moves when that file is added, so a project that
|
|
1341
1449
|
// opts in AFTER the first `getFormattersForFile` call for its cwd keeps
|
|
@@ -1436,6 +1544,18 @@ async function formatterConfigSignature(cwd) {
|
|
|
1436
1544
|
}));
|
|
1437
1545
|
return parts.join("|");
|
|
1438
1546
|
}
|
|
1547
|
+
async function getFormatterConfigSignature(cwd, normalizedCwd) {
|
|
1548
|
+
const existing = formatterSignatureFlights.get(normalizedCwd);
|
|
1549
|
+
if (existing)
|
|
1550
|
+
return existing.promise;
|
|
1551
|
+
const promise = formatterConfigSignature(cwd).finally(() => {
|
|
1552
|
+
const current = formatterSignatureFlights.get(normalizedCwd);
|
|
1553
|
+
if (current?.promise === promise)
|
|
1554
|
+
formatterSignatureFlights.delete(normalizedCwd);
|
|
1555
|
+
});
|
|
1556
|
+
formatterSignatureFlights.set(normalizedCwd, { promise });
|
|
1557
|
+
return promise;
|
|
1558
|
+
}
|
|
1439
1559
|
// --- Public API ---
|
|
1440
1560
|
export async function getFormattersForFile(filePath, cwd) {
|
|
1441
1561
|
const ext = path.extname(filePath).toLowerCase();
|
|
@@ -1449,31 +1569,19 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1449
1569
|
const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
|
|
1450
1570
|
? `${normalizedCwd}:${ext}:${base}`
|
|
1451
1571
|
: `${normalizedCwd}:${ext}`;
|
|
1452
|
-
|
|
1453
|
-
//
|
|
1572
|
+
// A warm entry is authoritative until the write-result seam invalidates its
|
|
1573
|
+
// config path. This is the only way to make the warm path free of config
|
|
1574
|
+
// polling while still reacting immediately to pi-authored create/remove/
|
|
1575
|
+
// change events. External editor changes remain a documented session-boundary
|
|
1576
|
+
// limitation, like other write-result-owned freshness seams.
|
|
1454
1577
|
let cached = detectionCache.get(normalizedCwd);
|
|
1455
|
-
if (cached
|
|
1456
|
-
// An EXISTING entry whose signature moved is the user telling us the world
|
|
1457
|
-
// changed. Re-probe PATH too, so "install the tool, touch the config" still
|
|
1458
|
-
// works within one session now that a durable absence latches (#1495).
|
|
1459
|
-
// Scoped to a real change on purpose: keying this off "no entry yet" would
|
|
1460
|
-
// drop every PATH verdict on the first save in each new directory, which in
|
|
1461
|
-
// a monorepo means re-probing forever.
|
|
1462
|
-
detectionCache.delete(normalizedCwd);
|
|
1463
|
-
cached = undefined;
|
|
1464
|
-
resetWhichLatches();
|
|
1465
|
-
}
|
|
1466
|
-
if (!cached) {
|
|
1467
|
-
cached = { signature: configSignature, entries: new Map() };
|
|
1468
|
-
detectionCache.set(normalizedCwd, cached);
|
|
1469
|
-
}
|
|
1470
|
-
if (cached.entries.has(cacheKey)) {
|
|
1578
|
+
if (cached?.entries.has(cacheKey)) {
|
|
1471
1579
|
const enabledNames = cached.entries.get(cacheKey);
|
|
1472
1580
|
const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
|
|
1473
1581
|
logLatency({
|
|
1474
1582
|
type: "phase",
|
|
1475
1583
|
phase: "formatter_selected",
|
|
1476
|
-
filePath
|
|
1584
|
+
filePath,
|
|
1477
1585
|
durationMs: 0,
|
|
1478
1586
|
metadata: {
|
|
1479
1587
|
formatter: selectedFormatterName,
|
|
@@ -1485,9 +1593,26 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1485
1593
|
});
|
|
1486
1594
|
if (!enabledNames || enabledNames.length === 0)
|
|
1487
1595
|
return [];
|
|
1488
|
-
// Return cached formatters by name (preserves priority order)
|
|
1489
1596
|
return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
|
|
1490
1597
|
}
|
|
1598
|
+
if (!cached) {
|
|
1599
|
+
const generation = formatterCacheGeneration.capture();
|
|
1600
|
+
const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
|
|
1601
|
+
// An invalidation can happen while the first signature walk is in flight.
|
|
1602
|
+
// Do not publish a pre-invalidation signature into the new generation.
|
|
1603
|
+
if (!generation.isCurrent()) {
|
|
1604
|
+
return getFormattersForFile(filePath, cwd);
|
|
1605
|
+
}
|
|
1606
|
+
// Another cold caller for this cwd can finish the shared signature flight
|
|
1607
|
+
// first. Re-read the LRU before installing an object so the later caller
|
|
1608
|
+
// merges its extension entry into the existing object instead of replacing
|
|
1609
|
+
// the earlier entry.
|
|
1610
|
+
cached = detectionCache.get(normalizedCwd);
|
|
1611
|
+
if (!cached) {
|
|
1612
|
+
cached = { signature: configSignature, entries: new Map() };
|
|
1613
|
+
detectionCache.set(normalizedCwd, cached);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1491
1616
|
// Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
|
|
1492
1617
|
const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
|
|
1493
1618
|
const formatterPolicy = getFormatterPolicyForFile(filePath);
|
|
@@ -1641,7 +1766,11 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1641
1766
|
return enabled;
|
|
1642
1767
|
// Store the list of enabled formatter names in cache
|
|
1643
1768
|
const enabledNames = enabled.map((f) => f.name);
|
|
1644
|
-
|
|
1769
|
+
// An invalidation may have removed or replaced this object while detection
|
|
1770
|
+
// awaited a tool probe. Never repopulate the old generation.
|
|
1771
|
+
if (detectionCache.get(normalizedCwd) === cached) {
|
|
1772
|
+
cached.entries.set(cacheKey, enabledNames);
|
|
1773
|
+
}
|
|
1645
1774
|
return enabled;
|
|
1646
1775
|
}
|
|
1647
1776
|
/**
|
|
@@ -1655,9 +1784,21 @@ export async function getFormattersForFile(filePath, cwd) {
|
|
|
1655
1784
|
* therefore re-arms every directory EXCEPT the one the user is working in.
|
|
1656
1785
|
*/
|
|
1657
1786
|
export function clearFormatterCache() {
|
|
1787
|
+
formatterCacheGeneration.bump();
|
|
1788
|
+
formatterSignatureFlights.clear();
|
|
1658
1789
|
detectionCache.clear();
|
|
1659
1790
|
resetWhichLatches();
|
|
1660
1791
|
}
|
|
1792
|
+
const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
|
|
1793
|
+
/** Invalidate selection when the write-result seam reports a config path. */
|
|
1794
|
+
export function invalidateFormatterCacheForPath(filePath) {
|
|
1795
|
+
const basename = filePath.includes("\\")
|
|
1796
|
+
? path.win32.basename(filePath)
|
|
1797
|
+
: path.basename(filePath);
|
|
1798
|
+
if (!formatterConfigBasenames.has(basename.toLowerCase()))
|
|
1799
|
+
return;
|
|
1800
|
+
clearFormatterCache();
|
|
1801
|
+
}
|
|
1661
1802
|
/**
|
|
1662
1803
|
* Test-only internals access for the session-state registry probe (#1895):
|
|
1663
1804
|
* the conformance suite must arm all four pieces of state this reset claims
|
|
@@ -1672,16 +1813,13 @@ export function _getFormatterResetStateForTests() {
|
|
|
1672
1813
|
};
|
|
1673
1814
|
}
|
|
1674
1815
|
export function clearFormatterRuntimeState() {
|
|
1675
|
-
|
|
1676
|
-
resetWhichLatches();
|
|
1816
|
+
clearFormatterCache();
|
|
1677
1817
|
// NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
|
|
1678
1818
|
// from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
|
|
1679
1819
|
// clearing the lazy-install hold here made "held for the session" mean "held
|
|
1680
1820
|
// for a turn", and a failing install re-ran every turn. The hold's only reset
|
|
1681
1821
|
// is `session_start`'s block in runtime-session.ts.
|
|
1682
1822
|
}
|
|
1683
|
-
// ESC is built via fromCharCode so no raw control byte sits in the source.
|
|
1684
|
-
const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*[A-Za-z]`, "g");
|
|
1685
1823
|
const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
|
|
1686
1824
|
const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
|
|
1687
1825
|
/**
|
|
@@ -1698,7 +1836,7 @@ const HAS_BOX_DRAWING = /[\u2500-\u257F]/;
|
|
|
1698
1836
|
*/
|
|
1699
1837
|
export function firstDiagnosticLine(text) {
|
|
1700
1838
|
for (const raw of (text ?? "").split("\n")) {
|
|
1701
|
-
const line = raw
|
|
1839
|
+
const line = stripAnsi(raw).trimEnd();
|
|
1702
1840
|
const stripped = line.replace(BOX_DRAWING_GLOBAL, "").trim();
|
|
1703
1841
|
if (!stripped)
|
|
1704
1842
|
continue;
|
|
@@ -1724,6 +1862,12 @@ async function resolveFormatterCommand(formatter, absolutePath, cwd) {
|
|
|
1724
1862
|
: null;
|
|
1725
1863
|
if (resolved === SKIP_FORMATTING)
|
|
1726
1864
|
return SKIP_FORMATTING;
|
|
1865
|
+
// The resolver proved the executable absent — DO NOT fall through to the
|
|
1866
|
+
// static bare command, which is the same binary it just probed and missing
|
|
1867
|
+
// (#2413). The caller turns this into a typed `unavailable` outcome, never a
|
|
1868
|
+
// spawn and never a failure.
|
|
1869
|
+
if (resolved === FORMATTER_UNAVAILABLE)
|
|
1870
|
+
return FORMATTER_UNAVAILABLE;
|
|
1727
1871
|
if (resolved !== null)
|
|
1728
1872
|
return resolved;
|
|
1729
1873
|
const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
|
|
@@ -1751,13 +1895,40 @@ export async function formatFile(filePath, formatter) {
|
|
|
1751
1895
|
if (cmd === SKIP_FORMATTING) {
|
|
1752
1896
|
// Style-preserving refusal (#1144): no repo config and no detectable
|
|
1753
1897
|
// indentation to pin — formatting would impose the tool's stock style.
|
|
1754
|
-
return { success: true, changed: false };
|
|
1898
|
+
return { success: true, changed: false, outcome: "skipped" };
|
|
1899
|
+
}
|
|
1900
|
+
if (cmd === FORMATTER_UNAVAILABLE) {
|
|
1901
|
+
// The resolver proved the executable absent (#2413): the oxfmt ENOENT
|
|
1902
|
+
// trap and its siblings. This is unavailable infrastructure, not a code
|
|
1903
|
+
// failure — success stays true so it is never counted as a failed file
|
|
1904
|
+
// or requeued, and the typed outcome keeps it out of the failure bucket.
|
|
1905
|
+
return {
|
|
1906
|
+
success: true,
|
|
1907
|
+
changed: false,
|
|
1908
|
+
outcome: "unavailable",
|
|
1909
|
+
error: `${formatter.name}: formatter executable not found`,
|
|
1910
|
+
};
|
|
1755
1911
|
}
|
|
1756
1912
|
// Run formatter without blocking the event loop.
|
|
1757
1913
|
const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
|
|
1758
1914
|
timeout: 15000,
|
|
1759
1915
|
cwd,
|
|
1760
1916
|
});
|
|
1917
|
+
// A resolver that could NOT prove absence (it never probed PATH — e.g.
|
|
1918
|
+
// black/sqlfluff, which only look in a venv) legitimately falls back to the
|
|
1919
|
+
// bare static command. When that bare command is missing, the spawn boundary
|
|
1920
|
+
// reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
|
|
1921
|
+
// as unavailable, not a formatting failure, closing the same #2413 class for
|
|
1922
|
+
// the formatters whose `null` correctly means "static untried" (no
|
|
1923
|
+
// oxfmt-specific string matching — this reads the typed spawn-failure kind).
|
|
1924
|
+
if (result.spawnFailure?.kind === "tool-not-found") {
|
|
1925
|
+
return {
|
|
1926
|
+
success: true,
|
|
1927
|
+
changed: false,
|
|
1928
|
+
outcome: "unavailable",
|
|
1929
|
+
error: result.spawnFailure.message,
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1761
1932
|
// Strict by default (#1337): only a formatter with a documented
|
|
1762
1933
|
// benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
|
|
1763
1934
|
// read as a successful run. Everything else that exits nonzero never
|
|
@@ -1770,6 +1941,7 @@ export async function formatFile(filePath, formatter) {
|
|
|
1770
1941
|
return {
|
|
1771
1942
|
success: false,
|
|
1772
1943
|
changed: false,
|
|
1944
|
+
outcome: "failed",
|
|
1773
1945
|
error: result.error?.message ||
|
|
1774
1946
|
firstDiagnosticLine(result.stderr) ||
|
|
1775
1947
|
// biome, ktlint and `mix format` report on STDOUT; without this
|
|
@@ -1785,12 +1957,14 @@ export async function formatFile(filePath, formatter) {
|
|
|
1785
1957
|
return {
|
|
1786
1958
|
success: true,
|
|
1787
1959
|
changed,
|
|
1960
|
+
outcome: changed ? "formatted" : "unchanged",
|
|
1788
1961
|
};
|
|
1789
1962
|
}
|
|
1790
1963
|
catch (err) {
|
|
1791
1964
|
return {
|
|
1792
1965
|
success: false,
|
|
1793
1966
|
changed: false,
|
|
1967
|
+
outcome: "failed",
|
|
1794
1968
|
error: err instanceof Error ? err.message : String(err),
|
|
1795
1969
|
};
|
|
1796
1970
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared re-check cadence for a cached "nothing here" verdict — the
|
|
3
|
+
* bound both `file-utils.ts`'s nested `.gitignore`/`.pi-lens.json` matcher
|
|
4
|
+
* (#2159/#2071) and `project-lens-config.ts`'s no-config discovery cache
|
|
5
|
+
* (#2483 round 2) use so a negative result is neither re-statted on every
|
|
6
|
+
* call nor cached for the life of the process.
|
|
7
|
+
*
|
|
8
|
+
* A declared leaf module, the same shape as `ledger-bounds.ts` /
|
|
9
|
+
* `spawn-output-cap.ts`: it imports nothing from `clients/`, because
|
|
10
|
+
* `file-utils.ts` already imports `project-lens-config.ts`, and the
|
|
11
|
+
* `clients/` acyclic-imports rule (`no-client-cycles`,
|
|
12
|
+
* `.dependency-cruiser.cjs`) would reject either of those two files
|
|
13
|
+
* importing a constant that lived inside the other. A shared value neither
|
|
14
|
+
* owns is the only way both stay on the same cadence without one importing
|
|
15
|
+
* the other.
|
|
16
|
+
*/
|
|
17
|
+
export const FRESHNESS_CADENCE_MS = 2_000;
|