@herbertgao/pi-extensions 2026.9.6 → 2026.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -8
- package/THIRD_PARTY_NOTICES.md +0 -4
- package/node_modules/@narumitw/pi-btw/README.md +2 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts +284 -365
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +3 -3
- package/node_modules/@narumitw/pi-btw/package.json +52 -53
- package/node_modules/@narumitw/pi-btw/src/bring-to-main.ts +494 -547
- package/node_modules/@narumitw/pi-btw/src/btw.ts +814 -856
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +673 -674
- package/node_modules/@narumitw/pi-btw/src/keybindings.ts +228 -270
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +309 -318
- package/node_modules/@narumitw/pi-btw/src/menu.ts +416 -454
- package/node_modules/@narumitw/pi-btw/src/settings.ts +203 -219
- package/node_modules/@narumitw/pi-btw/src/side-thread.ts +173 -201
- package/node_modules/@narumitw/pi-btw/src/text.ts +21 -23
- package/node_modules/@narumitw/pi-btw/src/transcript-markdown.ts +65 -0
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +611 -662
- package/node_modules/pi-lens/CHANGELOG.md +239 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +3 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +30 -4
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +2 -0
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +26 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +403 -35
- package/node_modules/pi-lens/dist/clients/bootstrap.js +6 -1
- package/node_modules/pi-lens/dist/clients/cascade-format.js +200 -4
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +10 -2
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +16 -1
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +12 -1
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +65 -0
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +28 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +9 -2
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +222 -0
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +389 -13
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +175 -25
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +62 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +21 -41
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +6 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +7 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +2 -21
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +4 -1
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +1 -0
- package/node_modules/pi-lens/dist/clients/effective-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +26 -1
- package/node_modules/pi-lens/dist/clients/file-role.js +21 -21
- package/node_modules/pi-lens/dist/clients/file-utils.js +156 -10
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +56 -9
- package/node_modules/pi-lens/dist/clients/format-service.js +39 -21
- package/node_modules/pi-lens/dist/clients/formatters.js +60 -13
- package/node_modules/pi-lens/dist/clients/generation-guard.js +2 -2
- package/node_modules/pi-lens/dist/clients/git-guard.js +11 -3
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +78 -8
- package/node_modules/pi-lens/dist/clients/installer/index.js +333 -114
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +7 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +20 -10
- package/node_modules/pi-lens/dist/clients/language-profile.js +23 -1
- package/node_modules/pi-lens/dist/clients/language-registry.js +5 -1
- package/node_modules/pi-lens/dist/clients/latency-logger.js +4 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +12 -3
- package/node_modules/pi-lens/dist/clients/lens-events.js +2 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +79 -0
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +25 -21
- package/node_modules/pi-lens/dist/clients/lsp/config.js +2 -2
- package/node_modules/pi-lens/dist/clients/lsp/index.js +7 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +16 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +19 -2
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +5 -3
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +12 -8
- package/node_modules/pi-lens/dist/clients/mcp/session.js +36 -4
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +24 -7
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +40 -2
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +23 -3
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +110 -15
- package/node_modules/pi-lens/dist/clients/path-utils.js +123 -3
- package/node_modules/pi-lens/dist/clients/performance-report.js +5 -1
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +1 -1
- package/node_modules/pi-lens/dist/clients/pipeline.js +74 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +37 -5
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +145 -18
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +4 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +59 -3
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +45 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +70 -11
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +147 -9
- package/node_modules/pi-lens/dist/clients/ruff-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +33 -5
- package/node_modules/pi-lens/dist/clients/runtime-context.js +6 -2
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +77 -7
- package/node_modules/pi-lens/dist/clients/runtime-session.js +21 -2
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +43 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +330 -85
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +147 -17
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +258 -5
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +6 -0
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +50 -5
- package/node_modules/pi-lens/dist/clients/sg-runner.js +23 -11
- package/node_modules/pi-lens/dist/clients/sgconfig.js +79 -2
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +6 -41
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +43 -0
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +423 -0
- package/node_modules/pi-lens/dist/clients/tool-config.js +50 -0
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +47 -76
- package/node_modules/pi-lens/dist/clients/tool-policy.js +163 -7
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +120 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +127 -189
- package/node_modules/pi-lens/dist/clients/widget-state.js +98 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +5 -2
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +7 -0
- package/node_modules/pi-lens/dist/index.js +27416 -24153
- package/node_modules/pi-lens/dist/mcp/server.js +5 -2
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +7 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +168 -53
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +244 -72
- package/node_modules/pi-lens/docs/audit1.md +5 -5
- package/node_modules/pi-lens/docs/dependencies.md +11 -0
- package/node_modules/pi-lens/docs/dispositions.md +52 -0
- package/node_modules/pi-lens/docs/environment-variables.md +2 -2
- package/node_modules/pi-lens/docs/features.md +4 -4
- package/node_modules/pi-lens/docs/globalconfig.md +3 -1
- package/node_modules/pi-lens/docs/language-coverage.md +1 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +53 -94
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +43 -38
- package/node_modules/pi-lens/docs/pi-lens-retro.md +97 -0
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +53 -66
- package/node_modules/pi-lens/docs/release-qa-baseline.md +22 -0
- package/node_modules/pi-lens/docs/settings.md +7 -4
- package/node_modules/pi-lens/docs/tree-sitter_rules_catalog.md +1 -1
- package/node_modules/pi-lens/docs/usage.md +2 -0
- package/node_modules/pi-lens/package.json +1 -1
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/sql-injection.yml +24 -12
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +40 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +15 -0
- package/node_modules/pi-lens/skills/pi-lens-lsp-navigation/SKILL.md +7 -0
- package/package.json +5 -11
- package/node_modules/@luxusai/pi-hindsight/CHANGELOG.md +0 -376
- package/node_modules/@luxusai/pi-hindsight/README.md +0 -92
- package/node_modules/@luxusai/pi-hindsight/docs/adr/001-memory-lifecycle-and-scope.md +0 -56
- package/node_modules/@luxusai/pi-hindsight/docs/adr/002-explicit-routing-strategy-seam.md +0 -169
- package/node_modules/@luxusai/pi-hindsight/docs/adr/003-tui-memory-mode-vocabulary.md +0 -107
- package/node_modules/@luxusai/pi-hindsight/docs/adr/004-lifeos-dual-bank-design.md +0 -89
- package/node_modules/@luxusai/pi-hindsight/docs/adr/005-domain-banks-and-agent-first-surface.md +0 -189
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/assets/logos/pi-hindsight-logo-dark@2x.webp +0 -0
- package/node_modules/@luxusai/pi-hindsight/docs/coding-memory-evaluation.md +0 -54
- package/node_modules/@luxusai/pi-hindsight/docs/compatibility.md +0 -75
- package/node_modules/@luxusai/pi-hindsight/docs/hindsight-core-functions.md +0 -300
- package/node_modules/@luxusai/pi-hindsight/docs/mission-and-mental-model-quality.md +0 -158
- package/node_modules/@luxusai/pi-hindsight/docs/next-opt-out-design.md +0 -164
- package/node_modules/@luxusai/pi-hindsight/docs/risky-memory-modes.md +0 -139
- package/node_modules/@luxusai/pi-hindsight/docs/starter-mental-model-suggestions.md +0 -74
- package/node_modules/@luxusai/pi-hindsight/docs/surface-reference.md +0 -261
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-operations.ts +0 -151
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-selection.ts +0 -18
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-settings-presenter.ts +0 -46
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/bank-templates.ts +0 -383
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/banking.ts +0 -240
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/knowledge-page-seed.ts +0 -176
- package/node_modules/@luxusai/pi-hindsight/extensions/banks/retain-strategies.ts +0 -178
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client-retry.ts +0 -41
- package/node_modules/@luxusai/pi-hindsight/extensions/client/client.ts +0 -386
- package/node_modules/@luxusai/pi-hindsight/extensions/client/fetch-compat.ts +0 -39
- package/node_modules/@luxusai/pi-hindsight/extensions/client/timeout.ts +0 -34
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-defaults.ts +0 -140
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-model.ts +0 -61
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-editing-registry.ts +0 -925
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-field-paths.ts +0 -247
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-normalize.ts +0 -547
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config-writer.ts +0 -480
- package/node_modules/@luxusai/pi-hindsight/extensions/config/config.ts +0 -182
- package/node_modules/@luxusai/pi-hindsight/extensions/config/setup-gate.ts +0 -86
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-execute.ts +0 -1010
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-parse.ts +0 -175
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-plan.ts +0 -425
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-presentation.ts +0 -210
- package/node_modules/@luxusai/pi-hindsight/extensions/imports/import-sessions.ts +0 -817
- package/node_modules/@luxusai/pi-hindsight/extensions/index.ts +0 -25
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/git-seed.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-recall.ts +0 -196
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-retain.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle-runtime.ts +0 -65
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/memory-lifecycle.ts +0 -260
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/mental-models.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/observation-scopes.ts +0 -59
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-cleanup.ts +0 -82
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall-visibility.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/recall.ts +0 -331
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-cursor.ts +0 -354
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-job-builder.ts +0 -66
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain-receipts.ts +0 -132
- package/node_modules/@luxusai/pi-hindsight/extensions/lifecycle/retain.ts +0 -228
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-bank-template-operations.ts +0 -74
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-config-operations.ts +0 -16
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-control-operations.ts +0 -648
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-diagnostics-operations.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-identity.ts +0 -68
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-service.ts +0 -71
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-operation-types.ts +0 -13
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-recall-operations.ts +0 -162
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-retain-operations.ts +0 -131
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-scope.ts +0 -104
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/memory-session-operations.ts +0 -37
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/operation-catalog.ts +0 -893
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/reflect-presenter.ts +0 -35
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/scope-migrate.ts +0 -177
- package/node_modules/@luxusai/pi-hindsight/extensions/operations/tools.ts +0 -7
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/flush-presenter.ts +0 -21
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/jsonl-queue-store.ts +0 -112
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-delivery.ts +0 -111
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-lock.ts +0 -214
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue-operations.ts +0 -70
- package/node_modules/@luxusai/pi-hindsight/extensions/queue/queue.ts +0 -392
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/bank-template-presentation.ts +0 -45
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/commands.ts +0 -9
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/guided-setup.ts +0 -860
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/prefill-input.ts +0 -105
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-flow.ts +0 -189
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-server-probe.ts +0 -299
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-actions.ts +0 -201
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-facts.ts +0 -47
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-render.ts +0 -239
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui-types.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/setup-tui.ts +0 -259
- package/node_modules/@luxusai/pi-hindsight/extensions/tui/tool-presenters.ts +0 -77
- package/node_modules/@luxusai/pi-hindsight/extensions/types.ts +0 -534
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/diagnostics.ts +0 -319
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/messages.ts +0 -325
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/sanitize.ts +0 -42
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-memory-meta.ts +0 -244
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session-operations.ts +0 -56
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/session.ts +0 -50
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-fields.ts +0 -167
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status-health.ts +0 -207
- package/node_modules/@luxusai/pi-hindsight/extensions/utils/status.ts +0 -120
- package/node_modules/@luxusai/pi-hindsight/extensions/version.ts +0 -8
- package/node_modules/@luxusai/pi-hindsight/package.json +0 -118
- package/node_modules/@luxusai/pi-hindsight/skills/hindsight-memory-doctor/SKILL.md +0 -75
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* Approach (inspired by OpenCode's Filesystem.normalizePath):
|
|
8
8
|
* - On Windows: try realpathSync.native() for canonical casing
|
|
9
9
|
* - Falls back to lowercase for files that don't exist yet
|
|
10
|
-
* - On non-Windows:
|
|
10
|
+
* - On non-Windows: adopt the on-disk casing of an existing path (#3098 — a
|
|
11
|
+
* case-insensitive POSIX filesystem otherwise derives two Map keys for one
|
|
12
|
+
* file); a path that does not exist is returned as-is, case-preserving
|
|
11
13
|
* - Always convert backslashes to forward slashes for Map key consistency
|
|
12
14
|
*/
|
|
13
15
|
import { existsSync, readFileSync, realpathSync, statSync, } from "node:fs";
|
|
@@ -91,6 +93,64 @@ export function isAbsoluteFilePath(filePath) {
|
|
|
91
93
|
export function splitPathSegments(filePath) {
|
|
92
94
|
return filePath.split(/[\\/]+/).filter(Boolean);
|
|
93
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Adopt `canonical`'s CASING for the trailing segments of `held`, stopping at
|
|
98
|
+
* the first segment that differs by more than case.
|
|
99
|
+
*
|
|
100
|
+
* `realpathSync.native` answers a different question than the one the POSIX
|
|
101
|
+
* arm of `normalizeFilePath` asks: it resolves symlinks AND reports on-disk
|
|
102
|
+
* casing, and we want only the second half. Walking from the tail and halting
|
|
103
|
+
* at the first structural divergence separates them with no extra syscall:
|
|
104
|
+
*
|
|
105
|
+
* held /var/folders/T/x/SUB/a.ts (macOS tmpdir, mis-cased segment)
|
|
106
|
+
* canonical /private/var/folders/T/x/sub/a.ts
|
|
107
|
+
* result /var/folders/T/x/sub/a.ts (case fixed, symlink prefix kept)
|
|
108
|
+
*
|
|
109
|
+
* The symlink prefix matters: macOS's `os.tmpdir()` is `/var/folders/...`,
|
|
110
|
+
* a symlink into `/private/var`, so a whole-string "is this a case variant"
|
|
111
|
+
* test would decline to canonicalize exactly the paths the #1024 regression
|
|
112
|
+
* test (and any macOS temp-dir workflow) runs on. Keeping the prefix is also
|
|
113
|
+
* what bounds this change: on POSIX, `normalizeFilePath` still never resolves
|
|
114
|
+
* a symlink, so a symlinked monorepo package keeps keying under the path the
|
|
115
|
+
* caller held (refs #2490 — a cwd fold for its own sake broke every monorepo).
|
|
116
|
+
*
|
|
117
|
+
* PURE string algebra: this pins what the rewrite DOES, never what the kernel
|
|
118
|
+
* reports — and from two strings alone it cannot tell "the same file, spelled
|
|
119
|
+
* with different case" from "a different file whose name happens to be a case
|
|
120
|
+
* variant". Any symlink whose BASENAME is a case variant of its target's
|
|
121
|
+
* basename (`<root>/MyProject` → `work/myproject`, `node_modules/Foo` →
|
|
122
|
+
* `../pkgs/foo`) has its name replaced while its own parent is kept, so the
|
|
123
|
+
* result names a different place — or no place at all. Measured in #3159
|
|
124
|
+
* review round 2 (F1): two different inodes collapsed onto ONE key (the #1024
|
|
125
|
+
* defect inverted — false suppression), and a symlinked package keyed under a
|
|
126
|
+
* path that does not exist on disk, breaking the #2490 bound above. The caller
|
|
127
|
+
* therefore CONFIRMS every rewrite against the filesystem before adopting it
|
|
128
|
+
* (see `normalizeFilePath`); do not use this function without that step.
|
|
129
|
+
*/
|
|
130
|
+
function adoptCanonicalCasing(held, canonical) {
|
|
131
|
+
const heldParts = held.split("/");
|
|
132
|
+
const realParts = canonical.split("/");
|
|
133
|
+
let i = heldParts.length - 1;
|
|
134
|
+
let j = realParts.length - 1;
|
|
135
|
+
let changed = false;
|
|
136
|
+
while (i >= 0 && j >= 0) {
|
|
137
|
+
const a = heldParts[i];
|
|
138
|
+
const b = realParts[j];
|
|
139
|
+
if (a !== b) {
|
|
140
|
+
if (a.toLowerCase() !== b.toLowerCase())
|
|
141
|
+
break;
|
|
142
|
+
heldParts[i] = b;
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
i--;
|
|
146
|
+
j--;
|
|
147
|
+
}
|
|
148
|
+
// Identity-preserving when nothing moved — this runs on every POSIX map-key
|
|
149
|
+
// derivation and the overwhelmingly common answer is "already canonical".
|
|
150
|
+
// NOT a behavioural branch (both arms produce an equal string): it is the
|
|
151
|
+
// same allocation guard `normalizeEphemeralMapKey` documents below.
|
|
152
|
+
return changed ? heldParts.join("/") : held;
|
|
153
|
+
}
|
|
94
154
|
/**
|
|
95
155
|
* Normalize a file path for consistent Map key usage.
|
|
96
156
|
*
|
|
@@ -100,7 +160,33 @@ export function splitPathSegments(filePath) {
|
|
|
100
160
|
* - If the file doesn't exist: resolves the path and lowercases
|
|
101
161
|
* (needed for new files where we haven't written yet)
|
|
102
162
|
*
|
|
103
|
-
* On
|
|
163
|
+
* On POSIX:
|
|
164
|
+
* - If the file exists: adopts the on-disk CASING of the trailing segments
|
|
165
|
+
* (see `adoptCanonicalCasing`) — no lowercasing, no symlink resolution.
|
|
166
|
+
* - If the file doesn't exist: returns the path as-is, case-PRESERVING. On a
|
|
167
|
+
* case-sensitive filesystem `SUB/a.ts` and `sub/a.ts` are two different
|
|
168
|
+
* files, and nothing may fold one into the other; only the filesystem's own
|
|
169
|
+
* answer for a path that EXISTS can tell the two apart, and for a path that
|
|
170
|
+
* does not exist there is no such answer to ask for.
|
|
171
|
+
*
|
|
172
|
+
* Why POSIX canonicalizes casing at all (#3098, the #1024 defect's live half):
|
|
173
|
+
* a case-insensitive POSIX filesystem — macOS's default APFS, `nocase` vfat /
|
|
174
|
+
* ntfs3 / cifs mounts — makes `SUB/a.ts` and `sub/a.ts` ONE file, so a raw
|
|
175
|
+
* mis-cased write (`lens_diagnostic_mark` anchors under `path.resolve(cwd,
|
|
176
|
+
* arg)`) and a `normalizeMapKey` read derived two anchors for one file and the
|
|
177
|
+
* agent's own disposition mark silently never applied. A case-preserving POSIX
|
|
178
|
+
* arm cannot close that: the two keys only become one if the normalizer asks
|
|
179
|
+
* the filesystem which name is really on disk.
|
|
180
|
+
*
|
|
181
|
+
* `realpathSync.native` is `realpath(3)`. On Darwin it rebuilds every
|
|
182
|
+
* component from the filesystem's own `ATTR_CMN_NAME` — Libc-1669.0.4
|
|
183
|
+
* `stdlib/FreeBSD/realpath.c:233` (`getattrlist(resolved, &_rp_alist, …,
|
|
184
|
+
* FSOPT_NOFOLLOW)` with `ATTR_CMN_NAME`) and `:348-354` ("attrs already has
|
|
185
|
+
* the real name") — which is why it returns on-disk casing on APFS/HFS+.
|
|
186
|
+
* MEASURED counter-example (#3098): a Linux ext4/tmpfs `chattr +F` casefold
|
|
187
|
+
* directory aliases the two spellings but `realpath(3)` there returns the
|
|
188
|
+
* spelling the caller asked with, so this arm is a no-op on casefolded Linux
|
|
189
|
+
* directories — filed with the transcript as #3154.
|
|
104
190
|
*
|
|
105
191
|
* Always converts backslashes to forward slashes for consistent Map keys.
|
|
106
192
|
*/
|
|
@@ -108,7 +194,31 @@ export function normalizeFilePath(filePath) {
|
|
|
108
194
|
// Convert backslashes to forward slashes first
|
|
109
195
|
const normalized = filePath.replace(/\\/g, "/");
|
|
110
196
|
if (process.platform !== "win32" && !isWindowsPath(normalized)) {
|
|
111
|
-
|
|
197
|
+
try {
|
|
198
|
+
const canonical = realpathSync.native(normalized);
|
|
199
|
+
// Fast path, not a guard: both arms answer `normalized` when the
|
|
200
|
+
// strings match, but skipping the two `split`s there is a measured
|
|
201
|
+
// 1.9 vs 2.3 microseconds per call on the per-edit seam (#3098).
|
|
202
|
+
if (canonical === normalized)
|
|
203
|
+
return normalized;
|
|
204
|
+
const adopted = adoptCanonicalCasing(normalized, canonical);
|
|
205
|
+
if (adopted === normalized)
|
|
206
|
+
return normalized;
|
|
207
|
+
// The rewrite is string algebra and can land on a DIFFERENT file
|
|
208
|
+
// (#3159 review round 2, F1 — see `adoptCanonicalCasing`). Adopt it
|
|
209
|
+
// only once the filesystem agrees it still names the file the caller
|
|
210
|
+
// held: `canonical` IS `realpath(normalized)`, so this asks exactly
|
|
211
|
+
// "does the rewritten spelling resolve to the same file?". One extra
|
|
212
|
+
// syscall, and only on the rare branch where casing actually moved —
|
|
213
|
+
// never on an already-canonical path. A throw here (the rewritten
|
|
214
|
+
// path does not exist, the #2490 monorepo case) lands in the catch
|
|
215
|
+
// below and keeps the caller's spelling, which is the same answer.
|
|
216
|
+
return realpathSync.native(adopted) === canonical ? adopted : normalized;
|
|
217
|
+
}
|
|
218
|
+
catch {
|
|
219
|
+
// Does not exist (or is unreadable): case-preserving, as above.
|
|
220
|
+
return normalized;
|
|
221
|
+
}
|
|
112
222
|
}
|
|
113
223
|
// Windows: try realpathSync.native() for canonical casing
|
|
114
224
|
// This resolves symlinks and returns the actual filesystem casing
|
|
@@ -130,6 +240,16 @@ export function normalizeFilePath(filePath) {
|
|
|
130
240
|
}
|
|
131
241
|
}
|
|
132
242
|
}
|
|
243
|
+
/** Resolve a filesystem identity once, retaining a usable absolute fallback. */
|
|
244
|
+
export function realpathOrResolve(filePath) {
|
|
245
|
+
const resolved = path.resolve(filePath);
|
|
246
|
+
try {
|
|
247
|
+
return realpathSync(resolved);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return resolved;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
133
253
|
/**
|
|
134
254
|
* Normalize a logged `filePath`/`cwd` value, but ONLY when it is already a
|
|
135
255
|
* fully-qualified path (#2219, the #2141 class's sibling loggers). Several
|
|
@@ -5,7 +5,11 @@ import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
|
5
5
|
const DEFAULT_PERF_TOP_N = 5;
|
|
6
6
|
const MAX_PERF_TOP_N = 50;
|
|
7
7
|
export const MAX_PERF_PHASE_SAMPLES = 20_000;
|
|
8
|
-
|
|
8
|
+
// Yield cadence for the full-window parse below. Exported so the occupancy
|
|
9
|
+
// guard can count yields against the lines fed instead of measuring a
|
|
10
|
+
// wall-clock block the scheduler can inflate (#2886): the count is a
|
|
11
|
+
// deterministic function of the input, not of worker contention.
|
|
12
|
+
export const PARSE_YIELD_EVERY = 500;
|
|
9
13
|
// The window follows the same threshold that rotates the log out from under it.
|
|
10
14
|
// Exported so a test can size a fixture to the window it means to saturate
|
|
11
15
|
// rather than to a second copy of the default that can drift out of agreement.
|
|
@@ -114,6 +114,6 @@ export function resolvePhpCsFixerConfig(filePath) {
|
|
|
114
114
|
const startDir = resolveToolCwd("formatter", "php-cs-fixer", absolute, {
|
|
115
115
|
cwd: path.parse(absolute).root,
|
|
116
116
|
allowHomeMarker: true,
|
|
117
|
-
});
|
|
117
|
+
}).cwd;
|
|
118
118
|
return PHP_CS_FIXER_CONFIG_NAMES.map((name) => path.join(startDir, name)).find(existsSync);
|
|
119
119
|
}
|
|
@@ -11,12 +11,14 @@
|
|
|
11
11
|
* 6. Test runner (run corresponding test file)
|
|
12
12
|
* 7. Cascade diagnostics (other files with errors, LSP only)
|
|
13
13
|
*/
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import * as nodeFs from "node:fs";
|
|
16
|
+
import * as nodeCrypto from "node:crypto";
|
|
15
17
|
import * as path from "node:path";
|
|
16
18
|
import { findNearestContaining, normalizeEphemeralMapKey, } from "./path-utils.js";
|
|
17
19
|
import { recordFromDispatchDiagnostic, } from "./actionable-warnings.js";
|
|
18
20
|
import { recordFromCodeQualityDiagnostic, } from "./code-quality-warnings.js";
|
|
19
|
-
import { recordDiagnostics } from "./widget-state.js";
|
|
21
|
+
import { admitWidgetDiagnosticsWrite, recordDiagnostics, } from "./widget-state.js";
|
|
20
22
|
import { getDiagnosticLogger } from "./diagnostic-logger.js";
|
|
21
23
|
import { getDiagnosticTracker } from "./diagnostic-tracker.js";
|
|
22
24
|
import { loadDispatchIntegration } from "./dispatch/lazy.js";
|
|
@@ -36,8 +38,10 @@ import { RUNTIME_CONFIG } from "./runtime-config.js";
|
|
|
36
38
|
import { getAmbientAbortSignal, safeSpawnAsync } from "./safe-spawn.js";
|
|
37
39
|
import { probeToolAsync } from "./tool-probe.js";
|
|
38
40
|
import { bounded } from "./deadline-utils.js";
|
|
41
|
+
import { HOOK_WALL_BUDGET_MS } from "./hook-budgets.js";
|
|
39
42
|
import { enabledAuxiliaryLspServerIds } from "./dispatch/auxiliary-lsp.js";
|
|
40
43
|
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
44
|
+
import { establishToolAgreement } from "./tool-agreement.js";
|
|
41
45
|
import { dropFindingsForMissingPaths } from "./advisory-provenance.js";
|
|
42
46
|
import { getAutofixPolicyForFile, getPreferredAutofixTools, getRubocopCommand, hasBiomeConfig, hasDetektConfig, hasEslintConfig, hasGolangciConfig, hasKtfmtConfig, hasKtlintConfig, hasOxlintConfig, hasRubocopConfig, hasSqlfluffConfig, hasStylelintConfig, markdownlintConfigArgs, } from "./tool-policy.js";
|
|
43
47
|
const LSP_MAX_FILE_BYTES = RUNTIME_CONFIG.pipeline.lspMaxFileBytes;
|
|
@@ -462,6 +466,17 @@ export async function runAutofix(filePath, cwd, getFlag, dbg, deps, getFlagSourc
|
|
|
462
466
|
}
|
|
463
467
|
for (const toolName of preferredAutofixTools) {
|
|
464
468
|
attemptedTools.push(toolName);
|
|
469
|
+
const agreement = establishToolAgreement(toolName, cwd);
|
|
470
|
+
if (agreement.decision === "decline") {
|
|
471
|
+
const reason = agreement.reason;
|
|
472
|
+
dbg(`autofix: ${toolName} declined (${reason})`);
|
|
473
|
+
recordDegradationOnce({
|
|
474
|
+
kind: "autofix-agreement-unavailable",
|
|
475
|
+
subject: agreement.subject,
|
|
476
|
+
reason,
|
|
477
|
+
});
|
|
478
|
+
continue;
|
|
479
|
+
}
|
|
465
480
|
if (toolName === "ruff") {
|
|
466
481
|
const ruffReady = ruffClient.isPythonFile(filePath)
|
|
467
482
|
? await ruffClient.ensureAvailable()
|
|
@@ -818,7 +833,7 @@ function buildAllClearOutput(_dispatchResult, elapsed, filePath) {
|
|
|
818
833
|
parts.push(`${elapsed}ms`);
|
|
819
834
|
return `checkmark ${parts.join(" · ")}`.replace("checkmark", "\u2713");
|
|
820
835
|
}
|
|
821
|
-
export async function runFormatPhase(filePath, getFormatService, dbg) {
|
|
836
|
+
export async function runFormatPhase(filePath, getFormatService, dbg, signal, budgetMs = HOOK_WALL_BUDGET_MS.tool_result_edit, hook = "tool_result_edit") {
|
|
822
837
|
let formatChanged = false;
|
|
823
838
|
let formattersUsed = [];
|
|
824
839
|
const formatFailures = [];
|
|
@@ -827,13 +842,19 @@ export async function runFormatPhase(filePath, getFormatService, dbg) {
|
|
|
827
842
|
const formatService = getFormatService();
|
|
828
843
|
try {
|
|
829
844
|
formatService.recordRead(filePath);
|
|
830
|
-
const result = await formatService.formatFile(filePath
|
|
845
|
+
const result = await formatService.formatFile(filePath, {
|
|
846
|
+
signal,
|
|
847
|
+
budgetMs,
|
|
848
|
+
hook,
|
|
849
|
+
});
|
|
831
850
|
// An unavailable tool is NOT a formatter that ran (#2413): keep it out of
|
|
832
851
|
// `formattersUsed` (which drives change bookkeeping / turn summaries) and
|
|
833
852
|
// out of `formatFailures` (which requeues). Record it once, distinctly.
|
|
834
853
|
for (const f of result.formatters) {
|
|
835
854
|
if (f.outcome !== "unavailable")
|
|
836
855
|
continue;
|
|
856
|
+
if (f.error?.includes("project tool agreement could not be established"))
|
|
857
|
+
continue;
|
|
837
858
|
const reason = f.error ?? "formatter executable not found";
|
|
838
859
|
formatUnavailable.push({ formatter: f.name, reason });
|
|
839
860
|
recordDegradationOnce({
|
|
@@ -931,6 +952,7 @@ function buildEnrichedBlockerOutput(blockers, fileContent = "") {
|
|
|
931
952
|
export async function runPipeline(ctx, deps) {
|
|
932
953
|
const { filePath, cwd, toolName, getFlag, getFlagSource, dbg } = ctx;
|
|
933
954
|
const { getFormatService } = deps;
|
|
955
|
+
admitWidgetDiagnosticsWrite(filePath, ctx.telemetry?.writeIndex);
|
|
934
956
|
const phase = createPhaseTracker(toolName, filePath);
|
|
935
957
|
const pipelineStart = Date.now();
|
|
936
958
|
clearGraphCache();
|
|
@@ -955,7 +977,7 @@ export async function runPipeline(ctx, deps) {
|
|
|
955
977
|
const immediateFormat = !!getFlag("immediate-format");
|
|
956
978
|
const formatDeferred = !autoformatDisabled && !immediateFormat && !!fileContent;
|
|
957
979
|
if (!autoformatDisabled && immediateFormat && fileContent) {
|
|
958
|
-
const formatResult = await runFormatPhase(filePath, getFormatService, dbg);
|
|
980
|
+
const formatResult = await runFormatPhase(filePath, getFormatService, dbg, ctx.signal);
|
|
959
981
|
formatChanged = formatResult.formatChanged;
|
|
960
982
|
formattersUsed = formatResult.formattersUsed;
|
|
961
983
|
formatFailures = formatResult.formatFailures;
|
|
@@ -1043,6 +1065,30 @@ export async function runPipeline(ctx, deps) {
|
|
|
1043
1065
|
attemptedTools,
|
|
1044
1066
|
skipReason: autofixSkipReason,
|
|
1045
1067
|
});
|
|
1068
|
+
// Capture the target's bytes immediately after pi-lens writes and before any
|
|
1069
|
+
// awaitable LSP or dispatch work. `fileModified` also covers side-effect
|
|
1070
|
+
// files, so target membership is the discriminator for pipeline ownership
|
|
1071
|
+
// (#2499).
|
|
1072
|
+
const fileModified = formatChanged || fixedCount > 0;
|
|
1073
|
+
const targetFileModified = piChangedFiles.has(path.resolve(filePath));
|
|
1074
|
+
const postWriteStateHash = fileModified && targetFileModified
|
|
1075
|
+
? (() => {
|
|
1076
|
+
try {
|
|
1077
|
+
return nodeCrypto
|
|
1078
|
+
.createHash("sha256")
|
|
1079
|
+
.update(nodeFs.readFileSync(filePath))
|
|
1080
|
+
.digest("hex");
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
recordDegradationOnce({
|
|
1084
|
+
kind: "pipeline-post-write-hash-unavailable",
|
|
1085
|
+
subject: filePath,
|
|
1086
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1087
|
+
});
|
|
1088
|
+
return undefined;
|
|
1089
|
+
}
|
|
1090
|
+
})()
|
|
1091
|
+
: undefined;
|
|
1046
1092
|
// --- 4. LSP file sync ---
|
|
1047
1093
|
// Sync once with final post-format/post-fix content so dispatch and cascade
|
|
1048
1094
|
// diagnostics do not observe stale pre-format text.
|
|
@@ -1060,6 +1106,21 @@ export async function runPipeline(ctx, deps) {
|
|
|
1060
1106
|
getFlag: getFlag,
|
|
1061
1107
|
};
|
|
1062
1108
|
const { dispatchLintWithResult, computeCascadeForFile } = await loadDispatchIntegration();
|
|
1109
|
+
// Capture the bytes the pipeline presents to analysis before the dispatch
|
|
1110
|
+
// promise can yield to another writer. The blocker evidence below belongs to
|
|
1111
|
+
// this analysis input, not to whatever happens to be on disk when the whole
|
|
1112
|
+
// pipeline returns.
|
|
1113
|
+
const inlineBlockerFileContent = fileContent
|
|
1114
|
+
? (() => {
|
|
1115
|
+
const content = Buffer.from(fileContent, "utf8");
|
|
1116
|
+
return content.byteLength <= 2 * 1024 * 1024
|
|
1117
|
+
? {
|
|
1118
|
+
size: content.byteLength,
|
|
1119
|
+
sha256: createHash("sha256").update(content).digest("hex"),
|
|
1120
|
+
}
|
|
1121
|
+
: undefined;
|
|
1122
|
+
})()
|
|
1123
|
+
: undefined;
|
|
1063
1124
|
const dispatchResult = await dispatchLintWithResult(filePath, cwd, piApi, ctx.modifiedRanges, {
|
|
1064
1125
|
model: ctx.telemetry?.model ?? "unknown",
|
|
1065
1126
|
sessionId: ctx.telemetry?.sessionId ?? "unknown",
|
|
@@ -1216,6 +1277,11 @@ export async function runPipeline(ctx, deps) {
|
|
|
1216
1277
|
dbg,
|
|
1217
1278
|
turnSeq: ctx.telemetry?.turnIndex,
|
|
1218
1279
|
writeSeq: ctx.telemetry?.writeIndex,
|
|
1280
|
+
// #3157: `cwd` here is the LANGUAGE root. The cascade's display
|
|
1281
|
+
// filter reads the disposition store and the `.pi-lens.json` rule
|
|
1282
|
+
// policy, both written under the PROJECT root (#1030) — the same
|
|
1283
|
+
// pair the dispatcher is handed above.
|
|
1284
|
+
projectRoot: ctx.projectRoot,
|
|
1219
1285
|
seqState: ctx.seqState,
|
|
1220
1286
|
turnEndCascadeSettleStart: ctx.turnEndCascadeSettleStart,
|
|
1221
1287
|
fileContent,
|
|
@@ -1250,7 +1316,6 @@ export async function runPipeline(ctx, deps) {
|
|
|
1250
1316
|
output = buildAllClearOutput(dispatchResult, elapsed, filePath);
|
|
1251
1317
|
}
|
|
1252
1318
|
phase.end("total", { hasOutput: !!output });
|
|
1253
|
-
const fileModified = formatChanged || fixedCount > 0;
|
|
1254
1319
|
const changedFiles = [...piChangedFiles];
|
|
1255
1320
|
emitLensAnalysisComplete({
|
|
1256
1321
|
cwd,
|
|
@@ -1276,6 +1341,7 @@ export async function runPipeline(ctx, deps) {
|
|
|
1276
1341
|
cascadePromise,
|
|
1277
1342
|
isError: false,
|
|
1278
1343
|
fileModified,
|
|
1344
|
+
postWriteStateHash,
|
|
1279
1345
|
changedFiles,
|
|
1280
1346
|
inlineBlockerSummary: dispatchResult.hasBlockers
|
|
1281
1347
|
? dispatchResult.blockerOutput.trim() || undefined
|
|
@@ -1338,5 +1404,8 @@ export async function runPipeline(ctx, deps) {
|
|
|
1338
1404
|
source: "autofix",
|
|
1339
1405
|
}
|
|
1340
1406
|
: undefined,
|
|
1407
|
+
inlineBlockerFileContent: dispatchResult.hasBlockers
|
|
1408
|
+
? inlineBlockerFileContent
|
|
1409
|
+
: undefined,
|
|
1341
1410
|
};
|
|
1342
1411
|
}
|
|
@@ -4,9 +4,17 @@ import { getProjectDataDir } from "../file-utils.js";
|
|
|
4
4
|
import { writeFileAtomic } from "../atomic-write.js";
|
|
5
5
|
import { readJsonCache } from "../json-cache-read.js";
|
|
6
6
|
import { freshnessFromMtime } from "../freshness.js";
|
|
7
|
+
import { hashDiagnosticContent } from "../lsp/diagnostic-binding.js";
|
|
8
|
+
// v3 (#2154, #3060 review F1): rows carry `fileFingerprints` — the size and
|
|
9
|
+
// sha256 of the bytes the scan actually read. A v2 snapshot has no content
|
|
10
|
+
// axis at all; its rows can only be judged by `mtime <= scannedAt`, the test
|
|
11
|
+
// that let an edit landing mid-scan read as fresh forever. Every row here is
|
|
12
|
+
// a POSITIVE claim, so v2 records are rejected by the version guard and the
|
|
13
|
+
// project is re-scanned rather than served under the axis that failed — the
|
|
14
|
+
// same clean break `WORKSPACE_DIAGNOSTICS_CACHE_VERSION` v3 (#2776) made.
|
|
7
15
|
// v2: cheap-tier scan now also runs ast-grep-napi (#308); invalidate older
|
|
8
16
|
// snapshots so a pre-ast-grep cache isn't served as complete via refreshRunners=cached.
|
|
9
|
-
export const PROJECT_DIAGNOSTICS_CACHE_VERSION =
|
|
17
|
+
export const PROJECT_DIAGNOSTICS_CACHE_VERSION = 3;
|
|
10
18
|
const SNAPSHOT_CACHE_FILE = "project-diagnostics.json";
|
|
11
19
|
const DELTA_CACHE_FILE = "project-diagnostics-delta.json";
|
|
12
20
|
function cachePath(cwd, fileName) {
|
|
@@ -80,15 +88,39 @@ export function reconcileProjectDiagnosticsSnapshot(snapshot) {
|
|
|
80
88
|
if (cached !== undefined)
|
|
81
89
|
return cached;
|
|
82
90
|
let stale;
|
|
83
|
-
let
|
|
91
|
+
let stat;
|
|
84
92
|
try {
|
|
85
|
-
|
|
93
|
+
stat = fs.statSync(filePath);
|
|
86
94
|
}
|
|
87
95
|
catch {
|
|
88
|
-
|
|
96
|
+
stat = undefined; // deleted / unreadable -> indeterminate -> drop
|
|
97
|
+
}
|
|
98
|
+
// #2154: when the scan recorded what this file's bytes WERE, that answers
|
|
99
|
+
// the question directly and the timestamp comparison below cannot — a
|
|
100
|
+
// file edited while the scan was still running carries an mtime at or
|
|
101
|
+
// before `scannedAt` while holding bytes no rule here ever saw. Size is
|
|
102
|
+
// the cheap reject; only a same-size file is read and hashed. An exact
|
|
103
|
+
// content match is stronger evidence than any mtime ordering, so it
|
|
104
|
+
// settles the verdict on its own: these are per-file syntax rules, so
|
|
105
|
+
// identical bytes mean identical findings no matter what the clock says.
|
|
106
|
+
const fingerprint = snapshot.fileFingerprints?.[filePath];
|
|
107
|
+
if (fingerprint && stat) {
|
|
108
|
+
stale = true;
|
|
109
|
+
if (stat.size === fingerprint.sizeBytes) {
|
|
110
|
+
try {
|
|
111
|
+
stale =
|
|
112
|
+
hashDiagnosticContent(fs.readFileSync(filePath, "utf-8")) !==
|
|
113
|
+
fingerprint.contentHash;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
stale = true; // unreadable now -> indeterminate -> drop
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
staleByFile.set(filePath, stale);
|
|
120
|
+
return stale;
|
|
89
121
|
}
|
|
90
122
|
const verdict = freshnessFromMtime({
|
|
91
|
-
mtimeMs,
|
|
123
|
+
mtimeMs: stat?.mtimeMs,
|
|
92
124
|
referenceMs: scannedAtMs,
|
|
93
125
|
});
|
|
94
126
|
// Pre-kernel policy: an unreadable/missing file is stale (dropped).
|