@herbertgao/pi-extensions 2026.8.6 → 2026.8.8
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 +8 -6
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +2 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +19 -7
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +30 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +320 -22
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +94 -77
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/message-display.ts +79 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/hover.ts +27 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +66 -17
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/layout.ts +41 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/diff-renderer.ts +75 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/diff/types.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +64 -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/README.md +30 -22
- package/node_modules/@herbertgao/pi-subagents/examples/agent-tool-description.md +5 -5
- package/node_modules/@herbertgao/pi-subagents/package.json +7 -7
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +7 -1
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +48 -19
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +17 -13
- package/node_modules/@herbertgao/pi-subagents/src/custom-agents.ts +26 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +219 -40
- package/node_modules/@herbertgao/pi-subagents/src/invocation-config.ts +101 -3
- package/node_modules/@herbertgao/pi-subagents/src/nested-tools.ts +18 -4
- package/node_modules/@herbertgao/pi-subagents/src/settings.ts +99 -0
- package/node_modules/@herbertgao/pi-subagents/src/types.ts +22 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +51 -3
- package/node_modules/@herbertgao/pi-subagents/src/ui/conversation-viewer.ts +20 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/fleet-list.ts +21 -6
- package/node_modules/@herbertgao/pi-subagents/src/usage.ts +129 -1
- package/node_modules/@herbertgao/pi-subagents/src/worktree.ts +20 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/README.md +5 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/ask-user-question.ts +174 -100
- package/node_modules/@juicesharp/rpiv-ask-user-question/config.ts +25 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/configuration.md +13 -4
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/hosts.md +5 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +10 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/localization.md +10 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/tool-schema.md +11 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/en.json +5 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/locales/zh.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +3 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +10 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +140 -97
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +63 -28
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/selectors/projections.ts +22 -15
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state-reducer.ts +11 -1
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/state.ts +14 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/response-envelope.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/tool/types.ts +11 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/multi-select-view.ts +31 -17
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/preview/preview-block-renderer.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/components/wrapping-select.ts +15 -6
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/dialog-builder.ts +86 -38
- package/node_modules/@juicesharp/rpiv-ask-user-question/view/tab-content-strategy.ts +76 -9
- package/node_modules/@narumitw/pi-btw/README.md +120 -111
- package/node_modules/@narumitw/pi-btw/dist/index.ts +2898 -0
- package/node_modules/@narumitw/pi-btw/dist/index.ts.map +7 -0
- package/node_modules/@narumitw/pi-btw/package.json +10 -6
- package/node_modules/@narumitw/pi-btw/src/btw.ts +36 -40
- package/node_modules/@narumitw/pi-btw/src/fullscreen-ui.ts +55 -10
- package/node_modules/@narumitw/pi-btw/src/main-tree-picker.ts +390 -0
- package/node_modules/@narumitw/pi-btw/src/menu.ts +45 -2
- package/node_modules/@narumitw/pi-btw/src/transcript-pager.ts +136 -45
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs +44 -1856
- package/node_modules/@pi-plugins/fast-mode/dist/index.mjs.map +1 -1
- package/node_modules/@pi-plugins/fast-mode/package.json +4 -6
- package/node_modules/pi-lens/CHANGELOG.md +999 -0
- package/node_modules/pi-lens/README.md +117 -3
- package/node_modules/pi-lens/config/ruff/core.toml +42 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +130 -27
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +126 -13
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +67 -11
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +9 -3
- package/node_modules/pi-lens/dist/clients/atomic-write.js +9 -3
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +381 -13
- package/node_modules/pi-lens/dist/clients/biome-client.js +61 -30
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +428 -0
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +129 -0
- package/node_modules/pi-lens/dist/clients/bootstrap.js +12 -1
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +12 -3
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +206 -0
- package/node_modules/pi-lens/dist/clients/cache-observability.js +353 -76
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +196 -0
- package/node_modules/pi-lens/dist/clients/cascade-format.js +20 -1
- package/node_modules/pi-lens/dist/clients/cascade-types.js +9 -1
- package/node_modules/pi-lens/dist/clients/child-unref.js +54 -6
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +76 -24
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +123 -31
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +8 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +5 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +84 -14
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +25 -3
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +8 -2
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +8 -3
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +147 -33
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +298 -0
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +52 -2
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +256 -88
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +7 -4
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +5 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +234 -18
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +187 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +6 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +11 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +398 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +9 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +11 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +18 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +9 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +895 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +8 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +4 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +16 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +60 -53
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +16 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +25 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +20 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +44 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +331 -55
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +38 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +97 -19
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +32 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +47 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +14 -4
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +127 -28
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +12 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +73 -12
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +275 -13
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +21 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +260 -27
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +734 -64
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +86 -11
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +188 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +9 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +21 -10
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +18 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +4 -2
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +28 -5
- package/node_modules/pi-lens/dist/clients/extension-log.js +22 -1
- package/node_modules/pi-lens/dist/clients/file-kinds.js +8 -2
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +316 -0
- package/node_modules/pi-lens/dist/clients/finding-identity.js +88 -0
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/formatters.js +500 -115
- package/node_modules/pi-lens/dist/clients/generation-guard.js +216 -0
- package/node_modules/pi-lens/dist/clients/git-guard.js +31 -0
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +181 -8
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +109 -9
- package/node_modules/pi-lens/dist/clients/grammar-source.js +333 -10
- package/node_modules/pi-lens/dist/clients/host-ports.js +8 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +1114 -100
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +58 -0
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +715 -0
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +643 -97
- package/node_modules/pi-lens/dist/clients/instance-registry.js +87 -17
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +87 -8
- package/node_modules/pi-lens/dist/clients/knip-client.js +310 -10
- package/node_modules/pi-lens/dist/clients/language-policy.js +8 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +9 -2
- package/node_modules/pi-lens/dist/clients/latency-logger.js +440 -11
- package/node_modules/pi-lens/dist/clients/lazy-import.js +21 -0
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +21 -0
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +128 -8
- package/node_modules/pi-lens/dist/clients/lsp/client.js +1660 -164
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +83 -1
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +389 -0
- package/node_modules/pi-lens/dist/clients/lsp/index.js +2423 -151
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +215 -0
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +146 -14
- package/node_modules/pi-lens/dist/clients/lsp/language.js +48 -4
- package/node_modules/pi-lens/dist/clients/lsp/server.js +277 -16
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +22 -0
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +79 -3
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +43 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +422 -28
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +30 -0
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +175 -0
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +3 -2
- package/node_modules/pi-lens/dist/clients/mcp/session.js +10 -2
- package/node_modules/pi-lens/dist/clients/module-report.js +22 -1
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +114 -23
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +15 -1
- package/node_modules/pi-lens/dist/clients/package-manager.js +170 -14
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +27 -0
- package/node_modules/pi-lens/dist/clients/path-utils.js +160 -0
- package/node_modules/pi-lens/dist/clients/pipeline.js +154 -33
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +37 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +17 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +89 -22
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +84 -14
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +15 -3
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +50 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +23 -9
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +3 -0
- package/node_modules/pi-lens/dist/clients/project-report.js +62 -27
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +299 -0
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +47 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +11 -25
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +8 -3
- package/node_modules/pi-lens/dist/clients/read-guard.js +253 -10
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +49 -17
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +50 -11
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +194 -31
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +52 -4
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +21 -0
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +6 -3
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +96 -7
- package/node_modules/pi-lens/dist/clients/runtime-context.js +8 -1
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +352 -45
- package/node_modules/pi-lens/dist/clients/runtime-session.js +373 -9
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +217 -22
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +212 -79
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +620 -67
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +220 -7
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +161 -0
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +34 -6
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +157 -24
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +184 -0
- package/node_modules/pi-lens/dist/clients/sg-runner.js +116 -22
- package/node_modules/pi-lens/dist/clients/single-flight.js +158 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +6 -2
- package/node_modules/pi-lens/dist/clients/stale-marker.js +17 -0
- package/node_modules/pi-lens/dist/clients/startup-scan.js +5 -2
- package/node_modules/pi-lens/dist/clients/startup-timing.js +19 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +151 -16
- package/node_modules/pi-lens/dist/clients/tool-event.js +56 -5
- package/node_modules/pi-lens/dist/clients/tool-policy.js +125 -20
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +75 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +769 -64
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +1 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +11 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +90 -0
- package/node_modules/pi-lens/dist/clients/trivy-client.js +19 -1
- package/node_modules/pi-lens/dist/clients/widget-state.js +336 -36
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +214 -14
- package/node_modules/pi-lens/dist/index.js +61407 -48577
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +476 -57
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +132 -43
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -7
- package/node_modules/pi-lens/docs/agent-guide.md +6 -1
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +515 -506
- package/node_modules/pi-lens/docs/dispositions.md +21 -0
- package/node_modules/pi-lens/docs/features.md +19 -5
- package/node_modules/pi-lens/docs/globalconfig.md +3 -0
- package/node_modules/pi-lens/docs/language-coverage.md +71 -5
- package/node_modules/pi-lens/docs/mcp.md +2 -2
- package/node_modules/pi-lens/docs/servercapabilities.md +4 -1
- package/node_modules/pi-lens/docs/settings.md +19 -1
- package/node_modules/pi-lens/docs/usage.md +1 -0
- package/node_modules/pi-lens/package.json +27 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-js-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/no-reflect-get-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/__snapshots__/ts-void-zero-snapshot.yml +23 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-object-param-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-chained-type-assertions-test.yml +15 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-compile-call-test.yml +29 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-dupe-class-members-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-js-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-inner-html-test.yml +7 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-known-value-widening-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-non-null-assertion-test.yml +216 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-js-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-apply-test.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-js-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-reflect-get-test.yml +2 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-return-value-in-generator-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-runtime-typeof-test.yml +33 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-server-bind-wildcard-test.yml +9 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-shape-in-symbol-names-test.yml +13 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-js-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-sql-in-code-test.yml +5 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-parameters-test.yml +12 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unknown-returns-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-any-test.yml +10 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-unsafe-dictionary-unknown-test.yml +8 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/redundant-unsafe-function-test.yml +35 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/require-safety-comment-for-as-unknown-as-test.yml +177 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/ts-void-zero-test.yml +1 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member-js.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-await-expression-member.yml +13 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-object-param.yml +60 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-chained-type-assertions.yml +57 -12
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-compile-call.yml +63 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-dupe-class-members.yml +52 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html-js.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-inner-html.yml +27 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-known-value-widening.yml +71 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-non-null-assertion.yml +316 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply-js.yml +22 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-apply.yml +23 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get-js.yml +33 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-reflect-get.yml +34 -8
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-return-value-in-generator.yml +22 -5
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-runtime-typeof.yml +121 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-server-bind-wildcard.yml +23 -7
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-shape-in-symbol-names.yml +68 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-sql-in-code.yml +41 -2
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-laundering.yml +6 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-parameters.yml +67 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unknown-returns.yml +106 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-any.yml +50 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-unsafe-dictionary-unknown.yml +56 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +2 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/redundant-unsafe-function.yml +32 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/require-safety-comment-for-as-unknown-as.yml +184 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/ts-void-zero.yml +5 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/self-scan-baseline.json +5 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +53 -1
- package/node_modules/pi-lens/scripts/download-grammars.js +25 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +9 -0
- package/node_modules/pi-lens/scripts/install-selftest.mjs +99 -11
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +75 -0
- package/node_modules/pi-lens/scripts/lib/warm-loader-cache.mjs +285 -0
- package/node_modules/pi-lens/scripts/warm-loader-cache.mjs +233 -0
- package/node_modules/pi-lens/skills/pi-lens-ast-grep/SKILL.md +2 -0
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/SKILL.md +80 -3
- package/node_modules/pi-lens/skills/pi-lens-write-ast-grep-rule/reference.md +41 -0
- package/node_modules/pi-lens/vendor/grammars/tree-sitter-cue.wasm +0 -0
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +36 -0
- package/node_modules/pi-mcp-adapter/README.md +47 -5
- package/node_modules/pi-mcp-adapter/cli.js +163 -1
- package/node_modules/pi-mcp-adapter/commands.ts +128 -22
- package/node_modules/pi-mcp-adapter/config.ts +25 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +5 -4
- package/node_modules/pi-mcp-adapter/index.ts +219 -32
- package/node_modules/pi-mcp-adapter/init.ts +36 -4
- package/node_modules/pi-mcp-adapter/lifecycle.ts +315 -24
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +39 -6
- package/node_modules/pi-mcp-adapter/mcp-bearer-store.ts +387 -0
- package/node_modules/pi-mcp-adapter/mcp-callback-server.ts +27 -3
- package/node_modules/pi-mcp-adapter/mcp-code.ts +4 -0
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +42 -13
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +32 -21
- package/node_modules/pi-mcp-adapter/package-mcp-loader.ts +168 -0
- package/node_modules/pi-mcp-adapter/package.json +3 -1
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +5 -4
- package/node_modules/pi-mcp-adapter/request-headers-command.ts +31 -37
- package/node_modules/pi-mcp-adapter/search-ranking.ts +92 -22
- package/node_modules/pi-mcp-adapter/server-manager.ts +140 -2
- package/node_modules/pi-mcp-adapter/session-recovery.ts +7 -0
- package/node_modules/pi-mcp-adapter/tool-approval.ts +15 -2
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +44 -4
- package/node_modules/pi-mcp-adapter/types.ts +2 -0
- package/node_modules/pi-mcp-adapter/ui-server.ts +18 -16
- package/node_modules/pi-mcp-adapter/utils.ts +70 -0
- package/node_modules/pi-web-access/CHANGELOG.md +37 -0
- package/node_modules/pi-web-access/README.md +32 -13
- package/node_modules/pi-web-access/anysearch.ts +4 -2
- package/node_modules/pi-web-access/brave.ts +16 -4
- package/node_modules/pi-web-access/chrome-cookies.ts +86 -15
- package/node_modules/pi-web-access/curator-page.ts +6 -3
- package/node_modules/pi-web-access/curator-server.ts +4 -1
- package/node_modules/pi-web-access/data-uri-sanitize.ts +406 -0
- package/node_modules/pi-web-access/exa.ts +18 -7
- package/node_modules/pi-web-access/extract.ts +40 -5
- package/node_modules/pi-web-access/firecrawl.ts +18 -2
- package/node_modules/pi-web-access/gemini-search.ts +16 -4
- package/node_modules/pi-web-access/github-extract.ts +65 -36
- package/node_modules/pi-web-access/index.ts +60 -61
- package/node_modules/pi-web-access/openai-search.ts +17 -6
- package/node_modules/pi-web-access/package.json +1 -1
- package/node_modules/pi-web-access/page-query.ts +4 -2
- package/node_modules/pi-web-access/parallel-mcp.ts +249 -0
- package/node_modules/pi-web-access/parallel.ts +2 -2
- package/node_modules/pi-web-access/pdf-extract.ts +18 -5
- package/node_modules/pi-web-access/query-rewrite.ts +51 -0
- package/node_modules/pi-web-access/serper.ts +211 -0
- package/node_modules/pi-web-access/ssrf-protection.ts +12 -2
- package/node_modules/pi-web-access/summary-model-scope.ts +12 -4
- package/node_modules/pi-web-access/summary-review.ts +44 -22
- package/node_modules/pi-web-access/tavily.ts +16 -4
- package/node_modules/pi-web-access/utils.ts +87 -0
- package/node_modules/pi-web-access/valyu.ts +199 -0
- package/package.json +14 -14
|
@@ -12,10 +12,15 @@
|
|
|
12
12
|
* implementing DeadCodeClient and adding to getDeadCodeClients().
|
|
13
13
|
*/
|
|
14
14
|
import { createSubsystemLogger } from "./extension-log.js";
|
|
15
|
+
import { incrementDegradationCount } from "./degradation-ledger.js";
|
|
16
|
+
import * as fs from "node:fs";
|
|
15
17
|
import * as path from "node:path";
|
|
16
18
|
import { findNearestMarkerRoot } from "./path-utils.js";
|
|
19
|
+
import { getScratchTreeFnmatchPatterns } from "./scratch-tree-policy.js";
|
|
17
20
|
import { safeSpawnAsync } from "./safe-spawn.js";
|
|
18
21
|
import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
|
|
22
|
+
import { firstOutputLine, spawnFailedWithNoOutput, } from "./dispatch/runners/utils/spawn-outcome.js";
|
|
23
|
+
import { formatToolFailure } from "./dispatch/runners/utils/tool-failure.js";
|
|
19
24
|
function emptyResult(language) {
|
|
20
25
|
return {
|
|
21
26
|
success: false,
|
|
@@ -28,17 +33,18 @@ function emptyResult(language) {
|
|
|
28
33
|
}
|
|
29
34
|
const ANALYSIS_TIMEOUT_MS = 30_000;
|
|
30
35
|
// Directories never worth scanning for the user's own dead code.
|
|
36
|
+
//
|
|
37
|
+
// Single-sourced from `scratch-tree-policy.ts`'s `EXCLUDED_DIRS`-derived list
|
|
38
|
+
// (#1562 sweep finding: this array had hand-drifted from `EXCLUDED_DIRS` —
|
|
39
|
+
// missing `.pi`/`.claude`/`.next`/`.turbo`/etc, the same single-source-of-
|
|
40
|
+
// truth defect #883 named for per-language lists). `site-packages`/`.eggs`
|
|
41
|
+
// are Python-packaging-specific and have no `EXCLUDED_DIRS` counterpart (no
|
|
42
|
+
// other scanner needs them), so they stay as a small local addition rather
|
|
43
|
+
// than polluting the shared cross-tool list.
|
|
44
|
+
const VULTURE_PYTHON_ONLY_EXCLUDES = ["*/site-packages/*", "*/.eggs/*"];
|
|
31
45
|
const VULTURE_EXCLUDES = [
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
"*/.tox/*",
|
|
35
|
-
"*/build/*",
|
|
36
|
-
"*/dist/*",
|
|
37
|
-
"*/node_modules/*",
|
|
38
|
-
"*/.git/*",
|
|
39
|
-
"*/site-packages/*",
|
|
40
|
-
"*/__pycache__/*",
|
|
41
|
-
"*/.eggs/*",
|
|
46
|
+
...getScratchTreeFnmatchPatterns(),
|
|
47
|
+
...VULTURE_PYTHON_ONLY_EXCLUDES,
|
|
42
48
|
];
|
|
43
49
|
// Decorators whose target is called by a framework, never by name in the tree.
|
|
44
50
|
// vulture cannot see those call sites, so every such symbol is a permanent
|
|
@@ -145,13 +151,13 @@ export class PythonDeadCodeClient {
|
|
|
145
151
|
"tox.ini",
|
|
146
152
|
], { boundaries: [".git", ".hg", ".svn"], homeDir: homeDirOverride });
|
|
147
153
|
}
|
|
148
|
-
async ensureAvailable() {
|
|
154
|
+
async ensureAvailable(root) {
|
|
149
155
|
const memo = this.availabilityLatch.read();
|
|
150
156
|
if (memo !== null)
|
|
151
157
|
return memo;
|
|
152
158
|
if (this.ensureInFlight)
|
|
153
159
|
return this.ensureInFlight;
|
|
154
|
-
this.ensureInFlight = this.doEnsureAvailable();
|
|
160
|
+
this.ensureInFlight = this.doEnsureAvailable(root);
|
|
155
161
|
try {
|
|
156
162
|
return await this.ensureInFlight;
|
|
157
163
|
}
|
|
@@ -159,15 +165,39 @@ export class PythonDeadCodeClient {
|
|
|
159
165
|
this.ensureInFlight = null;
|
|
160
166
|
}
|
|
161
167
|
}
|
|
162
|
-
|
|
168
|
+
/**
|
|
169
|
+
* `<root>/.venv/bin/vulture` (and `venv`/Windows `Scripts` equivalents),
|
|
170
|
+
* checked by existence only — no spawn. A project's own venv is the vulture
|
|
171
|
+
* that project's own config and dependency set were written against; the
|
|
172
|
+
* bare `vulture`/`python -m vulture` PATH probes below only ever see
|
|
173
|
+
* whatever happens to be active in the CALLING shell, not the project's
|
|
174
|
+
* (#1731, discipline B — the same venv-first shape sqlfluff's binary
|
|
175
|
+
* resolution already uses, `runner-helpers.ts` `createVenvFinder`).
|
|
176
|
+
*/
|
|
177
|
+
venvCandidates(root) {
|
|
178
|
+
const isWin = process.platform === "win32";
|
|
179
|
+
const relPaths = isWin
|
|
180
|
+
? [
|
|
181
|
+
path.join(".venv", "Scripts", "vulture.exe"),
|
|
182
|
+
path.join("venv", "Scripts", "vulture.exe"),
|
|
183
|
+
]
|
|
184
|
+
: [path.join(".venv", "bin", "vulture"), path.join("venv", "bin", "vulture")];
|
|
185
|
+
return relPaths
|
|
186
|
+
.map((rel) => path.join(root, rel))
|
|
187
|
+
.filter((full) => fs.existsSync(full))
|
|
188
|
+
.map((full) => ({ cmd: full, prefix: [] }));
|
|
189
|
+
}
|
|
190
|
+
async doEnsureAvailable(root) {
|
|
163
191
|
// Presence-gated, NOT auto-installed. vulture is a pure-Python package
|
|
164
192
|
// with no standalone binary, so "auto-install" would mean `pip install`
|
|
165
193
|
// into whatever Python environment happens to be active — wrong and
|
|
166
194
|
// intrusive for uv / poetry / conda / pipx users. So we only use vulture
|
|
167
|
-
// when the user already has it, probing
|
|
168
|
-
//
|
|
169
|
-
//
|
|
195
|
+
// when the user already has it, probing the project's own venv first
|
|
196
|
+
// (#1731), then the `vulture` console script and `python -m vulture` on
|
|
197
|
+
// PATH (the script dir is frequently not on PATH even when the package
|
|
198
|
+
// is installed). Mirrors govulncheck's no-install gating.
|
|
170
199
|
const candidates = [
|
|
200
|
+
...(root ? this.venvCandidates(root) : []),
|
|
171
201
|
{ cmd: "vulture", prefix: [] },
|
|
172
202
|
{ cmd: "python", prefix: ["-m", "vulture"] },
|
|
173
203
|
{ cmd: "python3", prefix: ["-m", "vulture"] },
|
|
@@ -183,6 +213,10 @@ export class PythonDeadCodeClient {
|
|
|
183
213
|
// numbers.
|
|
184
214
|
const sweepStartedAt = Date.now();
|
|
185
215
|
let sweepHostStallMs = 0;
|
|
216
|
+
/** What the last classified candidate returned (#1500 review): this was
|
|
217
|
+
* computed and thrown away, so every vulture row said "missing" with no
|
|
218
|
+
* trace of which candidate reported what. */
|
|
219
|
+
let sweepEvidence;
|
|
186
220
|
for (const c of candidates) {
|
|
187
221
|
const sampler = startHostStallSampler();
|
|
188
222
|
const startedAt = Date.now();
|
|
@@ -213,7 +247,11 @@ export class PythonDeadCodeClient {
|
|
|
213
247
|
});
|
|
214
248
|
return true;
|
|
215
249
|
}
|
|
216
|
-
const classified = classifyProbeFailure(probe, {
|
|
250
|
+
const classified = classifyProbeFailure(probe, {
|
|
251
|
+
hostStallMs,
|
|
252
|
+
command: c.cmd,
|
|
253
|
+
});
|
|
254
|
+
sweepEvidence = classified.evidence;
|
|
217
255
|
if (classified.outcome === "transient") {
|
|
218
256
|
sawTransient = true;
|
|
219
257
|
transientCause = classified.cause;
|
|
@@ -232,6 +270,8 @@ export class PythonDeadCodeClient {
|
|
|
232
270
|
latched: false,
|
|
233
271
|
retryAfterMs,
|
|
234
272
|
budgetMs: 5000,
|
|
273
|
+
classifiedBy: "probe",
|
|
274
|
+
...(sweepEvidence !== undefined && { evidence: sweepEvidence }),
|
|
235
275
|
});
|
|
236
276
|
return false;
|
|
237
277
|
}
|
|
@@ -246,6 +286,9 @@ export class PythonDeadCodeClient {
|
|
|
246
286
|
hostStallMs: sweepHostStallMs,
|
|
247
287
|
latched: true,
|
|
248
288
|
budgetMs: 5000,
|
|
289
|
+
// Every candidate probe was classified, and none was transient (#1500).
|
|
290
|
+
classifiedBy: "probe",
|
|
291
|
+
...(sweepEvidence !== undefined && { evidence: sweepEvidence }),
|
|
249
292
|
});
|
|
250
293
|
return false;
|
|
251
294
|
}
|
|
@@ -258,7 +301,7 @@ export class PythonDeadCodeClient {
|
|
|
258
301
|
summary: "No Python project root found; vulture skipped",
|
|
259
302
|
};
|
|
260
303
|
}
|
|
261
|
-
if (!(await this.ensureAvailable())) {
|
|
304
|
+
if (!(await this.ensureAvailable(root))) {
|
|
262
305
|
return {
|
|
263
306
|
...emptyResult(this.language),
|
|
264
307
|
success: true,
|
|
@@ -273,14 +316,35 @@ export class PythonDeadCodeClient {
|
|
|
273
316
|
this.inFlight.set(key, promise);
|
|
274
317
|
return promise;
|
|
275
318
|
}
|
|
319
|
+
/**
|
|
320
|
+
* True when `<root>/pyproject.toml` carries a `[tool.vulture]` table.
|
|
321
|
+
* vulture's own config discovery reads that table, but CLI flags override
|
|
322
|
+
* it — passing `--min-confidence`/`--exclude` unconditionally silently
|
|
323
|
+
* overrode a project's own thresholds and ignore list (#1731, discipline A).
|
|
324
|
+
* Same shape as `hasSqlfluffConfig`/`hasMypyConfig` in `tool-policy.ts`: a
|
|
325
|
+
* plain string search, no TOML parser, matching the section header only.
|
|
326
|
+
*/
|
|
327
|
+
hasProjectVultureConfig(root) {
|
|
328
|
+
try {
|
|
329
|
+
const content = fs.readFileSync(path.join(root, "pyproject.toml"), "utf-8");
|
|
330
|
+
return content.includes("[tool.vulture]");
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
276
336
|
async runAnalyze(root) {
|
|
277
337
|
const startMs = Date.now();
|
|
278
338
|
const invocation = this.resolved ?? { cmd: "vulture", prefix: [] };
|
|
339
|
+
// Let the project's own [tool.vulture] config win (#1731, discipline A):
|
|
340
|
+
// vulture discovers it unaided, but these two flags override it when
|
|
341
|
+
// passed, so they are omitted whenever the project ships that table.
|
|
342
|
+
const hasConfig = this.hasProjectVultureConfig(root);
|
|
279
343
|
const args = [
|
|
280
344
|
...invocation.prefix,
|
|
281
345
|
".",
|
|
282
|
-
`--min-confidence=${this.minConfidence}
|
|
283
|
-
`--exclude=${VULTURE_EXCLUDES.join(",")}
|
|
346
|
+
...(hasConfig ? [] : [`--min-confidence=${this.minConfidence}`]),
|
|
347
|
+
...(hasConfig ? [] : [`--exclude=${VULTURE_EXCLUDES.join(",")}`]),
|
|
284
348
|
`--ignore-decorators=${VULTURE_IGNORE_DECORATORS.join(",")}`,
|
|
285
349
|
];
|
|
286
350
|
const result = await safeSpawnAsync(invocation.cmd, args, {
|
|
@@ -299,14 +363,36 @@ export class PythonDeadCodeClient {
|
|
|
299
363
|
}
|
|
300
364
|
// vulture writes parse/usage errors to stderr and exits 1 with no
|
|
301
365
|
// stdout findings; distinguish that from "found dead code" (exit 1 WITH
|
|
302
|
-
// findings on stdout).
|
|
366
|
+
// findings on stdout). #1736 sweep: the ORIGINAL guard here required
|
|
367
|
+
// non-empty stderr to call it an error, so a nonzero exit with BOTH
|
|
368
|
+
// empty stdout and empty stderr (a silent crash) still fell through to
|
|
369
|
+
// "No dead code found" -- the same empty-distinguishes-clean-from-errored
|
|
370
|
+
// gap the knip fix closes. A nonzero exit with no findings on stdout is
|
|
371
|
+
// never clean now, regardless of whether stderr said anything.
|
|
303
372
|
const output = result.stdout || "";
|
|
304
373
|
if (!output.trim()) {
|
|
305
374
|
const stderr = (result.stderr || "").trim();
|
|
306
|
-
|
|
375
|
+
// Same discriminator every dispatch/runners linter uses
|
|
376
|
+
// (`spawnFailedWithNoOutput`) rather than a parallel hand-rolled
|
|
377
|
+
// check (result.error is already handled above, so this reduces to
|
|
378
|
+
// the `status !== 0` half).
|
|
379
|
+
if (spawnFailedWithNoOutput(result, output)) {
|
|
380
|
+
// #1816: one shared wording, one truncation, signal named.
|
|
381
|
+
const reason = formatToolFailure({
|
|
382
|
+
tool: "vulture",
|
|
383
|
+
status: result.status,
|
|
384
|
+
signal: result.signal,
|
|
385
|
+
stderr: result.stderr,
|
|
386
|
+
});
|
|
387
|
+
this.log(reason);
|
|
388
|
+
incrementDegradationCount({
|
|
389
|
+
kind: "runner-empty-result",
|
|
390
|
+
subject: "vulture",
|
|
391
|
+
reason,
|
|
392
|
+
});
|
|
307
393
|
return {
|
|
308
394
|
...emptyResult(this.language),
|
|
309
|
-
summary: `vulture error: ${stderr
|
|
395
|
+
summary: stderr ? `vulture error: ${firstOutputLine(stderr)}` : reason,
|
|
310
396
|
durationMs,
|
|
311
397
|
};
|
|
312
398
|
}
|
|
@@ -357,16 +443,22 @@ export function deadCodeIssues(result) {
|
|
|
357
443
|
];
|
|
358
444
|
}
|
|
359
445
|
/**
|
|
360
|
-
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
* and
|
|
364
|
-
* line in the key turns each shifted
|
|
365
|
-
*
|
|
366
|
-
*
|
|
446
|
+
* Joins finding-identity parts into a stable key for diffing one scan
|
|
447
|
+
* against the previous one. The string-only signature is load-bearing: a
|
|
448
|
+
* line/column number cannot be passed without a compile error. An edit
|
|
449
|
+
* shifts every line below it, and a scan delta is then filtered to exactly
|
|
450
|
+
* the files the edit touched — so a line in the key turns each shifted
|
|
451
|
+
* pre-existing finding into a "newly unused" report under a heading that
|
|
452
|
+
* blames the agent's own edit for orphaning it. Inserting four lines above
|
|
453
|
+
* one real finding produced four false ones (#1477, #1483). Shared by
|
|
454
|
+
* knip's per-turn delta in runtime-turn.ts so the rule lives in one place.
|
|
367
455
|
*/
|
|
456
|
+
export function stableFindingKey(...parts) {
|
|
457
|
+
return parts.map((part) => part ?? "").join(":");
|
|
458
|
+
}
|
|
459
|
+
/** Stable identity for diffing one scan against the previous one. */
|
|
368
460
|
export function deadCodeIssueKey(issue) {
|
|
369
|
-
return
|
|
461
|
+
return stableFindingKey(issue.category, issue.file, issue.name);
|
|
370
462
|
}
|
|
371
463
|
/**
|
|
372
464
|
* Format this turn's ATTRIBUTABLE delta — symbols that became unused in files
|
|
@@ -38,8 +38,15 @@ export function withDeadline(promise, options) {
|
|
|
38
38
|
const onReject = options.onReject ?? "propagate";
|
|
39
39
|
const ms = options.ms ??
|
|
40
40
|
(options.deadlineAt !== undefined ? options.deadlineAt - Date.now() : 0);
|
|
41
|
-
// Past deadline / non-positive budget: settle immediately, no timer.
|
|
41
|
+
// Past deadline / non-positive budget: settle immediately, no timer. This
|
|
42
|
+
// branch returns before the race below ever runs, so — same reason as the
|
|
43
|
+
// loser-leg catch a few lines down — `promise` needs its own no-op catch
|
|
44
|
+
// here too: without it, a `promise` that eventually rejects (the caller
|
|
45
|
+
// already invoked it; this function only decides how long to wait on it)
|
|
46
|
+
// surfaces as an unhandled rejection instead of being silently superseded
|
|
47
|
+
// by the immediate timeout/undefined settlement.
|
|
42
48
|
if (ms <= 0) {
|
|
49
|
+
promise.catch(() => { });
|
|
43
50
|
return onTimeout === "undefined"
|
|
44
51
|
? Promise.resolve(undefined)
|
|
45
52
|
: Promise.reject(new Error(`Timeout after ${Math.max(0, ms)}ms`));
|
|
@@ -140,6 +140,11 @@ export function flushDebugHandlesLog() {
|
|
|
140
140
|
}
|
|
141
141
|
function safeGetActiveResourcesInfo() {
|
|
142
142
|
try {
|
|
143
|
+
// SAFETY: `process.getActiveResourcesInfo` is a real Node API (>=17.3)
|
|
144
|
+
// that @types/node does not declare on `Process`. The cast only widens
|
|
145
|
+
// the type to say the method MIGHT be there; the `typeof fn ===
|
|
146
|
+
// "function"` guard on the next line is what actually decides, so a
|
|
147
|
+
// runtime without it returns [] rather than throwing.
|
|
143
148
|
const fn = process.getActiveResourcesInfo;
|
|
144
149
|
return typeof fn === "function" ? fn.call(process) : [];
|
|
145
150
|
}
|
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
/** Bounded, process-local telemetry for behavior degraded during one session. */
|
|
2
2
|
import { logExtension } from "./extension-log.js";
|
|
3
|
+
import { LEDGER_FIELD_MAX, truncateForLedger } from "./ledger-bounds.js";
|
|
4
|
+
import { logLatency } from "./latency-logger.js";
|
|
5
|
+
import { getSinkWriteFailures, resetSinkWriteFailures } from "./ndjson-logger.js";
|
|
6
|
+
// Re-exported so existing importers keep one name for the ledger's bound.
|
|
7
|
+
export { LEDGER_FIELD_MAX, truncateForLedger };
|
|
3
8
|
const ENTRIES_PER_KIND = 20;
|
|
4
9
|
const MAX_DISTINCT_KINDS = 32;
|
|
5
10
|
const OVERFLOW_KIND = "other";
|
|
6
11
|
const groups = new Map();
|
|
7
12
|
const onceKeys = new Set();
|
|
8
13
|
const tallies = new Map();
|
|
14
|
+
// Monotonic session-boundary counter (#1536 review F5): callers that keep
|
|
15
|
+
// their OWN once-per-session latch outside the ledger (a per-instance Set
|
|
16
|
+
// the ledger itself doesn't own) can compare this lazily at use time and
|
|
17
|
+
// clear their latch on a mismatch — the same clear-on-transition shape as
|
|
18
|
+
// project-trust.ts's trustGeneration, but keyed to the ledger's own reset
|
|
19
|
+
// (resetDegradationLedger, wired into handleSessionStart) rather than a
|
|
20
|
+
// trust change.
|
|
21
|
+
let ledgerGeneration = 0;
|
|
22
|
+
/** Current session generation. Bump on every `resetDegradationLedger()`. */
|
|
23
|
+
export function getDegradationLedgerGeneration() {
|
|
24
|
+
return ledgerGeneration;
|
|
25
|
+
}
|
|
9
26
|
export function recordDegradation(record) {
|
|
10
27
|
try {
|
|
11
28
|
const kind = boundedKind(record.kind);
|
|
@@ -16,6 +33,7 @@ export function recordDegradation(record) {
|
|
|
16
33
|
group = { count: 0, entries: [] };
|
|
17
34
|
groups.set(kind, group);
|
|
18
35
|
}
|
|
36
|
+
const admitted = group.entries.length < ENTRIES_PER_KIND;
|
|
19
37
|
group.count += 1;
|
|
20
38
|
// Bounded at RECORD time (#1366 review): reasons carry arbitrary error
|
|
21
39
|
// text; a 10KB message must never become a 10KB health line or a 10KB
|
|
@@ -23,10 +41,12 @@ export function recordDegradation(record) {
|
|
|
23
41
|
group.entries.push({ subject, reason });
|
|
24
42
|
if (group.entries.length > ENTRIES_PER_KIND)
|
|
25
43
|
group.entries.shift();
|
|
44
|
+
return admitted;
|
|
26
45
|
}
|
|
27
46
|
catch (error) {
|
|
28
47
|
debugLedgerFailure("record", error);
|
|
29
48
|
// Telemetry must never break the observed path.
|
|
49
|
+
return false;
|
|
30
50
|
}
|
|
31
51
|
}
|
|
32
52
|
/** Record at most once per kind/subject during the current session. */
|
|
@@ -38,7 +58,9 @@ export function recordDegradationOnce(record) {
|
|
|
38
58
|
if (onceKeys.has(key))
|
|
39
59
|
return;
|
|
40
60
|
onceKeys.add(key);
|
|
41
|
-
recordDegradation({ kind, subject, reason: record.reason })
|
|
61
|
+
if (recordDegradation({ kind, subject, reason: record.reason })) {
|
|
62
|
+
logDurableDegradation(kind, subject, 1);
|
|
63
|
+
}
|
|
42
64
|
}
|
|
43
65
|
catch (error) {
|
|
44
66
|
debugLedgerFailure("record-once", error);
|
|
@@ -48,6 +70,14 @@ export function recordDegradationOnce(record) {
|
|
|
48
70
|
/**
|
|
49
71
|
* Count a repeated degradation while retaining one latest-reason entry per
|
|
50
72
|
* kind/subject. The group count remains the exact event total.
|
|
73
|
+
*
|
|
74
|
+
* Returns `true` when this call is the FIRST occurrence recorded for this
|
|
75
|
+
* kind/subject pair (the ledger is the single source of truth for that
|
|
76
|
+
* tally already — via `tallies` — so callers that need a once-per-subject
|
|
77
|
+
* "rising edge" signal, e.g. to gate a verbose one-time log line before
|
|
78
|
+
* falling back to the bounded count, read it off this return value instead
|
|
79
|
+
* of hand-rolling their own parallel `Set`/latch). #1716 reuses this same
|
|
80
|
+
* signal to gate `navRequest`'s detailed timeout/late-answer log writes.
|
|
51
81
|
*/
|
|
52
82
|
export function incrementDegradationCount(record) {
|
|
53
83
|
try {
|
|
@@ -62,24 +92,49 @@ export function incrementDegradationCount(record) {
|
|
|
62
92
|
group = { count: 0, entries: [] };
|
|
63
93
|
groups.set(kind, group);
|
|
64
94
|
}
|
|
65
|
-
group.count += 1;
|
|
66
|
-
const entry = { subject, reason: truncateForLedger(`${reason} (count: ${count})`) };
|
|
67
95
|
const existing = group.entries.findIndex((candidate) => candidate.subject === subject);
|
|
96
|
+
const admitted = existing >= 0 || group.entries.length < ENTRIES_PER_KIND;
|
|
97
|
+
group.count += 1;
|
|
98
|
+
// #1816: append the count AFTER truncation, never before. `reason` is
|
|
99
|
+
// already bounded above, so re-truncating the concatenation pushed the
|
|
100
|
+
// suffix past LEDGER_FIELD_MAX and silently ate it — a 200-char reason
|
|
101
|
+
// lost the one field that says how often the degradation fired.
|
|
102
|
+
const entry = { subject, reason: `${reason} (count: ${count})` };
|
|
68
103
|
if (existing >= 0)
|
|
69
104
|
group.entries.splice(existing, 1);
|
|
70
105
|
group.entries.push(entry);
|
|
71
106
|
if (group.entries.length > ENTRIES_PER_KIND)
|
|
72
107
|
group.entries.shift();
|
|
108
|
+
// Durable rows use the summary's admission and emit the first event and
|
|
109
|
+
// power-of-two milestones only, keeping the sink bounded.
|
|
110
|
+
if (admitted && isPowerOfTwo(count)) {
|
|
111
|
+
logDurableDegradation(kind, subject, count);
|
|
112
|
+
}
|
|
113
|
+
return count === 1;
|
|
73
114
|
}
|
|
74
115
|
catch (error) {
|
|
75
116
|
debugLedgerFailure("increment", error);
|
|
76
117
|
// Telemetry must never break the observed path.
|
|
118
|
+
return false;
|
|
77
119
|
}
|
|
78
120
|
}
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
121
|
+
/**
|
|
122
|
+
* Persist the accepted ledger mutation through the existing rotated NDJSON
|
|
123
|
+
* latency stream. `logLatency` owns the timestamp, PID, serialization, secret
|
|
124
|
+
* redaction, and write queue. The subject and kind were already bounded by the
|
|
125
|
+
* ledger before reaching this seam.
|
|
126
|
+
*/
|
|
127
|
+
function logDurableDegradation(kind, subject, count) {
|
|
128
|
+
logLatency({
|
|
129
|
+
type: "phase",
|
|
130
|
+
phase: "degradation_ledger",
|
|
131
|
+
filePath: subject,
|
|
132
|
+
durationMs: 0,
|
|
133
|
+
metadata: { kind, subject, count, ledgerGeneration },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
function isPowerOfTwo(value) {
|
|
137
|
+
return value > 0 && (value & (value - 1)) === 0;
|
|
83
138
|
}
|
|
84
139
|
function boundedKind(value) {
|
|
85
140
|
const kind = truncateForLedger(value);
|
|
@@ -88,19 +143,29 @@ function boundedKind(value) {
|
|
|
88
143
|
// Keep one slot available for all kinds beyond the cardinality bound.
|
|
89
144
|
return groups.size < MAX_DISTINCT_KINDS - 1 ? kind : OVERFLOW_KIND;
|
|
90
145
|
}
|
|
91
|
-
function truncateForLedger(value) {
|
|
92
|
-
const text = normalizeForLedger(value);
|
|
93
|
-
return text.length > LEDGER_FIELD_MAX
|
|
94
|
-
? `${text.slice(0, LEDGER_FIELD_MAX)}…`
|
|
95
|
-
: text;
|
|
96
|
-
}
|
|
97
146
|
export function getDegradationSummary() {
|
|
98
|
-
|
|
147
|
+
const summary = [...groups.entries()].map(([kind, group]) => ({
|
|
99
148
|
kind,
|
|
100
149
|
count: group.count,
|
|
101
150
|
droppedCount: group.count - group.entries.length,
|
|
102
151
|
latestReasons: group.entries.map((entry) => ({ ...entry })),
|
|
103
152
|
}));
|
|
153
|
+
// Folded in at read time, not written into `groups` (#1970) — see the
|
|
154
|
+
// `log-sink-write-failure` doc comment on `DegradationKind` for why this
|
|
155
|
+
// kind never goes through `recordDegradation`.
|
|
156
|
+
const sinkFailures = getSinkWriteFailures();
|
|
157
|
+
if (sinkFailures.length > 0) {
|
|
158
|
+
summary.push({
|
|
159
|
+
kind: "log-sink-write-failure",
|
|
160
|
+
count: sinkFailures.reduce((total, sink) => total + sink.droppedCount, 0),
|
|
161
|
+
droppedCount: 0,
|
|
162
|
+
latestReasons: sinkFailures.map((sink) => ({
|
|
163
|
+
subject: truncateForLedger(sink.file),
|
|
164
|
+
reason: truncateForLedger(`${sink.droppedCount} dropped write(s) after reopen-retry failed`),
|
|
165
|
+
})),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return summary;
|
|
104
169
|
}
|
|
105
170
|
function isRenderableSummary(value) {
|
|
106
171
|
if (!Array.isArray(value))
|
|
@@ -148,6 +213,11 @@ export function resetDegradationLedger() {
|
|
|
148
213
|
groups.clear();
|
|
149
214
|
onceKeys.clear();
|
|
150
215
|
tallies.clear();
|
|
216
|
+
ledgerGeneration++;
|
|
217
|
+
// #1970, catalog shape 17: the sink write-failure tally is a
|
|
218
|
+
// process-lifetime latch too — it re-arms alongside the rest of the
|
|
219
|
+
// ledger rather than surviving past the session that observed it.
|
|
220
|
+
resetSinkWriteFailures();
|
|
151
221
|
}
|
|
152
222
|
export const DEGRADATION_ENTRIES_PER_KIND = ENTRIES_PER_KIND;
|
|
153
223
|
export const DEGRADATION_MAX_DISTINCT_KINDS = MAX_DISTINCT_KINDS;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body degradation for a DEMOTED finding (#1944).
|
|
3
|
+
*
|
|
4
|
+
* The demotion gates (#1631 dependency-drift, #1641 past-EOF) move a record
|
|
5
|
+
* out of the authoritative blocker channel and into the advisory channel.
|
|
6
|
+
* Until this module existed they changed the CHANNEL and nothing else: the
|
|
7
|
+
* advisory embedded the blocker body verbatim, so the agent still read
|
|
8
|
+
* "🔴 STOP — 11 issue(s) must be fixed" with dead line numbers under two
|
|
9
|
+
* hedge lines it ignored. Live evidence (session 01a0234c, 2026-08-21): the
|
|
10
|
+
* agent spent four re-run detours and about 7.4k tokens on a record whose
|
|
11
|
+
* coordinates no longer existed, then explained an unrelated REAL blocker
|
|
12
|
+
* away as "mid-edit state" in the same session.
|
|
13
|
+
*
|
|
14
|
+
* The rule this module owns, once, for every gated surface: a demoted body
|
|
15
|
+
* loses its authority vocabulary and its dead coordinates, and it says so in
|
|
16
|
+
* its own words. `clients/finding-delivery-gate.ts` documents the surrounding
|
|
17
|
+
* contract; this is the one implementation of its "Demotion rendering" arm
|
|
18
|
+
* for surfaces that carry a RENDERED BODY rather than a per-diagnostic row.
|
|
19
|
+
* Per-row surfaces (`widget-state.ts`, `tools/lens-diagnostics.ts`) already
|
|
20
|
+
* swap the coordinate for `PAST_EOF_STALE_MARKER` at render time and have no
|
|
21
|
+
* body to degrade.
|
|
22
|
+
*
|
|
23
|
+
* Pure and leaf: no store, no coordinator, no I/O. The caller supplies the
|
|
24
|
+
* dead lines it already computed while deciding to demote, so this module
|
|
25
|
+
* never re-derives a verdict from rendered prose (the
|
|
26
|
+
* re-derivation-vs-correlation screen).
|
|
27
|
+
*/
|
|
28
|
+
/** Suffix that replaces a cited coordinate the file no longer has. */
|
|
29
|
+
export const DEAD_LINE_ANNOTATION = "(line no longer exists)";
|
|
30
|
+
/**
|
|
31
|
+
* Authority vocabulary a demoted body must not carry, with its degraded
|
|
32
|
+
* replacement. Ordered: the full banner rewrite runs first so the narrower
|
|
33
|
+
* fallbacks only ever see text the banner rule did not already cover.
|
|
34
|
+
*
|
|
35
|
+
* This table is the single source of truth for the rule. A surface must not
|
|
36
|
+
* hand-roll its own "strip the STOP" pass — that is how #1631, #1641, and
|
|
37
|
+
* #1664 each ended up with a different answer to the same question.
|
|
38
|
+
*
|
|
39
|
+
* POSITION-ANCHORED (#1944 review F1). These patterns run over the BANNER
|
|
40
|
+
* region only, never the whole body. Diagnostic messages are arbitrary text
|
|
41
|
+
* from arbitrary runners, and they legitimately contain both tokens: the
|
|
42
|
+
* reviewer's probe turned ts(2304) `Cannot find name 'STOP'` into `Cannot
|
|
43
|
+
* find name 'Previously flagged'`, and mangled an English "must be fixed"
|
|
44
|
+
* inside a rule message. Rewriting a finding's own words is a worse defect
|
|
45
|
+
* than the banner it was cleaning up. See `splitBannerRegion`.
|
|
46
|
+
*/
|
|
47
|
+
const AUTHORITY_REWRITES = [
|
|
48
|
+
[
|
|
49
|
+
/[^\S\n]*(?:🔴|⛔|❌)?[^\S\n]*STOP[^\S\n]*[—–-][^\S\n]*(\d+)[^\S\n]+issue\(s\)[^\S\n]+must be fixed:?/g,
|
|
50
|
+
"$1 issue(s) were flagged before this file changed; the coordinates below may no longer exist:",
|
|
51
|
+
],
|
|
52
|
+
[/(?:🔴|⛔|❌)[^\S\n]*STOP\b[^\S\n]*[—–-]?/g, "Previously flagged —"],
|
|
53
|
+
[/\bSTOP\b/g, "Previously flagged"],
|
|
54
|
+
[/\bmust be fixed\b/g, "were flagged"],
|
|
55
|
+
];
|
|
56
|
+
/** ` L310: message` / `L310 message` at the head of a rendered row. */
|
|
57
|
+
const CITED_LINE_RE = /^([^\S\n]*)L(\d+)\b/;
|
|
58
|
+
/**
|
|
59
|
+
* Split a rendered body into its BANNER region and its CONTENT region
|
|
60
|
+
* (#1944 review F1).
|
|
61
|
+
*
|
|
62
|
+
* The body this module degrades is `InlineBlockerRecord.summary`, and its one
|
|
63
|
+
* producer is `formatDiagnostics(inlineBlockers, "blocking")`
|
|
64
|
+
* (`clients/dispatch/utils/format-utils.ts`), reaching the store as
|
|
65
|
+
* `dispatchResult.blockerOutput` -> `PipelineResult.inlineBlockerSummary` ->
|
|
66
|
+
* `recordInlineBlockers`. (`buildEnrichedBlockerOutput` in
|
|
67
|
+
* `clients/pipeline.ts` renders the same shape with code snippets, but it
|
|
68
|
+
* feeds the TOOL-RESULT output shown inline during the write, not this
|
|
69
|
+
* stored summary. #1944 review round: naming it here as a producer was
|
|
70
|
+
* wrong.) The shape is a header, then one ` L<n>: <message>` row per
|
|
71
|
+
* diagnostic, then indented continuations (`💡 Fix: …`) and an optional
|
|
72
|
+
* `… and N more` tail. So the first cited-line row is the boundary:
|
|
73
|
+
* everything above it is ours to rewrite, everything from it down is the
|
|
74
|
+
* findings' own words.
|
|
75
|
+
*
|
|
76
|
+
* A body with no cited row at all — `formatDiagnostics` omits `L<n>` for a
|
|
77
|
+
* lineless diagnostic — falls back to the first NON-BLANK line. Leading
|
|
78
|
+
* blanks must be skipped rather than counted (#1944 verify round, Edge-D):
|
|
79
|
+
* `formatDiagnostics` opens its output with a newline, so a naive "first
|
|
80
|
+
* line is the banner" rule degrades an empty string and leaves the real
|
|
81
|
+
* header at full authority. Production is currently saved from that only by
|
|
82
|
+
* an undocumented `.trim()` at `clients/pipeline.ts` (`inlineBlockerSummary`),
|
|
83
|
+
* and this module claims to own the rule, so it must hold on its own input.
|
|
84
|
+
*/
|
|
85
|
+
function splitBannerRegion(lines) {
|
|
86
|
+
const firstCitedRow = lines.findIndex((line) => CITED_LINE_RE.test(line));
|
|
87
|
+
if (firstCitedRow !== -1)
|
|
88
|
+
return { bannerEnd: firstCitedRow };
|
|
89
|
+
const firstNonBlank = lines.findIndex((line) => line.trim() !== "");
|
|
90
|
+
return { bannerEnd: firstNonBlank === -1 ? 0 : firstNonBlank + 1 };
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Degrade one demoted finding's rendered body.
|
|
94
|
+
*
|
|
95
|
+
* Two independent transforms, deliberately separable so each can be proven by
|
|
96
|
+
* its own test (and so deleting either one reds a different assertion):
|
|
97
|
+
*
|
|
98
|
+
* 1. Authority vocabulary: the STOP banner and the "must be fixed"
|
|
99
|
+
* imperative are rewritten to past-tense, non-directive wording.
|
|
100
|
+
* 2. Dead coordinates: every cited `L<n>` whose line the file no longer has
|
|
101
|
+
* renders as `L<n> (line no longer exists)`. The message text stays —
|
|
102
|
+
* demote, never drop (#1419).
|
|
103
|
+
*/
|
|
104
|
+
export function degradeDemotedFindingBody(summary, options = {}) {
|
|
105
|
+
const deadLines = new Set(options.deadLines ?? []);
|
|
106
|
+
let authorityMarkersRemoved = 0;
|
|
107
|
+
const sourceLines = summary.split("\n");
|
|
108
|
+
const { bannerEnd } = splitBannerRegion(sourceLines);
|
|
109
|
+
const degradedLines = sourceLines.map((line, index) => {
|
|
110
|
+
// Content rows keep every character. Only the banner region is ours.
|
|
111
|
+
if (index >= bannerEnd)
|
|
112
|
+
return line;
|
|
113
|
+
let rewritten = line;
|
|
114
|
+
for (const [pattern, replacement] of AUTHORITY_REWRITES) {
|
|
115
|
+
rewritten = rewritten.replace(pattern, (...args) => {
|
|
116
|
+
authorityMarkersRemoved += 1;
|
|
117
|
+
// Expand `$n` against this match's capture groups by hand. A nested
|
|
118
|
+
// `String.replace` with the SAME global regex would clobber the
|
|
119
|
+
// outer call's `lastIndex` mid-iteration.
|
|
120
|
+
return replacement.replace(/\$(\d)/g, (_, digit) => String(args[Number(digit)] ?? ""));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return rewritten;
|
|
124
|
+
});
|
|
125
|
+
let body = degradedLines.join("\n");
|
|
126
|
+
const deadLinesAnnotated = [];
|
|
127
|
+
if (deadLines.size > 0) {
|
|
128
|
+
body = degradedLines
|
|
129
|
+
.map((line) => {
|
|
130
|
+
const hit = CITED_LINE_RE.exec(line);
|
|
131
|
+
if (!hit)
|
|
132
|
+
return line;
|
|
133
|
+
const cited = Number.parseInt(hit[2] ?? "", 10);
|
|
134
|
+
if (!Number.isFinite(cited) || !deadLines.has(cited))
|
|
135
|
+
return line;
|
|
136
|
+
deadLinesAnnotated.push(cited);
|
|
137
|
+
return `${hit[1] ?? ""}L${cited} ${DEAD_LINE_ANNOTATION}${line.slice(hit[0].length)}`;
|
|
138
|
+
})
|
|
139
|
+
.join("\n");
|
|
140
|
+
}
|
|
141
|
+
return { body, authorityMarkersRemoved, deadLinesAnnotated };
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The one-line note a surface appends when it RETIRES a demoted finding after
|
|
145
|
+
* its single degraded delivery. Rendered inside the advisory itself, so an
|
|
146
|
+
* empty advisory section can only ever mean "nothing to say" — if something
|
|
147
|
+
* was dropped, this line is in the payload (#1944 AC3).
|
|
148
|
+
*/
|
|
149
|
+
export function formatRetirementNote(deadLines) {
|
|
150
|
+
const cited = deadLines.length > 0
|
|
151
|
+
? ` (cited ${deadLines.length === 1 ? "line" : "lines"} ${deadLines.join(", ")})`
|
|
152
|
+
: "";
|
|
153
|
+
return (`Retired after this delivery${cited}: the file no longer has these lines, ` +
|
|
154
|
+
"so this finding cannot be re-confirmed and will not be served again.");
|
|
155
|
+
}
|