pi-lens 4.1.1 → 4.1.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/CHANGELOG.md +96 -1
- package/dist/clients/actionable-warnings-logger.js +2 -1
- package/dist/clients/actionable-warnings.js +14 -5
- package/dist/clients/advisory-provenance.js +48 -18
- package/dist/clients/ast-grep-client.js +1 -3
- package/dist/clients/atomic-write-staging.js +16 -4
- package/dist/clients/bash-file-access.js +3 -1
- package/dist/clients/biome-client.js +1 -3
- package/dist/clients/blocker-freshness.js +21 -8
- package/dist/clients/bounded-cache.js +24 -8
- package/dist/clients/bounded-pid-file-lock.js +4 -3
- package/dist/clients/cache-manager.js +6 -7
- package/dist/clients/cache-observability.js +461 -66
- package/dist/clients/call-graph.js +238 -63
- package/dist/clients/codebase-model.js +13 -5
- package/dist/clients/complexity-client.js +1 -3
- package/dist/clients/dead-code-client.js +19 -8
- package/dist/clients/dead-code-logger.js +1 -2
- package/dist/clients/deadline-utils.js +3 -1
- package/dist/clients/debug-heap.js +8 -1
- package/dist/clients/degradation-ledger.js +1 -1
- package/dist/clients/dependency-checker.js +11 -7
- package/dist/clients/diagnostic-dispositions.js +39 -2
- package/dist/clients/diagnostic-line-freshness.js +5 -1
- package/dist/clients/dispatch/auxiliary-lsp.js +2 -3
- package/dist/clients/dispatch/dispatcher.js +13 -1
- package/dist/clients/dispatch/facts/function-facts.js +3 -1
- package/dist/clients/dispatch/facts/import-facts.js +2 -1
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +1 -1
- package/dist/clients/dispatch/integration.js +9 -4
- package/dist/clients/dispatch/rules/error-swallowing.js +3 -1
- package/dist/clients/dispatch/rules/pass-through-wrappers.js +6 -2
- package/dist/clients/dispatch/rules/unsafe-boundary.js +31 -7
- package/dist/clients/dispatch/runners/actionlint.js +5 -2
- package/dist/clients/dispatch/runners/biome-check.js +10 -14
- package/dist/clients/dispatch/runners/cpp-check.js +5 -1
- package/dist/clients/dispatch/runners/dotnet-build.js +1 -1
- package/dist/clients/dispatch/runners/elixir-check.js +3 -1
- package/dist/clients/dispatch/runners/eslint.js +25 -6
- package/dist/clients/dispatch/runners/fish-indent.js +1 -1
- package/dist/clients/dispatch/runners/gleam-check.js +1 -1
- package/dist/clients/dispatch/runners/golangci-lint.js +3 -1
- package/dist/clients/dispatch/runners/hadolint.js +13 -8
- package/dist/clients/dispatch/runners/helm-lint.js +4 -1
- package/dist/clients/dispatch/runners/htmlhint.js +25 -10
- package/dist/clients/dispatch/runners/javac.js +1 -1
- package/dist/clients/dispatch/runners/ktlint.js +1 -1
- package/dist/clients/dispatch/runners/lsp.js +1 -1
- package/dist/clients/dispatch/runners/markdownlint.js +28 -4
- package/dist/clients/dispatch/runners/mypy.js +6 -8
- package/dist/clients/dispatch/runners/oxlint.js +212 -11
- package/dist/clients/dispatch/runners/php-lint.js +1 -1
- package/dist/clients/dispatch/runners/phpstan.js +8 -4
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -6
- package/dist/clients/dispatch/runners/pyright.js +23 -14
- package/dist/clients/dispatch/runners/rust-clippy.js +1 -1
- package/dist/clients/dispatch/runners/shellcheck.js +7 -9
- package/dist/clients/dispatch/runners/shfmt.js +5 -2
- package/dist/clients/dispatch/runners/sqlfluff.js +7 -7
- package/dist/clients/dispatch/runners/stylelint.js +6 -8
- package/dist/clients/dispatch/runners/terragrunt.js +13 -8
- package/dist/clients/dispatch/runners/tflint.js +13 -8
- package/dist/clients/dispatch/runners/tree-sitter.js +17 -3
- package/dist/clients/dispatch/runners/utils/candidate-probe.js +7 -1
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +24 -1
- package/dist/clients/dispatch/runners/utils/tool-failure.js +35 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +1 -3
- package/dist/clients/dispatch/runners/yamllint.js +6 -8
- package/dist/clients/dispatch/runners/zig-check.js +1 -1
- package/dist/clients/dispatch/suppress-writer.js +2 -1
- package/dist/clients/dispatch/types.js +6 -1
- package/dist/clients/dispatch/utils/lsp-diagnostics.js +6 -2
- package/dist/clients/extension-log.js +5 -3
- package/dist/clients/feature-hints.js +8 -1
- package/dist/clients/file-kinds.js +34 -129
- package/dist/clients/file-utils.js +17 -1
- package/dist/clients/finding-delivery-gate.js +57 -2
- package/dist/clients/formatters.js +144 -27
- package/dist/clients/freshness.js +38 -0
- package/dist/clients/git-guard.js +43 -13
- package/dist/clients/go-client.js +1 -3
- package/dist/clients/grammar-source.js +1 -1
- package/dist/clients/gzip-stage-write.js +30 -4
- package/dist/clients/host-edit-normalize.js +1 -3
- package/dist/clients/install-diagnostics.js +4 -2
- package/dist/clients/installer/index.js +86 -77
- package/dist/clients/instance-reaper.js +5 -2
- package/dist/clients/instance-registry.js +4 -2
- package/dist/clients/jscpd-client.js +6 -1
- package/dist/clients/knip-client.js +29 -15
- package/dist/clients/language-profile.js +4 -1
- package/dist/clients/latency-logger.js +13 -2
- package/dist/clients/lens-config.js +1 -1
- package/dist/clients/lens-map.js +8 -3
- package/dist/clients/live-bus-emitter.js +1 -1
- package/dist/clients/lsp/config.js +4 -6
- package/dist/clients/lsp/edits.js +119 -32
- package/dist/clients/lsp/index.js +94 -35
- package/dist/clients/lsp/inferred-project.js +2 -1
- package/dist/clients/lsp/jvm-runtime.js +4 -1
- package/dist/clients/lsp/launch.js +11 -3
- package/dist/clients/lsp/pending-aux-coverage.js +152 -0
- package/dist/clients/lsp/server.js +19 -7
- package/dist/clients/lsp/tsserver-sync.js +10 -2
- package/dist/clients/lsp/workspace-diagnostics-cache.js +9 -4
- package/dist/clients/lsp-document-symbols.js +1 -3
- package/dist/clients/lsp-mutation.js +10 -20
- package/dist/clients/mcp/host-shim.js +1 -1
- package/dist/clients/memory-sampler.js +73 -7
- package/dist/clients/metrics-client.js +1 -3
- package/dist/clients/middle-man-analysis.js +8 -3
- package/dist/clients/module-report-lsp.js +7 -1
- package/dist/clients/module-report.js +14 -17
- package/dist/clients/ndjson-logger.js +3 -5
- package/dist/clients/opaque-mutation-scan.js +220 -0
- package/dist/clients/package-manager.js +6 -1
- package/dist/clients/pipeline.js +57 -9
- package/dist/clients/project-conventions.js +2 -8
- package/dist/clients/project-diagnostics/cache.js +10 -7
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +2 -1
- package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +3 -1
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +13 -9
- package/dist/clients/project-diagnostics/runner-adapters/trivy.js +3 -1
- package/dist/clients/project-diagnostics/scanner.js +5 -2
- package/dist/clients/project-lens-config.js +12 -5
- package/dist/clients/project-snapshot-fingerprint.js +48 -0
- package/dist/clients/project-snapshot-persist-worker.js +15 -1
- package/dist/clients/project-snapshot.js +472 -66
- package/dist/clients/read-expansion.js +12 -2
- package/dist/clients/read-guard-logger.js +5 -4
- package/dist/clients/read-guard-tool-lines.js +5 -2
- package/dist/clients/read-guard.js +1 -1
- package/dist/clients/resource-sampler.js +1 -1
- package/dist/clients/reverse-deps.js +6 -5
- package/dist/clients/review-graph/builder.js +42 -17
- package/dist/clients/review-graph/git-identity.js +8 -2
- package/dist/clients/review-graph/import-resolvers.js +10 -1
- package/dist/clients/review-graph/query.js +2 -1
- package/dist/clients/review-graph/tsconfig-paths.js +4 -2
- package/dist/clients/review-graph/workspace-modules.js +2 -2
- package/dist/clients/ruff-client.js +6 -5
- package/dist/clients/runtime-agent-end.js +63 -31
- package/dist/clients/runtime-context.js +12 -4
- package/dist/clients/runtime-coordinator.js +83 -4
- package/dist/clients/runtime-session.js +14 -0
- package/dist/clients/runtime-tool-call.js +59 -3
- package/dist/clients/runtime-tool-result.js +148 -34
- package/dist/clients/runtime-turn.js +147 -6
- package/dist/clients/rust-client.js +1 -3
- package/dist/clients/safe-spawn.js +150 -11
- package/dist/clients/security-scan-client.js +1 -3
- package/dist/clients/session-summary.js +3 -1
- package/dist/clients/sg-runner.js +1 -3
- package/dist/clients/source-filter.js +10 -6
- package/dist/clients/source-walker.js +1 -1
- package/dist/clients/spawn-timeout-cooldown.js +96 -0
- package/dist/clients/startup-scan.js +4 -1
- package/dist/clients/tool-definition.js +7 -0
- package/dist/clients/tool-policy.js +26 -11
- package/dist/clients/tool-render.js +2 -1
- package/dist/clients/tree-sitter-client.js +10 -4
- package/dist/clients/tree-sitter-logger.js +2 -2
- package/dist/clients/tree-sitter-symbol-extractor.js +12 -5
- package/dist/clients/warm-attach.js +12 -5
- package/dist/clients/widget-state.js +27 -11
- package/dist/clients/word-index.js +3 -1
- package/dist/clients/workspace-topology.js +8 -2
- package/dist/clients/zizmor-config.js +9 -1
- package/dist/i18n.js +13 -5
- package/dist/index.js +10797 -8627
- package/dist/mcp/server.js +3 -1
- package/dist/tools/ast-grep-replace.js +68 -12
- package/dist/tools/lens-diagnostic-mark.js +21 -7
- package/dist/tools/lens-diagnostics.js +164 -35
- package/dist/tools/lsp-diagnostics.js +24 -7
- package/dist/tools/lsp-navigation.js +3 -1
- package/dist/tools/module-report.js +1 -1
- package/dist/tools/project-report.js +1 -1
- package/dist/tools/symbol-search.js +5 -2
- package/docs/durable-store-audit-1202.md +1 -1
- package/docs/servercapabilities.md +1 -1
- package/package.json +160 -156
- package/rules/typos/_typos.toml +2 -2
- package/scripts/analyze-pi-lens-logs.mjs +31 -13
- package/scripts/download-grammars.js +9 -3
- package/scripts/rpc-load-check.mjs +30 -9
package/CHANGELOG.md
CHANGED
|
@@ -16,7 +16,7 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
16
16
|
|
|
17
17
|
### Security
|
|
18
18
|
|
|
19
|
-
## [4.1.
|
|
19
|
+
## [4.1.2] - 2026-08-24
|
|
20
20
|
|
|
21
21
|
### Added
|
|
22
22
|
|
|
@@ -44,6 +44,52 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
44
44
|
|
|
45
45
|
- **latency.log now says whether the LSP pool reused a client or spawned one (closes #1934)** — `lsp_client_selected` fired 5601 times in a 20.8h dogfood window carrying only `{serverId, candidateCount}`, so nothing in the log said whether the pool served a warm client or paid for a language-server spawn. This is the most expensive cache in pi-lens: a miss costs a process. The only estimate was a cross-record inference against `lsp_launch_candidate_success`, which counts something else, so a regression that halved pool reuse was invisible. The record now carries an `outcome` field with one of three values on the same record and the same denominator. `warm-reuse` means the pool served an already-connected client. `cold-spawn` means this selection waited on a spawn, whether it started that spawn or joined one already in flight, so a session-start burst where many files wait on the same spawn reads as several cold spawns and deflates the apparent reuse rate for that window. `spawn-failure` means a spawn ran and failed. Reuse rate is `warm-reuse / (warm-reuse + cold-spawn)`. A spawn failure is deliberately distinct from a clean decline: no root, a breaker already open, host trust refused, or a binary absent while installs are disabled. Declines emit no outcome record and keep their existing ones, so `lsp_client_unavailable` and the `outcome` field can both describe the same event and are counted separately. `getWarmClientForFile`, the warm-only lookup the cascade quiet window and read expansion call per file, previously returned `undefined` and emitted nothing; it now emits a bounded `lsp_warm_client_missing` record naming the candidate server and root, rising-edge per candidate set with the exact count in the degradation ledger.
|
|
46
46
|
|
|
47
|
+
- **Dogfood sessions on pi-lens's own repo now activate the oxlint, spellcheck, yamllint, and taplo runner lanes (refs #1844)** — four new root config files (`.oxlintrc.json`, `typos.toml`, `.yamllint`, `taplo.toml`) opt pi-lens's own repository into linters it already ships for every other project. `.oxlintrc.json` pins `oxlint` as a new exact-version devDependency (1.79.0, matching the runner's tested fixture), makes oxlint this repo's sole preferred JS/TS lint runner (dropping the `biome-check-json` fallback — that IS the dogfood intent), and excludes the fixture directories that carry no config override of their own; oxlint's nested-config discovery still finds the two fixture directories that deliberately escalate severity for the runner's own smoke tests, so those stay as they are. The oxlint runner also now tells a config-excluded file (`number_of_files: 0`) apart from a genuinely clean one, reporting `skipped` instead of a false `succeeded`. `typos.toml` allowlists evidenced false positives (a routine "mis-" prefix, HashiCorp's brand abbreviation, and similar); its presence enables typos-lsp's blocking opt-in, but findings stay advisory in practice since typos-lsp's own default diagnostic severity is warning. `.yamllint` scopes yamllint to hand-authored YAML (GitHub Actions workflows, top-level config), downgrades line-length to a warning rather than raising its threshold, and defuses the `on:` truthy false positive. `taplo.toml` activates both the TOML linter and the `taplo fmt` autofix formatter, and this PR runs that formatter once so activation doesn't start the lane red. Markdown linting was already live via the pre-existing `.markdownlint-cli2.jsonc` (since #1917) — confirmed with a real `markdownlint-cli2` run, no new config needed.
|
|
48
|
+
|
|
49
|
+
- **Memory-sample observability upgrades (refs #1999)** — The periodic
|
|
50
|
+
`memory_sample` latency.log line gains three fields. `peakWorkingSetBytes`
|
|
51
|
+
records the OS high-water working set from `process.resourceUsage().maxRSS`,
|
|
52
|
+
so an idle-moment rss sample can be told apart from true growth (on Windows
|
|
53
|
+
libuv backs both counters with one `GetProcessMemoryInfo()` call: rss reads
|
|
54
|
+
current `WorkingSetSize`, maxRSS reads `PeakWorkingSetSize`). A rising-edge
|
|
55
|
+
cadence tightens sampling from every 10 turns to every turn while heapUsed
|
|
56
|
+
grows more than 20% between samples, reverting once growth stabilizes; the
|
|
57
|
+
state resets at each primary session start. Each sample now also carries
|
|
58
|
+
session age, session start time, and turn count, so growth-vs-age curves are
|
|
59
|
+
plottable from logs alone. The `/lens-health` memory line shows peak WS when
|
|
60
|
+
known.
|
|
61
|
+
|
|
62
|
+
- **Opaque-write recovery for bash commands (refs #2000 phase 2)** — commands the path extractor does not recognize (python/node/perl/PowerShell internal writes) now get a bounded pre/post stat diff of the project source universe. Recovered files are attributed to the read guard as agent-authored and dispatched through the mutation seam, with explicit coverage-unknown telemetry instead of silent gaps.
|
|
63
|
+
|
|
64
|
+
- **Collect-later delivery for slow auxiliary LSP servers (closes #2001, refs #2002)** —
|
|
65
|
+
When an auxiliary scanner such as opengrep misses its aux-grace window,
|
|
66
|
+
pi-lens now marks the file and server pair in a bounded pending store
|
|
67
|
+
instead of dropping the scanner's eventual findings. The next turn end
|
|
68
|
+
probes the auxiliary's client cache through a read-only seam,
|
|
69
|
+
freshness-gates the result against the mark timestamp, and delivers
|
|
70
|
+
survivors as a `Late auxiliary diagnostics` advisory. A cited file that was
|
|
71
|
+
edited or deleted since the mark drops its findings, and both drop arms are
|
|
72
|
+
counted in the new `late_auxiliary_findings` latency record.
|
|
73
|
+
|
|
74
|
+
- **Freshness kernel: one comparator, one verdict type for staleness gates (closes #1739)** — `clients/freshness.ts` owns the mtime-vs-reference comparison and shared drift tolerance that six stores had independently reimplemented (three copies carried the identical #1710/#1711 tolerance defect). `freshnessFromMtime` returns an explicit verdict (`fresh` / `stale: modified-after-reference` / `indeterminate: no-mtime-evidence`) so each caller keeps its own no-evidence policy while sharing the comparison. A registered-or-fail sweep fails on any new out-of-kernel mtime-vs-reference comparison in `clients/`.
|
|
75
|
+
|
|
76
|
+
- **Shared fault-injection test kit (closes #1838)** — `tests/support/fault-injection.ts` promotes the bespoke fault probes reviewers kept rebuilding by hand into four one-call primitives: `spawnWedgedChild` (a real child whose stdin pipe is genuinely full — the #1811 fixture generalized, with its fail-fast-instead-of-hang trap asserted against), `delayInside` (deterministic completion delay inside any mocked async seam), `fireResetAt` (fire a lifecycle hook from inside a seam's implementation at a chosen call — the #1746-R2-F1 shape), and `starveBudget` plus `gatedPromise` (the tiny-budget starvation repro). Every primitive carries its own fidelity test in `fault-injection.test.ts`, so a neutered primitive goes red in CI instead of silently weakening every consumer.
|
|
77
|
+
|
|
78
|
+
- **Adversarial garbage battery for every CLI lint runner (closes #1839)** — Twenty runners now meet a 12-case battery of hostile outputs (truncated JSON, usage prose on the findings stream, unknown severities, hostile numbers) under format-blind invariants: never crash, never emit malformed diagnostics, never report clean on a nonzero exit carrying bytes. The first pass found 79 violations of that last invariant across 14 runners — all fixed by consolidating their identical tails onto one shared `finishParsedRun` seam. Also fixes htmlhint's `--rules` flag being fed JSON (the tool wants a ruleid list), which left zero rules enabled so every file read clean.
|
|
79
|
+
|
|
80
|
+
### Changed
|
|
81
|
+
|
|
82
|
+
- **pi-lens formats its own TypeScript with oxfmt (refs #1844)** — The
|
|
83
|
+
repository now carries an `.oxfmtrc.json` and an `oxfmt` devDependency, and
|
|
84
|
+
an advisory `oxfmt --check` job runs in CI. Because `hasOxfmtConfig` gates
|
|
85
|
+
the oxfmt formatter on exactly that config file, a pi-lens session opened on
|
|
86
|
+
this repository now dispatches oxfmt as the format runner for edited
|
|
87
|
+
TypeScript and JavaScript files.
|
|
88
|
+
|
|
89
|
+
- **Opaque-write recovery goes git-first (refs #2000)** — inside a git worktree the pre side records only a timestamp and `git status --porcelain` plus an mtime window answers what changed, with no file-universe cap: recovery now works on any repo size, including large monorepos where the stat-walk previously degraded every command to coverage-unknown. The bounded stat-diff path remains for small non-git trees.
|
|
90
|
+
|
|
91
|
+
- **ONE mutation seam: `RuntimeCoordinator.recordProjectMutation` (refs #2000 phase 1)** — the triplicated bump+change-log pairing (runtime-tool-result, runtime-agent-end, lsp-mutation) is consolidated onto one seam that bumps the seq store, appends a bounded attributed receipt ring (`getMutationsSince`, cap 512 with a surfaced dropped-count), and appends the durable change-log entry. Consumers derive touched-files answers from one store instead of three hand-copied pairings; phase 2's opaque-write recovery feeds the same seam.
|
|
92
|
+
|
|
47
93
|
### Fixed
|
|
48
94
|
|
|
49
95
|
- **Search credit ignores pipe truncation (closes #1908)** — `grep -C2 pattern
|
|
@@ -106,6 +152,55 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
106
152
|
|
|
107
153
|
- **Warmup and workspace-scan walkers gate by file extension before consulting the ignore matcher, cutting a 31.7s stall to sub-second on repos with large ignored file piles (closes #1974, reported by @0xkite)** — `isIgnored` recompiles minimatch patterns per ancestor directory on every call; a runtime-output directory like `wal/` holding tens of thousands of `.log` files, gitignored only by a file-level `*.log` pattern (so the directory itself is never pruned), forced every one of those files through that cost. Six walkers checked `isIgnored` before their own cheap extension gate, paying the expensive check for files the gate would have dropped anyway: `collectSourceFilesForWarmup` (`language-profile.ts`, the reported 31.7s case), `JscpdClient.hasSourceFilesRecursive` (`jscpd-client.ts`), `classifyEntry` (`source-filter.ts`), `makeSourceCountVisitor` (`startup-scan.ts`), `getModuleSourceFiles` (`review-graph/workspace-modules.ts`), and `collectWorkspaceDiagnosticFiles` (`lsp/index.ts`). All six now run their extension (or LSP-server) gate first; output is unchanged, since both gates still have to pass to keep a file — only the order changed. A repo-wide sweep of all 14 files that call `isIgnored` found `tree-sitter-client.ts` and `tools/lsp-diagnostics.ts` already extension-first, and cleared the remaining per-event/no-ext-gate sites by construction; see the PR body for the full per-site table.
|
|
108
154
|
|
|
155
|
+
- **oxfmt no longer fails on a file its own config ignores (refs #1844)** —
|
|
156
|
+
pi-lens offers oxfmt for every extension oxfmt supports, so in a project
|
|
157
|
+
whose oxfmt config carries `ignorePatterns`, oxfmt was selected for files it
|
|
158
|
+
then refused to touch. It exits 2 with "Expected at least one target file",
|
|
159
|
+
which the strict exit-code posture read as a formatting failure, so every
|
|
160
|
+
edit to an ignored file surfaced an error. pi-lens now passes
|
|
161
|
+
`--no-error-on-unmatched-pattern`, which makes an empty target set a clean
|
|
162
|
+
no-op. Every other nonzero exit, including an unparseable file, still fails.
|
|
163
|
+
|
|
164
|
+
- **ESLint warnings are no longer discarded (closes #1954)** — The eslint runner treated every exit 0 as "nothing found". ESLint exits 0 whenever no rule reaches error severity, so a run that produced only warning-severity findings was thrown away silently. The runner now parses stdout unconditionally and surfaces the findings; a clean file still reports clean, and exit 2 stays an unavailable skip.
|
|
165
|
+
|
|
166
|
+
- **Registered tools now always have descriptions in child/subagent sessions (closes #1988)** — the final registration boundary fills missing, empty, and
|
|
167
|
+
whitespace-only descriptions for active, lazy, activation, and wrapped tool
|
|
168
|
+
definitions.
|
|
169
|
+
|
|
170
|
+
- **mode=full no longer replays stale mid-edit blockers (refs #1993)** — a confirmed, fully-covered LSP sweep is now authoritative for its files: widget-store diagnostics captured from a since-fixed broken intermediate state are retired instead of rendering as current 🔴 blockers beside a clean sweep. Unconfirmed or timed-out sweeps keep the fail-open behavior.
|
|
171
|
+
|
|
172
|
+
- **Cache-miss attribution stays useful in long sessions (closes #1996)** — classify full model/provider identities and split unexplained or malformed cache evidence by bounded reason, with fail-closed request hashing, activation-owned primary/secondary isolation, sanitized numeric metadata, and a per-session cause summary.
|
|
173
|
+
|
|
174
|
+
- **Project snapshots no longer rewrite unchanged same-generation bodies (closes #1997)** — Persistence now coalesces concurrent requests and computes semantic identity on its worker before gzip or staging. Same-generation content changes and failed-write repairs still publish.
|
|
175
|
+
|
|
176
|
+
- **Classify oxlint no-match results as expected skips (closes #1998)**. Oxlint now carries `no-files-matched` through runner telemetry without emitting an extension error or claiming the file is clean. A single fail-closed state machine validates process completion, exit status, stderr, the captured banner, and every JSON summary field's type and range; truncated, malformed, wrong-status, or error-bearing lookalikes retain failure or unconfirmed telemetry.
|
|
177
|
+
|
|
178
|
+
- **Project-snapshot persistence validates gzip body integrity before skipping (closes #2008)** —
|
|
179
|
+
The skip decision trusted the meta sidecar and a fingerprint match alone, so a torn or
|
|
180
|
+
truncated gzip under an intact meta kept winning unchanged-dedupe and stayed canonical until
|
|
181
|
+
the project sequence advanced. The meta sidecar now records the on-disk gz byte length
|
|
182
|
+
(`gzBytes`) at every successful persist, and the dedupe baselines compare it against a live
|
|
183
|
+
stat: a size mismatch, or a legacy meta without the field, withholds the dedupe fingerprints
|
|
184
|
+
so the pending save republishes and rewrites the body. A skip that would honor evidence
|
|
185
|
+
failing this gate between dispatch and promotion is refused and rewritten synchronously.
|
|
186
|
+
Detections emit one `project_snapshot_body_integrity` latency record plus a bounded
|
|
187
|
+
`snapshot-integrity` degradation-ledger entry per corrupted body path. The baselines read is
|
|
188
|
+
also now a pure read: its in-process seeding write moved to the single dispatch seam that
|
|
189
|
+
owns the persist lifecycle.
|
|
190
|
+
|
|
191
|
+
- **Installer verify loop broken at the root (closes #2015)** — `verifyToolBinary` now spawns through `safeSpawnAsync` (tree-kill on timeout, typed kill-reason) instead of raw spawn whose SIGTERM orphaned grandchild node processes on Windows `.cmd` shims. A killed/inconclusive prober no longer counts as a verdict: the freshly installed binary is KEPT for cheap re-probe instead of deleted, ending the install/verify/reinstall churn (23 SIGTERMs and 4 cleanups observed in one day).
|
|
192
|
+
|
|
193
|
+
- **Windows `.cmd`/`.bat` spawns no longer fail when System32 is absent from the child PATH (closes #2023)** — The cmd.exe wrapper prefixed every spawn
|
|
194
|
+
with a bare `chcp 65001 &&`. When the child environment's PATH could not
|
|
195
|
+
resolve `chcp.com`, the lookup failed and `&&` short-circuited, so the whole
|
|
196
|
+
spawn exited 1 with empty output. chcp is now invoked via its pinned
|
|
197
|
+
`%SystemRoot%\System32\chcp.com` absolute path and chained with `&`, so a
|
|
198
|
+
code-page failure can never suppress the real command.
|
|
199
|
+
|
|
200
|
+
- **Register remaining agent-facing delivery surfaces (#2028)** — The per-edit 🔴 STOP block now drops blockers whose cited file no longer exists, so a deleted file's stale blocker no longer re-blocks the agent with no remediation. All five previously unregistered surfaces (stop blocker, `lsp_diagnostics` output, git-guard verdicts, read-guard preflight errors, thrashing notices) are registered in the finding-delivery gate registry.
|
|
201
|
+
|
|
202
|
+
- **Wall-clock budget tests get a quiet serial phase (closes #1920)** — Five test files asserting real elapsed-time budgets ran inside the default project's fork storm, measuring scheduler contention instead of code speed (`startup-overhead` measured 659-2321ms against a 500ms budget under load, green solo every time). They now run in a dedicated fully-serialized `wall-clock-budget` Vitest project that phases dead last on a quiet host. A coverage guard keeps the new include list from silently dropping renamed files.
|
|
203
|
+
|
|
109
204
|
## [4.1.0] - 2026-08-20
|
|
110
205
|
|
|
111
206
|
### Added
|
|
@@ -5,7 +5,8 @@ import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
|
5
5
|
const AW_LOG_DIR = getGlobalPiLensDir();
|
|
6
6
|
const AW_LOG_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log");
|
|
7
7
|
const AW_LOG_BACKUP_FILE = path.join(AW_LOG_DIR, "actionable-warnings.log.1");
|
|
8
|
-
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
8
|
+
const MAX_LOG_BYTES = Math.max(128 * 1024, Number.parseInt(process.env.PI_LENS_AW_LOG_MAX_BYTES ?? "1048576", 10) ||
|
|
9
|
+
1048576);
|
|
9
10
|
const writer = createNdjsonLogger({
|
|
10
11
|
filePath: AW_LOG_FILE,
|
|
11
12
|
maxBytes: MAX_LOG_BYTES,
|
|
@@ -633,7 +633,12 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
633
633
|
}
|
|
634
634
|
const edit = selected.edit;
|
|
635
635
|
const applied = await applyWorkspaceEdit(edit, args.cwd, args.mutationContext
|
|
636
|
-
? {
|
|
636
|
+
? {
|
|
637
|
+
mutationContext: {
|
|
638
|
+
...args.mutationContext,
|
|
639
|
+
emitSummary: false,
|
|
640
|
+
},
|
|
641
|
+
}
|
|
637
642
|
: undefined);
|
|
638
643
|
appliedResults.push(applied);
|
|
639
644
|
for (const changedFile of applied.files)
|
|
@@ -642,8 +647,7 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
642
647
|
}
|
|
643
648
|
catch (err) {
|
|
644
649
|
failedCount++;
|
|
645
|
-
const partial = err
|
|
646
|
-
.appliedWorkspaceEdit;
|
|
650
|
+
const partial = err.appliedWorkspaceEdit;
|
|
647
651
|
if (partial) {
|
|
648
652
|
appliedResults.push(partial);
|
|
649
653
|
for (const changedFile of partial.files)
|
|
@@ -656,13 +660,18 @@ export async function applyConservativeActionableWarningFixes(args) {
|
|
|
656
660
|
}
|
|
657
661
|
}
|
|
658
662
|
summary.changedFiles = [...changedFiles];
|
|
659
|
-
if (args.mutationContext &&
|
|
663
|
+
if (args.mutationContext &&
|
|
664
|
+
(summary.considered > 0 || appliedResults.length > 0)) {
|
|
660
665
|
recordLspMutationBatch(args.mutationContext, {
|
|
661
666
|
results: appliedResults,
|
|
662
667
|
considered: summary.considered,
|
|
663
668
|
completed: summary.applied,
|
|
664
669
|
failedCount,
|
|
665
|
-
status: failedCount > 0
|
|
670
|
+
status: failedCount > 0
|
|
671
|
+
? "failed"
|
|
672
|
+
: appliedResults.length > 0
|
|
673
|
+
? "success"
|
|
674
|
+
: "skipped",
|
|
666
675
|
bookkeep: false,
|
|
667
676
|
});
|
|
668
677
|
}
|
|
@@ -4,7 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { logLatency } from "./latency-logger.js";
|
|
5
5
|
import { normalizeMapKey, toProjectRelativePath } from "./path-utils.js";
|
|
6
6
|
import { resolveRunnerPath } from "./dispatch/runner-context.js";
|
|
7
|
-
import {
|
|
7
|
+
import { freshnessFromMtime } from "./freshness.js";
|
|
8
8
|
export const MAX_ADVISORY_AFFECTED_FILES = 256;
|
|
9
9
|
export function advisoryPathKey(filePath, cwd) {
|
|
10
10
|
return normalizeMapKey(path.resolve(cwd, filePath));
|
|
@@ -68,25 +68,41 @@ function isFiniteNumber(value) {
|
|
|
68
68
|
return typeof value === "number" && Number.isFinite(value);
|
|
69
69
|
}
|
|
70
70
|
function isCapturedHash(value) {
|
|
71
|
-
return typeof value === "string" &&
|
|
72
|
-
(/^[a-f0-9]{64}$/.test(value) ||
|
|
71
|
+
return (typeof value === "string" &&
|
|
72
|
+
(/^[a-f0-9]{64}$/.test(value) ||
|
|
73
|
+
value === "missing" ||
|
|
74
|
+
value.startsWith("unreadable:")));
|
|
73
75
|
}
|
|
74
76
|
function isWellFormed(value) {
|
|
75
77
|
if (!value || typeof value !== "object")
|
|
76
78
|
return false;
|
|
77
79
|
const record = value;
|
|
78
80
|
const revision = record.revision;
|
|
79
|
-
return !!revision &&
|
|
80
|
-
|
|
81
|
-
isFiniteNumber(revision.
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
isFiniteNumber(
|
|
85
|
-
|
|
81
|
+
return (!!revision &&
|
|
82
|
+
typeof revision.sessionId === "string" &&
|
|
83
|
+
isFiniteNumber(revision.projectSeq) &&
|
|
84
|
+
isFiniteNumber(revision.turnIndex) &&
|
|
85
|
+
isFiniteNumber(revision.generation) &&
|
|
86
|
+
isFiniteNumber(revision.capturedAt) &&
|
|
87
|
+
Array.isArray(record.files) &&
|
|
88
|
+
record.files.length > 0 &&
|
|
89
|
+
record.files.every((file) => !!file &&
|
|
90
|
+
typeof file.path === "string" &&
|
|
91
|
+
(file.role === "source" ||
|
|
92
|
+
file.role === "test" ||
|
|
93
|
+
file.role === "affected") &&
|
|
94
|
+
isFiniteNumber(file.mtimeMs) &&
|
|
95
|
+
isFiniteNumber(file.size) &&
|
|
96
|
+
isCapturedHash(file.sha256)));
|
|
86
97
|
}
|
|
87
98
|
export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
88
99
|
if (!isWellFormed(record.provenance)) {
|
|
89
|
-
return {
|
|
100
|
+
return {
|
|
101
|
+
status: "unknown",
|
|
102
|
+
reasons: ["malformed-or-legacy-provenance"],
|
|
103
|
+
allFilesDeleted: false,
|
|
104
|
+
changedPathCount: 0,
|
|
105
|
+
};
|
|
90
106
|
}
|
|
91
107
|
const provenance = record.provenance;
|
|
92
108
|
const reasons = [];
|
|
@@ -149,9 +165,19 @@ export function validateAdvisoryProvenance(record, cwd, runtime) {
|
|
|
149
165
|
}
|
|
150
166
|
const allFilesDeleted = deletedFiles === provenance.files.length;
|
|
151
167
|
if (unknown)
|
|
152
|
-
return {
|
|
168
|
+
return {
|
|
169
|
+
status: "unknown",
|
|
170
|
+
reasons,
|
|
171
|
+
allFilesDeleted,
|
|
172
|
+
changedPathCount: changedPaths.size,
|
|
173
|
+
};
|
|
153
174
|
return reasons.length > 0
|
|
154
|
-
? {
|
|
175
|
+
? {
|
|
176
|
+
status: "superseded",
|
|
177
|
+
reasons,
|
|
178
|
+
allFilesDeleted,
|
|
179
|
+
changedPathCount: changedPaths.size,
|
|
180
|
+
}
|
|
155
181
|
: { status: "current", reasons: [], allFilesDeleted, changedPathCount: 0 };
|
|
156
182
|
}
|
|
157
183
|
// ── Finding-cited path existence (#1461 slice 1) ──────────────────────────────
|
|
@@ -224,11 +250,15 @@ const MAX_LOGGED_DEAD_PATHS = 3;
|
|
|
224
250
|
export function findingPathFreshness(resolvedPath, scannedAtMs) {
|
|
225
251
|
try {
|
|
226
252
|
const stat = fs.statSync(resolvedPath);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
253
|
+
// No scan timestamp recorded: the pre-kernel behavior treated the
|
|
254
|
+
// path as live (no reference to be stale against) - preserved.
|
|
255
|
+
if (scannedAtMs === undefined)
|
|
256
|
+
return "live";
|
|
257
|
+
const verdict = freshnessFromMtime({
|
|
258
|
+
mtimeMs: stat.mtimeMs,
|
|
259
|
+
referenceMs: scannedAtMs,
|
|
260
|
+
});
|
|
261
|
+
return verdict.verdict === "stale" ? "stale" : "live";
|
|
232
262
|
}
|
|
233
263
|
catch (error) {
|
|
234
264
|
const code = error.code ?? "unknown";
|
|
@@ -113,9 +113,7 @@ export class AstGrepClient {
|
|
|
113
113
|
(fs.existsSync(projectRuleDir)
|
|
114
114
|
? projectRuleDir
|
|
115
115
|
: resolvePackagePath(import.meta.url, "rules"));
|
|
116
|
-
this.log = verbose
|
|
117
|
-
? createSubsystemLogger("ast-grep")
|
|
118
|
-
: () => { };
|
|
116
|
+
this.log = verbose ? createSubsystemLogger("ast-grep") : () => { };
|
|
119
117
|
this.ruleManager = new AstGrepRuleManager(this.ruleDir, this.log);
|
|
120
118
|
this.runner = new SgRunner(verbose);
|
|
121
119
|
}
|
|
@@ -19,7 +19,13 @@ export function stageOwnerPidFromName(name) {
|
|
|
19
19
|
}
|
|
20
20
|
export async function sweepOwnStagingFiles(directories, options) {
|
|
21
21
|
const uniqueDirectories = [...new Set(directories)].filter(Boolean);
|
|
22
|
-
const result = {
|
|
22
|
+
const result = {
|
|
23
|
+
directories: uniqueDirectories.length,
|
|
24
|
+
scanned: 0,
|
|
25
|
+
removed: 0,
|
|
26
|
+
liveSkipped: 0,
|
|
27
|
+
truncated: false,
|
|
28
|
+
};
|
|
23
29
|
for (const directory of uniqueDirectories) {
|
|
24
30
|
let handle;
|
|
25
31
|
try {
|
|
@@ -47,14 +53,20 @@ export async function sweepOwnStagingFiles(directories, options) {
|
|
|
47
53
|
continue;
|
|
48
54
|
}
|
|
49
55
|
try {
|
|
50
|
-
await fs.promises.rm(path.join(directory, entry.name), {
|
|
56
|
+
await fs.promises.rm(path.join(directory, entry.name), {
|
|
57
|
+
force: true,
|
|
58
|
+
});
|
|
51
59
|
result.removed++;
|
|
52
60
|
}
|
|
53
|
-
catch {
|
|
61
|
+
catch {
|
|
62
|
+
/* leave it for the next bounded sweep */
|
|
63
|
+
}
|
|
54
64
|
}
|
|
55
65
|
result.truncated ||= reachedEntryCap;
|
|
56
66
|
}
|
|
57
|
-
catch {
|
|
67
|
+
catch {
|
|
68
|
+
/* best-effort directory scan */
|
|
69
|
+
}
|
|
58
70
|
finally {
|
|
59
71
|
await handle.close().catch(() => { });
|
|
60
72
|
}
|
|
@@ -634,7 +634,9 @@ function collectGrepCommandFiles(command, cwd) {
|
|
|
634
634
|
return {
|
|
635
635
|
hasLineNumberGrep,
|
|
636
636
|
files,
|
|
637
|
-
context: truncated
|
|
637
|
+
context: truncated
|
|
638
|
+
? { before: 0, after: 0 }
|
|
639
|
+
: (context ?? { before: 0, after: 0 }),
|
|
638
640
|
};
|
|
639
641
|
}
|
|
640
642
|
function dedupePushSearchRead(out, seen, loc) {
|
|
@@ -58,9 +58,7 @@ export class BiomeClient {
|
|
|
58
58
|
ensureFlight = createSingleFlight();
|
|
59
59
|
log;
|
|
60
60
|
constructor(verbose = false) {
|
|
61
|
-
this.log = verbose
|
|
62
|
-
? createSubsystemLogger("biome")
|
|
63
|
-
: () => { };
|
|
61
|
+
this.log = verbose ? createSubsystemLogger("biome") : () => { };
|
|
64
62
|
}
|
|
65
63
|
/**
|
|
66
64
|
* Resolve the fastest available biome binary for `cwd`.
|
|
@@ -126,7 +126,11 @@ async function resolveForwardImportsMemoized(cwd, filePath, resolveForwardImport
|
|
|
126
126
|
catch {
|
|
127
127
|
imports = [];
|
|
128
128
|
}
|
|
129
|
-
importResolutionMemo.set(filePath, {
|
|
129
|
+
importResolutionMemo.set(filePath, {
|
|
130
|
+
mtimeMs: sig.mtimeMs,
|
|
131
|
+
size: sig.size,
|
|
132
|
+
imports,
|
|
133
|
+
});
|
|
130
134
|
return imports;
|
|
131
135
|
}
|
|
132
136
|
/** Test-only: clear the extractor and import-resolution memos. */
|
|
@@ -232,17 +236,26 @@ export async function collectForwardImportMtimes(cwd, filePath, resolveForwardIm
|
|
|
232
236
|
* +50ms). +50ms comfortably clears the measured skew while staying far below the
|
|
233
237
|
* gap between genuinely distinct edits.
|
|
234
238
|
*/
|
|
235
|
-
|
|
239
|
+
// Single implementation lives in the freshness kernel (#1739); this
|
|
240
|
+
// re-export preserves every existing importer's path.
|
|
241
|
+
export { MTIME_DRIFT_TOLERANCE_MS } from "./freshness.js";
|
|
242
|
+
import { freshnessFromMtime } from "./freshness.js";
|
|
236
243
|
async function detectDrift(cwd, filePath, recordedAtMs, resolveForwardImports, turnIndex) {
|
|
237
244
|
const drifted = [];
|
|
238
|
-
const
|
|
239
|
-
|
|
245
|
+
const ownFreshness = freshnessFromMtime({
|
|
246
|
+
mtimeMs: await statMtimeMs(filePath),
|
|
247
|
+
referenceMs: recordedAtMs,
|
|
248
|
+
});
|
|
249
|
+
if (ownFreshness.verdict === "stale")
|
|
240
250
|
drifted.push(filePath);
|
|
241
|
-
}
|
|
242
251
|
const { mtimes, truncated } = await collectForwardImportMtimes(cwd, filePath, resolveForwardImports, turnIndex);
|
|
243
|
-
for (const { path, mtimeMs } of mtimes) {
|
|
244
|
-
|
|
245
|
-
|
|
252
|
+
for (const { path: depPath, mtimeMs } of mtimes) {
|
|
253
|
+
const verdict = freshnessFromMtime({
|
|
254
|
+
mtimeMs,
|
|
255
|
+
referenceMs: recordedAtMs,
|
|
256
|
+
});
|
|
257
|
+
if (verdict.verdict === "stale")
|
|
258
|
+
drifted.push(depPath);
|
|
246
259
|
}
|
|
247
260
|
return { drifted, truncated };
|
|
248
261
|
}
|
|
@@ -13,7 +13,9 @@ export class BoundedLruCache {
|
|
|
13
13
|
}
|
|
14
14
|
return value;
|
|
15
15
|
}
|
|
16
|
-
has(key) {
|
|
16
|
+
has(key) {
|
|
17
|
+
return this.entries.has(key);
|
|
18
|
+
}
|
|
17
19
|
set(key, value) {
|
|
18
20
|
this.entries.delete(key);
|
|
19
21
|
this.entries.set(key, value);
|
|
@@ -24,11 +26,25 @@ export class BoundedLruCache {
|
|
|
24
26
|
this.entries.delete(oldest);
|
|
25
27
|
}
|
|
26
28
|
}
|
|
27
|
-
delete(key) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
29
|
+
delete(key) {
|
|
30
|
+
return this.entries.delete(key);
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.entries.clear();
|
|
34
|
+
}
|
|
35
|
+
get size() {
|
|
36
|
+
return this.entries.size;
|
|
37
|
+
}
|
|
38
|
+
keys() {
|
|
39
|
+
return this.entries.keys();
|
|
40
|
+
}
|
|
41
|
+
values() {
|
|
42
|
+
return this.entries.values();
|
|
43
|
+
}
|
|
44
|
+
entriesArray() {
|
|
45
|
+
return [...this.entries.entries()];
|
|
46
|
+
}
|
|
47
|
+
[Symbol.iterator]() {
|
|
48
|
+
return this.entries[Symbol.iterator]();
|
|
49
|
+
}
|
|
34
50
|
}
|
|
@@ -35,8 +35,7 @@ async function releaseQuarantineLock(lockPath, token) {
|
|
|
35
35
|
break;
|
|
36
36
|
}
|
|
37
37
|
catch (error) {
|
|
38
|
-
if (error.code !== "ENOENT" ||
|
|
39
|
-
attempt === 2)
|
|
38
|
+
if (error.code !== "ENOENT" || attempt === 2)
|
|
40
39
|
return;
|
|
41
40
|
await new Promise((resolve) => setImmediate(resolve));
|
|
42
41
|
}
|
|
@@ -111,7 +110,9 @@ async function tryAcquireQuarantineLock(lockPath, staleMs) {
|
|
|
111
110
|
await fsp.writeFile(path.join(lockPath, "owner.json"), JSON.stringify(owner), "utf8");
|
|
112
111
|
}
|
|
113
112
|
catch (error) {
|
|
114
|
-
await fsp
|
|
113
|
+
await fsp
|
|
114
|
+
.rm(lockPath, { recursive: true, force: true })
|
|
115
|
+
.catch(() => { });
|
|
115
116
|
throw error;
|
|
116
117
|
}
|
|
117
118
|
return () => releaseQuarantineLock(lockPath, owner.token);
|
|
@@ -39,9 +39,7 @@ function getTurnStatePath(cwd) {
|
|
|
39
39
|
export class CacheManager {
|
|
40
40
|
log;
|
|
41
41
|
constructor(verbose = false) {
|
|
42
|
-
this.log = verbose
|
|
43
|
-
? createSubsystemLogger("cache")
|
|
44
|
-
: () => { };
|
|
42
|
+
this.log = verbose ? createSubsystemLogger("cache") : () => { };
|
|
45
43
|
}
|
|
46
44
|
/**
|
|
47
45
|
* Convert a file path to a stable turn-state key.
|
|
@@ -226,11 +224,12 @@ export class CacheManager {
|
|
|
226
224
|
// stale-session eviction behavior for this legacy shape.
|
|
227
225
|
return "available";
|
|
228
226
|
}
|
|
229
|
-
if (state.owner.kind === owner.kind &&
|
|
230
|
-
state.owner.id === owner.id) {
|
|
227
|
+
if (state.owner.kind === owner.kind && state.owner.id === owner.id) {
|
|
231
228
|
return "owned";
|
|
232
229
|
}
|
|
233
|
-
return this.isTurnStateOwnerStale(state.owner)
|
|
230
|
+
return this.isTurnStateOwnerStale(state.owner)
|
|
231
|
+
? "available"
|
|
232
|
+
: "foreign-live";
|
|
234
233
|
}
|
|
235
234
|
isTurnStateOwnerStale(owner) {
|
|
236
235
|
if (owner.pid > 0 && owner.pid !== process.pid) {
|
|
@@ -243,7 +242,7 @@ export class CacheManager {
|
|
|
243
242
|
}
|
|
244
243
|
}
|
|
245
244
|
const lastSeen = Date.parse(owner.lastSeen);
|
|
246
|
-
return !Number.isFinite(lastSeen) || Date.now() - lastSeen > TURN_OWNER_STALE_MS;
|
|
245
|
+
return (!Number.isFinite(lastSeen) || Date.now() - lastSeen > TURN_OWNER_STALE_MS);
|
|
247
246
|
}
|
|
248
247
|
/**
|
|
249
248
|
* Add or update a file's modified ranges in turn state.
|