@herbertgao/pi-extensions 2026.10.1 → 2026.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -9
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/package.json +15 -10
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,1561 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Read-Before-Edit Guard for pi-lens
|
|
3
|
-
*
|
|
4
|
-
* Blocks edits that lack adequate prior reading:
|
|
5
|
-
* 1. Zero-read edit: never read this file in this branch
|
|
6
|
-
* 2. File modified since read: disk content changed (FileTime)
|
|
7
|
-
* 3. Out-of-range edit: edit target not covered by any previous read
|
|
8
|
-
* 4. LSP expansion exemption: single-line read expanded to full symbol counts
|
|
9
|
-
*
|
|
10
|
-
* Falls back safely when LSP is unavailable.
|
|
11
|
-
*/
|
|
12
|
-
import * as fs from "node:fs";
|
|
13
|
-
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
14
|
-
import { createFileTime } from "./file-time.js";
|
|
15
|
-
import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
|
|
16
|
-
import { normalizeEphemeralMapKey, normalizeFilePath } from "./path-utils.js";
|
|
17
|
-
import { logReadGuardEvent } from "./read-guard-logger.js";
|
|
18
|
-
export const READ_GUARD_STATE_VERSION = 1;
|
|
19
|
-
// --- Constants ---
|
|
20
|
-
const DEFAULT_CONFIG = {
|
|
21
|
-
enabled: true,
|
|
22
|
-
mode: "block",
|
|
23
|
-
contextLines: 3,
|
|
24
|
-
exemptions: [
|
|
25
|
-
{ pattern: "*.md", mode: "warn" },
|
|
26
|
-
{ pattern: "*.txt", mode: "allow" },
|
|
27
|
-
{ pattern: "*.log", mode: "allow" },
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
const OWN_EDIT_STALE_GRACE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_OWN_EDIT_GRACE_MS ?? "120000", 10) || 120000);
|
|
31
|
-
/** Avoid hashing very large reads in the hot path. */
|
|
32
|
-
const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_HASH_MAX_LINES ?? "3000", 10) || 3000);
|
|
33
|
-
/**
|
|
34
|
-
* Content bindings are defense in depth on a read-adjacent hot path. Cap the
|
|
35
|
-
* synchronous disk read so bridge registration never hashes an unbounded file.
|
|
36
|
-
*/
|
|
37
|
-
const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
|
|
38
|
-
const READ_GUARD_MAX_FILES = 256;
|
|
39
|
-
/**
|
|
40
|
-
* #1904 item 3: `enforceFileCap` bounds how many FILES the store holds, not how
|
|
41
|
-
* many records each file holds. One hot file reached 268 hash-bearing records in
|
|
42
|
-
* 75 minutes, and every record carries a `lineHashes` map, so a long session on
|
|
43
|
-
* a few files grows without bound. Cap records per file and drop oldest-first.
|
|
44
|
-
*
|
|
45
|
-
* Eviction is safe in the blocking direction: it narrows the staleness-rescue
|
|
46
|
-
* path — a stale edit rescued by an older snapshot that still matches — and can
|
|
47
|
-
* turn an allow into a "re-read and retry" block. It never turns a block into
|
|
48
|
-
* an allow. See `enforceRecordCapForFile` for which records go first, and why
|
|
49
|
-
* age alone is the wrong order.
|
|
50
|
-
*/
|
|
51
|
-
const READ_GUARD_MAX_RECORDS_PER_FILE = 128;
|
|
52
|
-
const NO_TRIM = {
|
|
53
|
-
evictedCount: 0,
|
|
54
|
-
evictedCreditCount: 0,
|
|
55
|
-
evictedGenuineCount: 0,
|
|
56
|
-
};
|
|
57
|
-
function enforceRecordCapForFile(records) {
|
|
58
|
-
if (records.length <= READ_GUARD_MAX_RECORDS_PER_FILE)
|
|
59
|
-
return NO_TRIM;
|
|
60
|
-
const excess = records.length - READ_GUARD_MAX_RECORDS_PER_FILE;
|
|
61
|
-
const evicted = new Set();
|
|
62
|
-
let evictedCreditCount = 0;
|
|
63
|
-
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
64
|
-
if (records[i].searchCredit !== undefined) {
|
|
65
|
-
evicted.add(i);
|
|
66
|
-
evictedCreditCount++;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
for (let i = 0; i < records.length && evicted.size < excess; i++) {
|
|
70
|
-
evicted.add(i);
|
|
71
|
-
}
|
|
72
|
-
const kept = records.filter((_, i) => !evicted.has(i));
|
|
73
|
-
records.length = 0;
|
|
74
|
-
for (const record of kept)
|
|
75
|
-
records.push(record);
|
|
76
|
-
return {
|
|
77
|
-
evictedCount: excess,
|
|
78
|
-
evictedCreditCount,
|
|
79
|
-
evictedGenuineCount: excess - evictedCreditCount,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* #1904 class sweep: `this.edits` is the same shape as `this.reads` — a
|
|
84
|
-
* per-file array that only ever grows. Its cap sits far above every consumer's
|
|
85
|
-
* reach, so trimming is inert: `canTreatStalenessAsOwnPriorEdit` reads only the
|
|
86
|
-
* last record, and `findRelocation`'s window saturates at
|
|
87
|
-
* RELOCATION_WINDOW_MAX / RELOCATION_WINDOW_PER_EDIT (20) applied edits. Only
|
|
88
|
-
* `getStats`, a debug surface, sees the older records at all.
|
|
89
|
-
*/
|
|
90
|
-
const READ_GUARD_MAX_EDITS_PER_FILE = 256;
|
|
91
|
-
// Unconsumed reads remain valid until edit or session end, but this high
|
|
92
|
-
// sanity cap prevents a read-only session from growing without bound.
|
|
93
|
-
const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
|
|
94
|
-
const READ_GUARD_IDLE_EVICT_MS_DEFAULT = 30 * 60_000;
|
|
95
|
-
export function captureReadContentBinding(filePath, offset, limit) {
|
|
96
|
-
try {
|
|
97
|
-
if (fs.statSync(filePath).size > READ_BINDING_MAX_BYTES)
|
|
98
|
-
return undefined;
|
|
99
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
100
|
-
// Allocate at most the classification prefix first. For range bindings,
|
|
101
|
-
// splitting then stops at the requested range end rather than the EOF.
|
|
102
|
-
const lines = splitLinesThrough(content, READ_HASH_MAX_LINES + 1);
|
|
103
|
-
if (lines.length <= READ_HASH_MAX_LINES) {
|
|
104
|
-
return {
|
|
105
|
-
hash: hashDiagnosticContent(content),
|
|
106
|
-
fullFile: true,
|
|
107
|
-
offset: 1,
|
|
108
|
-
limit: lines.length,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
const scopedOffset = Math.max(1, offset);
|
|
112
|
-
const scopedLimit = Math.min(Math.max(0, limit), READ_HASH_MAX_LINES);
|
|
113
|
-
if (scopedLimit === 0)
|
|
114
|
-
return undefined;
|
|
115
|
-
const rangeLines = splitLinesThrough(content, scopedOffset - 1 + scopedLimit);
|
|
116
|
-
const scopedContent = rangeLines
|
|
117
|
-
.slice(scopedOffset - 1, scopedOffset - 1 + scopedLimit)
|
|
118
|
-
.join("\n");
|
|
119
|
-
return {
|
|
120
|
-
hash: hashDiagnosticContent(scopedContent),
|
|
121
|
-
fullFile: false,
|
|
122
|
-
offset: scopedOffset,
|
|
123
|
-
limit: scopedLimit,
|
|
124
|
-
};
|
|
125
|
-
}
|
|
126
|
-
catch {
|
|
127
|
-
return undefined;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
export function _currentContentMatchesBindingForTests(filePath, binding) {
|
|
131
|
-
try {
|
|
132
|
-
const content = fs.readFileSync(filePath, "utf-8");
|
|
133
|
-
const boundContent = binding.fullFile
|
|
134
|
-
? content
|
|
135
|
-
: splitLines(content)
|
|
136
|
-
.slice(binding.offset - 1, binding.offset - 1 + binding.limit)
|
|
137
|
-
.join("\n");
|
|
138
|
-
return hashDiagnosticContent(boundContent) === binding.hash;
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
return false;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
const currentContentMatchesBinding = _currentContentMatchesBindingForTests;
|
|
145
|
-
// Adaptive relocation window (findRelocation). A globally-unique hash-sequence
|
|
146
|
-
// match always wins; when the content is duplicated elsewhere, we fall back to
|
|
147
|
-
// a match that is unique WITHIN this window of the original position. The window
|
|
148
|
-
// widens with edits already applied to the file (accumulated line drift) —
|
|
149
|
-
// floor + per-edit growth, capped — the analog of pi-hashline-readmap's
|
|
150
|
-
// edits-scaled relocation window.
|
|
151
|
-
const RELOCATION_WINDOW_MIN = Math.max(1, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_MIN ?? "40", 10) || 40);
|
|
152
|
-
const RELOCATION_WINDOW_PER_EDIT = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_PER_EDIT ?? "20", 10) || 20);
|
|
153
|
-
const RELOCATION_WINDOW_MAX = Math.max(RELOCATION_WINDOW_MIN, Number.parseInt(process.env.PI_LENS_READ_GUARD_RELOCATION_WINDOW_MAX ?? "400", 10) || 400);
|
|
154
|
-
function splitLines(text) {
|
|
155
|
-
return text.split(/\r?\n/);
|
|
156
|
-
}
|
|
157
|
-
/** `splitLines` semantics, but without scanning/allocating beyond `maxLines`. */
|
|
158
|
-
function splitLinesThrough(text, maxLines) {
|
|
159
|
-
if (maxLines <= 0)
|
|
160
|
-
return [];
|
|
161
|
-
const lines = [];
|
|
162
|
-
let start = 0;
|
|
163
|
-
while (lines.length < maxLines) {
|
|
164
|
-
const newline = text.indexOf("\n", start);
|
|
165
|
-
if (newline === -1) {
|
|
166
|
-
lines.push(text.slice(start));
|
|
167
|
-
break;
|
|
168
|
-
}
|
|
169
|
-
const end = newline > start && text[newline - 1] === "\r" ? newline - 1 : newline;
|
|
170
|
-
lines.push(text.slice(start, end));
|
|
171
|
-
start = newline + 1;
|
|
172
|
-
if (start === text.length && lines.length < maxLines) {
|
|
173
|
-
lines.push("");
|
|
174
|
-
break;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
return lines;
|
|
178
|
-
}
|
|
179
|
-
export function lineContentHash(line) {
|
|
180
|
-
// FNV-1a over whitespace-stripped content. This treats no-op formatter/touch
|
|
181
|
-
// changes as still-valid context while detecting semantic line changes.
|
|
182
|
-
const normalized = line.replace(/\s+/g, "");
|
|
183
|
-
let hash = 2166136261;
|
|
184
|
-
for (let i = 0; i < normalized.length; i++) {
|
|
185
|
-
hash = Math.imul(hash ^ normalized.charCodeAt(i), 16777619);
|
|
186
|
-
}
|
|
187
|
-
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
188
|
-
}
|
|
189
|
-
function readRangeCoversLine(read, lineNo) {
|
|
190
|
-
return (lineNo >= read.effectiveOffset &&
|
|
191
|
-
lineNo <= read.effectiveOffset + read.effectiveLimit - 1);
|
|
192
|
-
}
|
|
193
|
-
function readEffectiveRangeCoversRange(read, [startLine, endLine]) {
|
|
194
|
-
return (readRangeCoversLine(read, startLine) && readRangeCoversLine(read, endLine));
|
|
195
|
-
}
|
|
196
|
-
function captureLineHashes(filePath, offset, limit) {
|
|
197
|
-
if (limit <= 0 || limit > READ_HASH_MAX_LINES)
|
|
198
|
-
return undefined;
|
|
199
|
-
try {
|
|
200
|
-
const lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
201
|
-
const hashes = {};
|
|
202
|
-
const end = Math.min(lines.length, offset + limit - 1);
|
|
203
|
-
for (let lineNo = Math.max(1, offset); lineNo <= end; lineNo++) {
|
|
204
|
-
hashes[lineNo] = lineContentHash(lines[lineNo - 1] ?? "");
|
|
205
|
-
}
|
|
206
|
-
return Object.keys(hashes).length > 0 ? hashes : undefined;
|
|
207
|
-
}
|
|
208
|
-
catch {
|
|
209
|
-
return undefined;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
export function currentLinesMatchReadSnapshot(filePath, read, [startLine, endLine]) {
|
|
213
|
-
const hashes = read.lineHashes ?? {};
|
|
214
|
-
const missingLines = [];
|
|
215
|
-
const mismatchedLines = [];
|
|
216
|
-
for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
|
|
217
|
-
if (!readRangeCoversLine(read, lineNo) || hashes[lineNo] === undefined) {
|
|
218
|
-
missingLines.push(lineNo);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
if (missingLines.length > 0) {
|
|
222
|
-
return { checked: false, matches: false, missingLines, mismatchedLines };
|
|
223
|
-
}
|
|
224
|
-
let lines;
|
|
225
|
-
try {
|
|
226
|
-
lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
227
|
-
}
|
|
228
|
-
catch {
|
|
229
|
-
return {
|
|
230
|
-
checked: true,
|
|
231
|
-
matches: false,
|
|
232
|
-
missingLines,
|
|
233
|
-
mismatchedLines: [...Array(endLine - startLine + 1)].map((_, index) => startLine + index),
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
|
|
237
|
-
if (lineNo < 1 || lineNo > lines.length) {
|
|
238
|
-
mismatchedLines.push(lineNo);
|
|
239
|
-
continue;
|
|
240
|
-
}
|
|
241
|
-
if (lineContentHash(lines[lineNo - 1] ?? "") !== hashes[lineNo]) {
|
|
242
|
-
mismatchedLines.push(lineNo);
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
return {
|
|
246
|
-
checked: true,
|
|
247
|
-
matches: mismatchedLines.length === 0,
|
|
248
|
-
missingLines,
|
|
249
|
-
mismatchedLines,
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
// --- ReadGuard Class ---
|
|
253
|
-
export class ReadGuard {
|
|
254
|
-
config;
|
|
255
|
-
reads = new Map();
|
|
256
|
-
edits = new Map();
|
|
257
|
-
fileLastUsed = new Map();
|
|
258
|
-
fileIdleTimers = new Map();
|
|
259
|
-
/** Reads remain behavior-gating until the corresponding edit is published. */
|
|
260
|
-
consumedReadFiles = new Set();
|
|
261
|
-
fileTime;
|
|
262
|
-
exemptions = new Set(); // One-time exemptions via /lens-allow-edit
|
|
263
|
-
pendingCreations = new Map();
|
|
264
|
-
// Files that recordWritten() has fired on this session. Lets
|
|
265
|
-
// wasWrittenThisSession() return a deterministic answer for files the
|
|
266
|
-
// pi Write tool authored, independent of filesystem mtime granularity
|
|
267
|
-
// or clock skew (NFS, FAT32, etc.).
|
|
268
|
-
writtenThisSession = new Set();
|
|
269
|
-
unchangedThisSession = new Set();
|
|
270
|
-
// Existence-independent index for hasKnownPath/forgetPath (#1668 review
|
|
271
|
-
// F1). `this.key()` (normalizeFilePath) branches on whether `filePath`
|
|
272
|
-
// currently exists on disk, on EVERY platform since #3098 — an existing
|
|
273
|
-
// file resolves to its on-disk casing (win32: `realpathSync.native`;
|
|
274
|
-
// POSIX: the on-disk casing of the trailing segments), a missing one to a
|
|
275
|
-
// lowercased tail on win32 and to the caller's own spelling on POSIX.
|
|
276
|
-
// recordRead/recordWritten always key while the file is still on disk
|
|
277
|
-
// (real casing); hasKnownPath/forgetPath are queried AFTER an external
|
|
278
|
-
// delete already landed, when the path no longer exists — recomputing
|
|
279
|
-
// `this.key()` at that point returns a DIFFERENT string for any
|
|
280
|
-
// mixed-case basename (win32 `MyModule.ts` → `mymodule.ts`; POSIX
|
|
281
|
-
// `Components/x.ts` held against an on-disk `components/`), so a lookup
|
|
282
|
-
// against `reads`/`writtenThisSession` silently misses. This index maps
|
|
283
|
-
// a purely syntactic key (`normalizeEphemeralMapKey` — slash-fold +
|
|
284
|
-
// lowercase, no filesystem access, so it never depends on current disk
|
|
285
|
-
// state) to the REAL key `reads`/`writtenThisSession` used at record
|
|
286
|
-
// time, so a post-delete lookup finds the same entry regardless of what
|
|
287
|
-
// happened to the file since. Pruned inside `evictFile` so it never
|
|
288
|
-
// outlives the record it points at.
|
|
289
|
-
//
|
|
290
|
-
// ONLY hasKnownPath/forgetPath route through this index, because only they
|
|
291
|
-
// need the real key BACK. `noteCreatedFile` (keys while the file is still
|
|
292
|
-
// absent) and `recordWritten` (keys after it exists) straddle the same
|
|
293
|
-
// state change, and used to do it through `this.key()` directly — a
|
|
294
|
-
// spelling whose key moved orphaned the `pendingCreations` entry and the
|
|
295
|
-
// creation read was never injected (#3163, pre-existing on win32 and
|
|
296
|
-
// reachable on POSIX since #3098). They now key `pendingCreations` by the
|
|
297
|
-
// same existence-independent `normalizeEphemeralMapKey` spelling this index
|
|
298
|
-
// is keyed by; no index is needed there, since the real key is derived by
|
|
299
|
-
// `recordWritten` itself at the moment it is used.
|
|
300
|
-
knownPathIndex = new Map();
|
|
301
|
-
/** Running per-file record-cap trim totals for this session (#1913 F1). */
|
|
302
|
-
trimAccumulators = new Map();
|
|
303
|
-
sessionId;
|
|
304
|
-
sessionStartMs;
|
|
305
|
-
constructor(sessionId, config = {}) {
|
|
306
|
-
this.sessionId = sessionId;
|
|
307
|
-
this.sessionStartMs = Date.now();
|
|
308
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
309
|
-
this.fileTime = createFileTime(sessionId);
|
|
310
|
-
}
|
|
311
|
-
/**
|
|
312
|
-
* Canonical Map key for a file path. Read sources arrive with mixed
|
|
313
|
-
* separators/casing — the Read tool gives OS-native backslashes on Windows,
|
|
314
|
-
* while LSP-expanded and search-tool reads arrive slash-normalized from URIs.
|
|
315
|
-
* Keying the reads/edits/exemptions maps on the raw path made a read recorded
|
|
316
|
-
* under one form invisible to an edit checked under another, producing a false
|
|
317
|
-
* `zero_read` block despite the file having been read. `normalizeFilePath`
|
|
318
|
-
* folds separators and Windows casing to one key, so record and lookup always
|
|
319
|
-
* agree. Every map access in this class MUST key through here.
|
|
320
|
-
*/
|
|
321
|
-
key(filePath) {
|
|
322
|
-
return normalizeFilePath(filePath);
|
|
323
|
-
}
|
|
324
|
-
idleEvictMs() {
|
|
325
|
-
const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
|
|
326
|
-
return Number.isSafeInteger(value) && value > 0
|
|
327
|
-
? value
|
|
328
|
-
: READ_GUARD_IDLE_EVICT_MS_DEFAULT;
|
|
329
|
-
}
|
|
330
|
-
clearFileTimer(filePath) {
|
|
331
|
-
const timer = this.fileIdleTimers.get(filePath);
|
|
332
|
-
if (timer)
|
|
333
|
-
clearTimeout(timer);
|
|
334
|
-
this.fileIdleTimers.delete(filePath);
|
|
335
|
-
}
|
|
336
|
-
/**
|
|
337
|
-
* #1918: `enforceRecordCapForFile` (the per-file record cap) is the only
|
|
338
|
-
* `read-guard.ts` eviction path #1913 gave a bounded record. This whole-file
|
|
339
|
-
* evictor has four call sites — `enforceFileCap`'s consumed-file cap, its
|
|
340
|
-
* unconsumed-file cap, `touchFile`'s idle timer, and `forgetPath`'s
|
|
341
|
-
* external-delete cleanup — and none of them left any trace. A live
|
|
342
|
-
* regression in any of the four (e.g. a cap set too low) dropped a file's
|
|
343
|
-
* read/edit history with nothing in `~/.pi-lens/read-guard.log` to show it
|
|
344
|
-
* happened.
|
|
345
|
-
*
|
|
346
|
-
* Follows #1913's aggregate-after-first pattern for the three PRESSURE
|
|
347
|
-
* reasons (cap exhaustion, external delete): `incrementDegradationCount`
|
|
348
|
-
* keeps the exact per-file eviction tally (and its own power-of-two
|
|
349
|
-
* milestone rows in latency.log) while the read-guard.log line fires only
|
|
350
|
-
* on the rising edge, so a file evicted repeatedly across a session can't
|
|
351
|
-
* flood the log.
|
|
352
|
-
*
|
|
353
|
-
* `idle-timeout` is deliberately excluded from both the ledger tally and
|
|
354
|
-
* the log line (#1918 review F2): idle eviction is designed housekeeping,
|
|
355
|
-
* not a fault signal — a read-only session that idles out N files is
|
|
356
|
-
* healthy, ordinary behavior, and N is unbounded (every distinct file path
|
|
357
|
-
* touched that session is its own ledger subject). Recording it here would
|
|
358
|
-
* grow the ledger's tally map without bound and spam `pilens_health` in
|
|
359
|
-
* every healthy session, unlike the other three reasons, which are rare by
|
|
360
|
-
* construction (only real pressure or an explicit delete reaches them).
|
|
361
|
-
*/
|
|
362
|
-
evictFile(filePath, reason) {
|
|
363
|
-
this.clearFileTimer(filePath);
|
|
364
|
-
this.reads.delete(filePath);
|
|
365
|
-
this.edits.delete(filePath);
|
|
366
|
-
this.fileLastUsed.delete(filePath);
|
|
367
|
-
this.consumedReadFiles.delete(filePath);
|
|
368
|
-
this.writtenThisSession.delete(filePath);
|
|
369
|
-
// #1668 review F1: prune the reverse-pointing knownPathIndex entries
|
|
370
|
-
// too, so it never outlives the record it points at.
|
|
371
|
-
for (const [syntacticKey, stored] of this.knownPathIndex) {
|
|
372
|
-
if (stored === filePath)
|
|
373
|
-
this.knownPathIndex.delete(syntacticKey);
|
|
374
|
-
}
|
|
375
|
-
if (reason === "idle-timeout")
|
|
376
|
-
return;
|
|
377
|
-
const isRisingEdge = incrementDegradationCount({
|
|
378
|
-
kind: "read-guard-file-evicted",
|
|
379
|
-
subject: filePath,
|
|
380
|
-
reason: `evicted (${reason})`,
|
|
381
|
-
});
|
|
382
|
-
if (isRisingEdge) {
|
|
383
|
-
logReadGuardEvent({
|
|
384
|
-
event: "read_file_evicted",
|
|
385
|
-
sessionId: this.sessionId,
|
|
386
|
-
filePath,
|
|
387
|
-
metadata: { reason },
|
|
388
|
-
});
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
touchFile(filePath) {
|
|
392
|
-
const now = Date.now();
|
|
393
|
-
this.fileLastUsed.set(filePath, now);
|
|
394
|
-
this.clearFileTimer(filePath);
|
|
395
|
-
// An outstanding read is enforcement state, not a rebuildable cache entry.
|
|
396
|
-
// It must survive idle time and file-cap pressure until the edit consumes it.
|
|
397
|
-
if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
|
|
398
|
-
return;
|
|
399
|
-
const stamp = now;
|
|
400
|
-
const timer = setTimeout(() => {
|
|
401
|
-
if (this.fileLastUsed.get(filePath) !== stamp)
|
|
402
|
-
return;
|
|
403
|
-
// Never turn an outstanding read into a zero-read block through idle
|
|
404
|
-
// eviction. Only consumed reads and rebuildable edit history may expire.
|
|
405
|
-
if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
|
|
406
|
-
return;
|
|
407
|
-
// #1918 review F2: this reason is intentionally silent inside
|
|
408
|
-
// evictFile — idle eviction is routine housekeeping, not a fault, and
|
|
409
|
-
// every distinct file idling out this session is its own ledger
|
|
410
|
-
// subject, so recording it would grow unbounded and flood
|
|
411
|
-
// pilens_health on every healthy session. See evictFile's doc comment.
|
|
412
|
-
this.evictFile(filePath, "idle-timeout");
|
|
413
|
-
}, this.idleEvictMs());
|
|
414
|
-
timer.unref?.();
|
|
415
|
-
this.fileIdleTimers.set(filePath, timer);
|
|
416
|
-
}
|
|
417
|
-
enforceFileCap() {
|
|
418
|
-
while (this.reads.size > READ_GUARD_MAX_FILES) {
|
|
419
|
-
const victim = [...this.reads.keys()]
|
|
420
|
-
.filter((filePath) => this.consumedReadFiles.has(filePath))
|
|
421
|
-
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
|
|
422
|
-
if (!victim)
|
|
423
|
-
break;
|
|
424
|
-
this.evictFile(victim, "file-cap-consumed");
|
|
425
|
-
}
|
|
426
|
-
while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
|
|
427
|
-
const victim = [...this.reads.keys()]
|
|
428
|
-
.filter((filePath) => !this.consumedReadFiles.has(filePath))
|
|
429
|
-
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
|
|
430
|
-
if (!victim)
|
|
431
|
-
break;
|
|
432
|
-
// This is a normal read miss: a later edit must require a fresh read,
|
|
433
|
-
// never silently allow and never become a permanent hard-block.
|
|
434
|
-
this.evictFile(victim, "file-cap-unconsumed");
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
// --- Public API ---
|
|
438
|
-
/**
|
|
439
|
-
* Record that a file was read.
|
|
440
|
-
* Call this from the tool_call handler after any LSP expansion.
|
|
441
|
-
*/
|
|
442
|
-
recordRead(record, opts) {
|
|
443
|
-
const filePath = this.key(record.filePath);
|
|
444
|
-
if (opts?.supersedes) {
|
|
445
|
-
const records = this.reads.get(filePath);
|
|
446
|
-
const provisionalSource = `native-read:${opts.supersedes.toolCallId}:provisional`;
|
|
447
|
-
const provisionalIndex = records?.findIndex((candidate) => candidate.source === provisionalSource);
|
|
448
|
-
if (provisionalIndex !== undefined && provisionalIndex >= 0)
|
|
449
|
-
records.splice(provisionalIndex, 1);
|
|
450
|
-
}
|
|
451
|
-
// #1668 review F1: index by the existence-independent syntactic key
|
|
452
|
-
// while the file is (presumably) still on disk, so a later
|
|
453
|
-
// hasKnownPath/forgetPath lookup after an external delete can still
|
|
454
|
-
// find this entry's real key.
|
|
455
|
-
this.knownPathIndex.set(normalizeEphemeralMapKey(record.filePath), filePath);
|
|
456
|
-
const storedRecord = {
|
|
457
|
-
...record,
|
|
458
|
-
filePath,
|
|
459
|
-
lineHashes: record.lineHashes ??
|
|
460
|
-
captureLineHashes(filePath, record.effectiveOffset, record.effectiveLimit),
|
|
461
|
-
};
|
|
462
|
-
const arr = this.reads.get(storedRecord.filePath) ?? [];
|
|
463
|
-
this.consumedReadFiles.delete(storedRecord.filePath);
|
|
464
|
-
arr.push(storedRecord);
|
|
465
|
-
this.reads.set(storedRecord.filePath, arr);
|
|
466
|
-
// Capture BEFORE the trim: `readCountForFile` saturates at the cap once
|
|
467
|
-
// eviction starts, so it stops showing growth. `rawReadCountForFile` keeps
|
|
468
|
-
// the real arrival count observable alongside `evictedRecordCount`.
|
|
469
|
-
const rawReadCountForFile = arr.length;
|
|
470
|
-
const { evictedCount: evictedRecordCount, evictedCreditCount, evictedGenuineCount, } = enforceRecordCapForFile(arr);
|
|
471
|
-
this.touchFile(storedRecord.filePath);
|
|
472
|
-
this.enforceFileCap();
|
|
473
|
-
logReadGuardEvent({
|
|
474
|
-
event: "read_recorded",
|
|
475
|
-
sessionId: this.sessionId,
|
|
476
|
-
filePath: storedRecord.filePath,
|
|
477
|
-
requestedOffset: storedRecord.requestedOffset,
|
|
478
|
-
requestedLimit: storedRecord.requestedLimit,
|
|
479
|
-
effectiveOffset: storedRecord.effectiveOffset,
|
|
480
|
-
effectiveLimit: storedRecord.effectiveLimit,
|
|
481
|
-
symbol: storedRecord.enclosingSymbol?.name,
|
|
482
|
-
symbolKind: storedRecord.enclosingSymbol?.kind,
|
|
483
|
-
symbolStartLine: storedRecord.enclosingSymbol?.startLine,
|
|
484
|
-
symbolEndLine: storedRecord.enclosingSymbol?.endLine,
|
|
485
|
-
metadata: {
|
|
486
|
-
expandedByLsp: storedRecord.expandedByLsp,
|
|
487
|
-
turnIndex: storedRecord.turnIndex,
|
|
488
|
-
writeIndex: storedRecord.writeIndex,
|
|
489
|
-
readCountForFile: arr.length,
|
|
490
|
-
hashLineCount: Object.keys(storedRecord.lineHashes ?? {}).length,
|
|
491
|
-
...(storedRecord.source !== undefined && {
|
|
492
|
-
source: storedRecord.source,
|
|
493
|
-
}),
|
|
494
|
-
...(storedRecord.searchCredit !== undefined && {
|
|
495
|
-
searchCreditReason: storedRecord.searchCredit.reason,
|
|
496
|
-
searchCreditMarginBefore: storedRecord.searchCredit.marginBefore,
|
|
497
|
-
searchCreditMarginAfter: storedRecord.searchCredit.marginAfter,
|
|
498
|
-
}),
|
|
499
|
-
...(evictedRecordCount > 0 && {
|
|
500
|
-
evictedRecordCount,
|
|
501
|
-
rawReadCountForFile,
|
|
502
|
-
}),
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
// #1913: the eviction counters above ride `read_recorded`, which is
|
|
506
|
-
// gated behind PI_LENS_READ_GUARD_VERBOSE — off by default. That left a
|
|
507
|
-
// live eviction regression with no trace at default verbosity.
|
|
508
|
-
//
|
|
509
|
-
// review F1: a naive "emit every trim" fix floods read-guard.log once a
|
|
510
|
-
// hot file sits past the cap — every later push trims exactly 1 record
|
|
511
|
-
// (the array is always AT the cap before the push, so `excess` is
|
|
512
|
-
// always exactly 1), so 300 recordRead calls on one file is ~172
|
|
513
|
-
// identical always-on lines, and can rotate `edit_blocked` records out
|
|
514
|
-
// of the 1MB cap. `rawReadCountForFile` also freezes at cap+1 forever,
|
|
515
|
-
// so a raw per-trim record can't even discriminate thrash severity.
|
|
516
|
-
//
|
|
517
|
-
// Fix: emit the read-guard.log line ONCE per file per session, on the
|
|
518
|
-
// FIRST trim only. `trimAccumulators` still updates on every trim,
|
|
519
|
-
// queryable via `getTrimStats` — the running totals a health surface or
|
|
520
|
-
// a future emission point would need are never lost, even though
|
|
521
|
-
// read-guard.log stops re-announcing them. "Have we already logged
|
|
522
|
-
// this file's first trim" routes through the degradation ledger's own
|
|
523
|
-
// rising-edge tally (`incrementDegradationCount` — the pattern
|
|
524
|
-
// CLAUDE.md names for repeated degradations) instead of a hand-rolled
|
|
525
|
-
// per-file Set; subsequent trims still call it, so the ledger's own
|
|
526
|
-
// entry for this (kind, subject) keeps its reason text and count
|
|
527
|
-
// current for the health/degradation summary even after read-guard.log
|
|
528
|
-
// goes quiet.
|
|
529
|
-
if (evictedRecordCount > 0) {
|
|
530
|
-
const key = storedRecord.filePath;
|
|
531
|
-
const acc = this.trimAccumulators.get(key) ?? {
|
|
532
|
-
totalEvicted: 0,
|
|
533
|
-
evictedCreditCount: 0,
|
|
534
|
-
evictedGenuineCount: 0,
|
|
535
|
-
trimEventCount: 0,
|
|
536
|
-
};
|
|
537
|
-
acc.totalEvicted += evictedRecordCount;
|
|
538
|
-
acc.evictedCreditCount += evictedCreditCount;
|
|
539
|
-
acc.evictedGenuineCount += evictedGenuineCount;
|
|
540
|
-
acc.trimEventCount += 1;
|
|
541
|
-
this.trimAccumulators.set(key, acc);
|
|
542
|
-
const isRisingEdge = incrementDegradationCount({
|
|
543
|
-
kind: "read-guard-record-cap-trim",
|
|
544
|
-
subject: key,
|
|
545
|
-
reason: `trim #${acc.trimEventCount}: evicted ${evictedRecordCount} (credit ${evictedCreditCount}, genuine ${evictedGenuineCount})`,
|
|
546
|
-
});
|
|
547
|
-
if (isRisingEdge) {
|
|
548
|
-
logReadGuardEvent({
|
|
549
|
-
event: "read_cap_trimmed",
|
|
550
|
-
sessionId: this.sessionId,
|
|
551
|
-
filePath: storedRecord.filePath,
|
|
552
|
-
metadata: {
|
|
553
|
-
trimEventCount: acc.trimEventCount,
|
|
554
|
-
evictedRecordCount: acc.totalEvicted,
|
|
555
|
-
evictedCreditCount: acc.evictedCreditCount,
|
|
556
|
-
evictedGenuineCount: acc.evictedGenuineCount,
|
|
557
|
-
rawReadCountForFile,
|
|
558
|
-
},
|
|
559
|
-
});
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
// Also update FileTime stamp for this file
|
|
563
|
-
this.fileTime.read(storedRecord.filePath);
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Record a structured symbol read (the `readSymbol` engine capability / its
|
|
567
|
-
* MCP mirror) as a genuine read of that symbol's line range — the
|
|
568
|
-
* read-substitute tie-in for #245. `readSymbol` returns the verbatim body, so
|
|
569
|
-
* an edit within [startLine, endLine] is legitimately covered, exactly like a
|
|
570
|
-
* TS/LSP-expanded read that delivered the whole enclosing symbol. Line hashes
|
|
571
|
-
* for the range are captured by recordRead, so the edit is also
|
|
572
|
-
* snapshot-verified (drift since the symbol read still blocks).
|
|
573
|
-
*
|
|
574
|
-
* Intentionally NOT offered for module *outlines*: an outline shows a symbol's
|
|
575
|
-
* shape (name/signature/range), not its body, so granting edit coverage from
|
|
576
|
-
* it would let the agent edit lines it never saw. Only a body-delivering read
|
|
577
|
-
* (readSymbol / raw Read) records coverage.
|
|
578
|
-
*/
|
|
579
|
-
recordSymbolRead(filePath, symbol, turnIndex, writeIndex) {
|
|
580
|
-
const span = Math.max(1, symbol.endLine - symbol.startLine + 1);
|
|
581
|
-
this.recordRead({
|
|
582
|
-
filePath,
|
|
583
|
-
requestedOffset: symbol.startLine,
|
|
584
|
-
requestedLimit: span,
|
|
585
|
-
effectiveOffset: symbol.startLine,
|
|
586
|
-
effectiveLimit: span,
|
|
587
|
-
expandedByLsp: false,
|
|
588
|
-
enclosingSymbol: {
|
|
589
|
-
name: symbol.name,
|
|
590
|
-
kind: symbol.kind,
|
|
591
|
-
startLine: symbol.startLine,
|
|
592
|
-
endLine: symbol.endLine,
|
|
593
|
-
},
|
|
594
|
-
turnIndex,
|
|
595
|
-
writeIndex,
|
|
596
|
-
timestamp: Date.now(),
|
|
597
|
-
});
|
|
598
|
-
}
|
|
599
|
-
/**
|
|
600
|
-
* Check if an edit should be allowed.
|
|
601
|
-
* Returns verdict with action and optional reason for blocking.
|
|
602
|
-
*/
|
|
603
|
-
checkEdit(filePath, touchedLines, editRanges, options) {
|
|
604
|
-
// Canonicalize once: every map lookup below (and every private helper this
|
|
605
|
-
// passes filePath to) must agree with how recordRead keyed the read.
|
|
606
|
-
filePath = this.key(filePath);
|
|
607
|
-
if (this.reads.has(filePath) || this.edits.has(filePath))
|
|
608
|
-
this.touchFile(filePath);
|
|
609
|
-
// Check exemptions
|
|
610
|
-
if (this.exemptions.has(filePath)) {
|
|
611
|
-
this.exemptions.delete(filePath); // One-time use
|
|
612
|
-
const verdict = this.allow();
|
|
613
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
614
|
-
reasonKind: "manual_exemption",
|
|
615
|
-
});
|
|
616
|
-
return verdict;
|
|
617
|
-
}
|
|
618
|
-
// Check config exemptions by pattern
|
|
619
|
-
const exemptionMode = this.getExemptionMode(filePath);
|
|
620
|
-
if (exemptionMode === "allow") {
|
|
621
|
-
const verdict = this.allow();
|
|
622
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
623
|
-
reasonKind: "pattern_exemption",
|
|
624
|
-
exemptionMode,
|
|
625
|
-
});
|
|
626
|
-
return verdict;
|
|
627
|
-
}
|
|
628
|
-
// "warn" pattern exemptions downgrade all blocking verdicts to warnings.
|
|
629
|
-
const effectiveMode = exemptionMode === "warn" ? "warn" : undefined;
|
|
630
|
-
// 1. Zero-read check
|
|
631
|
-
const fileReads = this.reads.get(filePath);
|
|
632
|
-
if (!fileReads || fileReads.length === 0) {
|
|
633
|
-
// If the file was written after this session started, the agent authored
|
|
634
|
-
// it in this session (via Write or any other mechanism). Allow the edit —
|
|
635
|
-
// a synthetic read would have been injected for Write tool calls, but
|
|
636
|
-
// this catches cases where the write bypassed the hook or the session
|
|
637
|
-
// restarted between write and edit.
|
|
638
|
-
if (this.wasWrittenThisSession(filePath)) {
|
|
639
|
-
this.injectCreationRead(filePath, 0, 0);
|
|
640
|
-
const verdict = this.allow();
|
|
641
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
642
|
-
reasonKind: "session_authored",
|
|
643
|
-
});
|
|
644
|
-
return verdict;
|
|
645
|
-
}
|
|
646
|
-
const verdict = this.blockOrWarn("zero-read", `🔄 RETRYABLE — Edit without read: you have not read \`${filePath}\` in this conversation. Read it first, then retry: \`read path="${filePath}"\`.`, undefined, effectiveMode);
|
|
647
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
648
|
-
reasonKind: "zero_read",
|
|
649
|
-
});
|
|
650
|
-
return verdict;
|
|
651
|
-
}
|
|
652
|
-
const lastBoundRead = [...fileReads]
|
|
653
|
-
.reverse()
|
|
654
|
-
.find((read) => read.contentBinding !== undefined);
|
|
655
|
-
if (lastBoundRead?.contentBinding &&
|
|
656
|
-
!currentContentMatchesBinding(filePath, lastBoundRead.contentBinding)) {
|
|
657
|
-
const verdict = this.blockOrWarn("file-modified", `🔄 RETRYABLE — File modified since read\n\nYou last read \`${filePath}\` at ${new Date(lastBoundRead.timestamp).toISOString()}.\nThe file content no longer matches the bridge-recorded read.\n\nYour mental model is out of sync with the actual file content.\nTo proceed:\n 1. Re-read the file: \`read path="${filePath}"\``, undefined, effectiveMode);
|
|
658
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
659
|
-
reasonKind: "file_modified",
|
|
660
|
-
lastReadTimestamp: lastBoundRead.timestamp,
|
|
661
|
-
contentBindingMismatch: true,
|
|
662
|
-
});
|
|
663
|
-
return verdict;
|
|
664
|
-
}
|
|
665
|
-
// 2. FileTime check (actual staleness)
|
|
666
|
-
let ignoredOwnEditStaleness = false;
|
|
667
|
-
let ignoredHashStaleness = false;
|
|
668
|
-
let ignoredOldTextResolvedStaleness = false;
|
|
669
|
-
if (this.fileTime.hasChanged(filePath)) {
|
|
670
|
-
const lastRead = fileReads[fileReads.length - 1];
|
|
671
|
-
if (options?.oldTextResolved === true) {
|
|
672
|
-
// The host-facing preflight sets this only after oldText resolves to
|
|
673
|
-
// exactly one span in the live bytes. That content evidence is newer
|
|
674
|
-
// and stronger than FileTime's coarse external-write signal, matching
|
|
675
|
-
// the skipSnapshotCheck exception at the later range-stale gate.
|
|
676
|
-
ignoredOldTextResolvedStaleness = true;
|
|
677
|
-
}
|
|
678
|
-
else if (this.canTreatStalenessAsOwnPriorEdit(filePath, lastRead.timestamp)) {
|
|
679
|
-
ignoredOwnEditStaleness = true;
|
|
680
|
-
}
|
|
681
|
-
else if (this.canIgnoreStalenessByHashes(filePath, fileReads, touchedLines, editRanges)) {
|
|
682
|
-
ignoredHashStaleness = true;
|
|
683
|
-
}
|
|
684
|
-
else {
|
|
685
|
-
const verdict = this.blockOrWarn("file-modified", `🔄 RETRYABLE — File modified since read\n\nYou last read \`${filePath}\` at ${new Date(lastRead.timestamp).toISOString()}.\nThe file has been modified on disk since then (auto-format, external tool, or previous edit).\n\nYour mental model is out of sync with the actual file content.\nTo proceed:\n 1. Re-read the file: \`read path="${filePath}"\``, undefined, effectiveMode);
|
|
686
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
687
|
-
reasonKind: "file_modified",
|
|
688
|
-
lastReadTimestamp: lastRead.timestamp,
|
|
689
|
-
});
|
|
690
|
-
return verdict;
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
// If no line range specified, we can only check zero-read and FileTime
|
|
694
|
-
if (!touchedLines) {
|
|
695
|
-
const verdict = this.allow();
|
|
696
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
697
|
-
reasonKind: "no_line_info",
|
|
698
|
-
});
|
|
699
|
-
return verdict;
|
|
700
|
-
}
|
|
701
|
-
// 3. Range coverage check
|
|
702
|
-
// When the edit touches multiple disjoint spots (e.g. rename across 4 tool
|
|
703
|
-
// registrations), check each spot independently. Collapsing to a bounding
|
|
704
|
-
// box would falsely flag reads that cover exactly the right lines.
|
|
705
|
-
const rangesToCheck = editRanges && editRanges.length > 1 ? editRanges : [touchedLines];
|
|
706
|
-
let viaSymbol = false;
|
|
707
|
-
for (const range of rangesToCheck) {
|
|
708
|
-
const snapshotValidation = this.validateRangeSnapshot(filePath, range, !!options?.skipSnapshotCheck);
|
|
709
|
-
const coverage = this.checkCoverage(filePath, range);
|
|
710
|
-
if (!coverage.covered) {
|
|
711
|
-
const lastRead = fileReads[fileReads.length - 1];
|
|
712
|
-
const [editStart, editEnd] = range;
|
|
713
|
-
const lastReadEnd = lastRead.effectiveOffset + lastRead.effectiveLimit - 1;
|
|
714
|
-
// If oldText was resolved (content-verified), the model demonstrably
|
|
715
|
-
// knew the content it's replacing — line drift from prior edits in
|
|
716
|
-
// the session is the likely cause. Downgrade to warn rather than block.
|
|
717
|
-
const outOfRangeMode = options?.oldTextResolved
|
|
718
|
-
? "warn"
|
|
719
|
-
: effectiveMode;
|
|
720
|
-
const verdict = this.blockOrWarn("out-of-range", `🔄 RETRYABLE — Edit outside read range\n\nYou read \`${filePath}\` lines ${lastRead.effectiveOffset}-${lastReadEnd}${lastRead.enclosingSymbol ? ` (${lastRead.enclosingSymbol.kind} \`${lastRead.enclosingSymbol.name}\`)` : ""}, but your edit touches lines ${editStart}-${editEnd}.\n\nRead the relevant section first, then retry the edit:\n \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\``, {
|
|
721
|
-
editRange: range,
|
|
722
|
-
readRanges: fileReads.map((r) => ({
|
|
723
|
-
start: r.effectiveOffset,
|
|
724
|
-
end: r.effectiveOffset + r.effectiveLimit - 1,
|
|
725
|
-
})),
|
|
726
|
-
symbolRanges: fileReads
|
|
727
|
-
.filter((r) => r.enclosingSymbol)
|
|
728
|
-
.map((r) => ({
|
|
729
|
-
name: r.enclosingSymbol.name,
|
|
730
|
-
start: r.enclosingSymbol.startLine,
|
|
731
|
-
end: r.enclosingSymbol.endLine,
|
|
732
|
-
})),
|
|
733
|
-
}, outOfRangeMode);
|
|
734
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
735
|
-
reasonKind: "out_of_range",
|
|
736
|
-
oldTextResolved: options?.oldTextResolved ?? false,
|
|
737
|
-
});
|
|
738
|
-
return verdict;
|
|
739
|
-
}
|
|
740
|
-
if (snapshotValidation.shouldBlock && !options?.skipSnapshotCheck) {
|
|
741
|
-
const [editStart, editEnd] = range;
|
|
742
|
-
// Grace period: when the snapshot is stale because THIS session's own
|
|
743
|
-
// earlier edit shifted line numbers (ignoredOwnEditStaleness), and
|
|
744
|
-
// the agent read the file recently, downgrade to a warning rather
|
|
745
|
-
// than blocking. The agent has fresh context — they just don't
|
|
746
|
-
// know the exact new line numbers after the shift.
|
|
747
|
-
const RANGE_STALE_GRACE_MS = 60_000;
|
|
748
|
-
const lastRead = fileReads[fileReads.length - 1];
|
|
749
|
-
const graceActive = ignoredOwnEditStaleness &&
|
|
750
|
-
Date.now() - lastRead.timestamp < RANGE_STALE_GRACE_MS;
|
|
751
|
-
// Content-verified relocation: if the lines the agent read have
|
|
752
|
-
// merely shifted (same content, new offset), tell them exactly where
|
|
753
|
-
// so they re-target in one turn. We hint rather than silently
|
|
754
|
-
// re-apply: the host applies native range edits positionally and
|
|
755
|
-
// can't re-verify, so an unverified auto-relocation could corrupt.
|
|
756
|
-
const relocation = this.findRelocation(filePath, fileReads, range);
|
|
757
|
-
const relocationNote = relocation
|
|
758
|
-
? `\n\n📍 The content you read at lines ${relocation.from[0]}-${relocation.from[1]} now appears unchanged at lines ${relocation.to[0]}-${relocation.to[1]} — it shifted position. Re-target your edit to lines ${relocation.to[0]}-${relocation.to[1]}.`
|
|
759
|
-
: "";
|
|
760
|
-
const verdict = this.blockOrWarn("range-stale", `🔄 RETRYABLE — Edit range changed since read\n\nYou are editing \`${filePath}\` lines ${editStart}-${editEnd}, but those lines no longer match the content you read earlier.${relocationNote}\n\nRe-read the relevant section, then retry the edit using the current line range/content:\n \`read path="${filePath}" offset=${Math.max(1, editStart - 5)} limit=${Math.min(30, editEnd - editStart + 10)}\``, {
|
|
761
|
-
editRange: range,
|
|
762
|
-
readRanges: fileReads.map((r) => ({
|
|
763
|
-
start: r.effectiveOffset,
|
|
764
|
-
end: r.effectiveOffset + r.effectiveLimit - 1,
|
|
765
|
-
})),
|
|
766
|
-
symbolRanges: fileReads
|
|
767
|
-
.filter((r) => r.enclosingSymbol)
|
|
768
|
-
.map((r) => ({
|
|
769
|
-
name: r.enclosingSymbol.name,
|
|
770
|
-
start: r.enclosingSymbol.startLine,
|
|
771
|
-
end: r.enclosingSymbol.endLine,
|
|
772
|
-
})),
|
|
773
|
-
snapshot: {
|
|
774
|
-
status: snapshotValidation.status,
|
|
775
|
-
mismatchedLines: snapshotValidation.mismatchedLines,
|
|
776
|
-
missingLines: snapshotValidation.missingLines,
|
|
777
|
-
},
|
|
778
|
-
...(relocation ? { relocation } : {}),
|
|
779
|
-
}, graceActive ? "warn" : effectiveMode);
|
|
780
|
-
// Offer auto-apply only for a single-range edit: we relocated exactly
|
|
781
|
-
// one range, so shifting it is the whole edit. A multi-range edit
|
|
782
|
-
// could have other drifted spots we returned before checking, so it
|
|
783
|
-
// stays a hint.
|
|
784
|
-
if (relocation && rangesToCheck.length === 1) {
|
|
785
|
-
verdict.relocation = relocation;
|
|
786
|
-
}
|
|
787
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
788
|
-
reasonKind: "range_stale",
|
|
789
|
-
range,
|
|
790
|
-
mismatchedLines: snapshotValidation.mismatchedLines.slice(0, 20),
|
|
791
|
-
graceActive,
|
|
792
|
-
relocatedTo: relocation?.to ?? null,
|
|
793
|
-
relocationAutoApplyOffered: !!verdict.relocation,
|
|
794
|
-
});
|
|
795
|
-
return verdict;
|
|
796
|
-
}
|
|
797
|
-
if (coverage.viaSymbol)
|
|
798
|
-
viaSymbol = true;
|
|
799
|
-
}
|
|
800
|
-
const verdict = this.allow();
|
|
801
|
-
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
802
|
-
reasonKind: ignoredOldTextResolvedStaleness
|
|
803
|
-
? "file_modified_oldtext_unique"
|
|
804
|
-
: viaSymbol
|
|
805
|
-
? "symbol_coverage"
|
|
806
|
-
: "range_coverage",
|
|
807
|
-
viaSymbol,
|
|
808
|
-
ignoredOwnEditStaleness,
|
|
809
|
-
ignoredHashStaleness,
|
|
810
|
-
oldTextResolved: ignoredOldTextResolvedStaleness,
|
|
811
|
-
});
|
|
812
|
-
return verdict;
|
|
813
|
-
}
|
|
814
|
-
/**
|
|
815
|
-
* Check if this is a new file (no existing file on disk).
|
|
816
|
-
* New file writes are exempt from the guard.
|
|
817
|
-
*/
|
|
818
|
-
isNewFile(filePath) {
|
|
819
|
-
try {
|
|
820
|
-
return !fs.existsSync(filePath);
|
|
821
|
-
}
|
|
822
|
-
catch {
|
|
823
|
-
return true; // Assume new if we can't stat
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Whether pi-lens has any record of this path from a read or write this
|
|
828
|
-
* session (#1668). Gates external-delete detection: a path pi-lens never
|
|
829
|
-
* read or wrote is one no LSP server's cache was ever told about through
|
|
830
|
-
* us, so an `rm` naming it carries no signal worth checking disk for —
|
|
831
|
-
* this is a lookup against state already being tracked, never a fresh
|
|
832
|
-
* filesystem stat over an unbounded path set.
|
|
833
|
-
*
|
|
834
|
-
* MUST go through `knownPathIndex`, not `this.key(filePath)` directly
|
|
835
|
-
* (#1668 review F1): this is called AFTER a bash delete has already
|
|
836
|
-
* landed, when `filePath` no longer exists — `this.key()` at that point
|
|
837
|
-
* returns a lowercased-tail key, not the real-casing key `recordRead`/
|
|
838
|
-
* `recordWritten` stored while the file was still on disk. For a
|
|
839
|
-
* mixed-case basename (`MyModule.ts`) the two keys differ and a direct
|
|
840
|
-
* `this.key()` lookup always misses.
|
|
841
|
-
*/
|
|
842
|
-
hasKnownPath(filePath) {
|
|
843
|
-
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
844
|
-
return (stored !== undefined &&
|
|
845
|
-
(this.reads.has(stored) || this.writtenThisSession.has(stored)));
|
|
846
|
-
}
|
|
847
|
-
/**
|
|
848
|
-
* Every path this guard has a read or a write record for, as normalized keys
|
|
849
|
-
* (#2430).
|
|
850
|
-
*
|
|
851
|
-
* This is the read-guard half of the observational net's TRACKED-FILE SET:
|
|
852
|
-
* the files pi-lens has actually seen this session, which the `agent_settled`
|
|
853
|
-
* sweep hash-checks instead of walking the workspace. It reads state already
|
|
854
|
-
* held in memory and stats nothing; both backing containers are capped
|
|
855
|
-
* (`READ_GUARD_MAX_UNCONSUMED_FILES`), so the result is bounded by
|
|
856
|
-
* construction.
|
|
857
|
-
*
|
|
858
|
-
* `touchFile` is deliberately NOT called: enumerating is not using, and
|
|
859
|
-
* refreshing every entry's idle clock on each sweep would make the idle
|
|
860
|
-
* eviction unreachable.
|
|
861
|
-
*/
|
|
862
|
-
getTrackedPaths() {
|
|
863
|
-
const tracked = new Set(this.reads.keys());
|
|
864
|
-
for (const written of this.writtenThisSession)
|
|
865
|
-
tracked.add(written);
|
|
866
|
-
return [...tracked];
|
|
867
|
-
}
|
|
868
|
-
/**
|
|
869
|
-
* Drop all record of a path pi-lens confirmed no longer exists on disk
|
|
870
|
-
* (#1668, external delete). Without this a later write reusing the same
|
|
871
|
-
* path would inherit a stale writtenThisSession/reads entry from before
|
|
872
|
-
* the delete, and a repeat `rm` of the same already-gone path would keep
|
|
873
|
-
* matching {@link hasKnownPath} and re-emitting a type-3 notification.
|
|
874
|
-
*
|
|
875
|
-
* MUST evict through `knownPathIndex`, not `this.key(filePath)` directly
|
|
876
|
-
* (#1668 review F1) — same reasoning as {@link hasKnownPath}: `filePath`
|
|
877
|
-
* is already gone from disk by the time this runs, so recomputing
|
|
878
|
-
* `this.key()` targets the wrong (lowercased-tail) key for a mixed-case
|
|
879
|
-
* basename and leaves the real entry behind un-evicted.
|
|
880
|
-
*/
|
|
881
|
-
forgetPath(filePath) {
|
|
882
|
-
const stored = this.knownPathIndex.get(normalizeEphemeralMapKey(filePath));
|
|
883
|
-
this.evictFile(stored ?? this.key(filePath), "external-delete");
|
|
884
|
-
}
|
|
885
|
-
/**
|
|
886
|
-
* Mark a file as pending creation (Write tool to a non-existing file).
|
|
887
|
-
* Must be called from the tool_call handler before the write lands so
|
|
888
|
-
* isNewFile() still returns true. recordWritten will inject a synthetic
|
|
889
|
-
* read so immediate follow-up edits are not blocked by zero_read.
|
|
890
|
-
*
|
|
891
|
-
* Keyed by the existence-INDEPENDENT syntactic spelling
|
|
892
|
-
* (`normalizeEphemeralMapKey`, the same key `knownPathIndex` uses), NOT by
|
|
893
|
-
* `this.key()` (#3163). This entry is written while the file is still
|
|
894
|
-
* ABSENT and read back by `recordWritten` once it EXISTS — the one state
|
|
895
|
-
* change `normalizeFilePath` itself branches on — so a `this.key()` on each
|
|
896
|
-
* side answered with two different strings for any spelling whose key moves
|
|
897
|
-
* when the file appears (win32: a mixed-case basename, lower-cased in the
|
|
898
|
-
* absent branch by `resolveNonExisting`; POSIX since #3098: a parent whose
|
|
899
|
-
* canonical casing is adopted once `realpathSync.native` succeeds). The
|
|
900
|
-
* entry was then orphaned — nothing prunes `pendingCreations` — and the
|
|
901
|
-
* creation read never injected. The syntactic key never touches the
|
|
902
|
-
* filesystem, so it cannot move; the REAL key the injected read needs is
|
|
903
|
-
* derived by `recordWritten` itself and is not stored here.
|
|
904
|
-
*/
|
|
905
|
-
noteCreatedFile(filePath, turnIndex, writeIndex) {
|
|
906
|
-
this.pendingCreations.set(normalizeEphemeralMapKey(filePath), {
|
|
907
|
-
turnIndex,
|
|
908
|
-
writeIndex,
|
|
909
|
-
});
|
|
910
|
-
}
|
|
911
|
-
/**
|
|
912
|
-
* Refresh the FileTime stamp after the model's own write lands on disk.
|
|
913
|
-
* Call this from the tool_result handler so the next checkEdit on the same
|
|
914
|
-
* file doesn't see "file_modified" caused by our own previous edit.
|
|
915
|
-
*/
|
|
916
|
-
recordWritten(rawFilePath) {
|
|
917
|
-
const filePath = this.key(rawFilePath);
|
|
918
|
-
this.unchangedThisSession.delete(filePath);
|
|
919
|
-
// #1668 review F1: index by the existence-independent syntactic key
|
|
920
|
-
// (see `knownPathIndex`) so a later hasKnownPath/forgetPath lookup
|
|
921
|
-
// after an external delete can still find this entry's real key.
|
|
922
|
-
this.knownPathIndex.set(normalizeEphemeralMapKey(rawFilePath), filePath);
|
|
923
|
-
this.fileTime.read(filePath);
|
|
924
|
-
this.writtenThisSession.add(filePath);
|
|
925
|
-
if (this.reads.has(filePath))
|
|
926
|
-
this.consumedReadFiles.add(filePath);
|
|
927
|
-
this.touchFile(filePath);
|
|
928
|
-
this.enforceFileCap();
|
|
929
|
-
// #3163: `noteCreatedFile` announced this creation while the file was
|
|
930
|
-
// still absent, so the announcement is keyed by the syntactic spelling —
|
|
931
|
-
// look it up the same way, never through the existence-dependent
|
|
932
|
-
// `filePath` above. `injectCreationRead` still receives the real key.
|
|
933
|
-
const creationKey = normalizeEphemeralMapKey(rawFilePath);
|
|
934
|
-
const creation = this.pendingCreations.get(creationKey);
|
|
935
|
-
if (creation) {
|
|
936
|
-
this.pendingCreations.delete(creationKey);
|
|
937
|
-
this.injectCreationRead(filePath, creation.turnIndex, creation.writeIndex);
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
/** Record that a recognized mutation had complete evidence but changed no bytes. */
|
|
941
|
-
recordUnchanged(rawFilePath) {
|
|
942
|
-
const filePath = this.key(rawFilePath);
|
|
943
|
-
// A no-op command is scoped to this command. It must not erase a
|
|
944
|
-
// confirmed Write from earlier in the session.
|
|
945
|
-
if (!this.writtenThisSession.has(filePath))
|
|
946
|
-
this.unchangedThisSession.add(filePath);
|
|
947
|
-
}
|
|
948
|
-
/**
|
|
949
|
-
* Add a one-time exemption for a file.
|
|
950
|
-
* Called via /lens-allow-edit command.
|
|
951
|
-
*/
|
|
952
|
-
addExemption(filePath) {
|
|
953
|
-
this.exemptions.add(this.key(filePath));
|
|
954
|
-
logReadGuardEvent({
|
|
955
|
-
event: "exemption_added",
|
|
956
|
-
sessionId: this.sessionId,
|
|
957
|
-
filePath,
|
|
958
|
-
metadata: {
|
|
959
|
-
source: "lens-allow-edit",
|
|
960
|
-
},
|
|
961
|
-
});
|
|
962
|
-
}
|
|
963
|
-
/**
|
|
964
|
-
* Get summary statistics for /lens-health.
|
|
965
|
-
*/
|
|
966
|
-
getSummary() {
|
|
967
|
-
let totalEdits = 0;
|
|
968
|
-
let totalBlocks = 0;
|
|
969
|
-
let lspExpansionsHelped = 0;
|
|
970
|
-
const byReason = {};
|
|
971
|
-
const byFile = {};
|
|
972
|
-
for (const [filePath, records] of this.edits) {
|
|
973
|
-
for (const record of records) {
|
|
974
|
-
totalEdits++;
|
|
975
|
-
byFile[filePath] = byFile[filePath] ?? { edits: 0, blocks: 0 };
|
|
976
|
-
byFile[filePath].edits++;
|
|
977
|
-
if (record.verdict === "blocked") {
|
|
978
|
-
totalBlocks++;
|
|
979
|
-
byFile[filePath].blocks++;
|
|
980
|
-
}
|
|
981
|
-
if (record.reason) {
|
|
982
|
-
byReason[record.reason] = (byReason[record.reason] ?? 0) + 1;
|
|
983
|
-
}
|
|
984
|
-
// Count LSP expansions that allowed an edit
|
|
985
|
-
if (record.precedingReads.some((r) => r.expandedByLsp) &&
|
|
986
|
-
record.verdict === "allowed") {
|
|
987
|
-
lspExpansionsHelped++;
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return {
|
|
992
|
-
totalEdits,
|
|
993
|
-
totalBlocks,
|
|
994
|
-
byReason,
|
|
995
|
-
byFile,
|
|
996
|
-
lspExpansionsHelped,
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Get all read records for a file (for debugging).
|
|
1001
|
-
*/
|
|
1002
|
-
getReadHistory(filePath) {
|
|
1003
|
-
const key = this.key(filePath);
|
|
1004
|
-
if (this.reads.has(key))
|
|
1005
|
-
this.touchFile(key);
|
|
1006
|
-
return this.reads.get(key) ?? [];
|
|
1007
|
-
}
|
|
1008
|
-
/**
|
|
1009
|
-
* Session-lifetime record-cap trim totals for a file (#1913 review F1).
|
|
1010
|
-
* `recordRead` only writes ONE `read_cap_trimmed` read-guard.log line per
|
|
1011
|
-
* file per session (on the first trim), so this is the running-totals
|
|
1012
|
-
* surface for everything after that — the exact split this class already
|
|
1013
|
-
* computes on every trim, not re-derivable from `getReadHistory` alone
|
|
1014
|
-
* (which only shows the SURVIVING records, not what was evicted).
|
|
1015
|
-
*/
|
|
1016
|
-
getTrimStats(filePath) {
|
|
1017
|
-
const stats = this.trimAccumulators.get(this.key(filePath));
|
|
1018
|
-
return stats ? { ...stats } : undefined;
|
|
1019
|
-
}
|
|
1020
|
-
/**
|
|
1021
|
-
* Snapshot the read-set for persistence across a session resume (#1041).
|
|
1022
|
-
* Mirrors widget-state's `exportWidgetState`: the Map is emitted as
|
|
1023
|
-
* `[key, records]` tuples, keys already in `normalizeFilePath` form. Only
|
|
1024
|
-
* `reads` is serialized — it is the payload `checkEdit`'s zero-read/coverage
|
|
1025
|
-
* checks consult. Safe to call even when the guard is disabled (an empty or
|
|
1026
|
-
* never-populated `reads` simply exports zero entries).
|
|
1027
|
-
*/
|
|
1028
|
-
exportState() {
|
|
1029
|
-
return {
|
|
1030
|
-
version: READ_GUARD_STATE_VERSION,
|
|
1031
|
-
reads: [...this.reads.entries()].map(([key, records]) => [
|
|
1032
|
-
key,
|
|
1033
|
-
records.map((record) => ({ ...record })),
|
|
1034
|
-
]),
|
|
1035
|
-
};
|
|
1036
|
-
}
|
|
1037
|
-
/**
|
|
1038
|
-
* Rehydrate a persisted read-set (#1041) into this (fresh, post-resume)
|
|
1039
|
-
* guard, with mandatory staleness reconciliation: each read is re-verified
|
|
1040
|
-
* against the CURRENT on-disk content via its recorded `lineHashes`, and any
|
|
1041
|
-
* read whose file changed (or no longer exists, or that carries no verifiable
|
|
1042
|
-
* hashes) is DROPPED. A rehydrated read must never mask a real staleness — a
|
|
1043
|
-
* resume must not let the agent edit a file that changed on disk while it
|
|
1044
|
-
* believed it held a fresh read. Kept reads are replayed through
|
|
1045
|
-
* {@link recordRead}, which re-keys through {@link key} (idempotent — the
|
|
1046
|
-
* exported keys are already normalized) and re-stamps FileTime so the next
|
|
1047
|
-
* `checkEdit` sees a consistent baseline. Version-guarded and null-safe:
|
|
1048
|
-
* `undefined` / a mismatched version / a missing field loads as "no prior
|
|
1049
|
-
* reads". Returns a count of imported vs dropped reads for logging.
|
|
1050
|
-
*/
|
|
1051
|
-
importState(state) {
|
|
1052
|
-
const result = { imported: 0, dropped: 0 };
|
|
1053
|
-
if (!state || state.version !== READ_GUARD_STATE_VERSION)
|
|
1054
|
-
return result;
|
|
1055
|
-
// A corrupt/hand-edited sidecar must degrade to "no prior reads", never
|
|
1056
|
-
// throw: loadSessionState validates only version/widget, so a malformed
|
|
1057
|
-
// `reads` reaches here. If importState threw, the session_start try/catch
|
|
1058
|
-
// would abort the ENTIRE rehydration (incl. widget + mountLensWidget)
|
|
1059
|
-
// rather than just skipping the read-set.
|
|
1060
|
-
if (!Array.isArray(state.reads))
|
|
1061
|
-
return result;
|
|
1062
|
-
for (const entry of state.reads) {
|
|
1063
|
-
// Skip anything that isn't a well-formed [key, records] tuple.
|
|
1064
|
-
if (!Array.isArray(entry) || entry.length !== 2)
|
|
1065
|
-
continue;
|
|
1066
|
-
const [rawPath, records] = entry;
|
|
1067
|
-
if (typeof rawPath !== "string")
|
|
1068
|
-
continue;
|
|
1069
|
-
if (!Array.isArray(records) || records.length === 0)
|
|
1070
|
-
continue;
|
|
1071
|
-
const filePath = this.key(rawPath);
|
|
1072
|
-
let lines;
|
|
1073
|
-
try {
|
|
1074
|
-
lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
1075
|
-
}
|
|
1076
|
-
catch {
|
|
1077
|
-
// File gone since it was read → drop every read for it.
|
|
1078
|
-
result.dropped += records.length;
|
|
1079
|
-
continue;
|
|
1080
|
-
}
|
|
1081
|
-
for (const record of records) {
|
|
1082
|
-
const rehydrated = { ...record, filePath };
|
|
1083
|
-
// readHashesStillMatch returns false when the recorded hashes no
|
|
1084
|
-
// longer match disk OR when the read captured no hashes — both
|
|
1085
|
-
// unverifiable, so both drop (safety over convenience).
|
|
1086
|
-
if (this.readHashesStillMatch(rehydrated, lines)) {
|
|
1087
|
-
this.recordRead(rehydrated);
|
|
1088
|
-
result.imported += 1;
|
|
1089
|
-
}
|
|
1090
|
-
else {
|
|
1091
|
-
result.dropped += 1;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
}
|
|
1095
|
-
return result;
|
|
1096
|
-
}
|
|
1097
|
-
/**
|
|
1098
|
-
* Get all edit records for a file (for debugging).
|
|
1099
|
-
*/
|
|
1100
|
-
getEditHistory(filePath) {
|
|
1101
|
-
const key = this.key(filePath);
|
|
1102
|
-
if (this.edits.has(key))
|
|
1103
|
-
this.touchFile(key);
|
|
1104
|
-
return this.edits.get(key) ?? [];
|
|
1105
|
-
}
|
|
1106
|
-
// --- Private helpers ---
|
|
1107
|
-
injectCreationRead(filePath, turnIndex, writeIndex) {
|
|
1108
|
-
let lineCount = 0;
|
|
1109
|
-
try {
|
|
1110
|
-
lineCount = splitLines(fs.readFileSync(filePath, "utf-8")).length;
|
|
1111
|
-
}
|
|
1112
|
-
catch {
|
|
1113
|
-
return;
|
|
1114
|
-
}
|
|
1115
|
-
if (lineCount === 0)
|
|
1116
|
-
return;
|
|
1117
|
-
this.recordRead({
|
|
1118
|
-
filePath,
|
|
1119
|
-
requestedOffset: 1,
|
|
1120
|
-
requestedLimit: lineCount,
|
|
1121
|
-
effectiveOffset: 1,
|
|
1122
|
-
effectiveLimit: lineCount,
|
|
1123
|
-
expandedByLsp: false,
|
|
1124
|
-
turnIndex,
|
|
1125
|
-
writeIndex,
|
|
1126
|
-
timestamp: Date.now(),
|
|
1127
|
-
});
|
|
1128
|
-
}
|
|
1129
|
-
wasWrittenThisSession(filePath) {
|
|
1130
|
-
if (this.unchangedThisSession.has(filePath))
|
|
1131
|
-
return false;
|
|
1132
|
-
// Authoritative path: we observed a write of this file via recordWritten.
|
|
1133
|
-
// Survives mtime granularity (FAT32 ~2s), clock skew (NFS), and external
|
|
1134
|
-
// tools that touch mtime backward.
|
|
1135
|
-
if (this.writtenThisSession.has(filePath))
|
|
1136
|
-
return true;
|
|
1137
|
-
try {
|
|
1138
|
-
return fs.statSync(filePath).mtimeMs >= this.sessionStartMs;
|
|
1139
|
-
}
|
|
1140
|
-
catch {
|
|
1141
|
-
return false;
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
canTreatStalenessAsOwnPriorEdit(filePath, lastReadTimestamp) {
|
|
1145
|
-
const edits = this.edits.get(filePath) ?? [];
|
|
1146
|
-
const latest = edits.at(-1);
|
|
1147
|
-
if (!latest)
|
|
1148
|
-
return false;
|
|
1149
|
-
if (latest.verdict !== "allowed" && latest.verdict !== "warned")
|
|
1150
|
-
return false;
|
|
1151
|
-
if (latest.timestamp < lastReadTimestamp)
|
|
1152
|
-
return false;
|
|
1153
|
-
return Date.now() - latest.timestamp <= OWN_EDIT_STALE_GRACE_MS;
|
|
1154
|
-
}
|
|
1155
|
-
canIgnoreStalenessByHashes(filePath, reads, touchedLines, editRanges) {
|
|
1156
|
-
let lines;
|
|
1157
|
-
try {
|
|
1158
|
-
lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
1159
|
-
}
|
|
1160
|
-
catch {
|
|
1161
|
-
return false;
|
|
1162
|
-
}
|
|
1163
|
-
const rangesToCheck = touchedLines
|
|
1164
|
-
? editRanges && editRanges.length > 1
|
|
1165
|
-
? editRanges
|
|
1166
|
-
: [touchedLines]
|
|
1167
|
-
: undefined;
|
|
1168
|
-
if (!rangesToCheck) {
|
|
1169
|
-
const lastRead = reads.at(-1);
|
|
1170
|
-
return !!lastRead && this.readHashesStillMatch(lastRead, lines);
|
|
1171
|
-
}
|
|
1172
|
-
return rangesToCheck.every((range) => reads.some((read) => this.readCoversRange(read, range) &&
|
|
1173
|
-
this.readRangeHashesStillMatch(read, lines, range)));
|
|
1174
|
-
}
|
|
1175
|
-
readCoversRange(read, [editStart, editEnd]) {
|
|
1176
|
-
const readStart = Math.max(1, read.effectiveOffset - this.config.contextLines);
|
|
1177
|
-
const readEnd = read.effectiveOffset + read.effectiveLimit - 1 + this.config.contextLines;
|
|
1178
|
-
if (editStart >= readStart && editEnd <= readEnd)
|
|
1179
|
-
return true;
|
|
1180
|
-
if (!read.enclosingSymbol)
|
|
1181
|
-
return false;
|
|
1182
|
-
return (read.enclosingSymbol.startLine <= editStart &&
|
|
1183
|
-
read.enclosingSymbol.endLine >= editEnd);
|
|
1184
|
-
}
|
|
1185
|
-
validateRangeSnapshot(filePath, range,
|
|
1186
|
-
/**
|
|
1187
|
-
* What the CALLER will do with the verdict. `checkEdit` honours
|
|
1188
|
-
* `skipSnapshotCheck` for content-validated (oldText) edits, so a
|
|
1189
|
-
* `shouldBlock` verdict may never reach the gate (#1904 item 1).
|
|
1190
|
-
*/
|
|
1191
|
-
snapshotCheckSkipped) {
|
|
1192
|
-
const reads = this.reads.get(filePath) ?? [];
|
|
1193
|
-
const candidates = reads.filter((read) => this.readCoversRange(read, range));
|
|
1194
|
-
let status = "unavailable";
|
|
1195
|
-
let matchingReadIndex = -1;
|
|
1196
|
-
let missingLines = [];
|
|
1197
|
-
let mismatchedLines = [];
|
|
1198
|
-
let checkedCandidateCount = 0;
|
|
1199
|
-
let unavailableCandidateCount = 0;
|
|
1200
|
-
let hashUnavailableCandidateCount = 0;
|
|
1201
|
-
let lastMismatchTimestamp = -Infinity;
|
|
1202
|
-
let lastUnavailableTimestamp = -Infinity;
|
|
1203
|
-
for (let i = 0; i < candidates.length; i += 1) {
|
|
1204
|
-
const validation = currentLinesMatchReadSnapshot(filePath, candidates[i], range);
|
|
1205
|
-
if (!validation.checked) {
|
|
1206
|
-
unavailableCandidateCount += 1;
|
|
1207
|
-
if (readEffectiveRangeCoversRange(candidates[i], range)) {
|
|
1208
|
-
hashUnavailableCandidateCount += 1;
|
|
1209
|
-
}
|
|
1210
|
-
if (status === "unavailable") {
|
|
1211
|
-
missingLines = validation.missingLines;
|
|
1212
|
-
}
|
|
1213
|
-
lastUnavailableTimestamp = Math.max(lastUnavailableTimestamp, candidates[i].timestamp);
|
|
1214
|
-
continue;
|
|
1215
|
-
}
|
|
1216
|
-
checkedCandidateCount += 1;
|
|
1217
|
-
if (validation.matches) {
|
|
1218
|
-
status = "match";
|
|
1219
|
-
matchingReadIndex = i;
|
|
1220
|
-
missingLines = [];
|
|
1221
|
-
mismatchedLines = [];
|
|
1222
|
-
break;
|
|
1223
|
-
}
|
|
1224
|
-
status = "mismatch";
|
|
1225
|
-
missingLines = [];
|
|
1226
|
-
mismatchedLines = validation.mismatchedLines;
|
|
1227
|
-
lastMismatchTimestamp = Math.max(lastMismatchTimestamp, candidates[i].timestamp);
|
|
1228
|
-
}
|
|
1229
|
-
// Enforce only when no candidate that actually delivered the target range
|
|
1230
|
-
// lacks hashes. Context-only/symbol-only coverage may be unavailable without
|
|
1231
|
-
// weakening enforcement from another hash-checkable read of the same range.
|
|
1232
|
-
// Also suppress when a re-read (unavailable only due to context-zone boundary)
|
|
1233
|
-
// is more recent than the stale read that triggered the mismatch — the agent
|
|
1234
|
-
// refreshed their view, and the re-read's edge lines fall within contextLines.
|
|
1235
|
-
const shouldBlock = status === "mismatch" &&
|
|
1236
|
-
lastUnavailableTimestamp <= lastMismatchTimestamp &&
|
|
1237
|
-
checkedCandidateCount > 0 &&
|
|
1238
|
-
hashUnavailableCandidateCount === 0;
|
|
1239
|
-
// `enforced` below states INTENT — whether this validation reached a
|
|
1240
|
-
// decidable verdict. It says nothing about what the caller did with it.
|
|
1241
|
-
// `outcome` states the caller's actual behaviour, which is what an
|
|
1242
|
-
// enforcement rate must be computed from (#1904 item 1).
|
|
1243
|
-
const outcome = shouldBlock
|
|
1244
|
-
? snapshotCheckSkipped
|
|
1245
|
-
? "bypassed-content-match"
|
|
1246
|
-
: "enforced-block"
|
|
1247
|
-
: status === "match"
|
|
1248
|
-
? "enforced-pass"
|
|
1249
|
-
: "not-decidable";
|
|
1250
|
-
logReadGuardEvent({
|
|
1251
|
-
event: "range_snapshot_validation",
|
|
1252
|
-
sessionId: this.sessionId,
|
|
1253
|
-
filePath,
|
|
1254
|
-
metadata: {
|
|
1255
|
-
range,
|
|
1256
|
-
status,
|
|
1257
|
-
candidateReadCount: candidates.length,
|
|
1258
|
-
checkedCandidateCount,
|
|
1259
|
-
unavailableCandidateCount,
|
|
1260
|
-
hashUnavailableCandidateCount,
|
|
1261
|
-
matchingReadIndex,
|
|
1262
|
-
missingLineCount: missingLines.length,
|
|
1263
|
-
mismatchedLineCount: mismatchedLines.length,
|
|
1264
|
-
missingLines: missingLines.slice(0, 20),
|
|
1265
|
-
mismatchedLines: mismatchedLines.slice(0, 20),
|
|
1266
|
-
enforced: shouldBlock || status === "match",
|
|
1267
|
-
outcome,
|
|
1268
|
-
},
|
|
1269
|
-
});
|
|
1270
|
-
return {
|
|
1271
|
-
status,
|
|
1272
|
-
matchingReadIndex,
|
|
1273
|
-
missingLines,
|
|
1274
|
-
mismatchedLines,
|
|
1275
|
-
candidateReadCount: candidates.length,
|
|
1276
|
-
checkedCandidateCount,
|
|
1277
|
-
unavailableCandidateCount,
|
|
1278
|
-
shouldBlock,
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
readRangeHashesStillMatch(read, lines, [startLine, endLine]) {
|
|
1282
|
-
const hashes = read.lineHashes ?? {};
|
|
1283
|
-
for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
|
|
1284
|
-
if (!readRangeCoversLine(read, lineNo) || hashes[lineNo] === undefined) {
|
|
1285
|
-
return false;
|
|
1286
|
-
}
|
|
1287
|
-
if (lineNo < 1 || lineNo > lines.length)
|
|
1288
|
-
return false;
|
|
1289
|
-
if (lineContentHash(lines[lineNo - 1] ?? "") !== hashes[lineNo]) {
|
|
1290
|
-
return false;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
return true;
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* Content-verified relocation. When a range the agent read has drifted, find
|
|
1297
|
-
* where the read-time line-hash sequence for [startLine,endLine] now appears
|
|
1298
|
-
* in the current file. Returns the unique new location, or undefined when:
|
|
1299
|
-
* no recorded read captured hashes for the whole range; the sequence is too
|
|
1300
|
-
* short to be collision-resistant (<2 lines); or it now matches zero or
|
|
1301
|
-
* multiple spots. Powers a *hint* only — never a silent positional re-apply.
|
|
1302
|
-
*/
|
|
1303
|
-
findRelocation(filePath, reads, [startLine, endLine]) {
|
|
1304
|
-
const span = endLine - startLine + 1;
|
|
1305
|
-
// A single line's hash collides too easily to relocate on confidently.
|
|
1306
|
-
if (span < 2)
|
|
1307
|
-
return undefined;
|
|
1308
|
-
// Newest read that captured hashes for the entire target range wins.
|
|
1309
|
-
let wanted;
|
|
1310
|
-
for (let i = reads.length - 1; i >= 0; i -= 1) {
|
|
1311
|
-
const hashes = reads[i].lineHashes;
|
|
1312
|
-
if (!hashes)
|
|
1313
|
-
continue;
|
|
1314
|
-
const seq = [];
|
|
1315
|
-
let complete = true;
|
|
1316
|
-
for (let lineNo = startLine; lineNo <= endLine; lineNo += 1) {
|
|
1317
|
-
const h = hashes[lineNo];
|
|
1318
|
-
if (h === undefined) {
|
|
1319
|
-
complete = false;
|
|
1320
|
-
break;
|
|
1321
|
-
}
|
|
1322
|
-
seq.push(h);
|
|
1323
|
-
}
|
|
1324
|
-
if (complete) {
|
|
1325
|
-
wanted = seq;
|
|
1326
|
-
break;
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
if (!wanted)
|
|
1330
|
-
return undefined;
|
|
1331
|
-
let lines;
|
|
1332
|
-
try {
|
|
1333
|
-
lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
1334
|
-
}
|
|
1335
|
-
catch {
|
|
1336
|
-
return undefined;
|
|
1337
|
-
}
|
|
1338
|
-
const currentHashes = lines.map((line) => lineContentHash(line));
|
|
1339
|
-
const lastStart = currentHashes.length - span; // last valid 0-based start
|
|
1340
|
-
const matchStarts = [];
|
|
1341
|
-
for (let i = 0; i <= lastStart; i += 1) {
|
|
1342
|
-
let ok = true;
|
|
1343
|
-
for (let j = 0; j < span; j += 1) {
|
|
1344
|
-
if (currentHashes[i + j] !== wanted[j]) {
|
|
1345
|
-
ok = false;
|
|
1346
|
-
break;
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
if (ok)
|
|
1350
|
-
matchStarts.push(i + 1); // 1-indexed
|
|
1351
|
-
}
|
|
1352
|
-
let newStart;
|
|
1353
|
-
if (matchStarts.length === 1) {
|
|
1354
|
-
// Unique across the whole file → certainly the relocated span,
|
|
1355
|
-
// regardless of how far it drifted (e.g. a large refactor moved it).
|
|
1356
|
-
newStart = matchStarts[0];
|
|
1357
|
-
}
|
|
1358
|
-
else if (matchStarts.length > 1) {
|
|
1359
|
-
// Duplicated elsewhere: fall back to locality. Lines rarely teleport,
|
|
1360
|
-
// so accept a match unique WITHIN an adaptive window of the original
|
|
1361
|
-
// position — out-of-window duplicates don't poison a locally
|
|
1362
|
-
// unambiguous relocation. The window widens with the edits already
|
|
1363
|
-
// applied to this file this session (each prior edit shifts line
|
|
1364
|
-
// numbers, so accumulated drift grows).
|
|
1365
|
-
const appliedEdits = (this.edits.get(filePath) ?? []).filter((record) => record.verdict !== "blocked").length;
|
|
1366
|
-
const window = Math.min(RELOCATION_WINDOW_MAX, Math.max(RELOCATION_WINDOW_MIN, appliedEdits * RELOCATION_WINDOW_PER_EDIT));
|
|
1367
|
-
const lo = startLine - window;
|
|
1368
|
-
const hi = endLine + window;
|
|
1369
|
-
const local = matchStarts.filter((start) => start >= lo && start <= hi);
|
|
1370
|
-
if (local.length === 1)
|
|
1371
|
-
newStart = local[0];
|
|
1372
|
-
}
|
|
1373
|
-
if (newStart === undefined || newStart === startLine)
|
|
1374
|
-
return undefined;
|
|
1375
|
-
return { from: [startLine, endLine], to: [newStart, newStart + span - 1] };
|
|
1376
|
-
}
|
|
1377
|
-
readHashesStillMatch(read, lines) {
|
|
1378
|
-
const entries = Object.entries(read.lineHashes ?? {});
|
|
1379
|
-
if (entries.length === 0)
|
|
1380
|
-
return false;
|
|
1381
|
-
for (const [lineText, expected] of entries) {
|
|
1382
|
-
const lineNo = Number(lineText);
|
|
1383
|
-
if (!Number.isInteger(lineNo) || lineNo < 1 || lineNo > lines.length) {
|
|
1384
|
-
return false;
|
|
1385
|
-
}
|
|
1386
|
-
if (lineContentHash(lines[lineNo - 1] ?? "") !== expected)
|
|
1387
|
-
return false;
|
|
1388
|
-
}
|
|
1389
|
-
return true;
|
|
1390
|
-
}
|
|
1391
|
-
checkCoverage(filePath, touchedLines) {
|
|
1392
|
-
const [editStart, editEnd] = touchedLines;
|
|
1393
|
-
const reads = this.reads.get(filePath) ?? [];
|
|
1394
|
-
// First pass: check symbol coverage and any single read that covers the edit.
|
|
1395
|
-
for (const read of reads) {
|
|
1396
|
-
if (read.provisional === true)
|
|
1397
|
-
continue;
|
|
1398
|
-
const readStart = Math.max(1, read.effectiveOffset - this.config.contextLines);
|
|
1399
|
-
const readEnd = read.effectiveOffset +
|
|
1400
|
-
read.effectiveLimit -
|
|
1401
|
-
1 +
|
|
1402
|
-
this.config.contextLines;
|
|
1403
|
-
if (editStart >= readStart && editEnd <= readEnd) {
|
|
1404
|
-
return { covered: true, viaSymbol: false };
|
|
1405
|
-
}
|
|
1406
|
-
if (read.enclosingSymbol) {
|
|
1407
|
-
const symStart = read.enclosingSymbol.startLine;
|
|
1408
|
-
const symEnd = read.enclosingSymbol.endLine;
|
|
1409
|
-
if (symStart <= editStart && symEnd >= editEnd) {
|
|
1410
|
-
return { covered: true, viaSymbol: true };
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
}
|
|
1414
|
-
// Second pass: merge all read intervals and check if their union covers
|
|
1415
|
-
// [editStart, editEnd]. Handles multi-chunk reads (e.g. 1-100 + 101-200).
|
|
1416
|
-
const intervals = reads
|
|
1417
|
-
.filter((read) => read.provisional !== true)
|
|
1418
|
-
.map((read) => [
|
|
1419
|
-
Math.max(1, read.effectiveOffset - this.config.contextLines),
|
|
1420
|
-
read.effectiveOffset +
|
|
1421
|
-
read.effectiveLimit -
|
|
1422
|
-
1 +
|
|
1423
|
-
this.config.contextLines,
|
|
1424
|
-
]);
|
|
1425
|
-
intervals.sort((a, b) => a[0] - b[0]);
|
|
1426
|
-
// Merge overlapping/adjacent intervals
|
|
1427
|
-
const merged = [];
|
|
1428
|
-
for (const [s, e] of intervals) {
|
|
1429
|
-
if (merged.length > 0 && s <= merged[merged.length - 1][1] + 1) {
|
|
1430
|
-
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], e);
|
|
1431
|
-
}
|
|
1432
|
-
else {
|
|
1433
|
-
merged.push([s, e]);
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
for (const [s, e] of merged) {
|
|
1437
|
-
if (editStart >= s && editEnd <= e) {
|
|
1438
|
-
return { covered: true, viaSymbol: false };
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
return { covered: false, viaSymbol: false };
|
|
1442
|
-
}
|
|
1443
|
-
getExemptionMode(filePath) {
|
|
1444
|
-
for (const exemption of this.config.exemptions) {
|
|
1445
|
-
if (this.matchesPattern(filePath, exemption.pattern)) {
|
|
1446
|
-
return exemption.mode;
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return null;
|
|
1450
|
-
}
|
|
1451
|
-
matchesPattern(filePath, pattern) {
|
|
1452
|
-
// Simple glob matching — can be expanded
|
|
1453
|
-
if (pattern.startsWith("*")) {
|
|
1454
|
-
const suffix = pattern.slice(1);
|
|
1455
|
-
return filePath.endsWith(suffix);
|
|
1456
|
-
}
|
|
1457
|
-
if (pattern.includes("*")) {
|
|
1458
|
-
// Adjacent stars are equivalent to one star in this dialect. Collapse
|
|
1459
|
-
// them before compiling so a non-match cannot backtrack over every
|
|
1460
|
-
// nullable `.*` group (#2622).
|
|
1461
|
-
const collapsedPattern = pattern.replace(/\*+/g, "*");
|
|
1462
|
-
const regex = new RegExp(`^${collapsedPattern.replace(/\\/g, "\\\\").replace(/\./g, "\\.").replace(/\*/g, ".*")}$`);
|
|
1463
|
-
return regex.test(filePath);
|
|
1464
|
-
}
|
|
1465
|
-
return filePath === pattern;
|
|
1466
|
-
}
|
|
1467
|
-
blockOrWarn(_reason, message, details, overrideMode) {
|
|
1468
|
-
const mode = overrideMode ?? this.config.mode;
|
|
1469
|
-
if (mode === "warn") {
|
|
1470
|
-
return { action: "warn", reason: message, details };
|
|
1471
|
-
}
|
|
1472
|
-
return { action: "block", reason: message, details };
|
|
1473
|
-
}
|
|
1474
|
-
allow() {
|
|
1475
|
-
return { action: "allow" };
|
|
1476
|
-
}
|
|
1477
|
-
recordEdit(filePath, tool, touchedLines, verdict) {
|
|
1478
|
-
this.touchFile(filePath);
|
|
1479
|
-
const arr = this.edits.get(filePath) ?? [];
|
|
1480
|
-
arr.push({
|
|
1481
|
-
filePath,
|
|
1482
|
-
tool,
|
|
1483
|
-
touchedLines,
|
|
1484
|
-
precedingReads: this.reads.get(filePath) ?? [],
|
|
1485
|
-
verdict: mapVerdictAction(verdict.action),
|
|
1486
|
-
reason: verdict.reason,
|
|
1487
|
-
timestamp: Date.now(),
|
|
1488
|
-
});
|
|
1489
|
-
if (arr.length > READ_GUARD_MAX_EDITS_PER_FILE) {
|
|
1490
|
-
const trimmedCount = arr.length - READ_GUARD_MAX_EDITS_PER_FILE;
|
|
1491
|
-
arr.splice(0, trimmedCount);
|
|
1492
|
-
// #1918: the #282-289 doc comment argues this trim is inert in
|
|
1493
|
-
// practice (every consumer reads only the last record or a
|
|
1494
|
-
// bounded relocation window), but an argument is not a record —
|
|
1495
|
-
// if that assumption ever breaks, this is the only signal. Same
|
|
1496
|
-
// aggregate-after-first shape as `read_cap_trimmed` (#1913):
|
|
1497
|
-
// `incrementDegradationCount` keeps the exact per-file tally,
|
|
1498
|
-
// the read-guard.log line fires once on the rising edge.
|
|
1499
|
-
const isRisingEdge = incrementDegradationCount({
|
|
1500
|
-
kind: "read-guard-edits-cap-trim",
|
|
1501
|
-
subject: filePath,
|
|
1502
|
-
reason: `trimmed ${trimmedCount} edit record(s) past cap`,
|
|
1503
|
-
});
|
|
1504
|
-
if (isRisingEdge) {
|
|
1505
|
-
logReadGuardEvent({
|
|
1506
|
-
event: "edits_cap_trimmed",
|
|
1507
|
-
sessionId: this.sessionId,
|
|
1508
|
-
filePath,
|
|
1509
|
-
metadata: { trimmedCount, cappedLength: arr.length },
|
|
1510
|
-
});
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
this.edits.set(filePath, arr);
|
|
1514
|
-
}
|
|
1515
|
-
recordVerdict(filePath, tool, touchedLines, verdict, metadata = {}) {
|
|
1516
|
-
const normalizedTouchedLines = touchedLines ?? [1, 1];
|
|
1517
|
-
this.recordEdit(filePath, tool, normalizedTouchedLines, verdict);
|
|
1518
|
-
const reads = this.reads.get(filePath) ?? [];
|
|
1519
|
-
logReadGuardEvent({
|
|
1520
|
-
event: verdict.action === "allow"
|
|
1521
|
-
? "edit_allowed"
|
|
1522
|
-
: verdict.action === "warn"
|
|
1523
|
-
? "edit_warned"
|
|
1524
|
-
: "edit_blocked",
|
|
1525
|
-
sessionId: this.sessionId,
|
|
1526
|
-
filePath,
|
|
1527
|
-
metadata: {
|
|
1528
|
-
tool,
|
|
1529
|
-
touchedLines: touchedLines ?? null,
|
|
1530
|
-
normalizedTouchedLines,
|
|
1531
|
-
readCount: reads.length,
|
|
1532
|
-
reads: reads.map((read) => ({
|
|
1533
|
-
requestedOffset: read.requestedOffset,
|
|
1534
|
-
requestedLimit: read.requestedLimit,
|
|
1535
|
-
effectiveOffset: read.effectiveOffset,
|
|
1536
|
-
effectiveLimit: read.effectiveLimit,
|
|
1537
|
-
expandedByLsp: read.expandedByLsp,
|
|
1538
|
-
enclosingSymbol: read.enclosingSymbol ?? null,
|
|
1539
|
-
timestamp: read.timestamp,
|
|
1540
|
-
})),
|
|
1541
|
-
verdictAction: verdict.action,
|
|
1542
|
-
details: verdict.details,
|
|
1543
|
-
...metadata,
|
|
1544
|
-
},
|
|
1545
|
-
});
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
// --- Factory ---
|
|
1549
|
-
function mapVerdictAction(action) {
|
|
1550
|
-
switch (action) {
|
|
1551
|
-
case "allow":
|
|
1552
|
-
return "allowed";
|
|
1553
|
-
case "block":
|
|
1554
|
-
return "blocked";
|
|
1555
|
-
case "warn":
|
|
1556
|
-
return "warned";
|
|
1557
|
-
}
|
|
1558
|
-
}
|
|
1559
|
-
export function createReadGuard(sessionId, config) {
|
|
1560
|
-
return new ReadGuard(sessionId, config);
|
|
1561
|
-
}
|