pi-lens 4.1.2 → 4.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1308 -0
- package/README.md +3 -0
- package/config/biome/core.jsonc +11 -2
- package/config/dependency-cruiser-eager-allowlist.json +91 -0
- package/dist/clients/actionable-warnings-logger.js +18 -3
- package/dist/clients/actionable-warnings.js +927 -80
- package/dist/clients/agent-behavior-client.js +13 -4
- package/dist/clients/ast-grep-client.js +136 -9
- package/dist/clients/ast-grep-rule-manager.js +60 -5
- package/dist/clients/ast-grep-tool-logger.js +2 -2
- package/dist/clients/availability-probe-flight.js +12 -0
- package/dist/clients/bash-file-access.js +1 -3
- package/dist/clients/biome-client.js +16 -2
- package/dist/clients/blocker-freshness.js +34 -0
- package/dist/clients/bootstrap.js +509 -73
- package/dist/clients/bounded-cache.js +152 -12
- package/dist/clients/bounded-telemetry.js +115 -23
- package/dist/clients/build-identity.js +95 -0
- package/dist/clients/bundled-resource-health.js +113 -0
- package/dist/clients/bus-events-logger.js +2 -2
- package/dist/clients/bus-publish.js +3 -0
- package/dist/clients/cache/rule-cache.js +33 -5
- package/dist/clients/cache-manager.js +105 -6
- package/dist/clients/cache-observability.js +46 -19
- package/dist/clients/cargo-manifest.js +422 -0
- package/dist/clients/cascade-logger.js +17 -3
- package/dist/clients/code-quality-warnings.js +13 -3
- package/dist/clients/complexity-client.js +16 -5
- package/dist/clients/config-core/deny.js +221 -0
- package/dist/clients/config-core/index.js +47 -0
- package/dist/clients/config-core/merge.js +357 -0
- package/dist/clients/config-core/normalize.js +340 -0
- package/dist/clients/config-core/process-spec.js +248 -0
- package/dist/clients/config-core/provenance.js +171 -0
- package/dist/clients/config-core/records.js +218 -0
- package/dist/clients/config-core/resolve.js +125 -0
- package/dist/clients/config-core/safe-object.js +78 -0
- package/dist/clients/config-core/schema.js +167 -0
- package/dist/clients/config-diagnostic-codes.js +281 -0
- package/dist/clients/config-locations.js +160 -0
- package/dist/clients/config-resolve.js +789 -0
- package/dist/clients/config-schema.js +197 -0
- package/dist/clients/config-warn.js +407 -0
- package/dist/clients/dead-code-client.js +33 -25
- package/dist/clients/dead-code-logger.js +2 -2
- package/dist/clients/deadline-utils.js +178 -0
- package/dist/clients/debug-handles.js +2 -2
- package/dist/clients/debug-heap.js +3 -3
- package/dist/clients/deferred-lsp-work.js +106 -0
- package/dist/clients/degradation-ledger.js +166 -8
- package/dist/clients/demoted-finding-render.js +14 -0
- package/dist/clients/dependency-checker.js +28 -22
- package/dist/clients/deps/minimatch.js +1 -1
- package/dist/clients/diagnostic-line-freshness.js +12 -9
- package/dist/clients/diagnostic-logger.js +14 -4
- package/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/dist/clients/dispatch/dispatcher.js +304 -19
- package/dist/clients/dispatch/fact-store.js +328 -9
- package/dist/clients/dispatch/integration.js +214 -107
- package/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/dist/clients/dispatch/plan.js +21 -5
- package/dist/clients/dispatch/runners/actionlint.js +0 -1
- package/dist/clients/dispatch/runners/ast-grep-napi.js +550 -77
- package/dist/clients/dispatch/runners/biome-check.js +0 -1
- package/dist/clients/dispatch/runners/cpp-check.js +0 -1
- package/dist/clients/dispatch/runners/credo.js +0 -1
- package/dist/clients/dispatch/runners/cue-vet.js +0 -1
- package/dist/clients/dispatch/runners/dart-analyze.js +0 -1
- package/dist/clients/dispatch/runners/detekt.js +0 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +0 -1
- package/dist/clients/dispatch/runners/elixir-check.js +0 -1
- package/dist/clients/dispatch/runners/eslint.js +5 -2
- package/dist/clients/dispatch/runners/fact-rules.js +0 -1
- package/dist/clients/dispatch/runners/fish-indent.js +0 -1
- package/dist/clients/dispatch/runners/gleam-check.js +0 -1
- package/dist/clients/dispatch/runners/go-vet.js +1 -3
- package/dist/clients/dispatch/runners/golangci-lint.js +0 -1
- package/dist/clients/dispatch/runners/hadolint.js +0 -1
- package/dist/clients/dispatch/runners/helm-lint.js +16 -4
- package/dist/clients/dispatch/runners/helm-render.js +54 -10
- package/dist/clients/dispatch/runners/htmlhint.js +0 -1
- package/dist/clients/dispatch/runners/index.js +1 -3
- package/dist/clients/dispatch/runners/javac.js +0 -1
- package/dist/clients/dispatch/runners/ktlint.js +0 -1
- package/dist/clients/dispatch/runners/lsp.js +37 -2
- package/dist/clients/dispatch/runners/markdownlint.js +4 -2
- package/dist/clients/dispatch/runners/mypy.js +0 -1
- package/dist/clients/dispatch/runners/oxlint.js +24 -22
- package/dist/clients/dispatch/runners/php-lint.js +0 -1
- package/dist/clients/dispatch/runners/phpstan.js +0 -1
- package/dist/clients/dispatch/runners/prisma-validate.js +0 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -1
- package/dist/clients/dispatch/runners/pyright.js +7 -4
- package/dist/clients/dispatch/runners/rubocop.js +0 -1
- package/dist/clients/dispatch/runners/ruff.js +0 -1
- package/dist/clients/dispatch/runners/rust-clippy.js +9 -6
- package/dist/clients/dispatch/runners/shellcheck.js +0 -1
- package/dist/clients/dispatch/runners/shfmt.js +0 -1
- package/dist/clients/dispatch/runners/spellcheck.js +0 -1
- package/dist/clients/dispatch/runners/spotbugs.js +0 -1
- package/dist/clients/dispatch/runners/sqlfluff.js +0 -1
- package/dist/clients/dispatch/runners/stylelint.js +0 -1
- package/dist/clients/dispatch/runners/swiftlint.js +0 -1
- package/dist/clients/dispatch/runners/taplo.js +0 -1
- package/dist/clients/dispatch/runners/terragrunt.js +0 -1
- package/dist/clients/dispatch/runners/tflint.js +0 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +0 -1
- package/dist/clients/dispatch/runners/trivy-config.js +0 -1
- package/dist/clients/dispatch/runners/utils/availability-policy.js +3 -0
- package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +3 -4
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +270 -105
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/dist/clients/dispatch/runners/utils/toolchain-availability.js +89 -26
- package/dist/clients/dispatch/runners/vale.js +0 -1
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/dist/clients/dispatch/runners/yamllint.js +0 -1
- package/dist/clients/dispatch/runners/zig-check.js +0 -1
- package/dist/clients/disposition-logger.js +3 -3
- package/dist/clients/effective-config.js +403 -0
- package/dist/clients/error-class.js +23 -0
- package/dist/clients/event-loop-hold.js +274 -0
- package/dist/clients/event-loop-monitor.js +66 -1
- package/dist/clients/extension-log.js +2 -2
- package/dist/clients/feature-hints.js +2 -1
- package/dist/clients/file-time.js +6 -1
- package/dist/clients/file-utils.js +266 -13
- package/dist/clients/finding-delivery-gate.js +55 -13
- package/dist/clients/format-service.js +6 -1
- package/dist/clients/formatters.js +303 -104
- package/dist/clients/freshness-cadence.js +17 -0
- package/dist/clients/generated-artifacts.js +114 -19
- package/dist/clients/generation-guard.js +4 -9
- package/dist/clients/git-guard.js +198 -48
- package/dist/clients/git-tracked-ignore.js +32 -1
- package/dist/clients/go-client.js +39 -0
- package/dist/clients/govulncheck-client.js +5 -1
- package/dist/clients/gradle-ktfmt-style.js +252 -0
- package/dist/clients/gzip-stage-write.js +7 -0
- package/dist/clients/hashline-anchor.js +424 -0
- package/dist/clients/hook-budgets.js +76 -0
- package/dist/clients/host-edit-normalize.js +5 -2
- package/dist/clients/host-ports.js +1 -1
- package/dist/clients/installer/index.js +200 -84
- package/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/dist/clients/instance-reaper.js +108 -243
- package/dist/clients/instance-registry-lock.js +145 -0
- package/dist/clients/instance-registry.js +435 -122
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/language-profile.js +4 -2
- package/dist/clients/language-registry.js +595 -0
- package/dist/clients/latency-logger.js +179 -3
- package/dist/clients/ledger-bounds.js +13 -0
- package/dist/clients/lens-config.js +141 -30
- package/dist/clients/lens-engine.js +30 -37
- package/dist/clients/lens-flag-registry.js +67 -6
- package/dist/clients/lens-map.js +3 -1
- package/dist/clients/log-cleanup.js +2 -2
- package/dist/clients/lsp/aggregation.js +3 -1
- package/dist/clients/lsp/cascade-tier.js +4 -0
- package/dist/clients/lsp/client.js +672 -49
- package/dist/clients/lsp/config.js +504 -156
- package/dist/clients/lsp/diagnostic-binding.js +10 -5
- package/dist/clients/lsp/edits.js +15 -2
- package/dist/clients/lsp/index.js +1179 -247
- package/dist/clients/lsp/jvm-runtime.js +5 -0
- package/dist/clients/lsp/language.js +18 -169
- package/dist/clients/lsp/launch.js +38 -9
- package/dist/clients/lsp/pending-aux-coverage.js +113 -12
- package/dist/clients/lsp/server.js +426 -160
- package/dist/clients/lsp/session-roots.js +192 -0
- package/dist/clients/lsp/spawn-history.js +2 -7
- package/dist/clients/lsp/tsserver-sync.js +9 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +17 -7
- package/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/dist/clients/lsp-mutation.js +179 -23
- package/dist/clients/map-with-concurrency.js +36 -0
- package/dist/clients/mcp/analyze.js +45 -10
- package/dist/clients/mcp/session.js +19 -17
- package/dist/clients/memory-sampler.js +28 -1
- package/dist/clients/message-end-attribution.js +30 -0
- package/dist/clients/metrics-history.js +21 -4
- package/dist/clients/middle-man-analysis.js +2 -4
- package/dist/clients/module-report.js +20 -45
- package/dist/clients/mutating-tool.js +651 -0
- package/dist/clients/mutation-attribution.js +368 -0
- package/dist/clients/mutation-bridge.js +240 -0
- package/dist/clients/ndjson-logger.js +247 -20
- package/dist/clients/observed-mutation-sources.js +101 -0
- package/dist/clients/observed-mutation.js +1215 -0
- package/dist/clients/opaque-mutation-scan.js +300 -40
- package/dist/clients/opengrep-config.js +4 -0
- package/dist/clients/package-manager.js +201 -44
- package/dist/clients/partial-edit-apply.js +359 -80
- package/dist/clients/path-attribution-telemetry.js +23 -7
- package/dist/clients/path-keyed-map.js +35 -3
- package/dist/clients/path-utils.js +460 -13
- package/dist/clients/persist-debounce.js +8 -1
- package/dist/clients/php-cs-fixer-config.js +114 -0
- package/dist/clients/pipeline.js +77 -24
- package/dist/clients/probe-home-state.js +228 -0
- package/dist/clients/process-bridge.js +66 -0
- package/dist/clients/process-singletons.js +210 -0
- package/dist/clients/process-snapshot.js +68 -0
- package/dist/clients/project-diagnostics/fresh-fetch.js +4 -3
- package/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +27 -3
- package/dist/clients/project-diagnostics/scanner.js +61 -24
- package/dist/clients/project-lens-config.js +393 -122
- package/dist/clients/project-snapshot.js +85 -35
- package/dist/clients/python-environment.js +217 -0
- package/dist/clients/python-provenance.js +639 -0
- package/dist/clients/quiet-window.js +6 -1
- package/dist/clients/read-bridge.js +13 -21
- package/dist/clients/read-expansion.js +9 -46
- package/dist/clients/read-guard-logger.js +39 -6
- package/dist/clients/read-guard-tool-lines.js +364 -212
- package/dist/clients/read-guard.js +95 -7
- package/dist/clients/recent-touches.js +17 -5
- package/dist/clients/resource-sampler.js +195 -96
- package/dist/clients/review-graph/builder.js +752 -132
- package/dist/clients/review-graph/service.js +55 -3
- package/dist/clients/review-graph/shared-extraction-ir.js +7 -15
- package/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/dist/clients/review-graph/workspace-modules.js +10 -74
- package/dist/clients/review-graph-logger.js +30 -4
- package/dist/clients/runtime-agent-end.js +18 -2
- package/dist/clients/runtime-context.js +49 -16
- package/dist/clients/runtime-coordinator.js +93 -6
- package/dist/clients/runtime-session.js +169 -34
- package/dist/clients/runtime-tool-call.js +183 -35
- package/dist/clients/runtime-tool-result.js +894 -241
- package/dist/clients/runtime-turn.js +1264 -74
- package/dist/clients/rust-client.js +31 -0
- package/dist/clients/safe-spawn.js +191 -28
- package/dist/clients/sanitize.js +7 -1
- package/dist/clients/scratch-tree-policy.js +1 -3
- package/dist/clients/security-scan-client.js +51 -17
- package/dist/clients/session-lifecycle.js +238 -42
- package/dist/clients/session-start-observability.js +79 -0
- package/dist/clients/sessionstart-logger.js +12 -3
- package/dist/clients/sg-runner.js +27 -15
- package/dist/clients/sgconfig.js +6 -1
- package/dist/clients/shared-checkout-guard.js +338 -0
- package/dist/clients/skills-resolver.js +105 -0
- package/dist/clients/smells-rollup.js +2 -2
- package/dist/clients/spawn-output-cap.js +37 -0
- package/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/dist/clients/startup-scan.js +6 -7
- package/dist/clients/startup-timing.js +69 -4
- package/dist/clients/string-utils.js +33 -0
- package/dist/clients/subagent-mode.js +17 -4
- package/dist/clients/test-runner-client.js +586 -118
- package/dist/clients/test-runner-delivery.js +239 -0
- package/dist/clients/tool-definition.js +41 -1
- package/dist/clients/tool-policy.js +92 -22
- package/dist/clients/tree-sitter-cache.js +40 -40
- package/dist/clients/tree-sitter-client.js +97 -53
- package/dist/clients/tree-sitter-logger.js +19 -3
- package/dist/clients/tree-sitter-query-loader.js +81 -1
- package/dist/clients/tree-sitter-shared.js +51 -46
- package/dist/clients/tree-sitter-symbol-extractor.js +28 -0
- package/dist/clients/typos-config.js +5 -0
- package/dist/clients/user-notify.js +6 -2
- package/dist/clients/warm-attach.js +8 -10
- package/dist/clients/widget-state.js +191 -26
- package/dist/clients/word-index-logger.js +15 -3
- package/dist/clients/word-index-store.js +463 -0
- package/dist/clients/word-index.js +815 -125
- package/dist/clients/workspace-topology.js +25 -1
- package/dist/clients/zizmor-config.js +29 -6
- package/dist/index.js +68577 -53950
- package/dist/mcp/analyze-cli.js +3 -1
- package/dist/mcp/build-staleness.js +8 -7
- package/dist/mcp/cli.js +3 -1
- package/dist/mcp/server.js +133 -6
- package/dist/scripts/lib/process-scan.mjs +583 -0
- package/dist/scripts/lib/skills-predicate.mjs +129 -0
- package/dist/tools/effective-config.js +89 -0
- package/dist/tools/lens-diagnostics.js +99 -24
- package/dist/tools/lsp-diagnostics.js +81 -85
- package/dist/tools/lsp-navigation.js +40 -17
- package/dist/tools/shared.js +0 -1
- package/docs/agent-guide.md +68 -3
- package/docs/ast-grep_rules_catalog.md +41 -10
- package/docs/astplayground.md +26 -17
- package/docs/configuration.md +222 -0
- package/docs/dependencies.md +3 -3
- package/docs/features.md +44 -5
- package/docs/language-coverage.md +2 -2
- package/docs/pi-lens-fixer.md +25 -0
- package/docs/pi-lens-investigator.md +25 -0
- package/docs/pi-lens-reviewer.md +27 -0
- package/docs/pi-lens-subagent.md +38 -0
- package/docs/pi-lens-warden.md +55 -0
- package/docs/public-api-stability.md +359 -0
- package/docs/release-qa-baseline.md +182 -0
- package/docs/subagent-compat.md +110 -30
- package/docs/tree-sitter_rules_catalog.md +2 -2
- package/package.json +17 -21
- package/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/rules/tree-sitter-queries/python/python-cross-language-method.yml +3 -3
- package/rules/tree-sitter-queries/python/python-hallucinated-import.yml +2 -2
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +12 -2
- package/scripts/analyze-pi-lens-logs.mjs +192 -1
- package/scripts/install-selftest.mjs +58 -2
- package/scripts/lib/skills-predicate.mjs +129 -0
- package/scripts/rpc-load-check.mjs +3 -1
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
import * as path from "node:path";
|
|
2
|
+
import { BoundedSet } from "./bounded-cache.js";
|
|
2
3
|
import { createReadGuardEditBatchSummary, getReadGuardCorrelationId, logReadGuardEvent, } from "./read-guard-logger.js";
|
|
3
4
|
import { normalizeMapKey } from "./path-utils.js";
|
|
5
|
+
import { getMutationBridge } from "./mutation-bridge.js";
|
|
6
|
+
import { noteMutationHandled } from "./observed-mutation.js";
|
|
7
|
+
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
8
|
+
// #2450 fix round 3 (minor): gates the "bridge unavailable" dbg line to fire
|
|
9
|
+
// once per session, not once per FILE — a rename that touches many files in
|
|
10
|
+
// a bridge-less process (e.g. the MCP server) would otherwise spam the same
|
|
11
|
+
// line once per touched file. `recordDegradationOnce` itself has no
|
|
12
|
+
// first-occurrence return value (unlike `incrementDegradationCount`), so this
|
|
13
|
+
// tiny local latch is the minimal way to mirror that "once" intent onto the
|
|
14
|
+
// dbg line without changing the ledger's own once-per-(kind,subject) record.
|
|
15
|
+
//
|
|
16
|
+
// Catalog shape 17 (a process-lifetime latch must re-arm at session_start):
|
|
17
|
+
// wired into `handleSessionStart` (`clients/runtime-session.ts`) directly,
|
|
18
|
+
// NOT into `resetDegradationLedger()` — that lives in `degradation-ledger.ts`,
|
|
19
|
+
// which this module already imports, and reaching back from there to here
|
|
20
|
+
// would be a cycle.
|
|
21
|
+
let noBridgeDbgLogged = false;
|
|
22
|
+
export function resetLspMutationNoBridgeDbgLatch() {
|
|
23
|
+
noBridgeDbgLogged = false;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Test-only peek at the latch's current value (#2450 fix round 5, F2) — lets
|
|
27
|
+
* the session-state registry probe the actual internal state instead of
|
|
28
|
+
* inferring it from captured dbg messages, the same pattern every other
|
|
29
|
+
* process-private-scalar probe in `tests/support/session-state-registry.ts`
|
|
30
|
+
* uses (e.g. `_observedMutationStateForTests`).
|
|
31
|
+
*/
|
|
32
|
+
export function _lspMutationNoBridgeDbgLoggedForTests() {
|
|
33
|
+
return noBridgeDbgLogged;
|
|
34
|
+
}
|
|
4
35
|
const MAX_SAMPLES = 100;
|
|
5
36
|
const MAX_SUMMARIES_PER_CONTEXT = 100;
|
|
6
37
|
export function newLspMutationCorrelationId(toolCallId) {
|
|
@@ -52,8 +83,22 @@ function combineResults(results) {
|
|
|
52
83
|
}
|
|
53
84
|
function uniqueDetails(files, fileDetails) {
|
|
54
85
|
const byPath = new Map();
|
|
86
|
+
// #2016: `files` and `fileDetails` name the same paths, so without this the
|
|
87
|
+
// map-build loop and the lookup below each pay `realpathSync.native` for the
|
|
88
|
+
// same path (~200 microseconds per call on Windows; POSIX short-circuits, so
|
|
89
|
+
// CI cannot see it). The memo lives for one call, so it has no staleness
|
|
90
|
+
// window at all and needs no freshness design.
|
|
91
|
+
const keyMemo = new Map();
|
|
92
|
+
const keyFor = (filePath) => {
|
|
93
|
+
let key = keyMemo.get(filePath);
|
|
94
|
+
if (key === undefined) {
|
|
95
|
+
key = normalizeMapKey(path.resolve(filePath));
|
|
96
|
+
keyMemo.set(filePath, key);
|
|
97
|
+
}
|
|
98
|
+
return key;
|
|
99
|
+
};
|
|
55
100
|
for (const detail of fileDetails) {
|
|
56
|
-
const key =
|
|
101
|
+
const key = keyFor(detail.filePath);
|
|
57
102
|
const previous = byPath.get(key);
|
|
58
103
|
if (!previous) {
|
|
59
104
|
byPath.set(key, detail);
|
|
@@ -70,7 +115,7 @@ function uniqueDetails(files, fileDetails) {
|
|
|
70
115
|
importsChanged: previous.importsChanged || detail.importsChanged,
|
|
71
116
|
});
|
|
72
117
|
}
|
|
73
|
-
return files.map((filePath) => byPath.get(
|
|
118
|
+
return files.map((filePath) => byPath.get(keyFor(filePath)) ?? {
|
|
74
119
|
filePath,
|
|
75
120
|
// Resource operations have no already-computed text range. A small
|
|
76
121
|
// range is still enough to invalidate the touched-file turn state;
|
|
@@ -81,8 +126,35 @@ function uniqueDetails(files, fileDetails) {
|
|
|
81
126
|
}
|
|
82
127
|
function bookkeepLspMutation(context, files, fileDetails) {
|
|
83
128
|
const details = uniqueDetails(files, fileDetails);
|
|
129
|
+
// The direct path below needs `context.runtime` (the receipt) and
|
|
130
|
+
// `context.cacheManager` (the turn-state entry) threaded in by the caller.
|
|
131
|
+
// One caller structurally cannot: `workspace/applyEdit`'s server-initiated-edit
|
|
132
|
+
// handler (clients/lsp/client.ts) builds its own fallback `LspMutationContext`
|
|
133
|
+
// with neither, because there is no live reference to the runtime/cache-manager
|
|
134
|
+
// singletons at that call site. Rather than silently drop bookkeeping for that
|
|
135
|
+
// write, fall back to the mutation bridge (#2423) — mounted once at extension
|
|
136
|
+
// activation with live getters closing over the SAME singletons — which is
|
|
137
|
+
// exactly the seam built for "a producer that cannot reach the bookkeeping
|
|
138
|
+
// surfaces directly". This keeps `recordLspMutation` the ONE call site every
|
|
139
|
+
// LSP-applied edit bookkeeps through; the bridge is this function's OWN
|
|
140
|
+
// fallback, not a second seam other code reaches for on its behalf (#2450).
|
|
141
|
+
// Only the receipt (recordProjectMutation) and the turn-state entry
|
|
142
|
+
// (cacheManager.addModifiedRange) need the bridge fallback: the read-guard
|
|
143
|
+
// stamp is independent of them and already tolerates either being absent.
|
|
144
|
+
const useBridgeFallback = !context.runtime || !context.cacheManager;
|
|
84
145
|
for (const detail of details) {
|
|
85
146
|
const filePath = path.resolve(detail.filePath);
|
|
147
|
+
// #2450 review round 2 (F4): the SAME recordability gate the bridge
|
|
148
|
+
// fallback below already applies internally (`isRecordable`, mounted in
|
|
149
|
+
// index.ts). Applied here too so the direct (deps-threaded) branch
|
|
150
|
+
// cannot record a write the fallback branch would have silently
|
|
151
|
+
// dropped for the exact same reason (`no-read-guard` / ignored /
|
|
152
|
+
// vendor) — the two branches must agree on which files count as
|
|
153
|
+
// project source, not just on how they bookkeep the ones that do.
|
|
154
|
+
if (context.isRecordable && !context.isRecordable(filePath)) {
|
|
155
|
+
context.dbg?.(`lsp mutation not recordable, skipping ${filePath}`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
86
158
|
if (context.readGuard) {
|
|
87
159
|
try {
|
|
88
160
|
context.readGuard.recordWritten(filePath);
|
|
@@ -91,34 +163,118 @@ function bookkeepLspMutation(context, files, fileDetails) {
|
|
|
91
163
|
context.dbg?.(`lsp mutation read-guard stamp failed for ${filePath}: ${err}`);
|
|
92
164
|
}
|
|
93
165
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
166
|
+
if (useBridgeFallback) {
|
|
167
|
+
const bridge = getMutationBridge();
|
|
168
|
+
if (!bridge) {
|
|
169
|
+
// #2450 review round 2 (F4)/round 3 (minor): the MCP server process
|
|
170
|
+
// (`mcp/server.ts`) builds `lsp_navigation` with no
|
|
171
|
+
// `mutationDeps` AND never mounts the bridge (that only happens
|
|
172
|
+
// inside pi's own in-process extension activation, `index.ts`) —
|
|
173
|
+
// every LSP-applied edit in that process is unrecorded. Once per
|
|
174
|
+
// session (not once per file — a rename can touch many files in
|
|
175
|
+
// one call, and a session can issue many renames), not once per
|
|
176
|
+
// process lifetime silently. `subject` is a fixed, one-word
|
|
177
|
+
// degradation-ledger key (round 3: was `context.tool`, which
|
|
178
|
+
// varies per LSP operation — "lsp_navigation:rename" vs
|
|
179
|
+
// "...executeCommand" — so the SAME session could log this
|
|
180
|
+
// degradation once per operation kind instead of truly once).
|
|
181
|
+
recordDegradationOnce({
|
|
182
|
+
kind: "lsp-mutation-bridge-unmounted",
|
|
183
|
+
subject: "lsp-mutation:no-bridge",
|
|
184
|
+
reason: "workspace/applyEdit bookkeeping fell back to the mutation " +
|
|
185
|
+
"bridge, but no bridge is mounted in this process (e.g. the " +
|
|
186
|
+
"MCP server, which never runs pi's extension activation) — " +
|
|
187
|
+
"the write is unrecorded (no read-guard stamp, no turn-state " +
|
|
188
|
+
"entry, no change-log receipt).",
|
|
189
|
+
});
|
|
190
|
+
if (!noBridgeDbgLogged) {
|
|
191
|
+
noBridgeDbgLogged = true;
|
|
192
|
+
context.dbg?.(`lsp mutation bridge unavailable, dropping bookkeeping for ${filePath}`);
|
|
193
|
+
}
|
|
107
194
|
}
|
|
108
|
-
|
|
109
|
-
|
|
195
|
+
else {
|
|
196
|
+
try {
|
|
197
|
+
const recorded = bridge.recordMutation({
|
|
198
|
+
filePath,
|
|
199
|
+
kind: "edit",
|
|
200
|
+
editRanges: detail.range
|
|
201
|
+
? [[detail.range.start, detail.range.end]]
|
|
202
|
+
: undefined,
|
|
203
|
+
consumer: context.tool,
|
|
204
|
+
// Real value threaded through, not the bridge's own
|
|
205
|
+
// historical `false` default (#2450 review round 2, F1) —
|
|
206
|
+
// the tsserver organize-imports/add-import case is exactly
|
|
207
|
+
// the import-changing one.
|
|
208
|
+
importsChanged: detail.importsChanged ?? true,
|
|
209
|
+
// Equivalence with the direct branch below, which never
|
|
210
|
+
// enqueues a deferred autofix/format pass for an LSP-applied
|
|
211
|
+
// edit (#2450 review round 2, F3).
|
|
212
|
+
deferAutofix: false,
|
|
213
|
+
});
|
|
214
|
+
if (!recorded) {
|
|
215
|
+
context.dbg?.(`lsp mutation bridge dropped ${filePath} (recordMutation() ` +
|
|
216
|
+
"returned false — see the mutation_bridge debug output " +
|
|
217
|
+
"for the reason: malformed entry, out-of-scope path, or " +
|
|
218
|
+
"a bookkeeping error)");
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
context.dbg?.(`lsp mutation bridge fallback failed for ${filePath}: ${err}`);
|
|
223
|
+
}
|
|
110
224
|
}
|
|
111
225
|
}
|
|
226
|
+
else {
|
|
227
|
+
const runtime = context.runtime;
|
|
228
|
+
// One mutation seam (#2000 phase 1): bump + receipt + change-log live in
|
|
229
|
+
// RuntimeCoordinator.recordProjectMutation.
|
|
230
|
+
runtime?.recordProjectMutation?.({
|
|
231
|
+
filePath,
|
|
232
|
+
source: context.source,
|
|
233
|
+
cwd: context.cwd,
|
|
234
|
+
changedRange: detail.range,
|
|
235
|
+
onAppendError: (err) => context.dbg?.(`lsp mutation project change append failed for ${filePath}: ${err}`),
|
|
236
|
+
});
|
|
237
|
+
if (context.cacheManager) {
|
|
238
|
+
try {
|
|
239
|
+
context.cacheManager.addModifiedRange(filePath, detail.range ?? { start: 1, end: 1 }, detail.importsChanged ?? true, context.cwd, runtime?.telemetrySessionId, undefined,
|
|
240
|
+
// #2504 review round 2 (F1). #2504 added a containment
|
|
241
|
+
// filter to `addModifiedRange` and gave it `cwd` as the
|
|
242
|
+
// project root. On THIS path `cwd` is the calling
|
|
243
|
+
// directory, which `tools/lsp-navigation.ts` scopes to a
|
|
244
|
+
// sub-package while the LSP client's root spans the whole
|
|
245
|
+
// monorepo — so a server-initiated edit on a sibling
|
|
246
|
+
// package (#2450) and every edit whose context carries a
|
|
247
|
+
// separate bookkeeping cwd (#2479) were silently dropped
|
|
248
|
+
// from the worklist. Same root the `isRecordable` gate a
|
|
249
|
+
// few frames up already uses: `runtime.projectRoot`,
|
|
250
|
+
// falling back to `cwd` when no runtime is threaded.
|
|
251
|
+
runtime?.projectRoot ?? context.cwd);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
context.dbg?.(`lsp mutation modified-range tracking failed for ${filePath}: ${err}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
// #2465: the bridge-fallback branch above gets this for free —
|
|
258
|
+
// `recordMutationThroughSeam` calls `noteMutationHandled` for every
|
|
259
|
+
// entry it takes. The DIRECT branch bypasses the bridge entirely, so
|
|
260
|
+
// it has to say so itself, or the #2430 `agent_settled` sweep finds
|
|
261
|
+
// bytes on disk it has no baseline for and reports every
|
|
262
|
+
// `lsp_navigation` rename as drift no tool call explains. Placed
|
|
263
|
+
// after `addModifiedRange` and OUTSIDE its `try`: the mark is about
|
|
264
|
+
// "pi-lens wrote this file", which is true whether or not the
|
|
265
|
+
// turn-state insert threw.
|
|
266
|
+
noteMutationHandled(filePath);
|
|
267
|
+
}
|
|
268
|
+
// #2450 review round 2 (Partial-deps): its own surface, independent of
|
|
269
|
+
// whether the receipt/turn-state used the direct path or the bridge
|
|
270
|
+
// fallback above — a half-threaded caller (one that provides
|
|
271
|
+
// `recordAutofix` but not `runtime`/`cacheManager`) must not lose its
|
|
272
|
+
// turn-summary publisher just because SOME other surface fell back.
|
|
112
273
|
if (context.recordAutofix && context.source === "autofix") {
|
|
113
274
|
const key = normalizeMapKey(filePath);
|
|
114
|
-
const seen = context.autofixRecordedPaths ?? new
|
|
275
|
+
const seen = context.autofixRecordedPaths ?? new BoundedSet(MAX_SAMPLES);
|
|
115
276
|
context.autofixRecordedPaths = seen;
|
|
116
277
|
if (!seen.has(key)) {
|
|
117
|
-
if (seen.size >= MAX_SAMPLES) {
|
|
118
|
-
const oldest = seen.values().next().value;
|
|
119
|
-
if (oldest)
|
|
120
|
-
seen.delete(oldest);
|
|
121
|
-
}
|
|
122
278
|
seen.add(key);
|
|
123
279
|
context.recordAutofix(filePath);
|
|
124
280
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded worker pool — the repo's one shared `mapWithConcurrency`.
|
|
3
|
+
*
|
|
4
|
+
* Lived in `dependency-checker.ts` (its first caller) and was exported from
|
|
5
|
+
* there for `dispatch/runners/biome-check.ts`. #2504 added a third caller,
|
|
6
|
+
* `runtime-turn.ts`'s bounded test-runner batch — and runtime-turn must not
|
|
7
|
+
* drag an ANALYZER client into its eager import graph for a 15-line helper
|
|
8
|
+
* (that is exactly the eager-bootstrap cost #2467 removed). So the value moved
|
|
9
|
+
* to this zero-dependency leaf and `dependency-checker.ts` re-exports it: every
|
|
10
|
+
* existing importer keeps its specifier, and a consumer that only needs the
|
|
11
|
+
* pool no longer loads madge.
|
|
12
|
+
*
|
|
13
|
+
* Same shape as `ledger-bounds.ts` (#2426) for the degradation ledger's bound.
|
|
14
|
+
*
|
|
15
|
+
* NOT consolidated here: `lsp/client.ts` keeps its own file-local copy on
|
|
16
|
+
* purpose (see its comment) — that is a documented decision, not drift.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Run `mapper` over `items` with at most `concurrency` in flight at once.
|
|
20
|
+
*/
|
|
21
|
+
export async function mapWithConcurrency(items, concurrency, mapper) {
|
|
22
|
+
if (items.length === 0)
|
|
23
|
+
return;
|
|
24
|
+
let nextIndex = 0;
|
|
25
|
+
const workerCount = Math.max(1, Math.min(concurrency, items.length));
|
|
26
|
+
const worker = async () => {
|
|
27
|
+
while (true) {
|
|
28
|
+
const index = nextIndex++;
|
|
29
|
+
if (index >= items.length)
|
|
30
|
+
return;
|
|
31
|
+
await mapper(items[index]);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
35
|
+
await Promise.all(workers);
|
|
36
|
+
}
|
|
@@ -23,8 +23,8 @@ import { PathKeyedMap } from "../path-keyed-map.js";
|
|
|
23
23
|
import { normalizeMapKey } from "../path-utils.js";
|
|
24
24
|
import { loadProjectSnapshot } from "../project-snapshot.js";
|
|
25
25
|
import { buildOrUpdateGraph } from "../review-graph/service.js";
|
|
26
|
-
import { recordDiagnostics } from "../widget-state.js";
|
|
27
|
-
import { deserializeWordIndex,
|
|
26
|
+
import { classifyDiagnosticTier, recordDiagnostics } from "../widget-state.js";
|
|
27
|
+
import { deserializeWordIndex, removeWordIndexDocumentAsync, scheduleWordIndexPersist, updateWordIndexDocumentForEdit, WORD_INDEX_MAX_BYTES, } from "../word-index.js";
|
|
28
28
|
import { logWordIndex } from "../word-index-logger.js";
|
|
29
29
|
import { createMcpHost } from "./host-shim.js";
|
|
30
30
|
// #536: module-scoped FactStore for the warm-analyze graph seam, mirroring the
|
|
@@ -33,7 +33,9 @@ import { createMcpHost } from "./host-shim.js";
|
|
|
33
33
|
// tiers key off a stable FactStore instance across calls, so a fresh FactStore
|
|
34
34
|
// per call would defeat that reuse. Scoped separately from integration.ts's
|
|
35
35
|
// singleton since this file has no dependency on that module's internal state.
|
|
36
|
-
|
|
36
|
+
// Subject labels this store's capacity-eviction telemetry distinctly from
|
|
37
|
+
// the other five production FactStore instances (#2243 review round 3, F1).
|
|
38
|
+
const warmGraphFacts = new FactStore("mcp-analyze");
|
|
37
39
|
// #536 rider (issue body: "when #348 phase 2 lands, the word-index per-edit
|
|
38
40
|
// update should ride the SAME seam so both indexes stay warm together"):
|
|
39
41
|
// MCP has no RuntimeCoordinator/`runtime.wordIndex` to hold a live index the
|
|
@@ -168,6 +170,28 @@ export function _getWarmWordIndexCacheStateForTests() {
|
|
|
168
170
|
// is the persistent warm server's job (see the `lsp` honesty signal + Tier 2).
|
|
169
171
|
const WARMUP_CLIENT_WAIT_MS = 10_000;
|
|
170
172
|
const WARMUP_DIAGNOSTICS_WAIT_MS = 6_000;
|
|
173
|
+
/** One diagnostic, flattened to the fields an MCP consumer needs. */
|
|
174
|
+
/**
|
|
175
|
+
* Split the dispatch `warnings` bucket into real warnings vs advisories using
|
|
176
|
+
* the ONE severity-projection policy ({@link classifyDiagnosticTier}, #2414/
|
|
177
|
+
* #2417) so the model-facing counts never present a `hint`/`info`-tier finding
|
|
178
|
+
* as a warning (#2420, remainder of #2414). The dispatch `semantic` axis (which
|
|
179
|
+
* also drives blocking) is intentionally NOT consulted or mutated here: this is
|
|
180
|
+
* a display-count split, not a blocking-decision change. `blockers`
|
|
181
|
+
* (`semantic:"blocking"`) are computed upstream and never enter this bucket, so
|
|
182
|
+
* nothing that blocks can stop blocking through this seam. A non-advisory entry
|
|
183
|
+
* in the bucket (an `error`/`warning`-tier finding routed here as
|
|
184
|
+
* `semantic:"warning"`/`"none"`) stays counted as a warning exactly as before,
|
|
185
|
+
* so a mixed file's real warning count is unchanged.
|
|
186
|
+
*/
|
|
187
|
+
export function summarizeWarningTiers(warnings) {
|
|
188
|
+
let advisories = 0;
|
|
189
|
+
for (const w of warnings) {
|
|
190
|
+
if (classifyDiagnosticTier(w) === "advisory")
|
|
191
|
+
advisories++;
|
|
192
|
+
}
|
|
193
|
+
return { warnings: warnings.length - advisories, advisories };
|
|
194
|
+
}
|
|
171
195
|
function toMcpDiagnostic(diagnostic) {
|
|
172
196
|
return {
|
|
173
197
|
line: diagnostic.line,
|
|
@@ -233,7 +257,7 @@ export async function analyzeFile(filePath, cwd, options = {}) {
|
|
|
233
257
|
: path.resolve(cwd, filePath);
|
|
234
258
|
// no-delta by default → a full snapshot every call (not delta-filtered);
|
|
235
259
|
// caller flags win over the default.
|
|
236
|
-
const host = createMcpHost({ "no-delta": true, ...
|
|
260
|
+
const host = createMcpHost({ "no-delta": true, ...options.flags }, cwd);
|
|
237
261
|
if (options.warmLsp !== false) {
|
|
238
262
|
await warmLspForFile(absPath, host);
|
|
239
263
|
}
|
|
@@ -295,8 +319,9 @@ export async function analyzeFile(filePath, cwd, options = {}) {
|
|
|
295
319
|
// index with no `forward` map (or none loaded) ⇒ no-op (no incremental
|
|
296
320
|
// primitive available — the eventual full rebuild covers it); an
|
|
297
321
|
// oversized file is REMOVED, never partially indexed; the update is
|
|
298
|
-
//
|
|
299
|
-
//
|
|
322
|
+
// cooperative and serialized through the index's own operation queue
|
|
323
|
+
// (#2067 AC4), so this seam no longer holds the event loop for a whole
|
|
324
|
+
// replacement either; a successful update schedules the SAME debounced persist
|
|
300
325
|
// (`scheduleWordIndexPersist`, `PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS`)
|
|
301
326
|
// pi's path uses — no second persist mechanism.
|
|
302
327
|
//
|
|
@@ -313,10 +338,13 @@ export async function analyzeFile(filePath, cwd, options = {}) {
|
|
|
313
338
|
const content = fs.readFileSync(absPath, "utf8");
|
|
314
339
|
const byteLength = Buffer.byteLength(content, "utf-8");
|
|
315
340
|
if (byteLength > WORD_INDEX_MAX_BYTES) {
|
|
316
|
-
|
|
341
|
+
await removeWordIndexDocumentAsync(warmIndex, absPath);
|
|
317
342
|
}
|
|
318
343
|
else {
|
|
319
|
-
|
|
344
|
+
await updateWordIndexDocumentForEdit(warmIndex, {
|
|
345
|
+
path: absPath,
|
|
346
|
+
content,
|
|
347
|
+
});
|
|
320
348
|
}
|
|
321
349
|
scheduleWordIndexPersist(cwd, warmIndex);
|
|
322
350
|
}
|
|
@@ -339,7 +367,9 @@ export async function analyzeFile(filePath, cwd, options = {}) {
|
|
|
339
367
|
const lspRunner = latencyReport?.runners.find((runner) => runner.runnerId === "lsp");
|
|
340
368
|
const lsp = lspRunner
|
|
341
369
|
? {
|
|
342
|
-
ran: lspRunner.status !== "skipped" &&
|
|
370
|
+
ran: lspRunner.status !== "skipped" &&
|
|
371
|
+
lspRunner.status !== "when_skipped" &&
|
|
372
|
+
lspRunner.status !== "test_file_skipped",
|
|
343
373
|
status: lspRunner.status,
|
|
344
374
|
diagnosticCount: lspRunner.diagnosticCount,
|
|
345
375
|
durationMs: lspRunner.durationMs,
|
|
@@ -354,7 +384,12 @@ export async function analyzeFile(filePath, cwd, options = {}) {
|
|
|
354
384
|
counts: {
|
|
355
385
|
diagnostics: result.diagnostics.length,
|
|
356
386
|
blockers: result.blockers.length,
|
|
357
|
-
|
|
387
|
+
// #2420: `result.warnings` is the dispatch warnings bucket
|
|
388
|
+
// (`semantic:"warning"|"none"`), which still carries `hint`/`info`-tier
|
|
389
|
+
// findings folded in by the runner's severity→semantic map. Project
|
|
390
|
+
// them out through the shared tier policy so the model sees real
|
|
391
|
+
// warnings and advisories separately.
|
|
392
|
+
...summarizeWarningTiers(result.warnings),
|
|
358
393
|
fixed: result.fixed.length,
|
|
359
394
|
},
|
|
360
395
|
lsp,
|
|
@@ -20,7 +20,7 @@ import * as crypto from "node:crypto";
|
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
22
|
import { AstGrepClient } from "../ast-grep-client.js";
|
|
23
|
-
import { loadBootstrapClients } from "../bootstrap.js";
|
|
23
|
+
import { loadBootstrapClients, residentBootstrapAccess, } from "../bootstrap.js";
|
|
24
24
|
import { CacheManager, MCP_TURN_STATE_OWNER_ID, } from "../cache-manager.js";
|
|
25
25
|
import { resetDispatchBaselines } from "../dispatch/integration.js";
|
|
26
26
|
import { resetFormatService } from "../format-service.js";
|
|
@@ -96,23 +96,12 @@ export async function runSessionStart(cwd) {
|
|
|
96
96
|
dbg: noop,
|
|
97
97
|
log: noop,
|
|
98
98
|
runtime: ctx.runtime,
|
|
99
|
-
metricsClient: ctx.clients.metricsClient,
|
|
100
99
|
cacheManager: ctx.cacheManager,
|
|
101
|
-
todoScanner: ctx.clients.todoScanner,
|
|
102
100
|
astGrepClient: ctx.astGrepClient,
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
deadCodeClients: ctx.clients.deadCodeClients,
|
|
108
|
-
govulncheckClient: ctx.clients.govulncheckClient,
|
|
109
|
-
gitleaksClient: ctx.clients.gitleaksClient,
|
|
110
|
-
trivyClient: ctx.clients.trivyClient,
|
|
111
|
-
opengrepClient: ctx.clients.opengrepClient,
|
|
112
|
-
depChecker: ctx.clients.depChecker,
|
|
113
|
-
testRunnerClient: ctx.clients.testRunnerClient,
|
|
114
|
-
goClient: ctx.clients.goClient,
|
|
115
|
-
rustClient: ctx.clients.rustClient,
|
|
101
|
+
// The MCP server has already loaded the analyzer graph (an MCP call has
|
|
102
|
+
// nothing to defer to), so it hands the handler the same one-shape seam
|
|
103
|
+
// every other caller uses rather than a second, field-by-field shape.
|
|
104
|
+
bootstrap: residentBootstrapAccess(ctx.clients),
|
|
116
105
|
ensureTool: async (name) => (await import("../installer/index.js")).ensureTool(name),
|
|
117
106
|
cleanStaleTsBuildInfo: () => [],
|
|
118
107
|
resetDispatchBaselines: (cwdArg) => resetDispatchBaselines(cwdArg ?? cwd),
|
|
@@ -309,8 +298,13 @@ export function runTurnEndForIpc(cwd) {
|
|
|
309
298
|
const inFlight = inFlightIpcTurnEnds.get(cwd);
|
|
310
299
|
if (inFlight)
|
|
311
300
|
return inFlight;
|
|
301
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
302
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
303
|
+
// evict a live successor a second writer registered under the same cwd
|
|
304
|
+
// mid-flight, after which the next caller starts a duplicate Stop-hook pass.
|
|
312
305
|
const run = runTurnEndForIpcNow(cwd).finally(() => {
|
|
313
|
-
inFlightIpcTurnEnds.
|
|
306
|
+
if (inFlightIpcTurnEnds.get(cwd) === run)
|
|
307
|
+
inFlightIpcTurnEnds.delete(cwd);
|
|
314
308
|
});
|
|
315
309
|
inFlightIpcTurnEnds.set(cwd, run);
|
|
316
310
|
return run;
|
|
@@ -374,3 +368,11 @@ export function _resetTurnEndChain() {
|
|
|
374
368
|
pendingTurnEndDeliveries.clear();
|
|
375
369
|
inFlightIpcTurnEnds.clear();
|
|
376
370
|
}
|
|
371
|
+
/**
|
|
372
|
+
* Test hook — read the `runTurnEndForIpc` in-flight map directly (#1968's
|
|
373
|
+
* ABA regression: a second writer replacing an entry mid-flight, then a
|
|
374
|
+
* late-settling first run evicting it with a bare delete-by-key).
|
|
375
|
+
*/
|
|
376
|
+
export function _peekInFlightIpcTurnEnds() {
|
|
377
|
+
return inFlightIpcTurnEnds;
|
|
378
|
+
}
|
|
@@ -29,9 +29,11 @@
|
|
|
29
29
|
* `arrayBuffers` is used as the process-wide proxy instead — WASM linear
|
|
30
30
|
* memory backs an ArrayBuffer, so it is already included there.
|
|
31
31
|
*/
|
|
32
|
+
import { countPostingEntries, estimateWordIndexStoreBytes, } from "./word-index-store.js";
|
|
32
33
|
import { getSharedTreeSitterClient } from "./tree-sitter-shared.js";
|
|
33
34
|
import { getReviewGraphWorkspaceCacheSnapshot } from "./review-graph/builder.js";
|
|
34
35
|
import { getDispatchCascadeCacheStats } from "./dispatch/integration.js";
|
|
36
|
+
import { getLspDocumentTextRetentionSnapshot } from "./lsp/client.js";
|
|
35
37
|
/** Every N turns, emit one `memory_sample` latency.log line (#1123 item 2). */
|
|
36
38
|
export const MEMORY_SAMPLE_TURN_INTERVAL = 10;
|
|
37
39
|
/** True on turn 10, 20, 30, ... — never on turn 0 (nothing meaningful is
|
|
@@ -104,6 +106,13 @@ export function toMemoryProcessUsage(mem, resourceUsage) {
|
|
|
104
106
|
* process-global singletons) but every individual read is a `.size`/`.length`
|
|
105
107
|
* access — see the module docstring's hard constraint.
|
|
106
108
|
*/
|
|
109
|
+
/** Byte attribution for `dispatchCaches`, from measured per-entry costs: three
|
|
110
|
+
* isolated-store runs put a recently-clean neighbor entry at 320 bytes, and
|
|
111
|
+
* #2282 measured a 500-file batch's 1,000 session-fact entries at 439,560
|
|
112
|
+
* bytes (~440 each). O(1) — the cache key and Map bookkeeping are included. */
|
|
113
|
+
export function estimateDispatchCacheBytes(stats) {
|
|
114
|
+
return (stats.recentlyCleanNeighborCacheSize * 320 + stats.sessionFactEntries * 440);
|
|
115
|
+
}
|
|
107
116
|
export function collectMemorySampleSubsystems(wordIndex) {
|
|
108
117
|
const reviewGraph = getReviewGraphWorkspaceCacheSnapshot();
|
|
109
118
|
const treeSitterClient = getSharedTreeSitterClient();
|
|
@@ -121,16 +130,34 @@ export function collectMemorySampleSubsystems(wordIndex) {
|
|
|
121
130
|
: null;
|
|
122
131
|
const dispatchCaches = getDispatchCascadeCacheStats();
|
|
123
132
|
return {
|
|
133
|
+
lsp: (() => {
|
|
134
|
+
const retention = getLspDocumentTextRetentionSnapshot();
|
|
135
|
+
return {
|
|
136
|
+
clients: retention.clients,
|
|
137
|
+
clientRoots: retention.roots,
|
|
138
|
+
incrementalTextEntries: retention.entries,
|
|
139
|
+
incrementalTextBytes: retention.bytes,
|
|
140
|
+
};
|
|
141
|
+
})(),
|
|
124
142
|
reviewGraph,
|
|
125
143
|
wordIndex: wordIndex
|
|
126
144
|
? {
|
|
127
145
|
docs: wordIndex.docLengths.size,
|
|
146
|
+
fileTable: wordIndex.fileTable.size,
|
|
128
147
|
postings: wordIndex.postings.size,
|
|
148
|
+
// O(distinct tokens), reading only `.length`/`.byteLength` per
|
|
149
|
+
// list — no posting ELEMENT is touched, so this stays inside the
|
|
150
|
+
// module docstring’s bounded-read constraint.
|
|
151
|
+
postingEntries: countPostingEntries(wordIndex),
|
|
152
|
+
residentBytes: estimateWordIndexStoreBytes(wordIndex),
|
|
129
153
|
forwardEntries: wordIndex.forward?.size ?? 0,
|
|
130
154
|
}
|
|
131
155
|
: null,
|
|
132
156
|
treeSitter,
|
|
133
|
-
dispatchCaches
|
|
157
|
+
dispatchCaches: {
|
|
158
|
+
...dispatchCaches,
|
|
159
|
+
estimatedBytes: estimateDispatchCacheBytes(dispatchCaches),
|
|
160
|
+
},
|
|
134
161
|
};
|
|
135
162
|
}
|
|
136
163
|
/** Assemble one full sample. `mem` is injectable for tests; defaults to a
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/** Session-local attribution captured before a stale message_end can drain. */
|
|
2
|
+
let lastStableSessionId;
|
|
3
|
+
let previousSessionId;
|
|
4
|
+
/** Remember the stable id used by a live message_end's own cache row. */
|
|
5
|
+
export function noteLiveMessageEndSessionId(sessionId) {
|
|
6
|
+
lastStableSessionId = sessionId;
|
|
7
|
+
}
|
|
8
|
+
/** Return the newest usable id for a stale message_end attribution. */
|
|
9
|
+
export function getLastLiveMessageEndSessionId() {
|
|
10
|
+
return lastStableSessionId ?? previousSessionId;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Rotate the attribution anchor at the real session boundary. A queued stale
|
|
14
|
+
* message_end from the session being replaced may drain after this boundary,
|
|
15
|
+
* so retain exactly one prior anchor while making room for the replacement.
|
|
16
|
+
*/
|
|
17
|
+
export function rotateMessageEndAttribution() {
|
|
18
|
+
// Keep the prior anchor when no live message_end arrived this session:
|
|
19
|
+
// two back-to-back boundaries (reload/resume re-announcing) must not
|
|
20
|
+
// erase the only id a still-queued stale drain can use (#1956 R10).
|
|
21
|
+
if (lastStableSessionId !== undefined) {
|
|
22
|
+
previousSessionId = lastStableSessionId;
|
|
23
|
+
}
|
|
24
|
+
lastStableSessionId = undefined;
|
|
25
|
+
}
|
|
26
|
+
/** Full reset seam used by the session-state registry and test isolation. */
|
|
27
|
+
export function resetMessageEndAttribution() {
|
|
28
|
+
lastStableSessionId = undefined;
|
|
29
|
+
previousSessionId = undefined;
|
|
30
|
+
}
|
|
@@ -34,14 +34,31 @@ function isInsideGitRepo(startDir) {
|
|
|
34
34
|
/**
|
|
35
35
|
* Get current git commit hash (short)
|
|
36
36
|
*/
|
|
37
|
-
function getCurrentCommit() {
|
|
38
|
-
|
|
37
|
+
function getCurrentCommit(startDir) {
|
|
38
|
+
const repoStartDir = path.resolve(startDir);
|
|
39
|
+
if (!isInsideGitRepo(repoStartDir)) {
|
|
39
40
|
return "unknown";
|
|
40
41
|
}
|
|
42
|
+
let spawnDir = repoStartDir;
|
|
43
|
+
while (!fs.existsSync(spawnDir)) {
|
|
44
|
+
const parent = path.dirname(spawnDir);
|
|
45
|
+
if (parent === spawnDir)
|
|
46
|
+
return "unknown";
|
|
47
|
+
spawnDir = parent;
|
|
48
|
+
}
|
|
41
49
|
try {
|
|
50
|
+
// #2095: execSync inherits the child's stderr to THIS process by
|
|
51
|
+
// default (only stdout is captured for the return value), so a
|
|
52
|
+
// failing `git rev-parse` prints its raw "fatal: ..." line straight
|
|
53
|
+
// into the pi TUI — the catch below only ever sees the thrown
|
|
54
|
+
// non-zero-exit error, never the already-inherited stream. The
|
|
55
|
+
// explicit stdio array pipes stderr instead, so a failure is fully
|
|
56
|
+
// contained: silent here, same as the "unknown" fallback it feeds.
|
|
42
57
|
return execSync("git rev-parse --short HEAD", {
|
|
43
58
|
encoding: "utf-8",
|
|
44
59
|
timeout: 5000,
|
|
60
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
61
|
+
cwd: spawnDir,
|
|
45
62
|
}).trim();
|
|
46
63
|
}
|
|
47
64
|
catch {
|
|
@@ -99,7 +116,7 @@ export function captureSnapshot(filePath, metrics) {
|
|
|
99
116
|
pendingHistory = loadHistory();
|
|
100
117
|
}
|
|
101
118
|
const relativePath = path.relative(process.cwd(), filePath);
|
|
102
|
-
const commit = getCurrentCommit();
|
|
119
|
+
const commit = getCurrentCommit(path.dirname(filePath));
|
|
103
120
|
const snapshot = {
|
|
104
121
|
commit,
|
|
105
122
|
timestamp: new Date().toISOString(),
|
|
@@ -152,7 +169,7 @@ export function captureSnapshots(files) {
|
|
|
152
169
|
const history = loadHistory();
|
|
153
170
|
for (const file of files) {
|
|
154
171
|
const relativePath = path.relative(process.cwd(), file.filePath);
|
|
155
|
-
const commit = getCurrentCommit();
|
|
172
|
+
const commit = getCurrentCommit(path.dirname(file.filePath));
|
|
156
173
|
const snapshot = {
|
|
157
174
|
commit,
|
|
158
175
|
timestamp: new Date().toISOString(),
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* slice can add them once middle-man analysis has an AST-node entry point
|
|
28
28
|
* rather than this text-based one.
|
|
29
29
|
*/
|
|
30
|
+
import { escapeRegExp } from "./string-utils.js";
|
|
30
31
|
/** Self-reference token + member-access separator, per languageId. Both the
|
|
31
32
|
* self→field and field→method hops use the SAME separator in every language
|
|
32
33
|
* covered here (true even for PHP's `$this->field->method()`). */
|
|
@@ -65,9 +66,6 @@ const MIN_CANDIDATE_METHODS = 2;
|
|
|
65
66
|
* "mostly" — a class with a couple of forwarding convenience methods among
|
|
66
67
|
* real logic must NOT flag (the exact distinction #325 asks for). */
|
|
67
68
|
const DELEGATION_RATIO_THRESHOLD = 0.9;
|
|
68
|
-
function escapeRegExp(s) {
|
|
69
|
-
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
70
|
-
}
|
|
71
69
|
function buildForwardRegex(token, sep) {
|
|
72
70
|
const t = escapeRegExp(token);
|
|
73
71
|
const s = escapeRegExp(sep);
|
|
@@ -98,7 +96,7 @@ function paramNamesFromSignature(signature, languageId) {
|
|
|
98
96
|
.map((raw) => {
|
|
99
97
|
let name = raw;
|
|
100
98
|
// Rest/spread params forward as themselves textually (`...args`).
|
|
101
|
-
const isRest =
|
|
99
|
+
const isRest = name.startsWith("...");
|
|
102
100
|
name = name.replace(/^\.\.\./, "");
|
|
103
101
|
name = name.split("=")[0].trim(); // strip default value
|
|
104
102
|
name = name.split(":")[0].trim(); // strip type annotation
|