@herbertgao/pi-extensions 2026.8.14 → 2026.9.0
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/README.md +19 -13
- package/THIRD_PARTY_NOTICES.md +26 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +8 -1
- package/node_modules/@czottmann/pi-automode/README.md +4 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +71 -9
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/pi-bark/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +20 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +46 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +55 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/packets.ts +6 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/scroll.ts +5 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +34 -30
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +129 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +283 -59
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +264 -24
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@narumitw/pi-btw/src/text.ts +18 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +5 -16
- package/node_modules/@narumitw/pi-caffeinate/LICENSE +21 -0
- package/node_modules/@narumitw/pi-caffeinate/README.md +224 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +1112 -0
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-caffeinate/package.json +53 -0
- package/node_modules/@narumitw/pi-caffeinate/src/caffeinate.ts +811 -0
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +153 -0
- package/node_modules/@narumitw/pi-caffeinate/src/index.ts +1 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitor-process.ts +43 -0
- package/node_modules/@narumitw/pi-caffeinate/src/inhibitors.ts +167 -0
- package/node_modules/@narumitw/pi-caffeinate/src/settings.ts +202 -0
- package/node_modules/@tifan/pi-handoff/README.md +39 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +166 -5
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +42 -0
- package/node_modules/pi-mcp-adapter/README.md +15 -15
- package/node_modules/pi-mcp-adapter/commands.ts +59 -22
- package/node_modules/pi-mcp-adapter/config.ts +18 -4
- package/node_modules/pi-mcp-adapter/direct-tools.ts +26 -6
- package/node_modules/pi-mcp-adapter/dist/config.d.ts +4 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +13 -4
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +12 -4
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/error-signal.ts +15 -4
- package/node_modules/pi-mcp-adapter/errors.ts +141 -0
- package/node_modules/pi-mcp-adapter/host-html-template.ts +58 -5
- package/node_modules/pi-mcp-adapter/index.ts +2 -3
- package/node_modules/pi-mcp-adapter/init.ts +5 -0
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +105 -32
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +30 -9
- package/node_modules/pi-mcp-adapter/mcp-setup-panel.ts +66 -28
- package/node_modules/pi-mcp-adapter/mcp-status.ts +2 -0
- package/node_modules/pi-mcp-adapter/package.json +2 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +88 -25
- package/node_modules/pi-mcp-adapter/sandbox-proxy-template.ts +217 -0
- package/node_modules/pi-mcp-adapter/server-manager.ts +343 -6
- package/node_modules/pi-mcp-adapter/skills/mcp-scripting/SKILL.md +1 -0
- package/node_modules/pi-mcp-adapter/types.ts +19 -4
- package/node_modules/pi-mcp-adapter/ui-resource-handler.ts +18 -2
- package/node_modules/pi-mcp-adapter/ui-server.ts +179 -54
- package/node_modules/pi-mcp-adapter/ui-session.ts +28 -8
- package/node_modules/pi-web-access/CHANGELOG.md +20 -0
- package/node_modules/pi-web-access/README.md +15 -2
- package/node_modules/pi-web-access/extract.ts +57 -12
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +1 -1
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/package.json +21 -17
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -18,6 +18,8 @@ import { containerNameChain, getOpenDocumentSymbols, lspSymbolKindName, } from "
|
|
|
18
18
|
import { isAtOrAboveHomeDir, normalizeFilePath, normalizeMapKey, toProjectRelativePath, } from "../path-utils.js";
|
|
19
19
|
import { collectProjectSourceFilesWithBudgetAsync } from "../project-scan-policy.js";
|
|
20
20
|
import { getReviewGraphMaxFilesDerived } from "../project-scale.js";
|
|
21
|
+
import { compareOrdinal } from "../string-utils.js";
|
|
22
|
+
import { BoundedLruCache } from "../bounded-cache.js";
|
|
21
23
|
import { jsTsCandidatePaths, resolveAliasedImport, resolveImportToFiles, resolveProjectReferenceImport, resolveWorkspacePackageImport, } from "./import-resolvers.js";
|
|
22
24
|
import { RUNTIME_CONFIG } from "../runtime-config.js";
|
|
23
25
|
import { buildReverseDependencyIndexFromGraph, rankFilesByReverseDependencyCentrality, } from "../reverse-deps.js";
|
|
@@ -94,6 +96,52 @@ const MAIN_KIND_EXTENSIONS = Array.from(MAIN_KINDS).flatMap((kind) => KIND_EXTEN
|
|
|
94
96
|
export const REVIEW_GRAPH_SOURCE_EXTENSIONS = MAIN_KIND_EXTENSIONS;
|
|
95
97
|
const CHANGED_SYMBOLS_PREFIX = "session.reviewGraph.changedSymbols:";
|
|
96
98
|
const extractorCache = new Map();
|
|
99
|
+
const REVIEW_GRAPH_MAX_WARM_WORKSPACES = 8;
|
|
100
|
+
// Walker output is raw, so its spelling still needs the canonical path seam.
|
|
101
|
+
// Keep that expensive raw-to-canonical step per project across builds, bounded
|
|
102
|
+
// like TestRunnerClient's instance-lifetime canonicalRootMemo (#2058). A new
|
|
103
|
+
// spelling is resolved once; repeated builds reuse the result. The project
|
|
104
|
+
// memo is bounded and workspace-cache clears provide a freshness boundary.
|
|
105
|
+
const REVIEW_GRAPH_SOURCE_PATH_MEMO_ENTRIES = 16_384;
|
|
106
|
+
const _sourcePathMemos = new Map();
|
|
107
|
+
function sourcePathMemo(cwd) {
|
|
108
|
+
const key = normalizeMapKey(path.resolve(cwd));
|
|
109
|
+
const existing = _sourcePathMemos.get(key);
|
|
110
|
+
if (existing) {
|
|
111
|
+
// The workspace map is also bounded LRU; a hit must refresh its recency.
|
|
112
|
+
_sourcePathMemos.delete(key);
|
|
113
|
+
_sourcePathMemos.set(key, existing);
|
|
114
|
+
return existing;
|
|
115
|
+
}
|
|
116
|
+
const memo = {
|
|
117
|
+
cache: new BoundedLruCache(REVIEW_GRAPH_SOURCE_PATH_MEMO_ENTRIES),
|
|
118
|
+
normalizeCalls: { value: 0 },
|
|
119
|
+
};
|
|
120
|
+
_sourcePathMemos.set(key, memo);
|
|
121
|
+
while (_sourcePathMemos.size > REVIEW_GRAPH_MAX_WARM_WORKSPACES) {
|
|
122
|
+
const oldest = _sourcePathMemos.keys().next().value;
|
|
123
|
+
if (oldest === undefined)
|
|
124
|
+
break;
|
|
125
|
+
_sourcePathMemos.delete(oldest);
|
|
126
|
+
}
|
|
127
|
+
return memo;
|
|
128
|
+
}
|
|
129
|
+
function normalizeGraphSourcePath(memo, raw) {
|
|
130
|
+
const cached = memo.cache.get(raw);
|
|
131
|
+
if (cached !== undefined)
|
|
132
|
+
return cached;
|
|
133
|
+
const normalized = normalizeMapKey(raw);
|
|
134
|
+
// A missing file is normalized through resolveNonExisting, which lowercases
|
|
135
|
+
// its tail. It may be recreated with different casing before the next build,
|
|
136
|
+
// so never make that spelling a process-lifetime memo entry (#2072 F2).
|
|
137
|
+
if (fs.existsSync(raw))
|
|
138
|
+
memo.cache.set(raw, normalized);
|
|
139
|
+
memo.normalizeCalls.value++;
|
|
140
|
+
return normalized;
|
|
141
|
+
}
|
|
142
|
+
export function _resetReviewGraphSourcePathMemoForTests() {
|
|
143
|
+
_sourcePathMemos.clear();
|
|
144
|
+
}
|
|
97
145
|
// IN-FLIGHT Promise cache: deduplicates CONCURRENT buildOrUpdateGraph calls for
|
|
98
146
|
// the same (cwd, changedFiles). A separate workspace cache below preserves the
|
|
99
147
|
// expensive parsed graph across invocations when source file mtimes/sizes have
|
|
@@ -111,7 +159,6 @@ const extractorCache = new Map();
|
|
|
111
159
|
// operation's, not the process's.
|
|
112
160
|
const _buildCache = new Map();
|
|
113
161
|
const _workspaceGraphCache = new Map();
|
|
114
|
-
const REVIEW_GRAPH_MAX_WARM_WORKSPACES = 8;
|
|
115
162
|
const REVIEW_GRAPH_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
|
|
116
163
|
// A workspace-wide clear must invalidate builds for workspaces that are not
|
|
117
164
|
// resident yet, too. This process-wide component therefore survives cache
|
|
@@ -166,6 +213,7 @@ function evictWorkspaceGraph(key, entry) {
|
|
|
166
213
|
_buildCache.delete(buildKey);
|
|
167
214
|
}
|
|
168
215
|
}
|
|
216
|
+
_sourcePathMemos.delete(key);
|
|
169
217
|
_workspaceCacheEpochs.set(key, (_workspaceCacheEpochs.get(key) ?? 0) + 1);
|
|
170
218
|
_workspaceGraphCache.delete(key);
|
|
171
219
|
}
|
|
@@ -191,8 +239,16 @@ function touchWorkspaceGraph(key) {
|
|
|
191
239
|
function setWorkspaceGraph(key, entry, epoch) {
|
|
192
240
|
if (epoch !== undefined && workspaceCacheEpoch(key) !== epoch)
|
|
193
241
|
return false;
|
|
242
|
+
const previous = _workspaceGraphCache.get(key);
|
|
243
|
+
if (previous)
|
|
244
|
+
clearWorkspaceGraphTimer(previous);
|
|
245
|
+
// #2255: bound what the cache RETAINS. The caller keeps its full-graph
|
|
246
|
+
// reference for the current turn; only the retained copy is trimmed, so an
|
|
247
|
+
// over-budget repo never accumulates an unbounded graph across the process.
|
|
248
|
+
const boundedGraph = retainedGraph(key, entry.graph);
|
|
194
249
|
const resident = {
|
|
195
250
|
...entry,
|
|
251
|
+
graph: boundedGraph,
|
|
196
252
|
lastUsedAt: Date.now(),
|
|
197
253
|
};
|
|
198
254
|
_workspaceGraphCache.set(key, resident);
|
|
@@ -292,8 +348,14 @@ export function clearReviewGraphWorkspaceCache(cwd) {
|
|
|
292
348
|
for (const entry of _workspaceGraphCache.values())
|
|
293
349
|
clearWorkspaceGraphTimer(entry);
|
|
294
350
|
_workspaceGraphCache.clear();
|
|
351
|
+
_sourcePathMemos.clear();
|
|
295
352
|
_workspaceCacheEpoch++;
|
|
296
353
|
_sizeSkipVerdicts.clear();
|
|
354
|
+
// #2255 review V2: the non-cache retention registry is memory-attribution
|
|
355
|
+
// state for THIS session's graphs. Without this, a new session's
|
|
356
|
+
// `memory_sample` kept attributing the previous session's graph, which the
|
|
357
|
+
// registry still held a live `WeakRef` to.
|
|
358
|
+
_retainedGraphSites.clear();
|
|
297
359
|
}
|
|
298
360
|
else {
|
|
299
361
|
const normalized = normalizeMapKey(cwd);
|
|
@@ -311,6 +373,7 @@ export function clearReviewGraphWorkspaceCache(cwd) {
|
|
|
311
373
|
clearWorkspaceGraphTimer(entry);
|
|
312
374
|
_workspaceCacheEpochs.set(normalized, (_workspaceCacheEpochs.get(normalized) ?? 0) + 1);
|
|
313
375
|
_workspaceGraphCache.delete(normalized);
|
|
376
|
+
_sourcePathMemos.delete(normalized);
|
|
314
377
|
_sizeSkipVerdicts.delete(normalized);
|
|
315
378
|
}
|
|
316
379
|
_lastGraphBuildInfo = { reused: false, mode: "full", graphChanged: true };
|
|
@@ -324,23 +387,214 @@ export function _setReviewGraphBuildGateForTests(gate) {
|
|
|
324
387
|
_reviewGraphBuildGateForTests = gate;
|
|
325
388
|
}
|
|
326
389
|
/**
|
|
327
|
-
*
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*
|
|
390
|
+
* Measured heap coefficients for the graph's current object shape. An
|
|
391
|
+
* isolated store of the production node and edge objects was forced through
|
|
392
|
+
* GC and divided by its census: 450.5 bytes per node and 243.0 bytes per
|
|
393
|
+
* edge, including the two edge-index references. The measurement used 18,695
|
|
394
|
+
* nodes and 48,815 edges across three identical runs with --expose-gc.
|
|
395
|
+
*/
|
|
396
|
+
const REVIEW_GRAPH_NODE_RESIDENT_BYTES = 450.5;
|
|
397
|
+
const REVIEW_GRAPH_EDGE_RESIDENT_BYTES = 243.0;
|
|
398
|
+
export function estimateReviewGraphStoreBytes(totalNodes, totalEdges) {
|
|
399
|
+
return (totalNodes * REVIEW_GRAPH_NODE_RESIDENT_BYTES +
|
|
400
|
+
totalEdges * REVIEW_GRAPH_EDGE_RESIDENT_BYTES);
|
|
401
|
+
}
|
|
402
|
+
// --- In-memory live-graph byte bound (#2255) ---
|
|
403
|
+
// The persist element cap (`GRAPH_PERSIST_MAX_ELEMENTS_DEFAULT`) guards the
|
|
404
|
+
// synchronous serialize+gzip spike; it trims only the on-disk snapshot. The live
|
|
405
|
+
// `ReviewGraph` had no bound and grew with project size — the second unbounded
|
|
406
|
+
// dispatch store behind the #2240 OOM (FactStore's fileFacts was the first,
|
|
407
|
+
// bounded in #2243). This bound caps the RETAINED live graph by estimated
|
|
408
|
+
// resident bytes, using the same centrality-ranked induced-subgraph selection the
|
|
409
|
+
// snapshot uses (`capGraphForPersist`).
|
|
410
|
+
//
|
|
411
|
+
// A graph has TWO process-lifetime retention sites, not one: the workspace cache,
|
|
412
|
+
// and `session.reviewGraph` on a caller's FactStore — and two of those stores are
|
|
413
|
+
// module-scope (`dispatch/integration.ts`, `mcp/analyze.ts`, the latter never
|
|
414
|
+
// cleared). Bounding only the cache left the full graph resident on the fact, so
|
|
415
|
+
// both sites go through `retainedGraph` below, memoized per graph instance so the
|
|
416
|
+
// two sites share ONE bounded object instead of trimming twice (#2255 review F2).
|
|
417
|
+
export const GRAPH_MAX_IN_MEMORY_BYTES_DEFAULT = 512 * 1024 * 1024;
|
|
418
|
+
function graphMaxInMemoryBytes() {
|
|
419
|
+
const raw = Number(process.env.PI_LENS_GRAPH_MAX_IN_MEMORY_BYTES);
|
|
420
|
+
return Number.isFinite(raw) && raw > 0
|
|
421
|
+
? raw
|
|
422
|
+
: GRAPH_MAX_IN_MEMORY_BYTES_DEFAULT;
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* The graph any process-lifetime holder may RETAIN, bounded to the in-memory byte
|
|
426
|
+
* budget (#2255). Callers keep the full graph for the current turn; only what
|
|
427
|
+
* outlives the turn goes through here.
|
|
428
|
+
*
|
|
429
|
+
* In budget, this is an O(1) size read returning the same object, so a normal
|
|
430
|
+
* repository sees no behavior change. Over budget, the byte budget converts to an
|
|
431
|
+
* element cap using the graph's own node/edge split, then `capGraphForPersist`
|
|
432
|
+
* runs one centrality selection.
|
|
433
|
+
*
|
|
434
|
+
* Deliberately NOT memoized. A memo keyed on the source graph short-circuits the
|
|
435
|
+
* budget check, so a graph mutated in place after its first trim keeps answering
|
|
436
|
+
* with the stale earlier result (#2255 review R4). Callers that retain the same
|
|
437
|
+
* graph at two sites dedupe at the CALL SITE by capping once and sharing the
|
|
438
|
+
* result, which is both simpler and staleness-free.
|
|
439
|
+
*
|
|
440
|
+
* The result is marked `partial` AND `capTrimmed`. `partial` keeps every
|
|
441
|
+
* coverage-reporting consumer honest; `capTrimmed` records the narrower fact that
|
|
442
|
+
* the source WALK was complete and only size was cut, which is what lets the two
|
|
443
|
+
* incremental-base gates rebuild from it instead of forcing a full walk every
|
|
444
|
+
* turn. `capTrimmed` is process-local and stripped before persist.
|
|
445
|
+
*/
|
|
446
|
+
function retainedGraph(cwd, graph) {
|
|
447
|
+
const nodes = graph.nodes.size;
|
|
448
|
+
const edges = graph.edges.length;
|
|
449
|
+
const bytes = estimateReviewGraphStoreBytes(nodes, edges);
|
|
450
|
+
const budget = graphMaxInMemoryBytes();
|
|
451
|
+
// Deliberately NOT skipped for an already-partial graph. A full build over the
|
|
452
|
+
// source-walk entry budget sets `partial: true` on a graph that is still the
|
|
453
|
+
// full walked set, so skipping partials exempted the exact population this
|
|
454
|
+
// bound targets (#2255 review F3). The byte check below is the only
|
|
455
|
+
// idempotence this needs: a graph already under budget is returned as-is.
|
|
456
|
+
if (bytes <= budget)
|
|
457
|
+
return graph;
|
|
458
|
+
// Scale both axes by the same budget/bytes ratio so the element cap preserves
|
|
459
|
+
// the graph's node/edge split; `capGraphForPersist` re-splits the cap by that
|
|
460
|
+
// same ratio, landing the capped graph at or under the budget.
|
|
461
|
+
const elementCap = Math.max(1, Math.floor(((nodes + edges) * budget) / bytes));
|
|
462
|
+
let capped = capGraphForPersist(cwd, graph, elementCap);
|
|
463
|
+
// Floor: a selection that retains NOTHING is never an acceptable answer for a
|
|
464
|
+
// non-empty input — it silently converts "too big" into "no graph at all", and
|
|
465
|
+
// every query against it reads as a clean empty result (#2255 review F4).
|
|
466
|
+
// Fall back to a deterministic head slice, which is still bounded by the same
|
|
467
|
+
// element cap but cannot be empty, and say so under its own ledger kind so the
|
|
468
|
+
// two outcomes are never blended into one record.
|
|
469
|
+
if (capped.nodes.size === 0 && nodes > 0) {
|
|
470
|
+
capped = headSliceGraph(graph, elementCap);
|
|
471
|
+
incrementDegradationCount({
|
|
472
|
+
kind: "review-graph-memory-cap-floor",
|
|
473
|
+
subject: cwd,
|
|
474
|
+
reason: `centrality selection retained 0 of ${nodes} nodes at cap ${elementCap}; fell back to a ${capped.nodes.size}-node head slice`,
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
const cappedNodes = capped.nodes.size;
|
|
478
|
+
const cappedEdges = capped.edges.length;
|
|
479
|
+
capped.persistCoverage = {
|
|
480
|
+
...(capped.persistCoverage ?? graphCoverage(capped, elementCap)),
|
|
481
|
+
partial: true,
|
|
482
|
+
capTrimmed: true,
|
|
483
|
+
};
|
|
484
|
+
incrementDegradationCount({
|
|
485
|
+
kind: "review-graph-memory-cap",
|
|
486
|
+
subject: cwd,
|
|
487
|
+
reason: `live graph ${nodes}n/${edges}e ~${Math.round(bytes / (1024 * 1024))}MiB over ${Math.round(budget / (1024 * 1024))}MiB budget; trimmed to ${cappedNodes}n/${cappedEdges}e ~${Math.round(estimateReviewGraphStoreBytes(cappedNodes, cappedEdges) / (1024 * 1024))}MiB`,
|
|
488
|
+
});
|
|
489
|
+
return capped;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Deterministic non-empty fallback selection: take whole per-file node groups in
|
|
493
|
+
* stable path order until the element cap is reached, then the induced edges.
|
|
494
|
+
* Used only when centrality selection returns nothing (#2255 review F4).
|
|
495
|
+
*/
|
|
496
|
+
function headSliceGraph(graph, cap) {
|
|
497
|
+
const nodeBudget = Math.max(1, Math.floor((cap * graph.nodes.size) / (graph.nodes.size + graph.edges.length)));
|
|
498
|
+
const keptIds = new Set();
|
|
499
|
+
for (const [id] of graph.nodes) {
|
|
500
|
+
if (keptIds.size >= nodeBudget)
|
|
501
|
+
break;
|
|
502
|
+
keptIds.add(id);
|
|
503
|
+
}
|
|
504
|
+
const nodes = new Map([...graph.nodes].filter(([id]) => keptIds.has(id)));
|
|
505
|
+
const edgeBudget = Math.max(0, cap - nodes.size);
|
|
506
|
+
const edges = graph.edges
|
|
507
|
+
.filter((edge) => keptIds.has(edge.from) && keptIds.has(edge.to))
|
|
508
|
+
.slice(0, edgeBudget);
|
|
509
|
+
const sliced = {
|
|
510
|
+
...graph,
|
|
511
|
+
nodes,
|
|
512
|
+
edges,
|
|
513
|
+
edgesByFrom: new Map(),
|
|
514
|
+
edgesByTo: new Map(),
|
|
515
|
+
fileNodes: new Map(),
|
|
516
|
+
symbolNodesByFile: new Map(),
|
|
517
|
+
changedSymbolsByFile: new Map(),
|
|
518
|
+
persistCoverage: undefined,
|
|
519
|
+
};
|
|
520
|
+
rebuildIndexes(sliced);
|
|
521
|
+
return sliced;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Store the graph on `session.reviewGraph` bounded (#2255 review F2). Two of the
|
|
525
|
+
* FactStores this reaches are module-scope, so an unbounded value here outlives
|
|
526
|
+
* every session and defeats the cache bound entirely.
|
|
527
|
+
*
|
|
528
|
+
* This bounds the VALUE, not the store, and deliberately adds no second bounding
|
|
529
|
+
* mechanism to `FactStore` (#2243 owns the store-level discipline).
|
|
530
|
+
*
|
|
531
|
+
* `sessionFacts` entry COUNT is not fully bounded — `session.baseline.${path}`
|
|
532
|
+
* (`dispatch/dispatcher.ts`) and `session.baseline.cascade.${path}`
|
|
533
|
+
* (`dispatch/integration.ts`) mint one key per file touched and clear only on
|
|
534
|
+
* `clearAll`. That is #2282, a separate #2240 sibling; it is
|
|
535
|
+
* per-file `Diagnostic[]`, not a whole project graph. What this seam fixes is the
|
|
536
|
+
* one fact whose single VALUE is unbounded in project size.
|
|
537
|
+
*/
|
|
538
|
+
function setSessionReviewGraphFact(cwd, facts, graph) {
|
|
539
|
+
const retained = retainedGraph(cwd, graph);
|
|
540
|
+
// The raw `cwd` is the registry key on purpose. Folding it here would add a
|
|
541
|
+
// `realpath` probe to EVERY build, and the key only has to be stable: the
|
|
542
|
+
// snapshot deduplicates by graph object IDENTITY, so two spellings of one
|
|
543
|
+
// workspace cost one extra bounded registry slot and never a double count.
|
|
544
|
+
registerRetainedGraph(`fact:${cwd}`, retained);
|
|
545
|
+
facts.setSessionFact("session.reviewGraph", retained);
|
|
546
|
+
}
|
|
547
|
+
// Retention sites OUTSIDE the workspace cache — today `session.reviewGraph` on a
|
|
548
|
+
// caller's FactStore. Held through `WeakRef` so registering adds no retention of
|
|
549
|
+
// its own: a graph whose only remaining referent is this registry is collectable,
|
|
550
|
+
// and its slot is pruned on the next write or read. Keyed by site, so the map is
|
|
551
|
+
// bounded by the number of distinct workspaces, not by builds (#2255 review F2).
|
|
552
|
+
const _retainedGraphSites = new Map();
|
|
553
|
+
function registerRetainedGraph(site, graph) {
|
|
554
|
+
for (const [key, ref] of _retainedGraphSites) {
|
|
555
|
+
if (ref.deref() === undefined)
|
|
556
|
+
_retainedGraphSites.delete(key);
|
|
557
|
+
}
|
|
558
|
+
_retainedGraphSites.set(site, new WeakRef(graph));
|
|
559
|
+
}
|
|
560
|
+
/**
|
|
561
|
+
* O(retention-sites) snapshot of every resident review graph — NOT
|
|
562
|
+
* O(nodes)/O(edges): only `.size`/`.length` are read per graph, never the graph
|
|
563
|
+
* contents themselves (#1123 item 2 memory-attribution sample).
|
|
564
|
+
*
|
|
565
|
+
* Counts the workspace cache AND the non-cache retention sites, deduplicated by
|
|
566
|
+
* object IDENTITY. Reading the cache alone reported zero bytes while a full graph
|
|
567
|
+
* was still resident on `session.reviewGraph` — the sample read clean during the
|
|
568
|
+
* exact heap exhaustion it is cited to prove against (#2255 review F2). The
|
|
569
|
+
* dedupe matters because the bound hands both sites the same bounded object;
|
|
570
|
+
* summing them would double-count the common case.
|
|
332
571
|
*/
|
|
333
572
|
export function getReviewGraphWorkspaceCacheSnapshot() {
|
|
334
573
|
let totalNodes = 0;
|
|
335
574
|
let totalEdges = 0;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
575
|
+
const counted = new Set();
|
|
576
|
+
const count = (graph) => {
|
|
577
|
+
if (counted.has(graph))
|
|
578
|
+
return;
|
|
579
|
+
counted.add(graph);
|
|
580
|
+
totalNodes += graph.nodes.size;
|
|
581
|
+
totalEdges += graph.edges.length;
|
|
582
|
+
};
|
|
583
|
+
for (const entry of _workspaceGraphCache.values())
|
|
584
|
+
count(entry.graph);
|
|
585
|
+
for (const [key, ref] of _retainedGraphSites) {
|
|
586
|
+
const graph = ref.deref();
|
|
587
|
+
if (graph === undefined) {
|
|
588
|
+
_retainedGraphSites.delete(key);
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
count(graph);
|
|
339
592
|
}
|
|
340
593
|
return {
|
|
341
594
|
cacheEntries: _workspaceGraphCache.size,
|
|
342
595
|
totalNodes,
|
|
343
596
|
totalEdges,
|
|
597
|
+
residentBytes: estimateReviewGraphStoreBytes(totalNodes, totalEdges),
|
|
344
598
|
};
|
|
345
599
|
}
|
|
346
600
|
/** Test-only cache keys, including the LRU order from oldest to newest. */
|
|
@@ -349,6 +603,26 @@ export function _getReviewGraphWorkspaceCacheKeysForTests() {
|
|
|
349
603
|
.sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)
|
|
350
604
|
.map(([key]) => key);
|
|
351
605
|
}
|
|
606
|
+
/** Test-only replacement seam for the workspace idle-eviction family. */
|
|
607
|
+
export function _setReviewGraphWorkspaceEntryForTests(key, graph) {
|
|
608
|
+
setWorkspaceGraph(key, {
|
|
609
|
+
signature: "test",
|
|
610
|
+
fileSignatures: new Map(),
|
|
611
|
+
graph,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
/** Test-only drive of the session-fact retention seam the build paths use. */
|
|
615
|
+
export function _setSessionReviewGraphFactForTests(cwd, facts, graph) {
|
|
616
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
617
|
+
}
|
|
618
|
+
/** Test-only view of what a graph's coverage looks like once persisted. */
|
|
619
|
+
export function _persistedCoverageForTests(coverage) {
|
|
620
|
+
return stripProcessLocalCoverage(coverage);
|
|
621
|
+
}
|
|
622
|
+
/** Test-only read of the exact retained graph for a raw cache key (#2255). */
|
|
623
|
+
export function _getReviewGraphWorkspaceGraphForTests(key) {
|
|
624
|
+
return _workspaceGraphCache.get(key)?.graph;
|
|
625
|
+
}
|
|
352
626
|
export function _getReviewGraphCacheStateForTests(cwd) {
|
|
353
627
|
const cached = _workspaceGraphCache.get(normalizeMapKey(cwd));
|
|
354
628
|
if (!cached)
|
|
@@ -628,7 +902,7 @@ async function sourceSignatureMapAsync(files) {
|
|
|
628
902
|
}
|
|
629
903
|
function sourceSignatureFromMap(signatures) {
|
|
630
904
|
return [...signatures.entries()]
|
|
631
|
-
.sort(([a], [b]) => a
|
|
905
|
+
.sort(([a], [b]) => compareOrdinal(a, b))
|
|
632
906
|
.map(([file, signature]) => `${file}:${signature}`)
|
|
633
907
|
.join("|");
|
|
634
908
|
}
|
|
@@ -850,6 +1124,8 @@ export function _setReviewGraphEntryCounterForTests(counter) {
|
|
|
850
1124
|
_reviewGraphEntryCounterForTests = counter;
|
|
851
1125
|
}
|
|
852
1126
|
export async function getGraphSourceFiles(cwd) {
|
|
1127
|
+
const sourceMemo = sourcePathMemo(cwd);
|
|
1128
|
+
const normalizeCallsBefore = sourceMemo.normalizeCalls.value;
|
|
853
1129
|
// Async, chunked-yield walk (identical output to the sync collector) so the
|
|
854
1130
|
// per-edit cascade graph rebuild doesn't block the event loop on a large repo.
|
|
855
1131
|
//
|
|
@@ -889,6 +1165,7 @@ export async function getGraphSourceFiles(cwd) {
|
|
|
889
1165
|
// so the caller's `length > maxGraphFiles` check still trips.
|
|
890
1166
|
return {
|
|
891
1167
|
files: collected,
|
|
1168
|
+
pathNormalizeCalls: sourceMemo.normalizeCalls.value - normalizeCallsBefore,
|
|
892
1169
|
sourceFileCount: collected.length,
|
|
893
1170
|
maxFileCount: maxGraphFiles,
|
|
894
1171
|
entryBudgetExceeded,
|
|
@@ -897,7 +1174,7 @@ export async function getGraphSourceFiles(cwd) {
|
|
|
897
1174
|
const result = [];
|
|
898
1175
|
let sinceYield = 0;
|
|
899
1176
|
for (const raw of collected) {
|
|
900
|
-
const file =
|
|
1177
|
+
const file = normalizeGraphSourcePath(sourceMemo, raw);
|
|
901
1178
|
const kind = detectFileKind(file);
|
|
902
1179
|
// isWithinReviewGraphSizeLimit does a statSync per file — yield periodically
|
|
903
1180
|
// so the size-limit filter (one stat each) can't hold the loop in one burst.
|
|
@@ -917,19 +1194,30 @@ export async function getGraphSourceFiles(cwd) {
|
|
|
917
1194
|
}
|
|
918
1195
|
return {
|
|
919
1196
|
files: result,
|
|
1197
|
+
pathNormalizeCalls: sourceMemo.normalizeCalls.value - normalizeCallsBefore,
|
|
920
1198
|
sourceFileCount: result.length,
|
|
921
1199
|
maxFileCount: maxGraphFiles,
|
|
922
1200
|
entryBudgetExceeded,
|
|
923
1201
|
};
|
|
924
1202
|
}
|
|
925
1203
|
function addNode(graph, node) {
|
|
1204
|
+
// #2074: keep symbolNodesByFile live here, not only in rebuildIndexes, so the
|
|
1205
|
+
// incremental path can drop its terminal O(graph) reindex. Re-adding an id
|
|
1206
|
+
// already in the map must not push a duplicate — rebuildIndexes starts from
|
|
1207
|
+
// empty maps, so the two producers stay consistent.
|
|
1208
|
+
const isNew = !graph.nodes.has(node.id);
|
|
926
1209
|
graph.nodes.set(node.id, node);
|
|
927
1210
|
if (node.kind === "file" && node.filePath) {
|
|
928
1211
|
graph.fileNodes.set(node.filePath, node.id);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
if (isNew && node.kind === "symbol" && node.filePath) {
|
|
1215
|
+
const ids = graph.symbolNodesByFile.get(node.filePath) ?? [];
|
|
1216
|
+
ids.push(node.id);
|
|
1217
|
+
graph.symbolNodesByFile.set(node.filePath, ids);
|
|
929
1218
|
}
|
|
930
1219
|
}
|
|
931
|
-
function
|
|
932
|
-
graph.edges.push(edge);
|
|
1220
|
+
function indexEdge(graph, edge) {
|
|
933
1221
|
const from = graph.edgesByFrom.get(edge.from) ?? [];
|
|
934
1222
|
from.push(edge);
|
|
935
1223
|
graph.edgesByFrom.set(edge.from, from);
|
|
@@ -937,6 +1225,33 @@ function addEdge(graph, edge) {
|
|
|
937
1225
|
to.push(edge);
|
|
938
1226
|
graph.edgesByTo.set(edge.to, to);
|
|
939
1227
|
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Drop `edge` from both adjacency buckets (#2074). Costs one scan of the two
|
|
1230
|
+
* buckets the edge belongs to, never a scan of `graph.edges`, so removing a
|
|
1231
|
+
* changed file's edges stays proportional to that file's fan-in/fan-out.
|
|
1232
|
+
*/
|
|
1233
|
+
function unindexEdge(graph, edge) {
|
|
1234
|
+
const from = graph.edgesByFrom.get(edge.from);
|
|
1235
|
+
if (from) {
|
|
1236
|
+
const at = from.indexOf(edge);
|
|
1237
|
+
if (at >= 0)
|
|
1238
|
+
from.splice(at, 1);
|
|
1239
|
+
if (from.length === 0)
|
|
1240
|
+
graph.edgesByFrom.delete(edge.from);
|
|
1241
|
+
}
|
|
1242
|
+
const to = graph.edgesByTo.get(edge.to);
|
|
1243
|
+
if (to) {
|
|
1244
|
+
const at = to.indexOf(edge);
|
|
1245
|
+
if (at >= 0)
|
|
1246
|
+
to.splice(at, 1);
|
|
1247
|
+
if (to.length === 0)
|
|
1248
|
+
graph.edgesByTo.delete(edge.to);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
function addEdge(graph, edge) {
|
|
1252
|
+
graph.edges.push(edge);
|
|
1253
|
+
indexEdge(graph, edge);
|
|
1254
|
+
}
|
|
940
1255
|
function rebuildIndexes(graph) {
|
|
941
1256
|
graph.edgesByFrom = new Map();
|
|
942
1257
|
graph.edgesByTo = new Map();
|
|
@@ -1191,17 +1506,31 @@ function persistedData(pending) {
|
|
|
1191
1506
|
: undefined,
|
|
1192
1507
|
nodes: Array.from(pending.graph.nodes.entries()),
|
|
1193
1508
|
edges: pending.graph.edges,
|
|
1194
|
-
coverage: pending.graph.persistCoverage,
|
|
1509
|
+
coverage: stripProcessLocalCoverage(pending.graph.persistCoverage),
|
|
1195
1510
|
gitStamp: pending.gitStamp,
|
|
1196
1511
|
};
|
|
1197
1512
|
}
|
|
1513
|
+
/**
|
|
1514
|
+
* Drop coverage fields that are true only of THIS process's graph before the
|
|
1515
|
+
* snapshot is written (#2255 review F5).
|
|
1516
|
+
*
|
|
1517
|
+
* `capTrimmed` means "this process walked every file, then cut for size", which
|
|
1518
|
+
* is what makes a graph a safe incremental base. A snapshot read back in a later
|
|
1519
|
+
* process carries no such guarantee — the tree may have changed underneath it —
|
|
1520
|
+
* so persisting the marker would let a hydrated graph claim base-eligibility
|
|
1521
|
+
* nobody established, the #936 laundering shape. `partial` itself is preserved,
|
|
1522
|
+
* so the snapshot still reports honestly that it is incomplete.
|
|
1523
|
+
*/
|
|
1524
|
+
function stripProcessLocalCoverage(coverage) {
|
|
1525
|
+
if (!coverage?.capTrimmed)
|
|
1526
|
+
return coverage;
|
|
1527
|
+
const { capTrimmed: _capTrimmed, ...persisted } = coverage;
|
|
1528
|
+
return persisted;
|
|
1529
|
+
}
|
|
1198
1530
|
function countRetainedSourceFiles(graph, sourceFilePaths) {
|
|
1199
1531
|
if (sourceFilePaths === undefined)
|
|
1200
1532
|
return graph.fileNodes.size;
|
|
1201
|
-
const sourceKeys = new Set();
|
|
1202
|
-
for (const filePath of sourceFilePaths) {
|
|
1203
|
-
sourceKeys.add(normalizeMapKey(filePath));
|
|
1204
|
-
}
|
|
1533
|
+
const sourceKeys = new Set(sourceFilePaths);
|
|
1205
1534
|
let retained = 0;
|
|
1206
1535
|
for (const filePath of sourceKeys) {
|
|
1207
1536
|
if (graph.fileNodes.has(filePath))
|
|
@@ -1235,13 +1564,52 @@ function capGraphForPersist(cwd, graph, cap, options = {}) {
|
|
|
1235
1564
|
const nodeBudget = Math.max(1, Math.floor((cap * graph.nodes.size) / totalElements));
|
|
1236
1565
|
const reverseDeps = buildReverseDependencyIndexFromGraph({ cwd, graph });
|
|
1237
1566
|
const rankedFiles = rankFilesByReverseDependencyCentrality(reverseDeps);
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1567
|
+
// Both sides of this lookup must agree on path spelling. `rankedFiles` comes
|
|
1568
|
+
// from the reverse-dependency index, which keys by `normalizeMapKey`
|
|
1569
|
+
// (reverse-deps.ts), while this map keys by the RAW `node.filePath`. They agree
|
|
1570
|
+
// because production paths already arrive folded (normalizeGraphSourcePath), so
|
|
1571
|
+
// the raw pass is both correct and free. An unfolded path missed its lookup and
|
|
1572
|
+
// was dropped from the ranking with no signal (#2255 review F1).
|
|
1573
|
+
//
|
|
1574
|
+
// Folding every node up front would fix that but costs a `realpath` probe PER
|
|
1575
|
+
// NODE — measured at 93.5us per node, about 14 seconds at 150k nodes — on the
|
|
1576
|
+
// pre-existing persist cap that every over-cap build already pays. So fold
|
|
1577
|
+
// LAZILY: index raw first, and pay one folding pass only when the raw index
|
|
1578
|
+
// leaves nodes the ranking cannot reach. Reach, not emptiness, is the trigger:
|
|
1579
|
+
// a PARTIAL raw match would otherwise silently under-select the rest.
|
|
1580
|
+
const indexNodesByFile = (fold) => {
|
|
1581
|
+
const byFile = new Map();
|
|
1582
|
+
for (const [id, node] of graph.nodes) {
|
|
1583
|
+
if (!node.filePath)
|
|
1584
|
+
continue;
|
|
1585
|
+
const fileKey = fold ? normalizeMapKey(node.filePath) : node.filePath;
|
|
1586
|
+
const ids = byFile.get(fileKey) ?? [];
|
|
1587
|
+
ids.push(id);
|
|
1588
|
+
byFile.set(fileKey, ids);
|
|
1589
|
+
}
|
|
1590
|
+
return byFile;
|
|
1591
|
+
};
|
|
1592
|
+
// Measure reach in NODES, not files. Counting files is too weak: when only some
|
|
1593
|
+
// of a file's nodes are unfolded, the file still appears covered while its group
|
|
1594
|
+
// is short, and the missing nodes drop out of the ranking with no signal.
|
|
1595
|
+
const reachableNodes = (byFile) => {
|
|
1596
|
+
let reached = 0;
|
|
1597
|
+
for (const filePath of rankedFiles)
|
|
1598
|
+
reached += byFile.get(filePath)?.length ?? 0;
|
|
1599
|
+
return reached;
|
|
1600
|
+
};
|
|
1601
|
+
let placeableNodes = 0;
|
|
1602
|
+
for (const node of graph.nodes.values())
|
|
1603
|
+
if (node.filePath)
|
|
1604
|
+
placeableNodes++;
|
|
1605
|
+
let nodeIdsByFile = indexNodesByFile(false);
|
|
1606
|
+
if (reachableNodes(nodeIdsByFile) < placeableNodes) {
|
|
1607
|
+
const folded = indexNodesByFile(true);
|
|
1608
|
+
// Keep whichever index the ranking can actually reach. Folding repairs
|
|
1609
|
+
// unfolded input; it is never a downgrade for input that was already correct.
|
|
1610
|
+
if (reachableNodes(folded) > reachableNodes(nodeIdsByFile)) {
|
|
1611
|
+
nodeIdsByFile = folded;
|
|
1612
|
+
}
|
|
1245
1613
|
}
|
|
1246
1614
|
const keptIds = new Set();
|
|
1247
1615
|
for (const filePath of rankedFiles) {
|
|
@@ -1305,7 +1673,7 @@ function logPersistSuccess(key, pending, stats, workerState = { started: false,
|
|
|
1305
1673
|
type: "phase",
|
|
1306
1674
|
phase: "review_graph_persist",
|
|
1307
1675
|
filePath: pending.cachePath,
|
|
1308
|
-
durationMs: stats.
|
|
1676
|
+
durationMs: stats.durationMs,
|
|
1309
1677
|
metadata: { elements: pending.elementCount, ...stats },
|
|
1310
1678
|
});
|
|
1311
1679
|
logReviewGraph({
|
|
@@ -1313,6 +1681,7 @@ function logPersistSuccess(key, pending, stats, workerState = { started: false,
|
|
|
1313
1681
|
phase: "persist_succeeded",
|
|
1314
1682
|
elements: pending.elementCount,
|
|
1315
1683
|
...stats,
|
|
1684
|
+
durationMs: stats.durationMs,
|
|
1316
1685
|
observability: persistObservability(pending, {
|
|
1317
1686
|
status: "succeeded",
|
|
1318
1687
|
workerStarted: workerState.started,
|
|
@@ -1322,6 +1691,7 @@ function logPersistSuccess(key, pending, stats, workerState = { started: false,
|
|
|
1322
1691
|
});
|
|
1323
1692
|
}
|
|
1324
1693
|
function writePendingOnMainThread(key, pending, reason, workerState = { started: false, completed: false }) {
|
|
1694
|
+
const persistStarted = performance.now();
|
|
1325
1695
|
const serializeStarted = performance.now();
|
|
1326
1696
|
try {
|
|
1327
1697
|
const json = JSON.stringify(persistedData(pending));
|
|
@@ -1339,6 +1709,7 @@ function writePendingOnMainThread(key, pending, reason, workerState = { started:
|
|
|
1339
1709
|
gzBytes: gzip.byteLength,
|
|
1340
1710
|
serializeMs,
|
|
1341
1711
|
writeMs: performance.now() - writeStarted,
|
|
1712
|
+
durationMs: performance.now() - persistStarted,
|
|
1342
1713
|
offloaded: false,
|
|
1343
1714
|
}, {
|
|
1344
1715
|
started: workerState.started,
|
|
@@ -1427,7 +1798,8 @@ function handleWorkerResult(result) {
|
|
|
1427
1798
|
result.rawBytes === undefined ||
|
|
1428
1799
|
result.gzBytes === undefined ||
|
|
1429
1800
|
result.serializeMs === undefined ||
|
|
1430
|
-
result.writeMs === undefined
|
|
1801
|
+
result.writeMs === undefined ||
|
|
1802
|
+
result.durationMs === undefined) {
|
|
1431
1803
|
fs.rm(result.stagePath, { force: true }, () => { });
|
|
1432
1804
|
writePendingOnMainThread(key, pending, result.error ?? "invalid worker result", {
|
|
1433
1805
|
started: true,
|
|
@@ -1446,6 +1818,7 @@ function handleWorkerResult(result) {
|
|
|
1446
1818
|
gzBytes: result.gzBytes,
|
|
1447
1819
|
serializeMs: result.serializeMs,
|
|
1448
1820
|
writeMs: result.writeMs,
|
|
1821
|
+
durationMs: result.durationMs,
|
|
1449
1822
|
offloaded: true,
|
|
1450
1823
|
}, { started: true, completed: true });
|
|
1451
1824
|
}
|
|
@@ -1937,7 +2310,7 @@ function hashIgnoredIds(ignoredIds) {
|
|
|
1937
2310
|
if (ignoredIds === undefined)
|
|
1938
2311
|
return "unavailable";
|
|
1939
2312
|
const joined = [...ignoredIds]
|
|
1940
|
-
.sort((a, b) => a
|
|
2313
|
+
.sort((a, b) => compareOrdinal(a, b))
|
|
1941
2314
|
.join("\u0000");
|
|
1942
2315
|
return createHash("sha256").update(joined).digest("hex");
|
|
1943
2316
|
}
|
|
@@ -2418,6 +2791,7 @@ export function flushReviewGraphPersist(cwd, source = "cli") {
|
|
|
2418
2791
|
logReviewGraph({
|
|
2419
2792
|
cwd,
|
|
2420
2793
|
phase: "persist_succeeded",
|
|
2794
|
+
durationMs: performance.now() - startedAt,
|
|
2421
2795
|
elements: pending.elementCount,
|
|
2422
2796
|
rawBytes,
|
|
2423
2797
|
gzBytes: gzip.byteLength,
|
|
@@ -3263,22 +3637,52 @@ function removeFileOwnedGraphData(graph, filePath) {
|
|
|
3263
3637
|
if (graph.nodes.has(fileNodeId))
|
|
3264
3638
|
removedIds.add(fileNodeId);
|
|
3265
3639
|
const preservedIncomingSymbolEdges = [];
|
|
3640
|
+
const droppedEdges = [];
|
|
3266
3641
|
graph.edges = graph.edges.filter((edge) => {
|
|
3267
3642
|
const fromRemoved = removedIds.has(edge.from);
|
|
3268
3643
|
const toRemoved = removedIds.has(edge.to);
|
|
3269
|
-
if (fromRemoved)
|
|
3644
|
+
if (fromRemoved) {
|
|
3645
|
+
droppedEdges.push(edge);
|
|
3270
3646
|
return false;
|
|
3647
|
+
}
|
|
3271
3648
|
if (removedSymbolIds.has(edge.to)) {
|
|
3272
3649
|
preservedIncomingSymbolEdges.push({ ...edge });
|
|
3650
|
+
droppedEdges.push(edge);
|
|
3273
3651
|
return false;
|
|
3274
3652
|
}
|
|
3275
3653
|
// Preserve importer edges to the stable file node id; the node is re-added below.
|
|
3276
3654
|
if (toRemoved && edge.to === fileNodeId)
|
|
3277
3655
|
return true;
|
|
3656
|
+
if (toRemoved)
|
|
3657
|
+
droppedEdges.push(edge);
|
|
3278
3658
|
return !toRemoved;
|
|
3279
3659
|
});
|
|
3280
|
-
|
|
3660
|
+
// #2074: keep the adjacency indexes live instead of rebuilding them over the
|
|
3661
|
+
// whole graph afterwards. Only the dropped edges' own buckets are touched.
|
|
3662
|
+
// Unconditional: on an unindexed graph every bucket lookup simply misses.
|
|
3663
|
+
for (const edge of droppedEdges)
|
|
3664
|
+
unindexEdge(graph, edge);
|
|
3665
|
+
for (const id of removedIds) {
|
|
3666
|
+
const node = graph.nodes.get(id);
|
|
3281
3667
|
graph.nodes.delete(id);
|
|
3668
|
+
if (!node?.filePath)
|
|
3669
|
+
continue;
|
|
3670
|
+
if (node.kind === "file") {
|
|
3671
|
+
if (graph.fileNodes.get(node.filePath) === id) {
|
|
3672
|
+
graph.fileNodes.delete(node.filePath);
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
else if (node.kind === "symbol") {
|
|
3676
|
+
const ids = graph.symbolNodesByFile.get(node.filePath);
|
|
3677
|
+
if (!ids)
|
|
3678
|
+
continue;
|
|
3679
|
+
const at = ids.indexOf(id);
|
|
3680
|
+
if (at >= 0)
|
|
3681
|
+
ids.splice(at, 1);
|
|
3682
|
+
if (ids.length === 0)
|
|
3683
|
+
graph.symbolNodesByFile.delete(node.filePath);
|
|
3684
|
+
}
|
|
3685
|
+
}
|
|
3282
3686
|
return preservedIncomingSymbolEdges;
|
|
3283
3687
|
}
|
|
3284
3688
|
async function addFileToGraph(graph, cwd, file, facts, ignoredIds, contentOverride) {
|
|
@@ -3362,16 +3766,90 @@ async function addFileToGraph(graph, cwd, file, facts, ignoredIds, contentOverri
|
|
|
3362
3766
|
addCxxIncludeEdges(graph, cwd, file, ignoredIds, contentOverride);
|
|
3363
3767
|
}
|
|
3364
3768
|
}
|
|
3769
|
+
/**
|
|
3770
|
+
* #2074 acceptance instrumentation. `restoreComparisons` counts edge-metadata
|
|
3771
|
+
* stringifications inside `restoreValidIncomingEdges`; `importTargetEdgeScans`
|
|
3772
|
+
* counts edges visited by `importTargetsForFile`. Before this change both grew
|
|
3773
|
+
* with the whole graph on every one-file rebuild. They are the count-based
|
|
3774
|
+
* signal the issue asks for, because wall time on this hardware is IO-noisy
|
|
3775
|
+
* (#1920).
|
|
3776
|
+
*/
|
|
3777
|
+
const _rebuildCounters = { restoreComparisons: 0, importTargetEdgeScans: 0 };
|
|
3778
|
+
export function _getReviewGraphRebuildCountersForTests() {
|
|
3779
|
+
return { ..._rebuildCounters };
|
|
3780
|
+
}
|
|
3781
|
+
export function _resetReviewGraphRebuildCountersForTests() {
|
|
3782
|
+
_rebuildCounters.restoreComparisons = 0;
|
|
3783
|
+
_rebuildCounters.importTargetEdgeScans = 0;
|
|
3784
|
+
}
|
|
3785
|
+
/**
|
|
3786
|
+
* Identity of an edge for dedupe purposes, WITHIN one target bucket: the array
|
|
3787
|
+
* form needs no separator sentinel and cannot collide across fields.
|
|
3788
|
+
*/
|
|
3789
|
+
function edgeIdentityKey(edge) {
|
|
3790
|
+
_rebuildCounters.restoreComparisons++;
|
|
3791
|
+
return JSON.stringify([edge.from, edge.kind, edge.metadata ?? {}]);
|
|
3792
|
+
}
|
|
3365
3793
|
function restoreValidIncomingEdges(graph, edges) {
|
|
3366
|
-
|
|
3794
|
+
// #2074: dedupe per TARGET, using `edgesByTo`, instead of building a key set
|
|
3795
|
+
// over every edge in the graph. Preserved incoming edges point at symbols of
|
|
3796
|
+
// the files just re-extracted, and `removeFileOwnedGraphData` has already
|
|
3797
|
+
// dropped those symbols' incoming edges, so each bucket read here is tiny.
|
|
3798
|
+
// The per-target `Set` keeps the restore linear even when one changed file is
|
|
3799
|
+
// a hub with thousands of preserved incoming edges — scanning the bucket per
|
|
3800
|
+
// edge instead would be quadratic in that fan-in.
|
|
3801
|
+
const seenByTarget = new Map();
|
|
3367
3802
|
for (const edge of edges) {
|
|
3368
3803
|
if (!graph.nodes.has(edge.from) || !graph.nodes.has(edge.to))
|
|
3369
3804
|
continue;
|
|
3370
|
-
|
|
3371
|
-
if (
|
|
3805
|
+
let seen = seenByTarget.get(edge.to);
|
|
3806
|
+
if (!seen) {
|
|
3807
|
+
seen = new Set();
|
|
3808
|
+
for (const candidate of graph.edgesByTo.get(edge.to) ?? []) {
|
|
3809
|
+
seen.add(edgeIdentityKey(candidate));
|
|
3810
|
+
}
|
|
3811
|
+
seenByTarget.set(edge.to, seen);
|
|
3812
|
+
}
|
|
3813
|
+
const key = edgeIdentityKey(edge);
|
|
3814
|
+
if (seen.has(key))
|
|
3372
3815
|
continue;
|
|
3373
|
-
|
|
3374
|
-
|
|
3816
|
+
seen.add(key);
|
|
3817
|
+
addEdge(graph, edge);
|
|
3818
|
+
}
|
|
3819
|
+
}
|
|
3820
|
+
/**
|
|
3821
|
+
* Remove duplicates created when deferred targets converge after restoration.
|
|
3822
|
+
* The scan is limited to resolved edges and their target buckets, not the
|
|
3823
|
+
* complete graph, so same-batch repairs remain proportional to the resolved
|
|
3824
|
+
* edges and the fan-in of their target buckets.
|
|
3825
|
+
*/
|
|
3826
|
+
function dedupeResolvedEdges(graph, replacements) {
|
|
3827
|
+
const resolvedEdges = new Set(replacements.map(([, after]) => after));
|
|
3828
|
+
const seenByTarget = new Map();
|
|
3829
|
+
const removed = new Set();
|
|
3830
|
+
for (const [, edge] of replacements) {
|
|
3831
|
+
if (removed.has(edge))
|
|
3832
|
+
continue;
|
|
3833
|
+
let seen = seenByTarget.get(edge.to);
|
|
3834
|
+
if (!seen) {
|
|
3835
|
+
seen = new Set();
|
|
3836
|
+
for (const candidate of graph.edgesByTo.get(edge.to) ?? []) {
|
|
3837
|
+
if (!resolvedEdges.has(candidate)) {
|
|
3838
|
+
seen.add(edgeIdentityKey(candidate));
|
|
3839
|
+
}
|
|
3840
|
+
}
|
|
3841
|
+
seenByTarget.set(edge.to, seen);
|
|
3842
|
+
}
|
|
3843
|
+
const key = edgeIdentityKey(edge);
|
|
3844
|
+
if (seen.has(key)) {
|
|
3845
|
+
removed.add(edge);
|
|
3846
|
+
unindexEdge(graph, edge);
|
|
3847
|
+
continue;
|
|
3848
|
+
}
|
|
3849
|
+
seen.add(key);
|
|
3850
|
+
}
|
|
3851
|
+
if (removed.size > 0) {
|
|
3852
|
+
graph.edges = graph.edges.filter((edge) => !removed.has(edge));
|
|
3375
3853
|
}
|
|
3376
3854
|
}
|
|
3377
3855
|
const _graphImportChanges = new WeakMap();
|
|
@@ -3383,20 +3861,32 @@ function importTargetsForFile(graph, filePath) {
|
|
|
3383
3861
|
const normalized = normalizeMapKey(filePath);
|
|
3384
3862
|
const fileNodeId = graph.fileNodes.get(normalized) ?? `file:${normalized}`;
|
|
3385
3863
|
const targets = new Set();
|
|
3386
|
-
//
|
|
3387
|
-
//
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3864
|
+
// #2074: read this file's own out-edges from `edgesByFrom` rather than
|
|
3865
|
+
// scanning every edge in the graph. The only caller, `updateGraphFiles`,
|
|
3866
|
+
// indexes the graph before its first call and keeps the indexes live for the
|
|
3867
|
+
// rest of the update, so the bucket is always authoritative here.
|
|
3868
|
+
for (const edge of graph.edgesByFrom.get(fileNodeId) ?? []) {
|
|
3869
|
+
_rebuildCounters.importTargetEdgeScans++;
|
|
3391
3870
|
if (edge.kind !== "imports")
|
|
3392
3871
|
continue;
|
|
3393
3872
|
const target = graph.nodes.get(edge.to)?.filePath;
|
|
3394
3873
|
if (target)
|
|
3395
3874
|
targets.add(normalizeMapKey(target));
|
|
3396
3875
|
}
|
|
3397
|
-
return [...targets].sort((a, b) => a
|
|
3876
|
+
return [...targets].sort((a, b) => compareOrdinal(a, b));
|
|
3398
3877
|
}
|
|
3399
3878
|
async function updateGraphFiles(graph, cwd, files, facts, ignoredIds) {
|
|
3879
|
+
// #2074: index ONCE, up front, then keep the indexes live through the update.
|
|
3880
|
+
// This is the same single O(graph) pass the terminal `rebuildIndexes` used to
|
|
3881
|
+
// cost, moved to the front, and it buys three things: the two
|
|
3882
|
+
// `importTargetsForFile` calls per changed file become bucket lookups,
|
|
3883
|
+
// `restoreValidIncomingEdges` dedupes against a fan-in bucket instead of the
|
|
3884
|
+
// whole edge list, and `existedBefore` finally reads a POPULATED `fileNodes`.
|
|
3885
|
+
// `cloneGraph` returns empty indexes, so before this the first file's
|
|
3886
|
+
// `existedBefore` was always false on every update path — which forced
|
|
3887
|
+
// `importsChanged` true in `clients/dispatch/integration.ts:1077` and blocked
|
|
3888
|
+
// reverse-dependency index reuse on every incremental build.
|
|
3889
|
+
rebuildIndexes(graph);
|
|
3400
3890
|
const prior = files.map((file) => ({
|
|
3401
3891
|
filePath: normalizeMapKey(file),
|
|
3402
3892
|
existedBefore: graph.fileNodes.has(normalizeMapKey(file)),
|
|
@@ -3409,7 +3899,6 @@ async function updateGraphFiles(graph, cwd, files, facts, ignoredIds) {
|
|
|
3409
3899
|
}
|
|
3410
3900
|
restoreValidIncomingEdges(graph, preservedIncoming);
|
|
3411
3901
|
resolveDeferredSymbolEdges(graph, false);
|
|
3412
|
-
rebuildIndexes(graph);
|
|
3413
3902
|
graph.changedSymbolsByFile.clear();
|
|
3414
3903
|
for (const file of files) {
|
|
3415
3904
|
upsertChangedSymbols(graph, facts, file);
|
|
@@ -3433,6 +3922,12 @@ function resolveDeferredSymbolEdges(graph, rebuild = true) {
|
|
|
3433
3922
|
ids.push(node.id);
|
|
3434
3923
|
symbolNameToIds.set(node.symbolName, ids);
|
|
3435
3924
|
}
|
|
3925
|
+
// #2074: this pass REPLACES edge objects, so the adjacency buckets that hold
|
|
3926
|
+
// the old objects go stale. Callers that keep the indexes live (the
|
|
3927
|
+
// incremental path, which no longer reindexes afterwards) need each
|
|
3928
|
+
// replacement patched into the buckets; collect them here rather than paying
|
|
3929
|
+
// a whole-graph reindex for a handful of newly resolved edges.
|
|
3930
|
+
const replacements = [];
|
|
3436
3931
|
graph.edges = graph.edges.map((edge) => {
|
|
3437
3932
|
const targetNode = graph.nodes.get(edge.to);
|
|
3438
3933
|
if (!targetNode?.metadata?.unresolvedName)
|
|
@@ -3447,13 +3942,25 @@ function resolveDeferredSymbolEdges(graph, rebuild = true) {
|
|
|
3447
3942
|
if (importHintFile) {
|
|
3448
3943
|
const scoped = candidates.filter((id) => graph.nodes.get(id)?.filePath === importHintFile);
|
|
3449
3944
|
if (scoped.length === 1) {
|
|
3450
|
-
|
|
3945
|
+
const resolved = {
|
|
3946
|
+
...edge,
|
|
3947
|
+
to: scoped[0],
|
|
3948
|
+
resolution: "import",
|
|
3949
|
+
};
|
|
3950
|
+
replacements.push([edge, resolved]);
|
|
3951
|
+
return resolved;
|
|
3451
3952
|
}
|
|
3452
3953
|
}
|
|
3453
3954
|
if (candidates.length === 1) {
|
|
3454
3955
|
// Exactly one same-named real symbol exists graph-wide: the bare-name
|
|
3455
3956
|
// match is provably unambiguous (refs #655 — resolution confidence).
|
|
3456
|
-
|
|
3957
|
+
const resolved = {
|
|
3958
|
+
...edge,
|
|
3959
|
+
to: candidates[0],
|
|
3960
|
+
resolution: "exact",
|
|
3961
|
+
};
|
|
3962
|
+
replacements.push([edge, resolved]);
|
|
3963
|
+
return resolved;
|
|
3457
3964
|
}
|
|
3458
3965
|
// 0 or 2+ candidates (and no import hint narrowed it): stays on the
|
|
3459
3966
|
// unresolved placeholder, resolution stays "name-only" (set at edge
|
|
@@ -3461,8 +3968,47 @@ function resolveDeferredSymbolEdges(graph, rebuild = true) {
|
|
|
3461
3968
|
// confirmed graph node.
|
|
3462
3969
|
return edge;
|
|
3463
3970
|
});
|
|
3464
|
-
if (rebuild)
|
|
3971
|
+
if (rebuild) {
|
|
3465
3972
|
rebuildIndexes(graph);
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
if (replacements.length === 0)
|
|
3976
|
+
return;
|
|
3977
|
+
// Bucket ORDER is part of the contract, not just bucket membership:
|
|
3978
|
+
// `resolveUsedBy` in module-report walks `edgesByTo` and truncates at a cap,
|
|
3979
|
+
// so the order decides which callers a reader sees. `rebuildIndexes` orders
|
|
3980
|
+
// every bucket by position in `graph.edges`, and the incremental path must
|
|
3981
|
+
// match it exactly.
|
|
3982
|
+
//
|
|
3983
|
+
// `from` is unchanged by a resolution, so the from-bucket only needs the new
|
|
3984
|
+
// object swapped in at the OLD object's position — order is preserved for
|
|
3985
|
+
// free. `to` moves from the placeholder's bucket to the resolved symbol's,
|
|
3986
|
+
// and appending there would put an early edge behind later ones. Rebuild
|
|
3987
|
+
// exactly the affected to-buckets from `graph.edges`, which restores
|
|
3988
|
+
// canonical order without touching the rest of the index.
|
|
3989
|
+
const affectedTargets = new Set();
|
|
3990
|
+
for (const [before, after] of replacements) {
|
|
3991
|
+
const fromBucket = graph.edgesByFrom.get(before.from);
|
|
3992
|
+
if (fromBucket) {
|
|
3993
|
+
const at = fromBucket.indexOf(before);
|
|
3994
|
+
if (at >= 0)
|
|
3995
|
+
fromBucket[at] = after;
|
|
3996
|
+
}
|
|
3997
|
+
affectedTargets.add(before.to);
|
|
3998
|
+
affectedTargets.add(after.to);
|
|
3999
|
+
}
|
|
4000
|
+
for (const target of affectedTargets)
|
|
4001
|
+
graph.edgesByTo.set(target, []);
|
|
4002
|
+
for (const edge of graph.edges) {
|
|
4003
|
+
if (affectedTargets.has(edge.to))
|
|
4004
|
+
graph.edgesByTo.get(edge.to)?.push(edge);
|
|
4005
|
+
}
|
|
4006
|
+
for (const target of affectedTargets) {
|
|
4007
|
+
if (graph.edgesByTo.get(target)?.length === 0) {
|
|
4008
|
+
graph.edgesByTo.delete(target);
|
|
4009
|
+
}
|
|
4010
|
+
}
|
|
4011
|
+
dedupeResolvedEdges(graph, replacements);
|
|
3466
4012
|
}
|
|
3467
4013
|
/**
|
|
3468
4014
|
* #451: the freshness-provenance fields written onto a workspace cache entry by
|
|
@@ -3537,7 +4083,7 @@ async function tryIncrementalFromCache(cached, ctx) {
|
|
|
3537
4083
|
graphChanged: false,
|
|
3538
4084
|
});
|
|
3539
4085
|
graph.buildGeneration = generation;
|
|
3540
|
-
ctx.facts
|
|
4086
|
+
setSessionReviewGraphFact(ctx.cwd, ctx.facts, graph);
|
|
3541
4087
|
return graph;
|
|
3542
4088
|
}
|
|
3543
4089
|
// Record content hashes for the newly-added files too, so the next run can
|
|
@@ -3577,7 +4123,7 @@ async function tryIncrementalFromCache(cached, ctx) {
|
|
|
3577
4123
|
fromGeneration: priorGeneration,
|
|
3578
4124
|
changes: importChanges,
|
|
3579
4125
|
});
|
|
3580
|
-
ctx.facts
|
|
4126
|
+
setSessionReviewGraphFact(ctx.cwd, ctx.facts, graph);
|
|
3581
4127
|
return graph;
|
|
3582
4128
|
}
|
|
3583
4129
|
function hasGraphKindExtension(file) {
|
|
@@ -3594,10 +4140,16 @@ function hasGraphKindExtension(file) {
|
|
|
3594
4140
|
*/
|
|
3595
4141
|
async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, facts, seqHint, seqAtBuildStart, ignoredIds, cacheEpoch) {
|
|
3596
4142
|
const cached = _workspaceGraphCache.get(normalizedCwd);
|
|
3597
|
-
// Condition 2: need an in-process
|
|
3598
|
-
//
|
|
3599
|
-
//
|
|
3600
|
-
|
|
4143
|
+
// Condition 2: need an in-process entry that recorded a build seq and whose
|
|
4144
|
+
// SOURCE WALK was complete. An entry-budget-truncated or checkpoint graph has
|
|
4145
|
+
// UNSEEN files, so it is read-only orientation data and must force a complete
|
|
4146
|
+
// walk. A `capTrimmed` graph is a different case: this process walked every
|
|
4147
|
+
// file and then cut the graph for size, so rebuilding from it is safe, and
|
|
4148
|
+
// refusing it puts an over-budget repository on a full walk every single turn
|
|
4149
|
+
// (#2255 review F5). The marker is process-local and never hydrated from disk,
|
|
4150
|
+
// so a snapshot-derived partial still takes the refusal above.
|
|
4151
|
+
const cachedCoverage = cached?.graph.persistCoverage;
|
|
4152
|
+
if (cachedCoverage?.partial && cachedCoverage.capTrimmed !== true) {
|
|
3601
4153
|
return { fallback: "partial-base" };
|
|
3602
4154
|
}
|
|
3603
4155
|
if (!cached || cached.builtAtProjectSeq === undefined) {
|
|
@@ -3688,7 +4240,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
|
|
|
3688
4240
|
graphChanged: false,
|
|
3689
4241
|
});
|
|
3690
4242
|
graph.buildGeneration = generation;
|
|
3691
|
-
|
|
4243
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3692
4244
|
return { graph };
|
|
3693
4245
|
}
|
|
3694
4246
|
// Incremental re-extract over exactly the content-changed files. Reuses the
|
|
@@ -3746,7 +4298,7 @@ async function trySeqFastpath(cwd, buildId, normalizedCwd, normalizedChanged, fa
|
|
|
3746
4298
|
fromGeneration: priorGeneration,
|
|
3747
4299
|
changes: importChanges,
|
|
3748
4300
|
});
|
|
3749
|
-
|
|
4301
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3750
4302
|
return { graph };
|
|
3751
4303
|
}
|
|
3752
4304
|
async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
@@ -3793,7 +4345,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3793
4345
|
// treat as changed so dependents never trust stale derived state.
|
|
3794
4346
|
graphChanged: true,
|
|
3795
4347
|
});
|
|
3796
|
-
|
|
4348
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3797
4349
|
return graph;
|
|
3798
4350
|
}
|
|
3799
4351
|
// #451: capture the seq BEFORE any await — every builtAtProjectSeq stamp in
|
|
@@ -3820,6 +4372,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3820
4372
|
const sourceCollection = await getGraphSourceFiles(cwd);
|
|
3821
4373
|
const filesToBuild = sourceCollection.files;
|
|
3822
4374
|
const sourceFileCount = sourceCollection.sourceFileCount;
|
|
4375
|
+
const pathNormalizeCalls = sourceCollection.pathNormalizeCalls;
|
|
3823
4376
|
const sourceFilesTruncated = sourceCollection.entryBudgetExceeded;
|
|
3824
4377
|
const ignoredIds = await ignoredIdsPromise;
|
|
3825
4378
|
const maxGraphFiles = sourceCollection.maxFileCount;
|
|
@@ -3877,6 +4430,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3877
4430
|
sourceFileCount,
|
|
3878
4431
|
sourceFileCountTruncated: sourceFilesTruncated || filesToBuild.length > maxGraphFiles,
|
|
3879
4432
|
maxFileCount: maxGraphFiles,
|
|
4433
|
+
pathNormalizeCalls,
|
|
3880
4434
|
seqFastpathFallback,
|
|
3881
4435
|
// #459: a fresh empty graph is returned every call on this path (never
|
|
3882
4436
|
// persisted/reused) — treat it as changed so dependents never trust stale
|
|
@@ -3884,7 +4438,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3884
4438
|
// with a buildGeneration: absent ⇒ derived caches rebuild every time.
|
|
3885
4439
|
graphChanged: true,
|
|
3886
4440
|
});
|
|
3887
|
-
|
|
4441
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3888
4442
|
return graph;
|
|
3889
4443
|
}
|
|
3890
4444
|
// #782: the repo is within the cap on this build attempt — drop any
|
|
@@ -3905,8 +4459,17 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3905
4459
|
// re-persisting it would launder partial coverage onto disk as a complete
|
|
3906
4460
|
// snapshot (#936 review). Ignore it here — the disk tier rejects a partial
|
|
3907
4461
|
// base too, so the build falls through to a full rebuild.
|
|
3908
|
-
|
|
4462
|
+
//
|
|
4463
|
+
// `capTrimmed` is the one partial cause this does NOT refuse: this process
|
|
4464
|
+
// walked every file and then cut the graph to the memory budget, so no file is
|
|
4465
|
+
// unaccounted for. Refusing it forced a full walk on every turn for exactly
|
|
4466
|
+
// the repositories the memory bound targets (#2255 review F5). The marker is
|
|
4467
|
+
// process-local and stripped at persist, so a snapshot-hydrated partial —
|
|
4468
|
+
// whose completeness this process cannot vouch for — is still refused.
|
|
4469
|
+
const memCoverage = memCached?.graph.persistCoverage;
|
|
4470
|
+
if (memCoverage?.partial && memCoverage.capTrimmed !== true) {
|
|
3909
4471
|
memCached = undefined;
|
|
4472
|
+
}
|
|
3910
4473
|
if (memCached?.signature === signature) {
|
|
3911
4474
|
touchWorkspaceGraph(normalizedCwd);
|
|
3912
4475
|
const graph = cloneGraph(memCached.graph);
|
|
@@ -3929,7 +4492,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3929
4492
|
graphChanged: false,
|
|
3930
4493
|
});
|
|
3931
4494
|
graph.buildGeneration = generation;
|
|
3932
|
-
|
|
4495
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3933
4496
|
return graph;
|
|
3934
4497
|
}
|
|
3935
4498
|
if (memCached) {
|
|
@@ -3984,7 +4547,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
3984
4547
|
graphChanged: false,
|
|
3985
4548
|
});
|
|
3986
4549
|
graph.buildGeneration = generation;
|
|
3987
|
-
|
|
4550
|
+
setSessionReviewGraphFact(cwd, facts, graph);
|
|
3988
4551
|
return graph;
|
|
3989
4552
|
}
|
|
3990
4553
|
if (diskCached) {
|
|
@@ -4143,8 +4706,17 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4143
4706
|
// so change detection does not reread every file after the graph is built.
|
|
4144
4707
|
// #459: full rebuild ⇒ new generation.
|
|
4145
4708
|
const generation = ++_graphGenerationCounter;
|
|
4146
|
-
|
|
4147
|
-
|
|
4709
|
+
// #2255 review R1: cap ONCE and let both retention sites share the result. The
|
|
4710
|
+
// cache took a clone while the session fact took the original, two objects a
|
|
4711
|
+
// `cloneGraph` apart, so each ran its own centrality pass and retained its own
|
|
4712
|
+
// trimmed copy — two budgets resident, not one. Over budget the cap already
|
|
4713
|
+
// returns a fresh, fully-indexed graph, so it IS the snapshot and no clone is
|
|
4714
|
+
// needed. In budget nothing is trimmed and the clone happens exactly as before.
|
|
4715
|
+
const retained = retainedGraph(cwd, graph);
|
|
4716
|
+
const wasCapped = retained !== graph;
|
|
4717
|
+
const graphSnapshot = wasCapped ? retained : cloneGraph(graph);
|
|
4718
|
+
if (!wasCapped)
|
|
4719
|
+
rebuildIndexes(graphSnapshot);
|
|
4148
4720
|
// Keep the content generation on the persisted snapshot instance too, so
|
|
4149
4721
|
// scheduled persistence logs join the same graph identity as build success.
|
|
4150
4722
|
graphSnapshot.buildGeneration = generation;
|
|
@@ -4169,12 +4741,15 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4169
4741
|
mode: "full",
|
|
4170
4742
|
sourceFileCount,
|
|
4171
4743
|
sourceFileCountTruncated: sourceFilesTruncated,
|
|
4744
|
+
pathNormalizeCalls,
|
|
4172
4745
|
seqFastpathFallback,
|
|
4173
4746
|
...(persistReason ? { persistReason } : {}),
|
|
4174
4747
|
graphChanged: true,
|
|
4175
4748
|
});
|
|
4176
4749
|
graph.buildGeneration = generation;
|
|
4177
|
-
|
|
4750
|
+
// Share the cache's object, so an over-budget build retains ONE trimmed graph
|
|
4751
|
+
// across both sites. In budget this stores the equivalent clone the cache holds.
|
|
4752
|
+
setSessionReviewGraphFact(cwd, facts, graphSnapshot);
|
|
4178
4753
|
return graph;
|
|
4179
4754
|
}
|
|
4180
4755
|
/**
|
|
@@ -4196,7 +4771,7 @@ async function _doBuildGraph(cwd, changedFiles, facts, seqHint, buildId) {
|
|
|
4196
4771
|
* rather than by coincidence.
|
|
4197
4772
|
*/
|
|
4198
4773
|
function buildCacheKey(cwd, changedFiles) {
|
|
4199
|
-
return `${buildCacheWorkspaceKey(cwd)}|${[...changedFiles].sort((a, b) => a
|
|
4774
|
+
return `${buildCacheWorkspaceKey(cwd)}|${[...changedFiles].sort((a, b) => compareOrdinal(a, b)).join(",")}`;
|
|
4200
4775
|
}
|
|
4201
4776
|
/**
|
|
4202
4777
|
* Whether a build for this exact (cwd, changedFiles) key is PENDING right now
|
|
@@ -4254,6 +4829,7 @@ export function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
|
|
|
4254
4829
|
mode: buildInfo.mode,
|
|
4255
4830
|
sourceFileCount: buildInfo.sourceFileCount,
|
|
4256
4831
|
sourceFileCountTruncated: buildInfo.sourceFileCountTruncated,
|
|
4832
|
+
pathNormalizeCalls: buildInfo.pathNormalizeCalls,
|
|
4257
4833
|
}),
|
|
4258
4834
|
},
|
|
4259
4835
|
});
|
|
@@ -4276,6 +4852,7 @@ export function buildOrUpdateGraph(cwd, changedFiles, facts, seqHint) {
|
|
|
4276
4852
|
mode: buildInfo.mode,
|
|
4277
4853
|
sourceFileCount: buildInfo.sourceFileCount,
|
|
4278
4854
|
sourceFileCountTruncated: buildInfo.sourceFileCountTruncated,
|
|
4855
|
+
pathNormalizeCalls: buildInfo.pathNormalizeCalls,
|
|
4279
4856
|
}),
|
|
4280
4857
|
},
|
|
4281
4858
|
});
|