@herbertgao/pi-extensions 2026.8.3 → 2026.8.5
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 +45 -4
- package/examples/pi-footer.json +275 -0
- package/node_modules/@herbertgao/pi-cc-extensions/README.en.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/README.md +10 -2
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/config.ts +24 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/config/panel.ts +91 -1
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +7 -3
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/index.ts +7 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +21 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +4 -5
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/markdown-enhance.ts +5 -8
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/grouping.ts +22 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/result.ts +2 -4
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +3 -3
- package/node_modules/@herbertgao/pi-subagents/CHANGELOG.md +8 -0
- package/node_modules/@herbertgao/pi-subagents/package.json +8 -4
- package/node_modules/@herbertgao/pi-subagents/src/agent-color.ts +72 -67
- package/node_modules/@herbertgao/pi-subagents/src/agent-file-toggle.ts +255 -0
- package/node_modules/@herbertgao/pi-subagents/src/agent-manager.ts +191 -3
- package/node_modules/@herbertgao/pi-subagents/src/agent-runner.ts +15 -2
- package/node_modules/@herbertgao/pi-subagents/src/index.ts +289 -113
- package/node_modules/@herbertgao/pi-subagents/src/output-file.ts +24 -1
- package/node_modules/@herbertgao/pi-subagents/src/ui/agent-widget.ts +24 -2
- package/node_modules/@herbertgao/pi-subagents/src/ui/schedule-menu.ts +9 -8
- package/node_modules/@herbertgao/pi-subagents/src/ui/select-item.ts +48 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/docs/keyboard.md +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/package.json +2 -2
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/build-questionnaire.ts +7 -0
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/key-router.ts +16 -5
- package/node_modules/@juicesharp/rpiv-ask-user-question/state/questionnaire-session.ts +4 -2
- package/node_modules/@narumitw/pi-btw/README.md +8 -7
- package/node_modules/@narumitw/pi-btw/package.json +2 -2
- package/node_modules/@narumitw/pi-btw/src/menu.ts +1 -0
- package/node_modules/pi-footer/CHANGELOG.md +145 -0
- package/node_modules/pi-footer/LICENSE +21 -0
- package/node_modules/pi-footer/README.md +354 -0
- package/node_modules/pi-footer/package.json +68 -0
- package/node_modules/pi-footer/src/cache.ts +106 -0
- package/node_modules/pi-footer/src/colors.ts +237 -0
- package/node_modules/pi-footer/src/config.ts +202 -0
- package/node_modules/pi-footer/src/event-widgets.ts +41 -0
- package/node_modules/pi-footer/src/extension-statuses.ts +102 -0
- package/node_modules/pi-footer/src/git.ts +135 -0
- package/node_modules/pi-footer/src/index.ts +268 -0
- package/node_modules/pi-footer/src/metrics.ts +141 -0
- package/node_modules/pi-footer/src/presets.ts +383 -0
- package/node_modules/pi-footer/src/render.ts +114 -0
- package/node_modules/pi-footer/src/separators.ts +52 -0
- package/node_modules/pi-footer/src/types.ts +122 -0
- package/node_modules/pi-footer/src/ui/color-level-confirm.ts +22 -0
- package/node_modules/pi-footer/src/ui/color-options.ts +195 -0
- package/node_modules/pi-footer/src/ui/config-lifecycle.ts +61 -0
- package/node_modules/pi-footer/src/ui/edit-colors.ts +13 -0
- package/node_modules/pi-footer/src/ui/events.ts +35 -0
- package/node_modules/pi-footer/src/ui/extension-status-picker.ts +60 -0
- package/node_modules/pi-footer/src/ui/extension-statuses.ts +73 -0
- package/node_modules/pi-footer/src/ui/fields.ts +252 -0
- package/node_modules/pi-footer/src/ui/global-menu.ts +126 -0
- package/node_modules/pi-footer/src/ui/helpers.ts +44 -0
- package/node_modules/pi-footer/src/ui/layout.ts +17 -0
- package/node_modules/pi-footer/src/ui/line-list.ts +19 -0
- package/node_modules/pi-footer/src/ui/model.ts +64 -0
- package/node_modules/pi-footer/src/ui/navigation.ts +29 -0
- package/node_modules/pi-footer/src/ui/option-edit.ts +93 -0
- package/node_modules/pi-footer/src/ui/overlay-render.ts +103 -0
- package/node_modules/pi-footer/src/ui/screen-context.ts +18 -0
- package/node_modules/pi-footer/src/ui/screen-controller.ts +27 -0
- package/node_modules/pi-footer/src/ui/screen-render.ts +32 -0
- package/node_modules/pi-footer/src/ui/screen-state.ts +20 -0
- package/node_modules/pi-footer/src/ui/screen.ts +243 -0
- package/node_modules/pi-footer/src/ui/screens/add-widget.ts +92 -0
- package/node_modules/pi-footer/src/ui/screens/confirm-exit.ts +52 -0
- package/node_modules/pi-footer/src/ui/screens/controller.ts +12 -0
- package/node_modules/pi-footer/src/ui/screens/edit-colors.ts +68 -0
- package/node_modules/pi-footer/src/ui/screens/edit-widget.ts +115 -0
- package/node_modules/pi-footer/src/ui/screens/extension-status-row.ts +61 -0
- package/node_modules/pi-footer/src/ui/screens/global.ts +75 -0
- package/node_modules/pi-footer/src/ui/screens/line-list.ts +108 -0
- package/node_modules/pi-footer/src/ui/screens/main.ts +99 -0
- package/node_modules/pi-footer/src/ui/screens/terminal.ts +121 -0
- package/node_modules/pi-footer/src/ui/screens/widget-list.ts +154 -0
- package/node_modules/pi-footer/src/ui/terminal-menu.ts +32 -0
- package/node_modules/pi-footer/src/ui/theme.ts +48 -0
- package/node_modules/pi-footer/src/ui/title-bar.ts +100 -0
- package/node_modules/pi-footer/src/ui/widget-actions.ts +76 -0
- package/node_modules/pi-footer/src/ui.ts +66 -0
- package/node_modules/pi-footer/src/widgets/context.ts +28 -0
- package/node_modules/pi-footer/src/widgets/core/active-tools.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/context-window.ts +24 -0
- package/node_modules/pi-footer/src/widgets/core/cwd-basename.ts +19 -0
- package/node_modules/pi-footer/src/widgets/core/cwd.ts +108 -0
- package/node_modules/pi-footer/src/widgets/core/event.ts +34 -0
- package/node_modules/pi-footer/src/widgets/core/external-status.ts +102 -0
- package/node_modules/pi-footer/src/widgets/core/model-provider.ts +20 -0
- package/node_modules/pi-footer/src/widgets/core/model.ts +35 -0
- package/node_modules/pi-footer/src/widgets/core/provider.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/session-name.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/text-verbosity.ts +17 -0
- package/node_modules/pi-footer/src/widgets/core/thinking-level.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/ahead-behind.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/branch.ts +81 -0
- package/node_modules/pi-footer/src/widgets/git/clean.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/deletions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/diff.ts +38 -0
- package/node_modules/pi-footer/src/widgets/git/insertions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/remote.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/root.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/sha.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/staged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/status.ts +19 -0
- package/node_modules/pi-footer/src/widgets/git/unstaged.ts +17 -0
- package/node_modules/pi-footer/src/widgets/git/untracked.ts +17 -0
- package/node_modules/pi-footer/src/widgets/instance.ts +118 -0
- package/node_modules/pi-footer/src/widgets/layout/custom-text.ts +18 -0
- package/node_modules/pi-footer/src/widgets/layout/flex-separator.ts +32 -0
- package/node_modules/pi-footer/src/widgets/layout/separator.ts +57 -0
- package/node_modules/pi-footer/src/widgets/layout/spacer.ts +31 -0
- package/node_modules/pi-footer/src/widgets/options.ts +122 -0
- package/node_modules/pi-footer/src/widgets/project/runtime.ts +361 -0
- package/node_modules/pi-footer/src/widgets/registry.ts +223 -0
- package/node_modules/pi-footer/src/widgets/session/assistant-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/compactions.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/elapsed.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/last-activity.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/session-id.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/session-start.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/tool-results.ts +17 -0
- package/node_modules/pi-footer/src/widgets/session/total-messages.ts +19 -0
- package/node_modules/pi-footer/src/widgets/session/total-time.ts +18 -0
- package/node_modules/pi-footer/src/widgets/session/user-messages.ts +17 -0
- package/node_modules/pi-footer/src/widgets/store.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-hit-rate.ts +55 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-read.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/cache-write.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-bar.ts +108 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-length.ts +26 -0
- package/node_modules/pi-footer/src/widgets/tokens/context-remaining.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/context.ts +29 -0
- package/node_modules/pi-footer/src/widgets/tokens/cost.ts +51 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/input-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/output-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/tokens/tokens.ts +20 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-speed.ts +25 -0
- package/node_modules/pi-footer/src/widgets/tokens/total-tokens.ts +18 -0
- package/node_modules/pi-footer/src/widgets/types.ts +181 -0
- package/node_modules/pi-footer/src/widgets/utils/colors.ts +11 -0
- package/node_modules/pi-footer/src/widgets/utils/context.ts +112 -0
- package/node_modules/pi-footer/src/widgets/utils/session.ts +19 -0
- package/node_modules/pi-footer/src/widgets/utils/token-format.ts +78 -0
- package/node_modules/pi-lens/CHANGELOG.md +2440 -954
- package/node_modules/pi-lens/README.md +13 -0
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +73 -18
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +21 -4
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +68 -16
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +37 -10
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +63 -0
- package/node_modules/pi-lens/dist/clients/atomic-write.js +114 -14
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +193 -23
- package/node_modules/pi-lens/dist/clients/biome-client.js +29 -25
- package/node_modules/pi-lens/dist/clients/bootstrap.js +11 -2
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +34 -0
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +192 -0
- package/node_modules/pi-lens/dist/clients/bus-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +47 -5
- package/node_modules/pi-lens/dist/clients/cache-manager.js +97 -9
- package/node_modules/pi-lens/dist/clients/cache-observability.js +301 -14
- package/node_modules/pi-lens/dist/clients/call-graph.js +449 -117
- package/node_modules/pi-lens/dist/clients/child-unref.js +85 -0
- package/node_modules/pi-lens/dist/clients/codebase-model.js +52 -23
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +40 -0
- package/node_modules/pi-lens/dist/clients/complexity-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +14 -0
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +42 -0
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/debug-handles.js +206 -0
- package/node_modules/pi-lens/dist/clients/debug-heap.js +167 -0
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +153 -0
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +347 -75
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +61 -11
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +63 -20
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +23 -7
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +8 -18
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +8 -5
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +35 -0
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +17 -8
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +1085 -681
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +14 -0
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +11 -1
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +50 -0
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +135 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +23 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +5 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +154 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +3 -2
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +5 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +14 -6
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +4 -9
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +9 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +4 -8
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +2 -3
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +2 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +163 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +37 -5
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +3 -1
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +13 -7
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +3 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +277 -26
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +22 -0
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +5 -4
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +10 -3
- package/node_modules/pi-lens/dist/clients/durable-store.js +97 -0
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +94 -3
- package/node_modules/pi-lens/dist/clients/extension-log.js +139 -0
- package/node_modules/pi-lens/dist/clients/extension-mode.js +73 -0
- package/node_modules/pi-lens/dist/clients/file-kinds.js +57 -0
- package/node_modules/pi-lens/dist/clients/file-role.js +18 -3
- package/node_modules/pi-lens/dist/clients/file-utils.js +56 -34
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +16 -3
- package/node_modules/pi-lens/dist/clients/format-service.js +24 -8
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/formatters.js +358 -61
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +148 -15
- package/node_modules/pi-lens/dist/clients/git-guard.js +681 -11
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +4 -0
- package/node_modules/pi-lens/dist/clients/go-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +8 -0
- package/node_modules/pi-lens/dist/clients/grammar-source.js +12 -4
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +21 -3
- package/node_modules/pi-lens/dist/clients/host-ports.js +31 -0
- package/node_modules/pi-lens/dist/clients/installer/index.js +390 -99
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +142 -172
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +31 -54
- package/node_modules/pi-lens/dist/clients/knip-client.js +15 -36
- package/node_modules/pi-lens/dist/clients/language-policy.js +14 -2
- package/node_modules/pi-lens/dist/clients/language-profile.js +13 -7
- package/node_modules/pi-lens/dist/clients/latency-logger.js +23 -1
- package/node_modules/pi-lens/dist/clients/lens-config.js +12 -1
- package/node_modules/pi-lens/dist/clients/lens-engine.js +86 -17
- package/node_modules/pi-lens/dist/clients/lens-events.js +27 -4
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +8 -0
- package/node_modules/pi-lens/dist/clients/lens-map.js +94 -19
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +30 -0
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +2 -1
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/client.js +493 -48
- package/node_modules/pi-lens/dist/clients/lsp/config.js +13 -2
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +86 -0
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +1012 -121
- package/node_modules/pi-lens/dist/clients/lsp/index.js +1807 -780
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +32 -26
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +1 -1
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +83 -0
- package/node_modules/pi-lens/dist/clients/lsp/server.js +196 -32
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +28 -4
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +8 -0
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +248 -0
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +123 -32
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +157 -24
- package/node_modules/pi-lens/dist/clients/mcp/session.js +216 -8
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +115 -0
- package/node_modules/pi-lens/dist/clients/metrics-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/metrics-history.js +2 -1
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +5 -0
- package/node_modules/pi-lens/dist/clients/module-report.js +168 -3
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +319 -118
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +1 -1
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +120 -5
- package/node_modules/pi-lens/dist/clients/path-utils.js +110 -9
- package/node_modules/pi-lens/dist/clients/pipeline.js +47 -13
- package/node_modules/pi-lens/dist/clients/print-mode.js +21 -0
- package/node_modules/pi-lens/dist/clients/project-changes.js +40 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +3 -2
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +8 -0
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +21 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +86 -9
- package/node_modules/pi-lens/dist/clients/project-report.js +27 -13
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +175 -20
- package/node_modules/pi-lens/dist/clients/project-trust.js +181 -0
- package/node_modules/pi-lens/dist/clients/quiet-window.js +18 -4
- package/node_modules/pi-lens/dist/clients/read-bridge.js +147 -0
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +159 -1
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +150 -29
- package/node_modules/pi-lens/dist/clients/read-guard.js +185 -4
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +70 -84
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +1122 -221
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +2 -1
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +21 -10
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +46 -0
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +57 -6
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +51 -3
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +30 -1
- package/node_modules/pi-lens/dist/clients/ruff-client.js +27 -44
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +107 -73
- package/node_modules/pi-lens/dist/clients/runtime-context.js +36 -12
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +74 -6
- package/node_modules/pi-lens/dist/clients/runtime-session.js +405 -117
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +71 -48
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +218 -23
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +237 -64
- package/node_modules/pi-lens/dist/clients/rust-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +777 -62
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +2 -1
- package/node_modules/pi-lens/dist/clients/session-state-store.js +3 -2
- package/node_modules/pi-lens/dist/clients/sg-runner.js +284 -174
- package/node_modules/pi-lens/dist/clients/slow-fs.js +2 -1
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +224 -0
- package/node_modules/pi-lens/dist/clients/source-filter.js +184 -20
- package/node_modules/pi-lens/dist/clients/source-walker.js +66 -10
- package/node_modules/pi-lens/dist/clients/startup-scan.js +15 -16
- package/node_modules/pi-lens/dist/clients/startup-timing.js +1 -1
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +38 -8
- package/node_modules/pi-lens/dist/clients/tool-event.js +35 -0
- package/node_modules/pi-lens/dist/clients/tool-policy.js +388 -39
- package/node_modules/pi-lens/dist/clients/tool-render.js +155 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +6 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +419 -50
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +21 -0
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +7 -1
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +7 -2
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +44 -7
- package/node_modules/pi-lens/dist/clients/user-notify.js +51 -0
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +57 -0
- package/node_modules/pi-lens/dist/clients/warm-attach.js +33 -13
- package/node_modules/pi-lens/dist/clients/widget-state.js +300 -43
- package/node_modules/pi-lens/dist/clients/word-index.js +391 -64
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +89 -6
- package/node_modules/pi-lens/dist/index.js +49361 -36888
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +182 -29
- package/node_modules/pi-lens/dist/mcp/server.js +211 -44
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +93 -27
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +200 -36
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +153 -40
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +46 -3
- package/node_modules/pi-lens/dist/tools/module-report.js +9 -0
- package/node_modules/pi-lens/docs/agent-guide.md +2 -2
- package/node_modules/pi-lens/docs/agent-tools.md +21 -10
- package/node_modules/pi-lens/docs/analysisall.md +192 -0
- package/node_modules/pi-lens/docs/api-ports-inventory.md +165 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +5 -2
- package/node_modules/pi-lens/docs/durable-store-audit-1202.md +32 -0
- package/node_modules/pi-lens/docs/environment-variables.md +44 -0
- package/node_modules/pi-lens/docs/fable.md +27 -0
- package/node_modules/pi-lens/docs/features.md +31 -2
- package/node_modules/pi-lens/docs/globalconfig.md +45 -1
- package/node_modules/pi-lens/docs/language-coverage.md +4 -3
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +2 -2
- package/node_modules/pi-lens/docs/mcp.md +37 -4
- package/node_modules/pi-lens/docs/servercapabilities.md +3 -3
- package/node_modules/pi-lens/docs/settings.md +12 -1
- package/node_modules/pi-lens/docs/tools.md +4 -1
- package/node_modules/pi-lens/docs/usage.md +18 -0
- package/node_modules/pi-lens/package.json +10 -10
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-except-test.yml +49 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-bare-host-path-in-win32-branch-test.yml +87 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-nested-links-test.yml +11 -1
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-raw-json-store-write-test.yml +17 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/no-win32-isabsolute-for-qualification-test.yml +16 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-except.yml +3 -15
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-bare-host-path-in-win32-branch.yml +25 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-nested-links.yml +7 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-raw-json-store-write.yml +30 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/no-win32-isabsolute-for-qualification.yml +23 -0
- package/node_modules/pi-lens/rules/tree-sitter-queries/tsx/no-nested-links.yml +4 -6
- package/node_modules/pi-lens/rules/tree-sitter-queries/typescript/duplicate-function-arg.yml +2 -2
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +55 -2
- package/node_modules/pi-mcp-adapter/CHANGELOG.md +25 -0
- package/node_modules/pi-mcp-adapter/README.md +34 -3
- package/node_modules/pi-mcp-adapter/agent-plugin-loader.ts +1 -1
- package/node_modules/pi-mcp-adapter/commands.ts +15 -7
- package/node_modules/pi-mcp-adapter/direct-tools.ts +54 -7
- package/node_modules/pi-mcp-adapter/host-html-template.ts +0 -8
- package/node_modules/pi-mcp-adapter/index.ts +8 -3
- package/node_modules/pi-mcp-adapter/init.ts +29 -5
- package/node_modules/pi-mcp-adapter/lifecycle.ts +0 -4
- package/node_modules/pi-mcp-adapter/mcp-auth-flow.ts +1 -2
- package/node_modules/pi-mcp-adapter/mcp-auth.ts +60 -2
- package/node_modules/pi-mcp-adapter/mcp-panel.ts +45 -9
- package/node_modules/pi-mcp-adapter/mcp-script-worker.mjs +22 -20
- package/node_modules/pi-mcp-adapter/metadata-cache.ts +27 -4
- package/node_modules/pi-mcp-adapter/npx-resolver.ts +81 -26
- package/node_modules/pi-mcp-adapter/package.json +3 -3
- package/node_modules/pi-mcp-adapter/proxy-modes.ts +108 -26
- package/node_modules/pi-mcp-adapter/search-ranking.ts +86 -7
- package/node_modules/pi-mcp-adapter/server-manager.ts +36 -7
- package/node_modules/pi-mcp-adapter/session-recovery.ts +8 -2
- package/node_modules/pi-mcp-adapter/tool-approval.ts +48 -7
- package/node_modules/pi-mcp-adapter/tool-metadata.ts +42 -4
- package/node_modules/pi-mcp-adapter/tool-result-renderer.ts +138 -12
- package/node_modules/pi-mcp-adapter/types.ts +93 -27
- package/node_modules/pi-mcp-adapter/ui-server.ts +20 -1
- package/package.json +17 -13
- package/node_modules/pi-lens/dist/clients/ast-grep-parser.js +0 -86
- package/node_modules/pi-lens/dist/clients/lsp/interactive-install.js +0 -367
- package/node_modules/pi-lens/dist/clients/lsp/lsp-index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/server-strategies.js +0 -5
- package/node_modules/pi-lens/dist/clients/project-metadata.js +0 -690
- package/node_modules/pi-lens/dist/clients/source-groups.js +0 -96
- package/node_modules/pi-lens/dist/clients/startup-marker.js +0 -6
- package/node_modules/pi-lens/dist/clients/types.js +0 -11
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Automatic smells self-surfacing (#1123 item 3).
|
|
3
|
+
*
|
|
4
|
+
* `scripts/analyze-pi-lens-logs.mjs` (`npm run logs:smells`) already detects
|
|
5
|
+
* a wide catalogue of operational smells, but it is MANUAL — an operator has
|
|
6
|
+
* to think to run it. The #1123 investigation found 20 stale-ctx
|
|
7
|
+
* `emit_failed` rows (see `session-lifecycle.ts`'s stale-ctx guard) and 37
|
|
8
|
+
* opengrep respawns sitting in logs for days before an audit went looking.
|
|
9
|
+
* This module closes that gap with a small, ALWAYS-ON rollup that surfaces a
|
|
10
|
+
* subset of the analyzer's smells without anyone running the script.
|
|
11
|
+
*
|
|
12
|
+
* COST BOUND (the constraint that shapes the design): `~/.pi-lens/*.log`
|
|
13
|
+
* files are size-rotated at `PI_LENS_MAX_LOG_SIZE_MB` (default 10MB,
|
|
14
|
+
* `clients/log-cleanup.ts`). Re-scanning a whole rotated log on every
|
|
15
|
+
* `session_start` — the analyzer's own approach — is exactly the megabyte
|
|
16
|
+
* full-file read `session_start` (a hot, input-latency-sensitive path, see
|
|
17
|
+
* `runtime-session.ts`'s cold-start comment) cannot afford. So this module
|
|
18
|
+
* does a BOUNDED TAIL READ: at most `SMELLS_TAIL_BYTES_PER_FILE` bytes from
|
|
19
|
+
* the END of each source log, via a single `pread`-style `fs.readSync` at a
|
|
20
|
+
* computed offset — never a full-file read, by construction (see
|
|
21
|
+
* `tailReadText`). Two source files, ~64KB each: ~128KB total I/O, once per
|
|
22
|
+
* session_start plus one bounded re-check every `SMELLS_TURN_CHECK_INTERVAL`
|
|
23
|
+
* turns — negligible next to the multi-second full startup walk it rides
|
|
24
|
+
* alongside.
|
|
25
|
+
*
|
|
26
|
+
* The tail scan is bounded, but the tail can contain prior-session rows. The
|
|
27
|
+
* session-start caller supplies the session boundary, and rows are admitted
|
|
28
|
+
* only when their own UTC timestamp is at or after that boundary. This keeps
|
|
29
|
+
* the same bounded read without reporting historical failures as current, so
|
|
30
|
+
* this module does not additionally instrument the write call sites
|
|
31
|
+
* (`bus-publish.ts`, `clients/lsp/index.ts`'s respawn path) with parallel
|
|
32
|
+
* live counters. That keeps the change contained to one new module + three
|
|
33
|
+
* small call sites (session_start, `/lens-health`, `turn_end`) instead of
|
|
34
|
+
* touching every producer of the two source logs.
|
|
35
|
+
*
|
|
36
|
+
* Smells covered (deliberately a SUBSET — the two the issue named as having
|
|
37
|
+
* gone unnoticed; the full catalogue stays in the manual analyzer):
|
|
38
|
+
* - stale-ctx `emit_failed`: `bus-events.log` rows where
|
|
39
|
+
* `outcome === "emit_failed"` and `error` contains the SDK's
|
|
40
|
+
* `"stale after session replacement"` fragment (the known-benign class
|
|
41
|
+
* `session-lifecycle.ts` documents — still worth a glance if it recurs
|
|
42
|
+
* often, since a HIGH count could mask a real regression).
|
|
43
|
+
* - opengrep respawns: `latency.log` rows where
|
|
44
|
+
* `phase === "lsp_server_respawn"` and `metadata.serverId === "opengrep"`
|
|
45
|
+
* (`clients/lsp/index.ts`'s existing respawn log point — unmodified).
|
|
46
|
+
*
|
|
47
|
+
* Both counters are gated by trivial threshold constants (`SMELLS_THRESHOLDS`)
|
|
48
|
+
* so a single stray event never surfaces noise — only a repeating pattern
|
|
49
|
+
* does, matching the analyzer's own low/medium/high severity gating.
|
|
50
|
+
*/
|
|
51
|
+
import * as fs from "node:fs";
|
|
52
|
+
import * as path from "node:path";
|
|
53
|
+
import { getGlobalPiLensDir } from "./file-utils.js";
|
|
54
|
+
/** Bounded tail-read budget PER source log file — never a full-file scan. */
|
|
55
|
+
export const SMELLS_TAIL_BYTES_PER_FILE = 64 * 1024;
|
|
56
|
+
/** Re-check cadence at `turn_end`, mirroring `memory-sampler.ts`'s pattern —
|
|
57
|
+
* cheap enough (bounded ~128KB I/O) not to need finer throttling, but a
|
|
58
|
+
* fixed interval keeps it from running on every single turn. */
|
|
59
|
+
export const SMELLS_TURN_CHECK_INTERVAL = 20;
|
|
60
|
+
/** Trivial threshold constants (#1123 item 3's explicit design ask). Counts
|
|
61
|
+
* strictly below these never surface anywhere. */
|
|
62
|
+
export const SMELLS_THRESHOLDS = {
|
|
63
|
+
staleCtxEmitFailed: 5,
|
|
64
|
+
opengrepRespawn: 5,
|
|
65
|
+
};
|
|
66
|
+
const SMELL_LABELS = {
|
|
67
|
+
staleCtxEmitFailed: "stale-ctx emit_failed (bus-events.log)",
|
|
68
|
+
opengrepRespawn: "opengrep respawn (latency.log)",
|
|
69
|
+
};
|
|
70
|
+
const SMELL_KEYS = ["staleCtxEmitFailed", "opengrepRespawn"];
|
|
71
|
+
/**
|
|
72
|
+
* Read at most `maxBytes` from the END of `filePath`. Never reads more than
|
|
73
|
+
* that regardless of the file's actual size — the cost bound is enforced by
|
|
74
|
+
* construction (a single sized `fs.readSync` at a computed offset), not by a
|
|
75
|
+
* convention callers have to remember. Missing file / unreadable → "".
|
|
76
|
+
*
|
|
77
|
+
* The leading (possibly partial) line is dropped whenever the read started
|
|
78
|
+
* mid-file, since we can't tell if it's a truncated JSON row.
|
|
79
|
+
*/
|
|
80
|
+
export function tailReadText(filePath, maxBytes) {
|
|
81
|
+
let fd;
|
|
82
|
+
try {
|
|
83
|
+
fd = fs.openSync(filePath, "r");
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const size = fs.fstatSync(fd).size;
|
|
90
|
+
const readBytes = Math.min(size, maxBytes);
|
|
91
|
+
if (readBytes <= 0)
|
|
92
|
+
return "";
|
|
93
|
+
const start = size - readBytes;
|
|
94
|
+
const buf = Buffer.alloc(readBytes);
|
|
95
|
+
fs.readSync(fd, buf, 0, readBytes, start);
|
|
96
|
+
const text = buf.toString("utf8");
|
|
97
|
+
if (start <= 0)
|
|
98
|
+
return text;
|
|
99
|
+
const firstNewline = text.indexOf("\n");
|
|
100
|
+
return firstNewline >= 0 ? text.slice(firstNewline + 1) : "";
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
try {
|
|
107
|
+
fs.closeSync(fd);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* already closed / nothing to do */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function countMatchingLines(text, predicate, sessionStartMs) {
|
|
115
|
+
let count = 0;
|
|
116
|
+
for (const line of text.split("\n")) {
|
|
117
|
+
if (!line.trim())
|
|
118
|
+
continue;
|
|
119
|
+
let entry;
|
|
120
|
+
try {
|
|
121
|
+
entry = JSON.parse(line);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
continue; // partial/corrupt line at the tail boundary — skip, don't throw
|
|
125
|
+
}
|
|
126
|
+
if (entry &&
|
|
127
|
+
typeof entry === "object" &&
|
|
128
|
+
(sessionStartMs === undefined ||
|
|
129
|
+
isAtOrAfterSessionStart(entry, sessionStartMs)) &&
|
|
130
|
+
predicate(entry)) {
|
|
131
|
+
count++;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return count;
|
|
135
|
+
}
|
|
136
|
+
function isAtOrAfterSessionStart(entry, sessionStartMs) {
|
|
137
|
+
const timestamp = typeof entry.ts === "string" ? Date.parse(entry.ts) : NaN;
|
|
138
|
+
return Number.isFinite(timestamp) && timestamp >= sessionStartMs;
|
|
139
|
+
}
|
|
140
|
+
function isStaleCtxEmitFailed(entry) {
|
|
141
|
+
return (entry.outcome === "emit_failed" &&
|
|
142
|
+
typeof entry.error === "string" &&
|
|
143
|
+
entry.error.includes("stale after session replacement"));
|
|
144
|
+
}
|
|
145
|
+
function isOpengrepRespawn(entry) {
|
|
146
|
+
if (entry.phase !== "lsp_server_respawn")
|
|
147
|
+
return false;
|
|
148
|
+
const metadata = entry.metadata;
|
|
149
|
+
return metadata?.serverId === "opengrep";
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Bounded tail-scan of the two source logs. Total I/O is capped at
|
|
153
|
+
* `2 * SMELLS_TAIL_BYTES_PER_FILE` regardless of how large the (size-rotated,
|
|
154
|
+
* up to ~10MB) source files are. `root` is injectable for tests.
|
|
155
|
+
*/
|
|
156
|
+
export function countRecentSmells(root = getGlobalPiLensDir(), sessionStartMs) {
|
|
157
|
+
const busTail = tailReadText(path.join(root, "bus-events.log"), SMELLS_TAIL_BYTES_PER_FILE);
|
|
158
|
+
const latencyTail = tailReadText(path.join(root, "latency.log"), SMELLS_TAIL_BYTES_PER_FILE);
|
|
159
|
+
return {
|
|
160
|
+
staleCtxEmitFailed: countMatchingLines(busTail, isStaleCtxEmitFailed, sessionStartMs),
|
|
161
|
+
opengrepRespawn: countMatchingLines(latencyTail, isOpengrepRespawn, sessionStartMs),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* One `sessionstart.log` line, gated to fire only when at least one smell is
|
|
166
|
+
* AT/ABOVE its threshold — `null` (nothing logged) otherwise, so an ordinary
|
|
167
|
+
* session never gets a noise line. Pairs with `logSessionStart` the same way
|
|
168
|
+
* every other `session_start ...` line does (see `runtime-session.ts`).
|
|
169
|
+
*/
|
|
170
|
+
export function formatSmellsSessionStartLine(counts) {
|
|
171
|
+
const bits = [];
|
|
172
|
+
for (const key of SMELL_KEYS) {
|
|
173
|
+
if (counts[key] >= SMELLS_THRESHOLDS[key]) {
|
|
174
|
+
bits.push(`${SMELL_LABELS[key]} x${counts[key]}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if (bits.length === 0)
|
|
178
|
+
return null;
|
|
179
|
+
const tailKb = Math.round(SMELLS_TAIL_BYTES_PER_FILE / 1024);
|
|
180
|
+
return `session_start smells (last ${tailKb}KB tail): ${bits.join(", ")} — run \`npm run logs:smells\` for detail`;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Compact, ALWAYS-shown `/lens-health` line (unconditional, like
|
|
184
|
+
* `memory-sampler.ts`'s `formatMemoryHealthLine` — the health command is
|
|
185
|
+
* explicitly requested, so it always renders current counts even when
|
|
186
|
+
* everything is below threshold).
|
|
187
|
+
*/
|
|
188
|
+
export function formatSmellsHealthLine(counts) {
|
|
189
|
+
return (`Smells (recent tail-scan): stale-ctx emit_failed=${counts.staleCtxEmitFailed}` +
|
|
190
|
+
` · opengrep respawn=${counts.opengrepRespawn}`);
|
|
191
|
+
}
|
|
192
|
+
/** `true` on turn 20, 40, 60, ... — never turn 0. Pure so the cadence is
|
|
193
|
+
* unit-testable without driving a real turn loop (mirrors
|
|
194
|
+
* `memory-sampler.ts`'s `shouldEmitMemorySample`). */
|
|
195
|
+
export function shouldCheckSmellsThisTurn(turnIndex) {
|
|
196
|
+
return turnIndex > 0 && turnIndex % SMELLS_TURN_CHECK_INTERVAL === 0;
|
|
197
|
+
}
|
|
198
|
+
// Module-scope session state: which smells have already produced a turn-end
|
|
199
|
+
// note THIS session. Bounded to once per session per smell by construction —
|
|
200
|
+
// see `checkSmellsAndNoteOnce`.
|
|
201
|
+
const notifiedThisSession = new Set();
|
|
202
|
+
/**
|
|
203
|
+
* Given a fresh `counts` read, return zero or more turn-end note strings for
|
|
204
|
+
* smells crossing threshold for the FIRST time this session. A smell already
|
|
205
|
+
* notified this session never fires again, even if its count keeps growing —
|
|
206
|
+
* "once per session per smell" per #1123 item 3's design ask.
|
|
207
|
+
*/
|
|
208
|
+
export function checkSmellsAndNoteOnce(counts) {
|
|
209
|
+
const notes = [];
|
|
210
|
+
for (const key of SMELL_KEYS) {
|
|
211
|
+
if (notifiedThisSession.has(key))
|
|
212
|
+
continue;
|
|
213
|
+
if (counts[key] >= SMELLS_THRESHOLDS[key]) {
|
|
214
|
+
notifiedThisSession.add(key);
|
|
215
|
+
notes.push(`pi-lens smell: ${SMELL_LABELS[key]} x${counts[key]} (recent tail-scan) — run \`npm run logs:smells\` for detail`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return notes;
|
|
219
|
+
}
|
|
220
|
+
/** Reset the "already notified this session" state — call at `session_start`
|
|
221
|
+
* (a fresh session should be able to re-report) and from tests. */
|
|
222
|
+
export function resetSmellsSessionState() {
|
|
223
|
+
notifiedThisSession.clear();
|
|
224
|
+
}
|
|
@@ -19,8 +19,9 @@
|
|
|
19
19
|
import * as fs from "node:fs";
|
|
20
20
|
import * as path from "node:path";
|
|
21
21
|
import { getProjectIgnoreMatcher } from "./file-utils.js";
|
|
22
|
-
import {
|
|
22
|
+
import { classifyGeneratedOrArtifactDetailed, isDeclarationFile, } from "./generated-artifacts.js";
|
|
23
23
|
import { isCodeKindFile, KIND_EXTENSIONS } from "./file-kinds.js";
|
|
24
|
+
import { logLatency } from "./latency-logger.js";
|
|
24
25
|
import { normalizeEphemeralMapKey } from "./path-utils.js";
|
|
25
26
|
import { isSlowFs, SLOW_FS_REDUCED_MAX_FILES } from "./slow-fs.js";
|
|
26
27
|
import { shouldRecurseIntoDir, walkTreeRecursiveSync, walkTreeStackAsync, } from "./source-walker.js";
|
|
@@ -49,12 +50,50 @@ function probeExists(filePath, cache) {
|
|
|
49
50
|
* Order matters: first entry has highest precedence.
|
|
50
51
|
*/
|
|
51
52
|
export const SOURCE_PRECEDENCE = {
|
|
52
|
-
".ts": [".js", ".mjs", ".cjs"],
|
|
53
|
+
".ts": [".js", ".jsx", ".mjs", ".cjs"],
|
|
53
54
|
".tsx": [".jsx", ".js", ".mjs", ".cjs"],
|
|
55
|
+
".mts": [".mjs", ".js", ".jsx", ".cjs"],
|
|
56
|
+
".cts": [".cjs", ".js", ".jsx", ".mjs"],
|
|
54
57
|
".vue": [".js", ".mjs"],
|
|
55
58
|
".svelte": [".js", ".mjs"],
|
|
56
59
|
".coffee": [".js"],
|
|
57
60
|
};
|
|
61
|
+
/**
|
|
62
|
+
* Return source-twin candidates in the policy order for a compiled path.
|
|
63
|
+
* This is the pure counterpart to {@link findSourceSibling}: callers that
|
|
64
|
+
* already have a complete file identity set (the review graph and lens map)
|
|
65
|
+
* must not probe the filesystem or reimplement the extension matrix.
|
|
66
|
+
*/
|
|
67
|
+
export function sourceTwinCandidates(filePath) {
|
|
68
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
69
|
+
const stem = filePath.slice(0, filePath.length - ext.length);
|
|
70
|
+
const sourceExts = ext === ".mjs"
|
|
71
|
+
? [".mts", ".ts", ".tsx", ".cts"]
|
|
72
|
+
: ext === ".cjs"
|
|
73
|
+
? [".cts", ".ts", ".tsx", ".mts"]
|
|
74
|
+
: ext === ".jsx"
|
|
75
|
+
// Deliberately retain the broad fallback: a .jsx next to a .ts
|
|
76
|
+
// is treated as a build artifact even without a .tsx sibling.
|
|
77
|
+
? [".tsx", ".ts", ".mts", ".cts"]
|
|
78
|
+
: ext === ".js"
|
|
79
|
+
? [".ts", ".tsx", ".mts", ".cts"]
|
|
80
|
+
: [];
|
|
81
|
+
return sourceExts.map((sourceExt) => `${stem}${sourceExt}`);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Resolve a compiled file to its canonical source identity within a known
|
|
85
|
+
* file set. The caller supplies normalized identities; this helper deliberately
|
|
86
|
+
* does not touch the filesystem, so complete source-set walks remain bounded
|
|
87
|
+
* and point-in-time consistent.
|
|
88
|
+
*/
|
|
89
|
+
export function canonicalSourceTwin(filePath, availableFiles, excludedFiles) {
|
|
90
|
+
for (const candidate of sourceTwinCandidates(filePath)) {
|
|
91
|
+
if (availableFiles.has(candidate) && !excludedFiles?.has(candidate)) {
|
|
92
|
+
return candidate;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return filePath;
|
|
96
|
+
}
|
|
58
97
|
/**
|
|
59
98
|
* Every extension belonging to a supported file kind. KIND_EXTENSIONS is the
|
|
60
99
|
* single language-extension authority; deriving here makes a newly registered
|
|
@@ -142,16 +181,78 @@ function createKeptFilesAccumulator(maxFiles, prioritizeCodeKinds) {
|
|
|
142
181
|
list: () => [...codeFiles, ...otherFiles].slice(0, maxFiles),
|
|
143
182
|
};
|
|
144
183
|
}
|
|
145
|
-
function
|
|
184
|
+
function createSourceWalkSkipCounters() {
|
|
185
|
+
return {
|
|
186
|
+
generatedOrArtifactSkips: 0,
|
|
187
|
+
buildArtifactSkips: 0,
|
|
188
|
+
generatedDirSkips: 0,
|
|
189
|
+
generatedNameOverrides: 0,
|
|
190
|
+
generatedNameOnlySkips: 0,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Log a one-line rollup for a walk's skip counters through the existing
|
|
195
|
+
* latency/scan logging channel (`logLatency`, ndjson `latency.log`) — mirrors
|
|
196
|
+
* how `getGraphSourceFiles` logs `review_graph_source_walk_entry_budget`.
|
|
197
|
+
* Silent (no log line) when both counters are zero, so a healthy walk with no
|
|
198
|
+
* name/artifact-probe skips produces no new log noise.
|
|
199
|
+
*
|
|
200
|
+
* Logged once per walk call, at the walk layer itself
|
|
201
|
+
* (`collectSourceFilesWithBudget`/`collectSourceFilesWithBudgetAsync`) rather
|
|
202
|
+
* than at each of its several consumers (review-graph builder, project-
|
|
203
|
+
* diagnostics scanner, word-index, …): every one of those consumers funnels
|
|
204
|
+
* through these two functions (directly or via the plain `collectSourceFiles`/
|
|
205
|
+
* `collectSourceFilesAsync` wrappers), so logging here is a single choke point
|
|
206
|
+
* that covers all of them — the smaller diff versus exporting the counters and
|
|
207
|
+
* wiring a log call into each consumer separately.
|
|
208
|
+
*/
|
|
209
|
+
function logSourceWalkSkipsIfAny(rootDir, counters) {
|
|
210
|
+
if (counters.generatedOrArtifactSkips === 0 &&
|
|
211
|
+
counters.buildArtifactSkips === 0 &&
|
|
212
|
+
counters.generatedDirSkips === 0 &&
|
|
213
|
+
counters.generatedNameOverrides === 0) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
logLatency({
|
|
217
|
+
type: "phase",
|
|
218
|
+
phase: "source_walk_skip_summary",
|
|
219
|
+
filePath: rootDir,
|
|
220
|
+
durationMs: 0,
|
|
221
|
+
metadata: {
|
|
222
|
+
generatedOrArtifactSkips: counters.generatedOrArtifactSkips,
|
|
223
|
+
buildArtifactSkips: counters.buildArtifactSkips,
|
|
224
|
+
generatedDirSkips: counters.generatedDirSkips,
|
|
225
|
+
generatedNameOverrides: counters.generatedNameOverrides,
|
|
226
|
+
generatedNameOnlySkips: counters.generatedNameOnlySkips,
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Full verdict+evidence form of {@link shouldSkipGeneratedOrArtifact} (#1107
|
|
232
|
+
* phase 2) — exposes `classifyGeneratedOrArtifactDetailed`'s `"override"`
|
|
233
|
+
* outcome (a WEAK name-only match the content-probe escape hatch rescued)
|
|
234
|
+
* AND, for a `"generated"` verdict, WHICH evidence tier decided it, so
|
|
235
|
+
* `classifyEntry` can count a genuine skip, a rescue, and the residual
|
|
236
|
+
* "unconfirmed name-only skip" bucket all distinctly. See
|
|
237
|
+
* `GeneratedArtifactEvidence`'s doc for why the tier split matters: a
|
|
238
|
+
* tool-facing notice keyed on the RAW skip count fires permanently on any
|
|
239
|
+
* repo with a lockfile, drowning the signal it exists to surface.
|
|
240
|
+
*/
|
|
241
|
+
function classifySkipGeneratedOrArtifact(filePath, options) {
|
|
146
242
|
const includeDeclarations = options?.includeDeclarationFiles === true;
|
|
147
243
|
if (options?.includeGenerated === true) {
|
|
148
|
-
return !includeDeclarations && isDeclarationFile(filePath)
|
|
244
|
+
return !includeDeclarations && isDeclarationFile(filePath)
|
|
245
|
+
? { verdict: "generated", evidence: "strong" }
|
|
246
|
+
: { verdict: "clean" };
|
|
149
247
|
}
|
|
150
|
-
return
|
|
248
|
+
return classifyGeneratedOrArtifactDetailed(filePath, {
|
|
151
249
|
readContentHeader: options?.inspectGeneratedHeaders !== false,
|
|
152
250
|
includeDeclarations: !includeDeclarations,
|
|
153
251
|
});
|
|
154
252
|
}
|
|
253
|
+
function shouldSkipGeneratedOrArtifact(filePath, options) {
|
|
254
|
+
return classifySkipGeneratedOrArtifact(filePath, options).verdict === "generated";
|
|
255
|
+
}
|
|
155
256
|
/**
|
|
156
257
|
* Extract the basename (filename without extension) from a path.
|
|
157
258
|
*/
|
|
@@ -170,17 +271,20 @@ function getDir(filePath) {
|
|
|
170
271
|
* Returns the shadowing source file path if found, null otherwise.
|
|
171
272
|
*/
|
|
172
273
|
export function findSourceSibling(filePath, probeCache) {
|
|
274
|
+
for (const candidate of sourceTwinCandidates(filePath)) {
|
|
275
|
+
if (probeExists(candidate, probeCache))
|
|
276
|
+
return candidate;
|
|
277
|
+
}
|
|
278
|
+
// Keep the legacy non-JS source policies (Vue/Svelte/CoffeeScript) on the
|
|
279
|
+
// same seam without making them part of the JS/TS twin matrix above.
|
|
173
280
|
const ext = path.extname(filePath).toLowerCase();
|
|
174
281
|
const dir = getDir(filePath);
|
|
175
282
|
const base = getBasename(filePath);
|
|
176
|
-
// Find which precedence group this extension belongs to
|
|
177
283
|
for (const [sourceExt, shadowedExts] of Object.entries(SOURCE_PRECEDENCE)) {
|
|
178
|
-
if (shadowedExts.includes(ext)) {
|
|
179
|
-
// This file could be shadowed by a source file with sourceExt
|
|
284
|
+
if (!sourceExtsForCompiledExt(sourceExt).length && shadowedExts.includes(ext)) {
|
|
180
285
|
const siblingPath = path.join(dir, base + sourceExt);
|
|
181
|
-
if (probeExists(siblingPath, probeCache))
|
|
286
|
+
if (probeExists(siblingPath, probeCache))
|
|
182
287
|
return siblingPath;
|
|
183
|
-
}
|
|
184
288
|
}
|
|
185
289
|
}
|
|
186
290
|
return null;
|
|
@@ -191,6 +295,11 @@ export function findSourceSibling(filePath, probeCache) {
|
|
|
191
295
|
* @param probeCache - Optional per-walk memo (see {@link ArtifactProbeCache}).
|
|
192
296
|
* Omit for the original, uncached behavior.
|
|
193
297
|
*/
|
|
298
|
+
function sourceExtsForCompiledExt(sourceExt) {
|
|
299
|
+
return sourceExt === ".ts" || sourceExt === ".tsx" || sourceExt === ".mts" || sourceExt === ".cts"
|
|
300
|
+
? [sourceExt]
|
|
301
|
+
: [];
|
|
302
|
+
}
|
|
194
303
|
export function isBuildArtifact(filePath, probeCache) {
|
|
195
304
|
return findSourceSibling(filePath, probeCache) !== null;
|
|
196
305
|
}
|
|
@@ -261,7 +370,7 @@ function resolveCollectionConfig(rootDir, options, config) {
|
|
|
261
370
|
* produce identical results — the only difference between the two is that the
|
|
262
371
|
* async variant yields to the event loop every N entries.
|
|
263
372
|
*/
|
|
264
|
-
function classifyEntry(entry, fullPath, cfg, probeCache) {
|
|
373
|
+
function classifyEntry(entry, fullPath, cfg, probeCache, skipCounters) {
|
|
265
374
|
const { ignoreMatcher, extraExcludePatterns, extensions, options } = cfg;
|
|
266
375
|
if (entry.isDirectory()) {
|
|
267
376
|
const canRecurse = shouldRecurseIntoDir(entry, fullPath, {
|
|
@@ -269,7 +378,14 @@ function classifyEntry(entry, fullPath, cfg, probeCache) {
|
|
|
269
378
|
extraExcludeDirs: extraExcludePatterns,
|
|
270
379
|
skipGeneratedArtifactDirs: options?.includeGenerated !== true,
|
|
271
380
|
followSymlinks: options?.followSymlinks === true,
|
|
272
|
-
}
|
|
381
|
+
},
|
|
382
|
+
// #1107 phase 2: counts one event per PRUNED DIRECTORY, not per file
|
|
383
|
+
// inside it — see `generatedDirSkips`'s doc on `SourceCollectionResult`.
|
|
384
|
+
skipCounters
|
|
385
|
+
? () => {
|
|
386
|
+
skipCounters.generatedDirSkips += 1;
|
|
387
|
+
}
|
|
388
|
+
: undefined);
|
|
273
389
|
if (!canRecurse)
|
|
274
390
|
return {};
|
|
275
391
|
return { recurseInto: fullPath };
|
|
@@ -281,10 +397,35 @@ function classifyEntry(entry, fullPath, cfg, probeCache) {
|
|
|
281
397
|
if (!extensions.has(ext))
|
|
282
398
|
return {};
|
|
283
399
|
// Skip if this is a build artifact or generated/codegen output.
|
|
284
|
-
|
|
400
|
+
// #1107: counted separately (both are observability-only — neither
|
|
401
|
+
// changes what gets skipped) so an invisible coverage hole (a real
|
|
402
|
+
// `src/gen.ts` silently dropped by the name heuristic) is at least
|
|
403
|
+
// visible via `SourceCollectionResult` and the walk's rollup log line.
|
|
404
|
+
if (isBuildArtifact(fullPath, probeCache)) {
|
|
405
|
+
if (skipCounters)
|
|
406
|
+
skipCounters.buildArtifactSkips += 1;
|
|
285
407
|
return {};
|
|
286
|
-
|
|
408
|
+
}
|
|
409
|
+
// #1107 phase 2: the content-probe escape hatch can rescue a WEAK
|
|
410
|
+
// name-only match ("override") instead of skipping it — see
|
|
411
|
+
// `classifyGeneratedOrArtifactDetailed`'s doc for the evidence
|
|
412
|
+
// order/tradeoff. `evidence` additionally distinguishes a STRONG/
|
|
413
|
+
// content-CONFIRMED skip (expected, not a coverage-hole signal) from
|
|
414
|
+
// the residual "name-only" skip (unconfirmed — the one bucket a
|
|
415
|
+
// tool-facing notice should actually key off).
|
|
416
|
+
const classification = classifySkipGeneratedOrArtifact(fullPath, options);
|
|
417
|
+
if (classification.verdict === "generated") {
|
|
418
|
+
if (skipCounters) {
|
|
419
|
+
skipCounters.generatedOrArtifactSkips += 1;
|
|
420
|
+
if (classification.evidence === "name-only") {
|
|
421
|
+
skipCounters.generatedNameOnlySkips += 1;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
287
424
|
return {};
|
|
425
|
+
}
|
|
426
|
+
if (classification.verdict === "override" && skipCounters) {
|
|
427
|
+
skipCounters.generatedNameOverrides += 1;
|
|
428
|
+
}
|
|
288
429
|
return { keepFile: fullPath };
|
|
289
430
|
}
|
|
290
431
|
return {};
|
|
@@ -320,6 +461,8 @@ export function collectSourceFilesWithBudget(dir, options) {
|
|
|
320
461
|
// on return — never persisted across calls.
|
|
321
462
|
const probeCache = createArtifactProbeCache();
|
|
322
463
|
const budget = createEntryBudget(cfg.maxScanEntries);
|
|
464
|
+
// #1107: per-walk skip counters, discarded on return like the probe cache.
|
|
465
|
+
const skipCounters = createSourceWalkSkipCounters();
|
|
323
466
|
// #761: immediate-descent recursion driver (result-array order preserved),
|
|
324
467
|
// with both caps kept as this walker's own per-entry policy: the hard
|
|
325
468
|
// `maxFiles` results cap (#250) is checked BEFORE charging the entry budget
|
|
@@ -330,14 +473,23 @@ export function collectSourceFilesWithBudget(dir, options) {
|
|
|
330
473
|
return "stop"; // hard cap (#250)
|
|
331
474
|
if (!chargeEntryBudget(budget))
|
|
332
475
|
return "stop"; // entry budget (#760)
|
|
333
|
-
const { recurseInto, keepFile } = classifyEntry(entry, fullPath, cfg, probeCache);
|
|
476
|
+
const { recurseInto, keepFile } = classifyEntry(entry, fullPath, cfg, probeCache, skipCounters);
|
|
334
477
|
if (recurseInto)
|
|
335
478
|
return "recurse";
|
|
336
479
|
if (keepFile)
|
|
337
480
|
kept.push(keepFile);
|
|
338
481
|
return "skip";
|
|
339
482
|
});
|
|
340
|
-
|
|
483
|
+
logSourceWalkSkipsIfAny(rootDir, skipCounters);
|
|
484
|
+
return {
|
|
485
|
+
files: kept.list(),
|
|
486
|
+
entryBudgetExceeded: budget.exceeded,
|
|
487
|
+
generatedOrArtifactSkips: skipCounters.generatedOrArtifactSkips,
|
|
488
|
+
buildArtifactSkips: skipCounters.buildArtifactSkips,
|
|
489
|
+
generatedDirSkips: skipCounters.generatedDirSkips,
|
|
490
|
+
generatedNameOverrides: skipCounters.generatedNameOverrides,
|
|
491
|
+
generatedNameOnlySkips: skipCounters.generatedNameOnlySkips,
|
|
492
|
+
};
|
|
341
493
|
}
|
|
342
494
|
export function collectSourceFiles(dir, options) {
|
|
343
495
|
return collectSourceFilesWithBudget(dir, options).files;
|
|
@@ -345,7 +497,7 @@ export function collectSourceFiles(dir, options) {
|
|
|
345
497
|
/**
|
|
346
498
|
* Async, chunked-yield twin of {@link collectSourceFiles}. Returns the exact
|
|
347
499
|
* same file list (it shares `classifyEntry`), but yields to the event loop
|
|
348
|
-
*
|
|
500
|
+
* on a monotonic time budget so a large tree never holds the loop in
|
|
349
501
|
* one synchronous burst.
|
|
350
502
|
*
|
|
351
503
|
* Why this exists: on a ~2k-file project the synchronous `collectSourceFiles`
|
|
@@ -372,15 +524,18 @@ export async function collectSourceFilesWithBudgetAsync(dir, options) {
|
|
|
372
524
|
// caching across the whole call remains invalidation-free.
|
|
373
525
|
const probeCache = createArtifactProbeCache();
|
|
374
526
|
const budget = createEntryBudget(cfg.maxScanEntries);
|
|
527
|
+
// #1107: per-walk skip counters, discarded on return like the probe cache.
|
|
528
|
+
const skipCounters = createSourceWalkSkipCounters();
|
|
375
529
|
// #761: shared depth-first stack driver (its reverse-push mirrors the sync
|
|
376
530
|
// collector's left-to-right recursion). The async collector charges the
|
|
377
531
|
// entry budget (#760) FIRST, then checks the `maxFiles` cap immediately
|
|
378
532
|
// after keeping a file — subtly different from the sync collector's ordering
|
|
379
533
|
// but preserved verbatim, so both stay byte-identical to their pre-#761 form.
|
|
380
534
|
await walkTreeStackAsync(rootDir, (entry, fullPath) => {
|
|
535
|
+
options?.onEntryVisited?.();
|
|
381
536
|
if (!chargeEntryBudget(budget))
|
|
382
537
|
return "stop"; // entry budget (#760)
|
|
383
|
-
const { recurseInto, keepFile } = classifyEntry(entry, fullPath, cfg, probeCache);
|
|
538
|
+
const { recurseInto, keepFile } = classifyEntry(entry, fullPath, cfg, probeCache, skipCounters);
|
|
384
539
|
if (recurseInto)
|
|
385
540
|
return "recurse";
|
|
386
541
|
if (keepFile) {
|
|
@@ -394,13 +549,22 @@ export async function collectSourceFilesWithBudgetAsync(dir, options) {
|
|
|
394
549
|
// even on a cold scan where every kept file pays the 4 KB generated-
|
|
395
550
|
// header read (measured on a 2k-file fixture). Larger values regress
|
|
396
551
|
// past the ~50ms event-loop budget; see PERF-AUDIT.md.
|
|
397
|
-
|
|
552
|
+
budgetMs: Math.max(1, options?.budgetMs ?? 8),
|
|
398
553
|
// #703: prime the tracked-files set once before the walk so a tracked
|
|
399
554
|
// file matching a `.gitignore`/global pattern still surfaces. Fail-open
|
|
400
555
|
// on no-git/spawn failure.
|
|
401
556
|
beforeWalk: () => cfg.ignoreMatcher.ensureTrackedIndex(),
|
|
402
557
|
});
|
|
403
|
-
|
|
558
|
+
logSourceWalkSkipsIfAny(rootDir, skipCounters);
|
|
559
|
+
return {
|
|
560
|
+
files: kept.list(),
|
|
561
|
+
entryBudgetExceeded: budget.exceeded,
|
|
562
|
+
generatedOrArtifactSkips: skipCounters.generatedOrArtifactSkips,
|
|
563
|
+
buildArtifactSkips: skipCounters.buildArtifactSkips,
|
|
564
|
+
generatedDirSkips: skipCounters.generatedDirSkips,
|
|
565
|
+
generatedNameOverrides: skipCounters.generatedNameOverrides,
|
|
566
|
+
generatedNameOnlySkips: skipCounters.generatedNameOnlySkips,
|
|
567
|
+
};
|
|
404
568
|
}
|
|
405
569
|
/**
|
|
406
570
|
* Get statistics about source file filtering for debugging/monitoring.
|