pi-lens 3.8.73 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import { createFileTime } from "./file-time.js";
|
|
14
|
+
import { hashDiagnosticContent } from "./lsp/diagnostic-binding.js";
|
|
14
15
|
import { normalizeFilePath } from "./path-utils.js";
|
|
15
16
|
import { logReadGuardEvent } from "./read-guard-logger.js";
|
|
17
|
+
export const READ_GUARD_STATE_VERSION = 1;
|
|
16
18
|
// --- Constants ---
|
|
17
19
|
const DEFAULT_CONFIG = {
|
|
18
20
|
enabled: true,
|
|
@@ -27,6 +29,66 @@ const DEFAULT_CONFIG = {
|
|
|
27
29
|
const OWN_EDIT_STALE_GRACE_MS = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_OWN_EDIT_GRACE_MS ?? "120000", 10) || 120000);
|
|
28
30
|
/** Avoid hashing very large reads in the hot path. */
|
|
29
31
|
const READ_HASH_MAX_LINES = Math.max(0, Number.parseInt(process.env.PI_LENS_READ_GUARD_HASH_MAX_LINES ?? "3000", 10) || 3000);
|
|
32
|
+
/**
|
|
33
|
+
* Content bindings are defense in depth on a read-adjacent hot path. Cap the
|
|
34
|
+
* synchronous disk read so bridge registration never hashes an unbounded file.
|
|
35
|
+
*/
|
|
36
|
+
const READ_BINDING_MAX_BYTES = 4 * 1024 * 1024;
|
|
37
|
+
const READ_GUARD_MAX_FILES = 256;
|
|
38
|
+
// Unconsumed reads remain valid until edit or session end, but this high
|
|
39
|
+
// sanity cap prevents a read-only session from growing without bound.
|
|
40
|
+
const READ_GUARD_MAX_UNCONSUMED_FILES = 4096;
|
|
41
|
+
const READ_GUARD_IDLE_EVICT_MS_DEFAULT = 30 * 60_000;
|
|
42
|
+
export function captureReadContentBinding(filePath, offset, limit) {
|
|
43
|
+
try {
|
|
44
|
+
if (fs.statSync(filePath).size > READ_BINDING_MAX_BYTES)
|
|
45
|
+
return undefined;
|
|
46
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
47
|
+
// Allocate at most the classification prefix first. For range bindings,
|
|
48
|
+
// splitting then stops at the requested range end rather than the EOF.
|
|
49
|
+
const lines = splitLinesThrough(content, READ_HASH_MAX_LINES + 1);
|
|
50
|
+
if (lines.length <= READ_HASH_MAX_LINES) {
|
|
51
|
+
return {
|
|
52
|
+
hash: hashDiagnosticContent(content),
|
|
53
|
+
fullFile: true,
|
|
54
|
+
offset: 1,
|
|
55
|
+
limit: lines.length,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const scopedOffset = Math.max(1, offset);
|
|
59
|
+
const scopedLimit = Math.min(Math.max(0, limit), READ_HASH_MAX_LINES);
|
|
60
|
+
if (scopedLimit === 0)
|
|
61
|
+
return undefined;
|
|
62
|
+
const rangeLines = splitLinesThrough(content, scopedOffset - 1 + scopedLimit);
|
|
63
|
+
const scopedContent = rangeLines
|
|
64
|
+
.slice(scopedOffset - 1, scopedOffset - 1 + scopedLimit)
|
|
65
|
+
.join("\n");
|
|
66
|
+
return {
|
|
67
|
+
hash: hashDiagnosticContent(scopedContent),
|
|
68
|
+
fullFile: false,
|
|
69
|
+
offset: scopedOffset,
|
|
70
|
+
limit: scopedLimit,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export function _currentContentMatchesBindingForTests(filePath, binding) {
|
|
78
|
+
try {
|
|
79
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
80
|
+
const boundContent = binding.fullFile
|
|
81
|
+
? content
|
|
82
|
+
: splitLines(content)
|
|
83
|
+
.slice(binding.offset - 1, binding.offset - 1 + binding.limit)
|
|
84
|
+
.join("\n");
|
|
85
|
+
return hashDiagnosticContent(boundContent) === binding.hash;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const currentContentMatchesBinding = _currentContentMatchesBindingForTests;
|
|
30
92
|
// Adaptive relocation window (findRelocation). A globally-unique hash-sequence
|
|
31
93
|
// match always wins; when the content is duplicated elsewhere, we fall back to
|
|
32
94
|
// a match that is unique WITHIN this window of the original position. The window
|
|
@@ -39,6 +101,28 @@ const RELOCATION_WINDOW_MAX = Math.max(RELOCATION_WINDOW_MIN, Number.parseInt(pr
|
|
|
39
101
|
function splitLines(text) {
|
|
40
102
|
return text.split(/\r?\n/);
|
|
41
103
|
}
|
|
104
|
+
/** `splitLines` semantics, but without scanning/allocating beyond `maxLines`. */
|
|
105
|
+
function splitLinesThrough(text, maxLines) {
|
|
106
|
+
if (maxLines <= 0)
|
|
107
|
+
return [];
|
|
108
|
+
const lines = [];
|
|
109
|
+
let start = 0;
|
|
110
|
+
while (lines.length < maxLines) {
|
|
111
|
+
const newline = text.indexOf("\n", start);
|
|
112
|
+
if (newline === -1) {
|
|
113
|
+
lines.push(text.slice(start));
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
const end = newline > start && text[newline - 1] === "\r" ? newline - 1 : newline;
|
|
117
|
+
lines.push(text.slice(start, end));
|
|
118
|
+
start = newline + 1;
|
|
119
|
+
if (start === text.length && lines.length < maxLines) {
|
|
120
|
+
lines.push("");
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return lines;
|
|
125
|
+
}
|
|
42
126
|
export function lineContentHash(line) {
|
|
43
127
|
// FNV-1a over whitespace-stripped content. This treats no-op formatter/touch
|
|
44
128
|
// changes as still-valid context while detecting semantic line changes.
|
|
@@ -117,6 +201,10 @@ export class ReadGuard {
|
|
|
117
201
|
config;
|
|
118
202
|
reads = new Map();
|
|
119
203
|
edits = new Map();
|
|
204
|
+
fileLastUsed = new Map();
|
|
205
|
+
fileIdleTimers = new Map();
|
|
206
|
+
/** Reads remain behavior-gating until the corresponding edit is published. */
|
|
207
|
+
consumedReadFiles = new Set();
|
|
120
208
|
fileTime;
|
|
121
209
|
exemptions = new Set(); // One-time exemptions via /lens-allow-edit
|
|
122
210
|
pendingCreations = new Map();
|
|
@@ -146,6 +234,66 @@ export class ReadGuard {
|
|
|
146
234
|
key(filePath) {
|
|
147
235
|
return normalizeFilePath(filePath);
|
|
148
236
|
}
|
|
237
|
+
idleEvictMs() {
|
|
238
|
+
const value = Number.parseInt(process.env.PI_LENS_READ_GUARD_IDLE_EVICT_MS ?? "", 10);
|
|
239
|
+
return Number.isSafeInteger(value) && value > 0 ? value : READ_GUARD_IDLE_EVICT_MS_DEFAULT;
|
|
240
|
+
}
|
|
241
|
+
clearFileTimer(filePath) {
|
|
242
|
+
const timer = this.fileIdleTimers.get(filePath);
|
|
243
|
+
if (timer)
|
|
244
|
+
clearTimeout(timer);
|
|
245
|
+
this.fileIdleTimers.delete(filePath);
|
|
246
|
+
}
|
|
247
|
+
evictFile(filePath) {
|
|
248
|
+
this.clearFileTimer(filePath);
|
|
249
|
+
this.reads.delete(filePath);
|
|
250
|
+
this.edits.delete(filePath);
|
|
251
|
+
this.fileLastUsed.delete(filePath);
|
|
252
|
+
this.consumedReadFiles.delete(filePath);
|
|
253
|
+
this.writtenThisSession.delete(filePath);
|
|
254
|
+
}
|
|
255
|
+
touchFile(filePath) {
|
|
256
|
+
const now = Date.now();
|
|
257
|
+
this.fileLastUsed.set(filePath, now);
|
|
258
|
+
this.clearFileTimer(filePath);
|
|
259
|
+
// An outstanding read is enforcement state, not a rebuildable cache entry.
|
|
260
|
+
// It must survive idle time and file-cap pressure until the edit consumes it.
|
|
261
|
+
if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
|
|
262
|
+
return;
|
|
263
|
+
const stamp = now;
|
|
264
|
+
const timer = setTimeout(() => {
|
|
265
|
+
if (this.fileLastUsed.get(filePath) !== stamp)
|
|
266
|
+
return;
|
|
267
|
+
// Never turn an outstanding read into a zero-read block through idle
|
|
268
|
+
// eviction. Only consumed reads and rebuildable edit history may expire.
|
|
269
|
+
if (this.reads.has(filePath) && !this.consumedReadFiles.has(filePath))
|
|
270
|
+
return;
|
|
271
|
+
this.evictFile(filePath);
|
|
272
|
+
}, this.idleEvictMs());
|
|
273
|
+
timer.unref?.();
|
|
274
|
+
this.fileIdleTimers.set(filePath, timer);
|
|
275
|
+
}
|
|
276
|
+
enforceFileCap() {
|
|
277
|
+
while (this.reads.size > READ_GUARD_MAX_FILES) {
|
|
278
|
+
const victim = [...this.reads.keys()]
|
|
279
|
+
.filter((filePath) => this.consumedReadFiles.has(filePath))
|
|
280
|
+
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) - (this.fileLastUsed.get(b) ?? 0))[0];
|
|
281
|
+
if (!victim)
|
|
282
|
+
break;
|
|
283
|
+
this.evictFile(victim);
|
|
284
|
+
}
|
|
285
|
+
while (this.reads.size > READ_GUARD_MAX_UNCONSUMED_FILES) {
|
|
286
|
+
const victim = [...this.reads.keys()]
|
|
287
|
+
.filter((filePath) => !this.consumedReadFiles.has(filePath))
|
|
288
|
+
.sort((a, b) => (this.fileLastUsed.get(a) ?? 0) -
|
|
289
|
+
(this.fileLastUsed.get(b) ?? 0))[0];
|
|
290
|
+
if (!victim)
|
|
291
|
+
break;
|
|
292
|
+
// This is a normal read miss: a later edit must require a fresh read,
|
|
293
|
+
// never silently allow and never become a permanent hard-block.
|
|
294
|
+
this.evictFile(victim);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
149
297
|
// --- Public API ---
|
|
150
298
|
/**
|
|
151
299
|
* Record that a file was read.
|
|
@@ -160,8 +308,11 @@ export class ReadGuard {
|
|
|
160
308
|
captureLineHashes(filePath, record.effectiveOffset, record.effectiveLimit),
|
|
161
309
|
};
|
|
162
310
|
const arr = this.reads.get(storedRecord.filePath) ?? [];
|
|
311
|
+
this.consumedReadFiles.delete(storedRecord.filePath);
|
|
163
312
|
arr.push(storedRecord);
|
|
164
313
|
this.reads.set(storedRecord.filePath, arr);
|
|
314
|
+
this.touchFile(storedRecord.filePath);
|
|
315
|
+
this.enforceFileCap();
|
|
165
316
|
logReadGuardEvent({
|
|
166
317
|
event: "read_recorded",
|
|
167
318
|
sessionId: this.sessionId,
|
|
@@ -180,6 +331,9 @@ export class ReadGuard {
|
|
|
180
331
|
writeIndex: storedRecord.writeIndex,
|
|
181
332
|
readCountForFile: arr.length,
|
|
182
333
|
hashLineCount: Object.keys(storedRecord.lineHashes ?? {}).length,
|
|
334
|
+
...(storedRecord.source !== undefined && {
|
|
335
|
+
source: storedRecord.source,
|
|
336
|
+
}),
|
|
183
337
|
},
|
|
184
338
|
});
|
|
185
339
|
// Also update FileTime stamp for this file
|
|
@@ -227,6 +381,8 @@ export class ReadGuard {
|
|
|
227
381
|
// Canonicalize once: every map lookup below (and every private helper this
|
|
228
382
|
// passes filePath to) must agree with how recordRead keyed the read.
|
|
229
383
|
filePath = this.key(filePath);
|
|
384
|
+
if (this.reads.has(filePath) || this.edits.has(filePath))
|
|
385
|
+
this.touchFile(filePath);
|
|
230
386
|
// Check exemptions
|
|
231
387
|
if (this.exemptions.has(filePath)) {
|
|
232
388
|
this.exemptions.delete(filePath); // One-time use
|
|
@@ -270,6 +426,19 @@ export class ReadGuard {
|
|
|
270
426
|
});
|
|
271
427
|
return verdict;
|
|
272
428
|
}
|
|
429
|
+
const lastBoundRead = [...fileReads]
|
|
430
|
+
.reverse()
|
|
431
|
+
.find((read) => read.contentBinding !== undefined);
|
|
432
|
+
if (lastBoundRead?.contentBinding &&
|
|
433
|
+
!currentContentMatchesBinding(filePath, lastBoundRead.contentBinding)) {
|
|
434
|
+
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);
|
|
435
|
+
this.recordVerdict(filePath, "edit", touchedLines, verdict, {
|
|
436
|
+
reasonKind: "file_modified",
|
|
437
|
+
lastReadTimestamp: lastBoundRead.timestamp,
|
|
438
|
+
contentBindingMismatch: true,
|
|
439
|
+
});
|
|
440
|
+
return verdict;
|
|
441
|
+
}
|
|
273
442
|
// 2. FileTime check (actual staleness)
|
|
274
443
|
let ignoredOwnEditStaleness = false;
|
|
275
444
|
let ignoredHashStaleness = false;
|
|
@@ -314,7 +483,9 @@ export class ReadGuard {
|
|
|
314
483
|
// If oldText was resolved (content-verified), the model demonstrably
|
|
315
484
|
// knew the content it's replacing — line drift from prior edits in
|
|
316
485
|
// the session is the likely cause. Downgrade to warn rather than block.
|
|
317
|
-
const outOfRangeMode = options?.oldTextResolved
|
|
486
|
+
const outOfRangeMode = options?.oldTextResolved
|
|
487
|
+
? "warn"
|
|
488
|
+
: effectiveMode;
|
|
318
489
|
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)}\``, {
|
|
319
490
|
editRange: range,
|
|
320
491
|
readRanges: fileReads.map((r) => ({
|
|
@@ -434,6 +605,10 @@ export class ReadGuard {
|
|
|
434
605
|
filePath = this.key(filePath);
|
|
435
606
|
this.fileTime.read(filePath);
|
|
436
607
|
this.writtenThisSession.add(filePath);
|
|
608
|
+
if (this.reads.has(filePath))
|
|
609
|
+
this.consumedReadFiles.add(filePath);
|
|
610
|
+
this.touchFile(filePath);
|
|
611
|
+
this.enforceFileCap();
|
|
437
612
|
const creation = this.pendingCreations.get(filePath);
|
|
438
613
|
if (creation) {
|
|
439
614
|
this.pendingCreations.delete(filePath);
|
|
@@ -495,13 +670,96 @@ export class ReadGuard {
|
|
|
495
670
|
* Get all read records for a file (for debugging).
|
|
496
671
|
*/
|
|
497
672
|
getReadHistory(filePath) {
|
|
498
|
-
|
|
673
|
+
const key = this.key(filePath);
|
|
674
|
+
if (this.reads.has(key))
|
|
675
|
+
this.touchFile(key);
|
|
676
|
+
return this.reads.get(key) ?? [];
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Snapshot the read-set for persistence across a session resume (#1041).
|
|
680
|
+
* Mirrors widget-state's `exportWidgetState`: the Map is emitted as
|
|
681
|
+
* `[key, records]` tuples, keys already in `normalizeFilePath` form. Only
|
|
682
|
+
* `reads` is serialized — it is the payload `checkEdit`'s zero-read/coverage
|
|
683
|
+
* checks consult. Safe to call even when the guard is disabled (an empty or
|
|
684
|
+
* never-populated `reads` simply exports zero entries).
|
|
685
|
+
*/
|
|
686
|
+
exportState() {
|
|
687
|
+
return {
|
|
688
|
+
version: READ_GUARD_STATE_VERSION,
|
|
689
|
+
reads: [...this.reads.entries()].map(([key, records]) => [
|
|
690
|
+
key,
|
|
691
|
+
records.map((record) => ({ ...record })),
|
|
692
|
+
]),
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Rehydrate a persisted read-set (#1041) into this (fresh, post-resume)
|
|
697
|
+
* guard, with mandatory staleness reconciliation: each read is re-verified
|
|
698
|
+
* against the CURRENT on-disk content via its recorded `lineHashes`, and any
|
|
699
|
+
* read whose file changed (or no longer exists, or that carries no verifiable
|
|
700
|
+
* hashes) is DROPPED. A rehydrated read must never mask a real staleness — a
|
|
701
|
+
* resume must not let the agent edit a file that changed on disk while it
|
|
702
|
+
* believed it held a fresh read. Kept reads are replayed through
|
|
703
|
+
* {@link recordRead}, which re-keys through {@link key} (idempotent — the
|
|
704
|
+
* exported keys are already normalized) and re-stamps FileTime so the next
|
|
705
|
+
* `checkEdit` sees a consistent baseline. Version-guarded and null-safe:
|
|
706
|
+
* `undefined` / a mismatched version / a missing field loads as "no prior
|
|
707
|
+
* reads". Returns a count of imported vs dropped reads for logging.
|
|
708
|
+
*/
|
|
709
|
+
importState(state) {
|
|
710
|
+
const result = { imported: 0, dropped: 0 };
|
|
711
|
+
if (!state || state.version !== READ_GUARD_STATE_VERSION)
|
|
712
|
+
return result;
|
|
713
|
+
// A corrupt/hand-edited sidecar must degrade to "no prior reads", never
|
|
714
|
+
// throw: loadSessionState validates only version/widget, so a malformed
|
|
715
|
+
// `reads` reaches here. If importState threw, the session_start try/catch
|
|
716
|
+
// would abort the ENTIRE rehydration (incl. widget + mountLensWidget)
|
|
717
|
+
// rather than just skipping the read-set.
|
|
718
|
+
if (!Array.isArray(state.reads))
|
|
719
|
+
return result;
|
|
720
|
+
for (const entry of state.reads) {
|
|
721
|
+
// Skip anything that isn't a well-formed [key, records] tuple.
|
|
722
|
+
if (!Array.isArray(entry) || entry.length !== 2)
|
|
723
|
+
continue;
|
|
724
|
+
const [rawPath, records] = entry;
|
|
725
|
+
if (typeof rawPath !== "string")
|
|
726
|
+
continue;
|
|
727
|
+
if (!Array.isArray(records) || records.length === 0)
|
|
728
|
+
continue;
|
|
729
|
+
const filePath = this.key(rawPath);
|
|
730
|
+
let lines;
|
|
731
|
+
try {
|
|
732
|
+
lines = splitLines(fs.readFileSync(filePath, "utf-8"));
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// File gone since it was read → drop every read for it.
|
|
736
|
+
result.dropped += records.length;
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
for (const record of records) {
|
|
740
|
+
const rehydrated = { ...record, filePath };
|
|
741
|
+
// readHashesStillMatch returns false when the recorded hashes no
|
|
742
|
+
// longer match disk OR when the read captured no hashes — both
|
|
743
|
+
// unverifiable, so both drop (safety over convenience).
|
|
744
|
+
if (this.readHashesStillMatch(rehydrated, lines)) {
|
|
745
|
+
this.recordRead(rehydrated);
|
|
746
|
+
result.imported += 1;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
result.dropped += 1;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return result;
|
|
499
754
|
}
|
|
500
755
|
/**
|
|
501
756
|
* Get all edit records for a file (for debugging).
|
|
502
757
|
*/
|
|
503
758
|
getEditHistory(filePath) {
|
|
504
|
-
|
|
759
|
+
const key = this.key(filePath);
|
|
760
|
+
if (this.edits.has(key))
|
|
761
|
+
this.touchFile(key);
|
|
762
|
+
return this.edits.get(key) ?? [];
|
|
505
763
|
}
|
|
506
764
|
// --- Private helpers ---
|
|
507
765
|
injectCreationRead(filePath, turnIndex, writeIndex) {
|
|
@@ -848,6 +1106,7 @@ export class ReadGuard {
|
|
|
848
1106
|
return { action: "allow" };
|
|
849
1107
|
}
|
|
850
1108
|
recordEdit(filePath, tool, touchedLines, verdict) {
|
|
1109
|
+
this.touchFile(filePath);
|
|
851
1110
|
const arr = this.edits.get(filePath) ?? [];
|
|
852
1111
|
arr.push({
|
|
853
1112
|
filePath,
|