pi-lens 3.8.73 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2614 -12
- package/README.md +50 -21
- package/dist/clients/actionable-warnings.js +73 -18
- package/dist/clients/agent-behavior-client.js +21 -4
- package/dist/clients/ast-grep-client.js +68 -16
- package/dist/clients/ast-grep-types.js +0 -1
- package/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/dist/clients/atomic-write-staging.js +63 -0
- package/dist/clients/atomic-write.js +116 -16
- package/dist/clients/bash-file-access.js +193 -23
- package/dist/clients/biome-client.js +29 -25
- package/dist/clients/bootstrap.js +11 -2
- package/dist/clients/bounded-cache.js +34 -0
- package/dist/clients/bounded-pid-file-lock.js +192 -0
- package/dist/clients/bus-events-logger.js +5 -1
- package/dist/clients/bus-publish.js +10 -3
- package/dist/clients/cache/rule-cache.js +89 -6
- package/dist/clients/cache-manager.js +97 -9
- package/dist/clients/cache-observability.js +498 -0
- package/dist/clients/call-graph.js +460 -108
- package/dist/clients/cascade-logger.js +5 -1
- package/dist/clients/child-unref.js +85 -0
- package/dist/clients/codebase-model.js +52 -23
- package/dist/clients/collateral-test-role.js +40 -0
- package/dist/clients/complexity-client.js +6 -8
- package/dist/clients/console-guard-install.js +14 -0
- package/dist/clients/cooperative-budget.js +42 -0
- package/dist/clients/dead-code-client.js +2 -1
- package/dist/clients/debug-handles.js +206 -0
- package/dist/clients/debug-heap.js +167 -0
- package/dist/clients/degradation-ledger.js +153 -0
- package/dist/clients/dependency-checker.js +466 -63
- package/dist/clients/diagnostic-dispositions.js +83 -13
- package/dist/clients/diagnostics-publish.js +10 -3
- package/dist/clients/dispatch/auxiliary-lsp.js +34 -0
- package/dist/clients/dispatch/dispatcher.js +71 -13
- package/dist/clients/dispatch/fact-store.js +11 -0
- package/dist/clients/dispatch/facts/function-facts.js +37 -7
- package/dist/clients/dispatch/facts/import-facts.js +108 -119
- package/dist/clients/dispatch/facts/tree-sitter-facts.js +12 -12
- package/dist/clients/dispatch/facts/try-catch-facts.js +13 -0
- package/dist/clients/dispatch/indent-detect.js +35 -0
- package/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/dist/clients/dispatch/integration.js +1094 -611
- package/dist/clients/dispatch/lazy.js +14 -0
- package/dist/clients/dispatch/plan.js +11 -1
- package/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/dist/clients/dispatch/rule-policy.js +135 -0
- package/dist/clients/dispatch/rules/async-noise.js +1 -0
- package/dist/clients/dispatch/rules/unsafe-boundary.js +7 -0
- package/dist/clients/dispatch/runners/ast-grep-napi.js +47 -6
- package/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/dist/clients/dispatch/runners/detekt.js +3 -2
- package/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/dist/clients/dispatch/runners/index.js +5 -1
- package/dist/clients/dispatch/runners/lsp.js +14 -6
- package/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/dist/clients/dispatch/runners/psscriptanalyzer.js +10 -1
- package/dist/clients/dispatch/runners/pyright.js +9 -5
- package/dist/clients/dispatch/runners/ruff.js +4 -8
- package/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/dist/clients/dispatch/runners/taplo.js +2 -1
- package/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/dist/clients/dispatch/runners/tflint.js +37 -5
- package/dist/clients/dispatch/runners/tree-sitter.js +155 -166
- package/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/dist/clients/dispatch/runners/utils/runner-helpers.js +284 -34
- package/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/dist/clients/disposition-publish.js +10 -3
- package/dist/clients/durable-store.js +97 -0
- package/dist/clients/event-loop-monitor.js +94 -3
- package/dist/clients/extension-log.js +139 -0
- package/dist/clients/extension-mode.js +73 -0
- package/dist/clients/feature-hints.js +53 -7
- package/dist/clients/file-kinds.js +143 -31
- package/dist/clients/file-role.js +18 -3
- package/dist/clients/file-utils.js +56 -34
- package/dist/clients/fix-worklog.js +14 -2
- package/dist/clients/format-events-publish.js +16 -3
- package/dist/clients/format-service.js +24 -8
- package/dist/clients/formatters-lazy.js +8 -0
- package/dist/clients/formatters.js +358 -61
- package/dist/clients/generated-artifacts.js +169 -15
- package/dist/clients/git-guard.js +681 -11
- package/dist/clients/git-tracked-ignore.js +4 -0
- package/dist/clients/go-client.js +2 -1
- package/dist/clients/govulncheck-client.js +8 -0
- package/dist/clients/grammar-source.js +12 -4
- package/dist/clients/gzip-stage-write.js +102 -0
- package/dist/clients/host-ports.js +31 -0
- package/dist/clients/installer/index.js +733 -213
- package/dist/clients/instance-reaper.js +142 -172
- package/dist/clients/jscpd-client.js +31 -54
- package/dist/clients/knip-client.js +78 -36
- package/dist/clients/language-policy.js +14 -2
- package/dist/clients/language-profile.js +72 -77
- package/dist/clients/latency-logger.js +28 -16
- package/dist/clients/lens-config.js +140 -166
- package/dist/clients/lens-engine.js +124 -21
- package/dist/clients/lens-events.js +27 -4
- package/dist/clients/lens-flag-registry.js +293 -0
- package/dist/clients/lens-map.js +96 -26
- package/dist/clients/live-bus-emitter.js +30 -0
- package/dist/clients/log-cleanup.js +8 -18
- package/dist/clients/lsp/cascade-tier.js +23 -3
- package/dist/clients/lsp/client.js +566 -57
- package/dist/clients/lsp/config.js +74 -10
- package/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/dist/clients/lsp/edits.js +1012 -121
- package/dist/clients/lsp/index.js +1840 -803
- package/dist/clients/lsp/language.js +4 -1
- package/dist/clients/lsp/launch.js +40 -32
- package/dist/clients/lsp/path-utils.js +1 -1
- package/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/dist/clients/lsp/server.js +279 -60
- package/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/dist/clients/lsp-document-symbols.js +112 -0
- package/dist/clients/lsp-lazy.js +8 -0
- package/dist/clients/lsp-mutation.js +248 -0
- package/dist/clients/mcp/analyze.js +128 -36
- package/dist/clients/mcp/ipc.js +157 -24
- package/dist/clients/mcp/review.js +36 -3
- package/dist/clients/mcp/session.js +216 -8
- package/dist/clients/memory-sampler.js +115 -0
- package/dist/clients/metrics-client.js +2 -1
- package/dist/clients/metrics-history.js +2 -1
- package/dist/clients/module-report-lsp.js +5 -0
- package/dist/clients/module-report.js +267 -82
- package/dist/clients/ndjson-logger.js +334 -103
- package/dist/clients/opengrep-client.js +1 -1
- package/dist/clients/partial-edit-apply.js +120 -5
- package/dist/clients/path-keyed-map.js +76 -0
- package/dist/clients/path-utils.js +141 -5
- package/dist/clients/performance-report.js +327 -0
- package/dist/clients/pipeline.js +54 -13
- package/dist/clients/print-mode.js +21 -0
- package/dist/clients/project-changes.js +117 -4
- package/dist/clients/project-diagnostics/cache.js +3 -2
- package/dist/clients/project-diagnostics/extractors.js +26 -115
- package/dist/clients/project-diagnostics/fresh-fetch.js +149 -19
- package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +117 -0
- package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +37 -5
- package/dist/clients/project-diagnostics/scanner.js +268 -128
- package/dist/clients/project-lens-config.js +138 -34
- package/dist/clients/project-report.js +64 -25
- package/dist/clients/project-scale.js +8 -10
- package/dist/clients/project-snapshot-persist-worker.js +6 -0
- package/dist/clients/project-snapshot.js +767 -8
- package/dist/clients/project-trust.js +181 -0
- package/dist/clients/quiet-window.js +27 -5
- package/dist/clients/read-bridge.js +147 -0
- package/dist/clients/read-expansion.js +22 -12
- package/dist/clients/read-guard-logger.js +159 -1
- package/dist/clients/read-guard-tool-lines.js +150 -29
- package/dist/clients/read-guard.js +262 -3
- package/dist/clients/redact/secrets.js +449 -0
- package/dist/clients/resource-sampler.js +144 -39
- package/dist/clients/reverse-deps.js +48 -0
- package/dist/clients/review-graph/builder.js +2570 -326
- package/dist/clients/review-graph/git-identity.js +2 -1
- package/dist/clients/review-graph/import-resolvers.js +21 -10
- package/dist/clients/review-graph/persist-worker.js +10 -0
- package/dist/clients/review-graph/query.js +5 -0
- package/dist/clients/review-graph/shared-extraction-ir.js +90 -0
- package/dist/clients/review-graph/symbol-id.js +46 -0
- package/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/dist/clients/review-graph/workspace-modules.js +51 -3
- package/dist/clients/review-graph-logger.js +53 -0
- package/dist/clients/ruff-client.js +27 -44
- package/dist/clients/runtime-agent-end.js +111 -75
- package/dist/clients/runtime-context.js +36 -12
- package/dist/clients/runtime-coordinator.js +76 -8
- package/dist/clients/runtime-session.js +799 -137
- package/dist/clients/runtime-tool-call.js +117 -62
- package/dist/clients/runtime-tool-result.js +218 -23
- package/dist/clients/runtime-turn.js +284 -40
- package/dist/clients/rust-client.js +2 -1
- package/dist/clients/safe-spawn.js +873 -71
- package/dist/clients/security-scan-client.js +2 -1
- package/dist/clients/session-start-observability.js +15 -0
- package/dist/clients/session-state-store.js +5 -3
- package/dist/clients/sessionstart-logger.js +18 -0
- package/dist/clients/sg-runner.js +284 -159
- package/dist/clients/slow-fs.js +2 -1
- package/dist/clients/smells-rollup.js +224 -0
- package/dist/clients/source-filter.js +231 -43
- package/dist/clients/source-walker.js +66 -10
- package/dist/clients/startup-scan.js +15 -16
- package/dist/clients/startup-timing.js +9 -0
- package/dist/clients/test-runner-client.js +38 -8
- package/dist/clients/todo-scanner.js +30 -3
- package/dist/clients/tool-event.js +35 -0
- package/dist/clients/tool-policy.js +388 -39
- package/dist/clients/tool-render.js +155 -0
- package/dist/clients/tree-sitter-cache.js +153 -140
- package/dist/clients/tree-sitter-client.js +1722 -80
- package/dist/clients/tree-sitter-logger.js +64 -1
- package/dist/clients/tree-sitter-query-loader.js +117 -12
- package/dist/clients/tree-sitter-shared.js +48 -19
- package/dist/clients/tree-sitter-symbol-extractor.js +115 -11
- package/dist/clients/user-notify.js +51 -0
- package/dist/clients/vanished-instance-marker.js +57 -0
- package/dist/clients/warm-attach.js +33 -13
- package/dist/clients/widget-state.js +342 -49
- package/dist/clients/word-index-logger.js +44 -0
- package/dist/clients/word-index.js +612 -68
- package/dist/clients/workspace-topology.js +100 -7
- package/dist/index.js +65923 -47161
- package/dist/mcp/analyze-cli.js +185 -29
- package/dist/mcp/cli.js +110 -0
- package/dist/mcp/server.js +272 -50
- package/dist/tools/ast-grep-search.js +93 -27
- package/dist/tools/lens-diagnostics.js +247 -41
- package/dist/tools/lsp-diagnostics.js +153 -40
- package/dist/tools/lsp-navigation.js +51 -31
- package/dist/tools/module-report.js +9 -0
- package/dist/tools/symbol-search.js +21 -2
- package/docs/agent-guide.md +247 -0
- package/docs/agent-tools.md +21 -10
- package/docs/analysisall.md +192 -0
- package/docs/api-ports-inventory.md +165 -0
- package/docs/ast-grep_rules_catalog.md +5 -2
- package/docs/astplayground.md +3 -4
- package/docs/audit1.md +5 -5
- package/docs/durable-store-audit-1202.md +32 -0
- package/docs/environment-variables.md +197 -11
- package/docs/fable.md +27 -0
- package/docs/features.md +35 -5
- package/docs/globalconfig.md +101 -5
- package/docs/language-coverage.md +6 -5
- package/docs/lsp-capability-matrix.md +2 -2
- package/docs/mcp.md +54 -20
- package/docs/servercapabilities.md +7 -4
- package/docs/settings.md +192 -0
- package/docs/tools.md +18 -1
- package/docs/tree-sitter_rules_catalog.md +11 -31
- package/docs/usage.md +31 -0
- package/package.json +139 -138
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-js-test.yml +1 -0
- package/rules/ast-grep-rules/rule-tests/hardcoded-url-test.yml +2 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-case-declarations-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-js-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-javascript-url-test.yml +10 -0
- package/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-equality-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-js-test.yml +3 -0
- package/rules/ast-grep-rules/rule-tests/strict-inequality-test.yml +4 -0
- package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +22 -4
- package/rules/ast-grep-rules/rules/hardcoded-url.yml +22 -4
- package/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +4 -3
- package/rules/ast-grep-rules/rules/no-case-declarations.yml +4 -3
- package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +10 -0
- package/rules/ast-grep-rules/rules/no-console-except-error.yml +10 -0
- package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +42 -6
- package/rules/ast-grep-rules/rules/no-javascript-url.yml +42 -6
- package/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/rules/ast-grep-rules/rules/strict-equality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-equality.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality-js.yml +14 -1
- package/rules/ast-grep-rules/rules/strict-inequality.yml +14 -1
- package/rules/tree-sitter-queries/cpp/no-memset-sensitive-data.yml +9 -3
- package/rules/tree-sitter-queries/cpp/no-scoped-lock-without-args.yml +6 -6
- package/rules/tree-sitter-queries/css/calc-spacing.yml +8 -6
- package/rules/tree-sitter-queries/go/go-mutex-copy.yml +21 -18
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +3 -3
- package/rules/tree-sitter-queries/java/infinite-loop-java.yml +3 -3
- package/rules/tree-sitter-queries/java/junit-call-super.yml +6 -4
- package/rules/tree-sitter-queries/java/no-double-checked-locking.yml +10 -8
- package/rules/tree-sitter-queries/java/no-field-shadowing.yml +1 -1
- package/rules/tree-sitter-queries/java/prepared-statement-valid-indices.yml +2 -3
- package/rules/tree-sitter-queries/java/resources-closed.yml +2 -2
- package/rules/tree-sitter-queries/java/springboot-default-package.yml +6 -3
- package/rules/tree-sitter-queries/java/switch-fall-through.yml +6 -4
- package/rules/tree-sitter-queries/java/switch-non-case-labels.yml +4 -3
- package/rules/tree-sitter-queries/java/tests-include-assertions.yml +6 -5
- package/rules/tree-sitter-queries/javascript/switch-case-termination-js.yml +5 -8
- package/rules/tree-sitter-queries/javascript/switch-non-case-labels.yml +3 -2
- package/rules/tree-sitter-queries/kotlin/prepared-statement-indices.yml +9 -5
- package/rules/tree-sitter-queries/php/this-in-static-context.yml +5 -5
- package/rules/tree-sitter-queries/python/exit-signature-check.yml +2 -9
- package/rules/tree-sitter-queries/python/in-operator-unsupported.yml +3 -4
- package/rules/tree-sitter-queries/python/no-super-torchscript.yml +4 -8
- package/rules/tree-sitter-queries/python/notimplemented-boolean-context.yml +1 -5
- package/rules/tree-sitter-queries/python/python-empty-except.yml +1 -1
- package/rules/tree-sitter-queries/python/return-in-init.yml +0 -2
- package/rules/tree-sitter-queries/python/yield-return-outside-function.yml +0 -3
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +1 -12
- package/rules/tree-sitter-queries/ruby/ruby-eval.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +4 -4
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +4 -11
- package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +1 -1
- package/rules/tree-sitter-queries/ruby/ruby-string-eval.yml +2 -3
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +4 -4
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +4 -3
- package/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/rules/tree-sitter-queries/typescript/console-statement.yml +11 -0
- package/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +4 -9
- package/rules/tree-sitter-queries/typescript/empty-switch-case.yml +13 -9
- package/rules/tree-sitter-queries/typescript/infinite-loop.yml +2 -2
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +10 -1
- package/rules/tree-sitter-queries/typescript/switch-case-termination.yml +5 -8
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +7 -10
- package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +4 -2
- package/rules/typos/_typos.toml +17 -0
- package/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/skills/pi-lens-write-tree-sitter-rule/SKILL.md +5 -0
- package/dist/clients/ast-grep-parser.js +0 -86
- package/dist/clients/config-enabled-shape.js +0 -39
- package/dist/clients/lsp/interactive-install.js +0 -367
- package/dist/clients/lsp/lsp-index.js +0 -10
- package/dist/clients/lsp/server-strategies.js +0 -5
- package/dist/clients/project-metadata.js +0 -690
- package/dist/clients/source-groups.js +0 -96
- package/dist/clients/types.js +0 -11
- package/docs/env_variables.md +0 -10
- package/rules/tree-sitter-queries/abap-disabled/delete-where.yml +0 -46
- package/rules/tree-sitter-queries/cpp/no-confused-move-forward.yml +0 -59
- package/rules/tree-sitter-queries/cpp/noexcept-functions.yml +0 -58
- package/rules/tree-sitter-queries/java/mockito-initialized.yml +0 -66
- package/rules/tree-sitter-queries/java/no-wait-notify-on-thread.yml +0 -58
- package/rules/tree-sitter-queries/java/short-circuit-logic.yml +0 -57
- package/rules/tree-sitter-queries/java/spring-session-attributes-setcomplete.yml +0 -75
- package/rules/tree-sitter-queries/plsql-disabled/delete-update-where.yml +0 -56
- package/rules/tree-sitter-queries/plsql-disabled/end-loop-semicolon.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/fetch-bulk-collect-limit.yml +0 -47
- package/rules/tree-sitter-queries/plsql-disabled/forallsave-exceptions.yml +0 -51
- package/rules/tree-sitter-queries/plsql-disabled/nchar-nvarchar2-bytes.yml +0 -54
- package/rules/tree-sitter-queries/plsql-disabled/not-null-initialization.yml +0 -59
- package/rules/tree-sitter-queries/plsql-disabled/raise-application-error-codes.yml +0 -50
- package/rules/tree-sitter-queries/typescript-disabled/ts-path-traversal.yml +0 -71
|
@@ -2,8 +2,33 @@ import { stat } from "node:fs/promises";
|
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { visibleWidth } from "./deps/pi-tui.js";
|
|
5
|
+
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
5
6
|
import { fitLine } from "./tui-fit.js";
|
|
6
7
|
import { WriteOrderingGuard } from "./write-ordering-guard.js";
|
|
8
|
+
/**
|
|
9
|
+
* Canonical key for the `files` map (and `diagnosticsWriteGuard`) — #1020.
|
|
10
|
+
*
|
|
11
|
+
* The SAME file reaches this module under DIFFERENT path forms in one session:
|
|
12
|
+
* forward-slash (`C:/…/x.ts`) from the LSP client + cascade fold via
|
|
13
|
+
* `normalizeFilePath`, and backslash (`C:\…\x.ts`) from mode=full's reconcile
|
|
14
|
+
* writing `result.filePath` and from `path.resolve`/event inputs on Windows.
|
|
15
|
+
* Keyed raw, those coexisted as two entries: `mode=full` re-keyed on read and
|
|
16
|
+
* the clean entry hid the stale one, but `mode=all`'s `formatAllMode` reads the
|
|
17
|
+
* summaries verbatim and rendered the stale `blocking:1` as a 🔴 (#1020) — a
|
|
18
|
+
* resolved state that replayed as still-broken on every `mode=all`.
|
|
19
|
+
*
|
|
20
|
+
* `normalizeEphemeralMapKey` (slash-fold + win32-lowercase, NO filesystem I/O)
|
|
21
|
+
* is chosen over `normalizeMapKey`/`normalizeFilePath`, which call
|
|
22
|
+
* `realpathSync.native()` — real disk I/O on EVERY diagnostic/runner/formatter
|
|
23
|
+
* write, far too heavy for this hot path. The key only needs to be a stable
|
|
24
|
+
* syntactic fold that collapses `\`↔`/` and Windows drive-letter case, which
|
|
25
|
+
* this does; on-disk canonical casing is irrelevant for merely deduplicating a
|
|
26
|
+
* process-local footer cache. The human-readable path is preserved separately
|
|
27
|
+
* on the record's `filePath` (see `toDisplayPath`) for rendering/summaries.
|
|
28
|
+
*/
|
|
29
|
+
function fileMapKey(filePath) {
|
|
30
|
+
return normalizeEphemeralMapKey(filePath);
|
|
31
|
+
}
|
|
7
32
|
/**
|
|
8
33
|
* A diagnostic is "blocking" when pi-lens classifies it as a hard stop
|
|
9
34
|
* (`semantic === "blocking"`). Falls back to severity for sources that
|
|
@@ -32,7 +57,42 @@ let requestRenderFn = null;
|
|
|
32
57
|
* `clients/runtime-tool-result.ts:nextWriteIndex`).
|
|
33
58
|
*/
|
|
34
59
|
const diagnosticsWriteGuard = new WriteOrderingGuard();
|
|
60
|
+
/**
|
|
61
|
+
* Runner completions also mutate the shared file record: they mark the
|
|
62
|
+
* diagnostics snapshot pending. Keep that mutation in the same per-file order
|
|
63
|
+
* as the final diagnostic replacement, or an older pipeline can set a newer
|
|
64
|
+
* confirmed-clean record back to `(pending)` (#1198).
|
|
65
|
+
*/
|
|
66
|
+
const runnerWriteGuard = new WriteOrderingGuard();
|
|
35
67
|
const MAX_STORED_DIAGNOSTICS_PER_FILE = 12;
|
|
68
|
+
const MAX_INACTIVE_FILE_RECORDS = 1024;
|
|
69
|
+
const ACTIVE_FILE_IDLE_MS = 30 * 60_000;
|
|
70
|
+
const MAX_LSP_SERVER_RECORDS = 128;
|
|
71
|
+
// Pruning is a cold-size-boundary operation. Do not walk the whole file map
|
|
72
|
+
// for every record in a large diagnostics reconciliation; the full-scan path
|
|
73
|
+
// can legitimately create thousands of records in one synchronous batch.
|
|
74
|
+
let nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
|
|
75
|
+
function pruneInactiveFileRecords(now = Date.now()) {
|
|
76
|
+
if (files.size <= MAX_INACTIVE_FILE_RECORDS)
|
|
77
|
+
return;
|
|
78
|
+
const victims = [...files.entries()]
|
|
79
|
+
.filter(([, rec]) => now - rec.touchedAt > ACTIVE_FILE_IDLE_MS &&
|
|
80
|
+
!hasLiveDiagnostic(rec))
|
|
81
|
+
.sort(([, a], [, b]) => a.touchedAt - b.touchedAt);
|
|
82
|
+
for (const [key] of victims) {
|
|
83
|
+
if (files.size <= MAX_INACTIVE_FILE_RECORDS)
|
|
84
|
+
break;
|
|
85
|
+
files.delete(key);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function maybePruneInactiveFileRecords() {
|
|
89
|
+
if (files.size < nextInactivePruneSize)
|
|
90
|
+
return;
|
|
91
|
+
pruneInactiveFileRecords();
|
|
92
|
+
// A live-heavy map may remain above the soft bound. Do not rescan it for
|
|
93
|
+
// every subsequent file; the next lifecycle starts with a fresh state map.
|
|
94
|
+
nextInactivePruneSize = Number.POSITIVE_INFINITY;
|
|
95
|
+
}
|
|
36
96
|
// ── Public API ────────────────────────────────────────────────────────────────
|
|
37
97
|
export function setRenderCallback(fn) {
|
|
38
98
|
requestRenderFn = fn;
|
|
@@ -43,8 +103,15 @@ export function clearWidgetState() {
|
|
|
43
103
|
sessionLanguages = [];
|
|
44
104
|
requestRenderFn = null;
|
|
45
105
|
diagnosticsWriteGuard.clear();
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
runnerWriteGuard.clear();
|
|
107
|
+
nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
|
|
108
|
+
}
|
|
109
|
+
// v1 → v2 (#1186): per-entry `WidgetDiagnostic.observedAt`. v2 is a SUPERSET of
|
|
110
|
+
// v1 (the field is additive/optional), so `importWidgetState` accepts either and
|
|
111
|
+
// migrates a v1 record by inheriting each entry's `observedAt` from the record's
|
|
112
|
+
// `touchedAt`. A v1 file must never be rejected (that would silently drop resume
|
|
113
|
+
// diagnostics) nor crash.
|
|
114
|
+
export const WIDGET_STATE_VERSION = 2;
|
|
48
115
|
/**
|
|
49
116
|
* Snapshot the per-file widget diagnostics for persistence (#190). Excludes
|
|
50
117
|
* `lspServers` — those are process-bound (servers re-spawn fresh on the next
|
|
@@ -71,9 +138,29 @@ export function exportWidgetState() {
|
|
|
71
138
|
* Replaces the in-memory `files` map; ignores snapshots from a different
|
|
72
139
|
* version. Triggers a re-render if a callback is registered.
|
|
73
140
|
*/
|
|
141
|
+
/**
|
|
142
|
+
* #1186 v1→v2 migration: stamp each entry that lacks a per-entry `observedAt`
|
|
143
|
+
* with the record's `touchedAt` (the single stamp the whole record shared under
|
|
144
|
+
* v1). Non-mutating; entries that already carry a stamp (a v2 record) pass
|
|
145
|
+
* through untouched.
|
|
146
|
+
*/
|
|
147
|
+
function migrateEntryStamps(entries, recordTouchedAt) {
|
|
148
|
+
return (entries ?? []).map((d) => d.observedAt == null ? { ...d, observedAt: recordTouchedAt } : d);
|
|
149
|
+
}
|
|
74
150
|
export function importWidgetState(state) {
|
|
75
|
-
|
|
151
|
+
// Accept any known-or-older version and migrate (#1186): reject a missing
|
|
152
|
+
// snapshot, a missing/non-numeric `version` (NaN/undefined/null — the
|
|
153
|
+
// pre-#1186 guard `version !== WIDGET_STATE_VERSION` rejected these, and
|
|
154
|
+
// loosening that would silently admit a malformed/foreign snapshot), or a
|
|
155
|
+
// FUTURE version this build can't understand. Rejecting a v1
|
|
156
|
+
// (pre-per-entry-stamp) file, by contrast, would silently drop all resume
|
|
157
|
+
// diagnostics — so v1..current are accepted and migrated.
|
|
158
|
+
if (!state ||
|
|
159
|
+
typeof state.version !== "number" ||
|
|
160
|
+
state.version < 1 ||
|
|
161
|
+
state.version > WIDGET_STATE_VERSION) {
|
|
76
162
|
return false;
|
|
163
|
+
}
|
|
77
164
|
files.clear();
|
|
78
165
|
// A resumed session's writeIndex counter starts fresh (#190 rehydration is
|
|
79
166
|
// process-bound like lspServers, see the export above) — any ordering
|
|
@@ -81,22 +168,39 @@ export function importWidgetState(state) {
|
|
|
81
168
|
// drop them rather than risk a legitimate post-resume write being read as
|
|
82
169
|
// "superseded" against a stale token.
|
|
83
170
|
diagnosticsWriteGuard.clear();
|
|
171
|
+
runnerWriteGuard.clear();
|
|
84
172
|
for (const f of state.files ?? []) {
|
|
85
|
-
|
|
173
|
+
// Fold persisted keys through the same normalizer as live writes (#1020),
|
|
174
|
+
// or a persisted forward-slash key stays split from a fresh backslash key
|
|
175
|
+
// across a resumed session — a primary repro condition. Keep a readable
|
|
176
|
+
// display path on the record.
|
|
177
|
+
// #1186 migration: a v1 record's entries have no per-entry `observedAt`.
|
|
178
|
+
// Inherit the record's `touchedAt` (a safe, over-conservative default —
|
|
179
|
+
// the whole record shared that one stamp before), so the per-entry stale
|
|
180
|
+
// gate has a concrete observation time and never treats `undefined` as
|
|
181
|
+
// epoch-0 (which would drop every migrated entry on the first sweep).
|
|
182
|
+
const recordTouchedAt = f.touchedAt ?? Date.now();
|
|
183
|
+
files.set(fileMapKey(f.filePath), {
|
|
86
184
|
filePath: f.filePath,
|
|
87
185
|
runners: new Map(f.runners ?? []),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
186
|
+
// Failure entries do NOT survive a session restore (#1348 review):
|
|
187
|
+
// a fmt-failed marker is live advice about THIS session's last
|
|
188
|
+
// attempt; rehydrating one from a snapshot shows a stale failure the
|
|
189
|
+
// current session never observed (and same-mtime fixes would never
|
|
190
|
+
// clear it). Successes rehydrate as before.
|
|
191
|
+
formatters: new Map((f.formatters ?? []).filter(([, outcome]) => outcome?.success !== false)),
|
|
192
|
+
diagnostics: migrateEntryStamps(f.diagnostics, recordTouchedAt),
|
|
193
|
+
allDiagnostics: migrateEntryStamps(f.allDiagnostics, recordTouchedAt),
|
|
91
194
|
diagnosticCounts: f.diagnosticCounts ?? {
|
|
92
195
|
blocking: 0,
|
|
93
196
|
errors: 0,
|
|
94
197
|
warnings: 0,
|
|
95
198
|
},
|
|
96
199
|
hasFinalDiagnosticsSnapshot: f.hasFinalDiagnosticsSnapshot ?? false,
|
|
97
|
-
touchedAt:
|
|
200
|
+
touchedAt: recordTouchedAt,
|
|
98
201
|
});
|
|
99
202
|
}
|
|
203
|
+
pruneInactiveFileRecords();
|
|
100
204
|
sessionLanguages = state.sessionLanguages ?? [];
|
|
101
205
|
requestRenderFn?.();
|
|
102
206
|
return true;
|
|
@@ -129,15 +233,23 @@ export function recordFormatter(filePath, formatter, changed, success) {
|
|
|
129
233
|
const rec = getOrCreate(filePath);
|
|
130
234
|
rec.formatters.set(formatter, { changed, success });
|
|
131
235
|
rec.touchedAt = Date.now();
|
|
132
|
-
files.set(filePath, rec);
|
|
236
|
+
files.set(fileMapKey(filePath), rec);
|
|
133
237
|
requestRender();
|
|
134
238
|
}
|
|
135
|
-
export function recordRunner(filePath, runnerId, status, diagnosticCount, durationMs) {
|
|
239
|
+
export function recordRunner(filePath, runnerId, status, diagnosticCount, durationMs, writeIndex) {
|
|
240
|
+
const key = fileMapKey(filePath);
|
|
241
|
+
if (!runnerWriteGuard.shouldWrite(key, writeIndex))
|
|
242
|
+
return;
|
|
243
|
+
// Advance the final-diagnostics guard too. A runner completion is part of
|
|
244
|
+
// this pipeline's ordered write, even though its immediate effect is only to
|
|
245
|
+
// mark the record pending. This prevents an older final replacement from
|
|
246
|
+
// landing after a newer runner state (and vice versa).
|
|
247
|
+
diagnosticsWriteGuard.shouldWrite(key, writeIndex);
|
|
136
248
|
const rec = getOrCreate(filePath);
|
|
137
249
|
rec.runners.set(runnerId, { status, count: diagnosticCount, durationMs });
|
|
138
250
|
rec.hasFinalDiagnosticsSnapshot = false;
|
|
139
251
|
rec.touchedAt = Date.now();
|
|
140
|
-
files.set(filePath, rec);
|
|
252
|
+
files.set(fileMapKey(filePath), rec);
|
|
141
253
|
requestRender();
|
|
142
254
|
}
|
|
143
255
|
/**
|
|
@@ -150,7 +262,16 @@ export function recordRunner(filePath, runnerId, status, diagnosticCount, durati
|
|
|
150
262
|
function toSingleLineMessage(message) {
|
|
151
263
|
return (message ?? "").replace(/\s+/g, " ").trim();
|
|
152
264
|
}
|
|
153
|
-
export function recordDiagnostics(filePath, diagnostics, writeIndex
|
|
265
|
+
export function recordDiagnostics(filePath, diagnostics, writeIndex,
|
|
266
|
+
// #1093: when the truth was OBSERVED, not when it's being written. Defaults
|
|
267
|
+
// to `Date.now()` for the per-edit/live path (observed now). A reconcile
|
|
268
|
+
// replaying a CACHED view (e.g. the workspace-diagnostics cache-hit branch in
|
|
269
|
+
// `tools/lsp-diagnostics.ts`) must pass the cache entry's own scan timestamp
|
|
270
|
+
// here — otherwise a repeat "fresh check" that merely re-serves a stale
|
|
271
|
+
// cached view keeps bumping `touchedAt` to now(), permanently disarming
|
|
272
|
+
// `reconcileStaleWidgetFiles`'s `mtimeMs > touchedAt` gate so a resolved
|
|
273
|
+
// finding renders forever (the #1092 touchedAt-re-arming defect).
|
|
274
|
+
observedAt) {
|
|
154
275
|
// Drop a write that's superseded by a later same-turn edit to this file
|
|
155
276
|
// whose pipeline finished first (same race class as #555). No cache write,
|
|
156
277
|
// no count/timestamp update, no render trigger — the recorded state must
|
|
@@ -158,11 +279,27 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
|
|
|
158
279
|
// the `clients/mcp/analyze.ts` on-demand call site, which has no per-edit
|
|
159
280
|
// ordering token) always proceeds, same as version-less LSP servers in the
|
|
160
281
|
// #555 guard.
|
|
161
|
-
|
|
282
|
+
const key = fileMapKey(filePath);
|
|
283
|
+
if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
|
|
162
284
|
return;
|
|
163
|
-
|
|
285
|
+
// Keep runner state ordered with the final diagnostic replacement. The
|
|
286
|
+
// guards are deliberately advanced in both directions because either verb
|
|
287
|
+
// may be the first completion from a pipeline.
|
|
288
|
+
runnerWriteGuard.shouldWrite(key, writeIndex);
|
|
289
|
+
// Resolve the observation time ONCE (#1186): every incoming entry is stamped
|
|
290
|
+
// with it, and it also seeds the record's `touchedAt`. A fresh write (no
|
|
291
|
+
// `observedAt`) is observed now.
|
|
292
|
+
const observedTs = observedAt ?? Date.now();
|
|
293
|
+
const rec = getOrCreate(filePath, key);
|
|
294
|
+
commitDiagnostics(rec, filePath, normalizeDiagnostics(filePath, diagnostics, observedTs), observedTs, key);
|
|
295
|
+
}
|
|
296
|
+
/** Map the raw diagnostic shape callers pass into stored {@link WidgetDiagnostic}s.
|
|
297
|
+
* Every produced entry is stamped with `observedTs` (#1186) — the time THIS batch
|
|
298
|
+
* of diagnostics was observed — so the per-entry stale gate can later drop just
|
|
299
|
+
* the entries older than the file's mtime rather than the whole record. */
|
|
300
|
+
function normalizeDiagnostics(filePath, diagnostics, observedTs) {
|
|
164
301
|
const base = pathToFileURL(filePath).href;
|
|
165
|
-
|
|
302
|
+
return diagnostics.map((d) => {
|
|
166
303
|
const rule = d.rule ?? d.id;
|
|
167
304
|
const uri = d.line != null
|
|
168
305
|
? `${base}#L${d.line}${d.column != null ? `:${d.column}` : ""}`
|
|
@@ -176,12 +313,35 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
|
|
|
176
313
|
rule,
|
|
177
314
|
tool: d.tool,
|
|
178
315
|
uri,
|
|
316
|
+
observedAt: observedTs,
|
|
179
317
|
};
|
|
180
318
|
});
|
|
319
|
+
}
|
|
320
|
+
/** Store `normalized` as the record's complete diagnostic set: recompute counts,
|
|
321
|
+
* cap the display list, stamp `touchedAt` (at `observedAt` when given, else now
|
|
322
|
+
* — #1093), persist, and re-render. The caller decides what `normalized`
|
|
323
|
+
* contains (a full replace, or a merge — see `reconcileCascadeNeighborLspErrors`). */
|
|
324
|
+
function commitDiagnostics(rec, filePath, normalized, observedAt, key = fileMapKey(filePath)) {
|
|
325
|
+
rec.diagnosticCounts = countDiagnostics(normalized);
|
|
326
|
+
rec.diagnostics = capStoredDiagnostics(normalized);
|
|
327
|
+
rec.allDiagnostics = normalized;
|
|
328
|
+
rec.hasFinalDiagnosticsSnapshot = true;
|
|
329
|
+
// Record-level `touchedAt` is the FRESHEST per-entry observation in the merged
|
|
330
|
+
// set (#1186) — drives render recency and the empty-record stale gate. On a
|
|
331
|
+
// merge (`reconcileCascadeNeighborLspErrors`) this is the newest of the
|
|
332
|
+
// preserved + incoming entries, not the (possibly aging) incoming stamp, so a
|
|
333
|
+
// record holding a fresh preserved entry doesn't sort/gate as stale. Empty set
|
|
334
|
+
// falls back to the passed `observedAt` (or now).
|
|
335
|
+
rec.touchedAt = freshestObservation(normalized, observedAt ?? Date.now());
|
|
336
|
+
files.set(key, rec);
|
|
337
|
+
requestRender();
|
|
338
|
+
}
|
|
339
|
+
/** Recompute the {blocking, errors, warnings} tally for a diagnostic set. */
|
|
340
|
+
function countDiagnostics(diags) {
|
|
181
341
|
let blocking = 0;
|
|
182
342
|
let errors = 0;
|
|
183
343
|
let warnings = 0;
|
|
184
|
-
for (const diagnostic of
|
|
344
|
+
for (const diagnostic of diags) {
|
|
185
345
|
if (isBlocking(diagnostic))
|
|
186
346
|
blocking++;
|
|
187
347
|
if (diagnostic.severity === "error")
|
|
@@ -189,13 +349,69 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
|
|
|
189
349
|
else if (diagnostic.severity === "warning")
|
|
190
350
|
warnings++;
|
|
191
351
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
352
|
+
return { blocking, errors, warnings };
|
|
353
|
+
}
|
|
354
|
+
/** The newest per-entry `observedAt` in `diags`, or `fallback` when empty (or no
|
|
355
|
+
* entry carries a stamp). Used as the record-level `touchedAt` (#1186). */
|
|
356
|
+
function freshestObservation(diags, fallback) {
|
|
357
|
+
let newest;
|
|
358
|
+
for (const d of diags) {
|
|
359
|
+
if (d.observedAt != null && (newest === undefined || d.observedAt > newest)) {
|
|
360
|
+
newest = d.observedAt;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return newest ?? fallback;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* A stored diagnostic that came from a language SERVER (tsserver, pyright, …)
|
|
367
|
+
* AND is an error/blocking finding. `convertLspDiagnostics` tags every
|
|
368
|
+
* language-server diagnostic with `tool: "lsp"` and `retagAuxiliaryDiagnostics`
|
|
369
|
+
* re-tags auxiliary-LSP findings (opengrep/ast-grep/zizmor/typos) to their real
|
|
370
|
+
* tool id, so `tool === "lsp"` uniquely identifies a genuine language-server
|
|
371
|
+
* entry. Used by the cascade merge below to decide which existing entries an
|
|
372
|
+
* errors-only LSP re-check is entitled to replace.
|
|
373
|
+
*/
|
|
374
|
+
function isLspErrorEntry(d) {
|
|
375
|
+
return d.tool === "lsp" && (d.semantic === "blocking" || d.severity === "error");
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Reconcile a CONFIRMED cascade neighbor re-check (#1093) into the footer,
|
|
379
|
+
* MERGING rather than whole-replacing (unlike `recordDiagnostics`).
|
|
380
|
+
*
|
|
381
|
+
* The cascade only re-checks a neighbor through its LANGUAGE SERVER, and only
|
|
382
|
+
* for ERRORS (`severity === 1`; see `clients/dispatch/integration.ts`). A plain
|
|
383
|
+
* full-replace would therefore erase the neighbor's live findings from OTHER
|
|
384
|
+
* sources that the cascade never re-examined — biome/ruff/ast-grep runner
|
|
385
|
+
* findings, and even the language server's own WARNINGS — turning a
|
|
386
|
+
* cross-file-error re-check into a silent false-clean for everything else
|
|
387
|
+
* (#533). So we replace ONLY the existing LSP-error entries
|
|
388
|
+
* (`isLspErrorEntry`) with the cascade's fresh LSP errors and preserve
|
|
389
|
+
* everything else verbatim. A stale LSP warning or biome finding therefore
|
|
390
|
+
* survives an errors-only cascade — correct, because this check never looked at
|
|
391
|
+
* it; it self-corrects on the next per-edit dispatch or a `lens_diagnostics`
|
|
392
|
+
* scan (which DO re-examine every source).
|
|
393
|
+
*
|
|
394
|
+
* Only ever call this for a CONFIRMED result (a valid passive snapshot or a
|
|
395
|
+
* completed, NON-inconclusive active touch — #571). `writeIndex` and
|
|
396
|
+
* `observedAt` behave exactly as in `recordDiagnostics`.
|
|
397
|
+
*/
|
|
398
|
+
export function reconcileCascadeNeighborLspErrors(filePath, lspErrorDiagnostics, writeIndex, observedAt) {
|
|
399
|
+
const key = fileMapKey(filePath);
|
|
400
|
+
if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
|
|
401
|
+
return;
|
|
402
|
+
runnerWriteGuard.shouldWrite(key, writeIndex);
|
|
403
|
+
// #1186: the INCOMING LSP errors are stamped at THIS observation time
|
|
404
|
+
// (`observedAt`, e.g. an aging passive snapshot's `entry.ts`, or now for a
|
|
405
|
+
// fresh active touch). The PRESERVED entries keep their OWN prior per-entry
|
|
406
|
+
// `observedAt` — a fresh per-edit finding preserved through this errors-only
|
|
407
|
+
// merge is NOT re-aged to the incoming stamp. That per-entry split is exactly
|
|
408
|
+
// what lets `reconcileStaleWidgetFiles` drop the stale incoming entry while
|
|
409
|
+
// keeping the newer preserved one, instead of dropping the whole record.
|
|
410
|
+
const observedTs = observedAt ?? Date.now();
|
|
411
|
+
const rec = getOrCreate(filePath);
|
|
412
|
+
const incoming = normalizeDiagnostics(filePath, lspErrorDiagnostics, observedTs);
|
|
413
|
+
const preserved = rec.allDiagnostics.filter((d) => !isLspErrorEntry(d));
|
|
414
|
+
commitDiagnostics(rec, filePath, [...incoming, ...preserved], observedTs);
|
|
199
415
|
}
|
|
200
416
|
/**
|
|
201
417
|
* Reconcile a diagnostics result obtained OUTSIDE the per-edit dispatch
|
|
@@ -224,11 +440,18 @@ export function recordDiagnostics(filePath, diagnostics, writeIndex) {
|
|
|
224
440
|
* write for the same file — an omitted `writeIndex` always proceeds (same
|
|
225
441
|
* version-less fallback `recordDiagnostics` already documents), which is
|
|
226
442
|
* only safe for callers with no ordering token to give (e.g. tests).
|
|
443
|
+
*
|
|
444
|
+
* `observedAt` (#1093) is the wall-clock time the diagnostics were actually
|
|
445
|
+
* OBSERVED — pass it whenever the reconciled result is a replay of an older
|
|
446
|
+
* CACHED observation (the workspace-diagnostics cache-hit branch), so
|
|
447
|
+
* `touchedAt` records when the truth was seen, not when it was written. Omit
|
|
448
|
+
* it for genuinely fresh observations (a just-completed touch/scan), which are
|
|
449
|
+
* observed now.
|
|
227
450
|
*/
|
|
228
|
-
export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex) {
|
|
451
|
+
export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex, observedAt) {
|
|
229
452
|
if (!confirmed)
|
|
230
453
|
return;
|
|
231
|
-
recordDiagnostics(filePath, diagnostics, writeIndex);
|
|
454
|
+
recordDiagnostics(filePath, diagnostics, writeIndex, observedAt);
|
|
232
455
|
}
|
|
233
456
|
/**
|
|
234
457
|
* Drop widget entries whose file changed on disk after pi-lens last recorded
|
|
@@ -242,22 +465,60 @@ export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, write
|
|
|
242
465
|
*/
|
|
243
466
|
export async function reconcileStaleWidgetFiles() {
|
|
244
467
|
const entries = [...files.entries()];
|
|
245
|
-
const
|
|
468
|
+
const verdicts = await Promise.all(
|
|
469
|
+
// `mapKey` is the normalized `files` key (used for deletion); stat the
|
|
470
|
+
// record's real display path, not the lowercased key (#1020).
|
|
471
|
+
entries.map(async ([mapKey, rec]) => {
|
|
472
|
+
let mtimeMs;
|
|
246
473
|
try {
|
|
247
|
-
|
|
248
|
-
// +1ms tolerance: a freshly-recorded file has touchedAt >= mtime.
|
|
249
|
-
return st.mtimeMs > rec.touchedAt + 1 ? filePath : undefined;
|
|
474
|
+
mtimeMs = (await stat(rec.filePath)).mtimeMs;
|
|
250
475
|
}
|
|
251
476
|
catch {
|
|
252
|
-
return
|
|
477
|
+
return { mapKey, action: "drop" }; // deleted / unreadable → drop
|
|
478
|
+
}
|
|
479
|
+
// A clean record (no findings) has no per-entry stamps to consult —
|
|
480
|
+
// gate it on the record's own `touchedAt` exactly as before, so a ✓
|
|
481
|
+
// entry for a file that changed on disk still drops.
|
|
482
|
+
if (rec.allDiagnostics.length === 0) {
|
|
483
|
+
return mtimeMs > rec.touchedAt + 1
|
|
484
|
+
? { mapKey, action: "drop" }
|
|
485
|
+
: { mapKey, action: "keep" };
|
|
486
|
+
}
|
|
487
|
+
// #1186 per-ENTRY gate: drop only the entries observed BEFORE the file's
|
|
488
|
+
// current mtime; keep the rest. A merged record can hold a fresh
|
|
489
|
+
// preserved entry beside an entry replayed from an aging snapshot, so a
|
|
490
|
+
// per-RECORD gate over-cleared the whole record (the residual documented
|
|
491
|
+
// at dispatch/integration.ts). A missing per-entry stamp (a migrated
|
|
492
|
+
// pre-#1186 record) inherits the record's `touchedAt`. +1ms tolerance:
|
|
493
|
+
// a freshly-recorded file has observedAt >= mtime.
|
|
494
|
+
const survivors = rec.allDiagnostics.filter((d) => !(mtimeMs > (d.observedAt ?? rec.touchedAt) + 1));
|
|
495
|
+
if (survivors.length === rec.allDiagnostics.length) {
|
|
496
|
+
return { mapKey, action: "keep" }; // nothing stale
|
|
497
|
+
}
|
|
498
|
+
if (survivors.length === 0) {
|
|
499
|
+
return { mapKey, action: "drop" }; // every entry stale → drop record
|
|
253
500
|
}
|
|
501
|
+
return { mapKey, action: "prune", survivors };
|
|
254
502
|
}));
|
|
255
503
|
let dropped = 0;
|
|
256
|
-
for (const
|
|
257
|
-
if (
|
|
258
|
-
|
|
504
|
+
for (const v of verdicts) {
|
|
505
|
+
if (v.action === "keep")
|
|
506
|
+
continue;
|
|
507
|
+
if (v.action === "drop") {
|
|
508
|
+
files.delete(v.mapKey);
|
|
259
509
|
dropped += 1;
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
// prune: the file changed and shed its stale entries but retains fresher
|
|
513
|
+
// ones — keep the record, recompute counts/cap from the survivors, and
|
|
514
|
+
// still count it as a changed file so the agent is told to rescan.
|
|
515
|
+
const rec = files.get(v.mapKey);
|
|
516
|
+
if (rec) {
|
|
517
|
+
rec.allDiagnostics = v.survivors;
|
|
518
|
+
rec.diagnostics = capStoredDiagnostics(v.survivors);
|
|
519
|
+
rec.diagnosticCounts = countDiagnostics(v.survivors);
|
|
260
520
|
}
|
|
521
|
+
dropped += 1;
|
|
261
522
|
}
|
|
262
523
|
if (dropped > 0)
|
|
263
524
|
requestRenderFn?.();
|
|
@@ -313,12 +574,14 @@ export function getFileDiagnosticSummaries() {
|
|
|
313
574
|
* been recorded (caller must not confuse "never seen" with "seen and clean";
|
|
314
575
|
* an explicit `[]` from `recordDiagnostics` is a real empty array here).
|
|
315
576
|
*
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
577
|
+
* The `files` map key is normalized through `fileMapKey` (#1020), so any path
|
|
578
|
+
* form of the same file — forward-slash, backslash, or a different Windows
|
|
579
|
+
* drive-letter case — resolves to the same record. This read-side fold MUST
|
|
580
|
+
* stay identical to the write-side fold, or a file recorded under one form
|
|
581
|
+
* would silently read as `undefined` under another (e.g. via bus-publish).
|
|
319
582
|
*/
|
|
320
583
|
export function getFileDiagnostics(filePath) {
|
|
321
|
-
const rec = files.get(filePath);
|
|
584
|
+
const rec = files.get(fileMapKey(filePath));
|
|
322
585
|
if (!rec)
|
|
323
586
|
return undefined;
|
|
324
587
|
return rec.allDiagnostics.map((d) => ({ ...d }));
|
|
@@ -338,13 +601,20 @@ export const __testing = {
|
|
|
338
601
|
},
|
|
339
602
|
};
|
|
340
603
|
export function recordLsp(serverId, root, status, durationMs) {
|
|
341
|
-
const
|
|
604
|
+
const normalizedRoot = normalizeMapKey(root);
|
|
605
|
+
const key = `${serverId}@${normalizedRoot}`;
|
|
342
606
|
const mapped = status === "spawn_start"
|
|
343
607
|
? "spawning"
|
|
344
608
|
: status === "spawn_success"
|
|
345
609
|
? "ready"
|
|
346
610
|
: "failed";
|
|
347
611
|
lspServers.set(key, { serverId, root, status: mapped, durationMs });
|
|
612
|
+
while (lspServers.size > MAX_LSP_SERVER_RECORDS) {
|
|
613
|
+
const oldest = lspServers.keys().next().value;
|
|
614
|
+
if (oldest === undefined)
|
|
615
|
+
break;
|
|
616
|
+
lspServers.delete(oldest);
|
|
617
|
+
}
|
|
348
618
|
requestRender();
|
|
349
619
|
}
|
|
350
620
|
// ── Render ────────────────────────────────────────────────────────────────────
|
|
@@ -454,11 +724,17 @@ function formatFileRowVertical(rec, theme) {
|
|
|
454
724
|
const blocking = rec.diagnosticCounts.blocking;
|
|
455
725
|
const errors = rec.diagnosticCounts.errors;
|
|
456
726
|
const warnings = rec.diagnosticCounts.warnings;
|
|
727
|
+
const formatterFailed = hasFailedFormatter(rec);
|
|
728
|
+
// Diagnostic severity outranks formatter failure (#1348 review): a file
|
|
729
|
+
// with blocking diagnostics shows the blocking dot even if a format also
|
|
730
|
+
// failed -- same precedence as the horizontal renderer.
|
|
457
731
|
const dot = blocking > 0
|
|
458
732
|
? red("●")
|
|
459
|
-
:
|
|
460
|
-
?
|
|
461
|
-
:
|
|
733
|
+
: formatterFailed
|
|
734
|
+
? red("x")
|
|
735
|
+
: warnings > 0 || errors > 0
|
|
736
|
+
? yellow("!")
|
|
737
|
+
: green("✓");
|
|
462
738
|
const runnerNames = [...rec.runners.entries()]
|
|
463
739
|
.filter(([, r]) => r.status !== "skipped")
|
|
464
740
|
.map(([id]) => id)
|
|
@@ -471,11 +747,15 @@ function formatFileRowVertical(rec, theme) {
|
|
|
471
747
|
? " " + yellow(`${warnings}W`)
|
|
472
748
|
: " " + dim("clean");
|
|
473
749
|
const changedFormatters = [...rec.formatters.entries()]
|
|
474
|
-
.filter(([, f]) => f.changed)
|
|
750
|
+
.filter(([, f]) => f.changed && f.success)
|
|
475
751
|
.map(([name]) => name);
|
|
476
|
-
const
|
|
477
|
-
|
|
478
|
-
|
|
752
|
+
const failedFormatters = [...rec.formatters.entries()]
|
|
753
|
+
.filter(([, f]) => !f.success)
|
|
754
|
+
.map(([name]) => name);
|
|
755
|
+
const formatMark = (failedFormatters.length > 0
|
|
756
|
+
? red(` fmt-failed:${failedFormatters.join(",")}`)
|
|
757
|
+
: "") +
|
|
758
|
+
(changedFormatters.length > 0 ? dim(` fmt:${changedFormatters.join(",")}`) : "");
|
|
479
759
|
return ` ${dot} ${base} ${dim(runnerNames)}${formatMark}${counts}`;
|
|
480
760
|
}
|
|
481
761
|
function packHorizontalRow(recs, totalWidth, theme) {
|
|
@@ -543,6 +823,7 @@ function formatFileTokenHorizontal(rec, remainingWidth, theme) {
|
|
|
543
823
|
const errors = rec.diagnosticCounts.errors;
|
|
544
824
|
const warnings = rec.diagnosticCounts.warnings;
|
|
545
825
|
const formatterChanged = hasChangedFormatter(rec);
|
|
826
|
+
const formatterFailed = hasFailedFormatter(rec);
|
|
546
827
|
let dotChar;
|
|
547
828
|
if (blocking > 0)
|
|
548
829
|
dotChar = red("●");
|
|
@@ -552,6 +833,9 @@ function formatFileTokenHorizontal(rec, remainingWidth, theme) {
|
|
|
552
833
|
dotChar = dim("✎");
|
|
553
834
|
else
|
|
554
835
|
dotChar = dim("·");
|
|
836
|
+
if (formatterFailed && blocking === 0 && errors === 0 && warnings === 0) {
|
|
837
|
+
dotChar = red("x");
|
|
838
|
+
}
|
|
555
839
|
let countsStyled = "";
|
|
556
840
|
if (errors > 0 && warnings > 0) {
|
|
557
841
|
const eColor = blocking > 0 ? red : yellow;
|
|
@@ -588,8 +872,11 @@ function truncateBasename(name, maxWidth) {
|
|
|
588
872
|
return stem.slice(0, keep) + "…" + ext;
|
|
589
873
|
}
|
|
590
874
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
591
|
-
function getOrCreate(filePath) {
|
|
592
|
-
|
|
875
|
+
function getOrCreate(filePath, key = fileMapKey(filePath)) {
|
|
876
|
+
maybePruneInactiveFileRecords();
|
|
877
|
+
// Look up by the normalized key so mixed path forms of the same file share
|
|
878
|
+
// ONE record (#1020); keep the caller's verbatim path as the display path.
|
|
879
|
+
return (files.get(key) ?? {
|
|
593
880
|
filePath,
|
|
594
881
|
runners: new Map(),
|
|
595
882
|
formatters: new Map(),
|
|
@@ -601,10 +888,16 @@ function getOrCreate(filePath) {
|
|
|
601
888
|
});
|
|
602
889
|
}
|
|
603
890
|
function hasChangedFormatter(rec) {
|
|
604
|
-
return [...rec.formatters.values()].some((f) => f.changed);
|
|
891
|
+
return [...rec.formatters.values()].some((f) => f.changed && f.success);
|
|
892
|
+
}
|
|
893
|
+
function hasFailedFormatter(rec) {
|
|
894
|
+
return [...rec.formatters.values()].some((f) => !f.success);
|
|
605
895
|
}
|
|
606
896
|
function shouldRenderFile(rec) {
|
|
607
|
-
return rec.hasFinalDiagnosticsSnapshot || hasChangedFormatter(rec);
|
|
897
|
+
return rec.hasFinalDiagnosticsSnapshot || hasChangedFormatter(rec) || hasFailedFormatter(rec);
|
|
898
|
+
}
|
|
899
|
+
function hasLiveDiagnostic(rec) {
|
|
900
|
+
return rec.hasFinalDiagnosticsSnapshot && rec.diagnostics.length > 0;
|
|
608
901
|
}
|
|
609
902
|
function isPendingAnalysis(rec) {
|
|
610
903
|
return rec.runners.size > 0 && !rec.hasFinalDiagnosticsSnapshot;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent NDJSON trace of word-index build / refresh / persist outcomes
|
|
3
|
+
* (#926 durable reporting, extended for #958 incremental cross-session refresh).
|
|
4
|
+
*
|
|
5
|
+
* Every word-index signal used to ride solely on the optional `dbg` callback
|
|
6
|
+
* threaded through runtime-session / word-index. `dbg` varies by host and is a
|
|
7
|
+
* documented no-op in the MCP host (clients/mcp/session.ts's `dbg: noop`) —
|
|
8
|
+
* exactly the host where `pilens_symbol_search` READS this index. So the one
|
|
9
|
+
* decision that governs a query's freshness (full rebuild vs incremental
|
|
10
|
+
* refresh, and how many docs were refreshed/dropped/skipped/reused, whether the
|
|
11
|
+
* index is `truncated`, #928) and the one failure that silently corrupts every
|
|
12
|
+
* later query (a swallowed snapshot persist) were invisible precisely where
|
|
13
|
+
* they mattered — the same blind spot bus-events-logger.ts and
|
|
14
|
+
* review-graph-logger.ts were created to close.
|
|
15
|
+
*
|
|
16
|
+
* This gives the word index the same durable trace those siblings have via the
|
|
17
|
+
* shared `createNdjsonLogger` writer (self-registers for log-cleanup rotation),
|
|
18
|
+
* with `isTestMode()` gating writes off inside the test runner. Callers log
|
|
19
|
+
* DIRECTLY here — never through `dbg` — so the trace exists regardless of the
|
|
20
|
+
* host's `dbg` wiring.
|
|
21
|
+
*/
|
|
22
|
+
import * as path from "node:path";
|
|
23
|
+
import { isTestMode } from "./env-utils.js";
|
|
24
|
+
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
25
|
+
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
26
|
+
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
27
|
+
const WORD_INDEX_LOG_FILE = path.join(getGlobalPiLensDir(), "word-index.log");
|
|
28
|
+
const writer = createNdjsonLogger({
|
|
29
|
+
filePath: WORD_INDEX_LOG_FILE,
|
|
30
|
+
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
31
|
+
});
|
|
32
|
+
export function logWordIndex(entry) {
|
|
33
|
+
if (isTestMode()) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
writer.log({ ts: new Date().toISOString(), ...entry });
|
|
37
|
+
}
|
|
38
|
+
export function getWordIndexLogPath() {
|
|
39
|
+
return WORD_INDEX_LOG_FILE;
|
|
40
|
+
}
|
|
41
|
+
/** Resolve once all enqueued word-index writes are on disk (tests/shutdown). */
|
|
42
|
+
export function flushWordIndexLog() {
|
|
43
|
+
return writer.flush();
|
|
44
|
+
}
|