@herbertgao/pi-extensions 2026.10.1 → 2026.10.3
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 +14 -17
- 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/{remote-pi → @herbertgao/pi-agency}/LICENSE +1 -1
- 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/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/README.md +1 -1
- package/node_modules/@herbertgao/pi-subagents/package.json +4 -5
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/mention-clone.ts +48 -72
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +1 -1
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +15 -6
- package/node_modules/@herbertgao/pi-subagents/src/workflow/collisions.ts +4 -1
- package/node_modules/@herbertgao/pi-subagents/src/workflow/json-schema.ts +9 -8
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts +6 -1
- package/node_modules/@narumitw/pi-caffeinate/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@narumitw/pi-caffeinate/src/dbus-inhibit.ts +13 -1
- 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 +1159 -0
- package/node_modules/pi-lens/README.md +116 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- 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-2FQ62SPM.js +83 -0
- package/node_modules/pi-lens/dist/chunk-2UMYMRI3.js +118 -0
- package/node_modules/pi-lens/dist/chunk-34OCDWOS.js +48 -0
- package/node_modules/pi-lens/dist/chunk-3FERWMCH.js +144 -0
- package/node_modules/pi-lens/dist/chunk-3JL6IFD7.js +97 -0
- package/node_modules/pi-lens/dist/chunk-3JSEWUWI.js +327 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-3MRXIJ6I.js +79 -0
- package/node_modules/pi-lens/dist/chunk-3P4PF2BX.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-3PNFWVHT.js +201 -0
- package/node_modules/pi-lens/dist/chunk-3TTPKMEF.js +162 -0
- package/node_modules/pi-lens/dist/chunk-5JHXUETW.js +326 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-64LU2OVH.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6B5I6ICN.js +44 -0
- package/node_modules/pi-lens/dist/chunk-6P54TMWL.js +274 -0
- package/node_modules/pi-lens/dist/chunk-6UYA642J.js +145 -0
- package/node_modules/pi-lens/dist/chunk-6XX5HCLK.js +398 -0
- package/node_modules/pi-lens/dist/chunk-7AXO3ZDF.js +30 -0
- package/node_modules/pi-lens/dist/chunk-7BX5DWSA.js +1907 -0
- package/node_modules/pi-lens/dist/chunk-7ELY2CSM.js +902 -0
- package/node_modules/pi-lens/dist/chunk-7M4T5N3V.js +38 -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-AFZYMAIV.js +756 -0
- package/node_modules/pi-lens/dist/chunk-ALQRYH6O.js +112 -0
- package/node_modules/pi-lens/dist/chunk-AXKPQ6ZI.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-BHWHXMQ2.js +62 -0
- package/node_modules/pi-lens/dist/chunk-BJF25SUG.js +22 -0
- package/node_modules/pi-lens/dist/chunk-BNKLYOOO.js +18 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-CBD2B5CP.js +599 -0
- package/node_modules/pi-lens/dist/chunk-CFYISHUW.js +42 -0
- package/node_modules/pi-lens/dist/chunk-CMU4RMJB.js +30 -0
- package/node_modules/pi-lens/dist/chunk-CULQYFLU.js +14 -0
- package/node_modules/pi-lens/dist/chunk-D65GOCJB.js +289 -0
- package/node_modules/pi-lens/dist/chunk-D6YF5HKX.js +59 -0
- package/node_modules/pi-lens/dist/chunk-DMS52BEP.js +342 -0
- package/node_modules/pi-lens/dist/chunk-ELVWJYRV.js +211 -0
- package/node_modules/pi-lens/dist/chunk-EUT3B22Y.js +27 -0
- package/node_modules/pi-lens/dist/chunk-F5VLZHUW.js +146 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FGALODG3.js +55 -0
- package/node_modules/pi-lens/dist/chunk-G3RLJYA2.js +2358 -0
- package/node_modules/pi-lens/dist/chunk-GAYGGICG.js +32 -0
- package/node_modules/pi-lens/dist/chunk-H2W2AXD3.js +419 -0
- package/node_modules/pi-lens/dist/chunk-H5EV3VMF.js +114 -0
- package/node_modules/pi-lens/dist/chunk-HREZQCT3.js +106 -0
- package/node_modules/pi-lens/dist/chunk-HSIRMK2D.js +517 -0
- package/node_modules/pi-lens/dist/chunk-IMSQR4WO.js +184 -0
- package/node_modules/pi-lens/dist/chunk-IQZXOZ7S.js +4816 -0
- package/node_modules/pi-lens/dist/chunk-IR6XFA5P.js +37 -0
- package/node_modules/pi-lens/dist/chunk-IUKPRZER.js +78 -0
- package/node_modules/pi-lens/dist/chunk-IUR76K6A.js +262 -0
- package/node_modules/pi-lens/dist/chunk-J3FZR3YY.js +98 -0
- package/node_modules/pi-lens/dist/chunk-J6DFZDFA.js +562 -0
- package/node_modules/pi-lens/dist/chunk-J7WJZ6W7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-JONGANYH.js +202 -0
- package/node_modules/pi-lens/dist/chunk-KJCWXUUS.js +425 -0
- package/node_modules/pi-lens/dist/chunk-KJN4IOEJ.js +17 -0
- package/node_modules/pi-lens/dist/chunk-KMGPUKFD.js +25 -0
- package/node_modules/pi-lens/dist/chunk-LD7WFTRX.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNOMA3MP.js +169 -0
- package/node_modules/pi-lens/dist/chunk-MGIWSROG.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-MQA2YBAH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-MTDRLSI7.js +21720 -0
- package/node_modules/pi-lens/dist/chunk-N2IRDBZI.js +2355 -0
- package/node_modules/pi-lens/dist/chunk-N77WFUL7.js +50 -0
- package/node_modules/pi-lens/dist/chunk-N7YNJE5R.js +466 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NFXP47RV.js +999 -0
- package/node_modules/pi-lens/dist/chunk-NJF2X5XF.js +45 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-O7HZX7CA.js +309 -0
- package/node_modules/pi-lens/dist/chunk-OC7LYAU5.js +708 -0
- package/node_modules/pi-lens/dist/chunk-OEX24ECI.js +348 -0
- package/node_modules/pi-lens/dist/chunk-OYOB3ZFG.js +78 -0
- package/node_modules/pi-lens/dist/chunk-OYTMZXRF.js +41 -0
- package/node_modules/pi-lens/dist/chunk-OZJINN4O.js +84 -0
- package/node_modules/pi-lens/dist/chunk-PCHDGUKT.js +23 -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-RJFED73B.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RTQNRM5X.js +27 -0
- package/node_modules/pi-lens/dist/chunk-S6AMWKPC.js +189 -0
- package/node_modules/pi-lens/dist/chunk-T4QZ6FEH.js +308 -0
- package/node_modules/pi-lens/dist/chunk-T54VJRAD.js +5456 -0
- package/node_modules/pi-lens/dist/chunk-TA5L5KWQ.js +167 -0
- package/node_modules/pi-lens/dist/chunk-TQFBZVCG.js +27 -0
- package/node_modules/pi-lens/dist/chunk-U52SAGRM.js +62 -0
- package/node_modules/pi-lens/dist/chunk-UEAHAMEI.js +5814 -0
- package/node_modules/pi-lens/dist/chunk-UGSMCQ4D.js +327 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-USHG4DLH.js +459 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-V5MEBST3.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-VGEVL5EQ.js +29 -0
- package/node_modules/pi-lens/dist/chunk-VPWKF3GN.js +87 -0
- package/node_modules/pi-lens/dist/chunk-VSMJK3ZJ.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-VYKKMYVT.js +560 -0
- package/node_modules/pi-lens/dist/chunk-VZHJS6DR.js +442 -0
- package/node_modules/pi-lens/dist/chunk-WATC3IQR.js +213 -0
- package/node_modules/pi-lens/dist/chunk-WPE5FPGR.js +54 -0
- package/node_modules/pi-lens/dist/chunk-WSEWKCXU.js +188 -0
- package/node_modules/pi-lens/dist/chunk-WW5BLB74.js +659 -0
- package/node_modules/pi-lens/dist/chunk-XGUL4SLQ.js +406 -0
- package/node_modules/pi-lens/dist/chunk-XHSCVSOL.js +170 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-YLN4K6WD.js +385 -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-Z6BB4JLQ.js +478 -0
- package/node_modules/pi-lens/dist/chunk-Z7QQA2KT.js +27 -0
- package/node_modules/pi-lens/dist/chunk-ZETYB3XG.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-ZMOMPX2Z.js +87 -0
- package/node_modules/pi-lens/dist/chunk-ZOEYVXSV.js +149 -0
- package/node_modules/pi-lens/dist/chunk-ZZUFZUQZ.js +12120 -0
- package/node_modules/pi-lens/dist/index.js +39712 -29876
- 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 +22688 -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 +5 -4
- package/node_modules/pi-lens/docs/configuration.md +28 -2
- package/node_modules/pi-lens/docs/custom-rules.md +25 -2
- package/node_modules/pi-lens/docs/environment-variables.md +86 -8
- package/node_modules/pi-lens/docs/features.md +45 -5
- package/node_modules/pi-lens/docs/globalconfig.md +2 -1
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +37 -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 +160 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +736 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +75 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +189 -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/schema/pi-lens-config-v1.json +530 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +56 -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/docs/word-index.md +6 -0
- 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/rule-tests/no-identity-operator-on-literals-test.yml +12 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/throw-new-error-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-identity-operator-on-literals.yml +20 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error-js.yml +2 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/throw-new-error.yml +2 -2
- 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/node_modules/pi-web-access/CHANGELOG.md +28 -0
- package/node_modules/pi-web-access/README.md +10 -6
- package/node_modules/pi-web-access/ceramic.ts +219 -0
- package/node_modules/pi-web-access/curator-page.ts +3 -1
- package/node_modules/pi-web-access/curator-server.ts +1 -0
- package/node_modules/pi-web-access/dist/index.js +846 -588
- package/node_modules/pi-web-access/dist/mcp-cli.js +725 -516
- package/node_modules/pi-web-access/gemini-search.ts +8 -4
- package/node_modules/pi-web-access/github-extract.ts +34 -2
- package/node_modules/pi-web-access/index.ts +72 -30
- package/node_modules/pi-web-access/openai-search.ts +3 -3
- package/node_modules/pi-web-access/package.json +2 -2
- package/node_modules/pi-web-access/page-query.ts +6 -9
- package/node_modules/pi-web-access/parallel-mcp.ts +2 -0
- package/node_modules/pi-web-access/query-rewrite.ts +2 -2
- package/node_modules/pi-web-access/storage.ts +49 -0
- package/node_modules/pi-web-access/summary-review.ts +7 -6
- package/node_modules/pi-web-access/utils.ts +21 -6
- package/node_modules/pi-web-access/web-tool-core.ts +1 -0
- package/package.json +19 -19
- 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
- package/node_modules/pi-web-access/opencode-session-headers.ts +0 -24
- package/node_modules/remote-pi/README.md +0 -675
- package/node_modules/remote-pi/dist/actions/handlers.d.ts +0 -122
- package/node_modules/remote-pi/dist/actions/handlers.js +0 -160
- package/node_modules/remote-pi/dist/actions/handlers.js.map +0 -1
- package/node_modules/remote-pi/dist/actions/registry.d.ts +0 -39
- package/node_modules/remote-pi/dist/actions/registry.js +0 -58
- package/node_modules/remote-pi/dist/actions/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/bin/supervisord.d.ts +0 -2
- package/node_modules/remote-pi/dist/bin/supervisord.js +0 -86
- package/node_modules/remote-pi/dist/bin/supervisord.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/builtin_mirror.d.ts +0 -58
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js +0 -71
- package/node_modules/remote-pi/dist/commands/builtin_mirror.js.map +0 -1
- package/node_modules/remote-pi/dist/commands/list_commands.d.ts +0 -60
- package/node_modules/remote-pi/dist/commands/list_commands.js +0 -73
- package/node_modules/remote-pi/dist/commands/list_commands.js.map +0 -1
- package/node_modules/remote-pi/dist/config.d.ts +0 -62
- package/node_modules/remote-pi/dist/config.js +0 -115
- package/node_modules/remote-pi/dist/config.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/client.d.ts +0 -20
- package/node_modules/remote-pi/dist/daemon/client.js +0 -131
- package/node_modules/remote-pi/dist/daemon/client.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/control_protocol.d.ts +0 -176
- package/node_modules/remote-pi/dist/daemon/control_protocol.js +0 -63
- package/node_modules/remote-pi/dist/daemon/control_protocol.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_log.d.ts +0 -45
- package/node_modules/remote-pi/dist/daemon/cron_log.js +0 -72
- package/node_modules/remote-pi/dist/daemon/cron_log.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/cron_registry.d.ts +0 -80
- package/node_modules/remote-pi/dist/daemon/cron_registry.js +0 -194
- package/node_modules/remote-pi/dist/daemon/cron_registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/id.d.ts +0 -24
- package/node_modules/remote-pi/dist/daemon/id.js +0 -36
- package/node_modules/remote-pi/dist/daemon/id.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/install.d.ts +0 -170
- package/node_modules/remote-pi/dist/daemon/install.js +0 -547
- package/node_modules/remote-pi/dist/daemon/install.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/registry.d.ts +0 -63
- package/node_modules/remote-pi/dist/daemon/registry.js +0 -153
- package/node_modules/remote-pi/dist/daemon/registry.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/rpc_child.d.ts +0 -194
- package/node_modules/remote-pi/dist/daemon/rpc_child.js +0 -389
- package/node_modules/remote-pi/dist/daemon/rpc_child.js.map +0 -1
- package/node_modules/remote-pi/dist/daemon/supervisor.d.ts +0 -93
- package/node_modules/remote-pi/dist/daemon/supervisor.js +0 -596
- package/node_modules/remote-pi/dist/daemon/supervisor.js.map +0 -1
- package/node_modules/remote-pi/dist/extension_ui_bridge.d.ts +0 -26
- package/node_modules/remote-pi/dist/extension_ui_bridge.js +0 -341
- package/node_modules/remote-pi/dist/extension_ui_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/index.d.ts +0 -213
- package/node_modules/remote-pi/dist/index.js +0 -4996
- package/node_modules/remote-pi/dist/index.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_result.d.ts +0 -10
- package/node_modules/remote-pi/dist/mcp/mesh_result.js +0 -42
- package/node_modules/remote-pi/dist/mcp/mesh_result.js.map +0 -1
- package/node_modules/remote-pi/dist/mcp/mesh_server.d.ts +0 -16
- package/node_modules/remote-pi/dist/mcp/mesh_server.js +0 -327
- package/node_modules/remote-pi/dist/mcp/mesh_server.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/canonical.d.ts +0 -30
- package/node_modules/remote-pi/dist/mesh/canonical.js +0 -61
- package/node_modules/remote-pi/dist/mesh/canonical.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/client.d.ts +0 -18
- package/node_modules/remote-pi/dist/mesh/client.js +0 -125
- package/node_modules/remote-pi/dist/mesh/client.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/encoding.d.ts +0 -38
- package/node_modules/remote-pi/dist/mesh/encoding.js +0 -216
- package/node_modules/remote-pi/dist/mesh/encoding.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/self_revoke.d.ts +0 -73
- package/node_modules/remote-pi/dist/mesh/self_revoke.js +0 -401
- package/node_modules/remote-pi/dist/mesh/self_revoke.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/siblings.d.ts +0 -49
- package/node_modules/remote-pi/dist/mesh/siblings.js +0 -159
- package/node_modules/remote-pi/dist/mesh/siblings.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/types.d.ts +0 -34
- package/node_modules/remote-pi/dist/mesh/types.js +0 -11
- package/node_modules/remote-pi/dist/mesh/types.js.map +0 -1
- package/node_modules/remote-pi/dist/mesh/verify.d.ts +0 -17
- package/node_modules/remote-pi/dist/mesh/verify.js +0 -86
- package/node_modules/remote-pi/dist/mesh/verify.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/crypto.d.ts +0 -8
- package/node_modules/remote-pi/dist/pairing/crypto.js +0 -22
- package/node_modules/remote-pi/dist/pairing/crypto.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/handshake.d.ts +0 -28
- package/node_modules/remote-pi/dist/pairing/handshake.js +0 -113
- package/node_modules/remote-pi/dist/pairing/handshake.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/noise-sha256.d.ts +0 -16
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js +0 -103
- package/node_modules/remote-pi/dist/pairing/noise-sha256.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/qr.d.ts +0 -59
- package/node_modules/remote-pi/dist/pairing/qr.js +0 -125
- package/node_modules/remote-pi/dist/pairing/qr.js.map +0 -1
- package/node_modules/remote-pi/dist/pairing/storage.d.ts +0 -120
- package/node_modules/remote-pi/dist/pairing/storage.js +0 -604
- package/node_modules/remote-pi/dist/pairing/storage.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/codec.d.ts +0 -7
- package/node_modules/remote-pi/dist/protocol/codec.js +0 -49
- package/node_modules/remote-pi/dist/protocol/codec.js.map +0 -1
- package/node_modules/remote-pi/dist/protocol/types.d.ts +0 -393
- package/node_modules/remote-pi/dist/protocol/types.js +0 -2
- package/node_modules/remote-pi/dist/protocol/types.js.map +0 -1
- package/node_modules/remote-pi/dist/rooms.d.ts +0 -29
- package/node_modules/remote-pi/dist/rooms.js +0 -57
- package/node_modules/remote-pi/dist/rooms.js.map +0 -1
- package/node_modules/remote-pi/dist/session/address.d.ts +0 -49
- package/node_modules/remote-pi/dist/session/address.js +0 -58
- package/node_modules/remote-pi/dist/session/address.js.map +0 -1
- package/node_modules/remote-pi/dist/session/agent_bridge.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/agent_bridge.js +0 -146
- package/node_modules/remote-pi/dist/session/agent_bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/bridge.d.ts +0 -37
- package/node_modules/remote-pi/dist/session/bridge.js +0 -130
- package/node_modules/remote-pi/dist/session/bridge.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker.d.ts +0 -189
- package/node_modules/remote-pi/dist/session/broker.js +0 -507
- package/node_modules/remote-pi/dist/session/broker.js.map +0 -1
- package/node_modules/remote-pi/dist/session/broker_remote.d.ts +0 -74
- package/node_modules/remote-pi/dist/session/broker_remote.js +0 -583
- package/node_modules/remote-pi/dist/session/broker_remote.js.map +0 -1
- package/node_modules/remote-pi/dist/session/cwd_lock.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/cwd_lock.js +0 -126
- package/node_modules/remote-pi/dist/session/cwd_lock.js.map +0 -1
- package/node_modules/remote-pi/dist/session/envelope.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/envelope.js +0 -117
- package/node_modules/remote-pi/dist/session/envelope.js.map +0 -1
- package/node_modules/remote-pi/dist/session/global_config.d.ts +0 -33
- package/node_modules/remote-pi/dist/session/global_config.js +0 -73
- package/node_modules/remote-pi/dist/session/global_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/ipc.d.ts +0 -27
- package/node_modules/remote-pi/dist/session/ipc.js +0 -22
- package/node_modules/remote-pi/dist/session/ipc.js.map +0 -1
- package/node_modules/remote-pi/dist/session/leader_election.d.ts +0 -32
- package/node_modules/remote-pi/dist/session/leader_election.js +0 -106
- package/node_modules/remote-pi/dist/session/leader_election.js.map +0 -1
- package/node_modules/remote-pi/dist/session/local_config.d.ts +0 -55
- package/node_modules/remote-pi/dist/session/local_config.js +0 -157
- package/node_modules/remote-pi/dist/session/local_config.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_message.d.ts +0 -20
- package/node_modules/remote-pi/dist/session/mesh_message.js +0 -28
- package/node_modules/remote-pi/dist/session/mesh_message.js.map +0 -1
- package/node_modules/remote-pi/dist/session/mesh_node.d.ts +0 -116
- package/node_modules/remote-pi/dist/session/mesh_node.js +0 -573
- package/node_modules/remote-pi/dist/session/mesh_node.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer.d.ts +0 -139
- package/node_modules/remote-pi/dist/session/peer.js +0 -451
- package/node_modules/remote-pi/dist/session/peer.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_inventory.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/peer_inventory.js +0 -48
- package/node_modules/remote-pi/dist/session/peer_inventory.js.map +0 -1
- package/node_modules/remote-pi/dist/session/peer_limits.d.ts +0 -9
- package/node_modules/remote-pi/dist/session/peer_limits.js +0 -21
- package/node_modules/remote-pi/dist/session/peer_limits.js.map +0 -1
- package/node_modules/remote-pi/dist/session/setup_wizard.d.ts +0 -39
- package/node_modules/remote-pi/dist/session/setup_wizard.js +0 -63
- package/node_modules/remote-pi/dist/session/setup_wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tool_gate.d.ts +0 -5
- package/node_modules/remote-pi/dist/session/tool_gate.js +0 -11
- package/node_modules/remote-pi/dist/session/tool_gate.js.map +0 -1
- package/node_modules/remote-pi/dist/session/tools.d.ts +0 -24
- package/node_modules/remote-pi/dist/session/tools.js +0 -252
- package/node_modules/remote-pi/dist/session/tools.js.map +0 -1
- package/node_modules/remote-pi/dist/session/wizard.d.ts +0 -13
- package/node_modules/remote-pi/dist/session/wizard.js +0 -20
- package/node_modules/remote-pi/dist/session/wizard.js.map +0 -1
- package/node_modules/remote-pi/dist/settings.d.ts +0 -15
- package/node_modules/remote-pi/dist/settings.js +0 -52
- package/node_modules/remote-pi/dist/settings.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/peer_channel.d.ts +0 -37
- package/node_modules/remote-pi/dist/transport/peer_channel.js +0 -98
- package/node_modules/remote-pi/dist/transport/peer_channel.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/pi_forward_client.d.ts +0 -29
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js +0 -62
- package/node_modules/remote-pi/dist/transport/pi_forward_client.js.map +0 -1
- package/node_modules/remote-pi/dist/transport/relay_client.d.ts +0 -91
- package/node_modules/remote-pi/dist/transport/relay_client.js +0 -206
- package/node_modules/remote-pi/dist/transport/relay_client.js.map +0 -1
- package/node_modules/remote-pi/dist/ui/footer.d.ts +0 -32
- package/node_modules/remote-pi/dist/ui/footer.js +0 -34
- package/node_modules/remote-pi/dist/ui/footer.js.map +0 -1
- package/node_modules/remote-pi/docs/daemon.md +0 -289
- package/node_modules/remote-pi/package.json +0 -92
- package/node_modules/remote-pi/service-templates/launchd.plist.template +0 -35
- package/node_modules/remote-pi/service-templates/systemd.service.template +0 -19
- package/node_modules/remote-pi/service-templates/task-launcher.vbs.template +0 -10
- package/node_modules/remote-pi/service-templates/task-scheduler.xml.template +0 -38
- package/node_modules/remote-pi/skills/agent-network/SKILL.md +0 -310
|
@@ -1,2080 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Identifier-aware inverted word index + BM25 ranking.
|
|
3
|
-
*
|
|
4
|
-
* The lexical half of the "codebase mental model + hybrid ranking" ask (#162):
|
|
5
|
-
* a deterministic, zero-dep index over source identifiers that answers
|
|
6
|
-
* "which files are most relevant to <query>" with BM25 relevance plus a small
|
|
7
|
-
* set of priors (demote tests/vendor and doc files) and an optional graph
|
|
8
|
-
* centrality boost (importedBy count from the reverse-dependency index). It
|
|
9
|
-
* complements LSP/symbol navigation rather than duplicating the host's grep:
|
|
10
|
-
* grep finds raw substrings; this ranks files by identifier relevance.
|
|
11
|
-
*
|
|
12
|
-
* Built from file contents during the session scan, persisted in the project
|
|
13
|
-
* snapshot (serialize/deserialize below), and queried via an MCP tool. No
|
|
14
|
-
* embeddings, no native deps, no daemon — pure in-process TypeScript.
|
|
15
|
-
*/
|
|
16
|
-
import * as fs from "node:fs";
|
|
17
|
-
import * as path from "node:path";
|
|
18
|
-
import { createDeadline, forEachCooperatively, yieldIfOverBudget, } from "./cooperative-budget.js";
|
|
19
|
-
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
20
|
-
import { KIND_EXTENSIONS } from "./file-kinds.js";
|
|
21
|
-
import { isTestFileName } from "./file-role.js";
|
|
22
|
-
import { PathKeyedMap } from "./path-keyed-map.js";
|
|
23
|
-
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey } from "./path-utils.js";
|
|
24
|
-
import { createSingleFlight } from "./single-flight.js";
|
|
25
|
-
import { createDebounceScheduler, } from "./persist-debounce.js";
|
|
26
|
-
import { getWordIndexMaxFilesDerived } from "./project-scale.js";
|
|
27
|
-
import { compactPostingsIntoArena, compactPostingsIntoArenaCooperatively, countPostingBackingStores, countPostingEntries, estimateWordIndexStoreBytes, WordForwardEntry, WordIndexFileTable, WordPostingList, } from "./word-index-store.js";
|
|
28
|
-
import { logWordIndex } from "./word-index-logger.js";
|
|
29
|
-
/**
|
|
30
|
-
* The single normalizer every word-index path key folds through (#1025). The
|
|
31
|
-
* CHEAP form — slash-fold + win32-lowercase, NO filesystem I/O — because the
|
|
32
|
-
* index is a hot, single-process, in-memory structure whose keys this process
|
|
33
|
-
* produces itself (`collectSourceFilesAsync`'s walk, `path.resolve` at the
|
|
34
|
-
* per-edit seams). `normalizeMapKey`'s `realpathSync` per call would be wrong on
|
|
35
|
-
* the BM25 path. Applied at BOTH the build seam and the per-edit update seam so
|
|
36
|
-
* a file's on-disk casing (walk) and its tool-input casing (edit) can no longer
|
|
37
|
-
* diverge into a duplicate doc entry with stale postings (the #1025 item #2
|
|
38
|
-
* bug). Kept as a named alias so every seam is grep-visible and provably shares
|
|
39
|
-
* ONE normalizer with the {@link PathKeyedMap}s below.
|
|
40
|
-
*/
|
|
41
|
-
export const wordIndexKey = normalizeEphemeralMapKey;
|
|
42
|
-
// Common language keywords / boilerplate — indexing them adds noise and bloats
|
|
43
|
-
// postings without improving relevance. Kept deliberately small and
|
|
44
|
-
// language-agnostic.
|
|
45
|
-
const STOPWORDS = new Set([
|
|
46
|
-
"the",
|
|
47
|
-
"and",
|
|
48
|
-
"for",
|
|
49
|
-
"let",
|
|
50
|
-
"var",
|
|
51
|
-
"const",
|
|
52
|
-
"function",
|
|
53
|
-
"return",
|
|
54
|
-
"if",
|
|
55
|
-
"else",
|
|
56
|
-
"import",
|
|
57
|
-
"export",
|
|
58
|
-
"from",
|
|
59
|
-
"class",
|
|
60
|
-
"interface",
|
|
61
|
-
"type",
|
|
62
|
-
"enum",
|
|
63
|
-
"new",
|
|
64
|
-
"this",
|
|
65
|
-
"self",
|
|
66
|
-
"void",
|
|
67
|
-
"null",
|
|
68
|
-
"true",
|
|
69
|
-
"false",
|
|
70
|
-
"async",
|
|
71
|
-
"await",
|
|
72
|
-
"public",
|
|
73
|
-
"private",
|
|
74
|
-
"protected",
|
|
75
|
-
"static",
|
|
76
|
-
"def",
|
|
77
|
-
"fn",
|
|
78
|
-
"func",
|
|
79
|
-
"struct",
|
|
80
|
-
"impl",
|
|
81
|
-
"pub",
|
|
82
|
-
"use",
|
|
83
|
-
"mod",
|
|
84
|
-
"in",
|
|
85
|
-
"of",
|
|
86
|
-
"as",
|
|
87
|
-
"is",
|
|
88
|
-
"not",
|
|
89
|
-
"with",
|
|
90
|
-
]);
|
|
91
|
-
// Retain the old directory alternatives and anchors; filename classification
|
|
92
|
-
// belongs to file-role (#2928). The relevance multiplier remains 0.3.
|
|
93
|
-
const VENDOR_DIR_RE = /(^|[\\/])(?:tests?|__tests__|spec|specs|__mocks__|vendor|node_modules|examples?|fixtures?|\.git|dist|build|coverage)([\\/]|$)/i;
|
|
94
|
-
const DOC_FILE_RE = /\.(?:md|mdx|markdown|json|json5|jsonc|txt|rst|lock|ya?ml|toml|csv)$/i;
|
|
95
|
-
const TEST_VENDOR_PENALTY = 0.3;
|
|
96
|
-
const DOC_FILE_PENALTY = 0.5;
|
|
97
|
-
const BM25_K1 = 1.2;
|
|
98
|
-
const BM25_B = 0.75;
|
|
99
|
-
function isTestOrVendor(file) {
|
|
100
|
-
return isTestFileName(file) || VENDOR_DIR_RE.test(file);
|
|
101
|
-
}
|
|
102
|
-
function isDocFile(file) {
|
|
103
|
-
return DOC_FILE_RE.test(file);
|
|
104
|
-
}
|
|
105
|
-
/**
|
|
106
|
-
* Split an identifier into lowercased sub-tokens across camelCase, PascalCase,
|
|
107
|
-
* snake_case, kebab-case, dotted, and digit boundaries — and keep the whole
|
|
108
|
-
* lowercased identifier too. `getUserByID` → [getuserbyid, get, user, by, id];
|
|
109
|
-
* `MAX_RETRY_2` → [max_retry_2, max, retry, 2] (whole kept, plus parts).
|
|
110
|
-
*/
|
|
111
|
-
export function splitIdentifier(identifier) {
|
|
112
|
-
const parts = new Set();
|
|
113
|
-
const whole = identifier.toLowerCase();
|
|
114
|
-
if (whole.length >= 2 && !STOPWORDS.has(whole))
|
|
115
|
-
parts.add(whole);
|
|
116
|
-
for (const chunk of identifier.split(/[^A-Za-z0-9]+/)) {
|
|
117
|
-
if (!chunk)
|
|
118
|
-
continue;
|
|
119
|
-
const spaced = chunk
|
|
120
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1 $2") // camelCase → camel Case
|
|
121
|
-
.replace(/[A-Z](?=[A-Z][a-z])/g, "$& ") // HTTPServer → HTTP Server (linear; lookahead avoids super-linear backtracking, S5852)
|
|
122
|
-
.replace(/([A-Za-z])([0-9])/g, "$1 $2") // retry2 → retry 2
|
|
123
|
-
.replace(/([0-9])([A-Za-z])/g, "$1 $2"); // 2fa → 2 fa
|
|
124
|
-
for (const sub of spaced.split(/\s+/)) {
|
|
125
|
-
const token = sub.toLowerCase();
|
|
126
|
-
if (token.length >= 2 && !STOPWORDS.has(token))
|
|
127
|
-
parts.add(token);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return [...parts];
|
|
131
|
-
}
|
|
132
|
-
/** Extract identifier-like tokens from a line and split each into sub-tokens. */
|
|
133
|
-
export function tokenizeLine(line) {
|
|
134
|
-
const tokens = [];
|
|
135
|
-
const matches = line.match(/[A-Za-z_$][A-Za-z0-9_$]*/g);
|
|
136
|
-
if (!matches)
|
|
137
|
-
return tokens;
|
|
138
|
-
for (const match of matches) {
|
|
139
|
-
for (const token of splitIdentifier(match))
|
|
140
|
-
tokens.push(token);
|
|
141
|
-
}
|
|
142
|
-
return tokens;
|
|
143
|
-
}
|
|
144
|
-
const WORD_INDEX_BUILD_YIELD_BUDGET_MS = 8;
|
|
145
|
-
/**
|
|
146
|
-
* A line at or above this length is treated as a yield checkpoint on its own.
|
|
147
|
-
* The per-50-lines cadence is a proxy for work that only holds on hand-written
|
|
148
|
-
* source: a minified/bundled single-line file up to {@link WORD_INDEX_MAX_BYTES}
|
|
149
|
-
* that the source filter did not exclude would otherwise get ZERO in-document
|
|
150
|
-
* yields — one unbroken tokenize+push burst far above the budget (#1197 review
|
|
151
|
-
* finding 4). Checking the clock after any long line makes the cooperativeness
|
|
152
|
-
* claim true per BYTE, not just per line count.
|
|
153
|
-
*/
|
|
154
|
-
const WORD_INDEX_LONG_LINE_YIELD_CHARS = 4096;
|
|
155
|
-
/**
|
|
156
|
-
* Shared cooperative time-slicer for every bulk word-index path (#1197).
|
|
157
|
-
*
|
|
158
|
-
* Time-based, not count-based: "yield every N items" is only a bound when the
|
|
159
|
-
* per-item cost is bounded too, and the incremental refresh's per-document cost
|
|
160
|
-
* grows with the corpus (each replacement filters shared posting arrays). The
|
|
161
|
-
* #1197 outage was exactly that shape — 239 stale documents held pi's event loop
|
|
162
|
-
* for seconds between two "every 100 files" checkpoints. Callers combine this
|
|
163
|
-
* The shared deadline is checked at every bounded work unit so abort latency
|
|
164
|
-
* and occupancy are governed by the same monotonic budget.
|
|
165
|
-
*/
|
|
166
|
-
function createEmptyWordIndex(truncated) {
|
|
167
|
-
return {
|
|
168
|
-
postings: new Map(),
|
|
169
|
-
fileTable: new WordIndexFileTable(),
|
|
170
|
-
docLengths: new PathKeyedMap(wordIndexKey),
|
|
171
|
-
totalTokens: 0,
|
|
172
|
-
docCount: 0,
|
|
173
|
-
truncated,
|
|
174
|
-
forward: new PathKeyedMap(wordIndexKey),
|
|
175
|
-
fileMtimes: new PathKeyedMap(wordIndexKey),
|
|
176
|
-
fileSizes: new PathKeyedMap(wordIndexKey),
|
|
177
|
-
replacementStats: { count: 0, totalMs: 0, maxMs: 0 },
|
|
178
|
-
dirtyFiles: new Set(),
|
|
179
|
-
postingStoreCount: 0,
|
|
180
|
-
recompactFlight: createSingleFlight(),
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
export function countWordIndexPostingEntries(index) {
|
|
184
|
-
return countPostingEntries(index);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Estimated resident bytes of the index's two packed stores (#2069).
|
|
188
|
-
* Reported on the `full_rebuild`, `incremental_refresh`, and `cold_build`
|
|
189
|
-
* word-index log records and on the `memory_sample` word-index subsystem, so
|
|
190
|
-
* a heap census can be reconciled against the log without taking a snapshot —
|
|
191
|
-
* the gap that let #1999 under-count this subsystem by a factor of sixty.
|
|
192
|
-
*/
|
|
193
|
-
export function estimateWordIndexResidentBytes(index) {
|
|
194
|
-
return estimateWordIndexStoreBytes(index);
|
|
195
|
-
}
|
|
196
|
-
/**
|
|
197
|
-
* Record a posting whose file id the file table cannot resolve (#2069).
|
|
198
|
-
*
|
|
199
|
-
* Unreachable by construction: {@link WordIndexFileTable.release} only runs
|
|
200
|
-
* after the forward index has enumerated and removed every posting naming that
|
|
201
|
-
* id. If it fires, that invariant broke, and the visible symptom is a query
|
|
202
|
-
* returning FEWER results with nothing to distinguish it from a smaller match
|
|
203
|
-
* set — the clean-versus-errored ambiguity AGENTS.md catalogs as shape 10.
|
|
204
|
-
*
|
|
205
|
-
* `incrementDegradationCount` rather than `recordDegradationOnce` because
|
|
206
|
-
* ranking is a hot loop: the tally stays exact for every occurrence while only
|
|
207
|
-
* the first per (kind, orphaned id) writes a durable row. Subject is the id
|
|
208
|
-
* itself, so aggregation keeps the discriminating identity.
|
|
209
|
-
*/
|
|
210
|
-
function recordOrphanWordIndexFileId(fileId, token, seam) {
|
|
211
|
-
incrementDegradationCount({
|
|
212
|
-
kind: "word-index-orphan-file-id",
|
|
213
|
-
subject: `fileId:${fileId}`,
|
|
214
|
-
reason: `${seam} dropped a posting for token "${token}": the file table has no path for this id`,
|
|
215
|
-
});
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Decode one token’s postings into `{ file, line }` objects. Allocates, so it
|
|
219
|
-
* is for callers that genuinely need the display form (tests, diagnostics) —
|
|
220
|
-
* never for the hot ranking loop, which reads the packed lanes directly.
|
|
221
|
-
*/
|
|
222
|
-
export function wordIndexPostingHits(index, token) {
|
|
223
|
-
const list = index.postings.get(token);
|
|
224
|
-
if (!list)
|
|
225
|
-
return [];
|
|
226
|
-
const hits = [];
|
|
227
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
228
|
-
const fileId = list.fileIdAt(i);
|
|
229
|
-
const file = index.fileTable.pathFor(fileId);
|
|
230
|
-
if (file === undefined) {
|
|
231
|
-
recordOrphanWordIndexFileId(fileId, token, "decode");
|
|
232
|
-
continue;
|
|
233
|
-
}
|
|
234
|
-
hits.push({ file, line: list.lineAt(i) });
|
|
235
|
-
}
|
|
236
|
-
return hits;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Settle a freshly built index's posting store into its compact resident form
|
|
240
|
-
* (#2069): re-home every list into one shared arena, sized from the live entry
|
|
241
|
-
* counts. That both releases the doubling growth slack a build leaves behind
|
|
242
|
-
* and stops the long tail of rare tokens paying for an `ArrayBuffer` header
|
|
243
|
-
* each.
|
|
244
|
-
*/
|
|
245
|
-
function compactWordIndexPostings(index) {
|
|
246
|
-
compactPostingsIntoArena(index.postings);
|
|
247
|
-
index.postingStoreCount = countPostingBackingStores(index.postings);
|
|
248
|
-
}
|
|
249
|
-
/** Bump the running distinct-backing-store tally by `count` (#2117). */
|
|
250
|
-
function notePostingStoresAllocated(index, count) {
|
|
251
|
-
index.postingStoreCount = (index.postingStoreCount ?? 0) + count;
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Floor for the recompaction gate: below this many backing stores the arena is
|
|
255
|
-
* never repacked, so a small index never pays an O(vocab) walk it cannot amortize
|
|
256
|
-
* (#2117). This is also the whole gate for indexes whose vocabulary is under
|
|
257
|
-
* `WORD_INDEX_RECOMPACT_STORE_FLOOR / WORD_INDEX_RECOMPACT_STORE_FRACTION`
|
|
258
|
-
* tokens, keeping the pre-#2246 behaviour for small corpora bit-identical.
|
|
259
|
-
*/
|
|
260
|
-
const WORD_INDEX_RECOMPACT_STORE_FLOOR = 64;
|
|
261
|
-
/**
|
|
262
|
-
* Recompaction gate as a share of the live vocabulary (#2246). The flat 64-store
|
|
263
|
-
* threshold #2117 shipped was crossed by every single edit: one document
|
|
264
|
-
* replacement raises `postingStoreCount` by roughly the edited document's
|
|
265
|
-
* distinct-token count (hundreds), so the whole O(vocab) arena was rebuilt after
|
|
266
|
-
* every edit. Gating on a fraction of `postings.size` instead lets fragmentation
|
|
267
|
-
* accumulate proportionally to the index, so recompaction fires once per many
|
|
268
|
-
* edits rather than once per edit.
|
|
269
|
-
*
|
|
270
|
-
* Memory ceiling: the store count can reach a tenth of the vocabulary in private
|
|
271
|
-
* stores before a repack, each carrying a fixed header plus growth slack. #2117
|
|
272
|
-
* measured the fully-churned ceiling (every token private, store count =
|
|
273
|
-
* vocabulary) at +22% resident. The cost does not scale with the store count
|
|
274
|
-
* alone, because the tokens that churn most are also the ones carrying the
|
|
275
|
-
* longest posting lists. PR #2246's adversarial hot-token probe measured this
|
|
276
|
-
* gate at +13.7% resident at rest and +19.9% peak over base — inside the +22%
|
|
277
|
-
* #2117 accepted, but not by the wide margin a naive store-count ratio would
|
|
278
|
-
* predict. Raising the fraction further would cross that bound.
|
|
279
|
-
*/
|
|
280
|
-
const WORD_INDEX_RECOMPACT_STORE_FRACTION = 0.1;
|
|
281
|
-
/**
|
|
282
|
-
* Backing-store count that triggers a repack for `index`: a fixed floor, or a
|
|
283
|
-
* share of the live vocabulary once the index is large enough for the share to
|
|
284
|
-
* exceed the floor. O(1) — reads `postings.size`, never walks the vocabulary.
|
|
285
|
-
*/
|
|
286
|
-
function wordIndexRecompactThreshold(index) {
|
|
287
|
-
return Math.max(WORD_INDEX_RECOMPACT_STORE_FLOOR, Math.floor(index.postings.size * WORD_INDEX_RECOMPACT_STORE_FRACTION));
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Pack the arena if churn has spread the postings across more than the
|
|
291
|
-
* threshold of backing stores (#2117, #2246). Runs the exact O(vocab) store
|
|
292
|
-
* count as the authoritative gate — the per-edit hot path uses the O(1)
|
|
293
|
-
* `postingStoreCount` estimate, so this expensive walk only happens inside the
|
|
294
|
-
* bounded, off-hot-path recompaction it guards. Resets the estimate to the
|
|
295
|
-
* true post-compaction count.
|
|
296
|
-
*/
|
|
297
|
-
async function recompactWordIndexPostingsIfNeeded(index, root) {
|
|
298
|
-
const threshold = wordIndexRecompactThreshold(index);
|
|
299
|
-
const beforeStores = countPostingBackingStores(index.postings);
|
|
300
|
-
if (beforeStores <= threshold) {
|
|
301
|
-
index.postingStoreCount = beforeStores;
|
|
302
|
-
return;
|
|
303
|
-
}
|
|
304
|
-
const beforeBytes = estimateWordIndexResidentBytes(index);
|
|
305
|
-
await compactPostingsIntoArenaCooperatively(index.postings);
|
|
306
|
-
const afterStores = countPostingBackingStores(index.postings);
|
|
307
|
-
index.postingStoreCount = afterStores;
|
|
308
|
-
const afterBytes = estimateWordIndexResidentBytes(index);
|
|
309
|
-
const firstForRoot = incrementDegradationCount({
|
|
310
|
-
kind: "word-index-arena-recompact",
|
|
311
|
-
subject: path.resolve(root),
|
|
312
|
-
reason: `arena store threshold ${threshold} exceeded`,
|
|
313
|
-
metadata: { beforeBytes, afterBytes, beforeStores, afterStores },
|
|
314
|
-
});
|
|
315
|
-
// Keep the detailed record bounded per root. The ledger still counts every
|
|
316
|
-
// repeated recompaction and emits its own power-of-two summaries.
|
|
317
|
-
if (firstForRoot) {
|
|
318
|
-
logWordIndex({
|
|
319
|
-
phase: "incremental_refresh",
|
|
320
|
-
cwd: path.resolve(root),
|
|
321
|
-
trigger: "incremental_refresh",
|
|
322
|
-
reason: `arena_recompact beforeBytes=${beforeBytes} afterBytes=${afterBytes} beforeStores=${beforeStores} afterStores=${afterStores}`,
|
|
323
|
-
});
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
/**
|
|
327
|
-
* Serialize a recompaction through the per-index async operation queue so no
|
|
328
|
-
* async edit or refresh interleaves it (#2117). The queue plus the corruption-
|
|
329
|
-
* proof cooperative compactor together close the review-F2 data race: the queue
|
|
330
|
-
* excludes async operations, and the compactor's snapshot-and-skip publish
|
|
331
|
-
* excludes the synchronous per-edit path the queue cannot see. The per-index
|
|
332
|
-
* `recompactFlight` single-flight (#1753) keeps a burst of edits from stacking
|
|
333
|
-
* duplicates: a caller that arrives while one recompaction runs joins it rather
|
|
334
|
-
* than starting a second. The single slot uses a constant key.
|
|
335
|
-
*/
|
|
336
|
-
function enqueueWordIndexRecompact(index, root) {
|
|
337
|
-
if ((index.postingStoreCount ?? 0) <= wordIndexRecompactThreshold(index)) {
|
|
338
|
-
return Promise.resolve();
|
|
339
|
-
}
|
|
340
|
-
const flight = (index.recompactFlight ??= createSingleFlight());
|
|
341
|
-
return flight.run("recompact", () => enqueueAsyncWordIndexOperation(index, () => recompactWordIndexPostingsIfNeeded(index, root)));
|
|
342
|
-
}
|
|
343
|
-
/**
|
|
344
|
-
* O(1) hot-path gate for the synchronous per-edit seam. Reads the running
|
|
345
|
-
* store-count estimate, never a fresh `Set` over the vocabulary, then defers
|
|
346
|
-
* the actual work onto the async queue so it cannot stall the edit (#2117).
|
|
347
|
-
*/
|
|
348
|
-
function scheduleWordIndexRecompact(index, filePath) {
|
|
349
|
-
void enqueueWordIndexRecompact(index, path.dirname(filePath));
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Test hook: settle any queued arena recompaction for `index`. Draining the
|
|
353
|
-
* index's own async operation queue is enough because the recompaction runs
|
|
354
|
-
* through it; there is no module-level scheduler to flush.
|
|
355
|
-
*/
|
|
356
|
-
export async function flushWordIndexRecompactionsForTests(index) {
|
|
357
|
-
await (asyncWordIndexOperations.get(index) ?? Promise.resolve());
|
|
358
|
-
}
|
|
359
|
-
function recordWordIndexReplacement(index, startedAt) {
|
|
360
|
-
const stats = index.replacementStats ?? { count: 0, totalMs: 0, maxMs: 0 };
|
|
361
|
-
const durationMs = Math.max(0, Date.now() - startedAt);
|
|
362
|
-
stats.count += 1;
|
|
363
|
-
stats.totalMs += durationMs;
|
|
364
|
-
stats.maxMs = Math.max(stats.maxMs, durationMs);
|
|
365
|
-
index.replacementStats = stats;
|
|
366
|
-
}
|
|
367
|
-
/** Intern a document path once per index; posting removal compares this id. */
|
|
368
|
-
function internWordIndexFile(index, filePath) {
|
|
369
|
-
return index.fileTable.intern(wordIndexKey(filePath), filePath);
|
|
370
|
-
}
|
|
371
|
-
function appendWordIndexPostings(index, filePath, perTokenHits) {
|
|
372
|
-
const fileId = internWordIndexFile(index, filePath);
|
|
373
|
-
const tokenLineCounts = new Map();
|
|
374
|
-
for (const [token, lineNumbers] of perTokenHits) {
|
|
375
|
-
let list = index.postings.get(token);
|
|
376
|
-
if (list) {
|
|
377
|
-
// A grow allocates a fresh private store; count it so the O(1) gate
|
|
378
|
-
// tracks fragmentation without re-walking the vocabulary (#2117).
|
|
379
|
-
if (list.reserve(list.length + lineNumbers.length)) {
|
|
380
|
-
notePostingStoresAllocated(index, 1);
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
else {
|
|
384
|
-
list = new WordPostingList(token, lineNumbers.length);
|
|
385
|
-
index.postings.set(token, list);
|
|
386
|
-
notePostingStoresAllocated(index, 1);
|
|
387
|
-
}
|
|
388
|
-
// `list.token` is the canonical instance, so the forward entry points at
|
|
389
|
-
// the same string the postings map is keyed by instead of retaining this
|
|
390
|
-
// document’s own tokenizer allocation (#2069).
|
|
391
|
-
tokenLineCounts.set(list.token, lineNumbers.length);
|
|
392
|
-
for (const line of lineNumbers)
|
|
393
|
-
list.push(fileId, line);
|
|
394
|
-
}
|
|
395
|
-
return tokenLineCounts;
|
|
396
|
-
}
|
|
397
|
-
function commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt) {
|
|
398
|
-
const tokenLineCounts = appendWordIndexPostings(index, doc.path, perTokenHits);
|
|
399
|
-
index.docLengths.set(doc.path, docLength);
|
|
400
|
-
index.forward.set(doc.path, WordForwardEntry.fromTally(tokenLineCounts));
|
|
401
|
-
index.fileMtimes.set(doc.path, -1);
|
|
402
|
-
index.fileSizes.set(doc.path, Buffer.byteLength(doc.content, "utf-8"));
|
|
403
|
-
index.dirtyFiles?.add(wordIndexKey(doc.path));
|
|
404
|
-
index.totalTokens += docLength;
|
|
405
|
-
index.docCount += 1;
|
|
406
|
-
recordWordIndexReplacement(index, startedAt);
|
|
407
|
-
}
|
|
408
|
-
function indexWordLine(index, fileId, line, lineNumber, tokenLineCounts) {
|
|
409
|
-
const lineTokens = tokenizeLine(line);
|
|
410
|
-
const seenOnLine = new Set();
|
|
411
|
-
for (const token of lineTokens) {
|
|
412
|
-
if (seenOnLine.has(token))
|
|
413
|
-
continue;
|
|
414
|
-
seenOnLine.add(token);
|
|
415
|
-
let list = index.postings.get(token);
|
|
416
|
-
if (!list) {
|
|
417
|
-
list = new WordPostingList(token, 1);
|
|
418
|
-
index.postings.set(token, list);
|
|
419
|
-
}
|
|
420
|
-
list.push(fileId, lineNumber);
|
|
421
|
-
// Canonical instance, not this line’s fresh tokenizer allocation (#2069).
|
|
422
|
-
const canonical = list.token;
|
|
423
|
-
tokenLineCounts.set(canonical, (tokenLineCounts.get(canonical) ?? 0) + 1);
|
|
424
|
-
}
|
|
425
|
-
return lineTokens.length;
|
|
426
|
-
}
|
|
427
|
-
function finishWordIndexDocument(index, doc, docLength, tokenLineCounts) {
|
|
428
|
-
index.docLengths.set(doc.path, docLength);
|
|
429
|
-
index.forward?.set(doc.path, WordForwardEntry.fromTally(tokenLineCounts));
|
|
430
|
-
index.fileMtimes.set(doc.path, doc.mtimeMs ?? 0);
|
|
431
|
-
index.fileSizes.set(doc.path, doc.size ?? Buffer.byteLength(doc.content, "utf-8"));
|
|
432
|
-
// A build has no prior wire cache. Keeping the marker makes a future
|
|
433
|
-
// serializer cache explicit and gives incremental refresh one uniform seam.
|
|
434
|
-
index.dirtyFiles?.add(wordIndexKey(doc.path));
|
|
435
|
-
index.totalTokens += docLength;
|
|
436
|
-
index.docCount += 1;
|
|
437
|
-
}
|
|
438
|
-
function indexWordDocument(index, doc) {
|
|
439
|
-
const fileId = internWordIndexFile(index, doc.path);
|
|
440
|
-
const lines = doc.content.split(/\r?\n/);
|
|
441
|
-
const tokenLineCounts = new Map();
|
|
442
|
-
let docLength = 0;
|
|
443
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
444
|
-
docLength += indexWordLine(index, fileId, lines[i], i + 1, tokenLineCounts);
|
|
445
|
-
}
|
|
446
|
-
finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
|
|
447
|
-
}
|
|
448
|
-
export function buildWordIndex(files) {
|
|
449
|
-
const index = createEmptyWordIndex(files.truncated ?? false);
|
|
450
|
-
for (const doc of files)
|
|
451
|
-
indexWordDocument(index, doc);
|
|
452
|
-
compactWordIndexPostings(index);
|
|
453
|
-
return index;
|
|
454
|
-
}
|
|
455
|
-
/**
|
|
456
|
-
* Cooperative production builder for bulk/cold paths. It produces the exact
|
|
457
|
-
* same index as {@link buildWordIndex}, but time-slices both between documents
|
|
458
|
-
* and within a large document so startup warmup cannot monopolize pi's TUI
|
|
459
|
-
* event loop (#1197). The index is private until this resolves, so a superseded
|
|
460
|
-
* build never publishes a partial replacement.
|
|
461
|
-
*/
|
|
462
|
-
export async function buildWordIndexAsync(files, shouldContinue = () => true) {
|
|
463
|
-
const index = createEmptyWordIndex(files.truncated ?? false);
|
|
464
|
-
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
465
|
-
for (const doc of files) {
|
|
466
|
-
if (!shouldContinue())
|
|
467
|
-
throw new Error("word index build superseded");
|
|
468
|
-
const fileId = internWordIndexFile(index, doc.path);
|
|
469
|
-
const lines = doc.content.split(/\r?\n/);
|
|
470
|
-
const tokenLineCounts = new Map();
|
|
471
|
-
let docLength = 0;
|
|
472
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
473
|
-
const line = lines[i];
|
|
474
|
-
docLength += indexWordLine(index, fileId, line, i + 1, tokenLineCounts);
|
|
475
|
-
if (line.length >= WORD_INDEX_LONG_LINE_YIELD_CHARS ||
|
|
476
|
-
deadline.expired()) {
|
|
477
|
-
await yieldIfOverBudget(deadline);
|
|
478
|
-
if (!shouldContinue())
|
|
479
|
-
throw new Error("word index build superseded");
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
finishWordIndexDocument(index, doc, docLength, tokenLineCounts);
|
|
483
|
-
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
484
|
-
if (!shouldContinue())
|
|
485
|
-
throw new Error("word index build superseded");
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
compactWordIndexPostings(index);
|
|
489
|
-
return index;
|
|
490
|
-
}
|
|
491
|
-
/**
|
|
492
|
-
* Remove `filePath`'s postings/docLength/forward entry from `index` in place,
|
|
493
|
-
* using the forward index to know exactly which tokens to touch (no scan of
|
|
494
|
-
* unrelated postings). No-op (returns false) if the index has no forward
|
|
495
|
-
* index yet (pre-phase-2 / deserialized-old-shape) or the file isn't present —
|
|
496
|
-
* callers must treat `false` as "fall back to a full rebuild", never as
|
|
497
|
-
* silent success.
|
|
498
|
-
*/
|
|
499
|
-
export function removeWordIndexDocument(index, filePath) {
|
|
500
|
-
if (!index.forward)
|
|
501
|
-
return false;
|
|
502
|
-
const tokenLineCounts = index.forward.get(filePath);
|
|
503
|
-
if (!tokenLineCounts)
|
|
504
|
-
return false;
|
|
505
|
-
// `postings` is token-keyed (not a PathKeyedMap), so the file identity it
|
|
506
|
-
// carries must be resolved through the SAME normalizer the path maps use —
|
|
507
|
-
// otherwise a build-form hit (`SUB/a.ts`) survives an edit-form removal
|
|
508
|
-
// (`sub/a.ts`) on a case-insensitive FS and lingers as a stale posting
|
|
509
|
-
// (the #1025 item #2 bug this fix closes). Since #2069 that identity is the
|
|
510
|
-
// file table’s integer id, so the comparison below is an int compare rather
|
|
511
|
-
// than a per-element string compare.
|
|
512
|
-
const removedKey = wordIndexKey(filePath);
|
|
513
|
-
const removedId = index.fileTable.idFor(removedKey);
|
|
514
|
-
if (removedId === undefined)
|
|
515
|
-
return false;
|
|
516
|
-
for (const token of tokenLineCounts.keys()) {
|
|
517
|
-
const list = index.postings.get(token);
|
|
518
|
-
if (!list)
|
|
519
|
-
continue;
|
|
520
|
-
const next = list.withoutFile(removedId);
|
|
521
|
-
// `withoutFile` always returns a freshly allocated private store (#2117).
|
|
522
|
-
if (next.length > 0) {
|
|
523
|
-
index.postings.set(token, next);
|
|
524
|
-
notePostingStoresAllocated(index, 1);
|
|
525
|
-
}
|
|
526
|
-
else
|
|
527
|
-
index.postings.delete(token);
|
|
528
|
-
}
|
|
529
|
-
const docLength = index.docLengths.get(filePath) ?? 0;
|
|
530
|
-
index.docLengths.delete(filePath);
|
|
531
|
-
index.forward.delete(filePath);
|
|
532
|
-
index.fileMtimes.delete(filePath);
|
|
533
|
-
index.fileSizes.delete(filePath);
|
|
534
|
-
index.dirtyFiles?.add(wordIndexKey(filePath));
|
|
535
|
-
index.fileTable.release(removedKey);
|
|
536
|
-
index.totalTokens -= docLength;
|
|
537
|
-
index.docCount = Math.max(0, index.docCount - 1);
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
/**
|
|
541
|
-
* Add or replace `filePath`'s document in `index` in place: removes the prior
|
|
542
|
-
* postings for this file (if any, via {@link removeWordIndexDocument}'s
|
|
543
|
-
* forward-index lookup) then re-tokenizes `content` and adds the new
|
|
544
|
-
* postings/docLength/forward entry. df/N/totalTokens (avgdl) are updated as
|
|
545
|
-
* running stats — no full recompute over other documents.
|
|
546
|
-
*
|
|
547
|
-
* Returns `false` (no-op on `index`) when the index has no forward index —
|
|
548
|
-
* the caller must fall back to a full {@link buildWordIndex} rebuild in that
|
|
549
|
-
* case (documented at the `forward` field and enforced by callers, not
|
|
550
|
-
* silently patched here: a partially-forward-consistent index would corrupt
|
|
551
|
-
* future incremental updates).
|
|
552
|
-
*/
|
|
553
|
-
export function updateWordIndexDocument(index, doc) {
|
|
554
|
-
if (!index.forward)
|
|
555
|
-
return false;
|
|
556
|
-
const startedAt = Date.now();
|
|
557
|
-
// Remove the old contribution first (no-op if this is a brand new doc).
|
|
558
|
-
if (index.forward.has(doc.path)) {
|
|
559
|
-
removeWordIndexDocument(index, doc.path);
|
|
560
|
-
}
|
|
561
|
-
// Tokenize with line numbers attached (needed for WordHit.line) — this also
|
|
562
|
-
// yields the forward-index entry (distinct-line count per token) so the
|
|
563
|
-
// tokenization work happens exactly once for this document.
|
|
564
|
-
const lines = doc.content.split(/\r?\n/);
|
|
565
|
-
const perTokenHits = new Map();
|
|
566
|
-
let docLength = 0;
|
|
567
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
568
|
-
const lineTokens = tokenizeLine(lines[i]);
|
|
569
|
-
docLength += lineTokens.length;
|
|
570
|
-
const seenOnLine = new Set();
|
|
571
|
-
for (const token of lineTokens) {
|
|
572
|
-
if (seenOnLine.has(token))
|
|
573
|
-
continue;
|
|
574
|
-
seenOnLine.add(token);
|
|
575
|
-
const arr = perTokenHits.get(token);
|
|
576
|
-
if (arr)
|
|
577
|
-
arr.push(i + 1);
|
|
578
|
-
else
|
|
579
|
-
perTokenHits.set(token, [i + 1]);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
// Per-edit callers generally already have content but not a stat. -1 is an
|
|
583
|
-
// impossible real mtime, so it deliberately makes the document stale at the
|
|
584
|
-
// next startup refresh (`-1 !== realMtime` always) — unlike 0, which is a
|
|
585
|
-
// legal on-disk mtime (SOURCE_DATE_EPOCH=0, archive extraction) and would
|
|
586
|
-
// collide, leaving such a file never re-tokenized (#958 review F2).
|
|
587
|
-
commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt);
|
|
588
|
-
scheduleWordIndexRecompact(index, doc.path);
|
|
589
|
-
return true;
|
|
590
|
-
}
|
|
591
|
-
const asyncWordIndexOperations = new WeakMap();
|
|
592
|
-
function enqueueAsyncWordIndexOperation(index, operation) {
|
|
593
|
-
const previous = asyncWordIndexOperations.get(index) ?? Promise.resolve();
|
|
594
|
-
const run = previous.catch(() => undefined).then(operation);
|
|
595
|
-
const settled = run.then(() => undefined, () => undefined);
|
|
596
|
-
asyncWordIndexOperations.set(index, settled);
|
|
597
|
-
return run.finally(() => {
|
|
598
|
-
if (asyncWordIndexOperations.get(index) === settled) {
|
|
599
|
-
asyncWordIndexOperations.delete(index);
|
|
600
|
-
}
|
|
601
|
-
});
|
|
602
|
-
}
|
|
603
|
-
async function stageWordIndexDocumentRemoval(index, filePath, shouldContinue) {
|
|
604
|
-
if (!index.forward)
|
|
605
|
-
return undefined;
|
|
606
|
-
const tokenLineCounts = index.forward.get(filePath);
|
|
607
|
-
if (!tokenLineCounts)
|
|
608
|
-
return undefined;
|
|
609
|
-
const removedKey = wordIndexKey(filePath);
|
|
610
|
-
const removedId = index.fileTable.idFor(removedKey);
|
|
611
|
-
if (removedId === undefined)
|
|
612
|
-
return undefined;
|
|
613
|
-
const postings = new Map();
|
|
614
|
-
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
615
|
-
for (const token of tokenLineCounts.keys()) {
|
|
616
|
-
if (!shouldContinue())
|
|
617
|
-
throw new Error("word index refresh superseded");
|
|
618
|
-
const list = index.postings.get(token);
|
|
619
|
-
if (!list)
|
|
620
|
-
continue;
|
|
621
|
-
// One token's postings, filtered by the SAME packed primitive the
|
|
622
|
-
// synchronous path uses, so the staged result is identical rather than
|
|
623
|
-
// merely equivalent. The work unit is the token, not the posting element:
|
|
624
|
-
// a per-element deadline check cost one `performance.now()` per entry —
|
|
625
|
-
// on the order of a million per edit on a 2.2M-posting corpus — which is
|
|
626
|
-
// the tax that made this path unusable from the per-edit seam (#2067).
|
|
627
|
-
// One list's scan is a bounded, non-yieldable unit; the deadline is
|
|
628
|
-
// checked between lists.
|
|
629
|
-
const survivors = list.withoutFile(removedId);
|
|
630
|
-
postings.set(token, survivors.length > 0 ? survivors : undefined);
|
|
631
|
-
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
632
|
-
if (!shouldContinue())
|
|
633
|
-
throw new Error("word index refresh superseded");
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
return { postings, docLength: index.docLengths.get(filePath) ?? 0 };
|
|
637
|
-
}
|
|
638
|
-
function commitWordIndexDocumentRemoval(index, filePath, staged) {
|
|
639
|
-
for (const [token, list] of staged.postings) {
|
|
640
|
-
// Staged survivors come from `WordPostingList.withoutFile` — a fresh
|
|
641
|
-
// private store each, so the O(1) fragmentation gate must count them
|
|
642
|
-
// (#2117).
|
|
643
|
-
if (list) {
|
|
644
|
-
index.postings.set(token, list);
|
|
645
|
-
notePostingStoresAllocated(index, 1);
|
|
646
|
-
}
|
|
647
|
-
else
|
|
648
|
-
index.postings.delete(token);
|
|
649
|
-
}
|
|
650
|
-
index.docLengths.delete(filePath);
|
|
651
|
-
index.forward?.delete(filePath);
|
|
652
|
-
index.fileMtimes.delete(filePath);
|
|
653
|
-
index.fileSizes.delete(filePath);
|
|
654
|
-
index.dirtyFiles?.add(wordIndexKey(filePath));
|
|
655
|
-
index.fileTable.release(wordIndexKey(filePath));
|
|
656
|
-
index.totalTokens -= staged.docLength;
|
|
657
|
-
index.docCount = Math.max(0, index.docCount - 1);
|
|
658
|
-
}
|
|
659
|
-
/** Cooperative, atomically-published removal used only by bulk refresh. */
|
|
660
|
-
export async function removeWordIndexDocumentAsync(index, filePath, shouldContinue = () => true) {
|
|
661
|
-
return enqueueAsyncWordIndexOperation(index, async () => {
|
|
662
|
-
const staged = await stageWordIndexDocumentRemoval(index, filePath, shouldContinue);
|
|
663
|
-
if (!staged)
|
|
664
|
-
return false;
|
|
665
|
-
if (!shouldContinue())
|
|
666
|
-
throw new Error("word index refresh superseded");
|
|
667
|
-
commitWordIndexDocumentRemoval(index, filePath, staged);
|
|
668
|
-
return true;
|
|
669
|
-
});
|
|
670
|
-
}
|
|
671
|
-
/**
|
|
672
|
-
* The per-edit replacement primitive the cascade seam calls (#2067).
|
|
673
|
-
*
|
|
674
|
-
* `updateWordIndexDocumentAsync` plus the arena-recompaction gate the
|
|
675
|
-
* synchronous variant carries, which is what makes this the per-edit entry
|
|
676
|
-
* point rather than a rename. Bulk refresh must NOT use it: one refreshed
|
|
677
|
-
* document raises `postingStoreCount` by roughly its distinct-token count, so a
|
|
678
|
-
* per-document schedule would drive an O(vocabulary) recompaction between
|
|
679
|
-
* nearly every pair of documents. `refreshWordIndexIncrementally` therefore
|
|
680
|
-
* drives `updateWordIndexDocumentAsync` directly and recompacts ONCE after its
|
|
681
|
-
* loop; the per-edit seam has no such "after the loop" and schedules here.
|
|
682
|
-
*/
|
|
683
|
-
export async function updateWordIndexDocumentForEdit(index, doc) {
|
|
684
|
-
const updated = await updateWordIndexDocumentAsync(index, doc);
|
|
685
|
-
if (updated)
|
|
686
|
-
scheduleWordIndexRecompact(index, doc.path);
|
|
687
|
-
return updated;
|
|
688
|
-
}
|
|
689
|
-
/** Cooperative replacement whose old/new state is committed without an await. */
|
|
690
|
-
export async function updateWordIndexDocumentAsync(index, doc, shouldContinue = () => true) {
|
|
691
|
-
return enqueueAsyncWordIndexOperation(index, () => updateWordIndexDocumentAsyncUnsafe(index, doc, shouldContinue));
|
|
692
|
-
}
|
|
693
|
-
async function updateWordIndexDocumentAsyncUnsafe(index, doc, shouldContinue) {
|
|
694
|
-
if (!index.forward)
|
|
695
|
-
return false;
|
|
696
|
-
const startedAt = Date.now();
|
|
697
|
-
const removal = index.forward.has(doc.path)
|
|
698
|
-
? await stageWordIndexDocumentRemoval(index, doc.path, shouldContinue)
|
|
699
|
-
: undefined;
|
|
700
|
-
if (index.forward.has(doc.path) && !removal)
|
|
701
|
-
return false;
|
|
702
|
-
const perTokenHits = new Map();
|
|
703
|
-
let docLength = 0;
|
|
704
|
-
const lines = doc.content.split(/\r?\n/);
|
|
705
|
-
await forEachCooperatively(lines, (line, i) => {
|
|
706
|
-
const lineTokens = tokenizeLine(line);
|
|
707
|
-
docLength += lineTokens.length;
|
|
708
|
-
const seenOnLine = new Set();
|
|
709
|
-
for (const token of lineTokens) {
|
|
710
|
-
if (seenOnLine.has(token))
|
|
711
|
-
continue;
|
|
712
|
-
seenOnLine.add(token);
|
|
713
|
-
const arr = perTokenHits.get(token);
|
|
714
|
-
if (arr)
|
|
715
|
-
arr.push(i + 1);
|
|
716
|
-
else
|
|
717
|
-
perTokenHits.set(token, [i + 1]);
|
|
718
|
-
}
|
|
719
|
-
}, {
|
|
720
|
-
budgetMs: WORD_INDEX_BUILD_YIELD_BUDGET_MS,
|
|
721
|
-
shouldContinue,
|
|
722
|
-
abortMessage: "word index refresh superseded",
|
|
723
|
-
});
|
|
724
|
-
if (!shouldContinue())
|
|
725
|
-
throw new Error("word index refresh superseded");
|
|
726
|
-
if (removal)
|
|
727
|
-
commitWordIndexDocumentRemoval(index, doc.path, removal);
|
|
728
|
-
commitWordIndexDocumentReplacement(index, doc, perTokenHits, docLength, startedAt);
|
|
729
|
-
return true;
|
|
730
|
-
}
|
|
731
|
-
export const WORD_INDEX_MAX_BYTES = 512 * 1024;
|
|
732
|
-
/**
|
|
733
|
-
* Collect the bounded `{path, content}` doc set `buildWordIndex` consumes —
|
|
734
|
-
* the ONE file-walk-and-read implementation shared by every build path
|
|
735
|
-
* (session-start task, quick-mode warmup, cold-query background trigger),
|
|
736
|
-
* so a bound/skip-rule change lands in one place instead of three copies.
|
|
737
|
-
* `shouldContinue` lets a session-scoped caller abort early (session
|
|
738
|
-
* superseded) without this module knowing about RuntimeCoordinator.
|
|
739
|
-
*/
|
|
740
|
-
export async function collectWordIndexDocs(root, shouldContinue = () => true, preflightFiles) {
|
|
741
|
-
const { collectSourceFilesAsync } = await import("./source-filter.js");
|
|
742
|
-
// #747 hardening: pass the cap INTO the walk — without it,
|
|
743
|
-
// `collectSourceFilesAsync` defaults to an unbounded traversal and any
|
|
744
|
-
// slice below would only trim the result AFTER the whole tree (all of
|
|
745
|
-
// $HOME, on a misrooted cwd) has already been enumerated.
|
|
746
|
-
// #760: the walk is additionally bounded by source-filter's default
|
|
747
|
-
// visited-entry budget (DEFAULT_MAX_SCAN_ENTRIES), so a mixed tree with few
|
|
748
|
-
// source files among a huge pile of non-source files can't force a
|
|
749
|
-
// full-tree walk either; an index over the truncated list is acceptable.
|
|
750
|
-
const maxFiles = preflightFiles?.length ?? getWordIndexMaxFilesDerived(root);
|
|
751
|
-
// #894 review: prioritize code kinds within the cap — with broadened
|
|
752
|
-
// enumeration, thousands of data/doc files (locale JSON, fixtures, …)
|
|
753
|
-
// ahead of the code dirs in walk order could exhaust `maxFiles` and evict
|
|
754
|
-
// real source files from the index entirely (DOC_FILE_PENALTY can't
|
|
755
|
-
// rescue a file that never made the slice).
|
|
756
|
-
const files = preflightFiles
|
|
757
|
-
? preflightFiles.map((file) => file.path)
|
|
758
|
-
: await collectSourceFilesAsync(root, {
|
|
759
|
-
maxFiles,
|
|
760
|
-
prioritizeCodeKinds: true,
|
|
761
|
-
});
|
|
762
|
-
const truncated = preflightFiles?.truncated ?? files.length === maxFiles;
|
|
763
|
-
const docs = Object.assign([], { truncated, skipped: 0 });
|
|
764
|
-
if (!shouldContinue())
|
|
765
|
-
return docs;
|
|
766
|
-
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
767
|
-
for (const file of files.slice(0, maxFiles)) {
|
|
768
|
-
if (!shouldContinue())
|
|
769
|
-
return docs;
|
|
770
|
-
try {
|
|
771
|
-
// A preflight list saves the second full WALK, but its metadata may be
|
|
772
|
-
// stale by the time this rebuild reads the file. Re-stat each document at
|
|
773
|
-
// content-read time so the stored freshness stamp describes the bytes we
|
|
774
|
-
// actually index and the next incremental refresh cannot miss a change in
|
|
775
|
-
// the walk-to-read window (#1302).
|
|
776
|
-
const stat = fs.statSync(file);
|
|
777
|
-
if (stat.size <= WORD_INDEX_MAX_BYTES) {
|
|
778
|
-
docs.push({
|
|
779
|
-
path: file,
|
|
780
|
-
content: fs.readFileSync(file, "utf-8"),
|
|
781
|
-
mtimeMs: stat.mtimeMs,
|
|
782
|
-
size: stat.size,
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
else {
|
|
786
|
-
// Over the byte cap — enumerated but deliberately not indexed. Count
|
|
787
|
-
// it (L1, #958) so callers can keep coverage honest instead of the
|
|
788
|
-
// old silent drop.
|
|
789
|
-
docs.skipped += 1;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
catch {
|
|
793
|
-
// unreadable / vanished file — skip, but count it (see above).
|
|
794
|
-
docs.skipped += 1;
|
|
795
|
-
}
|
|
796
|
-
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
797
|
-
if (!shouldContinue())
|
|
798
|
-
return docs;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
return docs;
|
|
802
|
-
}
|
|
803
|
-
// Node's fs.promises.stat runs on libuv's threadpool (default 4 slots), so
|
|
804
|
-
// real parallelism tops out there; the surplus workers are queue depth that
|
|
805
|
-
// keeps the pool saturated without starving other threadpool consumers.
|
|
806
|
-
const WORD_INDEX_STAT_CONCURRENCY = 8;
|
|
807
|
-
const WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD = 0.3;
|
|
808
|
-
// A ratio alone misclassifies one stale file in a three-file project as dense.
|
|
809
|
-
// Below this absolute floor, per-document replacement is bounded and cheaper
|
|
810
|
-
// than a second full walk/read even when the percentage is high (#1197).
|
|
811
|
-
const WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS = 32;
|
|
812
|
-
/**
|
|
813
|
-
* Cost of one file's stat+read+decode expressed in tokenizer-token units, so
|
|
814
|
-
* the work model below can compare a path that re-reads EVERY file (full
|
|
815
|
-
* rebuild) against one that re-reads only the stale files (incremental) in a
|
|
816
|
-
* single currency. One read ≈ 300 tokens at the ~1 µs/token measured below.
|
|
817
|
-
* Order-of-magnitude by design — it only has to keep a tiny-vocabulary corpus
|
|
818
|
-
* (where posting scans are nearly free) from being sent to a full rebuild that
|
|
819
|
-
* re-reads everything to save a handful of array filters.
|
|
820
|
-
*/
|
|
821
|
-
const WORD_INDEX_FILE_READ_TOKEN_COST = 300;
|
|
822
|
-
/**
|
|
823
|
-
* Cost of scanning ONE posting-array element (the `wordIndexKey` compare in
|
|
824
|
-
* {@link removeWordIndexDocument}'s filter) relative to tokenizing one token,
|
|
825
|
-
* so both sides of the model below are denominated in tokens. Measured on this
|
|
826
|
-
* repository's own 2,062-document corpus: 213 ns per element scan against 979 ns
|
|
827
|
-
* per token — and the resulting predicted crossover (23 stale documents) lands
|
|
828
|
-
* on the directly measured one (~21, from 97.8 ms per document replacement
|
|
829
|
-
* against a 1,436 ms rebuild + re-read). The synthetic high-df corpus in the
|
|
830
|
-
* #1197 probe gives 0.09, so this is the conservative (incremental-favouring)
|
|
831
|
-
* end of the observed range.
|
|
832
|
-
*
|
|
833
|
-
* #2069 made the scan an Int32 compare over packed lanes instead of a string
|
|
834
|
-
* compare over boxed objects, so the real per-element cost is now BELOW this
|
|
835
|
-
* constant. The constant is left as measured: over-stating incremental cost
|
|
836
|
-
* only ever routes work to a full rebuild, which is the bounded side of the
|
|
837
|
-
* decision. Re-measuring it is #2067/#2068 territory, not this change.
|
|
838
|
-
*/
|
|
839
|
-
const WORD_INDEX_POSTING_SCAN_TOKEN_COST = 0.2;
|
|
840
|
-
/**
|
|
841
|
-
* Refresh a serializer-v2 index from the current bounded source-file set.
|
|
842
|
-
* The walk/stat pass is cheap; only sparse stale/new documents are read and
|
|
843
|
-
* tokenized. Expected dense/legacy states return `full-required` BEFORE any
|
|
844
|
-
* mutation; unexpected corruption or supersession still throws.
|
|
845
|
-
*/
|
|
846
|
-
export async function refreshWordIndexIncrementally(index, root, shouldContinue = () => true, options = {}) {
|
|
847
|
-
if (!index.forward || !index.fileMtimes || !index.fileSizes) {
|
|
848
|
-
return {
|
|
849
|
-
mode: "full-required",
|
|
850
|
-
reason: "missing-incremental-metadata",
|
|
851
|
-
timings: { sourceWalkMs: 0, statWalkMs: 0, refreshReadsMs: 0 },
|
|
852
|
-
};
|
|
853
|
-
}
|
|
854
|
-
const { collectSourceFilesAsync } = await import("./source-filter.js");
|
|
855
|
-
const maxFiles = getWordIndexMaxFilesDerived(root);
|
|
856
|
-
const sourceWalkStartMs = Date.now();
|
|
857
|
-
const walked = await collectSourceFilesAsync(root, {
|
|
858
|
-
maxFiles,
|
|
859
|
-
prioritizeCodeKinds: true,
|
|
860
|
-
});
|
|
861
|
-
const sourceWalkMs = Date.now() - sourceWalkStartMs;
|
|
862
|
-
if (!shouldContinue())
|
|
863
|
-
throw new Error("word index refresh superseded");
|
|
864
|
-
// This set-difference must run in the SAME normalized key space the path
|
|
865
|
-
// maps use (#1025 review). Otherwise a file whose stored displayPath became
|
|
866
|
-
// the edit form (last-writer-wins after a case/separator-divergent per-edit
|
|
867
|
-
// update) no longer string-matches its walk form here — even though
|
|
868
|
-
// `fileMtimes.get(walkForm)` folds and hits — which would (1) double-count
|
|
869
|
-
// churn (the same file scored as both "dropped" and "added"), possibly
|
|
870
|
-
// crossing the threshold and forcing a needless full rebuild, and (2) drop
|
|
871
|
-
// then re-add an unchanged file, opening a regression window because the drop
|
|
872
|
-
// loop runs BEFORE the re-read: a transient read failure (skipped++) would
|
|
873
|
-
// strand the file with no postings. Keying `current` and `oldSet` through
|
|
874
|
-
// `wordIndexKey` keeps build/edit/refresh convergent; `current`'s value
|
|
875
|
-
// retains the raw walk path for the stat/read and for the display key.
|
|
876
|
-
const current = new Map();
|
|
877
|
-
const statWalkStartMs = Date.now();
|
|
878
|
-
const statFile = options.statFile ?? ((file) => fs.promises.stat(file));
|
|
879
|
-
const requestedConcurrency = options.statConcurrency ?? WORD_INDEX_STAT_CONCURRENCY;
|
|
880
|
-
const statConcurrency = Number.isFinite(requestedConcurrency) && requestedConcurrency > 0
|
|
881
|
-
? Math.max(1, Math.floor(requestedConcurrency))
|
|
882
|
-
: WORD_INDEX_STAT_CONCURRENCY;
|
|
883
|
-
const statResults = [];
|
|
884
|
-
statResults.length = walked.length;
|
|
885
|
-
let cursor = 0;
|
|
886
|
-
let superseded = false;
|
|
887
|
-
const worker = async () => {
|
|
888
|
-
while (true) {
|
|
889
|
-
if (!shouldContinue()) {
|
|
890
|
-
superseded = true;
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
const slot = cursor++;
|
|
894
|
-
if (slot >= walked.length)
|
|
895
|
-
return;
|
|
896
|
-
const file = walked[slot];
|
|
897
|
-
try {
|
|
898
|
-
const stat = await statFile(file);
|
|
899
|
-
if (stat.size <= WORD_INDEX_MAX_BYTES) {
|
|
900
|
-
statResults[slot] = {
|
|
901
|
-
path: file,
|
|
902
|
-
mtimeMs: stat.mtimeMs,
|
|
903
|
-
size: stat.size,
|
|
904
|
-
};
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
catch {
|
|
908
|
-
// A rejection has statSync parity: the file is simply absent.
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
};
|
|
912
|
-
await Promise.all(Array.from({ length: Math.min(statConcurrency, walked.length) }, () => worker()));
|
|
913
|
-
if (superseded || !shouldContinue()) {
|
|
914
|
-
throw new Error("word index refresh superseded");
|
|
915
|
-
}
|
|
916
|
-
const statWalkMs = Date.now() - statWalkStartMs;
|
|
917
|
-
for (const result of statResults) {
|
|
918
|
-
if (result)
|
|
919
|
-
current.set(wordIndexKey(result.path), result);
|
|
920
|
-
}
|
|
921
|
-
const timings = { sourceWalkMs, statWalkMs, refreshReadsMs: 0 };
|
|
922
|
-
const preflightFiles = Object.assign([...current.values()], {
|
|
923
|
-
truncated: walked.length === maxFiles,
|
|
924
|
-
});
|
|
925
|
-
const oldSet = new Set([...index.docLengths.keys()].map(wordIndexKey));
|
|
926
|
-
let changedSet = 0;
|
|
927
|
-
for (const key of oldSet)
|
|
928
|
-
if (!current.has(key))
|
|
929
|
-
changedSet++;
|
|
930
|
-
for (const key of current.keys())
|
|
931
|
-
if (!oldSet.has(key))
|
|
932
|
-
changedSet++;
|
|
933
|
-
const denominator = Math.max(oldSet.size, current.size, 1);
|
|
934
|
-
if (changedSet / denominator > WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) {
|
|
935
|
-
return {
|
|
936
|
-
mode: "full-required",
|
|
937
|
-
reason: "file-set-churn",
|
|
938
|
-
preflightFiles,
|
|
939
|
-
timings,
|
|
940
|
-
};
|
|
941
|
-
}
|
|
942
|
-
// Replacing one document filters every shared posting array for that
|
|
943
|
-
// document's tokens. That is excellent for a sparse edit, but repeating it
|
|
944
|
-
// for most of the corpus becomes effectively quadratic. Decide the dense
|
|
945
|
-
// transition BEFORE dropping or editing anything so the old index remains a
|
|
946
|
-
// valid fallback until a separately-built full replacement is ready (#1197).
|
|
947
|
-
let staleDocuments = 0;
|
|
948
|
-
for (const { path: file, mtimeMs, size } of current.values()) {
|
|
949
|
-
if (index.fileMtimes.get(file) !== mtimeMs ||
|
|
950
|
-
(index.fileSizes.get(file) ?? -1) !== size) {
|
|
951
|
-
staleDocuments += 1;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
// Density alone is NOT a bound (#1197 review finding 1): the per-document cost
|
|
955
|
-
// GROWS with the corpus, so a stale set comfortably under any fixed ratio (or
|
|
956
|
-
// any fixed absolute ceiling) still costs orders of magnitude more than a
|
|
957
|
-
// rebuild on a big enough repository. 800 documents with 239 stale — 29.875%,
|
|
958
|
-
// just under the ratio — measured 90.6 s with a 39.6 s synchronous block
|
|
959
|
-
// against 1.3 s / 10.6 ms for a full build, and at the 6,000-file cap up to
|
|
960
|
-
// 1,799 stale documents would have stayed on that path.
|
|
961
|
-
//
|
|
962
|
-
// So compare estimated WORK, both sides denominated in tokenizer tokens:
|
|
963
|
-
// incremental ≈ stale x (docTokens x postingLength x scanCost + readCost)
|
|
964
|
-
// full ≈ totalTokens + corpusSize x readCost
|
|
965
|
-
// This is self-bounding in a way no constant is: the incremental path is taken
|
|
966
|
-
// only while its total estimated cost stays under ONE full rebuild, so the
|
|
967
|
-
// worst case is "about as expensive as rebuilding", whatever the corpus size.
|
|
968
|
-
//
|
|
969
|
-
// Both statistics come from cheap metadata passes — one over `postings`
|
|
970
|
-
// (distinct tokens, reading only `.length`) and one over `forward` (documents,
|
|
971
|
-
// reading only `.size`). Neither touches a posting ELEMENT, which is the work
|
|
972
|
-
// this decision exists to avoid.
|
|
973
|
-
let postingEntries = 0;
|
|
974
|
-
let weightedPostingEntries = 0;
|
|
975
|
-
for (const list of index.postings.values()) {
|
|
976
|
-
postingEntries += list.length;
|
|
977
|
-
weightedPostingEntries += list.length * list.length;
|
|
978
|
-
}
|
|
979
|
-
// The mean posting-array length weighted by OCCURRENCES, not the plain mean:
|
|
980
|
-
// a document's tokens are drawn from the frequency distribution, so the arrays
|
|
981
|
-
// a removal actually filters skew hard toward the high-document-frequency
|
|
982
|
-
// tokens. The unweighted mean (dominated by the long tail of df=1 tokens)
|
|
983
|
-
// underestimates the real cost by an order of magnitude.
|
|
984
|
-
const expectedPostingLength = postingEntries > 0 ? weightedPostingEntries / postingEntries : 0;
|
|
985
|
-
let distinctTokenEntries = 0;
|
|
986
|
-
for (const tokenLineCounts of index.forward.values()) {
|
|
987
|
-
distinctTokenEntries += tokenLineCounts.size;
|
|
988
|
-
}
|
|
989
|
-
const expectedDocumentTokens = index.docCount > 0 ? distinctTokenEntries / index.docCount : 0;
|
|
990
|
-
const estimatedIncrementalWork = staleDocuments *
|
|
991
|
-
(expectedDocumentTokens *
|
|
992
|
-
expectedPostingLength *
|
|
993
|
-
WORD_INDEX_POSTING_SCAN_TOKEN_COST +
|
|
994
|
-
WORD_INDEX_FILE_READ_TOKEN_COST);
|
|
995
|
-
const estimatedFullRebuildWork = index.totalTokens + current.size * WORD_INDEX_FILE_READ_TOKEN_COST;
|
|
996
|
-
// The document-count floor guards the RATIO test only — its documented job is
|
|
997
|
-
// that "one stale file in a three-file project" is 33% but not dense. It must
|
|
998
|
-
// NOT gate the work test: a floor is another constant, and letting one
|
|
999
|
-
// suppress the work comparison would reintroduce exactly the unbounded-on-the
|
|
1000
|
-
// -other-axis shape (31 documents at 98 ms each on this repository's own
|
|
1001
|
-
// corpus, and worse as the corpus grows). The work test needs no floor — on a
|
|
1002
|
-
// small project a full rebuild is cheap, so it never fires there.
|
|
1003
|
-
if ((staleDocuments >= WORD_INDEX_DENSE_REFRESH_MIN_DOCUMENTS &&
|
|
1004
|
-
staleDocuments / Math.max(current.size, 1) >
|
|
1005
|
-
WORD_INDEX_INCREMENTAL_CHURN_THRESHOLD) ||
|
|
1006
|
-
estimatedIncrementalWork > estimatedFullRebuildWork) {
|
|
1007
|
-
return {
|
|
1008
|
-
mode: "full-required",
|
|
1009
|
-
reason: "stale-document-churn",
|
|
1010
|
-
preflightFiles,
|
|
1011
|
-
timings,
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
const deadline = createDeadline(WORD_INDEX_BUILD_YIELD_BUDGET_MS);
|
|
1015
|
-
let dropped = 0;
|
|
1016
|
-
for (const key of oldSet) {
|
|
1017
|
-
if (!current.has(key)) {
|
|
1018
|
-
// `key` is already folded; removeWordIndexDocument re-folds it
|
|
1019
|
-
// idempotently via the PathKeyedMap, so the drop hits the right entry.
|
|
1020
|
-
if (!(await removeWordIndexDocumentAsync(index, key, shouldContinue))) {
|
|
1021
|
-
throw new Error(`failed to drop word-index document: ${key}`);
|
|
1022
|
-
}
|
|
1023
|
-
dropped++;
|
|
1024
|
-
}
|
|
1025
|
-
}
|
|
1026
|
-
let refreshed = 0;
|
|
1027
|
-
let skipped = 0;
|
|
1028
|
-
const refreshReadsStartMs = Date.now();
|
|
1029
|
-
for (const { path: file, mtimeMs, size } of current.values()) {
|
|
1030
|
-
// #1105: mtime-first, size-second freshness — mtime alone is a stale
|
|
1031
|
-
// signal (mtime-preserving content changes serve stale identifiers to
|
|
1032
|
-
// symbol_search). Size is free (the stat above already read it) and
|
|
1033
|
-
// catches every content change that alters byte length. Both come from the
|
|
1034
|
-
// SAME stat, so a mismatch on EITHER axis re-reads. `?? -1` makes a file
|
|
1035
|
-
// absent from the (possibly legacy, pre-#1105) size map always count as
|
|
1036
|
-
// changed → one-time re-read that repopulates the size, the safe direction.
|
|
1037
|
-
if (index.fileMtimes.get(file) !== mtimeMs ||
|
|
1038
|
-
(index.fileSizes.get(file) ?? -1) !== size) {
|
|
1039
|
-
// A file the walk/stat pass saw can still fail to read here — a
|
|
1040
|
-
// transient exclusive lock (antivirus, an editor, a build step) or a
|
|
1041
|
-
// file that vanished in the interim. Match collectWordIndexDocs'
|
|
1042
|
-
// tolerance: skip this one file and leave its existing posting (and
|
|
1043
|
-
// its old mtime, so it is retried next session) rather than aborting
|
|
1044
|
-
// the whole incremental pass into a full rebuild (#958 review F1).
|
|
1045
|
-
let content;
|
|
1046
|
-
try {
|
|
1047
|
-
content = fs.readFileSync(file, "utf-8");
|
|
1048
|
-
}
|
|
1049
|
-
catch {
|
|
1050
|
-
skipped++;
|
|
1051
|
-
continue;
|
|
1052
|
-
}
|
|
1053
|
-
if (!(await updateWordIndexDocumentAsync(index, { path: file, content }, shouldContinue))) {
|
|
1054
|
-
throw new Error(`failed to refresh word-index document: ${file}`);
|
|
1055
|
-
}
|
|
1056
|
-
// updateWordIndexDocument stamps mtime=-1 (per-edit convention); the
|
|
1057
|
-
// refresh path KNOWS the real on-disk stat, so overwrite both axes with
|
|
1058
|
-
// the true values so this document is not needlessly re-read next pass.
|
|
1059
|
-
index.fileMtimes.set(file, mtimeMs);
|
|
1060
|
-
index.fileSizes.set(file, size);
|
|
1061
|
-
refreshed++;
|
|
1062
|
-
}
|
|
1063
|
-
// Same OR as the stat loop. This is the loop the #1197 outage actually
|
|
1064
|
-
// blocked in: replacing one document costs more the larger the corpus is,
|
|
1065
|
-
// so a count-only checkpoint let 100 replacements run back to back for
|
|
1066
|
-
// seconds. The gate above keeps the stale set sparse; this keeps even a
|
|
1067
|
-
// sparse-but-expensive set off the event loop.
|
|
1068
|
-
if (deadline.expired() && (await yieldIfOverBudget(deadline))) {
|
|
1069
|
-
if (!shouldContinue())
|
|
1070
|
-
throw new Error("word index refresh superseded");
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
timings.refreshReadsMs = Date.now() - refreshReadsStartMs;
|
|
1074
|
-
// Route through the same guarded queue the per-edit seam uses, so a refresh
|
|
1075
|
-
// and a concurrent cascade edit cannot both drive a recompaction (#2117).
|
|
1076
|
-
await enqueueWordIndexRecompact(index, root);
|
|
1077
|
-
index.truncated = walked.length === maxFiles;
|
|
1078
|
-
return {
|
|
1079
|
-
mode: "incremental",
|
|
1080
|
-
refreshed,
|
|
1081
|
-
dropped,
|
|
1082
|
-
skipped,
|
|
1083
|
-
reused: current.size - refreshed - skipped,
|
|
1084
|
-
timings,
|
|
1085
|
-
};
|
|
1086
|
-
}
|
|
1087
|
-
const WORD_INDEX_QUERY_FILTER_KEYS = [
|
|
1088
|
-
"lang",
|
|
1089
|
-
"file",
|
|
1090
|
-
"ext",
|
|
1091
|
-
];
|
|
1092
|
-
/** Thrown by {@link parseWordIndexQuery}/{@link buildWordIndexQueryFilter} for
|
|
1093
|
-
* an unrecognized `key:` prefix or an unrecognized `lang:` value — a query
|
|
1094
|
-
* typo fails loudly with the supported list instead of silently degrading
|
|
1095
|
-
* into a literal search term (#1450 acceptance criterion). */
|
|
1096
|
-
export class WordIndexQueryError extends Error {
|
|
1097
|
-
constructor(message) {
|
|
1098
|
-
super(message);
|
|
1099
|
-
this.name = "WordIndexQueryError";
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
const WORD_INDEX_FILTER_TOKEN_RE = /^(-)?([A-Za-z][A-Za-z0-9_-]*):(.+)$/;
|
|
1103
|
-
/**
|
|
1104
|
-
* Split a word-index query string into plain search terms and `key:value`
|
|
1105
|
-
* prefix filters (#1450). Whitespace-delimited, one regex per token — no
|
|
1106
|
-
* quoting scheme invented here, matching the pre-existing query path (which
|
|
1107
|
-
* never supported quoting either). A `-` immediately before a recognized
|
|
1108
|
-
* `key:` negates that filter; a bare `-term` (no colon) is left as an
|
|
1109
|
-
* ordinary token, unchanged from prior behavior — `tokenizeLine` already
|
|
1110
|
-
* strips the leading `-` when it extracts identifiers.
|
|
1111
|
-
*
|
|
1112
|
-
* An unrecognized `key:` (e.g. `type:foo`) throws {@link WordIndexQueryError}
|
|
1113
|
-
* naming the supported prefixes, rather than falling through as a literal
|
|
1114
|
-
* term — a typo must fail loudly, never silently rank as if the filter never
|
|
1115
|
-
* existed. An empty value (`lang:`) is not treated as a filter token at all
|
|
1116
|
-
* (kept as a term) since there is nothing to filter by.
|
|
1117
|
-
*/
|
|
1118
|
-
export function parseWordIndexQuery(query) {
|
|
1119
|
-
const filters = [];
|
|
1120
|
-
const termParts = [];
|
|
1121
|
-
for (const raw of query.split(/\s+/)) {
|
|
1122
|
-
if (!raw)
|
|
1123
|
-
continue;
|
|
1124
|
-
const match = raw.match(WORD_INDEX_FILTER_TOKEN_RE);
|
|
1125
|
-
if (!match) {
|
|
1126
|
-
termParts.push(raw);
|
|
1127
|
-
continue;
|
|
1128
|
-
}
|
|
1129
|
-
const [, negation, keyRaw, value] = match;
|
|
1130
|
-
const key = keyRaw.toLowerCase();
|
|
1131
|
-
if (!value) {
|
|
1132
|
-
termParts.push(raw);
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
if (!WORD_INDEX_QUERY_FILTER_KEYS.includes(key)) {
|
|
1136
|
-
// Not a recognized filter key. Ordinary search terms legitimately
|
|
1137
|
-
// contain colons (std::vector, error:foo, http://…, C:\ paths, a
|
|
1138
|
-
// TODO:tag), and the old tokenizer searched them fine — a hard
|
|
1139
|
-
// error here is a usability regression, not typo protection. The
|
|
1140
|
-
// token passes through as a plain term; the loud-failure contract
|
|
1141
|
-
// survives where it is unambiguous: a KNOWN key with a bad value
|
|
1142
|
-
// (lang:notalang) still throws.
|
|
1143
|
-
termParts.push(raw);
|
|
1144
|
-
continue;
|
|
1145
|
-
}
|
|
1146
|
-
filters.push({
|
|
1147
|
-
key: key,
|
|
1148
|
-
value,
|
|
1149
|
-
negated: negation === "-",
|
|
1150
|
-
});
|
|
1151
|
-
}
|
|
1152
|
-
return { terms: termParts.join(" "), filters };
|
|
1153
|
-
}
|
|
1154
|
-
/** `lang:X` → the extension set for FileKind X (case-insensitive), drawn
|
|
1155
|
-
* exclusively from `KIND_EXTENSIONS` (clients/file-kinds.ts) — the single
|
|
1156
|
-
* source of truth (#894 invariant: no second, hand-maintained language list).
|
|
1157
|
-
* An unrecognized kind throws {@link WordIndexQueryError} listing the accepted
|
|
1158
|
-
* kinds (KIND_EXTENSIONS' own keys), the same loud-failure contract as an
|
|
1159
|
-
* unsupported `key:` prefix. */
|
|
1160
|
-
function resolveLangExtensions(value) {
|
|
1161
|
-
const kind = value.toLowerCase();
|
|
1162
|
-
const extensions = KIND_EXTENSIONS[kind];
|
|
1163
|
-
if (!extensions) {
|
|
1164
|
-
const known = Object.keys(KIND_EXTENSIONS)
|
|
1165
|
-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
|
|
1166
|
-
.join(", ");
|
|
1167
|
-
throw new WordIndexQueryError(`Unknown lang: "${value}" in word-index query — supported languages: ${known}.`);
|
|
1168
|
-
}
|
|
1169
|
-
return extensions;
|
|
1170
|
-
}
|
|
1171
|
-
/** `ext:ts` / `ext:.ts` → normalized to a single leading-dot, lowercased form for extname comparison. */
|
|
1172
|
-
function normalizeExtFilterValue(value) {
|
|
1173
|
-
return `.${value.replace(/^\.+/, "").toLowerCase()}`;
|
|
1174
|
-
}
|
|
1175
|
-
function resolveWordIndexFilter(filter) {
|
|
1176
|
-
if (filter.key === "lang") {
|
|
1177
|
-
const extensions = resolveLangExtensions(filter.value);
|
|
1178
|
-
return {
|
|
1179
|
-
key: filter.key,
|
|
1180
|
-
negated: filter.negated,
|
|
1181
|
-
test: (file) => extensions.includes(path.extname(file).toLowerCase()),
|
|
1182
|
-
};
|
|
1183
|
-
}
|
|
1184
|
-
if (filter.key === "ext") {
|
|
1185
|
-
const normalized = normalizeExtFilterValue(filter.value);
|
|
1186
|
-
return {
|
|
1187
|
-
key: filter.key,
|
|
1188
|
-
negated: filter.negated,
|
|
1189
|
-
test: (file) => path.extname(file).toLowerCase() === normalized,
|
|
1190
|
-
};
|
|
1191
|
-
}
|
|
1192
|
-
// "file": substring match against the index's own normalized display path
|
|
1193
|
-
// (#1450 — see the doc comment on {@link buildWordIndexQueryFilter} for the
|
|
1194
|
-
// case-behavior choice). Both sides fold through `wordIndexKey` so the
|
|
1195
|
-
// comparison matches the SAME normalization the index's own path keys use.
|
|
1196
|
-
const needle = wordIndexKey(filter.value);
|
|
1197
|
-
return {
|
|
1198
|
-
key: filter.key,
|
|
1199
|
-
negated: filter.negated,
|
|
1200
|
-
test: (_file, displayPath) => displayPath.includes(needle),
|
|
1201
|
-
};
|
|
1202
|
-
}
|
|
1203
|
-
/**
|
|
1204
|
-
* Build a pre-ranking predicate from parsed query filters (#1450). Composes
|
|
1205
|
-
* (AND) with `searchWordIndex`'s pre-existing `fileFilter` option (#771) —
|
|
1206
|
-
* `symbol_search`'s structured `paths`/`lang` params and this query's inline
|
|
1207
|
-
* filters both apply when both are present.
|
|
1208
|
-
*
|
|
1209
|
-
* Semantics: multiple positive filters of the SAME key OR together
|
|
1210
|
-
* (`lang:ts lang:go` matches either); filters of DIFFERENT keys AND
|
|
1211
|
-
* (`lang:ts file:clients/` requires both); a negated filter always subtracts,
|
|
1212
|
-
* regardless of any positive filter sharing its key.
|
|
1213
|
-
*
|
|
1214
|
-
* All filters are RESOLVED EAGERLY (extension lookups, `WordIndexQueryError`
|
|
1215
|
-
* thrown) here at build time — before any candidate file is tested — so a bad
|
|
1216
|
-
* `lang:` value fails once, loudly, rather than per-file during the scoring
|
|
1217
|
-
* loop.
|
|
1218
|
-
*
|
|
1219
|
-
* `file:` case behavior: matched via `wordIndexKey` (this module's single
|
|
1220
|
-
* path-key normalizer, `normalizeEphemeralMapKey`) on BOTH the candidate path
|
|
1221
|
-
* and the filter value — slash-folded everywhere, and case-folded ONLY on
|
|
1222
|
-
* win32 (Windows' case-insensitive filesystem), preserved on POSIX. This
|
|
1223
|
-
* mirrors the SAME normalization the word index's own path-keyed maps already
|
|
1224
|
-
* apply (#1025), rather than inventing a second, divergent case rule for this
|
|
1225
|
-
* one filter. The candidate path matched is the RAW index-stored path (not
|
|
1226
|
-
* cwd-relativized) — the index already stores paths in the form the caller
|
|
1227
|
-
* built it with (relative in tests, absolute in the real project walk), and
|
|
1228
|
-
* an absolute path's tail always contains its project-relative suffix, so
|
|
1229
|
-
* `file:clients/word-index.ts` still matches naturally either way.
|
|
1230
|
-
*/
|
|
1231
|
-
export function buildWordIndexQueryFilter(filters) {
|
|
1232
|
-
if (filters.length === 0)
|
|
1233
|
-
return undefined;
|
|
1234
|
-
const resolved = filters.map(resolveWordIndexFilter);
|
|
1235
|
-
const positivesByKey = new Map();
|
|
1236
|
-
const negatives = [];
|
|
1237
|
-
for (const r of resolved) {
|
|
1238
|
-
if (r.negated) {
|
|
1239
|
-
negatives.push(r);
|
|
1240
|
-
continue;
|
|
1241
|
-
}
|
|
1242
|
-
const group = positivesByKey.get(r.key) ?? [];
|
|
1243
|
-
group.push(r);
|
|
1244
|
-
positivesByKey.set(r.key, group);
|
|
1245
|
-
}
|
|
1246
|
-
return (file) => {
|
|
1247
|
-
const displayPath = wordIndexKey(file);
|
|
1248
|
-
for (const negative of negatives) {
|
|
1249
|
-
if (negative.test(file, displayPath))
|
|
1250
|
-
return false;
|
|
1251
|
-
}
|
|
1252
|
-
for (const group of positivesByKey.values()) {
|
|
1253
|
-
if (!group.some((r) => r.test(file, displayPath)))
|
|
1254
|
-
return false;
|
|
1255
|
-
}
|
|
1256
|
-
return true;
|
|
1257
|
-
};
|
|
1258
|
-
}
|
|
1259
|
-
function combineFileFilters(a, b) {
|
|
1260
|
-
if (!a)
|
|
1261
|
-
return b;
|
|
1262
|
-
if (!b)
|
|
1263
|
-
return a;
|
|
1264
|
-
return (file) => a(file) && b(file);
|
|
1265
|
-
}
|
|
1266
|
-
/**
|
|
1267
|
-
* Rank files for a query by BM25 over the query's identifier tokens, then apply
|
|
1268
|
-
* priors: demote test/vendor and doc/data files, and boost by graph centrality
|
|
1269
|
-
* when supplied. Returns the top {@link RankOptions.limit} files, highest first.
|
|
1270
|
-
*
|
|
1271
|
-
* The query string may mix plain terms with `lang:`/`file:`/`ext:` prefix
|
|
1272
|
-
* filters (+ `-` negation, #1450) — parsed by {@link parseWordIndexQuery} and
|
|
1273
|
-
* applied as a `fileFilter` (composed, AND, with any caller-supplied one)
|
|
1274
|
-
* BEFORE scoring, same as the pre-existing `paths`/`lang` options (#771): a
|
|
1275
|
-
* surviving file's score is unaffected by filtering. A query with no filter
|
|
1276
|
-
* tokens reproduces prior output byte-for-byte.
|
|
1277
|
-
*
|
|
1278
|
-
* DF-normalization note: BM25's per-token `idf` is computed from `docFrequency`
|
|
1279
|
-
* over the FULL (unfiltered) postings for that token — `fileFilter` (whether
|
|
1280
|
-
* from `options.fileFilter` or from this query's inline filters) is applied
|
|
1281
|
-
* AFTER `idf` is computed, per file, inside the same loop. This means a
|
|
1282
|
-
* filtered query reuses the GLOBAL, corpus-wide document frequency rather than
|
|
1283
|
-
* recomputing it over just the filtered subset. That is the pre-existing #771
|
|
1284
|
-
* behavior this change does not alter; it is an acceptable approximation
|
|
1285
|
-
* (idf reflects true corpus rarity, not an artifact of the filter) and keeps
|
|
1286
|
-
* filtered/unfiltered scores for the SAME file directly comparable, which is
|
|
1287
|
-
* the property #1450 asks for ("BM25 and centrality stay comparable within
|
|
1288
|
-
* the filtered set").
|
|
1289
|
-
*/
|
|
1290
|
-
export function searchWordIndex(index, query, options = {}) {
|
|
1291
|
-
const { demoteTestVendor = true, demoteDocs = true, centrality, limit = 20, fileFilter, } = options;
|
|
1292
|
-
const parsedQuery = parseWordIndexQuery(query);
|
|
1293
|
-
const queryFilter = buildWordIndexQueryFilter(parsedQuery.filters);
|
|
1294
|
-
const combinedFilter = combineFileFilters(fileFilter, queryFilter);
|
|
1295
|
-
const queryTokens = [...new Set(tokenizeLine(parsedQuery.terms))];
|
|
1296
|
-
if (queryTokens.length === 0)
|
|
1297
|
-
return [];
|
|
1298
|
-
const docCount = index.docCount || 1;
|
|
1299
|
-
const avgDocLength = index.totalTokens / docCount || 1;
|
|
1300
|
-
const scores = new Map();
|
|
1301
|
-
for (const token of queryTokens) {
|
|
1302
|
-
const posting = index.postings.get(token);
|
|
1303
|
-
if (!posting)
|
|
1304
|
-
continue;
|
|
1305
|
-
// Grouped by the packed file id, then resolved to the display path once
|
|
1306
|
-
// per (token, file) instead of once per posting (#2069). Insertion order,
|
|
1307
|
-
// document frequency, and the per-file line lists are identical to the
|
|
1308
|
-
// boxed representation, so scores and ordering are unchanged.
|
|
1309
|
-
const linesByFileId = new Map();
|
|
1310
|
-
for (let i = 0; i < posting.length; i += 1) {
|
|
1311
|
-
const fileId = posting.fileIdAt(i);
|
|
1312
|
-
const arr = linesByFileId.get(fileId);
|
|
1313
|
-
if (arr)
|
|
1314
|
-
arr.push(posting.lineAt(i));
|
|
1315
|
-
else
|
|
1316
|
-
linesByFileId.set(fileId, [posting.lineAt(i)]);
|
|
1317
|
-
}
|
|
1318
|
-
const docFrequency = linesByFileId.size;
|
|
1319
|
-
const idf = Math.log(1 + (docCount - docFrequency + 0.5) / (docFrequency + 0.5));
|
|
1320
|
-
for (const [fileId, lines] of linesByFileId) {
|
|
1321
|
-
const file = index.fileTable.pathFor(fileId);
|
|
1322
|
-
if (file === undefined) {
|
|
1323
|
-
// Dropping this silently would shorten the result list with nothing to
|
|
1324
|
-
// tell it apart from a smaller match set. Record, then drop.
|
|
1325
|
-
recordOrphanWordIndexFileId(fileId, token, "search");
|
|
1326
|
-
continue;
|
|
1327
|
-
}
|
|
1328
|
-
if (combinedFilter && !combinedFilter(file))
|
|
1329
|
-
continue;
|
|
1330
|
-
const termFrequency = lines.length;
|
|
1331
|
-
const docLength = index.docLengths.get(file) ?? avgDocLength;
|
|
1332
|
-
const denominator = termFrequency +
|
|
1333
|
-
BM25_K1 * (1 - BM25_B + BM25_B * (docLength / avgDocLength));
|
|
1334
|
-
const termScore = idf * ((termFrequency * (BM25_K1 + 1)) / denominator);
|
|
1335
|
-
const entry = scores.get(file) ?? {
|
|
1336
|
-
score: 0,
|
|
1337
|
-
hits: 0,
|
|
1338
|
-
lines: new Set(),
|
|
1339
|
-
};
|
|
1340
|
-
entry.score += termScore;
|
|
1341
|
-
entry.hits += termFrequency;
|
|
1342
|
-
for (const line of lines)
|
|
1343
|
-
entry.lines.add(line);
|
|
1344
|
-
scores.set(file, entry);
|
|
1345
|
-
}
|
|
1346
|
-
}
|
|
1347
|
-
const results = [];
|
|
1348
|
-
for (const [file, entry] of scores) {
|
|
1349
|
-
let score = entry.score;
|
|
1350
|
-
if (demoteTestVendor && isTestOrVendor(file))
|
|
1351
|
-
score *= TEST_VENDOR_PENALTY;
|
|
1352
|
-
if (demoteDocs && isDocFile(file))
|
|
1353
|
-
score *= DOC_FILE_PENALTY;
|
|
1354
|
-
const connections = centrality?.get(file);
|
|
1355
|
-
if (connections && connections > 0) {
|
|
1356
|
-
score *= 1 + Math.log(1 + connections) / 4;
|
|
1357
|
-
}
|
|
1358
|
-
results.push({
|
|
1359
|
-
file,
|
|
1360
|
-
score,
|
|
1361
|
-
hits: entry.hits,
|
|
1362
|
-
lines: [...entry.lines].sort((a, b) => a - b),
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
results.sort((a, b) => b.score - a.score || a.file.localeCompare(b.file));
|
|
1366
|
-
return results.slice(0, Math.max(0, limit));
|
|
1367
|
-
}
|
|
1368
|
-
/**
|
|
1369
|
-
* Build a centrality map (file → importedBy count) keyed by THIS index's file
|
|
1370
|
-
* paths, from the project snapshot's `reverseDeps` (importedBy). The snapshot
|
|
1371
|
-
* keys are normalized (`normalizeMapKey(resolve(...))`) while the index keys are
|
|
1372
|
-
* the raw scanned paths, so the caller injects a `normalizeKey` bridge; it
|
|
1373
|
-
* defaults to identity for testing. Pass the result to {@link searchWordIndex}
|
|
1374
|
-
* as `centrality` to boost well-connected files. Kept here (not in the engine)
|
|
1375
|
-
* so it stays pure + unit-testable without the normalizer dependency.
|
|
1376
|
-
*/
|
|
1377
|
-
export function centralityFromReverseDeps(index, reverseDeps, normalizeKey = (file) => file) {
|
|
1378
|
-
const centrality = new Map();
|
|
1379
|
-
if (!reverseDeps)
|
|
1380
|
-
return centrality;
|
|
1381
|
-
for (const file of index.docLengths.keys()) {
|
|
1382
|
-
const importers = reverseDeps[normalizeKey(file)];
|
|
1383
|
-
if (importers && importers.length > 0) {
|
|
1384
|
-
centrality.set(file, importers.length);
|
|
1385
|
-
}
|
|
1386
|
-
}
|
|
1387
|
-
return centrality;
|
|
1388
|
-
}
|
|
1389
|
-
/** Persisted word-index serialization format version. Bump on breaking format changes. */
|
|
1390
|
-
export const WORD_INDEX_FORMAT_VERSION = 2;
|
|
1391
|
-
const WORD_INDEX_INT32_MAX = 2_147_483_647;
|
|
1392
|
-
// A file id and line fit in one exact Number key through this boundary.
|
|
1393
|
-
const WORD_INDEX_PACKED_FILE_ID_MAX = 4_194_303;
|
|
1394
|
-
function isCanonicalWordIndexNumber(value) {
|
|
1395
|
-
return (typeof value === "number" &&
|
|
1396
|
-
Number.isFinite(value) &&
|
|
1397
|
-
Number.isSafeInteger(value) &&
|
|
1398
|
-
!Object.is(value, -0));
|
|
1399
|
-
}
|
|
1400
|
-
function isCanonicalWordIndexToken(token) {
|
|
1401
|
-
return (typeof token === "string" &&
|
|
1402
|
-
token.length >= 2 &&
|
|
1403
|
-
token === token.toLowerCase() &&
|
|
1404
|
-
/^[a-z0-9_$]+$/.test(token) &&
|
|
1405
|
-
!STOPWORDS.has(token));
|
|
1406
|
-
}
|
|
1407
|
-
const serializedWordIndexCaches = new WeakMap();
|
|
1408
|
-
let _lastSerializeWork;
|
|
1409
|
-
/** Test-only: work stats for the most recent {@link serializeWordIndex} call. */
|
|
1410
|
-
export function getLastWordIndexSerializeWork() {
|
|
1411
|
-
return _lastSerializeWork;
|
|
1412
|
-
}
|
|
1413
|
-
function serializeWordIndexFull(index) {
|
|
1414
|
-
const files = [...index.docLengths.keys()];
|
|
1415
|
-
// `files` carries whatever spelling `docLengths` last stored, which can
|
|
1416
|
-
// differ from the spelling the file table interned. Both fold through
|
|
1417
|
-
// `wordIndexKey`, so the slot lookup runs in the folded key space and a
|
|
1418
|
-
// separator- or case-divergent spelling still finds its postings (#1025).
|
|
1419
|
-
const slotByFileId = new Map();
|
|
1420
|
-
files.forEach((file, i) => {
|
|
1421
|
-
const fileId = index.fileTable.idFor(wordIndexKey(file));
|
|
1422
|
-
if (fileId !== undefined)
|
|
1423
|
-
slotByFileId.set(fileId, i);
|
|
1424
|
-
});
|
|
1425
|
-
const postings = [];
|
|
1426
|
-
for (const [token, list] of index.postings) {
|
|
1427
|
-
const flat = [];
|
|
1428
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
1429
|
-
const slot = slotByFileId.get(list.fileIdAt(i));
|
|
1430
|
-
if (slot === undefined)
|
|
1431
|
-
continue;
|
|
1432
|
-
flat.push(slot, list.lineAt(i));
|
|
1433
|
-
}
|
|
1434
|
-
if (flat.length > 0)
|
|
1435
|
-
postings.push([token, flat]);
|
|
1436
|
-
}
|
|
1437
|
-
const forward = index.forward
|
|
1438
|
-
? files.map((file, i) => [
|
|
1439
|
-
i,
|
|
1440
|
-
[...(index.forward.get(file)?.entries() ?? [])],
|
|
1441
|
-
])
|
|
1442
|
-
: undefined;
|
|
1443
|
-
const serialized = {
|
|
1444
|
-
version: WORD_INDEX_FORMAT_VERSION,
|
|
1445
|
-
files,
|
|
1446
|
-
postings,
|
|
1447
|
-
docLengths: files.map((file) => index.docLengths.get(file) ?? 0),
|
|
1448
|
-
totalTokens: index.totalTokens,
|
|
1449
|
-
indexedFileCount: index.docCount,
|
|
1450
|
-
truncated: index.truncated,
|
|
1451
|
-
fileMtimes: files.map((file) => index.fileMtimes.get(file) ?? 0),
|
|
1452
|
-
fileSizes: files.map((file) => index.fileSizes.get(file) ?? 0),
|
|
1453
|
-
forward,
|
|
1454
|
-
};
|
|
1455
|
-
const tokensByFile = new Map();
|
|
1456
|
-
if (index.forward) {
|
|
1457
|
-
for (const file of files) {
|
|
1458
|
-
tokensByFile.set(wordIndexKey(file), new Set(index.forward.get(file)?.keys() ?? []));
|
|
1459
|
-
}
|
|
1460
|
-
}
|
|
1461
|
-
_lastSerializeWork = {
|
|
1462
|
-
affectedTokenCount: postings.length,
|
|
1463
|
-
tookFullPath: true,
|
|
1464
|
-
};
|
|
1465
|
-
return { serialized, slotByFileId, tokensByFile };
|
|
1466
|
-
}
|
|
1467
|
-
/**
|
|
1468
|
-
* Refresh the cached wire view without walking untouched posting lanes.
|
|
1469
|
-
* Existing files keep their slots, so a document replacement only rebuilds
|
|
1470
|
-
* the token lists named by that document's old or new forward entry. A file
|
|
1471
|
-
* removal or reordering changes slot identity and deliberately takes the
|
|
1472
|
-
* bounded full path.
|
|
1473
|
-
*/
|
|
1474
|
-
function serializeWordIndexIncrementally(index, cache) {
|
|
1475
|
-
const files = [...index.docLengths.keys()];
|
|
1476
|
-
const priorFiles = cache.serialized.files;
|
|
1477
|
-
const currentKeys = new Set(files.map(wordIndexKey));
|
|
1478
|
-
const priorKeys = new Set(priorFiles.map(wordIndexKey));
|
|
1479
|
-
if ([...priorKeys].some((key) => !currentKeys.has(key))) {
|
|
1480
|
-
return serializeWordIndexFull(index);
|
|
1481
|
-
}
|
|
1482
|
-
// Re-flattening most lanes costs more than rebuilding the compact wire view.
|
|
1483
|
-
// The review fixture crosses over between 10% and 100% dirty, so use the
|
|
1484
|
-
// midpoint as a conservative bound until a workload-specific model exists.
|
|
1485
|
-
const dirty = index.dirtyFiles;
|
|
1486
|
-
if (dirty && dirty.size * 2 > files.length) {
|
|
1487
|
-
return serializeWordIndexFull(index);
|
|
1488
|
-
}
|
|
1489
|
-
// A replacement deletes and re-adds a PathKeyedMap entry, which changes Map
|
|
1490
|
-
// insertion order. Keep the cached wire slots stable instead of rewriting
|
|
1491
|
-
// every posting just because one document changed order.
|
|
1492
|
-
const filesInWireOrder = [
|
|
1493
|
-
...priorFiles,
|
|
1494
|
-
...files.filter((file) => !priorKeys.has(wordIndexKey(file))),
|
|
1495
|
-
];
|
|
1496
|
-
if (!dirty || dirty.size === 0) {
|
|
1497
|
-
_lastSerializeWork = { affectedTokenCount: 0, tookFullPath: false };
|
|
1498
|
-
return cache;
|
|
1499
|
-
}
|
|
1500
|
-
const slotByFileId = new Map(cache.slotByFileId);
|
|
1501
|
-
for (let i = priorFiles.length; i < filesInWireOrder.length; i += 1) {
|
|
1502
|
-
const fileId = index.fileTable.idFor(wordIndexKey(filesInWireOrder[i]));
|
|
1503
|
-
if (fileId !== undefined)
|
|
1504
|
-
slotByFileId.set(fileId, i);
|
|
1505
|
-
}
|
|
1506
|
-
const postings = cache.serialized.postings.slice();
|
|
1507
|
-
const postingAt = new Map();
|
|
1508
|
-
postings.forEach(([token], i) => postingAt.set(token, i));
|
|
1509
|
-
const removedTokens = new Set();
|
|
1510
|
-
const tokensByFile = new Map(cache.tokensByFile);
|
|
1511
|
-
const fileByKey = new Map(filesInWireOrder.map((file) => [wordIndexKey(file), file]));
|
|
1512
|
-
const affectedTokens = new Set();
|
|
1513
|
-
for (const fileKey of dirty) {
|
|
1514
|
-
const file = fileByKey.get(fileKey);
|
|
1515
|
-
const oldTokens = tokensByFile.get(fileKey) ?? new Set();
|
|
1516
|
-
const currentEntry = file === undefined ? undefined : index.forward?.get(file);
|
|
1517
|
-
const currentTokens = new Set(currentEntry?.keys() ?? []);
|
|
1518
|
-
for (const token of oldTokens)
|
|
1519
|
-
affectedTokens.add(token);
|
|
1520
|
-
for (const token of currentTokens)
|
|
1521
|
-
affectedTokens.add(token);
|
|
1522
|
-
if (file === undefined)
|
|
1523
|
-
tokensByFile.delete(fileKey);
|
|
1524
|
-
else
|
|
1525
|
-
tokensByFile.set(fileKey, currentTokens);
|
|
1526
|
-
}
|
|
1527
|
-
for (const token of affectedTokens) {
|
|
1528
|
-
const list = index.postings.get(token);
|
|
1529
|
-
const flat = [];
|
|
1530
|
-
if (list) {
|
|
1531
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
1532
|
-
const slot = slotByFileId.get(list.fileIdAt(i));
|
|
1533
|
-
if (slot !== undefined)
|
|
1534
|
-
flat.push(slot, list.lineAt(i));
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
const at = postingAt.get(token);
|
|
1538
|
-
if (flat.length === 0) {
|
|
1539
|
-
if (at !== undefined)
|
|
1540
|
-
removedTokens.add(token);
|
|
1541
|
-
}
|
|
1542
|
-
else if (at === undefined) {
|
|
1543
|
-
postings.push([token, flat]);
|
|
1544
|
-
postingAt.set(token, postings.length - 1);
|
|
1545
|
-
}
|
|
1546
|
-
else {
|
|
1547
|
-
postings[at] = [postings[at][0], flat];
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
const compactedPostings = removedTokens.size
|
|
1551
|
-
? postings.filter(([token]) => !removedTokens.has(token))
|
|
1552
|
-
: postings;
|
|
1553
|
-
const forward = index.forward
|
|
1554
|
-
? (cache.serialized.forward
|
|
1555
|
-
?.slice()
|
|
1556
|
-
.map((entry) => [entry[0], entry[1]]) ?? [])
|
|
1557
|
-
: undefined;
|
|
1558
|
-
for (const fileKey of dirty) {
|
|
1559
|
-
const file = fileByKey.get(fileKey);
|
|
1560
|
-
const fileId = file === undefined
|
|
1561
|
-
? undefined
|
|
1562
|
-
: index.fileTable.idFor(wordIndexKey(file));
|
|
1563
|
-
const slot = fileId === undefined ? undefined : slotByFileId.get(fileId);
|
|
1564
|
-
if (slot === undefined || !forward || file === undefined)
|
|
1565
|
-
continue;
|
|
1566
|
-
forward[slot] = [slot, [...(index.forward?.get(file)?.entries() ?? [])]];
|
|
1567
|
-
}
|
|
1568
|
-
const serialized = {
|
|
1569
|
-
...cache.serialized,
|
|
1570
|
-
files: filesInWireOrder,
|
|
1571
|
-
postings: compactedPostings,
|
|
1572
|
-
docLengths: filesInWireOrder.map((file) => index.docLengths.get(file) ?? 0),
|
|
1573
|
-
fileMtimes: filesInWireOrder.map((file) => index.fileMtimes.get(file) ?? 0),
|
|
1574
|
-
fileSizes: filesInWireOrder.map((file) => index.fileSizes.get(file) ?? 0),
|
|
1575
|
-
forward,
|
|
1576
|
-
totalTokens: index.totalTokens,
|
|
1577
|
-
indexedFileCount: index.docCount,
|
|
1578
|
-
truncated: index.truncated,
|
|
1579
|
-
};
|
|
1580
|
-
_lastSerializeWork = {
|
|
1581
|
-
affectedTokenCount: affectedTokens.size,
|
|
1582
|
-
tookFullPath: false,
|
|
1583
|
-
};
|
|
1584
|
-
return { serialized, slotByFileId, tokensByFile };
|
|
1585
|
-
}
|
|
1586
|
-
export function serializeWordIndex(index) {
|
|
1587
|
-
const prior = serializedWordIndexCaches.get(index);
|
|
1588
|
-
const cache = prior
|
|
1589
|
-
? serializeWordIndexIncrementally(index, prior)
|
|
1590
|
-
: serializeWordIndexFull(index);
|
|
1591
|
-
serializedWordIndexCaches.set(index, cache);
|
|
1592
|
-
index.dirtyFiles?.clear();
|
|
1593
|
-
return cache.serialized;
|
|
1594
|
-
}
|
|
1595
|
-
export function deserializeWordIndex(data) {
|
|
1596
|
-
if (!data ||
|
|
1597
|
-
data.version !== WORD_INDEX_FORMAT_VERSION ||
|
|
1598
|
-
!Array.isArray(data.files) ||
|
|
1599
|
-
!Array.isArray(data.postings) ||
|
|
1600
|
-
!Array.isArray(data.docLengths) ||
|
|
1601
|
-
!Array.isArray(data.fileMtimes)) {
|
|
1602
|
-
return null;
|
|
1603
|
-
}
|
|
1604
|
-
let canonical = data.docLengths.length === data.files.length &&
|
|
1605
|
-
data.fileMtimes.length === data.files.length;
|
|
1606
|
-
let needsDeepSanitize = !Array.isArray(data.forward);
|
|
1607
|
-
if (needsDeepSanitize)
|
|
1608
|
-
canonical = false;
|
|
1609
|
-
const fileKeys = new Set();
|
|
1610
|
-
for (const file of data.files) {
|
|
1611
|
-
if (typeof file !== "string")
|
|
1612
|
-
return null;
|
|
1613
|
-
const key = wordIndexKey(file);
|
|
1614
|
-
if (fileKeys.has(key)) {
|
|
1615
|
-
canonical = false;
|
|
1616
|
-
needsDeepSanitize = true;
|
|
1617
|
-
}
|
|
1618
|
-
fileKeys.add(key);
|
|
1619
|
-
}
|
|
1620
|
-
const docLengths = new PathKeyedMap(wordIndexKey);
|
|
1621
|
-
// Slot i of the wire `files` array becomes file id `fileIdBySlot[i]`. Two
|
|
1622
|
-
// slots whose spellings fold to one key share one id, exactly as a build
|
|
1623
|
-
// would intern them, so a divergent snapshot cannot deserialize into two
|
|
1624
|
-
// posting identities for one document.
|
|
1625
|
-
const fileTable = new WordIndexFileTable();
|
|
1626
|
-
const fileIdBySlot = data.files.map((file) => fileTable.intern(wordIndexKey(file), file));
|
|
1627
|
-
const slotByFileId = new Map();
|
|
1628
|
-
fileIdBySlot.forEach((fileId, slot) => {
|
|
1629
|
-
if (!slotByFileId.has(fileId))
|
|
1630
|
-
slotByFileId.set(fileId, slot);
|
|
1631
|
-
});
|
|
1632
|
-
const fileMtimes = new PathKeyedMap(wordIndexKey);
|
|
1633
|
-
const fileSizes = new PathKeyedMap(wordIndexKey);
|
|
1634
|
-
data.files.forEach((file, i) => {
|
|
1635
|
-
const value = data.docLengths[i];
|
|
1636
|
-
if (!isCanonicalWordIndexNumber(value) || value < 0)
|
|
1637
|
-
canonical = false;
|
|
1638
|
-
docLengths.set(file, isCanonicalWordIndexNumber(value) && value >= 0 ? value : 0);
|
|
1639
|
-
});
|
|
1640
|
-
data.files.forEach((file, i) => {
|
|
1641
|
-
const value = data.fileMtimes[i];
|
|
1642
|
-
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1643
|
-
canonical = false;
|
|
1644
|
-
fileMtimes.set(file, typeof value === "number" && Number.isFinite(value) ? value : 0);
|
|
1645
|
-
});
|
|
1646
|
-
// #1105: `fileSizes` is optional on the wire (pre-#1105 snapshots omit it).
|
|
1647
|
-
// Only populate when the array is present AND parallel to `files`; otherwise
|
|
1648
|
-
// leave it empty so the refresh gate re-reads every file once to repopulate,
|
|
1649
|
-
// rather than trusting a bogus/misaligned size. Never treated as "current".
|
|
1650
|
-
if (Array.isArray(data.fileSizes) &&
|
|
1651
|
-
data.fileSizes.length === data.files.length) {
|
|
1652
|
-
data.files.forEach((file, i) => {
|
|
1653
|
-
const value = data.fileSizes?.[i];
|
|
1654
|
-
if (!isCanonicalWordIndexNumber(value) || value < 0)
|
|
1655
|
-
canonical = false;
|
|
1656
|
-
fileSizes.set(file, isCanonicalWordIndexNumber(value) && value >= 0 ? value : 0);
|
|
1657
|
-
});
|
|
1658
|
-
}
|
|
1659
|
-
else {
|
|
1660
|
-
canonical = false;
|
|
1661
|
-
}
|
|
1662
|
-
const postings = new Map();
|
|
1663
|
-
const postingTokens = new Set();
|
|
1664
|
-
const postingFileCounts = new Map();
|
|
1665
|
-
for (const posting of data.postings) {
|
|
1666
|
-
if (!Array.isArray(posting) || posting.length !== 2) {
|
|
1667
|
-
canonical = false;
|
|
1668
|
-
needsDeepSanitize = true;
|
|
1669
|
-
continue;
|
|
1670
|
-
}
|
|
1671
|
-
const [token, flat] = posting;
|
|
1672
|
-
if (!isCanonicalWordIndexToken(token) ||
|
|
1673
|
-
postingTokens.has(token) ||
|
|
1674
|
-
!Array.isArray(flat) ||
|
|
1675
|
-
flat.length === 0 ||
|
|
1676
|
-
flat.length % 2 !== 0) {
|
|
1677
|
-
canonical = false;
|
|
1678
|
-
needsDeepSanitize = true;
|
|
1679
|
-
continue;
|
|
1680
|
-
}
|
|
1681
|
-
postingTokens.add(token);
|
|
1682
|
-
const lanes = [];
|
|
1683
|
-
const seenPairs = new Set();
|
|
1684
|
-
const observedCounts = new Map();
|
|
1685
|
-
for (let i = 0; i < flat.length; i += 2) {
|
|
1686
|
-
const slot = flat[i];
|
|
1687
|
-
const line = flat[i + 1];
|
|
1688
|
-
if (!isCanonicalWordIndexNumber(slot) ||
|
|
1689
|
-
slot < 0 ||
|
|
1690
|
-
slot >= fileIdBySlot.length ||
|
|
1691
|
-
!isCanonicalWordIndexNumber(line) ||
|
|
1692
|
-
line < 1 ||
|
|
1693
|
-
line > WORD_INDEX_INT32_MAX) {
|
|
1694
|
-
canonical = false;
|
|
1695
|
-
needsDeepSanitize = true;
|
|
1696
|
-
continue;
|
|
1697
|
-
}
|
|
1698
|
-
const fileId = fileIdBySlot[slot];
|
|
1699
|
-
const pairKey = fileId <= WORD_INDEX_PACKED_FILE_ID_MAX
|
|
1700
|
-
? fileId * (WORD_INDEX_INT32_MAX + 1) + line
|
|
1701
|
-
: `${fileId}:${line}`;
|
|
1702
|
-
if (seenPairs.has(pairKey)) {
|
|
1703
|
-
canonical = false;
|
|
1704
|
-
needsDeepSanitize = true;
|
|
1705
|
-
}
|
|
1706
|
-
seenPairs.add(pairKey);
|
|
1707
|
-
lanes.push(fileId, line);
|
|
1708
|
-
observedCounts.set(fileId, (observedCounts.get(fileId) ?? 0) + 1);
|
|
1709
|
-
}
|
|
1710
|
-
if (lanes.length === 0)
|
|
1711
|
-
continue;
|
|
1712
|
-
const list = WordPostingList.fromLanes(token, lanes);
|
|
1713
|
-
postings.set(token, list);
|
|
1714
|
-
postingFileCounts.set(token, observedCounts);
|
|
1715
|
-
}
|
|
1716
|
-
// Every list above was sized exactly from the wire payload, so there is no
|
|
1717
|
-
// growth slack to release — but the per-list `ArrayBuffer` headers are the
|
|
1718
|
-
// same third-of-the-store cost a fresh build pays, and a deserialized index
|
|
1719
|
-
// is the LONG-lived one (a warm session reuses it across turns).
|
|
1720
|
-
compactPostingsIntoArena(postings);
|
|
1721
|
-
let forward;
|
|
1722
|
-
if (Array.isArray(data.forward)) {
|
|
1723
|
-
const candidate = data.files.map(() => new Map());
|
|
1724
|
-
const forwardTotals = new Map();
|
|
1725
|
-
if (data.forward.length !== data.files.length)
|
|
1726
|
-
canonical = false;
|
|
1727
|
-
if (data.forward.length !== data.files.length)
|
|
1728
|
-
needsDeepSanitize = true;
|
|
1729
|
-
for (const [entryIndex, entry] of data.forward.entries()) {
|
|
1730
|
-
if (!Array.isArray(entry) || entry.length !== 2) {
|
|
1731
|
-
canonical = false;
|
|
1732
|
-
needsDeepSanitize = true;
|
|
1733
|
-
continue;
|
|
1734
|
-
}
|
|
1735
|
-
const [fileIdx, tokenCounts] = entry;
|
|
1736
|
-
if (!isCanonicalWordIndexNumber(fileIdx) ||
|
|
1737
|
-
fileIdx !== entryIndex ||
|
|
1738
|
-
fileIdx < 0 ||
|
|
1739
|
-
fileIdx >= data.files.length ||
|
|
1740
|
-
!Array.isArray(tokenCounts)) {
|
|
1741
|
-
canonical = false;
|
|
1742
|
-
needsDeepSanitize = true;
|
|
1743
|
-
continue;
|
|
1744
|
-
}
|
|
1745
|
-
for (const pair of tokenCounts) {
|
|
1746
|
-
if (!Array.isArray(pair) ||
|
|
1747
|
-
pair.length !== 2 ||
|
|
1748
|
-
!isCanonicalWordIndexToken(pair[0]) ||
|
|
1749
|
-
!isCanonicalWordIndexNumber(pair[1]) ||
|
|
1750
|
-
pair[1] < 1 ||
|
|
1751
|
-
pair[1] > WORD_INDEX_INT32_MAX ||
|
|
1752
|
-
candidate[fileIdx].has(pair[0])) {
|
|
1753
|
-
canonical = false;
|
|
1754
|
-
needsDeepSanitize = true;
|
|
1755
|
-
continue;
|
|
1756
|
-
}
|
|
1757
|
-
candidate[fileIdx].set(pair[0], pair[1]);
|
|
1758
|
-
forwardTotals.set(pair[0], (forwardTotals.get(pair[0]) ?? 0) + pair[1]);
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
let forwardMatchesPostings = true;
|
|
1762
|
-
for (const [token, list] of postings) {
|
|
1763
|
-
if (forwardTotals.get(token) !== list.length) {
|
|
1764
|
-
forwardMatchesPostings = false;
|
|
1765
|
-
}
|
|
1766
|
-
const observed = postingFileCounts.get(token);
|
|
1767
|
-
if (!observed) {
|
|
1768
|
-
forwardMatchesPostings = false;
|
|
1769
|
-
continue;
|
|
1770
|
-
}
|
|
1771
|
-
for (const [fileId, count] of observed) {
|
|
1772
|
-
const slot = slotByFileId.get(fileId);
|
|
1773
|
-
if (slot === undefined || candidate[slot].get(token) !== count) {
|
|
1774
|
-
forwardMatchesPostings = false;
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
}
|
|
1778
|
-
for (const token of forwardTotals.keys()) {
|
|
1779
|
-
if (!postingTokens.has(token))
|
|
1780
|
-
forwardMatchesPostings = false;
|
|
1781
|
-
}
|
|
1782
|
-
if (!forwardMatchesPostings)
|
|
1783
|
-
canonical = false;
|
|
1784
|
-
if (!forwardMatchesPostings)
|
|
1785
|
-
needsDeepSanitize = true;
|
|
1786
|
-
forward = new PathKeyedMap(wordIndexKey);
|
|
1787
|
-
for (let i = 0; i < data.files.length; i += 1) {
|
|
1788
|
-
forward.set(data.files[i], WordForwardEntry.fromTally(candidate[i]));
|
|
1789
|
-
}
|
|
1790
|
-
}
|
|
1791
|
-
else if (Object.prototype.hasOwnProperty.call(data, "forward")) {
|
|
1792
|
-
canonical = false;
|
|
1793
|
-
needsDeepSanitize = true;
|
|
1794
|
-
}
|
|
1795
|
-
if (needsDeepSanitize) {
|
|
1796
|
-
const actualForwardCounts = data.files.map(() => new Map());
|
|
1797
|
-
for (const [token, list] of postings) {
|
|
1798
|
-
const seenPairs = new Set();
|
|
1799
|
-
const sanitized = new WordPostingList(token, list.length);
|
|
1800
|
-
for (let i = 0; i < list.length; i += 1) {
|
|
1801
|
-
const fileId = list.fileIdAt(i);
|
|
1802
|
-
const line = list.lineAt(i);
|
|
1803
|
-
const pairKey = `${fileId}:${line}`;
|
|
1804
|
-
if (seenPairs.has(pairKey))
|
|
1805
|
-
continue;
|
|
1806
|
-
seenPairs.add(pairKey);
|
|
1807
|
-
sanitized.push(fileId, line);
|
|
1808
|
-
const slot = slotByFileId.get(fileId);
|
|
1809
|
-
if (slot !== undefined) {
|
|
1810
|
-
const counts = actualForwardCounts[slot];
|
|
1811
|
-
counts.set(token, (counts.get(token) ?? 0) + 1);
|
|
1812
|
-
}
|
|
1813
|
-
}
|
|
1814
|
-
postings.set(token, sanitized);
|
|
1815
|
-
}
|
|
1816
|
-
compactPostingsIntoArena(postings);
|
|
1817
|
-
if (Array.isArray(data.forward)) {
|
|
1818
|
-
forward = new PathKeyedMap(wordIndexKey);
|
|
1819
|
-
for (let i = 0; i < data.files.length; i += 1) {
|
|
1820
|
-
const canonicalSlot = slotByFileId.get(fileIdBySlot[i]) ?? i;
|
|
1821
|
-
forward.set(data.files[i], WordForwardEntry.fromTally(actualForwardCounts[canonicalSlot]));
|
|
1822
|
-
}
|
|
1823
|
-
}
|
|
1824
|
-
}
|
|
1825
|
-
const expectedTotalTokens = [...docLengths.values()].reduce((total, length) => total + length, 0);
|
|
1826
|
-
const totalTokens = isCanonicalWordIndexNumber(expectedTotalTokens)
|
|
1827
|
-
? expectedTotalTokens
|
|
1828
|
-
: 0;
|
|
1829
|
-
if (!isCanonicalWordIndexNumber(data.totalTokens) ||
|
|
1830
|
-
data.totalTokens !== totalTokens) {
|
|
1831
|
-
canonical = false;
|
|
1832
|
-
}
|
|
1833
|
-
if (typeof data.indexedFileCount !== "number" ||
|
|
1834
|
-
data.indexedFileCount !== docLengths.size) {
|
|
1835
|
-
canonical = false;
|
|
1836
|
-
}
|
|
1837
|
-
if (typeof data.truncated !== "boolean")
|
|
1838
|
-
canonical = false;
|
|
1839
|
-
const index = {
|
|
1840
|
-
postings,
|
|
1841
|
-
fileTable,
|
|
1842
|
-
docLengths,
|
|
1843
|
-
totalTokens,
|
|
1844
|
-
docCount: docLengths.size,
|
|
1845
|
-
truncated: data.truncated === true,
|
|
1846
|
-
forward,
|
|
1847
|
-
fileMtimes,
|
|
1848
|
-
fileSizes,
|
|
1849
|
-
// Postings were just packed into one arena above, so the running gate
|
|
1850
|
-
// starts from the exact post-compaction store count (#2117).
|
|
1851
|
-
postingStoreCount: countPostingBackingStores(postings),
|
|
1852
|
-
recompactFlight: createSingleFlight(),
|
|
1853
|
-
// A deserialized index is a fresh object distinct from whichever index
|
|
1854
|
-
// produced `data`. Without this, every per-edit dirty mark on a
|
|
1855
|
-
// session-start reload (#2068) is a no-op against `undefined`, and
|
|
1856
|
-
// serializeWordIndexIncrementally's `!dirty` branch then returns the
|
|
1857
|
-
// stale cached wire view forever, silently dropping every later edit
|
|
1858
|
-
// from the persisted snapshot.
|
|
1859
|
-
dirtyFiles: new Set(),
|
|
1860
|
-
};
|
|
1861
|
-
// Seed the wire cache from the snapshot just loaded so the FIRST persist
|
|
1862
|
-
// of a reloaded session (session-start's `snapshotSaveSyncMs`, #2068) also
|
|
1863
|
-
// takes the bounded incremental path instead of paying a full rebuild —
|
|
1864
|
-
// mirrors exactly what one priming `serializeWordIndex` call would cache.
|
|
1865
|
-
const tokensByFile = new Map();
|
|
1866
|
-
if (forward) {
|
|
1867
|
-
for (const file of data.files) {
|
|
1868
|
-
tokensByFile.set(wordIndexKey(file), new Set(forward.get(file)?.keys() ?? []));
|
|
1869
|
-
}
|
|
1870
|
-
}
|
|
1871
|
-
if (canonical) {
|
|
1872
|
-
serializedWordIndexCaches.set(index, {
|
|
1873
|
-
serialized: data,
|
|
1874
|
-
slotByFileId,
|
|
1875
|
-
tokensByFile,
|
|
1876
|
-
});
|
|
1877
|
-
}
|
|
1878
|
-
return index;
|
|
1879
|
-
}
|
|
1880
|
-
const buildStatuses = new Map();
|
|
1881
|
-
export function getWordIndexBuildStatus(cwd) {
|
|
1882
|
-
return buildStatuses.get(path.resolve(cwd));
|
|
1883
|
-
}
|
|
1884
|
-
/** Test-only: reset the in-flight-build guard between test files/cases. */
|
|
1885
|
-
export function _resetWordIndexBuildGuardForTests() {
|
|
1886
|
-
buildStatuses.clear();
|
|
1887
|
-
}
|
|
1888
|
-
/**
|
|
1889
|
-
* Fire a one-time bounded background build for `cwd` if one isn't already
|
|
1890
|
-
* running. Persists into the existing project snapshot (preserving its other
|
|
1891
|
-
* fields) so the next query — or the next real session — picks it up. Errors
|
|
1892
|
-
* are swallowed (this is best-effort warmth, not a request the caller is
|
|
1893
|
-
* waiting on); the guard always clears in a `finally` so a failed build can be
|
|
1894
|
-
* retried by a later query.
|
|
1895
|
-
*/
|
|
1896
|
-
export function triggerBackgroundWordIndexBuild(cwd, dbg, options = {}) {
|
|
1897
|
-
const key = path.resolve(cwd);
|
|
1898
|
-
// #747 hardening: this trigger is the one word-index build path with NO
|
|
1899
|
-
// session lifecycle in front of it (cold `symbol_search` queries, in-process
|
|
1900
|
-
// and via MCP where cwd can be a raw tool argument) — the session-start and
|
|
1901
|
-
// quick-mode-warmup builds sit behind `canWarmCaches`, which already refuses
|
|
1902
|
-
// a home-rooted cwd. Apply the same `isAtOrAboveHomeDir` ceiling here so a
|
|
1903
|
-
// cold query from $HOME never starts a whole-home walk-and-read.
|
|
1904
|
-
if (isAtOrAboveHomeDir(key, options.homeDir)) {
|
|
1905
|
-
const reason = `root at/above home directory (${key})`;
|
|
1906
|
-
dbg?.(`word-index cold-build: skipped — ${reason}`);
|
|
1907
|
-
logWordIndex({
|
|
1908
|
-
phase: "cold_build_refused",
|
|
1909
|
-
cwd: key,
|
|
1910
|
-
trigger: "cold_query",
|
|
1911
|
-
reason,
|
|
1912
|
-
});
|
|
1913
|
-
const status = { state: "refused", reason };
|
|
1914
|
-
buildStatuses.set(key, status);
|
|
1915
|
-
return status;
|
|
1916
|
-
}
|
|
1917
|
-
const current = buildStatuses.get(key);
|
|
1918
|
-
if (current?.state === "building")
|
|
1919
|
-
return current;
|
|
1920
|
-
const status = { state: "building" };
|
|
1921
|
-
buildStatuses.set(key, status);
|
|
1922
|
-
void (async () => {
|
|
1923
|
-
const startMs = Date.now();
|
|
1924
|
-
try {
|
|
1925
|
-
const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION, } = await import("./project-snapshot.js");
|
|
1926
|
-
const docs = await collectWordIndexDocs(key);
|
|
1927
|
-
const index = await buildWordIndexAsync(docs);
|
|
1928
|
-
const existing = loadProjectSnapshot(key);
|
|
1929
|
-
const snapshot = existing ?? {
|
|
1930
|
-
version: PROJECT_SNAPSHOT_VERSION,
|
|
1931
|
-
projectRoot: key,
|
|
1932
|
-
generatedAt: new Date().toISOString(),
|
|
1933
|
-
seq: 0,
|
|
1934
|
-
files: {},
|
|
1935
|
-
symbols: {},
|
|
1936
|
-
reverseDeps: {},
|
|
1937
|
-
cachedExports: [],
|
|
1938
|
-
};
|
|
1939
|
-
snapshot.generatedAt = new Date().toISOString();
|
|
1940
|
-
snapshot.wordIndex = serializeWordIndex(index);
|
|
1941
|
-
saveProjectSnapshot(key, snapshot);
|
|
1942
|
-
dbg?.(`word-index cold-build: ${index.docCount} files, ${index.postings.size} tokens (${Date.now() - startMs}ms)`);
|
|
1943
|
-
// M2, #958: durable decision/coverage record for the MCP-critical cold
|
|
1944
|
-
// path (this is where symbol_search reads the index and dbg is a no-op).
|
|
1945
|
-
logWordIndex({
|
|
1946
|
-
phase: "cold_build",
|
|
1947
|
-
cwd: key,
|
|
1948
|
-
trigger: "cold_query",
|
|
1949
|
-
durationMs: Date.now() - startMs,
|
|
1950
|
-
indexedFileCount: index.docCount,
|
|
1951
|
-
tokens: index.postings.size,
|
|
1952
|
-
postingEntries: countWordIndexPostingEntries(index),
|
|
1953
|
-
residentBytes: estimateWordIndexResidentBytes(index),
|
|
1954
|
-
truncated: index.truncated,
|
|
1955
|
-
skipped: docs.skipped,
|
|
1956
|
-
});
|
|
1957
|
-
buildStatuses.delete(key);
|
|
1958
|
-
}
|
|
1959
|
-
catch (err) {
|
|
1960
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
1961
|
-
buildStatuses.set(key, { state: "failed", reason });
|
|
1962
|
-
dbg?.(`word-index cold-build: failed: ${reason}`);
|
|
1963
|
-
logWordIndex({
|
|
1964
|
-
phase: "cold_build_failed",
|
|
1965
|
-
cwd: key,
|
|
1966
|
-
trigger: "cold_query",
|
|
1967
|
-
durationMs: Date.now() - startMs,
|
|
1968
|
-
error: reason,
|
|
1969
|
-
});
|
|
1970
|
-
}
|
|
1971
|
-
})();
|
|
1972
|
-
return status;
|
|
1973
|
-
}
|
|
1974
|
-
// --- Debounced per-edit persist (#348 phase 2) --------------------------------
|
|
1975
|
-
//
|
|
1976
|
-
// The per-edit seam (dispatch/integration.ts) updates `runtime.wordIndex` in
|
|
1977
|
-
// memory on every write, same as the review graph's per-edit rebuild. Without
|
|
1978
|
-
// coalescing, persisting that in-memory index on every single edit would mean
|
|
1979
|
-
// one full-snapshot JSON.stringify+write per keystroke-adjacent edit — the
|
|
1980
|
-
// same OOM-risking spike the graph's #260 circuit-breaker exists to prevent.
|
|
1981
|
-
// This reuses `createDebounceScheduler` (persist-debounce.ts) rather than
|
|
1982
|
-
// growing a second copy of the graph's bespoke pending-map+timer bookkeeping;
|
|
1983
|
-
// only the "write" callback differs, because the target differs: the graph
|
|
1984
|
-
// owns its own cache file, but the word index must merge into the SHARED
|
|
1985
|
-
// project-snapshot file via `saveRuntimeProjectSnapshot`/`saveProjectSnapshot`
|
|
1986
|
-
// (preserving unrelated snapshot fields, and honoring the seq-laundering guard
|
|
1987
|
-
// in project-snapshot.ts — see saveRuntimeProjectSnapshot's comment).
|
|
1988
|
-
const WORD_INDEX_PERSIST_DEBOUNCE_MS_DEFAULT = 1500;
|
|
1989
|
-
function wordIndexPersistDebounceMs() {
|
|
1990
|
-
const raw = Number(process.env.PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS);
|
|
1991
|
-
return Number.isFinite(raw) && raw >= 0
|
|
1992
|
-
? raw
|
|
1993
|
-
: WORD_INDEX_PERSIST_DEBOUNCE_MS_DEFAULT;
|
|
1994
|
-
}
|
|
1995
|
-
let wordIndexPersistScheduler;
|
|
1996
|
-
function getWordIndexPersistScheduler() {
|
|
1997
|
-
if (wordIndexPersistScheduler)
|
|
1998
|
-
return wordIndexPersistScheduler;
|
|
1999
|
-
wordIndexPersistScheduler = createDebounceScheduler({
|
|
2000
|
-
debounceMs: wordIndexPersistDebounceMs,
|
|
2001
|
-
write(_key, pending) {
|
|
2002
|
-
void writeWordIndexSnapshot(pending.cwd, pending.index, pending.dbg);
|
|
2003
|
-
},
|
|
2004
|
-
});
|
|
2005
|
-
return wordIndexPersistScheduler;
|
|
2006
|
-
}
|
|
2007
|
-
async function writeWordIndexSnapshot(cwd, index, dbg) {
|
|
2008
|
-
const persistStartedAt = Date.now();
|
|
2009
|
-
try {
|
|
2010
|
-
const { loadProjectSnapshot, saveProjectSnapshot, PROJECT_SNAPSHOT_VERSION, } = await import("./project-snapshot.js");
|
|
2011
|
-
const existing = loadProjectSnapshot(cwd);
|
|
2012
|
-
const snapshot = existing ?? {
|
|
2013
|
-
version: PROJECT_SNAPSHOT_VERSION,
|
|
2014
|
-
projectRoot: path.resolve(cwd),
|
|
2015
|
-
generatedAt: new Date().toISOString(),
|
|
2016
|
-
seq: 0,
|
|
2017
|
-
files: {},
|
|
2018
|
-
symbols: {},
|
|
2019
|
-
reverseDeps: {},
|
|
2020
|
-
cachedExports: [],
|
|
2021
|
-
};
|
|
2022
|
-
snapshot.generatedAt = new Date().toISOString();
|
|
2023
|
-
const serializeStartedAt = performance.now();
|
|
2024
|
-
snapshot.wordIndex = serializeWordIndex(index);
|
|
2025
|
-
const serializeMs = performance.now() - serializeStartedAt;
|
|
2026
|
-
saveProjectSnapshot(cwd, snapshot);
|
|
2027
|
-
const writeMs = performance.now() - serializeStartedAt - serializeMs;
|
|
2028
|
-
dbg?.(`word-index persist: ${index.docCount} files, ${index.postings.size} tokens`);
|
|
2029
|
-
// #958 review F1: durably record persist SUCCESS too, not just failures —
|
|
2030
|
-
// in the MCP host (`dbg` is a no-op) this is the only signal that the index
|
|
2031
|
-
// is actually being kept fresh across edits. Debounced (~1.5s), so not
|
|
2032
|
-
// spammy. Mirrors the review graph's persist_succeeded.
|
|
2033
|
-
logWordIndex({
|
|
2034
|
-
phase: "persist_succeeded",
|
|
2035
|
-
cwd: path.resolve(cwd),
|
|
2036
|
-
trigger: "per_edit",
|
|
2037
|
-
durationMs: Date.now() - persistStartedAt,
|
|
2038
|
-
serializeMs,
|
|
2039
|
-
writeMs,
|
|
2040
|
-
indexedFileCount: index.docCount,
|
|
2041
|
-
tokens: index.postings.size,
|
|
2042
|
-
postingEntries: countWordIndexPostingEntries(index),
|
|
2043
|
-
residentBytes: estimateWordIndexResidentBytes(index),
|
|
2044
|
-
replacementCount: index.replacementStats?.count ?? 0,
|
|
2045
|
-
totalReplacementMs: index.replacementStats?.totalMs ?? 0,
|
|
2046
|
-
maxReplacementMs: index.replacementStats?.maxMs ?? 0,
|
|
2047
|
-
});
|
|
2048
|
-
index.replacementStats = { count: 0, totalMs: 0, maxMs: 0 };
|
|
2049
|
-
}
|
|
2050
|
-
catch (err) {
|
|
2051
|
-
dbg?.(`word-index persist: failed: ${err}`);
|
|
2052
|
-
// M3, #958: a swallowed persist means every LATER symbol_search reads a
|
|
2053
|
-
// stale index with no trace — the exact silent-failure this durable log
|
|
2054
|
-
// exists to surface (dbg is a no-op in the MCP host).
|
|
2055
|
-
logWordIndex({
|
|
2056
|
-
phase: "persist_failed",
|
|
2057
|
-
cwd: path.resolve(cwd),
|
|
2058
|
-
trigger: "per_edit",
|
|
2059
|
-
indexedFileCount: index.docCount,
|
|
2060
|
-
error: err instanceof Error ? err.message : String(err),
|
|
2061
|
-
});
|
|
2062
|
-
}
|
|
2063
|
-
}
|
|
2064
|
-
/**
|
|
2065
|
-
* Schedule a debounced persist of `index` for `cwd`, coalescing a burst of
|
|
2066
|
-
* per-edit updates into one write after a quiet window (default 1500ms,
|
|
2067
|
-
* override via `PI_LENS_WORD_INDEX_PERSIST_DEBOUNCE_MS`, mirroring the review
|
|
2068
|
-
* graph's `PI_LENS_GRAPH_PERSIST_DEBOUNCE_MS`). Merges through the same
|
|
2069
|
-
* `saveProjectSnapshot` path phase 1 uses — preserves unrelated snapshot
|
|
2070
|
-
* fields and respects the seq-laundering guard (only ever writes wordIndex
|
|
2071
|
-
* for the CURRENT in-memory index, never re-stamps a stale one).
|
|
2072
|
-
*/
|
|
2073
|
-
export function scheduleWordIndexPersist(cwd, index, dbg) {
|
|
2074
|
-
const key = path.resolve(cwd);
|
|
2075
|
-
getWordIndexPersistScheduler().schedule(key, { cwd: key, index, dbg });
|
|
2076
|
-
}
|
|
2077
|
-
/** Test hook: force any pending debounced word-index persist to write immediately. */
|
|
2078
|
-
export function flushWordIndexPersistsForTests() {
|
|
2079
|
-
getWordIndexPersistScheduler().flushAll();
|
|
2080
|
-
}
|