pi-lens 3.8.73 → 3.8.74
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 +1116 -0
- package/README.md +37 -21
- package/dist/clients/atomic-write.js +3 -3
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/cache/rule-cache.js +43 -2
- package/dist/clients/cache-observability.js +211 -0
- package/dist/clients/call-graph.js +20 -0
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/complexity-client.js +4 -7
- package/dist/clients/dependency-checker.js +151 -20
- package/dist/clients/diagnostic-dispositions.js +22 -2
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +16 -1
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +14 -0
- package/dist/clients/dispatch/facts/import-facts.js +100 -101
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +5 -8
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/integration.js +94 -15
- 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 +24 -1
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/tree-sitter.js +153 -166
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +17 -18
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +86 -31
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/generated-artifacts.js +21 -0
- package/dist/clients/gzip-stage-write.js +84 -0
- package/dist/clients/installer/index.js +358 -129
- package/dist/clients/knip-client.js +64 -1
- package/dist/clients/language-profile.js +61 -72
- package/dist/clients/latency-logger.js +6 -16
- package/dist/clients/lens-config.js +128 -165
- package/dist/clients/lens-engine.js +39 -5
- package/dist/clients/lens-flag-registry.js +285 -0
- package/dist/clients/lens-map.js +2 -7
- package/dist/clients/log-cleanup.js +6 -17
- package/dist/clients/lsp/cascade-tier.js +22 -2
- package/dist/clients/lsp/client.js +74 -10
- package/dist/clients/lsp/config.js +62 -9
- package/dist/clients/lsp/index.js +34 -24
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +8 -6
- package/dist/clients/lsp/server.js +84 -29
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/mcp/analyze.js +5 -4
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/module-report.js +100 -80
- package/dist/clients/ndjson-logger.js +51 -21
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +35 -0
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +7 -0
- package/dist/clients/project-changes.js +77 -4
- 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 +109 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +247 -127
- package/dist/clients/project-lens-config.js +53 -26
- package/dist/clients/project-report.js +39 -14
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +611 -7
- package/dist/clients/quiet-window.js +9 -1
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard.js +78 -0
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +133 -14
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +1522 -179
- 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-logger.js +24 -0
- package/dist/clients/runtime-agent-end.js +4 -2
- package/dist/clients/runtime-coordinator.js +2 -2
- package/dist/clients/runtime-session.js +433 -59
- package/dist/clients/runtime-tool-call.js +46 -14
- package/dist/clients/runtime-turn.js +97 -26
- package/dist/clients/safe-spawn.js +102 -15
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +2 -1
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +48 -33
- package/dist/clients/source-filter.js +49 -25
- package/dist/clients/startup-marker.js +6 -0
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tree-sitter-cache.js +147 -139
- package/dist/clients/tree-sitter-client.js +1340 -67
- package/dist/clients/tree-sitter-logger.js +43 -1
- package/dist/clients/tree-sitter-query-loader.js +110 -11
- package/dist/clients/tree-sitter-shared.js +43 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +71 -4
- package/dist/clients/widget-state.js +50 -14
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +252 -35
- package/dist/clients/workspace-topology.js +11 -1
- package/dist/index.js +45544 -39255
- package/dist/mcp/analyze-cli.js +3 -0
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +63 -8
- package/dist/tools/lens-diagnostics.js +47 -5
- package/dist/tools/lsp-navigation.js +5 -28
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/environment-variables.md +153 -11
- package/docs/features.md +4 -3
- package/docs/globalconfig.md +56 -4
- package/docs/language-coverage.md +2 -2
- package/docs/mcp.md +17 -16
- package/docs/servercapabilities.md +5 -2
- package/docs/settings.md +181 -0
- package/docs/tools.md +14 -0
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +13 -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-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/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-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/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/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/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/config-enabled-shape.js +0 -39
- 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
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,1122 @@ All notable changes to pi-lens will be documented in this file.
|
|
|
10
10
|
|
|
11
11
|
### Fixed
|
|
12
12
|
|
|
13
|
+
## [3.8.74] - 2026-08-01
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **🙏 Huge thanks to @AngriestBird (Kenny McCormick), this round's standout community contributor** — five merged PRs landed this release: a single source of truth for CLI flags (#1011), secret redaction at the log-write boundary (#954), new slash commands (#906), tree-cache performance improvements (#877), and a test harness for rule-dispatch behavior (#871). Sustained, high-quality work spanning perf, security, config, and test infrastructure — thank you.
|
|
18
|
+
|
|
19
|
+
- **Prompt-cache observability (refs #1018, closes #1018)** — two provider-independent
|
|
20
|
+
signals now land in `~/.pi-lens/latency.log` as `type: "phase"` records.
|
|
21
|
+
(1) Response-side: a defensively feature-detected `message_end` subscription
|
|
22
|
+
(clients/agent-nudge.ts pattern — guarded, never throws on older hosts) logs one
|
|
23
|
+
`cache_usage` record per assistant message that carries a `usage`, with
|
|
24
|
+
`metadata: { provider, model, cacheRead, cacheWrite, input, output, cost }`
|
|
25
|
+
(provider/model read straight off the assistant message; `cost` is the total).
|
|
26
|
+
Messages with no usage (or non-assistant messages) are skipped rather than
|
|
27
|
+
logged as zeros. (2) Request-side: the existing `context` handler now hashes
|
|
28
|
+
`messages[0]` on every call and logs a `cache_prefix_break` record
|
|
29
|
+
(`metadata: { turnIndex, previousHash, currentHash, sessionId, sessionRole }`,
|
|
30
|
+
plus a baseline record on first observation) whenever that hash changes
|
|
31
|
+
turn-over-turn — a regression guard that flags anything (pi-lens or otherwise)
|
|
32
|
+
breaking the byte-stable prefix #1016 established. The baseline is keyed by the
|
|
33
|
+
stable per-session id (`ctx.sessionManager.getSessionId()`) in a small bounded
|
|
34
|
+
LRU, so a resume/reload keeps comparing against its own baseline while a
|
|
35
|
+
new/fork or a concurrent in-process subagent (#473) gets an independent baseline
|
|
36
|
+
instead of a spurious cross-session break; each record is tagged with the
|
|
37
|
+
read-only #473 `sessionRole` classification, and a primary `session_shutdown`
|
|
38
|
+
drops the ended session's entry. The hash is pure observation: it never changes
|
|
39
|
+
the handler's injection behavior or return value, and runs even on non-injecting
|
|
40
|
+
turns. New `clients/cache-observability.ts`; no new dependencies.
|
|
41
|
+
|
|
42
|
+
- **Word-index build/refresh/persist outcomes are now durably observable**
|
|
43
|
+
(refs #958, #926, #533) — every word-index signal previously rode solely on
|
|
44
|
+
the optional `dbg` callback, a documented no-op in the MCP host where
|
|
45
|
+
`symbol_search` reads the index. The structured logger (`~/.pi-lens/
|
|
46
|
+
word-index.log`, shared `createNdjsonLogger` infra) now records, independent
|
|
47
|
+
of `dbg`: the full-rebuild-vs-incremental decision plus honest coverage
|
|
48
|
+
(`indexedFileCount`/`truncated` and `refreshed`/`dropped`/`skipped`/`reused`
|
|
49
|
+
counts) for both the session-start and cold-query (MCP) build paths; a
|
|
50
|
+
swallowed snapshot **persist failure** as `persist_failed` (a silent stale
|
|
51
|
+
index otherwise leaves no trace); and the safety refusal for a root at/above
|
|
52
|
+
`$HOME`. The full-build collector (`collectWordIndexDocs`) now also returns a
|
|
53
|
+
`skipped` count for files it enumerated but could not index (over the byte
|
|
54
|
+
cap / unreadable), so a partial index is never reported as complete. The
|
|
55
|
+
fragile string-parsing `dbg` adapter this replaces is removed.
|
|
56
|
+
|
|
57
|
+
- **Closed four observability gaps in recently-changed typos-config and
|
|
58
|
+
project-snapshot code** (refs #533) — none of these change behavior, only
|
|
59
|
+
what's now logged: (1) the typos LSP's inject-vs-step-aside decision
|
|
60
|
+
(#967) now logs a `typos_config_resolved` phase in `latency.log` /
|
|
61
|
+
`sessionstart.log` with `mode: "project_config" | "injected_default"` and
|
|
62
|
+
the resolved `configPath`, so which typos config is actually active is no
|
|
63
|
+
longer a guess; (2) a project-snapshot body persist that falls back to the
|
|
64
|
+
synchronous main-thread gzip because the persist worker died/was
|
|
65
|
+
unavailable now logs an explicit `project_snapshot_worker_fallback` phase
|
|
66
|
+
with the `reason` (previously only visible via a test-only variable, or
|
|
67
|
+
buried in an `offloaded:false` success line) — this is the +656MB-risk
|
|
68
|
+
degraded path from #950; (3) a corrupt/truncated gzipped snapshot body
|
|
69
|
+
(gunzip/JSON.parse failure) now logs `project_snapshot_body_corrupt`
|
|
70
|
+
before failing open to a rebuild, instead of being indistinguishable from
|
|
71
|
+
"no snapshot yet"; (4) dropping the in-process authoritative snapshot
|
|
72
|
+
entry for an oversized (>24MB) body now logs
|
|
73
|
+
`project_snapshot_authoritative_dropped_oversized` — low-priority, but no
|
|
74
|
+
longer silent.
|
|
75
|
+
|
|
76
|
+
- **Review-graph checkpoint discards and write failures are now observable**
|
|
77
|
+
(refs #936, #533) — a present resume checkpoint that's rejected now logs
|
|
78
|
+
`checkpoint_discarded` with a `reason` (`corrupt`, `version_mismatch`,
|
|
79
|
+
`not_in_progress`, `git_stamp_mismatch`, `ignored_ids_mismatch`,
|
|
80
|
+
`removed_file`, `all_stale`) instead of silently falling back to a full cold
|
|
81
|
+
rebuild, and a failed checkpoint write (worker error/death, promote failure,
|
|
82
|
+
sync-write failure) logs `checkpoint_write_failed` — so "why isn't my
|
|
83
|
+
checkpoint resuming / persisting?" is diagnosable from `review-graph.log`.
|
|
84
|
+
|
|
85
|
+
- **The review-graph full build is now resumable across sessions** (refs #936
|
|
86
|
+
limit 2) — a cold full build (walk + tree-sitter parse of every source file)
|
|
87
|
+
previously restarted from scratch every session, so on a large repo with
|
|
88
|
+
short-lived sessions it could never finish. The extraction loop now
|
|
89
|
+
periodically checkpoints the PRE-resolution graph plus the exact set of files
|
|
90
|
+
already folded into it (with content hashes) to a dedicated
|
|
91
|
+
`review-graph.checkpoint.json.gz`, and a later session resumes from it,
|
|
92
|
+
re-walking only files that changed/appeared since. The checkpoint lives in
|
|
93
|
+
its own file (never the authoritative `review-graph.json.gz`) and its
|
|
94
|
+
hydrated graph carries `persistCoverage.inProgress` on top of `partial`, so
|
|
95
|
+
no reader (`getCachedReviewGraph`, `loadPersistedGraph`) can ever serve or
|
|
96
|
+
launder a mid-build checkpoint as a complete graph (honesty doctrine, #533).
|
|
97
|
+
Resume equivalence to a cold build is guaranteed by `addFileToGraph`'s
|
|
98
|
+
per-file contribution being order-independent (all cross-file linking is
|
|
99
|
+
deferred to `resolveDeferredSymbolEdges`): content-changed processed files
|
|
100
|
+
are evicted and re-walked, orphaned placeholder nodes are pruned, and any
|
|
101
|
+
removed file, ignored-id-set change, version bump, or git-identity mismatch
|
|
102
|
+
fails open to a cold build rather than risking a wrong graph. Checkpoint
|
|
103
|
+
stride/interval are tunable via `PI_LENS_GRAPH_CHECKPOINT_EVERY_FILES` /
|
|
104
|
+
`PI_LENS_GRAPH_CHECKPOINT_MIN_INTERVAL_MS`.
|
|
105
|
+
|
|
106
|
+
- **Bundled default typos allowlist for projects without their own config**
|
|
107
|
+
(fixes #967) — the `typos` auxiliary LSP now injects a small pi-lens-shipped
|
|
108
|
+
`_typos.toml` (`rules/typos/_typos.toml`, `[default.extend-identifiers]`
|
|
109
|
+
only: `dito`, `unparseable`) via `initializationOptions.config` whenever a
|
|
110
|
+
project has no `typos.toml`/`_typos.toml`/`.typos.toml` of its own. A
|
|
111
|
+
project's own config still always wins outright: when one is found,
|
|
112
|
+
pi-lens injects nothing at all (rather than letting typos-lsp merge ours
|
|
113
|
+
on top and risk our defaults outranking the team's own allowlist on key
|
|
114
|
+
collisions).
|
|
115
|
+
|
|
116
|
+
- **Session-start latency is attributable end to end** (refs #948) — latency
|
|
117
|
+
telemetry now separates host boot from pi-lens evaluation and records quick
|
|
118
|
+
and full session-start totals, pre-handler/bootstrap work, runtime reset,
|
|
119
|
+
log cleanup, LSP reset, sequence/snapshot reads (including snapshot bytes),
|
|
120
|
+
and delayed warmup scan/profile/index phases.
|
|
121
|
+
|
|
122
|
+
- **Review-graph LSP fallback nodes** (refs #307) — when tree-sitter yields
|
|
123
|
+
zero declarations, the builder may use `documentSymbol` from an already-live,
|
|
124
|
+
already-open capable server. Nodes carry `provenance: "lsp"`, hierarchical
|
|
125
|
+
containment survives persistence (including flat native-TypeScript-7 results
|
|
126
|
+
reconstructed through `containerName`), productive tree-sitter files never
|
|
127
|
+
pay the request, and unavailable/failed fallback attempts degrade without
|
|
128
|
+
opening or spawning while remaining visible in `review-graph.log`.
|
|
129
|
+
|
|
130
|
+
- **Standalone out-of-band review-graph build CLI** (refs #924) — `npx pi-lens
|
|
131
|
+
build-graph [--cwd <dir>]` reuses the session builder and queued atomic
|
|
132
|
+
persist path for CI/cron, forces the debounced snapshot write before exit,
|
|
133
|
+
and prints file/node/edge/element counts, JSON bytes, and duration. Unsafe
|
|
134
|
+
roots, build errors/skips, persist failures, and persist-cap trips exit
|
|
135
|
+
non-zero with their reason instead of silently leaving no snapshot.
|
|
136
|
+
|
|
137
|
+
- **`/lens-perf` surfaces slow phases in-session** (closes #767) — the command
|
|
138
|
+
shows independent top-five p50 and p99 rankings with sample counts for both
|
|
139
|
+
the current process session and the machine-wide active `latency.log` window.
|
|
140
|
+
It flushes pending writes, streams a tail bounded by the log rotation
|
|
141
|
+
threshold (`PI_LENS_MAX_LOG_SIZE_MB`, 10MB by default), caps retained samples,
|
|
142
|
+
and reports malformed/truncated input instead of silently reading it as clean.
|
|
143
|
+
Session startup total and scan-context computation are now logged as phases so
|
|
144
|
+
the startup regressions that motivated the command are visible there too.
|
|
145
|
+
|
|
146
|
+
### Changed
|
|
147
|
+
|
|
148
|
+
- **Session-start perf: bound the change-log replay + defer log cleanup (closes #1019)** —
|
|
149
|
+
the interactive session-start path recomputed the project sequence by reading
|
|
150
|
+
the ENTIRE append-only change log and folding every line — and each fold does a
|
|
151
|
+
`normalizeMapKey`/`realpathSync.native()` syscall per historical entry, so the
|
|
152
|
+
`session_start_sequence_read` phase (measured ~94ms, ~47% of a 200ms warm start)
|
|
153
|
+
grew unbounded with total log length, not the working set. The project snapshot
|
|
154
|
+
now embeds the DERIVED sequence index (`projectSeq` + per-file `fileSeqByPath`)
|
|
155
|
+
as of its `seq` (`clients/project-snapshot.ts`), mirrored into the tiny
|
|
156
|
+
`project-snapshot.meta.json` sidecar so session-start can hydrate it WITHOUT
|
|
157
|
+
parsing the 40-112MB body (preserving the #947 skip-stale optimization).
|
|
158
|
+
`readLatestProjectSequence` (`clients/project-changes.ts`) gained an optional
|
|
159
|
+
base param: it hydrates that index (O(files-in-snapshot), keys already
|
|
160
|
+
normalized → no per-key `realpath`) and folds ONLY entries with
|
|
161
|
+
`seq > snapshot.seq` — O(changes-since-snapshot). The fold uses the SAME
|
|
162
|
+
`Math.max`/`normalizeMapKey` the full replay does, so the result is
|
|
163
|
+
byte-identical to a full replay (proven by an equivalence test suite covering
|
|
164
|
+
no-new-entries, new/existing files, deletes, gaps/out-of-order, and the empty
|
|
165
|
+
log) and order-independent. Correctness-first fallbacks to a full replay: a
|
|
166
|
+
legacy/missing meta with no embedded index, a version-mismatched meta, and a
|
|
167
|
+
snapshot whose `seq` is AHEAD of the log (truncation/rotation) — it never serves
|
|
168
|
+
a wrong seq. Wired into BOTH the quick/interactive and the full session-start
|
|
169
|
+
paths. Secondary: `log_cleanup` (~7ms) was moved off the synchronous critical
|
|
170
|
+
path into a deferred `setImmediate` (it still runs every session and notifies
|
|
171
|
+
async — nothing on the hot path consumed its result).
|
|
172
|
+
|
|
173
|
+
- **Config-consistency pass on the #166 flag registry (refs #166, #533)** — the
|
|
174
|
+
#883 registry core (scope split, precedence, negation) was audited consistent
|
|
175
|
+
and left untouched; the gaps were all in #533 malformed/unknown-input
|
|
176
|
+
signalling, asymmetric between the global and project loaders. (1) The
|
|
177
|
+
project loader (`.pi-lens.json`) now warns once on an unrecognized top-level
|
|
178
|
+
key — a typo like `maxProjectFile` or `lps` previously did nothing silently —
|
|
179
|
+
while tolerating the foreign namespaces the shared file legitimately carries
|
|
180
|
+
for the LSP loader (`servers`, `serverOverrides`, `disabledServers`,
|
|
181
|
+
`warmFiles`, plus `$schema`); a user-level-only lens key placed at project
|
|
182
|
+
scope (e.g. `lsp`, `tests`, `delta`) gets a distinct "not honored at project
|
|
183
|
+
scope" signal instead of being lumped in with typos. (2) The recognized-key
|
|
184
|
+
catalogs are single-sourced (#883): `GLOBAL_NON_FLAG_CONFIG_SECTIONS` and
|
|
185
|
+
`PROJECT_FOREIGN_CONFIG_NAMESPACES` are declared once beside `LENS_FLAGS`, and
|
|
186
|
+
both loaders derive flag sections from the registry, so adding a namespace is
|
|
187
|
+
a one-line edit and adding a flag needs none — replacing a drift-prone inline
|
|
188
|
+
literal set in the global loader. (3) Three global scalars
|
|
189
|
+
(`dispatch.runnerTimeoutFloorMs`, `widget.visible`, `format.mode`) that
|
|
190
|
+
silently coerced a present-but-malformed value to `undefined` now warn on
|
|
191
|
+
invalid input through the same path as `actionableWarnings.autoFix.maxFixes`,
|
|
192
|
+
while staying silent when the key is absent (no false warnings). (4) Removed
|
|
193
|
+
five dead global-only accessors that bypassed the precedence chain
|
|
194
|
+
(`getGlobalAutoformatEnabled`, `getGlobalAutofixEnabled`,
|
|
195
|
+
`getGlobalImmediateFormatDefault`, `getGlobalContextInjectionEnabled`,
|
|
196
|
+
`getGlobalTurnSummaryEnabled`) — no non-test callers existed. (5) Updated
|
|
197
|
+
`docs/globalconfig.md`, whose "unknown keys are ignored" claim was stale.
|
|
198
|
+
|
|
199
|
+
- **Normalized call-graph impact checks into structured `ProjectDiagnostic`s**
|
|
200
|
+
(refs #179) — the last remaining adapter gap in #179's normalization sweep.
|
|
201
|
+
`runtime-turn.ts`'s turn-end call-graph impact check (WillBreak/MayBreak
|
|
202
|
+
callers of symbols edited this turn, via `impact()`/`call-graph.ts`) was
|
|
203
|
+
advisory-text only, unlike the other 9 runners already normalized through
|
|
204
|
+
`runner-adapters/*.ts`. Added `callGraphImpactToProjectDiagnostics`
|
|
205
|
+
(`project-diagnostics/runner-adapters/call-graph-impact.ts`), a pure mapper
|
|
206
|
+
attributing each diagnostic to the CALLER's file (no line — `impact()`'s
|
|
207
|
+
`ImpactResult` never carries one, and the underlying call-graph symbol
|
|
208
|
+
table only has a placeholder `line: 1`, so inventing one would be a fake
|
|
209
|
+
location). Severity is deliberately capped below this codebase's
|
|
210
|
+
"blocking" hard-stop tier for BOTH WillBreak and MayBreak (#533 honesty):
|
|
211
|
+
`impact()` is a name-resolution heuristic with no type information, so a
|
|
212
|
+
resolved caller is never a confirmed break. Wired at turn-end (not
|
|
213
|
+
`fresh-fetch.ts`/`ANALYZER_IDS`) since impact analysis is inherently
|
|
214
|
+
per-turn-edit, not a whole-project scan — same reasoning that keeps
|
|
215
|
+
test-runner's own turn-end fire out of that registry. The existing
|
|
216
|
+
human-readable advisory string is kept as-is (separate surface, same
|
|
217
|
+
dual-surface pattern already used by knip's turn-end delta). The call-graph
|
|
218
|
+
block now runs BEFORE the single `writeProjectDiagnosticsDeltaReport`
|
|
219
|
+
serialization point (alongside knip's delta push) rather than after it —
|
|
220
|
+
previously it pushed into `projectDiagnosticsDelta`/`projectDiagnosticsSources`
|
|
221
|
+
past the one-shot write, so a call-graph-only turn persisted nothing and a
|
|
222
|
+
mixed turn dropped the call-graph entries, leaving `lens_diagnostics` (a
|
|
223
|
+
pure reader of the persisted report) unable to ever surface the findings
|
|
224
|
+
(#533). Covered by a new `handleTurnEnd`-level regression test asserting the
|
|
225
|
+
persisted report for both call-graph-only and mixed turns.
|
|
226
|
+
|
|
227
|
+
- **Parallelized the per-turn madge dependency check** (refs #766) — the
|
|
228
|
+
turn-end circular-dependency pass previously ran one `await checkFile()`
|
|
229
|
+
per import-changed file in a sequential `for…await` loop, serializing N
|
|
230
|
+
madge subprocess spawns. `DependencyChecker.checkFilesBatch()` now runs
|
|
231
|
+
those spawns concurrently (bounded to 6 in flight) via a new turn-end
|
|
232
|
+
entry point. `DependencyChecker` keeps its circular-dep findings
|
|
233
|
+
(`lastCircular`/`circularFiles`) as shared instance state that a single-file
|
|
234
|
+
check overwrites wholesale, so naively parallelizing risked one file's
|
|
235
|
+
spawn clobbering a sibling's write depending on subprocess completion
|
|
236
|
+
order. The fix keeps every spawn's result local until all have settled,
|
|
237
|
+
then folds them into the shared state in original file order — matching
|
|
238
|
+
the sequential loop's file-by-file overwrite exactly, just without waiting
|
|
239
|
+
for each subprocess before starting the next. Behavior-preserving: same
|
|
240
|
+
per-file results, same final circular-dep state, same `dbg` logs — only
|
|
241
|
+
wall-clock time changes.
|
|
242
|
+
|
|
243
|
+
- **Every runtime toggle is now settable from BOTH the CLI and
|
|
244
|
+
`~/.pi-lens/config.json`, driven by one declarative registry** (closes #166) —
|
|
245
|
+
the flag/config mapping used to live in two disconnected places (twelve
|
|
246
|
+
`pi.registerFlag` calls in `index.ts` and an if/else chain in
|
|
247
|
+
`resolvePiLensFlagWithSource`), which let coverage gaps open and persist.
|
|
248
|
+
Seven flags were registered on the CLI but fell straight through the
|
|
249
|
+
resolver, so config could never set them: `--no-lens`, `--no-lsp`,
|
|
250
|
+
`--no-tests`, `--no-delta`, `--lens-guard`, `--no-opengrep`, and
|
|
251
|
+
`--no-read-guard`. They now read `lens.enabled`, `lsp.enabled`,
|
|
252
|
+
`tests.enabled`, `delta.enabled`, `guard.enabled`, `opengrep.enabled`, and
|
|
253
|
+
`readGuard.enabled` (positive keys — a `--no-*` flag is the key set `false`).
|
|
254
|
+
The four actionable-warning toggles went the other way: they were
|
|
255
|
+
config-only and are now registered CLI flags too
|
|
256
|
+
(`--lens-actionable-warnings`, `--lens-actionable-warning-actions`,
|
|
257
|
+
`--lens-actionable-warning-autofix`, `--lens-actionable-warning-all`).
|
|
258
|
+
`clients/lens-flag-registry.ts` is the single source both surfaces read, so
|
|
259
|
+
they cannot diverge again; the wiring test derives its expected flag set from
|
|
260
|
+
it rather than restating it (the old hand-written list had already drifted,
|
|
261
|
+
missing `lens-turn-summary`).
|
|
262
|
+
|
|
263
|
+
- **`actionableWarnings.autoFix.maxFixes` is now actually read** (#166) —
|
|
264
|
+
documented in `globalconfig.md` since #792 but no loader ever parsed it, so
|
|
265
|
+
`agent_end` always used the hardcoded default of 5 no matter what the config
|
|
266
|
+
said. Same defect class as the seven flags above: a documented config key
|
|
267
|
+
with nothing wiring it to the code that consumes it. Accepts any
|
|
268
|
+
non-negative whole number; `0` keeps the warning report while applying
|
|
269
|
+
nothing.
|
|
270
|
+
|
|
271
|
+
- **Flag provenance gained an `env` tier and reports the global tier more
|
|
272
|
+
accurately** (#166) — `PI_LENS_NO_CONTEXT_INJECTION` moved out of a
|
|
273
|
+
hardcoded check in `index.ts` into the registry's `env` binding, so it
|
|
274
|
+
resolves through the same chain as everything else (env → cli →
|
|
275
|
+
nested-project → project → global → default) and reports `source: "env"`.
|
|
276
|
+
The global tier now reports `source: "global"` whenever the key is present,
|
|
277
|
+
not only when it happens to differ from the built-in default. Affects debug
|
|
278
|
+
and mutation-skip log lines only, no resolved values change.
|
|
279
|
+
|
|
280
|
+
- **Unknown top-level keys in `~/.pi-lens/config.json` now warn once instead of
|
|
281
|
+
being dropped silently** (#166, refs #533) — a typo like `lps` for `lsp`
|
|
282
|
+
previously did nothing with no signal. The recognized-key set is derived from
|
|
283
|
+
the flag registry (`LENS_FLAGS`) plus the non-flag sections (`ignore`,
|
|
284
|
+
`dispatch`, `actionableWarnings`, `widget`) and `$schema`, so it stays in sync
|
|
285
|
+
with the single source of truth; anything else logs one `[pi-lens] ignoring
|
|
286
|
+
invalid global config …: unknown key "…"` line and is ignored.
|
|
287
|
+
|
|
288
|
+
- **The review-graph resume checkpoint (#936) now offloads its gzip to the
|
|
289
|
+
shared persist worker** (refs #958, #883) — mid-build checkpoint writes
|
|
290
|
+
previously ran a synchronous `gzipSync` of the growing graph on the event
|
|
291
|
+
loop; they now stream the stringify+gzip through the same worker the
|
|
292
|
+
authoritative snapshot uses (via the newly-shared `writeGzipStageFile` core),
|
|
293
|
+
generation-gated so a slow write can't clobber a newer checkpoint or
|
|
294
|
+
resurrect one after the build completes and retires it, and falling back to a
|
|
295
|
+
synchronous write when the worker is unavailable. Best-effort as before — a
|
|
296
|
+
lost checkpoint only costs a cold rebuild.
|
|
297
|
+
|
|
298
|
+
- **Project snapshot body is now written gzipped by a worker thread** (refs
|
|
299
|
+
#958 item 2) — the snapshot body (40-112MB observed) is persisted as
|
|
300
|
+
`project-snapshot.json.gz`, with the `JSON.stringify` + gzip run on a worker
|
|
301
|
+
thread off the save path, mirroring the review graph's
|
|
302
|
+
`persist-worker.ts`/generation-gated-promotion pattern (gzip measured 5-10x
|
|
303
|
+
on top of the #957 compaction win; the review-graph's own measurement was
|
|
304
|
+
60MB → 1.4MB). A slow worker write for generation N is discarded rather than
|
|
305
|
+
promoted over a newer generation N+1 already on disk, and the loader still
|
|
306
|
+
reads the previous uncompressed `project-snapshot.json` for one compatibility
|
|
307
|
+
release so an upgrade never loses a snapshot. The save path deliberately does
|
|
308
|
+
NOT sync-gzip (the #950 review measured a naïve sync gzip regressing host
|
|
309
|
+
memory by +656MB); when the worker is unavailable/dies the pending body falls
|
|
310
|
+
back to a synchronous main-thread gzip write, surfaced via the logger rather
|
|
311
|
+
than silently presented as saved (#533). Read-your-writes across the async
|
|
312
|
+
promotion is preserved by an in-process authoritative "latest write" that
|
|
313
|
+
`loadProjectSnapshot` consults before disk, so the merge-write callers
|
|
314
|
+
(`saveRuntimeProjectSnapshot`, word index, reverse deps) never observe a
|
|
315
|
+
stale body in the promotion window.
|
|
316
|
+
|
|
317
|
+
- **Session warmup refreshes the word index incrementally** (refs #958) —
|
|
318
|
+
the persisted serializer now carries per-file mtimes. Startup still performs
|
|
319
|
+
the bounded source walk, but reuses unchanged postings, re-tokenizes only
|
|
320
|
+
stale/new files, and drops deleted files; legacy indexes, refresh failures,
|
|
321
|
+
and file-set churn above 30% fall back to a full rebuild. The current derived
|
|
322
|
+
file cap and `truncated` state are re-evaluated on every refresh, and
|
|
323
|
+
`warmup_word_index` telemetry records mode/refreshed/dropped/reused counts.
|
|
324
|
+
|
|
325
|
+
- **Project scans feed compact structural IR into review-graph builds** (refs
|
|
326
|
+
#939) — each fully completed scanner file publishes content-hash-bound
|
|
327
|
+
imports, reexports, function summaries, symbols, and references. A following
|
|
328
|
+
or overlapping graph build reuses only exact-hash, successful entries;
|
|
329
|
+
stale, failed, absent, and cold one-shot paths parse normally. The handoff
|
|
330
|
+
retains neither source content nor WASM trees, and cancelled scans expose
|
|
331
|
+
only files completed before cancellation.
|
|
332
|
+
|
|
333
|
+
- **Session-start performance: LSP pre-warm, snapshot meta-gate, compact cached snapshot** (refs #947) — three
|
|
334
|
+
measured startup wins: (1) the dominant-language LSP pre-warm was gated on
|
|
335
|
+
full startup mode that the first-session heuristic never allows (82 quick
|
|
336
|
+
vs 0 full starts in 31k dogfood log lines), so it now runs inside the
|
|
337
|
+
quick-mode +2s background warmup pass — once per process,
|
|
338
|
+
generation-guarded, honoring subagent light mode, warm-attach, the
|
|
339
|
+
`no-lsp` flag, and the `canWarmCaches` guard, with a `warmup_lsp_prewarm`
|
|
340
|
+
phase record; (2) session start no longer sync-parses the whole
|
|
341
|
+
`project-snapshot.json` body (110-130ms at 40MB, ~0.5s at 112MB) before
|
|
342
|
+
checking freshness — the tiny `project-snapshot.meta.json` sidecar is read
|
|
343
|
+
first and a stale seq/version skips the body parse entirely
|
|
344
|
+
(`session_start_snapshot_load` records `skippedStale: true`), with
|
|
345
|
+
missing-meta installs falling back to the legacy parse; (3) the snapshot
|
|
346
|
+
is serialized compactly (~30% smaller) and its parsed body is cached
|
|
347
|
+
in-process per (path, mtime), so `saveRuntimeProjectSnapshot` and the
|
|
348
|
+
session-start/word-index/scan-context consumers no longer re-parse a file
|
|
349
|
+
pi-lens itself wrote seconds earlier.
|
|
350
|
+
|
|
351
|
+
- **Warm LSP names enrich tree-sitter read expansion** (refs #158) — partial
|
|
352
|
+
read expansion keeps tree-sitter's line boundaries authoritative, but an
|
|
353
|
+
already-open document with an already-active LSP can now replace the display
|
|
354
|
+
name/kind from `documentSymbol` (including `Class.method` ancestry) within a
|
|
355
|
+
150 ms best-effort ceiling. Cold, closed, unsupported, timed-out, or failed
|
|
356
|
+
servers retain the tree-sitter identity, and `ts_range_expanded` records
|
|
357
|
+
whether enrichment succeeded.
|
|
358
|
+
|
|
359
|
+
- **Installer subprocesses are lifetime-coupled** (refs #945) — npm, pip, gem,
|
|
360
|
+
and archive extraction now use the shared safe-spawn path, await full Windows
|
|
361
|
+
process-tree termination on timeout, and synchronously clean registered
|
|
362
|
+
installer children during parent exit/signals.
|
|
363
|
+
|
|
364
|
+
- **Managed tool installs are cross-process serialized** (refs #945) — a
|
|
365
|
+
dependency-free atomic lock protects the shared tools tree, verifies owners
|
|
366
|
+
before stale recovery, bounds lock waits with an honest error, and rechecks
|
|
367
|
+
discovery after acquisition to avoid duplicate package-manager runs.
|
|
368
|
+
|
|
369
|
+
- **Ordinary tests never install managed tools** (refs #945) — Vitest sets
|
|
370
|
+
`PI_LENS_DISABLE_TOOL_INSTALL=1`, its prewarm step creates a local synthetic
|
|
371
|
+
oxlint probe-cache entry without networking, and one-shot analysis explicitly
|
|
372
|
+
awaits probe-cache persistence before exit.
|
|
373
|
+
|
|
374
|
+
- **Installer orphan/locking regressions are process-tested** (refs #945) —
|
|
375
|
+
fake package-manager coverage verifies Windows timeout tree-kill, exactly one
|
|
376
|
+
install across concurrent processes, explicit install-disable refusal, and
|
|
377
|
+
Vitest's default no-install environment.
|
|
378
|
+
|
|
379
|
+
- **Downgrade TypeScript `unsafe-regex` to advisory and suppress escaped-before-
|
|
380
|
+
assignment false positives** (refs #932) — the coarse dynamic `RegExp`
|
|
381
|
+
heuristic no longer blocks edits and recognizes escape/replace calls in a
|
|
382
|
+
same-file identifier initializer; structural ReDoS detection remains with
|
|
383
|
+
the `redos-nested-quantifier` ast-grep rule.
|
|
384
|
+
|
|
385
|
+
- **Review-graph persistence no longer serializes or compresses on the event
|
|
386
|
+
loop** (refs #939) — debounced snapshots are materialized in one lazy,
|
|
387
|
+
unref'd worker and streamed through gzip into the new canonical
|
|
388
|
+
`review-graph.json.gz` cache. The main thread promotes only the current
|
|
389
|
+
generation's atomic staged file, so the synchronous CLI/exit flush can
|
|
390
|
+
supersede an in-flight worker without a stale overwrite. Loads retain one
|
|
391
|
+
release of fallback support for legacy uncompressed `review-graph.json`
|
|
392
|
+
snapshots; worker failures are logged and degrade to a synchronous persist.
|
|
393
|
+
Persist telemetry now records element count, raw/gzip bytes, serialization
|
|
394
|
+
and write time, and whether the work was offloaded.
|
|
395
|
+
|
|
396
|
+
- **Raise and make the review-graph persist ceiling useful** (refs #936) — the
|
|
397
|
+
default `GRAPH_PERSIST_MAX_ELEMENTS` cap is now 500,000 (still overrideable
|
|
398
|
+
through `PI_LENS_GRAPH_PERSIST_MAX_ELEMENTS`), matching measured startup
|
|
399
|
+
load/reindex costs and allowing the ~208,000-element #919 repository to
|
|
400
|
+
persist completely. Above the cap, persistence retains whole-file node groups
|
|
401
|
+
in existing reverse-dependency-centrality order plus induced edges that fit,
|
|
402
|
+
instead of dropping the snapshot. Exact total-versus-persisted node/edge
|
|
403
|
+
counts are stored and surfaced; read-only queries may use the partial graph,
|
|
404
|
+
while incremental builds reject it as a complete base.
|
|
405
|
+
|
|
406
|
+
- **Logger hot paths now coalesce queued lines and rotate during long sessions** (refs #935) — contiguous NDJSON entries drain through one append up to each truncate boundary while retaining peek-then-remove exit safety and one-write cross-process atomicity. `sessionstart.log` now uses one shared asynchronous writer for ordinary diagnostics (with the crash-adjacent LSP launch write intentionally synchronous), and latency/cascade/tree-sitter/bus-event logs enforce the existing 10 MB cap in process.
|
|
407
|
+
|
|
408
|
+
- **Incremental review-graph updates avoid redundant whole-graph copies and
|
|
409
|
+
index rebuilds** (refs #939) — file re-extraction now rebuilds derived indexes
|
|
410
|
+
once, immutable edges are array-copied without cloning every edge object, the
|
|
411
|
+
updated graph itself becomes the workspace snapshot, and debounced persistence
|
|
412
|
+
defers its O(graph) array materialization until the quiet-window flush.
|
|
413
|
+
|
|
414
|
+
- **Reverse-dependency indexes update at import-edge granularity** (refs #939)
|
|
415
|
+
instead of rebuilding from every graph edge after a one-file edit. Body-only
|
|
416
|
+
edits reuse the cached index without rewriting the project snapshot; import
|
|
417
|
+
changes patch only the touched `imports` and `importedBy` buckets.
|
|
418
|
+
|
|
419
|
+
- **Review-graph file-cap degradation is now explicit and count-honest** (refs
|
|
420
|
+
#921) — `project_report` says a capped project has “more than N files” instead
|
|
421
|
+
of presenting the cap+1 early-exit sentinel as an exact count. `module_report`
|
|
422
|
+
now marks graph-backed `usedBy`, blast-radius provenance, and
|
|
423
|
+
`semantic.source` as `unavailable:file-cap` and emits an actionable warning
|
|
424
|
+
with the cap plus both `.pi-lens.json#maxProjectFiles` and
|
|
425
|
+
`PI_LENS_REVIEW_GRAPH_MAX_FILES` controls, keeping disabled data distinct from
|
|
426
|
+
a genuinely empty/cold graph.
|
|
427
|
+
|
|
428
|
+
- **Project scans release every scan-local fact after each file** (refs #886,
|
|
429
|
+
#939) instead of retaining source content, imports, summaries, and other
|
|
430
|
+
derived per-file facts until the scan ends. The live dispatch store remains
|
|
431
|
+
untouched; only the scanner-owned store is cleared.
|
|
432
|
+
|
|
433
|
+
- Repair eight non-compiling Java, C++, CSS and PHP tree-sitter rules (refs #884).
|
|
434
|
+
|
|
435
|
+
- Repair four non-compiling Go, Rust, and Kotlin tree-sitter rules (refs #884).
|
|
436
|
+
|
|
437
|
+
- **Project diagnostics now use one file-major scan pass** (refs #896) —
|
|
438
|
+
tree-sitter rules, fact rules, and bundled ast-grep share each eligible
|
|
439
|
+
file's content read while retaining their individual extension/size gates,
|
|
440
|
+
diagnostic ordering, cancellation behavior, and latency telemetry. Full
|
|
441
|
+
review-graph builds likewise hash the bytes already read for extraction
|
|
442
|
+
instead of rereading every file after the graph is built.
|
|
443
|
+
|
|
444
|
+
- **A project scan runs its rule set in one tree walk, not one walk per rule**
|
|
445
|
+
(refs #675) — `runQueriesOnFile` compiles a language's rules into a single
|
|
446
|
+
multi-pattern query and maps matches back to the owning rule, keeping per-rule
|
|
447
|
+
metavars, predicates, post-filters, caps and ordering. Measured at 3.3× on
|
|
448
|
+
identical matches; the scan's tree-sitter phase went **14.4s → 3.6s** and a
|
|
449
|
+
full `mode=full` scan of pi-lens **24.4s → 12.2s**.
|
|
450
|
+
|
|
451
|
+
- **Project scans parse each file once** (refs #675) — the tree-sitter rules and
|
|
452
|
+
the fact rules both tree-sitter-parse the same files, and running them as two
|
|
453
|
+
whole-project sweeps meant the second one re-parsed every file the first had
|
|
454
|
+
already parsed and evicted (357 of 357 first touches missed on capacity in a
|
|
455
|
+
500-file `mode=full` scan). They now share one file-major pass: **722 parses →
|
|
456
|
+
367, 892ms → 437ms of parse time, zero capacity misses**, same diagnostics in
|
|
457
|
+
the same order. The 50-entry cache is unchanged and was never the problem —
|
|
458
|
+
within a phase it already ran at a 96.8% hit rate, any capacity short of the
|
|
459
|
+
full working set saves nothing, and one big enough to hold it would cost
|
|
460
|
+
150-300MB of resident WASM trees.
|
|
461
|
+
|
|
462
|
+
- **Tree-sitter cache pressure is measurable** (refs #675): every production
|
|
463
|
+
consumer, including partial-read expansion, uses the shared process-wide
|
|
464
|
+
client, and scope-isolated cache counters plus parser timing are emitted for
|
|
465
|
+
project scans and full review-graph builds.
|
|
466
|
+
|
|
467
|
+
- **Dead incremental-parse cache API removed.** `TreeCache.incrementalUpdate`,
|
|
468
|
+
`calculateEdit`, and `invalidate` had no callers; the header's "10-100×
|
|
469
|
+
speedup on edits" was advertising a path nothing ever took.
|
|
470
|
+
|
|
471
|
+
- **Faster LSP teardown and tool probes** (refs #448) — `killProcessTree`
|
|
472
|
+
resolves on the child's exit event instead of sleeping the full 1.5s
|
|
473
|
+
SIGTERM→SIGKILL escalation window (saves ~1.5s per graceful client
|
|
474
|
+
shutdown); tool availability checks skip `--version` spawns for binaries
|
|
475
|
+
that aren't on disk and no longer re-verify a probe-cache-validated managed
|
|
476
|
+
install on every analyze.
|
|
477
|
+
|
|
478
|
+
### Fixed
|
|
479
|
+
|
|
480
|
+
- **Read-before-edit guard now survives a session resume (#1041)** — the
|
|
481
|
+
guard's read-set was in-memory only, so a `pi --session <id>` resume (which
|
|
482
|
+
resets the runtime to a fresh empty guard) falsely `zero_read`-blocked the
|
|
483
|
+
first edit of any file read in the prior session. The read-set now rides the
|
|
484
|
+
same #190 `PersistedSessionState` save/load path that widget diagnostics use:
|
|
485
|
+
`ReadGuard.exportState()`/`importState()` persist and rehydrate `reads`,
|
|
486
|
+
reconciling each read against current disk (line-hash verified) so a resume
|
|
487
|
+
drops any read whose file changed or vanished — a rehydrated read never masks
|
|
488
|
+
a real staleness. Forked sessions adopt the parent's read-set via the same
|
|
489
|
+
in-memory hand-off as widget state. Backward-compatible: the field is optional
|
|
490
|
+
and pre-#1041 persisted sessions load cleanly as "no prior reads".
|
|
491
|
+
|
|
492
|
+
- **`turn_summary_emit` quiet-window task no longer fails on a stale pi ctx (the #483 quiet window's most frequent live-dogfood error — 55×)** —
|
|
493
|
+
the `agent_settled` quiet-window task `turn_summary_emit` reads the
|
|
494
|
+
`lens-turn-summary` flag through `pi.getFlag()` (via `getLensFlag`). The task
|
|
495
|
+
holder's `pi` is refreshed on every activation, but the quiet window is fired
|
|
496
|
+
fire-and-forget from `agent_settled`, so an interim session
|
|
497
|
+
replacement/reload (`ctx.newSession/fork/switchSession/reload` — common in the
|
|
498
|
+
in-process subagent flow #473) invalidates that captured `pi`; the very next
|
|
499
|
+
`pi.getFlag()` then throws the SDK's stale-ctx guard. That throw sat OUTSIDE
|
|
500
|
+
the existing `sendMessage` try/catch, so it escaped the task and the scheduler
|
|
501
|
+
logged `quiet_window: task "turn_summary_emit" failed: … stale after session
|
|
502
|
+
replacement or reload` — 55 times across two weeks of dogfooding, the single
|
|
503
|
+
most frequent error in `~/.pi-lens/sessionstart.log`, and it fired even with
|
|
504
|
+
the feature OFF (the flag READ throws before the flag value is known). Fix:
|
|
505
|
+
the task now recognises the stale-ctx throw (new `isStaleExtensionCtxError`
|
|
506
|
+
message matcher) at both pi touch points (flag read + emit) and degrades to a
|
|
507
|
+
logged no-op — the session that run's summary belonged to is gone, so there is
|
|
508
|
+
nothing to emit into — while a genuine error still propagates to the scheduler
|
|
509
|
+
(recorded `ok:false`). User impact was minimal (a lost summary for the exact
|
|
510
|
+
settle that raced a session swap, plus log noise), not silent feature
|
|
511
|
+
degradation: in steady state the task runs `ok:true`. Separately, the
|
|
512
|
+
quiet-window runner now logs the failing task's **stack** (not just `${err}`'s
|
|
513
|
+
message) so the next failure is diagnosable from the log alone; it remains
|
|
514
|
+
strictly non-fatal (per-task try/catch, `runQuietWindow` never rethrows, and
|
|
515
|
+
`agent_settled` invokes it fire-and-forget — a failing task can never break the
|
|
516
|
+
turn). Regression test drives the registered task against a `pi` whose
|
|
517
|
+
`getFlag` throws the stale-ctx error and asserts a no-op (no throw, no emit);
|
|
518
|
+
it fails pre-fix.
|
|
519
|
+
|
|
520
|
+
- **Word index no longer serves stale postings after a case/separator-divergent edit (refs #1025, closes #1025 item #2)** —
|
|
521
|
+
the word index's `docLengths`/`forward`/`fileMtimes` maps are keyed on file
|
|
522
|
+
paths, but the full/incremental BUILD keyed on the on-disk casing the
|
|
523
|
+
`readdirSync` walk reports while the per-edit UPDATE seams
|
|
524
|
+
(`clients/dispatch/integration.ts`, `clients/mcp/analyze.ts`) key via
|
|
525
|
+
`path.resolve()` on the raw tool-input path. When those forms differed only by
|
|
526
|
+
separator (`\` vs `/`, always) or case (on a case-insensitive FS), the plain-`Map`
|
|
527
|
+
keying made `forward.has(doc.path)` MISS, so the stale build-form entry was never
|
|
528
|
+
removed — a duplicate doc entry with stale postings served until a full rebuild,
|
|
529
|
+
silently degrading `symbol_search` relevance (the path-key divergence class,
|
|
530
|
+
#1020/#210). Fix: a new typed **`PathKeyedMap<V>`** primitive
|
|
531
|
+
(`clients/path-keyed-map.ts`) folds every key through a caller-supplied
|
|
532
|
+
normalizer INTERNALLY, so keying a raw path is structurally impossible; the word
|
|
533
|
+
index now routes all three path maps through it using the shared, exported
|
|
534
|
+
`wordIndexKey` (= `normalizeEphemeralMapKey` — cheap slash-fold + win32-lowercase,
|
|
535
|
+
NO `realpathSync`, so the hot BM25 path adds no filesystem I/O), and the
|
|
536
|
+
token-keyed `postings` cleanup compares `WordHit.file` through the same
|
|
537
|
+
normalizer, and the incremental-refresh set-difference
|
|
538
|
+
(`refreshWordIndexIncrementally`) now computes its `current`/`oldSet` keys in
|
|
539
|
+
that same normalized space (previously raw-keyed — a file whose stored display
|
|
540
|
+
path became the edit form would have been double-counted as churn and
|
|
541
|
+
dropped-then-re-added, defeating the incremental path and opening a
|
|
542
|
+
drop-before-readd regression window). `PathKeyedMap` stores each value's
|
|
543
|
+
ORIGINAL display path, so `symbol_search`/rendering surfaces still see the
|
|
544
|
+
on-disk form, not the folded key. Build-form, edit-form, and refresh keys now
|
|
545
|
+
converge on ONE entry regardless of casing/separator. The runtime-coordinator suspect maps (#1025 item #4) are left
|
|
546
|
+
for a follow-up (unproven trigger, and their correct normalizer is the
|
|
547
|
+
realpath-based `normalizeMapKey` matching the file's own `_fileSeq` convention —
|
|
548
|
+
a larger, higher-risk change across the concurrent-session drain logic).
|
|
549
|
+
|
|
550
|
+
- **Cascade honesty: degraded/uncomputed impact no longer renders as clean (refs #1023, closes #1023)** —
|
|
551
|
+
the cascade impact subsystem previously emitted an all-clear that was
|
|
552
|
+
indistinguishable from "genuinely nothing impacted" whenever it could NOT
|
|
553
|
+
compute impact: the review graph was size-skipped (`too_many_files` over
|
|
554
|
+
`PI_LENS_REVIEW_GRAPH_MAX_FILES`) or root-skipped (`unsafe_root`), the changed
|
|
555
|
+
file had no graph node, or the deferred compute threw — a silent under-report
|
|
556
|
+
exactly where downstream breakage is most likely (large/over-cap monorepos)
|
|
557
|
+
(#533). Impact results now carry a first-class `indeterminate` marker
|
|
558
|
+
(`clients/review-graph/types.ts`) set ONLY on a degraded/cold/errored/
|
|
559
|
+
missing-node compute; the already-known degraded state is threaded from
|
|
560
|
+
`getLastGraphBuildInfo().mode`/`skipReason` at the compute call site (no
|
|
561
|
+
re-derivation), a `missing_node` marker comes straight from
|
|
562
|
+
`computeImpactCascade`, and a thrown compute is tagged in the pipeline catch. A
|
|
563
|
+
new `indeterminate` `CascadeSkipReason` flows to the EXISTING turn-end seam
|
|
564
|
+
(`clients/runtime-turn.ts`), which renders a short honest note in the ADVISORY
|
|
565
|
+
tier ("Cascade could not compute downstream impact … a clean cascade result
|
|
566
|
+
does not cover them") instead of silence. The advisory tier (not the blocker
|
|
567
|
+
tier) is deliberate: an over-cap monorepo skips the graph on every edit, so a
|
|
568
|
+
blocker would fire hard and never clear turn state every turn (over-escalation,
|
|
569
|
+
the mirror of the silent-all-clear bug). A HEALTHY graph with a genuinely empty
|
|
570
|
+
dependent set stays silent (keyed strictly off the degraded mode/marker, never
|
|
571
|
+
off `neighbors.length === 0` alone — no crying wolf). Companion: the Tier-3 quiet-window reconcile
|
|
572
|
+
(`clients/lsp/cascade-tier.ts`) now RE-INJECTS a `resolved-found` cold-neighbor
|
|
573
|
+
error through the same neighbor→turn-end formatting via an `onResolvedFound`
|
|
574
|
+
callback wired in `index.ts`, instead of leaving it logs-only. The
|
|
575
|
+
neighbor-touch/freshness pipeline (neighborTouchCache, recentlyCleanNeighborCache,
|
|
576
|
+
the 40-neighbor budget, tier-aware touch model, deferred cascade flow) is
|
|
577
|
+
untouched.
|
|
578
|
+
|
|
579
|
+
- **Disposition marks under a nested monorepo language root are no longer silently dropped (refs #1030, closes #1030; pairs with #1024)** —
|
|
580
|
+
a `false-positive`/`flagged`/`defer` mark recorded via `lens_diagnostic_mark`
|
|
581
|
+
was invisible to the per-edit dispatch filter for every file living under a
|
|
582
|
+
nested language-root marker (`packages/app/tsconfig.json`, a nested
|
|
583
|
+
`package.json`/`pyproject.toml`/`go.mod`/etc.) in a monorepo. The mark tool
|
|
584
|
+
writes dispositions keyed on the **project root** (`runtime.projectRoot`), but
|
|
585
|
+
`dispatcher.ts`'s `applyDispositions` read keyed on `ctx.cwd`, which
|
|
586
|
+
`createDispatchContext` resolves to the nearest **language root** via
|
|
587
|
+
`resolveLanguageRootForFile` (a dispatch-internal tool/config-resolution
|
|
588
|
+
detail). Because both the anchor AND the persisted store location
|
|
589
|
+
(`getProjectDataDir`, keyed on `cwd`) derive from that base, write and read
|
|
590
|
+
computed different anchors AND opened entirely different
|
|
591
|
+
`diagnostic-dispositions.json` files — a total, deterministic failure (not
|
|
592
|
+
probabilistic) for every file under a nested marker. The tool returned success
|
|
593
|
+
while dispatch never saw the mark (a #533 honesty failure); `suppress` survived
|
|
594
|
+
only via its independent inline `pi-lens-ignore` comment. Fixed by reading the
|
|
595
|
+
disposition layer from the project root (`ctx.projectRoot`, already computed in
|
|
596
|
+
`createDispatchContext`), matching the write side — anchor base and store base
|
|
597
|
+
now agree on both sides. `resolveLanguageRootForFile` is untouched and still
|
|
598
|
+
drives actual dispatch/tool-root resolution. Orthogonal to #1024 (which fixes
|
|
599
|
+
the path *form* of the anchor); the two compose for a monorepo-stable anchor.
|
|
600
|
+
No new dependencies.
|
|
601
|
+
|
|
602
|
+
- **Disposition anchor path-form stability (refs #1024, closes #1024)** — the
|
|
603
|
+
`dd:`/`ddw:` disposition anchors (clients/diagnostic-dispositions.ts) are now
|
|
604
|
+
derived from a single canonical path form, closing a #533-adjacent
|
|
605
|
+
dropped-signal bug (sibling of #210/#1020). `relativeFile` — the chokepoint
|
|
606
|
+
both `computeStrictAnchor` and `computeWeakAnchor` route through — computed
|
|
607
|
+
`path.relative` on whichever raw path form the caller happened to hold: the
|
|
608
|
+
mark tool (tools/lens-diagnostic-mark.ts) passes a RAW `cwd` /
|
|
609
|
+
`path.resolve(cwd, arg)`, while the dispatch read side
|
|
610
|
+
(clients/dispatch/dispatcher.ts `createDispatchContext`) passes
|
|
611
|
+
`normalizeMapKey`-canonicalized (realpath'd) cwd/filePath. When the two forms
|
|
612
|
+
diverged (Windows drive/segment case, symlink/realpath), the agent's own
|
|
613
|
+
`false-positive`/`flagged` mark anchored under one id and the later
|
|
614
|
+
`applyDispositions` lookup under another, so the mark was invisible and the
|
|
615
|
+
"resolved" diagnostic kept re-firing / re-blocking every turn. Fix routes
|
|
616
|
+
BOTH `cwd` and `filePath` through `normalizeMapKey` inside `relativeFile`
|
|
617
|
+
(the same normalizer the read side already relies on — the #210 "every guard
|
|
618
|
+
map keys through `normalizeFilePath`" invariant), so write and read derive
|
|
619
|
+
identical anchors regardless of the form the caller passed. Anchor semantics
|
|
620
|
+
are unchanged: strict (`dd:`, line-hash) vs weak (`ddw:`) split, the prefixes,
|
|
621
|
+
and the `..`-escape fallback all behave exactly as before — only the path
|
|
622
|
+
component is now form-stable. Existing on-disk marks written under the old raw
|
|
623
|
+
form re-anchor to the canonical id; a stale orphan is re-derivable and simply
|
|
624
|
+
reappears once for the agent to re-mark (identical to today's pre-fix
|
|
625
|
+
behavior), so no read-time legacy fallback is needed.
|
|
626
|
+
|
|
627
|
+
- **`lens_diagnostics mode=all` no longer replays a resolved blocker (refs #1020, closes #1020)** —
|
|
628
|
+
the widget-state `files` map was keyed by the raw, non-normalized path string,
|
|
629
|
+
so the SAME file could land under two different key forms in one session:
|
|
630
|
+
the forward-slash form (`C:/…/x.ts`) the LSP client + cascade fold produce via
|
|
631
|
+
`normalizeFilePath`, and the backslash form (`C:\…\x.ts`) that `mode=full`'s
|
|
632
|
+
clean reconcile (`result.filePath`) and `path.resolve`/event inputs produce on
|
|
633
|
+
Windows. A stale blocking entry and the fresh clean entry then coexisted as two
|
|
634
|
+
separate map entries. `mode=full` rendered clean because its merge re-keys every
|
|
635
|
+
summary through `path.resolve`, but `mode=all`'s `formatAllMode` reads
|
|
636
|
+
`getFileDiagnosticSummaries()` verbatim with no dedup, so it saw both and
|
|
637
|
+
rendered the stale entry's `blocking:1` as a 🔴 — a resolved state that replayed
|
|
638
|
+
as still-broken on every `mode=all` (worst on Windows + resumed sessions), and a
|
|
639
|
+
#533 honesty failure since the tool prompt tells agents to use `mode=all` to
|
|
640
|
+
verify no blockers remain. Fixed at source: every write/read seam on the `files`
|
|
641
|
+
map (and the `diagnosticsWriteGuard`) now folds its key through one normalizer,
|
|
642
|
+
`normalizeEphemeralMapKey` (slash-fold + win32-lowercase, no filesystem I/O —
|
|
643
|
+
chosen over the `realpathSync`-backed `normalizeMapKey` because this is a hot
|
|
644
|
+
write path), and `importWidgetState` folds persisted keys on rehydrate so a
|
|
645
|
+
`/`-key snapshot and a fresh `\`-key write collapse across a resumed session.
|
|
646
|
+
The human-readable display path on each record is preserved verbatim (rendering
|
|
647
|
+
and path-relative math unchanged). No new dependencies.
|
|
648
|
+
|
|
649
|
+
- **Cache-friendly ephemeral context injection (refs #1016, closes #1016)** — the
|
|
650
|
+
`context` event handler in `index.ts` now splices pi-lens's ephemeral turn-end
|
|
651
|
+
findings in **immediately before the final message** instead of prepending them
|
|
652
|
+
at message index 0. Prepending flipped `messages[0]` every turn, which
|
|
653
|
+
invalidated the entire prompt-cache prefix on every prefix-caching provider
|
|
654
|
+
(Anthropic, Bedrock, and OpenAI — all key the cache on the exact token prefix).
|
|
655
|
+
Inserting before the last message keeps `messages[0]` (the real first user turn)
|
|
656
|
+
byte-stable so the prior conversation stays cached, while still keeping the real
|
|
657
|
+
user prompt as the trailing message — preserving the trailing-`user` cache
|
|
658
|
+
breakpoint and the historical `fe0ed5da` guarantee that input is never empty (the
|
|
659
|
+
existing transcript is always preserved, never dropped; an empty transcript falls
|
|
660
|
+
back to the prior prepend-only behavior). Because the `context` event fires
|
|
661
|
+
before **every** provider call (not just at turn boundaries), a trailing-role
|
|
662
|
+
guard (`isPlainUserPrompt`) only splices before the last message when it is a
|
|
663
|
+
plain user prompt; when the tail is a mid-loop `tool_result` (or assistant/tool
|
|
664
|
+
message) the findings are **appended** after the whole transcript instead — a
|
|
665
|
+
pure append that both preserves the `tool_use`↔`tool_result` adjacency all three
|
|
666
|
+
providers require (splicing between them is an HTTP 400) and keeps the entire
|
|
667
|
+
prior transcript as an untouched cache prefix. Changes shipped `dist` behavior
|
|
668
|
+
(the handler lives in the bundled dist path).
|
|
669
|
+
|
|
670
|
+
- **Fixed: `ts-ssrf` no longer flags fixed/const endpoint URLs built with
|
|
671
|
+
`new URL(...)` as SSRF sinks, while still catching tainted URLs** (closes
|
|
672
|
+
#1000, refs #533, #963) — a project-wide pi-free scan flagged fixed outbound
|
|
673
|
+
OAuth/profile endpoints as SSRF: `fetch(authUrl.toString())` where
|
|
674
|
+
`authUrl = new URL("auth/authorize", \`${BASE_URL_CLINE}/\`)` with a fixed
|
|
675
|
+
(module-const or imported) base. The `ts_ssrf_sink` post-filter already
|
|
676
|
+
exempted a `fetch()` of a file-local `const` literal string (#963) but had no
|
|
677
|
+
awareness of the `new URL(literalPath, fixedBase)` shape, so
|
|
678
|
+
`url.toString()`/`url.href` fell through to the broad taint heuristic. Extended
|
|
679
|
+
the post-filter to also exempt `fetch(u.toString())`/`fetch(u.href)` when `u`
|
|
680
|
+
resolves (same file) to a clean `const u = new URL(<literalPath>, <fixedBase>)`
|
|
681
|
+
— literal path plus a base that is a literal, a file-local literal `const`, or
|
|
682
|
+
an import binding (the `URL` ctor may be aliased, e.g. `NodeURL`). Query params
|
|
683
|
+
added later via `searchParams.set(...)` don't control origin/path and never
|
|
684
|
+
taint the destination. Still fires (regression fixtures added) when the base
|
|
685
|
+
or path comes from a function parameter, `process.env`/config (including a
|
|
686
|
+
const initialized from `process.env`), request data, or a parsed
|
|
687
|
+
redirect/`location` value — a missed SSRF stays far worse than one FP.
|
|
688
|
+
Adversarial review then closed two bypasses in that exemption: (1) a
|
|
689
|
+
post-construction origin mutation (`u.host = req.x` / `u.href = …` /
|
|
690
|
+
`u["host"] = …`, incl. augmented assignments) now re-taints the receiver and
|
|
691
|
+
fails closed — only query-only writes (`u.search`/`u.searchParams`) stay
|
|
692
|
+
exempt; (2) a request-tainted function *parameter* base is no longer exempted
|
|
693
|
+
just because an unrelated same-named module-level `const` literal exists
|
|
694
|
+
(scope-aware shadow check; imported bases stay trusted).
|
|
695
|
+
|
|
696
|
+
- **Fixed: `no-javascript-url`/`no-javascript-url-js` no longer flag defensive
|
|
697
|
+
`javascript:`-URL filters** (refs #533) — a dogfood run flagged code that was
|
|
698
|
+
*rejecting* `javascript:` links (e.g. `url.startsWith("javascript:")` inside a
|
|
699
|
+
guard) as if it were introducing one. The rule matched the `javascript:`
|
|
700
|
+
string literal alone with no way to tell "used as a sink" from "used to
|
|
701
|
+
detect/block it". Narrowed both rules to also exclude the literal when it's
|
|
702
|
+
the needle in `.startsWith`/`.endsWith`/`.includes`/`.indexOf`/`.search`/
|
|
703
|
+
`.match`, or one side of an `===`/`!==`/`==`/`!=` comparison — deliberately
|
|
704
|
+
NOT `.replace`/`.replaceAll`, since the literal there could be the malicious
|
|
705
|
+
replacement argument rather than the defensive search argument, and
|
|
706
|
+
excluding the whole call would hide that true positive. Both new exclusions
|
|
707
|
+
deliberately avoid `stopBy: end` (unlike the pre-existing test-literal
|
|
708
|
+
exclusion, which needs it) so the literal must be a DIRECT argument/operand
|
|
709
|
+
of the defensive call/comparison, not merely somewhere in its subtree — an
|
|
710
|
+
adversarial review caught that `stopBy: end` let a real sink smuggled
|
|
711
|
+
inside the call/comparison (e.g. `candidates.includes(link.href =
|
|
712
|
+
"javascript:...")`, a comma-operator sink, or `(a.href = "javascript:...")
|
|
713
|
+
=== expected`) go unflagged. Added valid fixtures reproducing the filter FP
|
|
714
|
+
and invalid fixtures for the replace-based sink and all three smuggled-sink
|
|
715
|
+
bypasses, confirmed against the real ast-grep 0.45.0 binary.
|
|
716
|
+
|
|
717
|
+
- **Contained spawn/callback failures that could crash the host (pidusage bug
|
|
718
|
+
class)** (refs #533) — a best-effort telemetry sampler recently killed a live
|
|
719
|
+
pi host with `Error: spawn UNKNOWN` (uncaughtException) because a bundled
|
|
720
|
+
dep's `child_process.spawn(...)` threw SYNCHRONOUSLY with no try/catch, from
|
|
721
|
+
a detached context the caller's `try/await/catch` could not contain. Audited
|
|
722
|
+
every spawn/detached-callback site in `clients/` and hardened the at-risk
|
|
723
|
+
best-effort/background ones to the repo's existing guard shape (try/catch
|
|
724
|
+
around the synchronous `spawn` + resolve gracefully, matching the async
|
|
725
|
+
`'error'`-listener path): `safeSpawnAsync`'s core spawn (the shared wrapper
|
|
726
|
+
dozens of best-effort callers rely on to NEVER reject), its Windows
|
|
727
|
+
`killPidTreeSync` (runs from `process` exit/signal handlers, where a throw
|
|
728
|
+
becomes an uncaughtException during shutdown; the POSIX branch was already
|
|
729
|
+
guarded), `SgRunner.exec`'s three ast-grep spawn branches, all six installer
|
|
730
|
+
tool-discovery/verify probes, the PSScriptAnalyzer runner's `spawnPs`, and
|
|
731
|
+
the MCP `analyzeFileFresh` worker fork. A synchronous spawn throw now
|
|
732
|
+
resolves the operation's normal failure value instead of escaping as an
|
|
733
|
+
unhandledRejection/uncaughtException. Behavior-preserving otherwise.
|
|
734
|
+
|
|
735
|
+
- **Fixed: opengrep security findings now surface project-wide in
|
|
736
|
+
`lens_diagnostics mode=full`** (refs #585, #584, #533) — opengrep's
|
|
737
|
+
whole-tree CLI scan ran at session-start and cached its findings, and the
|
|
738
|
+
cache-only extractor registry (`project-diagnostics/extractors.ts`) even
|
|
739
|
+
registered an `"opengrep"` row — but #585 replaced that registry in
|
|
740
|
+
production with `fetchFreshProjectDiagnostics`
|
|
741
|
+
(`project-diagnostics/fresh-fetch.ts`), whose `ANALYZER_IDS` list omitted
|
|
742
|
+
opengrep. Net: opengrep scanned + cached, yet nothing production read it
|
|
743
|
+
back, so semgrep-grade security findings (ERROR→blocking, CWE-tagged) never
|
|
744
|
+
reached the agent for unedited/project-wide files — a scan-and-orphan
|
|
745
|
+
honesty gap (#533). `fetchFreshProjectDiagnostics` now runs opengrep the
|
|
746
|
+
same way it runs gitleaks/trivy: an availability probe (opengrep is
|
|
747
|
+
structurally always-on, so no static project-type gate), then a fresh scan
|
|
748
|
+
that JOINS the session-start scan of the same root via
|
|
749
|
+
`SecurityScanClient.dedupeScan` rather than double-spawning a heavy scan,
|
|
750
|
+
with the result written back to cache and adapted to project diagnostics.
|
|
751
|
+
The per-edit aux-LSP push path (`clientScope: with-auxiliary`,
|
|
752
|
+
`AUXILIARY_LSP_PROFILES`) was verified already-working and is untouched;
|
|
753
|
+
opengrep stays excluded from the push-only LSP workspace sweep (#584). The
|
|
754
|
+
now-dead cache-only `extractCachedProjectDiagnostics` reader — the parallel
|
|
755
|
+
registry that shadowed `ANALYZER_IDS` and let opengrep silently diverge — was
|
|
756
|
+
removed so a single source of truth (#883) remains.
|
|
757
|
+
|
|
758
|
+
- **Fixed: test-runner findings now surface project-wide in `lens_diagnostics
|
|
759
|
+
mode=full`** (closes #1004, refs #585, #533) — the same `ANALYZER_IDS`
|
|
760
|
+
omission #585/#1003 fixed for opengrep also orphaned test-runner: the
|
|
761
|
+
per-edit turn_end test fire (`runtime-turn.ts`) caches failures under
|
|
762
|
+
`"test-runner-findings"`, but `fetchFreshProjectDiagnostics`
|
|
763
|
+
(`project-diagnostics/fresh-fetch.ts`) never read that cache back, so test
|
|
764
|
+
failures never reached the agent for unedited/project-wide `mode=full`
|
|
765
|
+
calls. Unlike opengrep/gitleaks/trivy, this is wired as a CACHE-READ (not a
|
|
766
|
+
fresh scan): test-runner has no "whole project" run to trigger — turn_end
|
|
767
|
+
only ever runs the targeted, cascade-aware test files touched by that
|
|
768
|
+
turn's edits — so `fetchFreshProjectDiagnostics` now peeks at the same
|
|
769
|
+
`"test-runner-findings"` cache key (never re-running the suite, never
|
|
770
|
+
writing back) and adapts it via the existing
|
|
771
|
+
`testRunnerFindingsToProjectDiagnostics`. Added a coverage guardrail
|
|
772
|
+
(`tests/clients/project-diagnostics/analyzer-coverage.test.ts`) that greps
|
|
773
|
+
the real session-start (`runHeavyweightTask`) and EVERY turn_end
|
|
774
|
+
`cacheManager.writeCache` call site in `runtime-turn.ts` (excluding a
|
|
775
|
+
short, explicit bookkeeping exclude-list — `errorDebt`/`turn-end-findings`/
|
|
776
|
+
`turn-end-findings-last` — that isn't analyzer-shaped) and asserts every
|
|
777
|
+
remaining id is a member of `ANALYZER_IDS`, so this whole #585 bug class
|
|
778
|
+
can't silently regress again for a future analyzer added to EITHER writer.
|
|
779
|
+
Adversarial review also caught two honesty/fidelity gaps in the initial
|
|
780
|
+
fix: (1) test-runner is cache-read, edit-scoped (the targeted test files
|
|
781
|
+
touched by the most recent turn's edits), unlike every other analyzer's
|
|
782
|
+
fresh whole-project scan — `mode=full` now emits an explicit "coverage is
|
|
783
|
+
edit-scoped, NOT a full-project run" caveat whenever test-runner
|
|
784
|
+
contributed findings (not only when cold), and a `stale` cached result
|
|
785
|
+
(the turn advanced before the test run finished) is prefixed
|
|
786
|
+
`[stale — from a prior turn]` in its diagnostic message, matching the
|
|
787
|
+
one-shot turn-context message's own stale wording; (2) `TestFailure.location`
|
|
788
|
+
(`"file.ts:42"`, set by the vitest/jest JSON parser in
|
|
789
|
+
`test-runner-client.ts`) never reached `ProjectDiagnostic.line`, so every
|
|
790
|
+
test-runner finding rendered as `L?:` — `runner-adapters/runner-findings.ts`
|
|
791
|
+
now parses a numeric trailing `:line[:col]` out of `location` (left alone
|
|
792
|
+
for pytest/mix's non-numeric `location` strings, which carry a test name or
|
|
793
|
+
module, not a line).
|
|
794
|
+
|
|
795
|
+
- **Guarded Windows CPU/RSS resource sampling so a best-effort sampler can no
|
|
796
|
+
longer crash the pi host** (refs #620, #533) — `clients/resource-sampler.ts`
|
|
797
|
+
sampled CPU%/RSS via `pidusage`, whose Windows path shells out to `gwmi`
|
|
798
|
+
through an internal `spawn(..., { shell: "powershell.exe" })` that has no
|
|
799
|
+
try/catch and runs from inside a ChildProcess `close` callback. Under real
|
|
800
|
+
Windows handle/commit pressure that spawn can throw `spawn UNKNOWN`
|
|
801
|
+
(errno -4094) **synchronously in that detached callback**, which the call
|
|
802
|
+
site's `try { await pidusage() } catch {}` cannot catch → `uncaughtException`
|
|
803
|
+
→ the whole host process dies (observed live). pidusage 4.0.1 offers no
|
|
804
|
+
option to avoid the gwmi path. The sampler now runs its OWN fully guarded
|
|
805
|
+
`Get-CimInstance Win32_Process` query on Windows (never calling `pidusage`
|
|
806
|
+
there), mirroring the existing `findDescendantPidsWindows` guard: a
|
|
807
|
+
synchronous spawn throw, a child `error` event, and a non-zero/garbage exit
|
|
808
|
+
all resolve to a partial/empty map — the sampler can now only ever lose a
|
|
809
|
+
data point, never throw into the heartbeat/spawn path. RSS comes from
|
|
810
|
+
`WorkingSetSize`; CPU% is preserved via the same KernelModeTime/UserModeTime
|
|
811
|
+
delta-over-elapsed-wall-time computation gwmi uses (a small per-pid history
|
|
812
|
+
tracks the prior cumulative CPU time). Linux/macOS keep using `pidusage`
|
|
813
|
+
(procfs/`ps` — not the crash vector) unchanged. A pid absent from the
|
|
814
|
+
returned map still means "unsampled this tick", never zero.
|
|
815
|
+
|
|
816
|
+
- **Stabilized the two remaining flaky/environment-sensitive tests tracked in
|
|
817
|
+
#902.** LSP workspace-diagnostics sweep flush-count assertions
|
|
818
|
+
(`workspace-diagnostics-sweep-batch-open.test.ts`,
|
|
819
|
+
`workspace-diagnostics-sweep-preopen-chunk.test.ts`) pinned an exact flush
|
|
820
|
+
count, but the pre-open pass's real `fs.promises.readFile` genuinely races
|
|
821
|
+
the real 100ms `WatchedFilesQueue` debounce timer — under full-suite
|
|
822
|
+
scheduler contention that can legitimately fragment or merge a chunk's
|
|
823
|
+
flush by one. Replaced the exact-count assertions with invariant checks
|
|
824
|
+
(coalescing happened; nowhere near one flush per file; no lost/duplicated
|
|
825
|
+
URIs) that tolerate that jitter without losing the #608/#621 regression
|
|
826
|
+
coverage — a test-robustness fix, not a product bug. Separately, two
|
|
827
|
+
ast-grep dispatch test harnesses
|
|
828
|
+
(`ast-grep-rule-tests.test.ts`, `ast-grep-catalog-rules.test.ts`) shelled
|
|
829
|
+
out to the real `ast-grep` CLI via a raw `execFileSync(..., { shell: true
|
|
830
|
+
})` per call — an uncached cmd.exe wrapper on Windows whose own exit code
|
|
831
|
+
can mask the real child's and which can intermittently fail to spawn at
|
|
832
|
+
all under the process-creation pressure of a full parallel test run.
|
|
833
|
+
Switched both to the already-hardened `safeSpawn` (`clients/safe-spawn.ts`,
|
|
834
|
+
#817) — cached PATH+PATHEXT resolution, direct `.exe`/`.com` spawn with no
|
|
835
|
+
shell involved — the same deterministic resolution production dispatch
|
|
836
|
+
already relies on. The third tracked item
|
|
837
|
+
(`startup-overhead.test.ts`'s "quick mode self-reports within 100ms")
|
|
838
|
+
was already reworked to a generous fixed budget + `retry: 2` in an earlier
|
|
839
|
+
pass; confirmed still adequate, no further change needed.
|
|
840
|
+
|
|
841
|
+
- **`pi-lens build-graph` honestly reports a capped, over-the-cap persist**
|
|
842
|
+
(closes #924, refs #936 limit 3) — the CLI's build-attempt check no longer
|
|
843
|
+
mistakes the benign "succeeded, but persisted a partial subgraph" reason
|
|
844
|
+
(#960's over-cap circuit-breaker) for a hard failure: it now exits 0 and
|
|
845
|
+
prints a `PARTIAL persist` line with `persistedNodes=X/Y`,
|
|
846
|
+
`persistedEdges=X/Y`, and the cap that was hit, instead of either silently
|
|
847
|
+
reporting "done" or failing loudly on a build/persist that actually
|
|
848
|
+
succeeded. `flushReviewGraphPersist()` now returns the persisted
|
|
849
|
+
`coverage` so any standalone caller can make the same distinction.
|
|
850
|
+
|
|
851
|
+
- **`ts-ssrf` no longer trusts naming convention as proof of a fixed URL**
|
|
852
|
+
(fixes #963) — the post-filter now resolves a bare `fetch(IDENT)` argument
|
|
853
|
+
against the file's AST and exempts it only when `IDENT` provably resolves
|
|
854
|
+
to a single `const` declarator initialized with a string literal (or a
|
|
855
|
+
template literal with no `${...}` substitutions). SCREAMING_SNAKE_CASE
|
|
856
|
+
naming is no longer sufficient by itself: `const TARGET_URL = req.query.url`
|
|
857
|
+
still flags, since the identifier's *initializer* — not its name — is
|
|
858
|
+
what's checked. Ambiguous/shadowed declarations, `let`/`var` bindings,
|
|
859
|
+
reassigned identifiers, and unresolved identifiers all fall through to the
|
|
860
|
+
existing broad heuristic and keep being flagged.
|
|
861
|
+
|
|
862
|
+
- **Remaining test-suite spawns routed off `shell:true` to close the Windows-spawn flake class** (refs #902) —
|
|
863
|
+
`tests/clients/ast-grep-rule-precedence-followups.test.ts` (CLI probe + `runCli`),
|
|
864
|
+
`tests/clients/coderabbit-ast-grep-rules.test.ts` (vendored-catalog smoke),
|
|
865
|
+
`tests/clients/dispatch/runners/ast-grep-playground-verify.test.ts` (async `runVerify`),
|
|
866
|
+
`tests/clients/dead-code-client.test.ts` (vulture probe), and
|
|
867
|
+
`tests/clients/lsp/clangd-lazy-indexing.test.ts` (clangd `where`/`which` probe) now spawn
|
|
868
|
+
through the hardened `safeSpawn`/`safeSpawnAsync` (`clients/safe-spawn.ts`, #817) instead of
|
|
869
|
+
a raw `execFileSync`/`spawnSync`/`spawn`/`execSync` with a fresh, uncached `cmd.exe` wrapper
|
|
870
|
+
per call — the documented intermittent ENOENT/EAGAIN source under windows-latest CI's
|
|
871
|
+
parallel process-creation pressure. Completes the sweep PR #993 started for the two
|
|
872
|
+
ast-grep runner test files; `git` spawns are left as-is (no `.cmd` shim, so out of the flake
|
|
873
|
+
class) and the `mcp/*` and MCP harness spawns that need bidirectional stdin streaming are
|
|
874
|
+
left as-is (`safeSpawnAsync` only exposes a close-based result, not a live stdin/stdout pipe).
|
|
875
|
+
|
|
876
|
+
- **Tree-sitter post-filters no longer leave silently dead rules** (refs #879) —
|
|
877
|
+
the 25 unknown filter references were resolved by implementing eight bounded,
|
|
878
|
+
fail-open-on-filter-error AST checks, expressing two conditions directly in
|
|
879
|
+
their queries, and removing fifteen rules whose promised semantic/framework
|
|
880
|
+
analysis could not be supported honestly. Coarse resource, assertion, and
|
|
881
|
+
sensitive-`memset` heuristics are advisory rather than blocking.
|
|
882
|
+
|
|
883
|
+
- **Capped word indexes disclose partial coverage** (refs #928) — snapshots now
|
|
884
|
+
persist indexed-file count and truncation state, and both `symbol_search`
|
|
885
|
+
surfaces report coverage instead of presenting capped zero-hit results as
|
|
886
|
+
authoritative.
|
|
887
|
+
|
|
888
|
+
- **`pilens_health` keeps disabled LSP servers visible** (refs #927) —
|
|
889
|
+
permanently broken server/root pairs now render with their failure count, and
|
|
890
|
+
temporary circuit-breaker cooldowns expose their retry deadline.
|
|
891
|
+
|
|
892
|
+
- **Cold `symbol_search` failures are now observable and honest** (refs #926) —
|
|
893
|
+
unavailable results distinguish an active build, a safety refusal, and the
|
|
894
|
+
last build's failure, while background build/persist errors reach a persistent
|
|
895
|
+
NDJSON log.
|
|
896
|
+
|
|
897
|
+
- **Failed heavyweight analyzers no longer masquerade as clean runs** (refs #925) —
|
|
898
|
+
unsuccessful results are reported distinctly, omitted from cache so the next
|
|
899
|
+
session retries, and valid fix-worklog records survive neighboring corrupt lines.
|
|
900
|
+
|
|
901
|
+
- **The footer refreshes as LSP servers come online during a cold
|
|
902
|
+
`lens_diagnostics mode=full` sweep** (refs #798), instead of showing
|
|
903
|
+
`LSP Inactive` until turn end. The repaint captures UI methods during the
|
|
904
|
+
active tool event, so async warm-up never touches a stale session context.
|
|
905
|
+
|
|
906
|
+
- **Tree-sitter WASM aborts are now visible instead of silently disabling
|
|
907
|
+
structural analysis for the rest of the process** (refs #915). The shared
|
|
908
|
+
runtime records a process-wide, timestamped `restart_required` health state,
|
|
909
|
+
logs a one-time actionable error, exposes it through `pilens_health`, and
|
|
910
|
+
marks project-scan responses with `treeSitterStatus`. A poisoned scan remains
|
|
911
|
+
truncated and never replaces the last complete snapshot. In-process retry is
|
|
912
|
+
deliberately unsafe: every new client imports the same cached `web-tree-sitter`
|
|
913
|
+
ES module and therefore reuses its corrupted Emscripten heap; restarting the
|
|
914
|
+
host is the isolation boundary.
|
|
915
|
+
|
|
916
|
+
- **`pilens_rebuild` can no longer destroy an npm-installed pi-lens** (refs
|
|
917
|
+
#920) — rebuilds are refused before spawning a package script unless the
|
|
918
|
+
package is a source checkout with `tsconfig.dist.json` outside
|
|
919
|
+
`node_modules`; installed servers also omit the tool from `tools/list`, so
|
|
920
|
+
subagent allowlists cannot discover it.
|
|
921
|
+
|
|
922
|
+
- **Review-graph background failures are no longer silent** (refs #919) —
|
|
923
|
+
`project_report`/`pilens_project_report` now surface the most recent build
|
|
924
|
+
attempt and its terminal skip/failure reason, including the persistence
|
|
925
|
+
element circuit-breaker. A dedicated `~/.pi-lens/review-graph.log` records
|
|
926
|
+
build and persistence starts, successes, skips, and failures, so a rejected
|
|
927
|
+
fire-and-forget build or an over-cap graph cannot look perpetually in progress.
|
|
928
|
+
|
|
929
|
+
- Resolve nested C# and F# project roots for dotnet builds (refs #895).
|
|
930
|
+
|
|
931
|
+
- **A mid-scan tree-sitter WASM abort no longer replaces the authoritative
|
|
932
|
+
project-diagnostics snapshot with a silently truncated result** (refs #891).
|
|
933
|
+
The partial scan is returned with `scanTruncated`, logs its completed/total
|
|
934
|
+
file counts and abort point, and leaves the previous complete cache intact.
|
|
935
|
+
|
|
936
|
+
- **Block-wrapped switch cases no longer report false fall-through errors**
|
|
937
|
+
(refs #910) — `switch-case-termination` now follows trailing statement
|
|
938
|
+
blocks to recognize a nested `break`, `return`, `throw`, or `continue`, while
|
|
939
|
+
still flagging empty and non-terminating blocks.
|
|
940
|
+
|
|
941
|
+
- **Project-wide enumeration now covers every registered file kind** (refs
|
|
942
|
+
#894) — `ALL_SCANNABLE_EXTENSIONS`, `WARMUP_SOURCE_EXTS`, and
|
|
943
|
+
`SUPPORTED_FILE_KINDS` now derive from the single `KIND_EXTENSIONS`
|
|
944
|
+
authority instead of three drifting language lists. TODO scans, symbol
|
|
945
|
+
search indexing, dominant-language LSP warmup, and language-profile
|
|
946
|
+
detection can now see Java, Swift, C/C++, PHP, and every other supported
|
|
947
|
+
kind. Code kinds keep priority over data/doc kinds (json/yaml/markdown/…)
|
|
948
|
+
inside the existing caps: the dominant-language LSP warm ranks code kinds
|
|
949
|
+
first, and the capped warmup/word-index walks fill code files before
|
|
950
|
+
non-code files, so a locale/fixture pile can't starve real languages.
|
|
951
|
+
Package-manager lockfiles (package-lock.json, pnpm-lock.yaml, …) are
|
|
952
|
+
filtered as generated artifacts, and the TODO scanner caps per-file reads
|
|
953
|
+
at 512 KiB. A coverage guard makes a newly registered kind automatically
|
|
954
|
+
enumerable — and classified as code or non-code — on both project-wide
|
|
955
|
+
paths.
|
|
956
|
+
|
|
957
|
+
- **CMake files now reach a real LSP server** (refs #892) — the CMake policy's
|
|
958
|
+
previous `lsp` fallback had no registered server and silently produced no
|
|
959
|
+
diagnostics. `cmake-language-server` now covers both `.cmake` files and the
|
|
960
|
+
canonical `CMakeLists.txt` basename, with managed pip installation.
|
|
961
|
+
|
|
962
|
+
- **Fish LSP policy is no longer dead wiring** (refs #893) — `fish-lsp` is now
|
|
963
|
+
registered for `.fish` files and auto-installed through npm; `fish_indent`
|
|
964
|
+
continues to run alongside it.
|
|
965
|
+
|
|
966
|
+
- **Editing an inherited tree-sitter rule now invalidates the inheriting language's RuleCache** (refs #878) — the cache key fingerprinted only the language's OWN rules directory, but `tsx` also runs the `typescript` rule set (`queriesForLanguage`), so a typescript-rule edit left the tsx entry's hash unchanged and stale compiled rules kept being served from the on-disk cache until a tsx rule happened to change. The fingerprint now covers the full effective rule set via `ruleFilesForLanguage`, a new loader-owned seam that derives from the same rule-source composition as rule selection, so the cache key can't drift from what the runner actually runs. The runner's cache-miss path also forces the query loader past its in-memory memo (`loadQueries(root, { force: true })`) — a correct key alone wasn't enough: the memo ignores rule-file mtimes, so within one process a miss re-persisted the PRE-edit rules under the fresh fingerprint and the staleness then survived restarts. The client's compiled-batch cache is likewise now keyed on rule content instead of rule ids — ids are stable across edits, so the shared client kept serving the pre-edit compiled patterns (and messages) for the process lifetime even after the reload. `CACHE_VERSION` bumped to `v6`.
|
|
967
|
+
|
|
968
|
+
- **Small edits no longer pay the entity-extraction cost** (refs #885) — the
|
|
969
|
+
<5-line skip threshold only guarded the zero-diagnostics early return; a
|
|
970
|
+
second `extractEntitySnapshot` block ran unconditionally, so trivial edits
|
|
971
|
+
still spent ~500-800ms per dispatch and, on unsaved buffers, parsed stale
|
|
972
|
+
disk content (thrashing the parse-cache entry). Extraction is now one
|
|
973
|
+
threshold-guarded block that receives the same `file.content` override the
|
|
974
|
+
diagnostics phase used.
|
|
975
|
+
|
|
976
|
+
- **The per-edit tree-sitter runner walks the tree once, not once per rule**
|
|
977
|
+
(refs #888) — the dispatch hot path ran ~30-40 `runQueryOnFile` walks per
|
|
978
|
+
edit behind a concurrency limiter that could not parallelize synchronous
|
|
979
|
+
WASM. It now calls `runQueriesOnFile` once (#675 batching) and distributes
|
|
980
|
+
the per-rule results; the per-rule `maxResults(10)` cap and modified-ranges
|
|
981
|
+
gating are unchanged.
|
|
982
|
+
|
|
983
|
+
- **A transient grammar-load failure no longer disables batched rule scans for the process lifetime** (refs #889) — `compileQueryBatch` cached `null` on ANY `build()` failure, including a transient `loadLanguage()` miss (offline lazy grammar fetch, mid-scan load error), so every later scan fell back to the per-rule path and paid ~3.3× forever. Load failures are now distinguished from genuine batch-compile failures: they are not cached (the next scan retries the load and recovers), with retries bounded at 3 consecutive load failures per rule set before the miss is cached, so a grammar that never loads doesn't hot-loop. Deterministic compile failures still cache `null` permanently. The query/batch cache keys also switched from a collision-prone 32-bit hash to the full pattern text.
|
|
984
|
+
|
|
985
|
+
- **module-report parses plain JS under the correct tree-sitter grammar** (closes
|
|
986
|
+
#887) — `tsLangForFile` hand-rolled a local extension map that sent
|
|
987
|
+
`.js`/`.mjs`/`.cjs` to the typescript grammar and `.jsx` to tsx, while every
|
|
988
|
+
other tree-sitter consumer resolves those extensions to `javascript` via the
|
|
989
|
+
shared `EXT_TO_LANG` (`clients/tree-sitter-shared.ts`), so each plain-JS file
|
|
990
|
+
was parsed and cached twice under two grammars (TreeCache keys are
|
|
991
|
+
`languageId:path`) and ran TS-grammar symbol queries on JS trees. It now
|
|
992
|
+
routes the extension-split kinds (jsts, and the c-vs-cpp split for cxx)
|
|
993
|
+
through the shared `resolveTreeSitterLanguage`, keeping the historical kind
|
|
994
|
+
default only for extensions the shared map does not cover (`.svelte`/`.vue`,
|
|
995
|
+
the C++ module-interface/Objective-C tail). The symbol extractor gains a
|
|
996
|
+
dedicated `javascript` defs/refs/import query set: the TypeScript symbol
|
|
997
|
+
queries do NOT compile against the javascript grammar
|
|
998
|
+
(`interface_declaration`/`type_alias_declaration`/`type_identifier` do not
|
|
999
|
+
exist there — a query naming them fails with "Bad node name"), so the
|
|
1000
|
+
javascript set is the same queries minus the type-only patterns, with class
|
|
1001
|
+
names matched as `(identifier)` instead of `(type_identifier)`.
|
|
1002
|
+
|
|
1003
|
+
- **A rule whose query fails to compile now warns once instead of silently reporting nothing** (refs #884) — both compile paths (`compileQueryBatch`'s per-rule drop and `compileRawQuery`, which every per-edit `runQueryOnFile` call falls back on) previously either `dbg()`-logged (invisible without verbose mode) or returned `null`/`[]` with no trail at all. They now call a shared `reportQueryCompileFailure`, mirroring the existing unimplemented-`post_filter` warning: one `console.error` per broken rule id, not per file. A new compile-guard test (`tests/clients/tree-sitter-rule-compile-guard.test.ts`) compiles every non-disabled shipped rule against its real grammar and caught the 32 rules #884 reports as currently broken (tracked there in a shrink-only `KNOWN_BROKEN` allowlist so follow-up fix PRs are forced to remove their entries, and the list can't grow or go stale unnoticed).
|
|
1004
|
+
|
|
1005
|
+
- **TreeCache mtime false misses and FIFO eviction** (closes #890) — a content
|
|
1006
|
+
hash match is now authoritative: a save-without-change (same bytes, newer
|
|
1007
|
+
mtime) is a cache hit that refreshes the entry's stat metadata instead of
|
|
1008
|
+
invalidating and re-parsing, and `get()` re-inserts hit entries so eviction
|
|
1009
|
+
is true LRU — hot per-edit files are no longer evicted by scan traffic.
|
|
1010
|
+
|
|
1011
|
+
- **Eight enabled typescript/javascript tree-sitter rules whose queries never
|
|
1012
|
+
compiled** (refs #884) — each had been silently dead since authoring because
|
|
1013
|
+
its query failed to compile against the real grammar, so it matched nothing
|
|
1014
|
+
and reported no diagnostics. Repaired against the actual node/field names and
|
|
1015
|
+
verified end-to-end (matches the bug, leaves correct code alone):
|
|
1016
|
+
`empty-switch-case`, `switch-case-termination`, `switch-case-termination-js`
|
|
1017
|
+
(switch cases carry their statements as direct `body:` children, not a
|
|
1018
|
+
`consequence: (statement_block)`); `infinite-loop` (`while (true)` wraps the
|
|
1019
|
+
condition in a `parenthesized_expression`; `for (;;)` has an `empty_statement`
|
|
1020
|
+
condition, not `(null)`); `duplicate-function-arg` (typescript parameters are
|
|
1021
|
+
`required_parameter`, not bare `(identifier)`; now also catches non-adjacent
|
|
1022
|
+
duplicates); `mixed-async-styles` (no `async_modifier` node — match the `async`
|
|
1023
|
+
token); `switch-non-case-labels` (JS) (a `labeled_statement` lives inside a
|
|
1024
|
+
`switch_case`, not directly under `switch_body`); and `ts-insecure-random`
|
|
1025
|
+
(the inline `(?i)` regex flag is invalid in JS `RegExp` — dropped the
|
|
1026
|
+
redundant name predicate and let the post-filter do the case-insensitive
|
|
1027
|
+
check, walking up from the `Math.random()` call so chained forms like
|
|
1028
|
+
`Math.random().toString(36)` are still attributed to their binding). Also
|
|
1029
|
+
implemented the four post-filters these rules referenced but that were never
|
|
1030
|
+
defined (`is_empty_block`, `no_break_or_return_in_body`, `same_param_name`,
|
|
1031
|
+
`no_terminating_statement`), which the batch runner had been failing closed on.
|
|
1032
|
+
|
|
1033
|
+
- **A column-0 comment after a `query: |` block no longer breaks the rule**
|
|
1034
|
+
(refs #884) — the query-block extractor kept every line more-indented than the
|
|
1035
|
+
key (to preserve `#eq?`/`#match?` predicate lines) but did not stop at a
|
|
1036
|
+
document-level `# …` comment sitting between the block and the next key, so the
|
|
1037
|
+
comment was appended to the query and made it fail to compile. This is what
|
|
1038
|
+
kept `mixed-async-styles` dead even after its query was otherwise correct.
|
|
1039
|
+
|
|
1040
|
+
- **Seven ruby security rules never compiled and never produced a finding** (refs
|
|
1041
|
+
#884) — `ruby-command-injection`, `ruby-eval`, `ruby-insecure-deserialization`,
|
|
1042
|
+
`ruby-insecure-random`, `ruby-open-struct`, `ruby-string-eval` and
|
|
1043
|
+
`ruby-weak-hash` were authored against JavaScript-grammar node names
|
|
1044
|
+
(`call_expression`, `method_call`, `command`, `interpolated_string`) that do not
|
|
1045
|
+
exist in tree-sitter-ruby, so every one failed to compile with a "Bad node name"
|
|
1046
|
+
error and was silently skipped. Each query is rewritten against the real ruby
|
|
1047
|
+
grammar (`call` with `method:`/`receiver:` fields, `scope_resolution` receivers
|
|
1048
|
+
for `Digest::MD5`, `string` for interpolated literals), with `#match?`/`#eq?`
|
|
1049
|
+
predicates moved inside the pattern's outermost parens. Per-rule positive and
|
|
1050
|
+
negative fixtures (`system` vs `File.read`, `Marshal.load` vs `YAML.safe_load`,
|
|
1051
|
+
`rand` vs `SecureRandom`, `OpenStruct.new` vs `Struct.new`, `Digest::MD5` vs
|
|
1052
|
+
`Digest::SHA256`, string vs block `class_eval`) pin the security intent.
|
|
1053
|
+
|
|
1054
|
+
- **Six python rules never compiled or never fired** (refs #884) —
|
|
1055
|
+
`python-empty-except` used a `body:` field that doesn't exist on
|
|
1056
|
+
`except_clause`; `in-operator-unsupported` used bare `"in"`/`"not"` `"in"`
|
|
1057
|
+
tokens the grammar doesn't expose that way (`not in` is a single token) and
|
|
1058
|
+
only matched `identifier` targets, so `x in None` never matched at all;
|
|
1059
|
+
`no-super-torchscript` looked for a `(call function: (identifier))` inside the
|
|
1060
|
+
decorator (`@torch.jit.script` has no call — it's a bare `attribute`) and
|
|
1061
|
+
expected the decorator directly on the method rather than the class;
|
|
1062
|
+
`notimplemented-boolean-context` used `("and" | "or")` (not valid tree-sitter
|
|
1063
|
+
query syntax — alternation is `[...]`) inside a non-existent `binary_operator`
|
|
1064
|
+
form and `unary_operator operator: ("not")`, when python's logical `not` is
|
|
1065
|
+
its own `not_operator` node with an `argument:` field;
|
|
1066
|
+
`yield-return-outside-function` referenced a `yield_expression` node type that
|
|
1067
|
+
doesn't exist in tree-sitter-python. All five queries are rewritten against
|
|
1068
|
+
the real grammar (verified via AST dumps against `tree-sitter-python.wasm`);
|
|
1069
|
+
`no-super-torchscript` and `in-operator-unsupported` gained
|
|
1070
|
+
`torchscript_super_call` / `check_in_operator_types` post-filters
|
|
1071
|
+
(`clients/tree-sitter-client.ts`) since neither had a working implementation
|
|
1072
|
+
behind their declared `post_filter` name. `exit-signature-check` compiled
|
|
1073
|
+
fine but its `@PARAM1?`/`@PARAM2?`/`@PARAM3?` captures never matched anything
|
|
1074
|
+
— the `?` quantifier was written after the capture name instead of the node,
|
|
1075
|
+
so the captured names were literally `PARAM1?`/`PARAM2?`/`PARAM3?` while the
|
|
1076
|
+
post_filter read `captures.PARAM1`; fixed to capture the whole `parameters`
|
|
1077
|
+
node and count named children instead (per-slot optional quantifiers turned
|
|
1078
|
+
out to match every valid sub-alignment, not just the maximal one, producing
|
|
1079
|
+
spurious duplicate matches for a fully-correct signature).
|
|
1080
|
+
|
|
1081
|
+
- **Project scans run tree-sitter rules for every supported language, not just 10 extensions** (closes #882, refs #877, #880) — the scanner's `TREE_SITTER_EXT_TO_LANG` covered only ts/tsx/js/py/go/rs/rb, so files whose grammars and non-disabled rule dirs already exist (c, cpp, csharp, css, php, java, kotlin) were silently skipped by the tree-sitter phase of project scans. It now derives the shared per-edit resolver (`EXT_TO_LANG`) so c/cpp/csharp/php/css and the `.tsx`→tsx / `.jsx`→javascript nuances can't drift from the per-edit path, and layers java/kotlin on top (grammars + rule dirs exist but no per-edit `appliesTo`). A regression test asserts the map covers every non-disabled rule dir whose grammar is loadable.
|
|
1082
|
+
|
|
1083
|
+
- **`.dart` files are now included in project-wide source enumeration** (closes #880, refs #876) — `ALL_SCANNABLE_EXTENSIONS` (`clients/source-filter.ts`) and `WARMUP_SOURCE_EXTS` (`clients/language-profile.ts`) were missing `.dart`, so Dart projects were fully supported per-edit (LSP, `dart-analyze`, `dart format`, autofix) but skipped by project-wide scans and cold-start language-profile warmup.
|
|
1084
|
+
|
|
1085
|
+
- **Tree-sitter rules were compiled against the wrong grammar** — a compiled
|
|
1086
|
+
query is bound to the language it compiled against, and running it on a tree
|
|
1087
|
+
from another grammar returns zero matches silently. Rules now compile against
|
|
1088
|
+
the language the file is parsed as. This is why the javascript→typescript rule
|
|
1089
|
+
merge never fired a single diagnostic in its life. That merge stays off — the
|
|
1090
|
+
rules are written against typescript node types and misfire on JS — but the
|
|
1091
|
+
same fix is what makes `.tsx` work.
|
|
1092
|
+
|
|
1093
|
+
- **Project scans ran rules from `-disabled/` directories** — the per-edit runner
|
|
1094
|
+
excluded them, the scanner read the raw loader map and ran them anyway.
|
|
1095
|
+
**1,936 of a 500-file scan's 2,590 tree-sitter findings came from rules that
|
|
1096
|
+
were explicitly switched off.** Rule-set selection now goes through one seam,
|
|
1097
|
+
`queriesForLanguage`.
|
|
1098
|
+
|
|
1099
|
+
- **`.tsx` parsed with the typescript grammar**, which produces ERROR nodes on
|
|
1100
|
+
JSX, and under a different language id than the fact providers use — so every
|
|
1101
|
+
`.tsx` file was parsed twice, once wrongly. It now parses as `tsx` and
|
|
1102
|
+
inherits the typescript rule set, which is rule-for-rule identical on both
|
|
1103
|
+
grammars. `.tsx` also gets those rules in the per-edit runner now, where it
|
|
1104
|
+
previously saw only its own two JSX rules.
|
|
1105
|
+
|
|
1106
|
+
- **An unimplemented `post_filter` reported every raw match instead of none.**
|
|
1107
|
+
40 of the 84 filters rules reference have no implementation; the default now
|
|
1108
|
+
drops the match and warns once. `duplicate-function-arg` (whose
|
|
1109
|
+
`same_param_name` filter was never implemented) was reporting 59 phantom
|
|
1110
|
+
duplicates across 60 files.
|
|
1111
|
+
|
|
1112
|
+
- **YAML scalars kept their trailing comments** in the rule loader's hand-rolled
|
|
1113
|
+
parser — `post_filter: not_in_test_block # skip test blocks` carried the
|
|
1114
|
+
comment as part of the filter name, so the filter silently never applied.
|
|
1115
|
+
|
|
1116
|
+
- **tree-sitter runner no longer double-increments already 1-indexed
|
|
1117
|
+
line/column** (refs #448) — dispositions recorded against the old off-by-one
|
|
1118
|
+
are anchored (`clients/diagnostic-dispositions.ts`) to the wrong physical
|
|
1119
|
+
line's content and will resurface once after upgrading; this is expected and
|
|
1120
|
+
one-time, not a regression.
|
|
1121
|
+
|
|
1122
|
+
### Security
|
|
1123
|
+
|
|
1124
|
+
- **Debug logs redact credential-shaped text before it reaches disk** (closes
|
|
1125
|
+
#327). The shared NDJSON boundary scrubs private keys, provider tokens, AWS
|
|
1126
|
+
access keys, and JWT/JWE compact tokens with deterministic linear scanners.
|
|
1127
|
+
The synchronous crash-adjacent LSP launch diagnostic uses the same redactor.
|
|
1128
|
+
|
|
13
1129
|
## [3.8.73] - 2026-07-28
|
|
14
1130
|
|
|
15
1131
|
### Added
|