@herbertgao/pi-extensions 2026.8.13 → 2026.8.15
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 +19 -14
- package/THIRD_PARTY_NOTICES.md +0 -1
- package/node_modules/@czottmann/pi-automode/CHANGELOG.md +19 -0
- package/node_modules/@czottmann/pi-automode/README.md +5 -1
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/classifier.ts +126 -13
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/config.ts +7 -3
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/constants.ts +2 -0
- package/node_modules/@czottmann/pi-automode/extensions/auto-mode/hard-deny.ts +134 -23
- package/node_modules/@czottmann/pi-automode/package.json +1 -1
- package/node_modules/@herbertgao/{pi-stash → pi-bark}/LICENSE +4 -5
- package/node_modules/@herbertgao/pi-bark/README.md +44 -0
- package/node_modules/@herbertgao/pi-bark/assets/pi-icon.png +0 -0
- package/node_modules/@herbertgao/pi-bark/package.json +53 -0
- package/node_modules/@herbertgao/pi-bark/src/index.ts +178 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +1 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +6 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +2 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +8 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/README.md +125 -88
- package/node_modules/@narumitw/pi-btw/dist/index.ts +140 -45
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +2 -2
- package/node_modules/@narumitw/pi-btw/package.json +5 -8
- package/node_modules/@narumitw/pi-btw/src/btw.ts +31 -26
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +106 -17
- package/node_modules/@narumitw/pi-btw/src/menu.ts +34 -2
- package/node_modules/@narumitw/pi-btw/src/settings.ts +16 -0
- package/node_modules/@tifan/pi-handoff/README.md +36 -13
- package/node_modules/@tifan/pi-handoff/package.json +2 -2
- package/node_modules/@tifan/pi-handoff/src/index.ts +160 -4
- package/node_modules/@tifan/pi-preferred-thinking/README.md +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/package.json +1 -1
- package/node_modules/@tifan/pi-preferred-thinking/src/index.ts +15 -2
- package/node_modules/@tifan/pi-recap/README.md +1 -1
- package/node_modules/@tifan/pi-recap/package.json +2 -2
- package/node_modules/@tifan/pi-recap/src/index.ts +15 -0
- package/node_modules/@tifan/pi-rename/README.md +42 -21
- package/node_modules/@tifan/pi-rename/package.json +1 -1
- package/node_modules/@tifan/pi-rename/src/index.ts +62 -11
- package/node_modules/@tifan/pi-rename/src/language.ts +18 -0
- package/node_modules/@tifan/pi-rename/src/models.ts +69 -13
- package/node_modules/@tifan/pi-rename/src/naming.ts +36 -7
- package/node_modules/@tifan/pi-rename/src/sanitize.ts +22 -14
- package/node_modules/pi-lens/CHANGELOG.md +320 -0
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +79 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +16 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +13 -6
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +12 -0
- package/node_modules/pi-lens/dist/clients/biome-client.js +7 -0
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +20 -0
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +56 -19
- package/node_modules/pi-lens/dist/clients/build-identity.js +95 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +3 -0
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +2 -1
- package/node_modules/pi-lens/dist/clients/cache-observability.js +2 -1
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +15 -1
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +14 -11
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +56 -4
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +14 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +20 -3
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +1 -1
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +39 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +150 -9
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +231 -9
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +205 -100
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +87 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +21 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +174 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +54 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +1 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +37 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +11 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +8 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +76 -25
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +19 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +7 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +51 -17
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +66 -1
- package/node_modules/pi-lens/dist/clients/file-utils.js +157 -7
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +5 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +28 -3
- package/node_modules/pi-lens/dist/clients/git-guard.js +197 -47
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +32 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +5 -1
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +7 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +131 -34
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +3 -1
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +4 -13
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +145 -0
- package/node_modules/pi-lens/dist/clients/instance-registry.js +432 -122
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +6 -1
- package/node_modules/pi-lens/dist/clients/language-profile.js +4 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +19 -1
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +3 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +4 -0
- package/node_modules/pi-lens/dist/clients/lsp/client.js +516 -31
- package/node_modules/pi-lens/dist/clients/lsp/config.js +25 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +9 -4
- package/node_modules/pi-lens/dist/clients/lsp/index.js +509 -182
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +5 -0
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +36 -8
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +116 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +142 -13
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +99 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +16 -6
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +32 -23
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +16 -2
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +12 -6
- package/node_modules/pi-lens/dist/clients/mcp/session.js +14 -1
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +23 -1
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +30 -0
- package/node_modules/pi-lens/dist/clients/metrics-history.js +21 -4
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +232 -18
- package/node_modules/pi-lens/dist/clients/package-manager.js +6 -18
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +14 -1
- package/node_modules/pi-lens/dist/clients/path-utils.js +32 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -8
- package/node_modules/pi-lens/dist/clients/process-singletons.js +210 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +2 -1
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +10 -4
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +33 -11
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +24 -2
- package/node_modules/pi-lens/dist/clients/read-guard.js +74 -7
- package/node_modules/pi-lens/dist/clients/recent-touches.js +17 -5
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +639 -62
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +5 -1
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +21 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +68 -0
- package/node_modules/pi-lens/dist/clients/runtime-session.js +30 -12
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +15 -0
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +106 -7
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +242 -27
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +158 -18
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +48 -17
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +238 -42
- package/node_modules/pi-lens/dist/clients/sg-runner.js +27 -15
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +338 -0
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +37 -0
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +15 -1
- package/node_modules/pi-lens/dist/clients/startup-scan.js +6 -7
- package/node_modules/pi-lens/dist/clients/startup-timing.js +69 -4
- package/node_modules/pi-lens/dist/clients/string-utils.js +20 -0
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +419 -88
- package/node_modules/pi-lens/dist/clients/tool-policy.js +1 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +11 -5
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +17 -1
- package/node_modules/pi-lens/dist/clients/warm-attach.js +8 -10
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +13 -1
- package/node_modules/pi-lens/dist/clients/word-index-store.js +448 -0
- package/node_modules/pi-lens/dist/clients/word-index.js +582 -104
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +17 -0
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +26 -6
- package/node_modules/pi-lens/dist/index.js +11243 -6651
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +8 -7
- package/node_modules/pi-lens/dist/mcp/cli.js +3 -1
- package/node_modules/pi-lens/dist/mcp/server.js +6 -1
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +4 -0
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +14 -5
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +3 -1
- package/node_modules/pi-lens/docs/agent-guide.md +4 -2
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +41 -10
- package/node_modules/pi-lens/docs/astplayground.md +26 -17
- package/node_modules/pi-lens/package.json +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/c-no-malloc-free-test.yml +20 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-malloc-free-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/cpp-no-printf-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-fmt-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-panic-in-lib-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-no-underscore-func-name-test.yml +18 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-errors-is-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/go-prefer-string-builder-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/hyphenated-svg-attribute-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-lateinit-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-nullable-boolean-test.yml +15 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-println-test.yml +19 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unnecessary-let-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-unsafe-cast-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-no-var-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-data-class-test.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/kotlin-prefer-is-empty-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-important-test.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-types-test.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-string-concat-in-loop-test.yml +100 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-system-out-println-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/prefer-string-is-empty-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/python-optional-type-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/reducible-list-comprehension-test.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ruby-symbol-to-proc-candidate-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/rust-no-chars-enumerate-test.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/c-no-malloc-free.yml +39 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-malloc-free.yml +45 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/cpp-no-printf.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-fmt-println.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-panic-in-lib.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-no-underscore-func-name.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-errors-is.yml +40 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/go-prefer-string-builder.yml +43 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/hyphenated-svg-attribute.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-lateinit.yml +37 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-nullable-boolean.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-println.yml +42 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unnecessary-let.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-unsafe-cast.yml +28 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-no-var.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-data-class.yml +66 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/kotlin-prefer-is-empty.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-important.yml +21 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-types.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-string-concat-in-loop.yml +90 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-system-out-println.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/prefer-string-is-empty.yml +11 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/python-optional-type.yml +22 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/reducible-list-comprehension.yml +29 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ruby-symbol-to-proc-candidate.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/rust-no-chars-enumerate.yml +19 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +35 -0
- package/node_modules/pi-mcp-adapter/README.md +5 -2
- package/node_modules/pi-mcp-adapter/cli.js +4 -4
- package/node_modules/pi-mcp-adapter/commands.ts +1 -1
- package/node_modules/pi-mcp-adapter/config.ts +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js +1 -0
- package/node_modules/pi-mcp-adapter/dist/config.js.map +1 -1
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.d.ts +24 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js +336 -0
- package/node_modules/pi-mcp-adapter/dist/mcp-bearer-store.js.map +1 -0
- package/node_modules/pi-mcp-adapter/dist/types.d.ts +3 -1
- package/node_modules/pi-mcp-adapter/dist/types.js.map +1 -1
- package/node_modules/pi-mcp-adapter/index.ts +90 -6
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +124 -32
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +0 -2
- package/node_modules/pi-mcp-adapter/mcp-oauth-provider.ts +89 -0
- package/node_modules/pi-mcp-adapter/mcp-references.ts +9 -1
- package/node_modules/pi-mcp-adapter/package.json +1 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +62 -22
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +1 -1
- package/node_modules/pi-mcp-adapter/server-manager.ts +6 -0
- package/node_modules/pi-mcp-adapter/types.ts +3 -1
- package/node_modules/pi-web-access/CHANGELOG.md +42 -0
- package/node_modules/pi-web-access/README.md +26 -10
- package/node_modules/pi-web-access/curator-page.ts +12 -3
- package/node_modules/pi-web-access/curator-server.ts +3 -1
- package/node_modules/pi-web-access/extract.ts +88 -13
- package/node_modules/pi-web-access/gemini-search.ts +10 -5
- package/node_modules/pi-web-access/github-extract.ts +47 -3
- package/node_modules/pi-web-access/index.ts +40 -40
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +61 -8
- package/node_modules/pi-web-access/xcrawl.ts +264 -0
- package/package.json +19 -22
- package/node_modules/@herbertgao/pi-stash/README.md +0 -34
- package/node_modules/@herbertgao/pi-stash/package.json +0 -51
- package/node_modules/@herbertgao/pi-stash/src/index.ts +0 -118
- package/node_modules/@tifan/pi-titlebar-spinner/README.md +0 -24
- package/node_modules/@tifan/pi-titlebar-spinner/package.json +0 -39
- package/node_modules/@tifan/pi-titlebar-spinner/src/index.ts +0 -49
|
@@ -43,12 +43,32 @@
|
|
|
43
43
|
* runtime condition — that edge is invisible to this gate, so a blocker whose only
|
|
44
44
|
* stale dependency arrives that way survives a replay. This is an honest boundary,
|
|
45
45
|
* not a silently-closed one.
|
|
46
|
+
*
|
|
47
|
+
* Delivery cap (#1950). A demoted-but-confirmable entry (`alreadyStale` above)
|
|
48
|
+
* is deliberately NOT retired after its past-EOF sibling's one-delivery rule
|
|
49
|
+
* (#1944, `blocker-past-eof.ts`) — its coordinates are still in bounds, so a
|
|
50
|
+
* fresh dispatch can genuinely confirm or clear it, and retiring on delivery
|
|
51
|
+
* one would discard a recoverable finding. But nothing capped how many times
|
|
52
|
+
* the SAME demoted record re-serves: incident data (#1944's lane sweep) showed
|
|
53
|
+
* 18 `alreadyStale` re-serves in one window, with repeat deliveries carrying
|
|
54
|
+
* near-zero information after the first. `runtime-turn.ts` counts each
|
|
55
|
+
* delivery (`InlineBlockerRecord.staleDeliveryCount`) and retires the record
|
|
56
|
+
* via `RuntimeCoordinator.retireDemotedDependencyDriftBlocker` once it reaches
|
|
57
|
+
* `DEPENDENCY_DRIFT_MAX_DELIVERIES` — a count, not a TTL or a session
|
|
58
|
+
* boundary, because "how many times has the agent already been told" is the
|
|
59
|
+
* quantity that actually went to zero information, not "how much time has
|
|
60
|
+
* passed". The ledger reason says "capped, re-run can still confirm" so it
|
|
61
|
+
* reads distinctly from #1944's "retired, unrecoverable" — a capped record
|
|
62
|
+
* COULD still resolve a fresh dispatch; it just stopped being handed one for
|
|
63
|
+
* free.
|
|
46
64
|
*/
|
|
47
65
|
import * as fs from "node:fs";
|
|
48
66
|
import { normalizeEphemeralMapKey } from "./path-utils.js";
|
|
49
67
|
import { resolveImportToFiles } from "./review-graph/import-resolvers.js";
|
|
50
68
|
import { getSharedTreeSitterClient, resolveTreeSitterLanguage, } from "./tree-sitter-shared.js";
|
|
51
69
|
import { TreeSitterSymbolExtractor } from "./tree-sitter-symbol-extractor.js";
|
|
70
|
+
/** See the module doc's "Delivery cap (#1950)" section above. */
|
|
71
|
+
export const DEPENDENCY_DRIFT_MAX_DELIVERIES = 3;
|
|
52
72
|
/**
|
|
53
73
|
* Upper bound on the number of forward imports stat-checked for a single blocker.
|
|
54
74
|
* Blocking entries are rare, but one file can import many modules; the sweep must
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
* that must re-arm at `session_start` and will eventually be forgotten
|
|
25
25
|
* (the #1266–#1625 defect class).
|
|
26
26
|
* 2. **A per-turn cap names its turn.** `capPerTurn` carries the caller's
|
|
27
|
-
* `turnIndex` in the same object as the limit
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `noteImportResolutionMemoTurn` (`clients/blocker-freshness.ts`).
|
|
27
|
+
* `turnIndex` in the same object as the limit. A bounded 64-turn window
|
|
28
|
+
* keeps overlapping async turns independent; results older than that window
|
|
29
|
+
* fail closed. Session start clears the window when numbering restarts.
|
|
31
30
|
* 3. **Identity always survives.** Every emitted record carries the
|
|
32
31
|
* discriminating identity in `filePath` and in `metadata.identity`, so
|
|
33
32
|
* aggregation can still answer "which file, which method, which record is
|
|
@@ -85,6 +84,14 @@ export const BOUNDED_TELEMETRY_PHASES = [
|
|
|
85
84
|
* exact count.
|
|
86
85
|
*/
|
|
87
86
|
"lsp_warm_client_missing",
|
|
87
|
+
/** #2052: a file's nearest LSP root is outside the session ceiling. */
|
|
88
|
+
"lsp_capability_skip",
|
|
89
|
+
/**
|
|
90
|
+
* #2044: failed-first test state was retired after a confirmed missing path,
|
|
91
|
+
* retained because the filesystem verdict was indeterminate, or evicted at
|
|
92
|
+
* the state cap. Selection checks and detailed rows are both capped per turn.
|
|
93
|
+
*/
|
|
94
|
+
"test_runner_failed_target_state",
|
|
88
95
|
/**
|
|
89
96
|
* #1723: an event-loop block at or above the floor. Not a degradation, so
|
|
90
97
|
* no ledger kind; bounded by call cadence (one `turn_end` runs it once per
|
|
@@ -97,6 +104,17 @@ export const BOUNDED_TELEMETRY_PHASES = [
|
|
|
97
104
|
* queue of them, so it is rising-edge per event name.
|
|
98
105
|
*/
|
|
99
106
|
"session_event_stale_ctx_skip",
|
|
107
|
+
/**
|
|
108
|
+
* #2007: a worktree-mutating git command declined because another live
|
|
109
|
+
* session shares this dirty checkout. Rising edge per checkout root; the
|
|
110
|
+
* ledger keeps the exact repeat count.
|
|
111
|
+
*/
|
|
112
|
+
"shared_checkout_switch_blocked",
|
|
113
|
+
/**
|
|
114
|
+
* #2007: `git status` could not report the working-tree state, so the same
|
|
115
|
+
* command was declined on an UNKNOWN rather than assumed clean.
|
|
116
|
+
*/
|
|
117
|
+
"shared_checkout_probe_failed",
|
|
100
118
|
];
|
|
101
119
|
const REGISTERED_PHASES = new Set(BOUNDED_TELEMETRY_PHASES);
|
|
102
120
|
/** Report whether a phase name is in the registry (used by the sweep). */
|
|
@@ -104,14 +122,15 @@ export function isBoundedTelemetryPhase(phase) {
|
|
|
104
122
|
return REGISTERED_PHASES.has(phase);
|
|
105
123
|
}
|
|
106
124
|
/**
|
|
107
|
-
* Per-turn admission counters, keyed by phase.
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* count across a session boundary that happened to land on the same index).
|
|
125
|
+
* Per-turn admission counters, keyed by turn and phase. Multiple turns remain
|
|
126
|
+
* live because async work can finish T1 after T2 has already emitted. The map
|
|
127
|
+
* retains a bounded recent window; work older than that window fails closed
|
|
128
|
+
* rather than reopening an exhausted budget. Session start clears all state.
|
|
112
129
|
*/
|
|
130
|
+
const MAX_TRACKED_TURN_CAPS = 64;
|
|
113
131
|
let turnCounts = new Map();
|
|
114
|
-
let
|
|
132
|
+
let latestCountedTurnIndex;
|
|
133
|
+
let retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
|
|
115
134
|
/**
|
|
116
135
|
* Decide whether this occurrence earns a detailed record, and count it in the
|
|
117
136
|
* ledger either way. Returns `true` when the caller should write the record.
|
|
@@ -180,14 +199,25 @@ export function emitBounded(phase, identity, payload, options = {}) {
|
|
|
180
199
|
function admitAgainstTurnCap(phase, cap) {
|
|
181
200
|
if (cap === undefined)
|
|
182
201
|
return true;
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
202
|
+
if (cap.turnIndex <= retiredThroughTurnIndex)
|
|
203
|
+
return false;
|
|
204
|
+
let counts = turnCounts.get(cap.turnIndex);
|
|
205
|
+
if (!counts) {
|
|
206
|
+
if (turnCounts.size >= MAX_TRACKED_TURN_CAPS) {
|
|
207
|
+
const oldestTrackedTurn = Math.min(...turnCounts.keys());
|
|
208
|
+
if (cap.turnIndex < oldestTrackedTurn)
|
|
209
|
+
return false;
|
|
210
|
+
turnCounts.delete(oldestTrackedTurn);
|
|
211
|
+
retiredThroughTurnIndex = Math.max(retiredThroughTurnIndex, oldestTrackedTurn);
|
|
212
|
+
}
|
|
213
|
+
counts = new Map();
|
|
214
|
+
turnCounts.set(cap.turnIndex, counts);
|
|
186
215
|
}
|
|
187
|
-
|
|
216
|
+
latestCountedTurnIndex = Math.max(latestCountedTurnIndex ?? cap.turnIndex, cap.turnIndex);
|
|
217
|
+
const used = counts.get(phase) ?? 0;
|
|
188
218
|
if (used >= cap.limit)
|
|
189
219
|
return false;
|
|
190
|
-
|
|
220
|
+
counts.set(phase, used + 1);
|
|
191
221
|
return true;
|
|
192
222
|
}
|
|
193
223
|
/**
|
|
@@ -198,9 +228,16 @@ function admitAgainstTurnCap(phase, cap) {
|
|
|
198
228
|
*/
|
|
199
229
|
export function resetBoundedTelemetry() {
|
|
200
230
|
turnCounts = new Map();
|
|
201
|
-
|
|
231
|
+
latestCountedTurnIndex = undefined;
|
|
232
|
+
retiredThroughTurnIndex = Number.NEGATIVE_INFINITY;
|
|
233
|
+
}
|
|
234
|
+
/** Test-only: one turn's live admission count for a phase. */
|
|
235
|
+
export function _boundedTurnCountForTest(phase, turnIndex = latestCountedTurnIndex) {
|
|
236
|
+
if (turnIndex === undefined)
|
|
237
|
+
return 0;
|
|
238
|
+
return turnCounts.get(turnIndex)?.get(phase) ?? 0;
|
|
202
239
|
}
|
|
203
|
-
/** Test-only:
|
|
204
|
-
export function
|
|
205
|
-
return turnCounts.
|
|
240
|
+
/** Test-only: number of turn buckets retained by the bounded counter. */
|
|
241
|
+
export function _boundedTrackedTurnsForTest() {
|
|
242
|
+
return turnCounts.size;
|
|
206
243
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #1775: `sessionstart.log` recorded no build identity — no commit hash, no
|
|
3
|
+
* build timestamp. Dogfood forensics ("does this session include PR #1727?")
|
|
4
|
+
* had to reconstruct that by hand: run `git merge-base --is-ancestor` in the
|
|
5
|
+
* serving checkout and compare `dist/` mtimes against the session-start time.
|
|
6
|
+
*
|
|
7
|
+
* The running extension can be a different checkout than the repo an
|
|
8
|
+
* investigator is looking at, so identity is derived from the RUNNING
|
|
9
|
+
* build's own files — `getPackageRoot` (already used by grammar-source.ts to
|
|
10
|
+
* find pi-lens's own assets under both the unbundled dev layout and a
|
|
11
|
+
* packaged install) plus the mtime of the very entry file that is executing
|
|
12
|
+
* right now — never from `process.cwd()` and never assumed to be a git
|
|
13
|
+
* checkout.
|
|
14
|
+
*
|
|
15
|
+
* Commit resolution reuses `review-graph/git-identity.ts`'s
|
|
16
|
+
* `resolveGitIdentity` — a PURE filesystem read, no `git` subprocess. This
|
|
17
|
+
* runs inside the `session_start` handler, ahead of the #473
|
|
18
|
+
* concurrent-secondary classification, so every in-process subagent pays
|
|
19
|
+
* whatever this costs; a synchronous `git rev-parse`/`git status` spawn pair
|
|
20
|
+
* (fix round 1's approach, reverted here) measured 150-300ms typical and up
|
|
21
|
+
* to ~10s worst case on two 5s timeouts. `resolveGitIdentity` is a handful of
|
|
22
|
+
* `fs.readFileSync` calls, LRU-cached per process, so this path never spawns.
|
|
23
|
+
*
|
|
24
|
+
* The dirty working-tree flag genuinely needs `git status`, which has no
|
|
25
|
+
* spawn-free equivalent — so it is dropped from this record rather than
|
|
26
|
+
* reintroducing a spawn on the session-start hot path (refs #1775 followup
|
|
27
|
+
* comment: dirty flag deferred, commit + entryMtime + version shipped).
|
|
28
|
+
*/
|
|
29
|
+
import * as fs from "node:fs";
|
|
30
|
+
import * as path from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { isTestMode } from "./env-utils.js";
|
|
33
|
+
import { getPackageRoot } from "./package-root.js";
|
|
34
|
+
import { normalizeFilePath } from "./path-utils.js";
|
|
35
|
+
import { resolveGitIdentity } from "./review-graph/git-identity.js";
|
|
36
|
+
/**
|
|
37
|
+
* A `.git` found by walking up from `root` only counts as owning `root` when
|
|
38
|
+
* its resolved worktree top-level IS `root` — not merely an ancestor of it.
|
|
39
|
+
* Without this check, a packaged install nested anywhere under an unrelated
|
|
40
|
+
* repo (a dotfiles checkout in $HOME is the common case) would confidently
|
|
41
|
+
* report that repo's HEAD as the running build's identity.
|
|
42
|
+
*/
|
|
43
|
+
function resolveOwnedCommit(root) {
|
|
44
|
+
const identity = resolveGitIdentity(root);
|
|
45
|
+
if (!identity)
|
|
46
|
+
return "unknown";
|
|
47
|
+
const normalizedRoot = normalizeFilePath(path.resolve(root));
|
|
48
|
+
if (identity.worktreeRoot !== normalizedRoot)
|
|
49
|
+
return "unknown";
|
|
50
|
+
return identity.headCommit.slice(0, 8);
|
|
51
|
+
}
|
|
52
|
+
function readPackageVersion(root) {
|
|
53
|
+
try {
|
|
54
|
+
const raw = fs.readFileSync(path.join(root, "package.json"), "utf-8");
|
|
55
|
+
const pkg = JSON.parse(raw);
|
|
56
|
+
return pkg.version ?? "unknown";
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return "unknown";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function statEntryMtime(entryImportMetaUrl) {
|
|
63
|
+
try {
|
|
64
|
+
return fs.statSync(fileURLToPath(entryImportMetaUrl)).mtime.toISOString();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Entry path unresolvable (e.g. a non-file:// URL) — "unknown" rather
|
|
68
|
+
// than throw out of a session-start observability line.
|
|
69
|
+
return "unknown";
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve the identity of the build currently running — pure filesystem
|
|
74
|
+
* reads, no subprocess. `entryImportMetaUrl` must be the ENTRY module's own
|
|
75
|
+
* `import.meta.url` (index.ts) — passing a different module's URL would
|
|
76
|
+
* report that module's mtime instead of the running build's.
|
|
77
|
+
*
|
|
78
|
+
* Returns `undefined` inside the test runner: nothing computed, nothing
|
|
79
|
+
* logged (`sessionstart-logger`'s writer is a no-op there too), so a caller
|
|
80
|
+
* on the session-start hot path never pays this cost during tests.
|
|
81
|
+
*/
|
|
82
|
+
export function getBuildIdentity(entryImportMetaUrl) {
|
|
83
|
+
if (isTestMode())
|
|
84
|
+
return undefined;
|
|
85
|
+
const root = getPackageRoot(entryImportMetaUrl);
|
|
86
|
+
return {
|
|
87
|
+
commit: resolveOwnedCommit(root),
|
|
88
|
+
entryMtime: statEntryMtime(entryImportMetaUrl),
|
|
89
|
+
version: readPackageVersion(root),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** One bounded, human-readable line for sessionstart.log's free-text writer. */
|
|
93
|
+
export function formatBuildIdentity(identity) {
|
|
94
|
+
return `session_start: build identity — commit=${identity.commit} entryMtime=${identity.entryMtime} version=${identity.version}`;
|
|
95
|
+
}
|
|
@@ -23,6 +23,7 @@ import { appendRecentTouches } from "./recent-touches.js";
|
|
|
23
23
|
import { createLiveBusEmitter, recordStaleBusFailure, resolveLiveBusEmitter, } from "./live-bus-emitter.js";
|
|
24
24
|
export const BUS_FILES_TOUCHED_EVENT = "pilens:files:touched";
|
|
25
25
|
export const BUS_FILES_TOUCHED_VERSION = 1;
|
|
26
|
+
const BUS_LOG_PATH_CAP = 64;
|
|
26
27
|
const liveEmitter = createLiveBusEmitter();
|
|
27
28
|
let hasLoggedFailure = false;
|
|
28
29
|
// Log-once-per-process guards for the two structural-no-op outcomes below
|
|
@@ -148,6 +149,8 @@ export function publishFilesTouched(args) {
|
|
|
148
149
|
cwd: payload.cwd,
|
|
149
150
|
reason: args.reason,
|
|
150
151
|
fileCount: payload.paths.length,
|
|
152
|
+
writer: "pi-lens",
|
|
153
|
+
paths: payload.paths.slice(0, BUS_LOG_PATH_CAP),
|
|
151
154
|
});
|
|
152
155
|
}
|
|
153
156
|
catch (err) {
|
|
@@ -11,6 +11,7 @@ import { getProjectDataDir } from "../file-utils.js";
|
|
|
11
11
|
import { readJsonCache } from "../json-cache-read.js";
|
|
12
12
|
import { resolvePackagePath } from "../package-root.js";
|
|
13
13
|
import { writeFileAtomic } from "../atomic-write.js";
|
|
14
|
+
import { compareOrdinal } from "../string-utils.js";
|
|
14
15
|
// v4: cache skip_test_files + fix_action — v3 entries silently dropped them,
|
|
15
16
|
// and ruleHash (rule-file mtimes) never invalidates on a code-only fix.
|
|
16
17
|
// v5 (#675): rule SELECTION changed in code — javascript no longer inherits the
|
|
@@ -69,7 +70,7 @@ export class RuleCache {
|
|
|
69
70
|
}
|
|
70
71
|
computeRuleHash(ruleFiles) {
|
|
71
72
|
const hash = crypto.createHash("sha256");
|
|
72
|
-
for (const file of ruleFiles.sort(
|
|
73
|
+
for (const file of ruleFiles.sort(compareOrdinal)) {
|
|
73
74
|
const resolved = path.resolve(file);
|
|
74
75
|
if (!fs.existsSync(resolved))
|
|
75
76
|
continue;
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
import { createHash } from "node:crypto";
|
|
51
51
|
import { lazyEnvNumber } from "./env-utils.js";
|
|
52
52
|
import { logLatency } from "./latency-logger.js";
|
|
53
|
+
import { compareOrdinal } from "./string-utils.js";
|
|
53
54
|
/** RuntimeCoordinator's counter is process-global when sessions share a host. */
|
|
54
55
|
const PROCESS_TURN_SCOPE = "process-global-runtime";
|
|
55
56
|
const SECONDARY_TURN_SCOPE = "unavailable-concurrent-secondary";
|
|
@@ -490,7 +491,7 @@ function boundedHashArray(value, depth, seen, state) {
|
|
|
490
491
|
return `array:${value.length}:[${items.join(",")}]`;
|
|
491
492
|
}
|
|
492
493
|
function boundedHashObject(value, depth, seen, state) {
|
|
493
|
-
const keys = Object.keys(value).sort((left, right) => left
|
|
494
|
+
const keys = Object.keys(value).sort((left, right) => compareOrdinal(left, right));
|
|
494
495
|
if (keys.length > 24)
|
|
495
496
|
state.contentTruncated = true;
|
|
496
497
|
const fields = keys
|
|
@@ -3,17 +3,31 @@ import { isTestMode } from "./env-utils.js";
|
|
|
3
3
|
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
4
4
|
import { createNdjsonLogger } from "./ndjson-logger.js";
|
|
5
5
|
import { getMaxLogSizeMB } from "./log-cleanup.js";
|
|
6
|
+
import { normalizeLoggedPath } from "./path-utils.js";
|
|
6
7
|
const CASCADE_LOG_DIR = getGlobalPiLensDir();
|
|
7
8
|
const CASCADE_LOG_FILE = path.join(CASCADE_LOG_DIR, "cascade.log");
|
|
8
9
|
const writer = createNdjsonLogger({
|
|
9
10
|
filePath: CASCADE_LOG_FILE,
|
|
10
11
|
maxBytes: getMaxLogSizeMB() * 1024 * 1024,
|
|
11
12
|
});
|
|
13
|
+
/**
|
|
14
|
+
* #2219 (the #2141 class): call sites across `dispatch/integration.ts`,
|
|
15
|
+
* `runtime-turn.ts`, and `runtime-coordinator.ts` feed a mix of raw
|
|
16
|
+
* `filePath`/`cwd` params and (for `lsp/cascade-tier.ts`) the
|
|
17
|
+
* `"<quiet-window>"` sentinel. Normalize once here, the single emit seam —
|
|
18
|
+
* same pattern as `review-graph-logger.ts`'s `logReviewGraph`, guarded via
|
|
19
|
+
* `normalizeLoggedPath` so the non-path sentinel passes through unchanged
|
|
20
|
+
* instead of being resolved against the process cwd.
|
|
21
|
+
*/
|
|
12
22
|
export function logCascade(entry) {
|
|
13
23
|
if (isTestMode()) {
|
|
14
24
|
return;
|
|
15
25
|
}
|
|
16
|
-
writer.log({
|
|
26
|
+
writer.log({
|
|
27
|
+
ts: new Date().toISOString(),
|
|
28
|
+
...entry,
|
|
29
|
+
filePath: normalizeLoggedPath(entry.filePath),
|
|
30
|
+
});
|
|
17
31
|
}
|
|
18
32
|
export function getCascadeLogPath() {
|
|
19
33
|
return CASCADE_LOG_FILE;
|
|
@@ -101,9 +101,9 @@ export function parseVultureOutput(output, root) {
|
|
|
101
101
|
*
|
|
102
102
|
* vulture finds unused functions, classes, methods, imports, variables and
|
|
103
103
|
* attributes by static analysis of the whole tree. It has no JSON reporter, so
|
|
104
|
-
* we parse its stable one-line-per-finding text output.
|
|
105
|
-
*
|
|
106
|
-
* output is success, not failure.
|
|
104
|
+
* we parse its stable one-line-per-finding text output. A clean run exits 0;
|
|
105
|
+
* found dead code exits 3 with findings on stdout. A non-zero exit with
|
|
106
|
+
* parseable output is success, not failure (see runAnalyze for the table).
|
|
107
107
|
*/
|
|
108
108
|
export class PythonDeadCodeClient {
|
|
109
109
|
id = "python";
|
|
@@ -245,6 +245,7 @@ export class PythonDeadCodeClient {
|
|
|
245
245
|
latched: true,
|
|
246
246
|
hostStallMs,
|
|
247
247
|
budgetMs: 5000,
|
|
248
|
+
classifiedBy: "probe",
|
|
248
249
|
});
|
|
249
250
|
return true;
|
|
250
251
|
}
|
|
@@ -370,14 +371,16 @@ export class PythonDeadCodeClient {
|
|
|
370
371
|
durationMs,
|
|
371
372
|
};
|
|
372
373
|
}
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
// findings on stdout
|
|
376
|
-
//
|
|
377
|
-
//
|
|
378
|
-
//
|
|
379
|
-
//
|
|
380
|
-
//
|
|
374
|
+
// Verified exit-code table (vulture 2.16, probed live during the #1758
|
|
375
|
+
// review): a clean run exits 0 with empty stdout; dead code found exits
|
|
376
|
+
// 3 with findings on stdout; invalid input or a parse error exits 1
|
|
377
|
+
// with empty stdout and the error on stderr. #1736 sweep: the ORIGINAL
|
|
378
|
+
// guard here required non-empty stderr to call it an error, so a
|
|
379
|
+
// nonzero exit with BOTH empty stdout and empty stderr (a silent crash)
|
|
380
|
+
// still fell through to "No dead code found" -- the same
|
|
381
|
+
// empty-distinguishes-clean-from-errored gap the knip fix closes. A
|
|
382
|
+
// nonzero exit with no findings on stdout is never clean now,
|
|
383
|
+
// regardless of whether stderr said anything.
|
|
381
384
|
const output = result.stdout || "";
|
|
382
385
|
if (!output.trim()) {
|
|
383
386
|
const stderr = (result.stderr || "").trim();
|
|
@@ -3,6 +3,12 @@ import { logExtension } from "./extension-log.js";
|
|
|
3
3
|
import { LEDGER_FIELD_MAX, truncateForLedger } from "./ledger-bounds.js";
|
|
4
4
|
import { logLatency } from "./latency-logger.js";
|
|
5
5
|
import { getSinkWriteFailures, resetSinkWriteFailures, } from "./ndjson-logger.js";
|
|
6
|
+
// #2146: pulled at READ time, never pushed. `process-singletons.ts` is a
|
|
7
|
+
// dependency leaf on purpose — it cannot import this module without closing a
|
|
8
|
+
// no-client-cycles cycle through instance-registry/instance-reaper — so the
|
|
9
|
+
// ledger reaches IN for its reset log, the same inversion `getSinkWriteFailures`
|
|
10
|
+
// above uses.
|
|
11
|
+
import { getProcessSingletonResets, PROCESS_SINGLETON_RESET_KIND, } from "./process-singletons.js";
|
|
6
12
|
// Re-exported so existing importers keep one name for the ledger's bound.
|
|
7
13
|
export { LEDGER_FIELD_MAX, truncateForLedger };
|
|
8
14
|
const ENTRIES_PER_KIND = 20;
|
|
@@ -59,7 +65,7 @@ export function recordDegradationOnce(record) {
|
|
|
59
65
|
return;
|
|
60
66
|
onceKeys.add(key);
|
|
61
67
|
if (recordDegradation({ kind, subject, reason: record.reason })) {
|
|
62
|
-
logDurableDegradation(kind, subject, 1);
|
|
68
|
+
logDurableDegradation(kind, subject, 1, record.metadata);
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
catch (error) {
|
|
@@ -108,7 +114,7 @@ export function incrementDegradationCount(record) {
|
|
|
108
114
|
// Durable rows use the summary's admission and emit the first event and
|
|
109
115
|
// power-of-two milestones only, keeping the sink bounded.
|
|
110
116
|
if (admitted && isPowerOfTwo(count)) {
|
|
111
|
-
logDurableDegradation(kind, subject, count);
|
|
117
|
+
logDurableDegradation(kind, subject, count, record.metadata);
|
|
112
118
|
}
|
|
113
119
|
return count === 1;
|
|
114
120
|
}
|
|
@@ -124,15 +130,34 @@ export function incrementDegradationCount(record) {
|
|
|
124
130
|
* redaction, and write queue. The subject and kind were already bounded by the
|
|
125
131
|
* ledger before reaching this seam.
|
|
126
132
|
*/
|
|
127
|
-
function logDurableDegradation(kind, subject, count) {
|
|
133
|
+
function logDurableDegradation(kind, subject, count, metadata) {
|
|
134
|
+
const boundedMetadata = boundLedgerMetadata(metadata);
|
|
128
135
|
logLatency({
|
|
129
136
|
type: "phase",
|
|
130
137
|
phase: "degradation_ledger",
|
|
131
138
|
filePath: subject,
|
|
132
139
|
durationMs: 0,
|
|
133
|
-
metadata: {
|
|
140
|
+
metadata: {
|
|
141
|
+
...boundedMetadata,
|
|
142
|
+
kind,
|
|
143
|
+
subject,
|
|
144
|
+
count,
|
|
145
|
+
ledgerGeneration,
|
|
146
|
+
},
|
|
134
147
|
});
|
|
135
148
|
}
|
|
149
|
+
const MAX_METADATA_KEYS = 8;
|
|
150
|
+
function boundLedgerMetadata(metadata) {
|
|
151
|
+
if (!metadata)
|
|
152
|
+
return {};
|
|
153
|
+
const entries = Object.entries(metadata);
|
|
154
|
+
const kept = entries.slice(0, MAX_METADATA_KEYS);
|
|
155
|
+
const bounded = Object.fromEntries(kept.map(([key, value]) => [key, truncateForLedger(value)]));
|
|
156
|
+
const dropped = entries.length - kept.length;
|
|
157
|
+
if (dropped > 0)
|
|
158
|
+
bounded.metadataDropped = dropped;
|
|
159
|
+
return bounded;
|
|
160
|
+
}
|
|
136
161
|
function isPowerOfTwo(value) {
|
|
137
162
|
return value > 0 && (value & (value - 1)) === 0;
|
|
138
163
|
}
|
|
@@ -165,6 +190,21 @@ export function getDegradationSummary() {
|
|
|
165
190
|
})),
|
|
166
191
|
});
|
|
167
192
|
}
|
|
193
|
+
// #2146, same read-time fold: process-singleton resets live in the leaf
|
|
194
|
+
// module's own bounded log. One entry per family, so this group's count is
|
|
195
|
+
// the number of families this build could not adopt, never an event tally.
|
|
196
|
+
const singletonResets = getProcessSingletonResets();
|
|
197
|
+
if (singletonResets.length > 0) {
|
|
198
|
+
summary.push({
|
|
199
|
+
kind: PROCESS_SINGLETON_RESET_KIND,
|
|
200
|
+
count: singletonResets.length,
|
|
201
|
+
droppedCount: 0,
|
|
202
|
+
latestReasons: singletonResets.map((reset) => ({
|
|
203
|
+
subject: truncateForLedger(reset.family),
|
|
204
|
+
reason: truncateForLedger(reset.reason),
|
|
205
|
+
})),
|
|
206
|
+
});
|
|
207
|
+
}
|
|
168
208
|
return summary;
|
|
169
209
|
}
|
|
170
210
|
function isRenderableSummary(value) {
|
|
@@ -218,6 +258,18 @@ export function resetDegradationLedger() {
|
|
|
218
258
|
// process-lifetime latch too — it re-arms alongside the rest of the
|
|
219
259
|
// ledger rather than surviving past the session that observed it.
|
|
220
260
|
resetSinkWriteFailures();
|
|
261
|
+
// #2146 review F3: the OTHER pulled source, `getProcessSingletonResets()`,
|
|
262
|
+
// deliberately does NOT re-arm here, and the difference from its neighbour
|
|
263
|
+
// above is the point. A sink write failure recurs — new writes fail, so
|
|
264
|
+
// clearing the tally costs nothing and a later session re-observes the
|
|
265
|
+
// problem. A process-singleton reset happens once, at module-evaluation
|
|
266
|
+
// time, and cannot recur: after it, the container holds only compatible
|
|
267
|
+
// cells. Clearing it would show the fact in the first session's
|
|
268
|
+
// `pilens_health` and hide it from every session after, which is exactly
|
|
269
|
+
// when someone reads that line. The row is bounded independently of the
|
|
270
|
+
// session (one entry per family, capped at 16), so leaving it costs a fixed
|
|
271
|
+
// handful of lines and keeps a process-scope fact visible for the process's
|
|
272
|
+
// life. Deliberate exception to catalog shape 17, not an oversight.
|
|
221
273
|
}
|
|
222
274
|
export const DEGRADATION_ENTRIES_PER_KIND = ENTRIES_PER_KIND;
|
|
223
275
|
export const DEGRADATION_MAX_DISTINCT_KINDS = MAX_DISTINCT_KINDS;
|
|
@@ -153,3 +153,17 @@ export function formatRetirementNote(deadLines) {
|
|
|
153
153
|
return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
|
|
154
154
|
"so this finding cannot be re-confirmed and will not be served again.");
|
|
155
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The one-line note a surface appends when it RETIRES a dependency-drift
|
|
158
|
+
* demotion at its delivery cap (#1950). Deliberately worded differently from
|
|
159
|
+
* {@link formatRetirementNote}: that one means the finding is provably
|
|
160
|
+
* unrecoverable (the cited lines are gone), while THIS retirement is a
|
|
161
|
+
* bounded-noise cap on a record that is still recoverable — a fresh dispatch
|
|
162
|
+
* of the file could still confirm or clear it. Saying "cannot be re-confirmed"
|
|
163
|
+
* here would be false.
|
|
164
|
+
*/
|
|
165
|
+
export function formatDeliveryCapNote(deliveryCount) {
|
|
166
|
+
return (`Not shown again after ${deliveryCount} deliveries with no re-run: ` +
|
|
167
|
+
"this finding may still be accurate, but re-serving it further adds no " +
|
|
168
|
+
"new information. A fresh dispatch of this file will re-confirm or clear it.");
|
|
169
|
+
}
|
|
@@ -14,6 +14,7 @@ import * as path from "node:path";
|
|
|
14
14
|
import { findNodeToolBinary } from "./package-manager.js";
|
|
15
15
|
import { isFullyQualified } from "./path-utils.js";
|
|
16
16
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
17
|
+
import { compareOrdinal } from "./string-utils.js";
|
|
17
18
|
import { createAvailabilityChecker, discoverManagedTool, getManagedToolEnvironment, resolveAvailableOrInstall, } from "./dispatch/runners/utils/runner-helpers.js";
|
|
18
19
|
import { createAvailabilityLatch, } from "./dispatch/runners/utils/availability-policy.js";
|
|
19
20
|
/**
|
|
@@ -501,8 +502,14 @@ export class DependencyChecker {
|
|
|
501
502
|
const existing = this.checkInFlight.get(key);
|
|
502
503
|
if (existing)
|
|
503
504
|
return existing;
|
|
505
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
506
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
507
|
+
// evict a live successor a second writer registered under the same key
|
|
508
|
+
// mid-flight, after which the next caller starts a duplicate check.
|
|
504
509
|
const promise = this.runCheckFile(normalized, projectRoot, gen).finally(() => {
|
|
505
|
-
this.checkInFlight.
|
|
510
|
+
if (this.checkInFlight.get(key) === promise) {
|
|
511
|
+
this.checkInFlight.delete(key);
|
|
512
|
+
}
|
|
506
513
|
});
|
|
507
514
|
this.checkInFlight.set(key, promise);
|
|
508
515
|
return promise;
|
|
@@ -781,8 +788,14 @@ export class DependencyChecker {
|
|
|
781
788
|
// does, and the two overlap (runtime-session, fresh-fetch), so it takes a
|
|
782
789
|
// generation too — claimed before the spawn, as late as a scan can.
|
|
783
790
|
const gen = ++this.opGeneration;
|
|
791
|
+
// Identity-guarded release (#1968's pattern): delete only if THIS run is
|
|
792
|
+
// still the registered one. A bare delete-by-key lets a late-settling run
|
|
793
|
+
// evict a live successor a second writer registered under the same key
|
|
794
|
+
// mid-flight, after which the next caller starts a duplicate scan.
|
|
784
795
|
const promise = this.runScanProject(projectRoot, gen).finally(() => {
|
|
785
|
-
this.scanInFlight.
|
|
796
|
+
if (this.scanInFlight.get(projectRoot) === promise) {
|
|
797
|
+
this.scanInFlight.delete(projectRoot);
|
|
798
|
+
}
|
|
786
799
|
});
|
|
787
800
|
this.scanInFlight.set(projectRoot, promise);
|
|
788
801
|
return promise;
|
|
@@ -830,7 +843,11 @@ export class DependencyChecker {
|
|
|
830
843
|
const seen = new Set();
|
|
831
844
|
let output = `[Circular Deps] ${circular.length} cycle(s) found:\n`;
|
|
832
845
|
for (const dep of circular) {
|
|
833
|
-
|
|
846
|
+
// Copy before sorting: `dep.path` is rendered verbatim below (and by
|
|
847
|
+
// other CircularDep consumers, e.g. madge.ts's diagnostic renderer),
|
|
848
|
+
// so the dedupe key must not reorder — let alone mutate in place —
|
|
849
|
+
// the path a user reads.
|
|
850
|
+
const cycleKey = [...dep.path].sort(compareOrdinal).join("→");
|
|
834
851
|
if (seen.has(cycleKey))
|
|
835
852
|
continue;
|
|
836
853
|
seen.add(cycleKey);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Observed-latency classification shared by dispatch and LSP tier seams. */
|
|
2
|
+
import { normalizeMapKey } from "../path-utils.js";
|
|
3
|
+
/** A runner gets this much time before the next run moves off the write path. */
|
|
4
|
+
export const COLLECT_LATER_THRESHOLD_MS = 5_000;
|
|
5
|
+
const slowRunners = new Map();
|
|
6
|
+
function key(projectRoot, runnerId) {
|
|
7
|
+
return `${normalizeMapKey(projectRoot)}\u0000${runnerId}`;
|
|
8
|
+
}
|
|
9
|
+
/** Read the current observed tier. Missing observations stay inline. */
|
|
10
|
+
export function classifyObservedRunner(projectRoot, runnerId) {
|
|
11
|
+
return slowRunners.has(key(projectRoot, runnerId))
|
|
12
|
+
? "collect-later"
|
|
13
|
+
: "inline";
|
|
14
|
+
}
|
|
15
|
+
/** Record one completed latency observation and return the resulting tier. */
|
|
16
|
+
export function observeRunnerLatency(options) {
|
|
17
|
+
const normalizedProjectRoot = normalizeMapKey(options.projectRoot);
|
|
18
|
+
const runnerKey = key(normalizedProjectRoot, options.runnerId);
|
|
19
|
+
if (options.timedOut === true ||
|
|
20
|
+
options.durationMs > COLLECT_LATER_THRESHOLD_MS) {
|
|
21
|
+
slowRunners.set(runnerKey, {
|
|
22
|
+
projectRoot: normalizedProjectRoot,
|
|
23
|
+
runnerId: options.runnerId,
|
|
24
|
+
});
|
|
25
|
+
return "collect-later";
|
|
26
|
+
}
|
|
27
|
+
// A fast completed run is affirmative recovery, not merely absence of a
|
|
28
|
+
// failure. This deliberately prevents a process-lifetime latch.
|
|
29
|
+
slowRunners.delete(runnerKey);
|
|
30
|
+
return "inline";
|
|
31
|
+
}
|
|
32
|
+
/** Session boundary reset. */
|
|
33
|
+
export function resetObservedRunnerLatency() {
|
|
34
|
+
slowRunners.clear();
|
|
35
|
+
}
|
|
36
|
+
/** Test-only inspection without exposing mutable state. */
|
|
37
|
+
export function observedRunnerTierForTests(projectRoot, runnerId) {
|
|
38
|
+
return classifyObservedRunner(projectRoot, runnerId);
|
|
39
|
+
}
|