@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
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Monotonic deny precedence (#2425, scope item 4; #2415 AC 3).
|
|
3
|
+
*
|
|
4
|
+
* Ordinary config values are last-tier-wins. Denials are not. A denial is a
|
|
5
|
+
* SECURITY decision, and the tier that made it is usually the tier the user
|
|
6
|
+
* actually controls, so the ordinary rule is exactly backwards for it: today a
|
|
7
|
+
* repository can hand pi-lens a `.pi-lens.json` that re-enables a server the
|
|
8
|
+
* user disabled globally, and nothing notices.
|
|
9
|
+
*
|
|
10
|
+
* The rule, in one sentence: a denial from an OPERATOR tier is never lifted, and
|
|
11
|
+
* a denial from any other tier is lifted only by an explicit operator allow of
|
|
12
|
+
* higher precedence. `provenance.ts` owns the three-way tier classification;
|
|
13
|
+
* this module owns what it means.
|
|
14
|
+
*
|
|
15
|
+
* The third class is why the sentence says "any other tier" rather than "a repo
|
|
16
|
+
* tier" (#2440 review finding F3). `builtin` is `default`, not `operator`: a
|
|
17
|
+
* denial pi-lens itself ships is liftable by the person running pi-lens — their
|
|
18
|
+
* global config, their environment, their command line — and remains unliftable
|
|
19
|
+
* by anything that arrived with a checkout. Treating a shipped default as an
|
|
20
|
+
* operator decision made it permanent for everyone, which is a hard-coded
|
|
21
|
+
* decision rather than a default.
|
|
22
|
+
*
|
|
23
|
+
* The two shapes a denial takes (`schema.ts`'s `x-deny`):
|
|
24
|
+
*
|
|
25
|
+
* - `boolean-false` — `enabled: false`. Resolution below.
|
|
26
|
+
* - `array-union` — `disabledServers: ["x"]`. The union of every tier's
|
|
27
|
+
* members, always. There is no vocabulary for UN-denying a member, so a
|
|
28
|
+
* nearer tier that omits one is expressing nothing, not an allow. This is the
|
|
29
|
+
* one place `x-merge-strategy` does not decide the outcome: a `replace`
|
|
30
|
+
* strategy still governs how the array is SPELLED across tiers, and the deny
|
|
31
|
+
* union still governs which members survive. A deny that a merge strategy
|
|
32
|
+
* could erase would not be monotonic.
|
|
33
|
+
*
|
|
34
|
+
* Pure functions over their arguments. No state, no ledger writes: a denial
|
|
35
|
+
* being honored is the normal case, not a degradation.
|
|
36
|
+
*/
|
|
37
|
+
import { isOperatorTier, tierPrecedence, } from "./provenance.js";
|
|
38
|
+
function provenanceOfContribution(source, key) {
|
|
39
|
+
if (!source)
|
|
40
|
+
return undefined;
|
|
41
|
+
return {
|
|
42
|
+
tier: source.tier,
|
|
43
|
+
key,
|
|
44
|
+
...(source.file === undefined ? {} : { file: source.file }),
|
|
45
|
+
...(source.trust === undefined ? {} : { trust: source.trust }),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/** Build a provenance entry for a resolved deny leaf. */
|
|
49
|
+
export function denyProvenance(contributions, resolution, key) {
|
|
50
|
+
return provenanceOfContribution(contributions[resolution.winner], key);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* One provenance entry per SURVIVING member of an `array-union`, keyed by the
|
|
54
|
+
* member's own JSON pointer.
|
|
55
|
+
*
|
|
56
|
+
* The array's own entry still exists and still names the first denying tier;
|
|
57
|
+
* these are strictly additional, so a consumer that asks about the array is
|
|
58
|
+
* unaffected and a consumer that asks about a member gets the tier that
|
|
59
|
+
* actually denied it (#2427 review round 2, F2).
|
|
60
|
+
*/
|
|
61
|
+
export function denyMemberProvenance(contributions, resolution, key) {
|
|
62
|
+
const entries = [];
|
|
63
|
+
resolution.memberWinners.forEach((index, position) => {
|
|
64
|
+
const pointer = `${key}/${position}`;
|
|
65
|
+
const entry = provenanceOfContribution(contributions[index], pointer);
|
|
66
|
+
if (entry)
|
|
67
|
+
entries.push(entry);
|
|
68
|
+
});
|
|
69
|
+
return entries;
|
|
70
|
+
}
|
|
71
|
+
/** The `memberWinners` of a leaf with no members. One shared frozen empty. */
|
|
72
|
+
const EMPTY_MEMBER_WINNERS = Object.freeze([]);
|
|
73
|
+
/** Contributions sorted by tier precedence, lowest first, ties in caller order. */
|
|
74
|
+
function byPrecedence(contributions) {
|
|
75
|
+
return contributions
|
|
76
|
+
.map((contribution, index) => ({ contribution, index }))
|
|
77
|
+
.sort((left, right) => tierPrecedence(left.contribution.tier) -
|
|
78
|
+
tierPrecedence(right.contribution.tier));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a `boolean-false` deny.
|
|
82
|
+
*
|
|
83
|
+
* - No `false` anywhere: ordinary last-tier-wins.
|
|
84
|
+
* - An OPERATOR tier said `false`: DENIED, pinned, unliftable by anything. The
|
|
85
|
+
* winner is the LOWEST-precedence operator denial, because that is the answer
|
|
86
|
+
* to "who denied this" — the first authority to say no, not the last tier to
|
|
87
|
+
* repeat it. This holds even for a nearer operator tier: `cli: true` does not
|
|
88
|
+
* lift `global: false`. The escape hatch for an operator who changes their
|
|
89
|
+
* mind is to change the operator-tier config that denied, not to out-rank it
|
|
90
|
+
* from another operator tier.
|
|
91
|
+
* - Otherwise every denial came from `default` or `repo` tiers, and an OPERATOR
|
|
92
|
+
* tier ranked above the FIRST such denial lifts it by saying `true` outright.
|
|
93
|
+
* That is what makes a built-in default a default (`builtin: false` +
|
|
94
|
+
* `global: true` -> `true`) while keeping repository content out of the
|
|
95
|
+
* decision (`builtin: false` + `project: true` -> `false`). A repo tier never
|
|
96
|
+
* lifts anything, its own class included.
|
|
97
|
+
*
|
|
98
|
+
* The lift test asks `isOperatorTier`, NOT `!isRepoTier`. The negative spelling
|
|
99
|
+
* is what silently swept `builtin` into the operator class and made every
|
|
100
|
+
* shipped default permanent; the affirmative one cannot.
|
|
101
|
+
*
|
|
102
|
+
* Non-boolean contributions are ignored here: `normalize.ts` has already
|
|
103
|
+
* dropped values that failed the schema, so anything left is a boolean.
|
|
104
|
+
*/
|
|
105
|
+
export function resolveBooleanDeny(contributions) {
|
|
106
|
+
const ordered = byPrecedence(contributions);
|
|
107
|
+
if (ordered.length === 0)
|
|
108
|
+
return {
|
|
109
|
+
value: undefined,
|
|
110
|
+
winner: -1,
|
|
111
|
+
denied: false,
|
|
112
|
+
memberWinners: EMPTY_MEMBER_WINNERS,
|
|
113
|
+
};
|
|
114
|
+
const denials = ordered.filter((entry) => entry.contribution.value === false);
|
|
115
|
+
if (denials.length === 0) {
|
|
116
|
+
const last = ordered[ordered.length - 1];
|
|
117
|
+
return {
|
|
118
|
+
value: last.contribution.value,
|
|
119
|
+
winner: last.index,
|
|
120
|
+
denied: false,
|
|
121
|
+
memberWinners: EMPTY_MEMBER_WINNERS,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const operatorDenial = denials.find((entry) => isOperatorTier(entry.contribution.tier));
|
|
125
|
+
if (operatorDenial) {
|
|
126
|
+
return {
|
|
127
|
+
value: false,
|
|
128
|
+
winner: operatorDenial.index,
|
|
129
|
+
denied: true,
|
|
130
|
+
memberWinners: EMPTY_MEMBER_WINNERS,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// Every denial came from a shipped default or from repo content. An operator
|
|
134
|
+
// tier ranked above the FIRST such denial may lift it by saying `true`.
|
|
135
|
+
const firstDenial = denials[0];
|
|
136
|
+
const lift = ordered.find((entry) => entry.contribution.value === true &&
|
|
137
|
+
isOperatorTier(entry.contribution.tier) &&
|
|
138
|
+
tierPrecedence(entry.contribution.tier) >
|
|
139
|
+
tierPrecedence(firstDenial.contribution.tier));
|
|
140
|
+
if (lift) {
|
|
141
|
+
return {
|
|
142
|
+
value: true,
|
|
143
|
+
winner: lift.index,
|
|
144
|
+
denied: false,
|
|
145
|
+
memberWinners: EMPTY_MEMBER_WINNERS,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
value: false,
|
|
150
|
+
winner: firstDenial.index,
|
|
151
|
+
denied: true,
|
|
152
|
+
memberWinners: EMPTY_MEMBER_WINNERS,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Resolve an `array-union` deny: the union of every tier's members.
|
|
157
|
+
*
|
|
158
|
+
* Members keep first-contribution order, lowest precedence first, so the
|
|
159
|
+
* resolved list reads oldest-authority-first and is stable across runs.
|
|
160
|
+
* Duplicates are folded by their JSON encoding, which is exact for the
|
|
161
|
+
* primitives a deny list holds and conservative (keeps both) for anything else.
|
|
162
|
+
*
|
|
163
|
+
* The winner is the FIRST tier that contributed a surviving member: the tier
|
|
164
|
+
* that answers "why can I not turn this back on".
|
|
165
|
+
*/
|
|
166
|
+
export function resolveArrayDeny(contributions) {
|
|
167
|
+
const ordered = byPrecedence(contributions);
|
|
168
|
+
const seen = new Set();
|
|
169
|
+
const members = [];
|
|
170
|
+
// Positionally parallel to `members`: the contribution that introduced each
|
|
171
|
+
// surviving member, which is the per-member answer to "who denied this"
|
|
172
|
+
// (#2427 review round 2, F2).
|
|
173
|
+
const memberWinners = [];
|
|
174
|
+
let winner = -1;
|
|
175
|
+
for (const entry of ordered) {
|
|
176
|
+
const value = entry.contribution.value;
|
|
177
|
+
if (!Array.isArray(value))
|
|
178
|
+
continue;
|
|
179
|
+
for (const member of value) {
|
|
180
|
+
const identity = primitiveIdentity(member);
|
|
181
|
+
if (identity !== undefined) {
|
|
182
|
+
if (seen.has(identity))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(identity);
|
|
185
|
+
}
|
|
186
|
+
members.push(member);
|
|
187
|
+
memberWinners.push(entry.index);
|
|
188
|
+
if (winner === -1)
|
|
189
|
+
winner = entry.index;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (winner === -1) {
|
|
193
|
+
// Nothing denied. Still report the array so a consumer sees an empty list
|
|
194
|
+
// rather than an absent field, and attribute it to the last contributor.
|
|
195
|
+
const last = ordered[ordered.length - 1];
|
|
196
|
+
return {
|
|
197
|
+
value: members,
|
|
198
|
+
winner: last ? last.index : -1,
|
|
199
|
+
denied: false,
|
|
200
|
+
memberWinners,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return { value: members, winner, denied: members.length > 0, memberWinners };
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* A fold key for a primitive member, or `undefined` for anything else.
|
|
207
|
+
*
|
|
208
|
+
* Only primitives are de-duplicated. Two structurally equal objects can carry
|
|
209
|
+
* different meaning, and keeping both is the conservative half of a deny list —
|
|
210
|
+
* a union that dropped one would be un-denying something.
|
|
211
|
+
*/
|
|
212
|
+
function primitiveIdentity(member) {
|
|
213
|
+
if (typeof member === "string")
|
|
214
|
+
return `s:${member}`;
|
|
215
|
+
if (typeof member === "number" || typeof member === "boolean") {
|
|
216
|
+
return `p:${String(member)}`;
|
|
217
|
+
}
|
|
218
|
+
if (member === null)
|
|
219
|
+
return "null";
|
|
220
|
+
return undefined;
|
|
221
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE shared config core (#2425). Every future config loader, catalog, and
|
|
3
|
+
* selector resolves through this module rather than growing a fourth merge
|
|
4
|
+
* semantics of its own.
|
|
5
|
+
*
|
|
6
|
+
* The pipeline, in one line:
|
|
7
|
+
*
|
|
8
|
+
* RawConfig -> validate(schema) -> NormalizedConfig -> merge(sources) -> Resolved<T>
|
|
9
|
+
*
|
|
10
|
+
* `resolveConfig` runs both halves and is the front door. It is a pure function
|
|
11
|
+
* of its arguments: no file reads, no ledger writes, no logging. Reporting is a
|
|
12
|
+
* separate, explicit step — `reportPiLensConfigRecords` in
|
|
13
|
+
* `clients/config-resolve.ts` — so a caller decides when a user gets warned and
|
|
14
|
+
* under which subsystem; a library that warns on its own would fragment the
|
|
15
|
+
* warn-once latch across every consumer.
|
|
16
|
+
*
|
|
17
|
+
* That step used to live HERE, in `records.ts`, which meant this "pure" library
|
|
18
|
+
* imported `config-warn.js` and the degradation ledger (#2426). Purity was the
|
|
19
|
+
* claim and the import graph disagreed: it broke every suite that `vi.mock`s the
|
|
20
|
+
* ledger wholesale, and it closed seven import cycles the moment the first
|
|
21
|
+
* loader — all of which sit downstream of `file-utils.ts` — resolved through the
|
|
22
|
+
* core. The reporting function moved to the module that owns the loaders'
|
|
23
|
+
* subsystem vocabulary, and the one constant records needs from the ledger
|
|
24
|
+
* (`DEGRADATION_ENTRIES_PER_KIND`) moved to the `ledger-bounds.js` leaf that
|
|
25
|
+
* exists for exactly this. Nothing under `config-core/` imports a sink now.
|
|
26
|
+
*
|
|
27
|
+
* ALL THREE LOADERS ARE WIRED as of #2426, through `clients/config-resolve.ts`
|
|
28
|
+
* and the canonical schema in `clients/config-schema.ts`:
|
|
29
|
+
* `clients/lsp/config.ts` (`loadLSPConfig`), `clients/lens-config.ts`
|
|
30
|
+
* (`loadPiLensGlobalConfig`), and `clients/project-lens-config.ts`
|
|
31
|
+
* (`loadPiLensProjectConfig`) are now projections of one resolution. #2416
|
|
32
|
+
* brings the `lsp.servers.<id>` shape the schema currently only reserves.
|
|
33
|
+
*/
|
|
34
|
+
export { additionalPropertyPolicy, DEFAULT_MERGE_STRATEGY, DENY_KEY, DENY_POLICIES, denyPolicyOf, isDenyPolicy, isConfigObject, isKnownSchemaType, isMergeStrategy, isPlainObject, isSchemaNode, itemsSchema, keyedField, MERGE_STRATEGY_KEY, mergeStrategyOf, propertySchema, SCHEMA_TYPES, schemaType, STABILITY_TIER_KEY, } from "./schema.js";
|
|
35
|
+
export { compareKeys, isOperatorTier, isRepoTier, provenanceFor, provenanceView, SOURCE_TIERS, TIER_CLASS, TIER_PRECEDENCE, tierPrecedence, } from "./provenance.js";
|
|
36
|
+
export { isUnsafeConfigKey, MAX_CONFIG_DEPTH, safeAssign, UNSAFE_CONFIG_KEYS, UNSAFE_KEY_REASON, } from "./safe-object.js";
|
|
37
|
+
export { denyProvenance, resolveArrayDeny, resolveBooleanDeny, } from "./deny.js";
|
|
38
|
+
export { validate, } from "./normalize.js";
|
|
39
|
+
export { boundedKeyLabel, jsonTypeName, MAX_MIGRATION_RECORDS, MAX_RECORD_KEY_LENGTH, MigrationRecordCollector, migrationSubject, } from "./records.js";
|
|
40
|
+
export { buildProcessSpec, MAX_ARGV_BYTES, MAX_ARGV_ENTRIES, MAX_ENV_BYTES, MAX_ENV_ENTRIES, MAX_TIMEOUT_MS, redactProcessSpec, toSpawnArgs, } from "./process-spec.js";
|
|
41
|
+
// `resolveConfig` and its types live in `./resolve.js` (#2426) so a caller can
|
|
42
|
+
// import the front door without also importing this barrel's width — in
|
|
43
|
+
// particular `process-spec.js` -> `project-trust.js`, which closed three
|
|
44
|
+
// import cycles once the config loaders (all downstream of `file-utils.ts`)
|
|
45
|
+
// started resolving through the core. The public surface is unchanged: this is
|
|
46
|
+
// still where the supported entry point is exported from.
|
|
47
|
+
export { resolveConfig, } from "./resolve.js";
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Field-wise merge with per-leaf provenance (#2425, scope items 2 and 3).
|
|
3
|
+
*
|
|
4
|
+
* The three loaders this replaces each merge differently: `lsp/config.ts`
|
|
5
|
+
* spreads two objects and hand-patches four keys, `lens-config.ts` projects
|
|
6
|
+
* field by field, and `project-lens-config.ts` never merges with the global
|
|
7
|
+
* config at all. One merger with a per-node strategy expresses all three, and
|
|
8
|
+
* — unlike any of them — it can say afterwards where each value came from.
|
|
9
|
+
*
|
|
10
|
+
* Rules:
|
|
11
|
+
*
|
|
12
|
+
* - OBJECTS ARE ALWAYS FIELD-WISE. A nearer tier that sets one key of an object
|
|
13
|
+
* does not erase the rest. This is #2415's "never whole-object replacement".
|
|
14
|
+
* - ARRAYS follow the node's `x-merge-strategy`: `replace` by default, or
|
|
15
|
+
* `append`, or `keyed:<field>`.
|
|
16
|
+
* - DENY-ANNOTATED NODES go to `deny.ts` instead, whatever the merge strategy
|
|
17
|
+
* says. Monotonic deny outranks precedence by construction.
|
|
18
|
+
* - EVERY LEAF GETS PROVENANCE, keyed by its JSON-pointer path in the RESOLVED
|
|
19
|
+
* value, so a consumer navigates from an entry to the value it describes.
|
|
20
|
+
*
|
|
21
|
+
* Sources are sorted by tier precedence rather than trusted in caller order, so
|
|
22
|
+
* assembling them in a different order cannot silently invert precedence. Ties
|
|
23
|
+
* keep caller order, which is how several `nested-project` files stay ordered
|
|
24
|
+
* from outermost to innermost.
|
|
25
|
+
*
|
|
26
|
+
* `merge()`'S CONTRACT IS NARROWER THAN "IT ENFORCES THE TWO BOUNDS TOO", which
|
|
27
|
+
* an earlier version of this comment claimed (#2440 review round 3, finding
|
|
28
|
+
* F8). It required
|
|
29
|
+
* every source's `value` to already be `validate()`'s output — a sentence on
|
|
30
|
+
* `ConfigSource`, not a compiler check — and does NOT independently re-walk a
|
|
31
|
+
* value to confirm that. Five arms hand a contributor's value back BY
|
|
32
|
+
* REFERENCE, never walked: the leaf fall-through in `mergeNode`, the
|
|
33
|
+
* no-object arm in `mergeObject`, the no-array arm in `mergeArray`, `replace`,
|
|
34
|
+
* and `appendArrays`. Any of those can be reached with an opaque schema node
|
|
35
|
+
* whose tiers disagree on shape (one contributes an array, another a deeply
|
|
36
|
+
* nested object at the same key) — `isObjectNode`/`isArrayNode` both answer
|
|
37
|
+
* "no" for a mixed-shape node, the recursion falls through to the leaf arm,
|
|
38
|
+
* and whatever depth or hostile keys live inside that value ride along
|
|
39
|
+
* unexamined and unrecorded. Closing those five arms would mean re-deriving
|
|
40
|
+
* `normalize.ts`'s walk a second time here — the "half-enforcement" this
|
|
41
|
+
* function used to attempt and get half right.
|
|
42
|
+
*
|
|
43
|
+
* The fix is the CONTRACT, not a second walk: `merge()` is exported from this
|
|
44
|
+
* file (`@internal`) for `resolveConfig` and for tests that probe it
|
|
45
|
+
* directly, but it is not part of `index.ts`'s public surface. `resolveConfig`
|
|
46
|
+
* is the one supported way in, and it validates every source before merging —
|
|
47
|
+
* a mutation test in `hostile-input.test.ts` proves that by skipping
|
|
48
|
+
* `validate()` on one source and watching the suite go red.
|
|
49
|
+
*
|
|
50
|
+
* The depth counter (`mergeNode`) and the prototype-key check (`mergeObject`,
|
|
51
|
+
* over the KEYS an already-validated object's field-wise merge is about to
|
|
52
|
+
* combine) still run. They are a backstop for the common, well-typed
|
|
53
|
+
* recursion — a value `validate()` already bounded and sanitized, or a
|
|
54
|
+
* same-shape hand-built value in a direct `merge()` test — not an
|
|
55
|
+
* independent enforcement boundary; the paragraph above is what they do NOT
|
|
56
|
+
* cover. The key check also still matters for one real case even after
|
|
57
|
+
* `validate()`: a hostile key one tier introduces at a node the OTHER tier
|
|
58
|
+
* also contributes to is a key `mergeObject` discovers only when it unions
|
|
59
|
+
* that node's field names across contributions, so it is the one place that
|
|
60
|
+
* can still name and record it, even though `safeAssign` alone would already
|
|
61
|
+
* have refused the write.
|
|
62
|
+
*
|
|
63
|
+
* Pure: no state, no I/O, no ledger writes. Records, when the caller supplies a
|
|
64
|
+
* collector, describe what the two backstops refused.
|
|
65
|
+
*/
|
|
66
|
+
import { denyMemberProvenance, denyProvenance, resolveArrayDeny, resolveBooleanDeny, } from "./deny.js";
|
|
67
|
+
import { isUnsafeConfigKey, MAX_CONFIG_DEPTH, safeAssign, UNSAFE_KEY_REASON, } from "./safe-object.js";
|
|
68
|
+
import { boundedKeyLabel, MigrationRecordCollector, migrationSubject, } from "./records.js";
|
|
69
|
+
import { tierPrecedence, } from "./provenance.js";
|
|
70
|
+
import { denyPolicyOf, isConfigObject, isKnownSchemaType, isPlainObject, isSchemaNode, itemsSchema, keyedField, mergeStrategyOf, propertySchema, schemaType, } from "./schema.js";
|
|
71
|
+
/**
|
|
72
|
+
* Merge normalized sources into one resolved config with per-leaf provenance.
|
|
73
|
+
*
|
|
74
|
+
* Sources whose value is `undefined` contribute nothing — that is how a config
|
|
75
|
+
* file that failed validation drops out without a special case.
|
|
76
|
+
*
|
|
77
|
+
* @internal Requires every source's `value` to already be `validate()`'s
|
|
78
|
+
* output. Not part of `clients/config-core/index.ts`'s public surface —
|
|
79
|
+
* `resolveConfig` is. See this module's doc comment for what merging a
|
|
80
|
+
* hand-built value can still get past.
|
|
81
|
+
*/
|
|
82
|
+
export function merge(sources, schema, options = {}) {
|
|
83
|
+
const ordered = [...sources]
|
|
84
|
+
.map((source, order) => ({ source, order }))
|
|
85
|
+
.sort((left, right) => {
|
|
86
|
+
const byTier = tierPrecedence(left.source.tier) - tierPrecedence(right.source.tier);
|
|
87
|
+
return byTier !== 0 ? byTier : left.order - right.order;
|
|
88
|
+
})
|
|
89
|
+
.map((entry, rank) => ({ source: entry.source, rank }));
|
|
90
|
+
const contributions = [];
|
|
91
|
+
for (const entry of ordered) {
|
|
92
|
+
// A source whose value is `undefined` contributes nothing. Filtering with
|
|
93
|
+
// a narrowing loop rather than `.filter()` keeps the element type exact:
|
|
94
|
+
// `Array.filter` cannot drop `undefined` from the value's type on its own.
|
|
95
|
+
const value = entry.source.value;
|
|
96
|
+
if (value === undefined)
|
|
97
|
+
continue;
|
|
98
|
+
contributions.push({ source: entry.source, value, rank: entry.rank });
|
|
99
|
+
}
|
|
100
|
+
const provenance = new Map();
|
|
101
|
+
const context = {
|
|
102
|
+
provenance,
|
|
103
|
+
collector: options.collector ?? new MigrationRecordCollector(),
|
|
104
|
+
file: options.file ?? "",
|
|
105
|
+
};
|
|
106
|
+
const value = mergeNode(contributions, schema, "", context, 0);
|
|
107
|
+
return { value: value, provenance };
|
|
108
|
+
}
|
|
109
|
+
/** Record one node the merger's own bounds refused. Never carries a value. */
|
|
110
|
+
function record(context, entry) {
|
|
111
|
+
const key = boundedKeyLabel(entry.key);
|
|
112
|
+
context.collector.add({
|
|
113
|
+
code: entry.code,
|
|
114
|
+
file: context.file,
|
|
115
|
+
key,
|
|
116
|
+
subject: migrationSubject(context.file, key),
|
|
117
|
+
reason: entry.reason,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
function provenanceOf(contribution, key) {
|
|
121
|
+
const { source } = contribution;
|
|
122
|
+
return {
|
|
123
|
+
tier: source.tier,
|
|
124
|
+
key,
|
|
125
|
+
...(source.file === undefined ? {} : { file: source.file }),
|
|
126
|
+
...(source.trust === undefined ? {} : { trust: source.trust }),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function stamp(context, contribution, key) {
|
|
130
|
+
context.provenance.set(key, provenanceOf(contribution, key));
|
|
131
|
+
}
|
|
132
|
+
function mergeNode(contributions, schema, key, context, depth) {
|
|
133
|
+
if (contributions.length === 0)
|
|
134
|
+
return undefined;
|
|
135
|
+
// The merger's own depth bound. `validate()` already truncated anything it
|
|
136
|
+
// walked, so this fires only for a value that reached `merge()` without it —
|
|
137
|
+
// which the exported signature permits and a review probe demonstrated.
|
|
138
|
+
if (depth > MAX_CONFIG_DEPTH) {
|
|
139
|
+
record(context, {
|
|
140
|
+
code: "PILENS_CFG_0005",
|
|
141
|
+
key,
|
|
142
|
+
reason: `config nesting exceeds ${MAX_CONFIG_DEPTH} levels; ignored`,
|
|
143
|
+
});
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const denyPolicy = denyPolicyOf(schema);
|
|
147
|
+
if (denyPolicy)
|
|
148
|
+
return mergeDeny(contributions, denyPolicy, key, context);
|
|
149
|
+
if (isObjectNode(schema, contributions)) {
|
|
150
|
+
return mergeObject(contributions, schema, key, context, depth);
|
|
151
|
+
}
|
|
152
|
+
if (isArrayNode(schema, contributions)) {
|
|
153
|
+
return mergeArray(contributions, schema, key, context, depth);
|
|
154
|
+
}
|
|
155
|
+
const winner = contributions[contributions.length - 1];
|
|
156
|
+
stamp(context, winner, key);
|
|
157
|
+
return winner.value;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* An object node: either the schema says so, or every contribution IS a plain
|
|
161
|
+
* object. The second arm matters because a schema may be opaque about a
|
|
162
|
+
* sub-object, and merging two objects whole would drop the field-wise rule the
|
|
163
|
+
* moment a schema is incomplete.
|
|
164
|
+
*/
|
|
165
|
+
function isObjectNode(schema, contributions) {
|
|
166
|
+
const declared = schemaType(schema);
|
|
167
|
+
if (declared === "object")
|
|
168
|
+
return true;
|
|
169
|
+
// Only a RECOGNIZED non-object type rules object-ness out. An unrecognized
|
|
170
|
+
// keyword leaves the node opaque, and an opaque node is decided by the
|
|
171
|
+
// value's shape — the same rule `normalize.ts` walks by, so one schema typo
|
|
172
|
+
// cannot give the two halves different merge semantics.
|
|
173
|
+
if (isKnownSchemaType(declared))
|
|
174
|
+
return false;
|
|
175
|
+
if (schema && isSchemaNode(schema.properties))
|
|
176
|
+
return true;
|
|
177
|
+
return contributions.every((entry) => isPlainObject(entry.value));
|
|
178
|
+
}
|
|
179
|
+
function isArrayNode(schema, contributions) {
|
|
180
|
+
const declared = schemaType(schema);
|
|
181
|
+
if (declared === "array")
|
|
182
|
+
return true;
|
|
183
|
+
if (isKnownSchemaType(declared))
|
|
184
|
+
return false;
|
|
185
|
+
return contributions.every((entry) => Array.isArray(entry.value));
|
|
186
|
+
}
|
|
187
|
+
function mergeDeny(contributions, policy, key, context) {
|
|
188
|
+
const denyContributions = contributions.map((entry) => ({
|
|
189
|
+
tier: entry.source.tier,
|
|
190
|
+
value: entry.value,
|
|
191
|
+
...(entry.source.file === undefined ? {} : { file: entry.source.file }),
|
|
192
|
+
...(entry.source.trust === undefined ? {} : { trust: entry.source.trust }),
|
|
193
|
+
}));
|
|
194
|
+
const resolution = policy === "boolean-false"
|
|
195
|
+
? resolveBooleanDeny(denyContributions)
|
|
196
|
+
: resolveArrayDeny(denyContributions);
|
|
197
|
+
const entry = denyProvenance(denyContributions, resolution, key);
|
|
198
|
+
if (entry)
|
|
199
|
+
context.provenance.set(key, entry);
|
|
200
|
+
// Plus one entry per surviving MEMBER of a union (#2427 review round 2,
|
|
201
|
+
// F2). The array's own entry answers "which tier denied first"; a caller
|
|
202
|
+
// asking about one member — "why is THIS server off" — needs the tier that
|
|
203
|
+
// contributed that member, and reading the array entry for it reported the
|
|
204
|
+
// wrong tier for every member after the first.
|
|
205
|
+
for (const member of denyMemberProvenance(denyContributions, resolution, key)) {
|
|
206
|
+
context.provenance.set(member.key, member);
|
|
207
|
+
}
|
|
208
|
+
return resolution.value;
|
|
209
|
+
}
|
|
210
|
+
function mergeObject(contributions, schema, key, context, depth) {
|
|
211
|
+
const objects = contributions.filter((entry) => isConfigObject(entry.value));
|
|
212
|
+
if (objects.length === 0) {
|
|
213
|
+
const winner = contributions[contributions.length - 1];
|
|
214
|
+
stamp(context, winner, key);
|
|
215
|
+
return winner.value;
|
|
216
|
+
}
|
|
217
|
+
// Field order: lowest tier's keys first, each nearer tier appending only the
|
|
218
|
+
// keys it introduces. A resolved config therefore reads in the order the
|
|
219
|
+
// user's own files introduced things.
|
|
220
|
+
//
|
|
221
|
+
// A prototype-modifying name is refused HERE, before it can become a child
|
|
222
|
+
// key: dropping it at the assignment alone would still have walked it and
|
|
223
|
+
// stamped provenance at a pointer no resolved value can be read from.
|
|
224
|
+
const names = [];
|
|
225
|
+
for (const entry of objects) {
|
|
226
|
+
for (const name of Object.keys(entry.value)) {
|
|
227
|
+
if (names.includes(name))
|
|
228
|
+
continue;
|
|
229
|
+
if (isUnsafeConfigKey(name)) {
|
|
230
|
+
record(context, {
|
|
231
|
+
code: "PILENS_CFG_0006",
|
|
232
|
+
key: `${key}/${name}`,
|
|
233
|
+
reason: UNSAFE_KEY_REASON,
|
|
234
|
+
});
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
names.push(name);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const out = {};
|
|
241
|
+
for (const name of names) {
|
|
242
|
+
const childKey = `${key}/${name}`;
|
|
243
|
+
const childContributions = [];
|
|
244
|
+
for (const entry of objects) {
|
|
245
|
+
const parent = entry.value;
|
|
246
|
+
if (!Object.hasOwn(parent, name))
|
|
247
|
+
continue;
|
|
248
|
+
childContributions.push({
|
|
249
|
+
source: entry.source,
|
|
250
|
+
value: parent[name],
|
|
251
|
+
rank: entry.rank,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
const merged = mergeNode(childContributions, propertySchema(schema, name), childKey, context, depth + 1);
|
|
255
|
+
if (merged !== undefined)
|
|
256
|
+
safeAssign(out, name, merged);
|
|
257
|
+
}
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
function mergeArray(contributions, schema, key, context, depth) {
|
|
261
|
+
const arrays = contributions.filter((entry) => Array.isArray(entry.value));
|
|
262
|
+
if (arrays.length === 0) {
|
|
263
|
+
const winner = contributions[contributions.length - 1];
|
|
264
|
+
stamp(context, winner, key);
|
|
265
|
+
return winner.value;
|
|
266
|
+
}
|
|
267
|
+
const strategy = mergeStrategyOf(schema);
|
|
268
|
+
const keyed = keyedField(strategy);
|
|
269
|
+
if (keyed !== undefined) {
|
|
270
|
+
return mergeKeyedArray(arrays, schema, keyed, key, context, depth);
|
|
271
|
+
}
|
|
272
|
+
if (strategy === "append")
|
|
273
|
+
return appendArrays(arrays, key, context);
|
|
274
|
+
// `replace`: the highest-precedence contributor supplies the whole array,
|
|
275
|
+
// and one provenance entry at the array's own pointer says who. Per-element
|
|
276
|
+
// entries would be noise: every element has the same answer.
|
|
277
|
+
const winner = arrays[arrays.length - 1];
|
|
278
|
+
stamp(context, winner, key);
|
|
279
|
+
return [...winner.value];
|
|
280
|
+
}
|
|
281
|
+
function appendArrays(arrays, key, context) {
|
|
282
|
+
const out = [];
|
|
283
|
+
for (const entry of arrays) {
|
|
284
|
+
for (const member of entry.value) {
|
|
285
|
+
stamp(context, entry, `${key}/${out.length}`);
|
|
286
|
+
out.push(member);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// The array itself is attributed to its lowest-precedence contributor: with
|
|
290
|
+
// `append`, that is the tier the list STARTED at.
|
|
291
|
+
stamp(context, arrays[0], key);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* `keyed:<field>` — match entries across tiers by `<field>` and merge them
|
|
296
|
+
* field-wise, so a project file can override one server's `command` without
|
|
297
|
+
* restating the whole catalog entry, and without erasing the sibling entries.
|
|
298
|
+
*
|
|
299
|
+
* Entries missing the key field cannot be matched, so they are appended in
|
|
300
|
+
* contribution order and attributed to their own source. Dropping them would be
|
|
301
|
+
* silent data loss; matching them together would merge unrelated entries.
|
|
302
|
+
*/
|
|
303
|
+
function mergeKeyedArray(arrays, schema, field, key, context, depth) {
|
|
304
|
+
const items = itemsSchema(schema);
|
|
305
|
+
const order = [];
|
|
306
|
+
const groups = new Map();
|
|
307
|
+
const unkeyed = [];
|
|
308
|
+
for (const entry of arrays) {
|
|
309
|
+
for (const member of entry.value) {
|
|
310
|
+
const identity = keyIdentity(member, field);
|
|
311
|
+
const memberContribution = {
|
|
312
|
+
source: entry.source,
|
|
313
|
+
value: member,
|
|
314
|
+
rank: entry.rank,
|
|
315
|
+
};
|
|
316
|
+
if (identity === undefined) {
|
|
317
|
+
unkeyed.push(memberContribution);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const group = groups.get(identity);
|
|
321
|
+
if (group) {
|
|
322
|
+
group.push(memberContribution);
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
order.push(identity);
|
|
326
|
+
groups.set(identity, [memberContribution]);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
const out = [];
|
|
330
|
+
const pushMerged = (group) => {
|
|
331
|
+
const merged = mergeNode(group, items, `${key}/${out.length}`, context, depth + 1);
|
|
332
|
+
// An entry can only merge to `undefined` when its group is empty, which
|
|
333
|
+
// the construction above cannot produce; dropping it keeps the resolved
|
|
334
|
+
// list free of holes either way.
|
|
335
|
+
if (merged !== undefined)
|
|
336
|
+
out.push(merged);
|
|
337
|
+
};
|
|
338
|
+
for (const identity of order)
|
|
339
|
+
pushMerged(groups.get(identity) ?? []);
|
|
340
|
+
for (const entry of unkeyed)
|
|
341
|
+
pushMerged([entry]);
|
|
342
|
+
// Attribute the list itself to its lowest-precedence contributor, matching
|
|
343
|
+
// `append`: a keyed list is a list every tier extends, not one a tier owns.
|
|
344
|
+
stamp(context, arrays[0], key);
|
|
345
|
+
return out;
|
|
346
|
+
}
|
|
347
|
+
function keyIdentity(member, field) {
|
|
348
|
+
if (!isConfigObject(member))
|
|
349
|
+
return undefined;
|
|
350
|
+
const value = member[field];
|
|
351
|
+
if (typeof value === "string")
|
|
352
|
+
return `s:${value}`;
|
|
353
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
354
|
+
return `n:${String(value)}`;
|
|
355
|
+
}
|
|
356
|
+
return undefined;
|
|
357
|
+
}
|