@herbertgao/pi-extensions 2026.10.1 → 2026.10.2
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 +13 -9
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/package.json +15 -10
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,1653 +0,0 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { Worker } from "node:worker_threads";
|
|
5
|
-
import { gunzipSync, gzipSync } from "node:zlib";
|
|
6
|
-
import { writeFileAtomic } from "./atomic-write.js";
|
|
7
|
-
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
8
|
-
import { getProjectDataDir } from "./file-utils.js";
|
|
9
|
-
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
10
|
-
import { readJsonCache } from "./json-cache-read.js";
|
|
11
|
-
import { logLatency } from "./latency-logger.js";
|
|
12
|
-
import { normalizeMapKey } from "./path-utils.js";
|
|
13
|
-
import { fingerprintProjectSnapshotJson } from "./project-snapshot-fingerprint.js";
|
|
14
|
-
import { detectProjectConventions, } from "./project-conventions.js";
|
|
15
|
-
import { deserializeWordIndex, serializeWordIndex, } from "./word-index.js";
|
|
16
|
-
// v2: added `wordIndex` (identifier inverted index + BM25, #162). Bumping the
|
|
17
|
-
// version invalidates pre-v2 snapshots so they rebuild with the new field.
|
|
18
|
-
export const PROJECT_SNAPSHOT_VERSION = 2;
|
|
19
|
-
function parseSequenceIndex(value) {
|
|
20
|
-
if (!value || typeof value !== "object")
|
|
21
|
-
return undefined;
|
|
22
|
-
const index = value;
|
|
23
|
-
if (typeof index.projectSeq !== "number")
|
|
24
|
-
return undefined;
|
|
25
|
-
if (!Array.isArray(index.fileSeqByPath))
|
|
26
|
-
return undefined;
|
|
27
|
-
const fileSeqByPath = index.fileSeqByPath.filter((entry) => Array.isArray(entry) &&
|
|
28
|
-
typeof entry[0] === "string" &&
|
|
29
|
-
typeof entry[1] === "number");
|
|
30
|
-
return { projectSeq: index.projectSeq, fileSeqByPath };
|
|
31
|
-
}
|
|
32
|
-
// #958 item 2: the canonical snapshot body is now streamed gzip
|
|
33
|
-
// (`project-snapshot.json.gz`), written by a worker thread off the save path
|
|
34
|
-
// (see the persist plumbing below). The previous uncompressed
|
|
35
|
-
// `project-snapshot.json` remains readable for ONE compatibility release so an
|
|
36
|
-
// upgrading user doesn't lose their snapshot — see loadSnapshotBody's legacy
|
|
37
|
-
// fallback. gzip measured 5-10x on top of the #957 compaction win (the
|
|
38
|
-
// review-graph's own measurement was 60MB → 1.4MB).
|
|
39
|
-
export function getProjectSnapshotPath(cwd) {
|
|
40
|
-
return path.join(getProjectDataDir(cwd), "cache", "project-snapshot.json.gz");
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Pre-#958 uncompressed body path. Read as a one-release fallback when no
|
|
44
|
-
* `.json.gz` is present; deleted whenever a fresh gz body is promoted so the
|
|
45
|
-
* two never coexist.
|
|
46
|
-
*/
|
|
47
|
-
export function getProjectSnapshotLegacyPath(cwd) {
|
|
48
|
-
return path.join(getProjectDataDir(cwd), "cache", "project-snapshot.json");
|
|
49
|
-
}
|
|
50
|
-
export function getProjectSnapshotMetaPath(cwd) {
|
|
51
|
-
return path.join(getProjectDataDir(cwd), "cache", "project-snapshot.meta.json");
|
|
52
|
-
}
|
|
53
|
-
export function isProjectSnapshotFresh(snapshot, currentProjectSeq) {
|
|
54
|
-
return (!!snapshot &&
|
|
55
|
-
snapshot.version === PROJECT_SNAPSHOT_VERSION &&
|
|
56
|
-
snapshot.seq === currentProjectSeq);
|
|
57
|
-
}
|
|
58
|
-
function parseSnapshot(value) {
|
|
59
|
-
if (!value || typeof value !== "object")
|
|
60
|
-
return null;
|
|
61
|
-
const snapshot = value;
|
|
62
|
-
if (snapshot.version !== PROJECT_SNAPSHOT_VERSION)
|
|
63
|
-
return null;
|
|
64
|
-
if (typeof snapshot.projectRoot !== "string")
|
|
65
|
-
return null;
|
|
66
|
-
if (typeof snapshot.generatedAt !== "string")
|
|
67
|
-
return null;
|
|
68
|
-
if (typeof snapshot.seq !== "number")
|
|
69
|
-
return null;
|
|
70
|
-
if (!Array.isArray(snapshot.cachedExports))
|
|
71
|
-
return null;
|
|
72
|
-
return {
|
|
73
|
-
version: PROJECT_SNAPSHOT_VERSION,
|
|
74
|
-
projectRoot: snapshot.projectRoot,
|
|
75
|
-
generatedAt: snapshot.generatedAt,
|
|
76
|
-
seq: snapshot.seq,
|
|
77
|
-
files: snapshot.files ?? {},
|
|
78
|
-
symbols: snapshot.symbols ?? {},
|
|
79
|
-
reverseDeps: snapshot.reverseDeps ?? {},
|
|
80
|
-
cachedExports: snapshot.cachedExports.filter((entry) => Array.isArray(entry) &&
|
|
81
|
-
typeof entry[0] === "string" &&
|
|
82
|
-
typeof entry[1] === "string"),
|
|
83
|
-
sequenceIndex: parseSequenceIndex(snapshot.sequenceIndex),
|
|
84
|
-
wordIndex: snapshot.wordIndex,
|
|
85
|
-
projectRulesScan: snapshot.projectRulesScan,
|
|
86
|
-
startupScan: snapshot.startupScan,
|
|
87
|
-
languageProfile: snapshot.languageProfile,
|
|
88
|
-
conventions: snapshot.conventions,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
function parseSnapshotMeta(value) {
|
|
92
|
-
if (!value || typeof value !== "object")
|
|
93
|
-
return null;
|
|
94
|
-
const meta = value;
|
|
95
|
-
if (typeof meta.version !== "number")
|
|
96
|
-
return null;
|
|
97
|
-
if (typeof meta.seq !== "number")
|
|
98
|
-
return null;
|
|
99
|
-
return {
|
|
100
|
-
timestamp: typeof meta.timestamp === "string" ? meta.timestamp : "",
|
|
101
|
-
version: meta.version,
|
|
102
|
-
seq: meta.seq,
|
|
103
|
-
fingerprint: typeof meta.fingerprint === "string" &&
|
|
104
|
-
/^[a-f0-9]{64}$/.test(meta.fingerprint)
|
|
105
|
-
? meta.fingerprint
|
|
106
|
-
: undefined,
|
|
107
|
-
gzBytes: typeof meta.gzBytes === "number" &&
|
|
108
|
-
Number.isInteger(meta.gzBytes) &&
|
|
109
|
-
meta.gzBytes > 0
|
|
110
|
-
? meta.gzBytes
|
|
111
|
-
: undefined,
|
|
112
|
-
sequenceIndex: parseSequenceIndex(meta.sequenceIndex),
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* Read the tiny meta sidecar (`project-snapshot.meta.json`) WITHOUT parsing
|
|
117
|
-
* the (potentially 40-112MB) snapshot body. Written on every save; absent on
|
|
118
|
-
* legacy installs — callers must treat a `null` return as "no opinion" and
|
|
119
|
-
* fall through to parsing the body. #947.
|
|
120
|
-
*/
|
|
121
|
-
export function readProjectSnapshotMeta(cwd) {
|
|
122
|
-
const meta = readJsonCache(getProjectSnapshotMetaPath(cwd), (parsed) => parseSnapshotMeta(parsed) ?? undefined);
|
|
123
|
-
return meta ?? null;
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Cheap staleness verdict from the meta sidecar alone. When this returns
|
|
127
|
-
* true, the snapshot body CANNOT be fresh (isProjectSnapshotFresh would
|
|
128
|
-
* reject it on the same two fields), so the expensive body parse can be
|
|
129
|
-
* skipped entirely. #947.
|
|
130
|
-
*/
|
|
131
|
-
export function isProjectSnapshotMetaStale(meta, currentProjectSeq) {
|
|
132
|
-
return (meta.version !== PROJECT_SNAPSHOT_VERSION || meta.seq !== currentProjectSeq);
|
|
133
|
-
}
|
|
134
|
-
export const SNAPSHOT_PARSE_CACHE_MAX = 4;
|
|
135
|
-
// #957 review: the cache exists to avoid re-parsing NORMAL snapshots within a
|
|
136
|
-
// session. A 112MB-class body parses to hundreds of MB of heap — pinning that
|
|
137
|
-
// for process lifetime inverts the win, so oversized bodies are simply never
|
|
138
|
-
// cached (they re-parse per read, exactly the pre-#947 behavior). Measured
|
|
139
|
-
// against the UNCOMPRESSED body size, never the (much smaller) gz file size.
|
|
140
|
-
const SNAPSHOT_PARSE_CACHE_MAX_BYTES = 24 * 1024 * 1024;
|
|
141
|
-
const snapshotParseCache = new BoundedFifoMap(SNAPSHOT_PARSE_CACHE_MAX);
|
|
142
|
-
function withoutWordIndex(snapshot) {
|
|
143
|
-
if (!snapshot?.wordIndex)
|
|
144
|
-
return snapshot;
|
|
145
|
-
const { wordIndex: _releasedPostings, ...stripped } = snapshot;
|
|
146
|
-
return stripped;
|
|
147
|
-
}
|
|
148
|
-
function cacheParsedSnapshot(snapshotPath, entry) {
|
|
149
|
-
// Refresh recency (delete then set moves the key to the newest position;
|
|
150
|
-
// BoundedFifoMap itself never reorders an already-present key on set()).
|
|
151
|
-
snapshotParseCache.delete(snapshotPath);
|
|
152
|
-
snapshotParseCache.set(snapshotPath, entry);
|
|
153
|
-
}
|
|
154
|
-
const authoritativeSnapshots = new Map();
|
|
155
|
-
export const PROJECT_SNAPSHOT_MAX_WARM_ROOTS = 8;
|
|
156
|
-
const PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT = 20 * 60_000;
|
|
157
|
-
function projectSnapshotIdleEvictMs() {
|
|
158
|
-
const value = Number.parseInt(process.env.PI_LENS_PROJECT_SNAPSHOT_IDLE_EVICT_MS ?? "", 10);
|
|
159
|
-
return Number.isSafeInteger(value) && value > 0
|
|
160
|
-
? value
|
|
161
|
-
: PROJECT_SNAPSHOT_IDLE_EVICT_MS_DEFAULT;
|
|
162
|
-
}
|
|
163
|
-
function clearAuthoritativeSnapshotTimer(entry) {
|
|
164
|
-
if (entry.idleTimer)
|
|
165
|
-
clearTimeout(entry.idleTimer);
|
|
166
|
-
entry.idleTimer = undefined;
|
|
167
|
-
}
|
|
168
|
-
function deleteAuthoritativeSnapshot(key) {
|
|
169
|
-
const entry = authoritativeSnapshots.get(key);
|
|
170
|
-
if (entry)
|
|
171
|
-
clearAuthoritativeSnapshotTimer(entry);
|
|
172
|
-
authoritativeSnapshots.delete(key);
|
|
173
|
-
}
|
|
174
|
-
function scheduleAuthoritativeSnapshotEviction(key, entry) {
|
|
175
|
-
clearAuthoritativeSnapshotTimer(entry);
|
|
176
|
-
const generation = entry.lastUsedAt;
|
|
177
|
-
entry.idleTimer = setTimeout(() => {
|
|
178
|
-
entry.idleTimer = undefined;
|
|
179
|
-
if (authoritativeSnapshots.get(key) !== entry ||
|
|
180
|
-
entry.lastUsedAt !== generation)
|
|
181
|
-
return;
|
|
182
|
-
deleteAuthoritativeSnapshot(key);
|
|
183
|
-
}, projectSnapshotIdleEvictMs());
|
|
184
|
-
entry.idleTimer.unref?.();
|
|
185
|
-
}
|
|
186
|
-
function touchAuthoritativeSnapshot(key, entry) {
|
|
187
|
-
entry.lastUsedAt = Date.now();
|
|
188
|
-
scheduleAuthoritativeSnapshotEviction(key, entry);
|
|
189
|
-
}
|
|
190
|
-
function enforceAuthoritativeSnapshotCap() {
|
|
191
|
-
while (authoritativeSnapshots.size > PROJECT_SNAPSHOT_MAX_WARM_ROOTS) {
|
|
192
|
-
const victim = [...authoritativeSnapshots.entries()].sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)[0];
|
|
193
|
-
if (!victim)
|
|
194
|
-
return;
|
|
195
|
-
clearAuthoritativeSnapshotTimer(victim[1]);
|
|
196
|
-
deleteAuthoritativeSnapshot(victim[0]);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
/** Test-only cache keys, in LRU order from oldest to newest. */
|
|
200
|
-
export function _getAuthoritativeSnapshotCacheKeysForTests() {
|
|
201
|
-
return [...authoritativeSnapshots.entries()]
|
|
202
|
-
.sort(([, a], [, b]) => a.lastUsedAt - b.lastUsedAt)
|
|
203
|
-
.map(([key]) => key);
|
|
204
|
-
}
|
|
205
|
-
/** Test hook: drop all cached parses + authoritative writes (per-worker isolation). */
|
|
206
|
-
export function _resetProjectSnapshotParseCacheForTests() {
|
|
207
|
-
snapshotParseCache.clear();
|
|
208
|
-
for (const entry of authoritativeSnapshots.values())
|
|
209
|
-
clearAuthoritativeSnapshotTimer(entry);
|
|
210
|
-
authoritativeSnapshots.clear();
|
|
211
|
-
}
|
|
212
|
-
/** Test hook: prove the parse-cache tier never owns serialized postings. */
|
|
213
|
-
export function _projectSnapshotParseCacheRetainsWordIndexForTests() {
|
|
214
|
-
return [...snapshotParseCache.values()].some((entry) => entry.snapshot?.wordIndex !== undefined);
|
|
215
|
-
}
|
|
216
|
-
/** Resolve which body file is currently on disk: gz canonical, else legacy. */
|
|
217
|
-
function resolveSnapshotBodyPath(cwd) {
|
|
218
|
-
const gzPath = getProjectSnapshotPath(cwd);
|
|
219
|
-
try {
|
|
220
|
-
const stat = fs.statSync(gzPath);
|
|
221
|
-
return { path: gzPath, gz: true, mtimeMs: stat.mtimeMs, size: stat.size };
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
/* fall through to the legacy uncompressed body */
|
|
225
|
-
}
|
|
226
|
-
const legacyPath = getProjectSnapshotLegacyPath(cwd);
|
|
227
|
-
try {
|
|
228
|
-
const stat = fs.statSync(legacyPath);
|
|
229
|
-
return {
|
|
230
|
-
path: legacyPath,
|
|
231
|
-
gz: false,
|
|
232
|
-
mtimeMs: stat.mtimeMs,
|
|
233
|
-
size: stat.size,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
/**
|
|
241
|
-
* Read + parse the body off disk, transparently gunzipping the `.json.gz`
|
|
242
|
-
* canonical form and falling back to the pre-#958 uncompressed `.json` body
|
|
243
|
-
* for one compatibility release. Returns the parsed snapshot (or null on any
|
|
244
|
-
* failure) plus the UNCOMPRESSED byte length, so the caller can apply the
|
|
245
|
-
* heap-bounded cache gate against the real body size rather than the gz size.
|
|
246
|
-
*/
|
|
247
|
-
// Test-observable count of ACTUAL disk body reads (both gz and legacy), so the
|
|
248
|
-
// #947 meta-gate tests can assert "body parsed / not parsed" independently of
|
|
249
|
-
// the compression format — the pre-#958 tests keyed this off a readJsonCache
|
|
250
|
-
// spy, which the gz path (gunzip + JSON.parse) legitimately bypasses.
|
|
251
|
-
let _snapshotBodyReadCountForTests = 0;
|
|
252
|
-
export function getSnapshotBodyReadCountForTests() {
|
|
253
|
-
return _snapshotBodyReadCountForTests;
|
|
254
|
-
}
|
|
255
|
-
export function resetSnapshotBodyReadCountForTests() {
|
|
256
|
-
_snapshotBodyReadCountForTests = 0;
|
|
257
|
-
}
|
|
258
|
-
function readSnapshotBody(bodyPath, gz) {
|
|
259
|
-
_snapshotBodyReadCountForTests++;
|
|
260
|
-
if (!gz) {
|
|
261
|
-
const snapshot = readJsonCache(bodyPath, (parsed) => parseSnapshot(parsed) ?? undefined) ?? null;
|
|
262
|
-
let rawBytes = 0;
|
|
263
|
-
try {
|
|
264
|
-
rawBytes = fs.statSync(bodyPath).size;
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
/* best-effort */
|
|
268
|
-
}
|
|
269
|
-
return { snapshot, rawBytes };
|
|
270
|
-
}
|
|
271
|
-
try {
|
|
272
|
-
const json = gunzipSync(fs.readFileSync(bodyPath)).toString("utf-8");
|
|
273
|
-
const snapshot = parseSnapshot(JSON.parse(json)) ?? null;
|
|
274
|
-
return { snapshot, rawBytes: Buffer.byteLength(json) };
|
|
275
|
-
}
|
|
276
|
-
catch (err) {
|
|
277
|
-
// Corrupt / truncated gz, or a parse failure: fail open exactly like
|
|
278
|
-
// readJsonCache does — a null return rebuilds the snapshot. Log it so a
|
|
279
|
-
// corrupt body is diagnosable rather than indistinguishable from "no
|
|
280
|
-
// snapshot yet" (both return a null snapshot here).
|
|
281
|
-
logLatency({
|
|
282
|
-
type: "phase",
|
|
283
|
-
phase: "project_snapshot_body_corrupt",
|
|
284
|
-
filePath: bodyPath,
|
|
285
|
-
durationMs: 0,
|
|
286
|
-
metadata: { error: err instanceof Error ? err.message : String(err) },
|
|
287
|
-
});
|
|
288
|
-
return { snapshot: null, rawBytes: 0 };
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
/**
|
|
292
|
-
* Scan a JSON value starting at `start` (skipping any leading whitespace)
|
|
293
|
-
* and return the index one past its end. Handles strings, objects, arrays,
|
|
294
|
-
* and bare scalars (numbers/`true`/`false`/`null`) — correctly skipping
|
|
295
|
-
* nested strings/braces/brackets (respecting `\"` escapes) WITHOUT ever
|
|
296
|
-
* constructing a JS value for them. This is the primitive
|
|
297
|
-
* `stripTopLevelJsonKeys` (#1785 F5) is built on: `JSON.parse` has no way to
|
|
298
|
-
* skip a subtree's construction cost — a reviver only prunes the
|
|
299
|
-
* already-built result — so avoiding a field's parse cost requires never
|
|
300
|
-
* handing its text to `JSON.parse` in the first place.
|
|
301
|
-
*/
|
|
302
|
-
function skipJsonValue(text, start) {
|
|
303
|
-
let i = start;
|
|
304
|
-
while (i < text.length && /\s/.test(text[i]))
|
|
305
|
-
i++;
|
|
306
|
-
const ch = text[i];
|
|
307
|
-
if (ch === '"') {
|
|
308
|
-
i++;
|
|
309
|
-
while (i < text.length) {
|
|
310
|
-
if (text[i] === "\\") {
|
|
311
|
-
i += 2;
|
|
312
|
-
continue;
|
|
313
|
-
}
|
|
314
|
-
if (text[i] === '"') {
|
|
315
|
-
i++;
|
|
316
|
-
break;
|
|
317
|
-
}
|
|
318
|
-
i++;
|
|
319
|
-
}
|
|
320
|
-
return i;
|
|
321
|
-
}
|
|
322
|
-
if (ch === "{" || ch === "[") {
|
|
323
|
-
const open = ch;
|
|
324
|
-
const close = open === "{" ? "}" : "]";
|
|
325
|
-
let depth = 0;
|
|
326
|
-
let inString = false;
|
|
327
|
-
for (; i < text.length; i++) {
|
|
328
|
-
const c = text[i];
|
|
329
|
-
if (inString) {
|
|
330
|
-
if (c === "\\") {
|
|
331
|
-
i++;
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
if (c === '"')
|
|
335
|
-
inString = false;
|
|
336
|
-
continue;
|
|
337
|
-
}
|
|
338
|
-
if (c === '"') {
|
|
339
|
-
inString = true;
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
if (c === open)
|
|
343
|
-
depth++;
|
|
344
|
-
else if (c === close) {
|
|
345
|
-
depth--;
|
|
346
|
-
if (depth === 0) {
|
|
347
|
-
i++;
|
|
348
|
-
break;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
return i;
|
|
353
|
-
}
|
|
354
|
-
// A bare scalar (number/true/false/null): scan to the next structural
|
|
355
|
-
// character. Every snapshot value is one of the five JSON kinds above, so
|
|
356
|
-
// this branch only ever fires for those scalars.
|
|
357
|
-
while (i < text.length && !",}]".includes(text[i]))
|
|
358
|
-
i++;
|
|
359
|
-
return i;
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Reconstruct a JSON object literal with the given top-level keys removed,
|
|
363
|
-
* without ever materializing their values as JS objects (#1785 F5). Used to
|
|
364
|
-
* excise the expensive fields — `wordIndex`'s postings graph above all,
|
|
365
|
-
* `files`/`symbols`/`reverseDeps` too — before `JSON.parse` ever sees them,
|
|
366
|
-
* so a caller that only needs a few small top-level fields (`seq`,
|
|
367
|
-
* `cachedExports`, `projectRulesScan`, …) doesn't pay to construct the
|
|
368
|
-
* fields it's about to discard. Correctness relies only on the writer
|
|
369
|
-
* (`JSON.stringify(snapshot)`, `clients/gzip-stage-write.ts`) never emitting
|
|
370
|
-
* pretty-printed/indented output — compact `JSON.stringify` output is what
|
|
371
|
-
* every persist path in this file produces.
|
|
372
|
-
*/
|
|
373
|
-
function stripTopLevelJsonKeys(text, keysToStrip) {
|
|
374
|
-
const objStart = text.indexOf("{");
|
|
375
|
-
if (objStart === -1)
|
|
376
|
-
return text;
|
|
377
|
-
let out = text.slice(0, objStart + 1);
|
|
378
|
-
let i = objStart + 1;
|
|
379
|
-
let wroteField = false;
|
|
380
|
-
while (i < text.length) {
|
|
381
|
-
while (i < text.length && /\s/.test(text[i]))
|
|
382
|
-
i++;
|
|
383
|
-
if (text[i] === "}" || i >= text.length) {
|
|
384
|
-
out += text.slice(i);
|
|
385
|
-
break;
|
|
386
|
-
}
|
|
387
|
-
if (text[i] === ",") {
|
|
388
|
-
i++;
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const keyStart = i;
|
|
392
|
-
const keyTextEnd = skipJsonValue(text, keyStart);
|
|
393
|
-
let key;
|
|
394
|
-
try {
|
|
395
|
-
key = JSON.parse(text.slice(keyStart, keyTextEnd));
|
|
396
|
-
}
|
|
397
|
-
catch {
|
|
398
|
-
// Malformed key text — bail out to the untouched original rather
|
|
399
|
-
// than risk producing invalid JSON; the caller's own JSON.parse
|
|
400
|
-
// on the result will then fail loudly instead of silently.
|
|
401
|
-
return text;
|
|
402
|
-
}
|
|
403
|
-
let j = keyTextEnd;
|
|
404
|
-
while (j < text.length && /\s/.test(text[j]))
|
|
405
|
-
j++;
|
|
406
|
-
// text[j] is ':' for a well-formed object.
|
|
407
|
-
const valueStart = j + 1;
|
|
408
|
-
const valueEnd = skipJsonValue(text, valueStart);
|
|
409
|
-
if (!keysToStrip.has(key)) {
|
|
410
|
-
out += (wroteField ? "," : "") + text.slice(keyStart, valueEnd);
|
|
411
|
-
wroteField = true;
|
|
412
|
-
}
|
|
413
|
-
i = valueEnd;
|
|
414
|
-
}
|
|
415
|
-
return out;
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Test-only direct access to the raw-text stripper (#1785 F5). Unlike
|
|
419
|
-
* asserting on `loadProjectSnapshotExportsAndRules`'s RETURN shape — which
|
|
420
|
-
* only proves the output omits the heavy fields, not that their parse cost
|
|
421
|
-
* was ever avoided (the function could strip nothing and still hand-pick 4
|
|
422
|
-
* fields out of a fully-parsed object) — this lets a test inspect the
|
|
423
|
-
* INTERMEDIATE text `JSON.parse` actually receives, which is the only way to
|
|
424
|
-
* prove the expensive fields' text never reached `JSON.parse` at all.
|
|
425
|
-
*/
|
|
426
|
-
export function _stripTopLevelJsonKeysForTests(text, keysToStrip) {
|
|
427
|
-
return stripTopLevelJsonKeys(text, new Set(keysToStrip));
|
|
428
|
-
}
|
|
429
|
-
// A fixed, 4-entry, import-time constant — not per-session accumulating
|
|
430
|
-
// state, so it needs no session_start reset (counted in
|
|
431
|
-
// tests/support/session-state-registry.ts's SESSION_STATE_SYMBOL_COUNTS
|
|
432
|
-
// pin for "project-snapshot.ts", alongside the bounded digest hook below).
|
|
433
|
-
//
|
|
434
|
-
// Adding a key here? Add it to `NARROW_DIGEST_HEAVY_KEY_LITERALS` too (~40
|
|
435
|
-
// lines down) — it's a DELIBERATELY separate, independent list (#1785 F7: a
|
|
436
|
-
// digest that reads its own containsHeavyKey answer off THIS set can't
|
|
437
|
-
// detect this set failing/emptying, so it must not).
|
|
438
|
-
const HEAVY_SNAPSHOT_KEYS = new Set([
|
|
439
|
-
"wordIndex",
|
|
440
|
-
"files",
|
|
441
|
-
"symbols",
|
|
442
|
-
"reverseDeps",
|
|
443
|
-
]);
|
|
444
|
-
// #1785 F7 (review round 5): a FIXED LITERAL list, independent of
|
|
445
|
-
// `HEAVY_SNAPSHOT_KEYS` — the digest below exists to detect a broken/emptied
|
|
446
|
-
// `HEAVY_SNAPSHOT_KEYS`, so deriving `containsHeavyKey` from that same
|
|
447
|
-
// constant makes the check vacuous by construction: empty the set and the
|
|
448
|
-
// stripper strips nothing AND `[...HEAVY_SNAPSHOT_KEYS].some(...)` iterates
|
|
449
|
-
// zero entries and reports `false` — the exact failure mode this hook exists
|
|
450
|
-
// to catch, silently passing. Verified: with the shared-constant version,
|
|
451
|
-
// emptying `HEAVY_SNAPSHOT_KEYS` left 38 tests green instead of red.
|
|
452
|
-
const NARROW_DIGEST_HEAVY_KEY_LITERALS = [
|
|
453
|
-
"wordIndex",
|
|
454
|
-
"files",
|
|
455
|
-
"symbols",
|
|
456
|
-
"reverseDeps",
|
|
457
|
-
];
|
|
458
|
-
let _lastNarrowParseDigestForTests;
|
|
459
|
-
export function getLastNarrowParseDigestForTests() {
|
|
460
|
-
return _lastNarrowParseDigestForTests;
|
|
461
|
-
}
|
|
462
|
-
export function resetLastNarrowParseDigestForTests() {
|
|
463
|
-
_lastNarrowParseDigestForTests = undefined;
|
|
464
|
-
}
|
|
465
|
-
function parseExportsAndRulesOnly(json) {
|
|
466
|
-
const narrowed = stripTopLevelJsonKeys(json, HEAVY_SNAPSHOT_KEYS);
|
|
467
|
-
_lastNarrowParseDigestForTests = {
|
|
468
|
-
length: narrowed.length,
|
|
469
|
-
containsHeavyKey: NARROW_DIGEST_HEAVY_KEY_LITERALS.some((key) => narrowed.includes(`"${key}":`)),
|
|
470
|
-
};
|
|
471
|
-
const parsed = JSON.parse(narrowed);
|
|
472
|
-
if (parsed.version !== PROJECT_SNAPSHOT_VERSION)
|
|
473
|
-
return null;
|
|
474
|
-
if (typeof parsed.seq !== "number")
|
|
475
|
-
return null;
|
|
476
|
-
if (!Array.isArray(parsed.cachedExports))
|
|
477
|
-
return null;
|
|
478
|
-
return {
|
|
479
|
-
version: PROJECT_SNAPSHOT_VERSION,
|
|
480
|
-
seq: parsed.seq,
|
|
481
|
-
cachedExports: parsed.cachedExports.filter((entry) => Array.isArray(entry) &&
|
|
482
|
-
typeof entry[0] === "string" &&
|
|
483
|
-
typeof entry[1] === "string"),
|
|
484
|
-
projectRulesScan: parsed.projectRulesScan,
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
function readSnapshotExportsAndRulesBody(bodyPath, gz) {
|
|
488
|
-
try {
|
|
489
|
-
const json = gz
|
|
490
|
-
? gunzipSync(fs.readFileSync(bodyPath)).toString("utf-8")
|
|
491
|
-
: fs.readFileSync(bodyPath, "utf-8");
|
|
492
|
-
return parseExportsAndRulesOnly(json);
|
|
493
|
-
}
|
|
494
|
-
catch (err) {
|
|
495
|
-
logLatency({
|
|
496
|
-
type: "phase",
|
|
497
|
-
phase: "project_snapshot_body_corrupt",
|
|
498
|
-
filePath: bodyPath,
|
|
499
|
-
durationMs: 0,
|
|
500
|
-
metadata: {
|
|
501
|
-
error: err instanceof Error ? err.message : String(err),
|
|
502
|
-
narrow: true,
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
return null;
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
/**
|
|
509
|
-
* #1785 F5: a narrow counterpart to `loadProjectSnapshot` for callers that
|
|
510
|
-
* only need `seq`/`version`/`cachedExports`/`projectRulesScan` — critically,
|
|
511
|
-
* NOT `wordIndex`, `files`, `symbols`, or `reverseDeps`. The full
|
|
512
|
-
* `loadProjectSnapshot` (even `loadProjectSnapshotWithoutWordIndex`, which
|
|
513
|
-
* only strips the RETAINED copy AFTER a full parse) pays `gunzip` +
|
|
514
|
-
* `JSON.parse` for the ENTIRE body regardless — dominated by `wordIndex`'s
|
|
515
|
-
* postings graph (measured: +200ms warm / +700ms cold on a 29MB body,
|
|
516
|
-
* 209-278ms on this repo's own 16.7MB snapshot). This function excises the
|
|
517
|
-
* heavy fields from the raw text BEFORE parsing (`stripTopLevelJsonKeys`),
|
|
518
|
-
* so their construction cost is never paid at all; the remaining cost is
|
|
519
|
-
* dominated by `cachedExports` (typically a small array), not the postings.
|
|
520
|
-
*
|
|
521
|
-
* Consults the same in-process authoritative-write cache
|
|
522
|
-
* `loadProjectSnapshotInternal` does, for the same read-your-own-write
|
|
523
|
-
* reason — a save's in-memory object is cheap to narrow (no parse needed at
|
|
524
|
-
* all) and must win over a possibly-stale on-disk body exactly like the full
|
|
525
|
-
* loader.
|
|
526
|
-
*/
|
|
527
|
-
export function loadProjectSnapshotExportsAndRules(cwd) {
|
|
528
|
-
const key = normalizeMapKey(cwd);
|
|
529
|
-
const body = resolveSnapshotBodyPath(cwd);
|
|
530
|
-
const authoritative = authoritativeSnapshots.get(key);
|
|
531
|
-
if (authoritative) {
|
|
532
|
-
// `body === null` means nothing is on disk — that is never "the body
|
|
533
|
-
// changed size", so it must not be read as superseding our own write.
|
|
534
|
-
// See the matching comment in loadProjectSnapshotInternal.
|
|
535
|
-
const notSuperseded = body === null ||
|
|
536
|
-
(body.mtimeMs <= authoritative.knownMtime &&
|
|
537
|
-
body.size === authoritative.knownSize);
|
|
538
|
-
if (notSuperseded) {
|
|
539
|
-
const { version, seq, cachedExports, projectRulesScan } = authoritative.snapshot;
|
|
540
|
-
return { version, seq, cachedExports, projectRulesScan };
|
|
541
|
-
}
|
|
542
|
-
// On mismatch, unlike the full loader, this narrow loader neither
|
|
543
|
-
// deletes the entry nor touches its idle timer (it never calls
|
|
544
|
-
// touchAuthoritativeSnapshot even on a hit). The stale entry is safe to
|
|
545
|
-
// leave in place: nothing here re-arms its eviction, so it is still
|
|
546
|
-
// bounded by whatever idle window the full loader (or the original
|
|
547
|
-
// write) last set, not left to live indefinitely.
|
|
548
|
-
}
|
|
549
|
-
if (!body)
|
|
550
|
-
return null;
|
|
551
|
-
return readSnapshotExportsAndRulesBody(body.path, body.gz);
|
|
552
|
-
}
|
|
553
|
-
function loadProjectSnapshotInternal(cwd, requireWordIndex) {
|
|
554
|
-
const key = normalizeMapKey(cwd);
|
|
555
|
-
const body = resolveSnapshotBodyPath(cwd);
|
|
556
|
-
// Authoritative in-process write wins while our own (possibly still
|
|
557
|
-
// in-flight) write has not been superseded on disk by a newer external mtime
|
|
558
|
-
// or a different-size body in the same/coarser bucket. `body === null` means
|
|
559
|
-
// nothing is on disk — either our just-scheduled write hasn't landed yet, or
|
|
560
|
-
// something removed the body after we wrote it (e.g. a cleared cache dir).
|
|
561
|
-
// Neither case is "the body changed size", so serve the in-process object.
|
|
562
|
-
const authoritative = authoritativeSnapshots.get(key);
|
|
563
|
-
if (authoritative) {
|
|
564
|
-
const notSuperseded = body === null ||
|
|
565
|
-
(body.mtimeMs <= authoritative.knownMtime &&
|
|
566
|
-
body.size === authoritative.knownSize);
|
|
567
|
-
if (notSuperseded) {
|
|
568
|
-
touchAuthoritativeSnapshot(key, authoritative);
|
|
569
|
-
return authoritative.snapshot;
|
|
570
|
-
}
|
|
571
|
-
// An external writer changed the body beyond our metadata stamp — honor
|
|
572
|
-
// disk and stop serving the now-stale in-memory object.
|
|
573
|
-
deleteAuthoritativeSnapshot(key);
|
|
574
|
-
}
|
|
575
|
-
if (!body) {
|
|
576
|
-
snapshotParseCache.delete(getProjectSnapshotPath(cwd));
|
|
577
|
-
return null;
|
|
578
|
-
}
|
|
579
|
-
const cacheKey = body.path;
|
|
580
|
-
const cached = snapshotParseCache.get(cacheKey);
|
|
581
|
-
// Both mtime AND size must match (see the `size` field doc on
|
|
582
|
-
// SnapshotParseCacheEntry for why: coarse FAT/exFAT mtime resolution can
|
|
583
|
-
// otherwise alias a just-rewritten file onto a stale cache entry).
|
|
584
|
-
if (cached && cached.mtimeMs === body.mtimeMs && cached.size === body.size) {
|
|
585
|
-
if (!requireWordIndex || !cached.snapshot || cached.snapshot.wordIndex) {
|
|
586
|
-
return cached.snapshot;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
const { snapshot, rawBytes } = readSnapshotBody(body.path, body.gz);
|
|
590
|
-
// Serialized postings expand into a much larger object graph. Cache only a
|
|
591
|
-
// shallow postings-stripped body: metadata/report consumers stay warm while
|
|
592
|
-
// the live warm WordIndex remains the sole retained postings graph (#1370).
|
|
593
|
-
const cacheSnapshot = withoutWordIndex(snapshot);
|
|
594
|
-
if (rawBytes > 0 &&
|
|
595
|
-
(rawBytes <= SNAPSHOT_PARSE_CACHE_MAX_BYTES || snapshot?.wordIndex)) {
|
|
596
|
-
cacheParsedSnapshot(cacheKey, {
|
|
597
|
-
mtimeMs: body.mtimeMs,
|
|
598
|
-
size: body.size,
|
|
599
|
-
snapshot: cacheSnapshot,
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
603
|
-
snapshotParseCache.delete(cacheKey);
|
|
604
|
-
}
|
|
605
|
-
return snapshot;
|
|
606
|
-
}
|
|
607
|
-
/** Load the canonical body, including serialized postings when present. */
|
|
608
|
-
export function loadProjectSnapshot(cwd) {
|
|
609
|
-
return loadProjectSnapshotInternal(cwd, true);
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* Load snapshot metadata without retaining or re-reading serialized postings.
|
|
613
|
-
* After publication this is served by the postings-stripped parse cache.
|
|
614
|
-
*/
|
|
615
|
-
export function loadProjectSnapshotWithoutWordIndex(cwd) {
|
|
616
|
-
return loadProjectSnapshotInternal(cwd, false);
|
|
617
|
-
}
|
|
618
|
-
const _snapshotGenerationStates = new Map();
|
|
619
|
-
const _successfulSnapshotPersists = new BoundedFifoMap(PROJECT_SNAPSHOT_MAX_WARM_ROOTS);
|
|
620
|
-
const _failedSnapshotPersists = new BoundedFifoMap(PROJECT_SNAPSHOT_MAX_WARM_ROOTS);
|
|
621
|
-
const _activeSnapshotPersists = new Map();
|
|
622
|
-
const _queuedSnapshotPersists = new Map();
|
|
623
|
-
const _snapshotWorkerRequests = new Map();
|
|
624
|
-
let _snapshotPersistWorker;
|
|
625
|
-
let _snapshotWorkerRequestId = 0;
|
|
626
|
-
let _snapshotWorkerDisabled = false;
|
|
627
|
-
let _snapshotGenerationGateEnabledForTests = true;
|
|
628
|
-
let _snapshotPromotionSeamForTests;
|
|
629
|
-
let _lastSnapshotPersistErrorForTests;
|
|
630
|
-
let _snapshotExiting = false;
|
|
631
|
-
let _snapshotWorkerBodyWritesForTests = 0;
|
|
632
|
-
function snapshotWorkerEnabled() {
|
|
633
|
-
// The synchronous fallback writer is a legitimate degraded mode (hosts that
|
|
634
|
-
// can't spawn a worker); tests also force it so a save→load is fully
|
|
635
|
-
// synchronous. Production defaults to the worker.
|
|
636
|
-
const raw = process.env.PI_LENS_SNAPSHOT_PERSIST_SYNC;
|
|
637
|
-
return !(raw === "1" || raw === "true");
|
|
638
|
-
}
|
|
639
|
-
function pendingSnapshotIsCurrent(pending) {
|
|
640
|
-
return (!_snapshotGenerationGateEnabledForTests ||
|
|
641
|
-
_snapshotGenerationStates.get(pending.key)?.generation ===
|
|
642
|
-
pending.generation);
|
|
643
|
-
}
|
|
644
|
-
function rememberSuccessfulSnapshotPersist(key, record) {
|
|
645
|
-
_successfulSnapshotPersists.delete(key);
|
|
646
|
-
_successfulSnapshotPersists.set(key, record);
|
|
647
|
-
}
|
|
648
|
-
function assessSnapshotBodyIntegrity(meta, body) {
|
|
649
|
-
if (meta.gzBytes === undefined) {
|
|
650
|
-
// Legacy meta (pre-#2008): nothing to compare against, so the evidence is
|
|
651
|
-
// untrusted until the next successful persist populates gzBytes.
|
|
652
|
-
return { outcome: "legacy-meta", actualBytes: body.size };
|
|
653
|
-
}
|
|
654
|
-
if (!body.gz || body.size !== meta.gzBytes) {
|
|
655
|
-
return {
|
|
656
|
-
outcome: "size-mismatch",
|
|
657
|
-
expectedGzBytes: meta.gzBytes,
|
|
658
|
-
actualBytes: body.size,
|
|
659
|
-
};
|
|
660
|
-
}
|
|
661
|
-
return {
|
|
662
|
-
outcome: "ok",
|
|
663
|
-
expectedGzBytes: meta.gzBytes,
|
|
664
|
-
actualBytes: body.size,
|
|
665
|
-
};
|
|
666
|
-
}
|
|
667
|
-
/**
|
|
668
|
-
* Bounded observability for #2008: the degradation ledger keeps one entry per
|
|
669
|
-
* corrupted subject with the exact detection count for the session; every
|
|
670
|
-
* detection also emits one `project_snapshot_body_integrity` latency row
|
|
671
|
-
* naming expected vs actual bytes, so an operator can confirm both the
|
|
672
|
-
* detection and the forced republish from logs alone.
|
|
673
|
-
*/
|
|
674
|
-
function noteSnapshotBodyIntegrityWithheld(args) {
|
|
675
|
-
incrementDegradationCount({
|
|
676
|
-
kind: "snapshot-integrity",
|
|
677
|
-
subject: args.gzPath,
|
|
678
|
-
reason: args.verdict,
|
|
679
|
-
});
|
|
680
|
-
logLatency({
|
|
681
|
-
type: "phase",
|
|
682
|
-
phase: "project_snapshot_body_integrity",
|
|
683
|
-
filePath: args.gzPath,
|
|
684
|
-
durationMs: 0,
|
|
685
|
-
metadata: {
|
|
686
|
-
outcome: "dedupe_withheld",
|
|
687
|
-
reason: args.verdict,
|
|
688
|
-
...(args.seq !== undefined ? { seq: args.seq } : {}),
|
|
689
|
-
...(args.expectedGzBytes === undefined
|
|
690
|
-
? {}
|
|
691
|
-
: { expectedGzBytes: args.expectedGzBytes }),
|
|
692
|
-
actualBytes: args.actualBytes,
|
|
693
|
-
},
|
|
694
|
-
});
|
|
695
|
-
}
|
|
696
|
-
/** Log one row naming a skip that was refused because integrity regressed. */
|
|
697
|
-
function noteSnapshotSkipRefused(args) {
|
|
698
|
-
logLatency({
|
|
699
|
-
type: "phase",
|
|
700
|
-
phase: "project_snapshot_body_integrity",
|
|
701
|
-
filePath: args.gzPath,
|
|
702
|
-
durationMs: 0,
|
|
703
|
-
metadata: {
|
|
704
|
-
outcome: "skip_refused_rewrite",
|
|
705
|
-
reason: args.verdict,
|
|
706
|
-
seq: args.seq,
|
|
707
|
-
},
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
/**
|
|
711
|
-
* PURE READ of the durable meta+body evidence for a snapshot key (#2008
|
|
712
|
-
* refactor): it must never mutate {@link _successfulSnapshotPersists}. This
|
|
713
|
-
* runs up to three times per persist — admission in saveProjectSnapshot, the
|
|
714
|
-
* dispatch-time refresh, and the skip-honor re-read — including on results
|
|
715
|
-
* whose fingerprints are only partially used, so the seeding write lives in
|
|
716
|
-
* {@link seedSnapshotPersistBaselineFromDurable} and is called only by the
|
|
717
|
-
* seam that owns the persist lifecycle (dispatchSnapshotPersist).
|
|
718
|
-
*/
|
|
719
|
-
function snapshotPersistBaselinesFor(cwd, key) {
|
|
720
|
-
const local = _successfulSnapshotPersists.get(key);
|
|
721
|
-
const meta = readProjectSnapshotMeta(cwd);
|
|
722
|
-
const body = resolveSnapshotBodyPath(cwd);
|
|
723
|
-
// No body means no dedupe evidence. This keeps same-seq deletion repair live.
|
|
724
|
-
if (!meta?.fingerprint || !body) {
|
|
725
|
-
if (!body)
|
|
726
|
-
_successfulSnapshotPersists.delete(key);
|
|
727
|
-
return { fingerprints: [] };
|
|
728
|
-
}
|
|
729
|
-
// #2008: a meta whose recorded gz size no longer matches the on-disk body
|
|
730
|
-
// describes evidence we cannot trust — a truncated/torn gzip under an
|
|
731
|
-
// intact meta used to win same-fingerprint dedupe forever, keeping the
|
|
732
|
-
// corrupt body canonical until seq advanced. Withhold the fingerprints so
|
|
733
|
-
// the pending save republishes (and rewrites) the body.
|
|
734
|
-
const integrity = assessSnapshotBodyIntegrity(meta, body);
|
|
735
|
-
if (integrity.outcome !== "ok") {
|
|
736
|
-
noteSnapshotBodyIntegrityWithheld({
|
|
737
|
-
gzPath: getProjectSnapshotPath(cwd),
|
|
738
|
-
verdict: integrity.outcome,
|
|
739
|
-
seq: meta.seq,
|
|
740
|
-
expectedGzBytes: integrity.expectedGzBytes,
|
|
741
|
-
actualBytes: integrity.actualBytes,
|
|
742
|
-
});
|
|
743
|
-
return { fingerprints: [] };
|
|
744
|
-
}
|
|
745
|
-
const durable = {
|
|
746
|
-
seq: meta.seq,
|
|
747
|
-
fingerprint: meta.fingerprint,
|
|
748
|
-
generatedAt: meta.timestamp,
|
|
749
|
-
generation: _snapshotGenerationStates.get(key)?.generation ?? 0,
|
|
750
|
-
gzBytes: meta.gzBytes,
|
|
751
|
-
};
|
|
752
|
-
const fingerprints = [durable.fingerprint];
|
|
753
|
-
// A sibling process may have advanced durable state from local A to B. An
|
|
754
|
-
// unchanged replay of A is stale work and must not overwrite B. A third
|
|
755
|
-
// semantic state C matches neither fingerprint and still publishes.
|
|
756
|
-
if (local && local.fingerprint !== durable.fingerprint) {
|
|
757
|
-
fingerprints.push(local.fingerprint);
|
|
758
|
-
}
|
|
759
|
-
return { durable, fingerprints, integrity: "ok" };
|
|
760
|
-
}
|
|
761
|
-
/**
|
|
762
|
-
* Seed the in-process baseline for `key` from freshly-read durable state.
|
|
763
|
-
* Deliberately separate from {@link snapshotPersistBaselinesFor} so the read
|
|
764
|
-
* stays pure; called only at the dispatch seam where the persist lifecycle
|
|
765
|
-
* begins.
|
|
766
|
-
*/
|
|
767
|
-
function seedSnapshotPersistBaselineFromDurable(key, durable) {
|
|
768
|
-
if (!durable || _successfulSnapshotPersists.has(key))
|
|
769
|
-
return;
|
|
770
|
-
rememberSuccessfulSnapshotPersist(key, durable);
|
|
771
|
-
}
|
|
772
|
-
function writeProjectSnapshotMeta(metaPath, snapshot, bodyRecord) {
|
|
773
|
-
writeFileAtomic(metaPath, JSON.stringify({
|
|
774
|
-
timestamp: bodyRecord?.generatedAt ?? snapshot.generatedAt,
|
|
775
|
-
version: snapshot.version,
|
|
776
|
-
seq: snapshot.seq,
|
|
777
|
-
...(bodyRecord
|
|
778
|
-
? {
|
|
779
|
-
fingerprint: bodyRecord.fingerprint,
|
|
780
|
-
...(bodyRecord.gzBytes === undefined
|
|
781
|
-
? {}
|
|
782
|
-
: { gzBytes: bodyRecord.gzBytes }),
|
|
783
|
-
}
|
|
784
|
-
: {}),
|
|
785
|
-
...(snapshot.sequenceIndex
|
|
786
|
-
? { sequenceIndex: snapshot.sequenceIndex }
|
|
787
|
-
: {}),
|
|
788
|
-
}), { bestEffort: false });
|
|
789
|
-
}
|
|
790
|
-
function recordSnapshotPersistFailure(pending, error, bodyPersisted = false) {
|
|
791
|
-
// Honesty (#533): a failed async body write must be surfaced, never left to
|
|
792
|
-
// masquerade as a saved snapshot. The meta gate is already self-healing (an
|
|
793
|
-
// old body under a newer-seq meta is rejected on the body's own embedded
|
|
794
|
-
// seq), and dropping the authoritative entry means the next load reflects
|
|
795
|
-
// what is ACTUALLY on disk rather than the object we failed to persist.
|
|
796
|
-
_lastSnapshotPersistErrorForTests = error;
|
|
797
|
-
_failedSnapshotPersists.set(pending.key, {
|
|
798
|
-
seq: pending.snapshot.seq,
|
|
799
|
-
generation: pending.generation,
|
|
800
|
-
});
|
|
801
|
-
deleteAuthoritativeSnapshot(pending.key);
|
|
802
|
-
logLatency({
|
|
803
|
-
type: "phase",
|
|
804
|
-
phase: "project_snapshot_persist_failed",
|
|
805
|
-
filePath: pending.gzPath,
|
|
806
|
-
durationMs: 0,
|
|
807
|
-
metadata: {
|
|
808
|
-
error,
|
|
809
|
-
seq: pending.snapshot.seq,
|
|
810
|
-
outcome: bodyPersisted ? "metadata_repair_required" : "failed",
|
|
811
|
-
...(bodyPersisted ? { bodyPersisted: true } : {}),
|
|
812
|
-
},
|
|
813
|
-
});
|
|
814
|
-
// #1333: the logLatency call above already carries this failure to
|
|
815
|
-
// latency.log — the console.error was a duplicate RAW write into pi's frame.
|
|
816
|
-
}
|
|
817
|
-
function logSnapshotPersistSuccess(pending, fingerprint, stats) {
|
|
818
|
-
rememberSuccessfulSnapshotPersist(pending.key, {
|
|
819
|
-
seq: pending.snapshot.seq,
|
|
820
|
-
fingerprint,
|
|
821
|
-
generatedAt: pending.snapshot.generatedAt,
|
|
822
|
-
generation: pending.generation,
|
|
823
|
-
rawBytes: stats.rawBytes,
|
|
824
|
-
gzBytes: stats.gzBytes,
|
|
825
|
-
});
|
|
826
|
-
_failedSnapshotPersists.delete(pending.key);
|
|
827
|
-
logLatency({
|
|
828
|
-
type: "phase",
|
|
829
|
-
phase: "project_snapshot_persist",
|
|
830
|
-
filePath: pending.gzPath,
|
|
831
|
-
durationMs: stats.serializeMs + stats.writeMs,
|
|
832
|
-
metadata: { seq: pending.snapshot.seq, outcome: "executed", ...stats },
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
function logSnapshotPersistDecision(args) {
|
|
836
|
-
logLatency({
|
|
837
|
-
type: "phase",
|
|
838
|
-
phase: "project_snapshot_persist_decision",
|
|
839
|
-
filePath: getProjectSnapshotPath(args.cwd),
|
|
840
|
-
durationMs: 0,
|
|
841
|
-
metadata: {
|
|
842
|
-
seq: args.seq,
|
|
843
|
-
decision: args.decision,
|
|
844
|
-
...(args.fingerprint
|
|
845
|
-
? { fingerprint: args.fingerprint.slice(0, 12) }
|
|
846
|
-
: {}),
|
|
847
|
-
...(args.avoidedRawBytes === undefined
|
|
848
|
-
? {}
|
|
849
|
-
: { avoidedRawBytes: args.avoidedRawBytes }),
|
|
850
|
-
...(args.avoidedGzipBytes === undefined
|
|
851
|
-
? {}
|
|
852
|
-
: { avoidedGzipBytes: args.avoidedGzipBytes }),
|
|
853
|
-
},
|
|
854
|
-
});
|
|
855
|
-
}
|
|
856
|
-
/**
|
|
857
|
-
* Reconcile the authoritative in-process entry with a body that just landed on
|
|
858
|
-
* disk: update its `knownMtime` so a subsequent load keeps serving our own
|
|
859
|
-
* object without re-parsing, and DROP oversized bodies (their post-promotion
|
|
860
|
-
* disk read is the pre-#947 behavior — we won't pin hundreds of MB of heap).
|
|
861
|
-
*/
|
|
862
|
-
function reconcileAuthoritativeAfterWrite(pending, rawBytes) {
|
|
863
|
-
const entry = authoritativeSnapshots.get(pending.key);
|
|
864
|
-
// Only reconcile the entry that still belongs to THIS (latest) generation —
|
|
865
|
-
// a superseding save already replaced it with a newer object.
|
|
866
|
-
if (!entry || entry.snapshot !== pending.snapshot)
|
|
867
|
-
return;
|
|
868
|
-
// The worker needs the serialized snapshot until promotion completes, but
|
|
869
|
-
// retaining it afterward duplicates the mutable warm index's postings. A
|
|
870
|
-
// shared reference is unsafe: ProjectSnapshot stores serialized arrays while
|
|
871
|
-
// WordIndex owns mutable Map/PathKeyedMap state. Drop the authoritative copy
|
|
872
|
-
// after publication; later merge-writers rehydrate the canonical disk body.
|
|
873
|
-
if (pending.snapshot.wordIndex) {
|
|
874
|
-
try {
|
|
875
|
-
const stat = fs.statSync(pending.gzPath);
|
|
876
|
-
cacheParsedSnapshot(pending.gzPath, {
|
|
877
|
-
mtimeMs: stat.mtimeMs,
|
|
878
|
-
size: stat.size,
|
|
879
|
-
snapshot: withoutWordIndex(pending.snapshot),
|
|
880
|
-
});
|
|
881
|
-
}
|
|
882
|
-
catch {
|
|
883
|
-
// A cache miss is safe: the first metadata consumer reconstructs it.
|
|
884
|
-
}
|
|
885
|
-
deleteAuthoritativeSnapshot(pending.key);
|
|
886
|
-
logLatency({
|
|
887
|
-
type: "phase",
|
|
888
|
-
phase: "project_snapshot_word_index_released",
|
|
889
|
-
filePath: pending.gzPath,
|
|
890
|
-
durationMs: 0,
|
|
891
|
-
metadata: { rawBytes },
|
|
892
|
-
});
|
|
893
|
-
return;
|
|
894
|
-
}
|
|
895
|
-
if (rawBytes > SNAPSHOT_PARSE_CACHE_MAX_BYTES) {
|
|
896
|
-
// Benign but invisible otherwise: the next load will re-parse this body
|
|
897
|
-
// from disk instead of serving the in-process object.
|
|
898
|
-
logLatency({
|
|
899
|
-
type: "phase",
|
|
900
|
-
phase: "project_snapshot_authoritative_dropped_oversized",
|
|
901
|
-
filePath: pending.gzPath,
|
|
902
|
-
durationMs: 0,
|
|
903
|
-
metadata: { rawBytes, maxBytes: SNAPSHOT_PARSE_CACHE_MAX_BYTES },
|
|
904
|
-
});
|
|
905
|
-
deleteAuthoritativeSnapshot(pending.key);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
try {
|
|
909
|
-
const stat = fs.statSync(pending.gzPath);
|
|
910
|
-
entry.knownMtime = stat.mtimeMs;
|
|
911
|
-
entry.knownSize = stat.size;
|
|
912
|
-
}
|
|
913
|
-
catch {
|
|
914
|
-
// If we can't stat our own write, leave the metadata as-is; the worst case
|
|
915
|
-
// is one extra disk re-parse on the next load.
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
function finalizeProjectSnapshotMeta(pending, record) {
|
|
919
|
-
try {
|
|
920
|
-
writeProjectSnapshotMeta(getProjectSnapshotMetaPath(pending.cwd), pending.snapshot, record);
|
|
921
|
-
return true;
|
|
922
|
-
}
|
|
923
|
-
catch (err) {
|
|
924
|
-
recordSnapshotPersistFailure(pending, err instanceof Error ? err.message : String(err), true);
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
function completeSnapshotPersist(pending) {
|
|
929
|
-
if (_activeSnapshotPersists.get(pending.key) !== pending)
|
|
930
|
-
return;
|
|
931
|
-
_activeSnapshotPersists.delete(pending.key);
|
|
932
|
-
if (_snapshotExiting)
|
|
933
|
-
return;
|
|
934
|
-
const queued = _queuedSnapshotPersists.get(pending.key);
|
|
935
|
-
if (!queued)
|
|
936
|
-
return;
|
|
937
|
-
_queuedSnapshotPersists.delete(pending.key);
|
|
938
|
-
dispatchSnapshotPersist(queued);
|
|
939
|
-
}
|
|
940
|
-
function writeSnapshotBodyOnMainThread(pending, reason) {
|
|
941
|
-
if (!pendingSnapshotIsCurrent(pending)) {
|
|
942
|
-
completeSnapshotPersist(pending);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
if (reason) {
|
|
946
|
-
// We took the synchronous main-thread gzip path (the +656MB-risk path,
|
|
947
|
-
// #950) instead of the worker. Surface it rather than burying it in an
|
|
948
|
-
// `offloaded:false` success line below. `degraded` distinguishes a REAL
|
|
949
|
-
// degradation (worker died/unavailable/promote-failed) from the benign
|
|
950
|
-
// `exit_hook` teardown flush, so an operator triaging worker health isn't
|
|
951
|
-
// misled by normal process-exit flushes.
|
|
952
|
-
logLatency({
|
|
953
|
-
type: "phase",
|
|
954
|
-
phase: "project_snapshot_worker_fallback",
|
|
955
|
-
filePath: pending.gzPath,
|
|
956
|
-
durationMs: 0,
|
|
957
|
-
metadata: {
|
|
958
|
-
reason,
|
|
959
|
-
seq: pending.snapshot.seq,
|
|
960
|
-
degraded: reason !== "exit_hook",
|
|
961
|
-
},
|
|
962
|
-
});
|
|
963
|
-
}
|
|
964
|
-
try {
|
|
965
|
-
const serializeStarted = performance.now();
|
|
966
|
-
const json = JSON.stringify(pending.snapshot);
|
|
967
|
-
const serializeMs = performance.now() - serializeStarted;
|
|
968
|
-
const rawBytes = Buffer.byteLength(json);
|
|
969
|
-
const fingerprint = fingerprintProjectSnapshotJson(json, pending.snapshot.generatedAt);
|
|
970
|
-
if (pending.dedupeFingerprints.includes(fingerprint)) {
|
|
971
|
-
// Re-read the tiny durable sidecar before honoring a fingerprint
|
|
972
|
-
// captured at dispatch time (#2008): a body torn between dispatch and
|
|
973
|
-
// execution must fall through to the full write, not skip.
|
|
974
|
-
const current = snapshotPersistBaselinesFor(pending.cwd, pending.key);
|
|
975
|
-
if (current.integrity === "ok") {
|
|
976
|
-
const prior = current.durable ?? pending.durablePersist;
|
|
977
|
-
if (authoritativeSnapshots.get(pending.key)?.snapshot === pending.snapshot) {
|
|
978
|
-
deleteAuthoritativeSnapshot(pending.key);
|
|
979
|
-
}
|
|
980
|
-
logSnapshotPersistDecision({
|
|
981
|
-
cwd: pending.cwd,
|
|
982
|
-
seq: pending.snapshot.seq,
|
|
983
|
-
fingerprint,
|
|
984
|
-
decision: "skipped_unchanged",
|
|
985
|
-
avoidedRawBytes: rawBytes,
|
|
986
|
-
avoidedGzipBytes: prior?.gzBytes,
|
|
987
|
-
});
|
|
988
|
-
return;
|
|
989
|
-
}
|
|
990
|
-
if (current.integrity !== undefined) {
|
|
991
|
-
noteSnapshotSkipRefused({
|
|
992
|
-
gzPath: pending.gzPath,
|
|
993
|
-
verdict: current.integrity,
|
|
994
|
-
seq: pending.snapshot.seq,
|
|
995
|
-
});
|
|
996
|
-
}
|
|
997
|
-
// integrity === undefined: the meta/body evidence vanished entirely
|
|
998
|
-
// since dispatch — falling through to the full write repairs it, the
|
|
999
|
-
// same contract as deletion-before-dispatch.
|
|
1000
|
-
}
|
|
1001
|
-
const writeStarted = performance.now();
|
|
1002
|
-
const gzip = gzipSync(json);
|
|
1003
|
-
fs.mkdirSync(path.dirname(pending.gzPath), { recursive: true });
|
|
1004
|
-
writeFileAtomic(pending.gzPath, gzip, { bestEffort: false });
|
|
1005
|
-
fs.rmSync(pending.legacyPath, { force: true });
|
|
1006
|
-
const metadataFinalized = finalizeProjectSnapshotMeta(pending, {
|
|
1007
|
-
fingerprint,
|
|
1008
|
-
generatedAt: pending.snapshot.generatedAt,
|
|
1009
|
-
gzBytes: gzip.byteLength,
|
|
1010
|
-
});
|
|
1011
|
-
if (!metadataFinalized)
|
|
1012
|
-
return;
|
|
1013
|
-
reconcileAuthoritativeAfterWrite(pending, rawBytes);
|
|
1014
|
-
logSnapshotPersistSuccess(pending, fingerprint, {
|
|
1015
|
-
rawBytes,
|
|
1016
|
-
gzBytes: gzip.byteLength,
|
|
1017
|
-
serializeMs,
|
|
1018
|
-
writeMs: performance.now() - writeStarted,
|
|
1019
|
-
offloaded: false,
|
|
1020
|
-
});
|
|
1021
|
-
if (reason)
|
|
1022
|
-
_lastSnapshotPersistErrorForTests = reason;
|
|
1023
|
-
}
|
|
1024
|
-
catch (err) {
|
|
1025
|
-
recordSnapshotPersistFailure(pending, err instanceof Error ? err.message : String(err));
|
|
1026
|
-
}
|
|
1027
|
-
finally {
|
|
1028
|
-
completeSnapshotPersist(pending);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
/**
|
|
1032
|
-
* The ForTests promotion seam covers worker-message and every main-thread
|
|
1033
|
-
* fallback promotion. It stays sync and seam-free in production, identical to
|
|
1034
|
-
* calling the writer directly. The process-exit hook is deliberately the sole
|
|
1035
|
-
* direct write because an exit handler cannot await this asynchronous seam.
|
|
1036
|
-
*/
|
|
1037
|
-
function dispatchMainThreadWriteThroughSeam(pending, reason) {
|
|
1038
|
-
if (_snapshotPromotionSeamForTests) {
|
|
1039
|
-
void _snapshotPromotionSeamForTests().then(() => writeSnapshotBodyOnMainThread(pending, reason));
|
|
1040
|
-
return;
|
|
1041
|
-
}
|
|
1042
|
-
writeSnapshotBodyOnMainThread(pending, reason);
|
|
1043
|
-
}
|
|
1044
|
-
function handleSnapshotWorkerResult(result) {
|
|
1045
|
-
const pending = _snapshotWorkerRequests.get(result.id);
|
|
1046
|
-
if (!pending) {
|
|
1047
|
-
fs.rmSync(result.stagePath, { force: true });
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
_snapshotWorkerRequests.delete(result.id);
|
|
1051
|
-
if (result.error ||
|
|
1052
|
-
result.rawBytes === undefined ||
|
|
1053
|
-
result.gzBytes === undefined ||
|
|
1054
|
-
result.serializeMs === undefined ||
|
|
1055
|
-
result.writeMs === undefined ||
|
|
1056
|
-
result.semanticFingerprint === undefined) {
|
|
1057
|
-
fs.rmSync(result.stagePath, { force: true });
|
|
1058
|
-
dispatchMainThreadWriteThroughSeam(pending, result.error ?? "invalid worker result");
|
|
1059
|
-
return;
|
|
1060
|
-
}
|
|
1061
|
-
// Generation gate: a newer save already superseded this one — discard the
|
|
1062
|
-
// stale stage file rather than promote it over the fresher body.
|
|
1063
|
-
if (!result.skippedUnchanged)
|
|
1064
|
-
_snapshotWorkerBodyWritesForTests++;
|
|
1065
|
-
if (_snapshotGenerationGateEnabledForTests &&
|
|
1066
|
-
_snapshotGenerationStates.get(pending.key)?.generation !== result.generation) {
|
|
1067
|
-
// The stale stage is part of the promotion transaction: remove it before
|
|
1068
|
-
// returning so a superseded save cannot leave an orphan behind.
|
|
1069
|
-
fs.rmSync(result.stagePath, { force: true });
|
|
1070
|
-
completeSnapshotPersist(pending);
|
|
1071
|
-
return;
|
|
1072
|
-
}
|
|
1073
|
-
if (result.skippedUnchanged) {
|
|
1074
|
-
// Re-read the tiny durable sidecar at the decision seam. A sibling process
|
|
1075
|
-
// may have advanced B after this request captured A. A stale replay of A
|
|
1076
|
-
// must preserve B's body metadata, not restore the captured A sidecar.
|
|
1077
|
-
const baselines = snapshotPersistBaselinesFor(pending.cwd, pending.key);
|
|
1078
|
-
if (baselines.integrity !== "ok") {
|
|
1079
|
-
// The worker skipped WITHOUT staging a body, trusting the durable
|
|
1080
|
-
// fingerprint captured at dispatch. The re-read says that evidence no
|
|
1081
|
-
// longer passes the #2008 integrity gate — refuse the skip and rewrite
|
|
1082
|
-
// synchronously so a torn/missing body cannot outlive this save. The
|
|
1083
|
-
// sync writer recomputes the same baselines, finds them untrusted, and
|
|
1084
|
-
// performs the full gzip+stage+promote (its finally completes this
|
|
1085
|
-
// pending, so do not complete it twice here).
|
|
1086
|
-
if (baselines.integrity !== undefined) {
|
|
1087
|
-
noteSnapshotSkipRefused({
|
|
1088
|
-
gzPath: pending.gzPath,
|
|
1089
|
-
verdict: baselines.integrity,
|
|
1090
|
-
seq: pending.snapshot.seq,
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
dispatchMainThreadWriteThroughSeam(pending, "snapshot body integrity regressed");
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
|
-
const prior = baselines.durable ?? pending.durablePersist;
|
|
1097
|
-
if (authoritativeSnapshots.get(pending.key)?.snapshot === pending.snapshot) {
|
|
1098
|
-
deleteAuthoritativeSnapshot(pending.key);
|
|
1099
|
-
}
|
|
1100
|
-
logSnapshotPersistDecision({
|
|
1101
|
-
cwd: pending.cwd,
|
|
1102
|
-
seq: pending.snapshot.seq,
|
|
1103
|
-
fingerprint: result.semanticFingerprint,
|
|
1104
|
-
decision: "skipped_unchanged",
|
|
1105
|
-
avoidedRawBytes: result.rawBytes,
|
|
1106
|
-
avoidedGzipBytes: prior?.gzBytes,
|
|
1107
|
-
});
|
|
1108
|
-
completeSnapshotPersist(pending);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
try {
|
|
1112
|
-
fs.renameSync(result.stagePath, pending.gzPath);
|
|
1113
|
-
fs.rmSync(pending.legacyPath, { force: true });
|
|
1114
|
-
const metadataFinalized = finalizeProjectSnapshotMeta(pending, {
|
|
1115
|
-
fingerprint: result.semanticFingerprint,
|
|
1116
|
-
generatedAt: pending.snapshot.generatedAt,
|
|
1117
|
-
gzBytes: result.gzBytes,
|
|
1118
|
-
});
|
|
1119
|
-
if (!metadataFinalized) {
|
|
1120
|
-
completeSnapshotPersist(pending);
|
|
1121
|
-
return;
|
|
1122
|
-
}
|
|
1123
|
-
reconcileAuthoritativeAfterWrite(pending, result.rawBytes);
|
|
1124
|
-
logSnapshotPersistSuccess(pending, result.semanticFingerprint, {
|
|
1125
|
-
rawBytes: result.rawBytes,
|
|
1126
|
-
gzBytes: result.gzBytes,
|
|
1127
|
-
serializeMs: result.serializeMs,
|
|
1128
|
-
writeMs: result.writeMs,
|
|
1129
|
-
offloaded: true,
|
|
1130
|
-
});
|
|
1131
|
-
completeSnapshotPersist(pending);
|
|
1132
|
-
}
|
|
1133
|
-
catch (err) {
|
|
1134
|
-
fs.rm(result.stagePath, { force: true }, () => { });
|
|
1135
|
-
dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
|
|
1136
|
-
}
|
|
1137
|
-
}
|
|
1138
|
-
function dispatchSnapshotPersist(pending) {
|
|
1139
|
-
// A queued request may have waited behind the publication that created its
|
|
1140
|
-
// best dedupe evidence. Refresh only the tiny sidecar/body stat here; never
|
|
1141
|
-
// retain the stale admission-time baseline through dispatch.
|
|
1142
|
-
const baselines = snapshotPersistBaselinesFor(pending.cwd, pending.key);
|
|
1143
|
-
pending.durablePersist = baselines.durable;
|
|
1144
|
-
pending.dedupeFingerprints = baselines.fingerprints;
|
|
1145
|
-
seedSnapshotPersistBaselineFromDurable(pending.key, baselines.durable);
|
|
1146
|
-
_activeSnapshotPersists.set(pending.key, pending);
|
|
1147
|
-
if (!snapshotWorkerEnabled()) {
|
|
1148
|
-
dispatchMainThreadWriteThroughSeam(pending, undefined);
|
|
1149
|
-
return;
|
|
1150
|
-
}
|
|
1151
|
-
const worker = getSnapshotPersistWorker();
|
|
1152
|
-
if (!worker) {
|
|
1153
|
-
dispatchMainThreadWriteThroughSeam(pending, "persist worker unavailable");
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
const id = ++_snapshotWorkerRequestId;
|
|
1157
|
-
_snapshotWorkerRequests.set(id, pending);
|
|
1158
|
-
const request = {
|
|
1159
|
-
id,
|
|
1160
|
-
generation: pending.generation,
|
|
1161
|
-
stagePath: pending.stagePath,
|
|
1162
|
-
data: pending.snapshot,
|
|
1163
|
-
priorFingerprints: pending.dedupeFingerprints,
|
|
1164
|
-
testDelayMs: process.env.NODE_ENV === "test"
|
|
1165
|
-
? Number(process.env.PI_LENS_TEST_SNAPSHOT_PERSIST_WORKER_DELAY_MS) ||
|
|
1166
|
-
undefined
|
|
1167
|
-
: undefined,
|
|
1168
|
-
};
|
|
1169
|
-
try {
|
|
1170
|
-
worker.postMessage(request);
|
|
1171
|
-
}
|
|
1172
|
-
catch (err) {
|
|
1173
|
-
_snapshotWorkerRequests.delete(id);
|
|
1174
|
-
dispatchMainThreadWriteThroughSeam(pending, err instanceof Error ? err.message : String(err));
|
|
1175
|
-
}
|
|
1176
|
-
}
|
|
1177
|
-
function handleSnapshotWorkerDeath(reason) {
|
|
1178
|
-
_snapshotPersistWorker = undefined;
|
|
1179
|
-
_snapshotWorkerDisabled = true;
|
|
1180
|
-
const requests = [..._snapshotWorkerRequests.values()];
|
|
1181
|
-
_snapshotWorkerRequests.clear();
|
|
1182
|
-
for (const pending of requests)
|
|
1183
|
-
dispatchMainThreadWriteThroughSeam(pending, reason);
|
|
1184
|
-
}
|
|
1185
|
-
function resolveSnapshotPersistWorkerPath() {
|
|
1186
|
-
// esbuild does NOT rewrite new URL(...) asset refs, so from the bundled
|
|
1187
|
-
// dist/index.js a sibling ./project-snapshot-persist-worker.js resolves
|
|
1188
|
-
// beside the BUNDLE where nothing exists. Try the compiled-sibling layout
|
|
1189
|
-
// first (source checkout / unbundled dist/clients tree), then the dist-tree
|
|
1190
|
-
// path relative to the bundle entry — same shape as the review graph's
|
|
1191
|
-
// resolvePersistWorkerPath (#950 review F1).
|
|
1192
|
-
const candidates = [
|
|
1193
|
-
new URL("./project-snapshot-persist-worker.js", import.meta.url),
|
|
1194
|
-
new URL("./clients/project-snapshot-persist-worker.js", import.meta.url),
|
|
1195
|
-
];
|
|
1196
|
-
for (const url of candidates) {
|
|
1197
|
-
try {
|
|
1198
|
-
const resolved = fileURLToPath(url);
|
|
1199
|
-
if (fs.existsSync(resolved))
|
|
1200
|
-
return resolved;
|
|
1201
|
-
}
|
|
1202
|
-
catch {
|
|
1203
|
-
/* try next layout */
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
return undefined;
|
|
1207
|
-
}
|
|
1208
|
-
function getSnapshotPersistWorker() {
|
|
1209
|
-
if (_snapshotWorkerDisabled)
|
|
1210
|
-
return undefined;
|
|
1211
|
-
if (_snapshotPersistWorker)
|
|
1212
|
-
return _snapshotPersistWorker;
|
|
1213
|
-
try {
|
|
1214
|
-
const workerPath = resolveSnapshotPersistWorkerPath();
|
|
1215
|
-
if (workerPath === undefined) {
|
|
1216
|
-
handleSnapshotWorkerDeath("persist worker script not found in any layout");
|
|
1217
|
-
return undefined;
|
|
1218
|
-
}
|
|
1219
|
-
const worker = new Worker(workerPath);
|
|
1220
|
-
// The ForTests promotion seam wraps ONLY when set — the production path
|
|
1221
|
-
// binds the sync handler directly, so scheduling is byte-identical when
|
|
1222
|
-
// no test seam is installed (the async-handler variant of this shifted
|
|
1223
|
-
// promotion timing under full-suite load and flaked the round-trip test).
|
|
1224
|
-
worker.on("message", (result) => {
|
|
1225
|
-
if (_snapshotPromotionSeamForTests) {
|
|
1226
|
-
void _snapshotPromotionSeamForTests().then(() => handleSnapshotWorkerResult(result));
|
|
1227
|
-
return;
|
|
1228
|
-
}
|
|
1229
|
-
handleSnapshotWorkerResult(result);
|
|
1230
|
-
});
|
|
1231
|
-
worker.on("error", (err) => handleSnapshotWorkerDeath(err.message));
|
|
1232
|
-
worker.on("exit", (code) => {
|
|
1233
|
-
if (_snapshotPersistWorker === worker)
|
|
1234
|
-
_snapshotPersistWorker = undefined;
|
|
1235
|
-
// Any body still queued when the worker exits was abandoned mid-flight
|
|
1236
|
-
// (a crash, a `terminate()`, or host recycling) — it will never be
|
|
1237
|
-
// promoted by this worker, so fall it back to the sync writer rather
|
|
1238
|
-
// than let it vanish (honesty, #533). `terminate()` can report a 0
|
|
1239
|
-
// exit code on some platforms, so this cannot key off `code !== 0`.
|
|
1240
|
-
const stranded = [..._snapshotWorkerRequests.values()];
|
|
1241
|
-
if (stranded.length > 0) {
|
|
1242
|
-
_snapshotWorkerRequests.clear();
|
|
1243
|
-
for (const pending of stranded) {
|
|
1244
|
-
dispatchMainThreadWriteThroughSeam(pending, `persist worker exited with code ${code}`);
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
// Only an ABNORMAL exit disables respawning; a clean idle exit (nothing
|
|
1248
|
-
// stranded) just drops the reference so the next persist respawns.
|
|
1249
|
-
if (code !== 0)
|
|
1250
|
-
_snapshotWorkerDisabled = true;
|
|
1251
|
-
});
|
|
1252
|
-
// #1148: adding a message listener refs the Worker's public MessagePort.
|
|
1253
|
-
// Unref only after every listener is installed so it stays background-only.
|
|
1254
|
-
worker.unref();
|
|
1255
|
-
_snapshotPersistWorker = worker;
|
|
1256
|
-
return worker;
|
|
1257
|
-
}
|
|
1258
|
-
catch (err) {
|
|
1259
|
-
handleSnapshotWorkerDeath(err instanceof Error ? err.message : String(err));
|
|
1260
|
-
return undefined;
|
|
1261
|
-
}
|
|
1262
|
-
}
|
|
1263
|
-
// #950 review F3: a process that dies between a worker's staged write and its
|
|
1264
|
-
// promotion leaves project-snapshot.json.gz.stage-<pid>-<gen> (and the worker's
|
|
1265
|
-
// .tmp-<pid>) behind forever. Sweep leftovers from PRIOR processes once per
|
|
1266
|
-
// cache dir; our own live stage files carry this pid and are skipped.
|
|
1267
|
-
const _sweptSnapshotStageDirs = new Set();
|
|
1268
|
-
function sweepStaleSnapshotStageFiles(cacheDir) {
|
|
1269
|
-
if (_sweptSnapshotStageDirs.has(cacheDir))
|
|
1270
|
-
return;
|
|
1271
|
-
_sweptSnapshotStageDirs.add(cacheDir);
|
|
1272
|
-
fs.readdir(cacheDir, (err, entries) => {
|
|
1273
|
-
if (err)
|
|
1274
|
-
return;
|
|
1275
|
-
const ownMarker = `.stage-${process.pid}-`;
|
|
1276
|
-
for (const entry of entries) {
|
|
1277
|
-
if (!entry.startsWith("project-snapshot.json.gz.stage-"))
|
|
1278
|
-
continue;
|
|
1279
|
-
if (entry.includes(ownMarker))
|
|
1280
|
-
continue;
|
|
1281
|
-
fs.rm(path.join(cacheDir, entry), { force: true }, () => { });
|
|
1282
|
-
}
|
|
1283
|
-
});
|
|
1284
|
-
}
|
|
1285
|
-
// Flush any in-flight worker writes synchronously at process teardown so a body
|
|
1286
|
-
// whose worker hasn't promoted yet isn't lost. Sync writes only (no child
|
|
1287
|
-
// spawn — the teardown libuv hazard); best-effort.
|
|
1288
|
-
let _snapshotExitHookInstalled = false;
|
|
1289
|
-
function ensureSnapshotPersistExitHook() {
|
|
1290
|
-
if (_snapshotExitHookInstalled)
|
|
1291
|
-
return;
|
|
1292
|
-
_snapshotExitHookInstalled = true;
|
|
1293
|
-
process.once("exit", () => {
|
|
1294
|
-
_snapshotExiting = true;
|
|
1295
|
-
const latestByKey = new Map();
|
|
1296
|
-
for (const pending of _snapshotWorkerRequests.values()) {
|
|
1297
|
-
latestByKey.set(pending.key, pending);
|
|
1298
|
-
}
|
|
1299
|
-
for (const pending of _queuedSnapshotPersists.values()) {
|
|
1300
|
-
const prior = latestByKey.get(pending.key);
|
|
1301
|
-
// Queued work is later admission order. Equal-seq requests deliberately
|
|
1302
|
-
// share a gate generation, so a tie must still select the queued payload.
|
|
1303
|
-
if (!prior || prior.generation <= pending.generation) {
|
|
1304
|
-
latestByKey.set(pending.key, pending);
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
_snapshotWorkerRequests.clear();
|
|
1308
|
-
_queuedSnapshotPersists.clear();
|
|
1309
|
-
_activeSnapshotPersists.clear();
|
|
1310
|
-
for (const pending of latestByKey.values()) {
|
|
1311
|
-
// Only the newest generation per key still matters; older ones are
|
|
1312
|
-
// superseded and their stage files are swept on next launch.
|
|
1313
|
-
if (_snapshotGenerationStates.get(pending.key)?.generation !==
|
|
1314
|
-
pending.generation)
|
|
1315
|
-
continue;
|
|
1316
|
-
writeSnapshotBodyOnMainThread(pending, "exit_hook");
|
|
1317
|
-
}
|
|
1318
|
-
void _snapshotPersistWorker?.terminate();
|
|
1319
|
-
});
|
|
1320
|
-
}
|
|
1321
|
-
export function saveProjectSnapshot(cwd, snapshot) {
|
|
1322
|
-
const gzPath = getProjectSnapshotPath(cwd);
|
|
1323
|
-
const legacyPath = getProjectSnapshotLegacyPath(cwd);
|
|
1324
|
-
const metaPath = getProjectSnapshotMetaPath(cwd);
|
|
1325
|
-
const cacheDir = path.dirname(gzPath);
|
|
1326
|
-
const key = normalizeMapKey(cwd);
|
|
1327
|
-
fs.mkdirSync(cacheDir, { recursive: true });
|
|
1328
|
-
const baselines = snapshotPersistBaselinesFor(cwd, key);
|
|
1329
|
-
const priorPersist = baselines.durable;
|
|
1330
|
-
logSnapshotPersistDecision({
|
|
1331
|
-
cwd,
|
|
1332
|
-
seq: snapshot.seq,
|
|
1333
|
-
decision: "requested",
|
|
1334
|
-
});
|
|
1335
|
-
if (_failedSnapshotPersists.delete(key)) {
|
|
1336
|
-
logSnapshotPersistDecision({ cwd, seq: snapshot.seq, decision: "retry" });
|
|
1337
|
-
}
|
|
1338
|
-
// #958: for a new seq, meta is written FIRST and body SECOND. A crash or
|
|
1339
|
-
// failure between the two writes can now only produce
|
|
1340
|
-
// "meta already claims the new seq, body hasn't caught up yet" (the meta
|
|
1341
|
-
// races ahead). The meta-first gate (isProjectSnapshotMetaStale) reads
|
|
1342
|
-
// that as *fresh* and falls through to parsing the body, whose own
|
|
1343
|
-
// embedded `seq` is still the old one, so `isProjectSnapshotFresh`
|
|
1344
|
-
// correctly rejects it as stale on the body's own merits — one wasted
|
|
1345
|
-
// parse, self-healing, no data lost. The OLD body-then-meta order could
|
|
1346
|
-
// instead leave an old-seq meta sitting over a freshly written body,
|
|
1347
|
-
// which the meta-first gate discards WITHOUT ever reading it — throwing
|
|
1348
|
-
// away a genuinely fresh snapshot. That direction is not recoverable
|
|
1349
|
-
// until the next save, so it's the one this reorder eliminates.
|
|
1350
|
-
//
|
|
1351
|
-
// The new-seq meta write stays synchronous and throwing. Same-seq work does
|
|
1352
|
-
// not need to advance the freshness gate, so it leaves the current sidecar
|
|
1353
|
-
// untouched until the worker has a semantic verdict. This prevents a stale
|
|
1354
|
-
// local request from replacing a
|
|
1355
|
-
// newer sibling process's same-seq fingerprint before it can coalesce.
|
|
1356
|
-
if (priorPersist?.seq !== snapshot.seq) {
|
|
1357
|
-
writeProjectSnapshotMeta(metaPath, snapshot);
|
|
1358
|
-
}
|
|
1359
|
-
// Record the authoritative in-process write BEFORE handing the body off, so
|
|
1360
|
-
// a merge-read between now and the worker's promotion sees our own object
|
|
1361
|
-
// (the on-disk body still holds the previous generation until promotion). The
|
|
1362
|
-
// pre-write mtime is our baseline: while disk stays at it (or below), our
|
|
1363
|
-
// object wins; a promotion or an external write moves past it. Baseline off
|
|
1364
|
-
// the CURRENTLY-RESOLVED body — which is the legacy uncompressed
|
|
1365
|
-
// `project-snapshot.json` in the one-release upgrade window before the first
|
|
1366
|
-
// gz promotion — not gz-only: statting only gzPath there would leave the
|
|
1367
|
-
// baseline at -Infinity, so the load gate (which resolves the legacy body's
|
|
1368
|
-
// real, positive mtime) would reject our own fresh write and serve the stale
|
|
1369
|
-
// legacy body to a merge-consumer, silently dropping this snapshot's fields.
|
|
1370
|
-
const priorBody = resolveSnapshotBodyPath(cwd);
|
|
1371
|
-
const knownMtime = priorBody ? priorBody.mtimeMs : Number.NEGATIVE_INFINITY;
|
|
1372
|
-
const knownSize = priorBody ? priorBody.size : Number.NEGATIVE_INFINITY;
|
|
1373
|
-
const authoritativeEntry = {
|
|
1374
|
-
snapshot,
|
|
1375
|
-
knownMtime,
|
|
1376
|
-
knownSize,
|
|
1377
|
-
lastUsedAt: Date.now(),
|
|
1378
|
-
};
|
|
1379
|
-
const previousAuthoritativeEntry = authoritativeSnapshots.get(key);
|
|
1380
|
-
if (previousAuthoritativeEntry)
|
|
1381
|
-
clearAuthoritativeSnapshotTimer(previousAuthoritativeEntry);
|
|
1382
|
-
authoritativeSnapshots.set(key, authoritativeEntry);
|
|
1383
|
-
scheduleAuthoritativeSnapshotEviction(key, authoritativeEntry);
|
|
1384
|
-
enforceAuthoritativeSnapshotCap();
|
|
1385
|
-
// A stale disk-parse-cache entry for this path must not out-vote the fresh
|
|
1386
|
-
// authoritative write once the latter is dropped (oversized bodies).
|
|
1387
|
-
snapshotParseCache.delete(gzPath);
|
|
1388
|
-
const generationState = _snapshotGenerationStates.get(key);
|
|
1389
|
-
const generation = generationState?.seq === snapshot.seq
|
|
1390
|
-
? generationState.generation
|
|
1391
|
-
: (generationState?.generation ?? 0) + 1;
|
|
1392
|
-
_snapshotGenerationStates.set(key, { generation, seq: snapshot.seq });
|
|
1393
|
-
const stagePath = `${gzPath}.stage-${process.pid}-${generation}`;
|
|
1394
|
-
const pending = {
|
|
1395
|
-
key,
|
|
1396
|
-
cwd,
|
|
1397
|
-
gzPath,
|
|
1398
|
-
legacyPath,
|
|
1399
|
-
stagePath,
|
|
1400
|
-
snapshot,
|
|
1401
|
-
generation,
|
|
1402
|
-
durablePersist: priorPersist,
|
|
1403
|
-
dedupeFingerprints: baselines.fingerprints,
|
|
1404
|
-
};
|
|
1405
|
-
sweepStaleSnapshotStageFiles(cacheDir);
|
|
1406
|
-
ensureSnapshotPersistExitHook();
|
|
1407
|
-
if (_activeSnapshotPersists.has(key)) {
|
|
1408
|
-
_queuedSnapshotPersists.set(key, pending);
|
|
1409
|
-
logSnapshotPersistDecision({
|
|
1410
|
-
cwd,
|
|
1411
|
-
seq: snapshot.seq,
|
|
1412
|
-
decision: "coalesced",
|
|
1413
|
-
});
|
|
1414
|
-
return;
|
|
1415
|
-
}
|
|
1416
|
-
dispatchSnapshotPersist(pending);
|
|
1417
|
-
}
|
|
1418
|
-
// --- Test hooks for the worker persist path ---------------------------------
|
|
1419
|
-
/** Test-only: wait until worker requests have either landed or degraded. */
|
|
1420
|
-
export async function waitForProjectSnapshotPersistsForTests() {
|
|
1421
|
-
for (let attempts = 0; attempts < 200 &&
|
|
1422
|
-
(_snapshotWorkerRequests.size > 0 ||
|
|
1423
|
-
_activeSnapshotPersists.size > 0 ||
|
|
1424
|
-
_queuedSnapshotPersists.size > 0); attempts++) {
|
|
1425
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
/** Test-only: force any in-flight worker body write to the sync main-thread path. */
|
|
1429
|
-
export function flushProjectSnapshotPersistsForTests() {
|
|
1430
|
-
const requests = [..._snapshotWorkerRequests.values()];
|
|
1431
|
-
_snapshotWorkerRequests.clear();
|
|
1432
|
-
for (const pending of requests) {
|
|
1433
|
-
dispatchMainThreadWriteThroughSeam(pending, undefined);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
/** Test-only: exercise the degraded worker-death path. */
|
|
1437
|
-
export async function terminateProjectSnapshotPersistWorkerForTests() {
|
|
1438
|
-
const worker = _snapshotPersistWorker;
|
|
1439
|
-
if (worker)
|
|
1440
|
-
await worker.terminate();
|
|
1441
|
-
}
|
|
1442
|
-
/** Test-only: restore worker creation + clear generation state after a deliberate death. */
|
|
1443
|
-
export function resetProjectSnapshotPersistWorkerForTests() {
|
|
1444
|
-
_snapshotWorkerDisabled = false;
|
|
1445
|
-
_snapshotGenerationGateEnabledForTests = true;
|
|
1446
|
-
_snapshotPromotionSeamForTests = undefined;
|
|
1447
|
-
_snapshotPersistWorker = undefined;
|
|
1448
|
-
_snapshotWorkerRequests.clear();
|
|
1449
|
-
_snapshotGenerationStates.clear();
|
|
1450
|
-
_successfulSnapshotPersists.clear();
|
|
1451
|
-
_failedSnapshotPersists.clear();
|
|
1452
|
-
_activeSnapshotPersists.clear();
|
|
1453
|
-
_queuedSnapshotPersists.clear();
|
|
1454
|
-
_snapshotExiting = false;
|
|
1455
|
-
_snapshotWorkerBodyWritesForTests = 0;
|
|
1456
|
-
_lastSnapshotPersistErrorForTests = undefined;
|
|
1457
|
-
}
|
|
1458
|
-
/** Test-only: expose bounded queue state without retaining snapshot bodies. */
|
|
1459
|
-
export function getProjectSnapshotPersistStateForTests(cwd) {
|
|
1460
|
-
const key = normalizeMapKey(cwd);
|
|
1461
|
-
return {
|
|
1462
|
-
active: _activeSnapshotPersists.has(key),
|
|
1463
|
-
queued: _queuedSnapshotPersists.has(key),
|
|
1464
|
-
successfulFingerprint: _successfulSnapshotPersists.get(key)?.fingerprint,
|
|
1465
|
-
workerBodyWrites: _snapshotWorkerBodyWritesForTests,
|
|
1466
|
-
};
|
|
1467
|
-
}
|
|
1468
|
-
/** Test-only mutation switch for proving the supersession invariant. */
|
|
1469
|
-
export function setProjectSnapshotGenerationGateForTests(enabled) {
|
|
1470
|
-
_snapshotGenerationGateEnabledForTests = enabled;
|
|
1471
|
-
}
|
|
1472
|
-
/** Test-only seam immediately before generation-gated promotion. */
|
|
1473
|
-
export function setProjectSnapshotPromotionSeamForTests(seam) {
|
|
1474
|
-
_snapshotPromotionSeamForTests = seam;
|
|
1475
|
-
}
|
|
1476
|
-
export function getProjectSnapshotPersistErrorForTests() {
|
|
1477
|
-
return _lastSnapshotPersistErrorForTests;
|
|
1478
|
-
}
|
|
1479
|
-
// ── #2442 test-only seams: exercise the three BoundedFifoMap-backed caches ──
|
|
1480
|
-
// through the exact same write path production uses (cacheParsedSnapshot /
|
|
1481
|
-
// rememberSuccessfulSnapshotPersist's delete+set refresh, and a direct write
|
|
1482
|
-
// mirroring recordSnapshotPersistFailure's), without the real gzip/worker
|
|
1483
|
-
// persistence pipeline or a full ProjectSnapshot fixture.
|
|
1484
|
-
/** Test-only: seed the parse cache exactly as `cacheParsedSnapshot` would. */
|
|
1485
|
-
export function _seedSnapshotParseCacheForTests(key, entry) {
|
|
1486
|
-
cacheParsedSnapshot(key, entry);
|
|
1487
|
-
}
|
|
1488
|
-
export function _snapshotParseCacheKeysForTests() {
|
|
1489
|
-
return [...snapshotParseCache.keys()];
|
|
1490
|
-
}
|
|
1491
|
-
/**
|
|
1492
|
-
* #2442 test-only: the exact `snapshotParseCache.get(key)` the production
|
|
1493
|
-
* read at {@link loadProjectSnapshot}'s parse-cache hit performs. A `.has()`
|
|
1494
|
-
* would not do — `.has()` reorders nothing on either bounded class, so a
|
|
1495
|
-
* has-based test cannot tell FIFO from LRU, which is the axis these bounded
|
|
1496
|
-
* tests exist to pin (#2442 review F4).
|
|
1497
|
-
*/
|
|
1498
|
-
export function _snapshotParseCacheGetForTests(key) {
|
|
1499
|
-
return snapshotParseCache.get(key) !== undefined;
|
|
1500
|
-
}
|
|
1501
|
-
/** Test-only: seed the successful-persist cache exactly as production would. */
|
|
1502
|
-
export function _seedSuccessfulSnapshotPersistForTests(key, record) {
|
|
1503
|
-
rememberSuccessfulSnapshotPersist(key, record);
|
|
1504
|
-
}
|
|
1505
|
-
export function _successfulSnapshotPersistKeysForTests() {
|
|
1506
|
-
return [..._successfulSnapshotPersists.keys()];
|
|
1507
|
-
}
|
|
1508
|
-
/** #2442 test-only: the same `_successfulSnapshotPersists.get(key)` the
|
|
1509
|
-
* persist-dedupe read and `getProjectSnapshotPersistStateForTests` perform.
|
|
1510
|
-
* See {@link _snapshotParseCacheGetForTests} for why `.has()` will not do. */
|
|
1511
|
-
export function _successfulSnapshotPersistGetForTests(key) {
|
|
1512
|
-
return _successfulSnapshotPersists.get(key) !== undefined;
|
|
1513
|
-
}
|
|
1514
|
-
/** Test-only: seed the failed-persist cache with the same write
|
|
1515
|
-
* `recordSnapshotPersistFailure` performs, without its side effects
|
|
1516
|
-
* (deleteAuthoritativeSnapshot / logLatency) or a full PendingSnapshotBody. */
|
|
1517
|
-
export function _seedFailedSnapshotPersistForTests(key, record) {
|
|
1518
|
-
_failedSnapshotPersists.set(key, record);
|
|
1519
|
-
}
|
|
1520
|
-
export function _failedSnapshotPersistKeysForTests() {
|
|
1521
|
-
return [..._failedSnapshotPersists.keys()];
|
|
1522
|
-
}
|
|
1523
|
-
export function buildProjectSnapshotFromRuntime(args) {
|
|
1524
|
-
return {
|
|
1525
|
-
version: PROJECT_SNAPSHOT_VERSION,
|
|
1526
|
-
projectRoot: normalizeMapKey(path.resolve(args.cwd)),
|
|
1527
|
-
generatedAt: new Date().toISOString(),
|
|
1528
|
-
seq: args.runtime.projectSeq,
|
|
1529
|
-
files: {},
|
|
1530
|
-
symbols: {},
|
|
1531
|
-
reverseDeps: {},
|
|
1532
|
-
cachedExports: [...args.runtime.cachedExports.entries()].sort((a, b) => a[0].localeCompare(b[0])),
|
|
1533
|
-
// #1019: capture the runtime's live sequence index AT this seq. The runtime
|
|
1534
|
-
// is seeded from the change log at session start and bumped in lockstep with
|
|
1535
|
-
// every append, so `getFileSeqEntries()` IS the fold of the log up to
|
|
1536
|
-
// `projectSeq` — and its keys are already `normalizeMapKey(path.resolve())`,
|
|
1537
|
-
// the exact form the change-log replay produces.
|
|
1538
|
-
sequenceIndex: {
|
|
1539
|
-
projectSeq: args.runtime.projectSeq,
|
|
1540
|
-
fileSeqByPath: args.runtime.getFileSeqEntries(),
|
|
1541
|
-
},
|
|
1542
|
-
wordIndex: args.runtime.wordIndex
|
|
1543
|
-
? serializeWordIndex(args.runtime.wordIndex)
|
|
1544
|
-
: undefined,
|
|
1545
|
-
projectRulesScan: args.runtime.projectRulesScan,
|
|
1546
|
-
startupScan: args.startupScan,
|
|
1547
|
-
languageProfile: args.languageProfile,
|
|
1548
|
-
conventions: args.conventions,
|
|
1549
|
-
};
|
|
1550
|
-
}
|
|
1551
|
-
export function hydrateRuntimeFromProjectSnapshot(runtime, snapshot) {
|
|
1552
|
-
runtime.cachedExports.clear();
|
|
1553
|
-
for (const [name, filePath] of snapshot.cachedExports) {
|
|
1554
|
-
runtime.cachedExports.set(name, filePath);
|
|
1555
|
-
}
|
|
1556
|
-
if (snapshot.projectRulesScan) {
|
|
1557
|
-
runtime.projectRulesScan = snapshot.projectRulesScan;
|
|
1558
|
-
}
|
|
1559
|
-
runtime.wordIndex = deserializeWordIndex(snapshot.wordIndex);
|
|
1560
|
-
}
|
|
1561
|
-
/**
|
|
1562
|
-
* #1785 F2/F3: additive counterpart to `hydrateRuntimeFromProjectSnapshot`
|
|
1563
|
-
* for a LATE/retroactive hydration attempt — one that runs after other work
|
|
1564
|
-
* may already have populated the runtime for real (e.g. quick mode's
|
|
1565
|
-
* background warmup building a genuine `wordIndex`, docCount > 0). The
|
|
1566
|
-
* unconditional version above is only safe for the FIRST hydration attempt,
|
|
1567
|
-
* made before anything else has run: clearing `cachedExports` is correct
|
|
1568
|
-
* there because there is nothing yet to destroy.
|
|
1569
|
-
*
|
|
1570
|
-
* A late call cannot make that assumption, and `cachedExports` and
|
|
1571
|
-
* `projectRulesScan` are populated by INDEPENDENT tasks from whatever else is
|
|
1572
|
-
* running (nothing else in quick mode touches either), so this guards each
|
|
1573
|
-
* field on its OWN "nothing computed since" check rather than one
|
|
1574
|
-
* all-or-nothing bail-out — the field a concurrent task actually populated is
|
|
1575
|
-
* protected without needlessly withholding the other, still-idle field the
|
|
1576
|
-
* snapshot could still supply. Returns whether it hydrated ANY field, so the
|
|
1577
|
-
* caller can log honestly instead of claiming success when nothing changed.
|
|
1578
|
-
*
|
|
1579
|
-
* #1785 F5: takes ONLY the narrow `{ cachedExports, projectRulesScan }`
|
|
1580
|
-
* shape (see `ProjectSnapshotExportsAndRules`) — deliberately no `wordIndex`
|
|
1581
|
-
* parameter at all, not even an optional one. The retroactive path's
|
|
1582
|
-
* captured/reloaded snapshot always comes from
|
|
1583
|
-
* `loadProjectSnapshotExportsAndRules`, which never parses `wordIndex` in
|
|
1584
|
-
* the first place (avoiding its dominant share of a full body parse's cost —
|
|
1585
|
-
* see that function's doc comment for the measured numbers). This is also
|
|
1586
|
-
* the right behavior, not just a performance shortcut: quick mode's warmup
|
|
1587
|
-
* is the only thing that ever builds a `wordIndex` in this window, and F2's
|
|
1588
|
-
* hazard was this exact late-hydration path nulling that live index from a
|
|
1589
|
-
* disk copy that predated it — removing the parameter removes the hazard by
|
|
1590
|
-
* construction instead of merely guarding against it at runtime.
|
|
1591
|
-
*/
|
|
1592
|
-
export function hydrateRuntimeFromProjectSnapshotIfIdle(runtime, snapshot) {
|
|
1593
|
-
let hydratedAnything = false;
|
|
1594
|
-
if (runtime.cachedExports.size === 0 && snapshot.cachedExports.length > 0) {
|
|
1595
|
-
runtime.cachedExports.clear();
|
|
1596
|
-
for (const [name, filePath] of snapshot.cachedExports) {
|
|
1597
|
-
runtime.cachedExports.set(name, filePath);
|
|
1598
|
-
}
|
|
1599
|
-
hydratedAnything = true;
|
|
1600
|
-
}
|
|
1601
|
-
if (!runtime.projectRulesScan.hasCustomRules &&
|
|
1602
|
-
runtime.projectRulesScan.rules.length === 0 &&
|
|
1603
|
-
snapshot.projectRulesScan) {
|
|
1604
|
-
runtime.projectRulesScan = snapshot.projectRulesScan;
|
|
1605
|
-
hydratedAnything = true;
|
|
1606
|
-
}
|
|
1607
|
-
return hydratedAnything;
|
|
1608
|
-
}
|
|
1609
|
-
export function saveRuntimeProjectSnapshot(args) {
|
|
1610
|
-
try {
|
|
1611
|
-
if (typeof args.runtime.projectSeq !== "number")
|
|
1612
|
-
return;
|
|
1613
|
-
const existing = loadProjectSnapshot(args.cwd);
|
|
1614
|
-
let conventions = args.conventions ?? existing?.conventions;
|
|
1615
|
-
if (!conventions) {
|
|
1616
|
-
try {
|
|
1617
|
-
conventions = detectProjectConventions(args.cwd);
|
|
1618
|
-
}
|
|
1619
|
-
catch (err) {
|
|
1620
|
-
args.dbg?.(`project_snapshot: convention detection failed: ${err}`);
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
const snapshot = buildProjectSnapshotFromRuntime({
|
|
1624
|
-
...args,
|
|
1625
|
-
startupScan: args.startupScan ?? existing?.startupScan,
|
|
1626
|
-
languageProfile: args.languageProfile ?? existing?.languageProfile,
|
|
1627
|
-
conventions,
|
|
1628
|
-
});
|
|
1629
|
-
if (existing) {
|
|
1630
|
-
snapshot.files = existing.files ?? {};
|
|
1631
|
-
snapshot.symbols = existing.symbols ?? {};
|
|
1632
|
-
snapshot.reverseDeps = existing.reverseDeps ?? {};
|
|
1633
|
-
// The word index is built by its own session task, which may not have
|
|
1634
|
-
// finished when another task triggers a save — keep the prior index
|
|
1635
|
-
// rather than clobbering it with undefined. #348: only carry it forward
|
|
1636
|
-
// when `existing` was built AT THIS SAME seq — otherwise a stale
|
|
1637
|
-
// snapshot's leftover index (already correctly rejected as stale by
|
|
1638
|
-
// isProjectSnapshotFresh on load, seq mismatch) would get silently
|
|
1639
|
-
// re-stamped with the CURRENT seq by this save, "laundering" a stale
|
|
1640
|
-
// index into looking fresh before the word-index task even runs.
|
|
1641
|
-
if (!snapshot.wordIndex &&
|
|
1642
|
-
existing.wordIndex &&
|
|
1643
|
-
existing.seq === snapshot.seq) {
|
|
1644
|
-
snapshot.wordIndex = existing.wordIndex;
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
saveProjectSnapshot(args.cwd, snapshot);
|
|
1648
|
-
args.dbg?.(`project_snapshot: saved seq=${snapshot.seq} exports=${snapshot.cachedExports.length}`);
|
|
1649
|
-
}
|
|
1650
|
-
catch (err) {
|
|
1651
|
-
args.dbg?.(`project_snapshot: save failed: ${err}`);
|
|
1652
|
-
}
|
|
1653
|
-
}
|