@herbertgao/pi-extensions 2026.10.1 → 2026.10.2
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 +13 -9
- package/THIRD_PARTY_NOTICES.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
- package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
- package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
- package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
- package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
- package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
- package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
- package/node_modules/@herbertgao/pi-agency/README.md +40 -0
- package/node_modules/@herbertgao/pi-agency/package.json +53 -0
- package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
- package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
- package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
- package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
- package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
- package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
- package/node_modules/@tifan/pi-recap/package.json +1 -1
- package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
- package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
- package/node_modules/pi-antigravity/README.md +23 -2
- package/node_modules/pi-antigravity/package.json +5 -3
- package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
- package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
- package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
- package/node_modules/pi-antigravity/src/image/image.ts +86 -47
- package/node_modules/pi-antigravity/src/image/native.ts +69 -0
- package/node_modules/pi-antigravity/src/index.ts +52 -137
- package/node_modules/pi-antigravity/src/search/search.ts +130 -31
- package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
- package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
- package/node_modules/pi-antigravity/src/types/types.ts +1 -1
- package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
- package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
- package/node_modules/pi-lens/CHANGELOG.md +1064 -0
- package/node_modules/pi-lens/README.md +115 -25
- package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
- package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
- package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
- package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
- package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
- package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
- package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
- package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
- package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
- package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
- package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
- package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
- package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
- package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
- package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
- package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
- package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
- package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
- package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
- package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
- package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
- package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
- package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
- package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
- package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
- package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
- package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
- package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
- package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
- package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
- package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
- package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
- package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
- package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
- package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
- package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
- package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
- package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
- package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
- package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
- package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
- package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
- package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
- package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
- package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
- package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
- package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
- package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
- package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
- package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
- package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
- package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
- package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
- package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
- package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
- package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
- package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
- package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
- package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
- package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
- package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
- package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
- package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
- package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
- package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
- package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
- package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
- package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
- package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
- package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
- package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
- package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
- package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
- package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
- package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
- package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
- package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
- package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
- package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
- package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
- package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
- package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
- package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
- package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
- package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
- package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
- package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
- package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
- package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
- package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
- package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
- package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
- package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
- package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
- package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
- package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
- package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
- package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
- package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
- package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
- package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
- package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
- package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
- package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
- package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
- package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
- package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
- package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
- package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
- package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
- package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
- package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
- package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
- package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
- package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
- package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
- package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
- package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
- package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
- package/node_modules/pi-lens/dist/index.js +16456 -8335
- package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
- package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
- package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
- package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
- package/node_modules/pi-lens/dist/probes/installer.js +135 -0
- package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
- package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
- package/node_modules/pi-lens/docs/agent-guide.md +13 -4
- package/node_modules/pi-lens/docs/agent-tools.md +42 -0
- package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
- package/node_modules/pi-lens/docs/configuration.md +20 -1
- package/node_modules/pi-lens/docs/environment-variables.md +52 -2
- package/node_modules/pi-lens/docs/features.md +8 -4
- package/node_modules/pi-lens/docs/globalconfig.md +1 -0
- package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
- package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
- package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
- package/node_modules/pi-lens/docs/mcp.md +21 -5
- package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
- package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
- package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
- package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
- package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
- package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
- package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
- package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
- package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
- package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
- package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
- package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
- package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
- package/node_modules/pi-lens/docs/settings.md +1 -0
- package/node_modules/pi-lens/docs/usage.md +5 -1
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
- package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
- package/node_modules/pi-lens/package.json +18 -6
- package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
- package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
- package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
- package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
- package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
- package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
- package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
- package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
- package/package.json +15 -10
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
- package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
- package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
- package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
- package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
- package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
- package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
- package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
- package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
- package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
- package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
- package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
- package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
- package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
- package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
- package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
- package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
- package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
- package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
- package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
- package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
- package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
- package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
- package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
- package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
- package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
- package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
- package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
- package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
- package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
- package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
- package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
- package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
- package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
- package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
- package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
- package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
- package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
- package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
- package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
- package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
- package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
- package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
- package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
- package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
- package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
- package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
- package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
- package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
- package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
- package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
- package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
- package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
- package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
- package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
- package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
- package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
- package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
- package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
- package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
- package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
- package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
- package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
- package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
- package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
- package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
- package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
- package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
- package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
- package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
- package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
- package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
- package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
- package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
- package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
- package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
- package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
- package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
- package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
- package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
- package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
- package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
- package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
- package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
- package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
- package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
- package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
- package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
- package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
- package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
- package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
- package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
- package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
- package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
- package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
- package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
- package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
- package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
- package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
- package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
- package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
- package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
- package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
- package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
- package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
- package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
- package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
- package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
- package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
- package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
- package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
- package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
- package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
- package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
- package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
- package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
- package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
- package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
- package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
- package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
- package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
- package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
- package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
- package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
- package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
- package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
- package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
- package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
- package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
- package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
- package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
- package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
- package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
- package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
- package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
- package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
- package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
- package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
- package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
- package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
- package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
- package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
- package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
- package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
- package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
- package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
- package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
- package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
- package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
- package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
- package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
- package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
- package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
- package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
- package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
- package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
- package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
- package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
- package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
- package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
- package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
- package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
- package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
- package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
- package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
- package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
- package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
- package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
- package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
- package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
- package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
- package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
- package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
- package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
- package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
- package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
- package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
- package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
- package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
- package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
- package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
- package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
- package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
- package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
- package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
- package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
- package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
- package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
- package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
- package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
- package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
- package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
- package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
- package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
- package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
- package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
- package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
- package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
- package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
- package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
- package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
- package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
- package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
- package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
- package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
- package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
- package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
- package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
- package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
- package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
- package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
- package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
- package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
- package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
- package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
- package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
- package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
- package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
- package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
- package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
- package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
- package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
- package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
- package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
- package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
- package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
- package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
- package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
- package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
- package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
- package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
- package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
- package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
- package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
- package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
- package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
- package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
- package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
- package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
- package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
- package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
- package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
- package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
- package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
- package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
- package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
- package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
- package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
- package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
- package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
- package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
- package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
- package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
- package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
- package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
- package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
- package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
- package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
- package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
- package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
- package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
- package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
- package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
- package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
- package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
- package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
- package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
- package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
- package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
- package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
- package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
- package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
- package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
- package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
- package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
- package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
- package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
- package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
- package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
- package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
- package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
- package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
- package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
- package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
- package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
- package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
- package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
- package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
- package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
- package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
- package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
- package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
- package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
- package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
- package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
- package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
- package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
- package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
- package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
- package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
- package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
- package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
- package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
- package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
- package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
- package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
- package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
- package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
- package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
- package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
- package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
- package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
- package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
- package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
- package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
- package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
- package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
- package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
- package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
- package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
- package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
- package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
- package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
- package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
- package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
- package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
- package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
- package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
- package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
- package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
- package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
- package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
- package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
- package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
- package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
- package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
- package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
- package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
- package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
- package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
- package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
- package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
- package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
- package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
- package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
- package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
- package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
- package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
- package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
- package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
- package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
- package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
- package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
- package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
- package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
- package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
- package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
- package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
- package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
- package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
- package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
- package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
- package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
- package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
- package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
- package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
- package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
- package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
- package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
- package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
- package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
- package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
- package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
- package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
- package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
- package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
- package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
- package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
- package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
- package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
- package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
- package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
- package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
- package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
- package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
- package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
- package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
- package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
- package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
- package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
- package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
- package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
- package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
- package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
- package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
- package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
- package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
- package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
- package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
- package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
- package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
- package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
- package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
- package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
- package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
- package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
- package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
- package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
- package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
- package/node_modules/pi-lens/dist/i18n.js +0 -69
- package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
- package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
- package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
- package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
- package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
- package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
- package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
- package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
- package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
- package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
- package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
- package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
- package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
- package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
- package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
- package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
- package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
- package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
- package/node_modules/pi-lens/dist/tools/shared.js +0 -29
- package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
|
@@ -1,2571 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* lens_diagnostics tool — cached project diagnostic state (issue #159).
|
|
3
|
-
*
|
|
4
|
-
* Three modes:
|
|
5
|
-
* delta (default) — fixable warnings from the current agent turn, read from
|
|
6
|
-
* the actionable-warnings and code-quality-warnings caches.
|
|
7
|
-
* all — all known diagnostic counts across every file pi-lens has
|
|
8
|
-
* seen this session, read from the widget state.
|
|
9
|
-
* full — active project-wide LSP diagnostic scan merged with all.
|
|
10
|
-
*/
|
|
11
|
-
import { promises as fs } from "node:fs";
|
|
12
|
-
import * as fsSync from "node:fs";
|
|
13
|
-
import * as path from "node:path";
|
|
14
|
-
import { BoundedLruCache } from "../clients/bounded-cache.js";
|
|
15
|
-
import { Type } from "../clients/deps/typebox.js";
|
|
16
|
-
import { anchorsForDiagnostic, applyDispositions, applyWeakDispositions, hasAnyDispositionMarks, hasStrictDispositionMarks, getDisposition, } from "../clients/diagnostic-dispositions.js";
|
|
17
|
-
import { DEPENDENCY_DRIFT_MAX_DELIVERIES } from "../clients/blocker-freshness.js";
|
|
18
|
-
import { freshnessFromMtime } from "../clients/freshness.js";
|
|
19
|
-
import { applyFindingPolicy, loadProjectRulePolicyMap, } from "../clients/dispatch/finding-policy.js";
|
|
20
|
-
import { gateFindingsByPathFreshness } from "../clients/advisory-provenance.js";
|
|
21
|
-
import { formatCacheAgeLabel, markUnreconciledFindings, } from "../clients/finding-delivery-gate.js";
|
|
22
|
-
import { normalizeRuleId } from "../clients/dispatch/rule-id-normalize.js";
|
|
23
|
-
import { applyRulePolicy, } from "../clients/dispatch/rule-policy.js";
|
|
24
|
-
import { compactRenderResult } from "./render-compact.js";
|
|
25
|
-
import { combineAbortSignals } from "../clients/deadline-utils.js";
|
|
26
|
-
import { getProjectIgnoreMatcher } from "../clients/file-utils.js";
|
|
27
|
-
import { isAtOrAboveHomeDir, normalizeEphemeralMapKey, normalizeFilePath, normalizeMapKey, realpathOrResolve, } from "../clients/path-utils.js";
|
|
28
|
-
import { getLSPService } from "../clients/lsp/index.js";
|
|
29
|
-
import { retireInlineBlockerAndResyncGuard } from "../clients/git-guard.js";
|
|
30
|
-
import { primaryServerId, resolveLspCwdForFile, } from "../clients/lsp/config.js";
|
|
31
|
-
import { getFullScanWallClockMs } from "../clients/lsp/workspace-sweep-hold.js";
|
|
32
|
-
import { demoteInferredProjectSweepResults } from "../clients/lsp/inferred-project.js";
|
|
33
|
-
import { hashDiagnosticContent, } from "../clients/lsp/diagnostic-binding.js";
|
|
34
|
-
import { loadProjectDiagnosticsDeltaReport, loadProjectDiagnosticsSnapshot, PROJECT_DIAGNOSTICS_CACHE_VERSION, reconcileProjectDiagnosticsSnapshot, } from "../clients/project-diagnostics/cache.js";
|
|
35
|
-
import { formatCacheAgeOld, formatNotRunEntry, } from "../clients/project-diagnostics/extractors.js";
|
|
36
|
-
import { ANALYZER_IDS, fetchFreshProjectDiagnostics, } from "../clients/project-diagnostics/fresh-fetch.js";
|
|
37
|
-
import { loadBootstrapClients } from "../clients/bootstrap.js";
|
|
38
|
-
import { resolveLensToolName, } from "../clients/tool-config.js";
|
|
39
|
-
import { generatedSkipNotice, scanTruncationNotice, } from "../clients/lens-engine.js";
|
|
40
|
-
import { scanProjectDiagnostics } from "../clients/project-diagnostics/scanner.js";
|
|
41
|
-
import { classifyDiagnosticTier, getFileDiagnosticSummaries, reconcileCorrelatedScanDiagnostics, reconcileScanDiagnostics, reconcileStaleWidgetDependencyBlockers, reconcileStaleWidgetFiles, widgetDiagnosticUri, } from "../clients/widget-state.js";
|
|
42
|
-
import { claimPhaseOncePerSession, logLatency, } from "../clients/latency-logger.js";
|
|
43
|
-
import { logExtension } from "../clients/extension-log.js";
|
|
44
|
-
import { recordDegradationOnce } from "../clients/degradation-ledger.js";
|
|
45
|
-
import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
|
|
46
|
-
import { findAuxiliaryProfileForSource, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
|
|
47
|
-
import { detectFileRole } from "../clients/file-role.js";
|
|
48
|
-
import { STALE_LINE_MARKER } from "../clients/stale-marker.js";
|
|
49
|
-
import { makeProgressReporter, scanningSummaryLine } from "./scan-progress.js";
|
|
50
|
-
import { createLspDiagnosticsTool, LSP_SEVERITY_FILTERS, MAX_BATCH_FILES, renderBatchOutcomeLines, } from "./lsp-diagnostics.js";
|
|
51
|
-
import { demotePastEofDiagnostics, PAST_EOF_STALE_MARKER, resyncDocumentOnPastEof, } from "../clients/diagnostic-line-freshness.js";
|
|
52
|
-
// The widget state exposes the full per-file diagnostic set; this is the tool's
|
|
53
|
-
// own generous display budget per file (independent of the TUI's 12 cap), to
|
|
54
|
-
// keep output bounded on a pathologically broken file.
|
|
55
|
-
const MAX_DIAGNOSTICS_PER_FILE = 50;
|
|
56
|
-
// `paths` (#461) is a wrapper-facing scope restrictor (e.g. "git-staged files
|
|
57
|
-
// only"), not a bulk-listing mechanism — a wrapper that hits this needs to
|
|
58
|
-
// narrow, not paginate. Erroring (rather than silently truncating) means a
|
|
59
|
-
// caller can never believe it checked files it didn't (issue's stated
|
|
60
|
-
// invariant).
|
|
61
|
-
const MAX_PATHS_ENTRIES = MAX_BATCH_FILES;
|
|
62
|
-
// #1623: the reason rendered for every heavyweight-analyzer lane (gitleaks,
|
|
63
|
-
// trivy, govulncheck, dead-code, knip, jscpd, madge, opengrep, test-runner)
|
|
64
|
-
// when mode=full is called without refreshRunners=cheap/all/cached — the
|
|
65
|
-
// "quick mode" gate that skips the expensive fresh-fetch entirely (see
|
|
66
|
-
// `formatFullMode`'s `analyzersPromise`). One shared function so it renders
|
|
67
|
-
// identically everywhere it's used, naming the tool THIS host can call
|
|
68
|
-
// (#2535 F1 — the shared const named the pi tool on MCP too).
|
|
69
|
-
function notRequestedReason(host) {
|
|
70
|
-
return ("refreshRunners not requested this call (quick mode) — pass " +
|
|
71
|
-
`refreshRunners=cheap/all/cached to ${resolveLensToolName("lens_diagnostics", host) ?? "diagnostics"} mode=full to run it`);
|
|
72
|
-
}
|
|
73
|
-
// Wall-clock ceiling for the whole mode=full scan. Even with per-file budgets
|
|
74
|
-
// and abort-signal honoring, a pathological state (a language server hanging on
|
|
75
|
-
// spawn/initialize across many files) could otherwise stall the tool
|
|
76
|
-
// indefinitely — an unattended session was observed hung for ~8h. This hard cap
|
|
77
|
-
// aborts the scan so it always returns (partial) rather than never. Env-tunable.
|
|
78
|
-
// #1618: single source with `clients/runtime-turn.ts`'s idle-reset base delay
|
|
79
|
-
// (see `getBaseLspIdleResetMs`), which derives itself from this value so the
|
|
80
|
-
// two constants can no longer drift back into the idle-reset-fires-mid-sweep
|
|
81
|
-
// relationship that caused #1618 in the first place.
|
|
82
|
-
const FULL_SCAN_WALL_CLOCK_MS = getFullScanWallClockMs();
|
|
83
|
-
// Binding verification is a fallback for result producers that supplied a
|
|
84
|
-
// content hash without an already-computed disk verdict. Keep it one-file-at-a-
|
|
85
|
-
// time and yield periodically: a full scan can contain thousands of results,
|
|
86
|
-
// and a synchronous read loop here would freeze the host even though the LSP
|
|
87
|
-
// work itself was asynchronous (#1198 follow-up).
|
|
88
|
-
const FULL_SCAN_BINDING_YIELD_EVERY = 16;
|
|
89
|
-
async function findFullScanBindingMismatches(results) {
|
|
90
|
-
const mismatched = new WeakSet();
|
|
91
|
-
let sinceYield = 0;
|
|
92
|
-
for (const result of results) {
|
|
93
|
-
if (result.boundToCurrentDisk === false) {
|
|
94
|
-
mismatched.add(result);
|
|
95
|
-
}
|
|
96
|
-
else if (
|
|
97
|
-
// A `true` verdict came from the LSP/cache binding seam and has already
|
|
98
|
-
// compared its fingerprint with disk. Only legacy/partial producers need
|
|
99
|
-
// this fallback read; `unknown` remains unknown when no hash exists.
|
|
100
|
-
result.boundToCurrentDisk !== true &&
|
|
101
|
-
result.contentHash !== undefined) {
|
|
102
|
-
try {
|
|
103
|
-
// Promise-based, sequential reads bound both event-loop occupancy and
|
|
104
|
-
// outstanding file handles. A read failure deliberately leaves the
|
|
105
|
-
// verdict unknown (the historical fail-open behavior).
|
|
106
|
-
const content = await fs.readFile(result.filePath, "utf-8");
|
|
107
|
-
if (hashDiagnosticContent(content) !== result.contentHash) {
|
|
108
|
-
mismatched.add(result);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
// Unreadable/deleted files cannot disprove the binding. Leave them
|
|
113
|
-
// unknown for the existing caller policy, rather than manufacturing a
|
|
114
|
-
// mismatch or a clean verdict here.
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
sinceYield += 1;
|
|
118
|
-
if (sinceYield >= FULL_SCAN_BINDING_YIELD_EVERY) {
|
|
119
|
-
sinceYield = 0;
|
|
120
|
-
await new Promise((resolve) => setImmediate(resolve));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return mismatched;
|
|
124
|
-
}
|
|
125
|
-
export function createLensDiagnosticsTool(cacheManager, getCwd, getLspService = getLSPService,
|
|
126
|
-
// Flush any debounced per-edit dispatches before reading, so files the agent
|
|
127
|
-
// fixed earlier in the turn are re-dispatched and the widget reflects the
|
|
128
|
-
// CURRENT state — not the pre-fix diagnostics still pending in the debounce
|
|
129
|
-
// window. Injected (index wires `flushDebouncedToolResults`); optional so the
|
|
130
|
-
// tool stays decoupled and testable.
|
|
131
|
-
flushPending = async () => { },
|
|
132
|
-
// #571: mode=full's own fresh LSP results are reconciled into the footer
|
|
133
|
-
// (widget-state's `allDiagnostics`) for files this scan got a CONFIRMED
|
|
134
|
-
// (non-timed-out) result for — otherwise an unedited file whose diagnostics
|
|
135
|
-
// only changed because of a change to a file it depends on stays stale in
|
|
136
|
-
// the footer forever. Each reconciled write draws a fresh token from the
|
|
137
|
-
// SAME monotonic source `pipeline.ts`'s per-edit writes use
|
|
138
|
-
// (`RuntimeCoordinator.nextWriteIndex()`, injected by index.ts) so the
|
|
139
|
-
// existing `WriteOrderingGuard` (#555/#560) can tell this scan-originated
|
|
140
|
-
// write apart from a concurrent, genuinely newer per-edit write for the
|
|
141
|
-
// same file. Optional/undefined in tests — an omitted writeIndex always
|
|
142
|
-
// proceeds (see `reconcileScanDiagnostics`'s doc comment).
|
|
143
|
-
nextWriteIndex,
|
|
144
|
-
// #798/#338: capture host UI methods synchronously from the active tool event.
|
|
145
|
-
// The returned closure is safe for the later async sweep to invoke because it
|
|
146
|
-
// never dereferences the session-guarded ctx.ui getter.
|
|
147
|
-
captureLspStatusRepaint, getRuntime, isLensGuardEnabled = () => true) {
|
|
148
|
-
const lspProbe = createLspDiagnosticsTool(nextWriteIndex, (info) => {
|
|
149
|
-
const runtime = getRuntime?.();
|
|
150
|
-
if (!runtime)
|
|
151
|
-
return;
|
|
152
|
-
const retired = retireInlineBlockerAndResyncGuard({
|
|
153
|
-
runtime,
|
|
154
|
-
cacheManager,
|
|
155
|
-
cwd: info.cwd,
|
|
156
|
-
filePath: info.filePath,
|
|
157
|
-
...(info.writeIndex === undefined
|
|
158
|
-
? {}
|
|
159
|
-
: { writeIndex: info.writeIndex }),
|
|
160
|
-
coveredSources: info.coveredSources,
|
|
161
|
-
lensGuardEnabled: isLensGuardEnabled(),
|
|
162
|
-
});
|
|
163
|
-
if (retired) {
|
|
164
|
-
logExtension({
|
|
165
|
-
subsystem: "git-guard",
|
|
166
|
-
level: "debug",
|
|
167
|
-
message: `inline_blocker: retired for ${info.filePath}`,
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
}, getLspService);
|
|
171
|
-
return {
|
|
172
|
-
name: "lens_diagnostics",
|
|
173
|
-
label: "Project Diagnostics",
|
|
174
|
-
description: 'Query pi-lens diagnostics from the session cache or an active LSP probe, at paths or workspace scope. Empty cache is not proof of clean; probe changed paths when findings are absent or stale. Example: `{source: "lsp", scope: "paths", paths: ["src/app.ts"]}`.',
|
|
175
|
-
promptSnippet: "lens_diagnostics source=session reads the session cache and an empty cache is not proof of a clean file; use source=lsp scope=paths for changed files when cached findings are absent or stale",
|
|
176
|
-
renderResult: compactRenderResult(({ details, args, isError, text }) => {
|
|
177
|
-
if (details?.source === "lsp") {
|
|
178
|
-
const count = details.totalDiagnostics ?? 0;
|
|
179
|
-
const files = details.filesChecked ?? details.filesScanned ?? 0;
|
|
180
|
-
const noun = count === 1 ? "diagnostic" : "diagnostics";
|
|
181
|
-
const filePath = typeof details?.filePath === "string"
|
|
182
|
-
? details.filePath
|
|
183
|
-
: Array.isArray(args?.paths) &&
|
|
184
|
-
args.paths.length === 1 &&
|
|
185
|
-
typeof args.paths[0] === "string"
|
|
186
|
-
? args.paths[0]
|
|
187
|
-
: undefined;
|
|
188
|
-
const singleFile = (details?.mode === "file" || files === 1) &&
|
|
189
|
-
typeof filePath === "string"
|
|
190
|
-
? ` ${path.basename(filePath)}`
|
|
191
|
-
: "";
|
|
192
|
-
const scope = files > 1 ? ` across ${files} files` : singleFile;
|
|
193
|
-
if (isError)
|
|
194
|
-
return `lens_diagnostics lsp — ${text.split("\n")[0] ?? "error"}`;
|
|
195
|
-
if ((details.navigationOnlyFiles ?? 0) > 0)
|
|
196
|
-
return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.navigationOnlyFiles} navigation-only`;
|
|
197
|
-
if ((details.unconfirmedFiles ?? 0) > 0)
|
|
198
|
-
return `lens_diagnostics${scope} — ${count} ${noun} · ${details.cleanFiles ?? 0} clean · ${details.unconfirmedFiles} unconfirmed${details.timedOutFiles ? ` (${details.timedOutFiles} timed out)` : ""}`;
|
|
199
|
-
const outcomeCounts = details.outcomeCounts;
|
|
200
|
-
const tooLargeLines = renderBatchOutcomeLines(details.outcomes ?? []);
|
|
201
|
-
if (tooLargeLines.length > 0)
|
|
202
|
-
return `lens_diagnostics${scope} — too large: ${tooLargeLines.join("; ")}`;
|
|
203
|
-
const notConfirmed = outcomeCounts
|
|
204
|
-
? (outcomeCounts.inconclusive ?? 0) +
|
|
205
|
-
(outcomeCounts.unavailable ?? 0) +
|
|
206
|
-
(outcomeCounts.unsupported ?? 0) +
|
|
207
|
-
(outcomeCounts.failed ?? 0) +
|
|
208
|
-
(outcomeCounts.too_large ?? 0)
|
|
209
|
-
: 0;
|
|
210
|
-
if (notConfirmed > 0)
|
|
211
|
-
return `lens_diagnostics${scope} — ${count} ${noun} · ${notConfirmed} checks not confirmed`;
|
|
212
|
-
if ((details.incompleteFiles ?? 0) > 0)
|
|
213
|
-
return `lens_diagnostics${scope} — incomplete (${details.incompleteFiles} files not confirmed)`;
|
|
214
|
-
if (count === 0 && details.unconfirmed)
|
|
215
|
-
return details.timedOut
|
|
216
|
-
? `lens_diagnostics${scope} — timed out (result may be incomplete)`
|
|
217
|
-
: `lens_diagnostics${scope} — unconfirmed (server cannot confirm clean)`;
|
|
218
|
-
return `lens_diagnostics${scope} — ${count} ${noun}`;
|
|
219
|
-
}
|
|
220
|
-
// Streaming progress partials render the live bar (see scanningSummaryLine)
|
|
221
|
-
// instead of the details-driven summary, which would show "0 diagnostics"
|
|
222
|
-
// mid-scan.
|
|
223
|
-
const scanning = scanningSummaryLine(details, text);
|
|
224
|
-
if (scanning)
|
|
225
|
-
return scanning;
|
|
226
|
-
const mode = details?.mode ?? (typeof args.mode === "string" ? args.mode : "delta");
|
|
227
|
-
if (isError) {
|
|
228
|
-
return `lens_diagnostics ${mode} — ${text.split("\n")[0] ?? "error"}`;
|
|
229
|
-
}
|
|
230
|
-
// #533: a "clean"/zero render must say so when heavyweight analyzers never
|
|
231
|
-
// ran this session — a bare "clean" would otherwise misrepresent a cold
|
|
232
|
-
// cache as a confirmed answer from those analyzers.
|
|
233
|
-
const coldSuffix = details?.coldRunners && details.coldRunners.length > 0
|
|
234
|
-
? ` (${details.coldRunners.length} cold: ${details.coldRunners.join(", ")})`
|
|
235
|
-
: "";
|
|
236
|
-
const failedSuffix = details?.failedAnalyzers && details.failedAnalyzers.length > 0
|
|
237
|
-
? ` (${details.failedAnalyzers.length} failed: ${details.failedAnalyzers.map((item) => item.id).join(", ")})`
|
|
238
|
-
: "";
|
|
239
|
-
if (mode === "delta") {
|
|
240
|
-
const aw = details?.actionableWarnings ?? 0;
|
|
241
|
-
const cq = details?.qualityIssues ?? 0;
|
|
242
|
-
const pd = details?.projectDiagnostics ?? 0;
|
|
243
|
-
if (aw + cq + pd === 0)
|
|
244
|
-
return `lens_diagnostics delta — clean${coldSuffix}${failedSuffix}`;
|
|
245
|
-
return `lens_diagnostics delta — ${aw} actionable · ${cq} quality · ${pd} project${coldSuffix}${failedSuffix}`;
|
|
246
|
-
}
|
|
247
|
-
// #1799: `semantic === "blocking"` iff `severity === "error"` holds for
|
|
248
|
-
// RAW classification, but not for the rendered counts — a
|
|
249
|
-
// dependency-drift demotion (#1631, widget-state.ts `isBlocking` vs
|
|
250
|
-
// `countDiagnostics`) revokes an error's blocking authority (stale)
|
|
251
|
-
// while the finding itself stays real evidence, so it lands in
|
|
252
|
-
// `totalErrors` with `totalBlocking` unchanged. That is the one case
|
|
253
|
-
// where the two totals genuinely disagree, and it must still render —
|
|
254
|
-
// otherwise a file with drift-demoted errors reads as "clean". What
|
|
255
|
-
// must NOT happen is printing both terms for the SAME findings (the
|
|
256
|
-
// actual #1799 bug), so `errors` renders only when `blocking === 0`,
|
|
257
|
-
// mirroring the per-file row's own guard below.
|
|
258
|
-
const b = details?.totalBlocking ?? 0;
|
|
259
|
-
const e = details?.totalErrors ?? 0;
|
|
260
|
-
const w = details?.totalWarnings ?? 0;
|
|
261
|
-
// #2414: hint/info tier never contributes to `b + e + w` — a session
|
|
262
|
-
// with only style opinions is genuinely "clean" of code defects, but
|
|
263
|
-
// the one-line summary still names the advisory count so it isn't
|
|
264
|
-
// indistinguishable from a session with zero findings at all.
|
|
265
|
-
const adv = details?.totalAdvisories ?? 0;
|
|
266
|
-
const files = details?.filesWithIssues ?? details?.filesChecked ?? 0;
|
|
267
|
-
if (b + e + w === 0) {
|
|
268
|
-
const advisorySuffix = adv > 0 ? `, ${adv} hint${adv === 1 ? "" : "s"}` : "";
|
|
269
|
-
return `lens_diagnostics ${mode} — clean${advisorySuffix} (${files} files)${coldSuffix}${failedSuffix}`;
|
|
270
|
-
}
|
|
271
|
-
const parts = [`${b} blocking`];
|
|
272
|
-
if (b === 0 && e > 0)
|
|
273
|
-
parts.push(`${e} errors`);
|
|
274
|
-
parts.push(`${w} warnings`);
|
|
275
|
-
return `lens_diagnostics ${mode} — ${parts.join(" · ")} (${files} files)${coldSuffix}${failedSuffix}`;
|
|
276
|
-
}),
|
|
277
|
-
parameters: Type.Object({
|
|
278
|
-
source: Type.Optional(Type.String({
|
|
279
|
-
enum: ["session", "lsp"],
|
|
280
|
-
description: "Evidence source: session cache (empty cache is not proof of a " +
|
|
281
|
-
"clean file — use source=lsp for changed files) or an active LSP probe.",
|
|
282
|
-
})),
|
|
283
|
-
scope: Type.Optional(Type.String({
|
|
284
|
-
enum: ["paths", "workspace"],
|
|
285
|
-
description:
|
|
286
|
-
// #2860: no schema value maps to "current turn's delta" — the
|
|
287
|
-
// default (omitted scope) already gives that for
|
|
288
|
-
// source=session, and for source=lsp `delta` was byte-identical
|
|
289
|
-
// to `paths` (round-2 verify N/F4), so exposing it would only
|
|
290
|
-
// invite a caller to believe it does something distinct.
|
|
291
|
-
"Coverage scope: explicit paths, or workspace (default: current turn's delta for source=session).",
|
|
292
|
-
})),
|
|
293
|
-
mode: Type.Optional(Type.String({
|
|
294
|
-
enum: ["delta", "all", "full"],
|
|
295
|
-
description: "delta = current turn; all = cache-only; full = active LSP scan of paths. Empty cache is not proof of clean.",
|
|
296
|
-
})),
|
|
297
|
-
path: Type.Optional(Type.String({
|
|
298
|
-
description: "One file or directory for source=lsp checks.",
|
|
299
|
-
})),
|
|
300
|
-
concurrency: Type.Optional(Type.Number({
|
|
301
|
-
description: "Source=lsp batch concurrency (maximum 16).",
|
|
302
|
-
})),
|
|
303
|
-
waitMs: Type.Optional(Type.Number({
|
|
304
|
-
description: "Source=lsp per-file wait budget in milliseconds.",
|
|
305
|
-
})),
|
|
306
|
-
serverScope: Type.Optional(Type.String({
|
|
307
|
-
enum: ["primary", "all"],
|
|
308
|
-
description: "Source=lsp server coverage: primary or all.",
|
|
309
|
-
})),
|
|
310
|
-
refreshRunners: Type.Optional(Type.Union([
|
|
311
|
-
Type.Boolean(),
|
|
312
|
-
Type.String({ enum: ["cached", "cheap", "all", "none"] }),
|
|
313
|
-
], {
|
|
314
|
-
description: "Analyzer refresh mode for full view.",
|
|
315
|
-
})),
|
|
316
|
-
maxProjectFiles: Type.Optional(Type.Number({
|
|
317
|
-
description: "Project-file limit for cheap runners.",
|
|
318
|
-
})),
|
|
319
|
-
maxLspFiles: Type.Optional(Type.Number({
|
|
320
|
-
description: "File limit for the LSP sweep.",
|
|
321
|
-
})),
|
|
322
|
-
includeGenerated: Type.Optional(Type.Boolean({
|
|
323
|
-
description: "Include generated-name paths in full scans.",
|
|
324
|
-
})),
|
|
325
|
-
analysisRoot: Type.Optional(Type.String({
|
|
326
|
-
description: "Explicit project directory for heavyweight analyzers; it must exist and resolve strictly below the home directory.",
|
|
327
|
-
})),
|
|
328
|
-
severity: Type.Optional(Type.String({
|
|
329
|
-
enum: [...LSP_SEVERITY_FILTERS],
|
|
330
|
-
description: "Filter by severity threshold (default: all): error shows errors; warning includes errors and warnings; information includes errors, warnings, and information; hint and all show every known tier.",
|
|
331
|
-
})),
|
|
332
|
-
paths: Type.Optional(Type.Array(Type.String(), {
|
|
333
|
-
maxItems: MAX_PATHS_ENTRIES,
|
|
334
|
-
description: "Files or directories to filter.",
|
|
335
|
-
})),
|
|
336
|
-
}),
|
|
337
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
338
|
-
const requestedSource = params.source;
|
|
339
|
-
const requestedScope = params.scope;
|
|
340
|
-
const legacyMode = params.mode;
|
|
341
|
-
const source = requestedSource ?? "session";
|
|
342
|
-
const scope = requestedScope ??
|
|
343
|
-
(legacyMode === "delta" || legacyMode === undefined
|
|
344
|
-
? source === "lsp"
|
|
345
|
-
? "paths"
|
|
346
|
-
: "delta"
|
|
347
|
-
: "workspace");
|
|
348
|
-
const cwd = ctx.cwd ?? getCwd();
|
|
349
|
-
if (source === "lsp") {
|
|
350
|
-
const lspParams = { ...params };
|
|
351
|
-
delete lspParams.source;
|
|
352
|
-
delete lspParams.scope;
|
|
353
|
-
// #2860: explicit `path`/`paths` always win. `scope=workspace`
|
|
354
|
-
// only supplies a `cwd` default (a full workspace sweep) when the
|
|
355
|
-
// caller gave NEITHER — it must never override an explicit
|
|
356
|
-
// narrower request (the #2052 root-eviction hang and the
|
|
357
|
-
// SKILL.md "check a folder" recipe both depend on this: a
|
|
358
|
-
// directory `path` under scope=workspace scans that directory,
|
|
359
|
-
// not the whole project).
|
|
360
|
-
const hasExplicitPath = typeof lspParams.path === "string" && lspParams.path.length > 0;
|
|
361
|
-
const hasExplicitPaths = Array.isArray(lspParams.paths) && lspParams.paths.length > 0;
|
|
362
|
-
if (scope === "workspace" && !hasExplicitPath && !hasExplicitPaths) {
|
|
363
|
-
lspParams.path = cwd;
|
|
364
|
-
}
|
|
365
|
-
const lspStart = Date.now();
|
|
366
|
-
const result = (await lspProbe.execute(_toolCallId, lspParams, signal, onUpdate, ctx.signal ? { cwd, signal: ctx.signal } : { cwd }));
|
|
367
|
-
// #2860 F11: the folded route's own durable trace — without this,
|
|
368
|
-
// F1's class of defect (a real LSP result rendering as "clean")
|
|
369
|
-
// leaves no record in latency.log distinguishing source=lsp from
|
|
370
|
-
// source=session. One record per tool call (not per file), same
|
|
371
|
-
// convention as the mode=all `blocker_freshness_widget_gate`
|
|
372
|
-
// phase above.
|
|
373
|
-
logLatency({
|
|
374
|
-
type: "phase",
|
|
375
|
-
toolName: "lens_diagnostics",
|
|
376
|
-
filePath: cwd,
|
|
377
|
-
phase: "lens_diagnostics_lsp_route",
|
|
378
|
-
durationMs: Date.now() - lspStart,
|
|
379
|
-
metadata: {
|
|
380
|
-
scope,
|
|
381
|
-
isError: result.isError === true,
|
|
382
|
-
totalDiagnostics: result.details?.totalDiagnostics ?? 0,
|
|
383
|
-
cleanFiles: result.details?.cleanFiles ?? 0,
|
|
384
|
-
unconfirmedFiles: result.details?.unconfirmedFiles ?? 0,
|
|
385
|
-
timedOutFiles: result.details?.timedOutFiles ?? 0,
|
|
386
|
-
},
|
|
387
|
-
});
|
|
388
|
-
return {
|
|
389
|
-
...result,
|
|
390
|
-
isError: result.isError === true,
|
|
391
|
-
details: { ...result.details, source, scope },
|
|
392
|
-
};
|
|
393
|
-
}
|
|
394
|
-
if (requestedSource !== undefined) {
|
|
395
|
-
const effectiveMode = legacyMode ??
|
|
396
|
-
(scope === "workspace" ? "all" : scope === "paths" ? "all" : "delta");
|
|
397
|
-
params = { ...params, mode: effectiveMode };
|
|
398
|
-
}
|
|
399
|
-
const repaintLspStatus = captureLspStatusRepaint?.(ctx);
|
|
400
|
-
const mode = params.mode ?? "delta";
|
|
401
|
-
const severity = params.severity ?? "all";
|
|
402
|
-
const refreshRunners = params.refreshRunners;
|
|
403
|
-
const parsePositiveInt = (value) => typeof value === "number" && Number.isFinite(value) && value > 0
|
|
404
|
-
? Math.floor(value)
|
|
405
|
-
: undefined;
|
|
406
|
-
const maxProjectFiles = parsePositiveInt(params.maxProjectFiles);
|
|
407
|
-
const maxLspFiles = parsePositiveInt(params.maxLspFiles);
|
|
408
|
-
const includeGenerated = params.includeGenerated === true;
|
|
409
|
-
let pathsScope;
|
|
410
|
-
try {
|
|
411
|
-
pathsScope = resolvePathsScope(cwd, params.paths);
|
|
412
|
-
}
|
|
413
|
-
catch (err) {
|
|
414
|
-
return {
|
|
415
|
-
content: [
|
|
416
|
-
{
|
|
417
|
-
type: "text",
|
|
418
|
-
text: err instanceof Error ? err.message : String(err),
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
isError: true,
|
|
422
|
-
details: { mode, pathsError: true },
|
|
423
|
-
};
|
|
424
|
-
}
|
|
425
|
-
// Escape aborts the agent *turn*, which fires ctx.signal (the turn-wired
|
|
426
|
-
// abort); the positional signal is the tool-call signal. A registered
|
|
427
|
-
// extension tool only reliably sees the turn abort via ctx.signal, so honor
|
|
428
|
-
// BOTH — else a long mode=full scan ignores Escape (the reported bug).
|
|
429
|
-
const abortSignal = combineAbortSignals(signal, ctx.signal);
|
|
430
|
-
// Reflect the agent's just-made fixes before reporting: flush pending
|
|
431
|
-
// per-edit dispatches (re-records fixed files), then drop entries whose
|
|
432
|
-
// file changed on disk afterwards / was deleted (stale, e.g. external
|
|
433
|
-
// edits). Together these stop fixed-this-session findings from lingering.
|
|
434
|
-
await flushPending();
|
|
435
|
-
const staleDropped = await reconcileStaleWidgetFiles();
|
|
436
|
-
if (mode === "all") {
|
|
437
|
-
// #1631 dependency-axis gate for the mode=all store. `reconcileStaleWidgetFiles`
|
|
438
|
-
// (above) only sees the diagnosed file's OWN mtime; this demotes blocking
|
|
439
|
-
// findings whose forward imports drifted out-of-band. Paired reconciles, one
|
|
440
|
-
// read site — the same gate that covers the turn-end inline-blocker store.
|
|
441
|
-
const dependencyGateStart = Date.now();
|
|
442
|
-
const { demoted: dependencyDemoted, truncatedImports } = await reconcileStaleWidgetDependencyBlockers(cwd, getRuntime?.()?.turnIndex);
|
|
443
|
-
logLatency({
|
|
444
|
-
type: "phase",
|
|
445
|
-
toolName: "lens_diagnostics",
|
|
446
|
-
filePath: cwd,
|
|
447
|
-
phase: "blocker_freshness_widget_gate",
|
|
448
|
-
durationMs: Date.now() - dependencyGateStart,
|
|
449
|
-
metadata: {
|
|
450
|
-
demoted: dependencyDemoted,
|
|
451
|
-
truncatedImports,
|
|
452
|
-
surface: "mode=all",
|
|
453
|
-
},
|
|
454
|
-
});
|
|
455
|
-
return formatAllMode(cwd, severity, undefined, undefined, staleDropped, pathsScope, dependencyDemoted);
|
|
456
|
-
}
|
|
457
|
-
if (mode === "full") {
|
|
458
|
-
// Fold a hard wall-clock ceiling into the abort signal so the scan
|
|
459
|
-
// always terminates (partial) even if a hung server would otherwise
|
|
460
|
-
// stall it forever. AbortSignal.timeout aborts with a TimeoutError.
|
|
461
|
-
const ceiling = AbortSignal.timeout(FULL_SCAN_WALL_CLOCK_MS);
|
|
462
|
-
const fullSignal = combineAbortSignals(abortSignal, ceiling);
|
|
463
|
-
// Stream a throttled progress bar: the full scan is opaque for minutes
|
|
464
|
-
// otherwise.
|
|
465
|
-
const onProgress = makeProgressReporter(onUpdate);
|
|
466
|
-
return formatFullMode(cwd, severity, getLspService(), cacheManager, {
|
|
467
|
-
refreshRunners,
|
|
468
|
-
maxProjectFiles,
|
|
469
|
-
maxLspFiles,
|
|
470
|
-
includeGenerated,
|
|
471
|
-
signal: fullSignal,
|
|
472
|
-
wallClockMs: FULL_SCAN_WALL_CLOCK_MS,
|
|
473
|
-
onProgress,
|
|
474
|
-
onServerReady: repaintLspStatus,
|
|
475
|
-
pathsScope,
|
|
476
|
-
nextWriteIndex,
|
|
477
|
-
runtime: getRuntime?.(),
|
|
478
|
-
analysisRoot: typeof params.analysisRoot === "string"
|
|
479
|
-
? params.analysisRoot
|
|
480
|
-
: undefined,
|
|
481
|
-
host: ctx.host ?? "pi",
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
return formatDeltaMode(cacheManager, cwd, severity, pathsScope);
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
// ── delta mode ────────────────────────────────────────────────────────────────
|
|
489
|
-
function formatProjectDeltaDiagnostic(diagnostic, stale) {
|
|
490
|
-
// #1944: a demoted row loses the authority marker along with its
|
|
491
|
-
// coordinate. It kept the 🔴 while its line was replaced by the stale
|
|
492
|
-
// marker, which is the same "changed the channel, not the body" defect the
|
|
493
|
-
// turn-end advisory carried — `formatFullMode` (this file, the `d.stale`
|
|
494
|
-
// arm of its marker) already drops the marker for exactly this reason.
|
|
495
|
-
const marker = stale
|
|
496
|
-
? "○"
|
|
497
|
-
: diagnostic.semantic === "blocking" || diagnostic.severity === "error"
|
|
498
|
-
? "🔴"
|
|
499
|
-
: "ℹ";
|
|
500
|
-
const rule = diagnostic.rule ?? diagnostic.code ?? diagnostic.runner;
|
|
501
|
-
const where = stale ? STALE_LINE_MARKER : `L${diagnostic.line ?? "?"}`;
|
|
502
|
-
return ` ${marker} ${where} ${rule} ${diagnostic.message}`;
|
|
503
|
-
}
|
|
504
|
-
/**
|
|
505
|
-
* #1634 review round R3: `appendProjectDiagnosticsDeltaLines` re-serves the
|
|
506
|
-
* persisted project-diagnostics DELTA report verbatim — the third of
|
|
507
|
-
* `mode=delta`'s three arms (the other two, actionable/quality warnings, were
|
|
508
|
-
* gated in the F3 pass above), and it carried the identical unfixed shape:
|
|
509
|
-
* cited `file:line` findings from a report with its own `generatedAt`, no
|
|
510
|
-
* freshness check. Same gate, same policy as `applyDeltaFreshnessGate`:
|
|
511
|
-
* missing file -> DROP, edited-since -> DEMOTE (loses its line, keeps
|
|
512
|
-
* rule/message), live -> unchanged. See `clients/finding-delivery-gate.ts`
|
|
513
|
-
* surface `lens-diagnostics:mode-delta`.
|
|
514
|
-
*/
|
|
515
|
-
function appendProjectDiagnosticsDeltaLines(groups, cwd, report, severity, includeFile) {
|
|
516
|
-
const scoped = (report?.diagnostics ?? []).filter((diagnostic) => includeFile(diagnostic.filePath) &&
|
|
517
|
-
matchesSeverity(projectDiagnosticToWidget(diagnostic, diagnostic.filePath), severity));
|
|
518
|
-
const { "lens-diagnostics-delta-project": gated } = gateFindingsByPathFreshness({
|
|
519
|
-
cwd,
|
|
520
|
-
sources: {
|
|
521
|
-
"lens-diagnostics-delta-project": {
|
|
522
|
-
findings: scoped,
|
|
523
|
-
scannedAt: report?.generatedAt,
|
|
524
|
-
citedPath: (d) => d.filePath,
|
|
525
|
-
onMissing: "drop",
|
|
526
|
-
},
|
|
527
|
-
},
|
|
528
|
-
});
|
|
529
|
-
const staleSet = new Set(gated.stale);
|
|
530
|
-
// Concatenating live-then-stale reorders a file's demoted rows to the end
|
|
531
|
-
// of its bucket below (rather than each diagnostic's original report
|
|
532
|
-
// order) — cosmetic only, findings are neither dropped nor duplicated.
|
|
533
|
-
const diagnostics = [...gated.live, ...gated.stale];
|
|
534
|
-
const byFile = new Map();
|
|
535
|
-
for (const diagnostic of diagnostics) {
|
|
536
|
-
const filePath = path.resolve(diagnostic.filePath);
|
|
537
|
-
const bucket = byFile.get(filePath) ?? [];
|
|
538
|
-
bucket.push(diagnostic);
|
|
539
|
-
byFile.set(filePath, bucket);
|
|
540
|
-
}
|
|
541
|
-
for (const [filePath, fileDiagnostics] of byFile) {
|
|
542
|
-
const group = getDeltaFileGroup(groups, cwd, filePath);
|
|
543
|
-
for (const diagnostic of fileDiagnostics) {
|
|
544
|
-
group.projectLines.push(formatProjectDeltaDiagnostic(diagnostic, staleSet.has(diagnostic)));
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
return diagnostics.length;
|
|
548
|
-
}
|
|
549
|
-
/**
|
|
550
|
-
* Bounded content memo for {@link applyCachedDispositions}' strict-immediate
|
|
551
|
-
* path: one read per (file revision), not per query. Keyed through
|
|
552
|
-
* `normalizeEphemeralMapKey` (cheap slash-fold + win32-lowercase — these keys
|
|
553
|
-
* are produced and consumed inside this process only) and validated against a
|
|
554
|
-
* fresh stat's mtimeMs+size on every hit, so an edit invalidates without a
|
|
555
|
-
* TTL. Insertion-ordered LRU cap bounds resident source bytes; repeated
|
|
556
|
-
* queries and multi-file reports that cite the same file reuse one read.
|
|
557
|
-
*/
|
|
558
|
-
export const CACHED_CONTENT_MEMO_CAP = 64;
|
|
559
|
-
const cachedContentMemo = new BoundedLruCache(CACHED_CONTENT_MEMO_CAP);
|
|
560
|
-
function readContentForAnchors(filePath, stat) {
|
|
561
|
-
const key = normalizeEphemeralMapKey(filePath);
|
|
562
|
-
const hit = cachedContentMemo.get(key);
|
|
563
|
-
if (hit && hit.mtimeMs === stat.mtimeMs && hit.size === stat.size) {
|
|
564
|
-
return hit.content;
|
|
565
|
-
}
|
|
566
|
-
let content;
|
|
567
|
-
try {
|
|
568
|
-
content = fsSync.readFileSync(filePath, "utf-8");
|
|
569
|
-
}
|
|
570
|
-
catch {
|
|
571
|
-
return undefined;
|
|
572
|
-
}
|
|
573
|
-
cachedContentMemo.set(key, {
|
|
574
|
-
mtimeMs: stat.mtimeMs,
|
|
575
|
-
size: stat.size,
|
|
576
|
-
content,
|
|
577
|
-
});
|
|
578
|
-
return content;
|
|
579
|
-
}
|
|
580
|
-
/** #2442 test-only: exercise the bounded content memo (LRU) directly,
|
|
581
|
-
* without a full lens_diagnostics dispatch. */
|
|
582
|
-
export function _readContentForAnchorsForTests(filePath, stat) {
|
|
583
|
-
return readContentForAnchors(filePath, stat);
|
|
584
|
-
}
|
|
585
|
-
/**
|
|
586
|
-
* ONE disposition seam for every cache-only lens_diagnostics surface —
|
|
587
|
-
* mode=all summaries, mode=delta's actionable/quality warnings, the delta
|
|
588
|
-
* project-diagnostics report, and the empty-delta carried-over note. Cached
|
|
589
|
-
* findings predate any lens_diagnostic_mark, so each surface must re-apply
|
|
590
|
-
* dispositions at query time; before this seam each site hand-picked its own
|
|
591
|
-
* variant and three of them served stale STRICT false-positive marks via the
|
|
592
|
-
* weak-only filter (a weak filter can never drop a strict-anchored mark, nor
|
|
593
|
-
* any blocking finding — #1625 F1).
|
|
594
|
-
*
|
|
595
|
-
* Strict-immediate path: stat the file once, then compare its mtime against
|
|
596
|
-
* the newest cached diagnostic's own observation stamp through the freshness
|
|
597
|
-
* kernel (`freshnessFromMtime`). Fresh or stamp-less means the current bytes
|
|
598
|
-
* still describe what these findings were observed against, so the content is
|
|
599
|
-
* read (memoized per revision above) and the FULL applyDispositions filter
|
|
600
|
-
* runs — a fresh false-positive mark converges immediately, including on
|
|
601
|
-
* blocking findings.
|
|
602
|
-
*
|
|
603
|
-
* Weak fallback (zero further I/O): a file that is gone/unreadable, or whose
|
|
604
|
-
* mtime moved AFTER these diagnostics were observed. Stale-content edge: the
|
|
605
|
-
* cached findings describe a superseded revision, so their line numbers may no
|
|
606
|
-
* longer align with the lines a mark hashed; deriving strict anchors from the
|
|
607
|
-
* new content could match a DIFFERENT occurrence that merely shares the line
|
|
608
|
-
* number and identical text, so the mark waits for the next real dispatch to
|
|
609
|
-
* re-derive both sides against matching revisions. Weak suppress/defer marks
|
|
610
|
-
* are intent-level and never look at content, so they keep applying here.
|
|
611
|
-
*/
|
|
612
|
-
function applyCachedDispositions(cachedDiagnostics, cwd, filePath) {
|
|
613
|
-
// Zero-I/O fast paths (#1625 F2 precedent): with no marks at all there is
|
|
614
|
-
// nothing to filter; with only weak suppress/defer marks a strict anchor
|
|
615
|
-
// can never match, so the weak filter serves them without any stat/read.
|
|
616
|
-
if (!hasAnyDispositionMarks(cwd))
|
|
617
|
-
return cachedDiagnostics;
|
|
618
|
-
if (!hasStrictDispositionMarks(cwd)) {
|
|
619
|
-
return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
|
|
620
|
-
}
|
|
621
|
-
let stat;
|
|
622
|
-
try {
|
|
623
|
-
stat = fsSync.statSync(filePath);
|
|
624
|
-
}
|
|
625
|
-
catch {
|
|
626
|
-
return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
|
|
627
|
-
}
|
|
628
|
-
const referenceMs = cachedDiagnostics.reduce((latest, d) => Math.max(latest, d.observedAt ?? 0), 0);
|
|
629
|
-
if (referenceMs > 0 &&
|
|
630
|
-
freshnessFromMtime({ mtimeMs: stat.mtimeMs, referenceMs }).verdict ===
|
|
631
|
-
"stale") {
|
|
632
|
-
return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
|
|
633
|
-
}
|
|
634
|
-
const content = readContentForAnchors(filePath, stat);
|
|
635
|
-
if (content === undefined) {
|
|
636
|
-
return applyWeakDispositions(cachedDiagnostics, cwd, filePath);
|
|
637
|
-
}
|
|
638
|
-
return applyDispositions(cachedDiagnostics, cwd, filePath, content);
|
|
639
|
-
}
|
|
640
|
-
/**
|
|
641
|
-
* #755: drop project-diagnostics-delta findings disposed suppress/defer before
|
|
642
|
-
* delta mode re-serves them. Anchored via `projectDiagnosticToWidget` so the
|
|
643
|
-
* (tool, rule) an agent's mark binds to matches mode=full's own project-runner
|
|
644
|
-
* filter (`applyInlineSuppressionsToSummaries`), keeping the two paths in
|
|
645
|
-
* agreement. Goes through the shared {@link applyCachedDispositions} seam so
|
|
646
|
-
* strict false-positive anchors apply immediately, like every other cache-only
|
|
647
|
-
* surface. Also applies the project's
|
|
648
|
-
* `.pi-lens.json` `rules.<id>.disable`/`select` policy so a project's policy
|
|
649
|
-
* overlays the same delta report cache; the cache is otherwise insensitive to
|
|
650
|
-
* a project-config edit (the per-edit path picks it up immediately, but a
|
|
651
|
-
* non-dispatch tool query would replay the pre-edit state).
|
|
652
|
-
*/
|
|
653
|
-
function filterDeltaReportDispositions(report, cwd, policyMap) {
|
|
654
|
-
if (!report?.diagnostics.length)
|
|
655
|
-
return report;
|
|
656
|
-
const kept = report.diagnostics.filter((d) => applyCachedDispositions([projectDiagnosticToWidget(d, d.filePath)], cwd, d.filePath).length === 1);
|
|
657
|
-
const policyKept = applyRulePolicy(kept, policyMap);
|
|
658
|
-
if (policyKept.length === report.diagnostics.length)
|
|
659
|
-
return report;
|
|
660
|
-
return { ...report, diagnostics: policyKept };
|
|
661
|
-
}
|
|
662
|
-
/**
|
|
663
|
-
* #1634 review round: `formatDeltaMode` re-serves the `actionable-warnings`/
|
|
664
|
-
* `code-quality-warnings` caches verbatim — each cited `file:line`, no
|
|
665
|
-
* freshness check — the SAME shape #1622 fixed for gitleaks/trivy-secrets,
|
|
666
|
-
* unfixed here because `mode=delta` is the tool's DEFAULT and easy to miss
|
|
667
|
-
* in a per-store sweep. Both reports carry a report-level `generatedAt`; a
|
|
668
|
-
* warning is gated against it exactly like a scanner finding against
|
|
669
|
-
* `scannedAt`: missing file → DROP (nothing to fix in a file that's gone),
|
|
670
|
-
* edited-since → DEMOTE (loses its line, keeps its rule/message, marked
|
|
671
|
-
* `STALE_LINE_MARKER`), live → unchanged. See
|
|
672
|
-
* `clients/finding-delivery-gate.ts` surface `lens-diagnostics:mode-delta`.
|
|
673
|
-
*/
|
|
674
|
-
function applyDeltaFreshnessGate(files, cwd, generatedAt) {
|
|
675
|
-
// #2504 review round 4 (F1): an actionable-warnings report is no longer the
|
|
676
|
-
// product of exactly ONE pass. A deferred off-hook LSP pull upserts its
|
|
677
|
-
// per-file entries into whatever report is persisted when it lands, so one
|
|
678
|
-
// report can carry entries observed minutes apart. Judging the older half
|
|
679
|
-
// against the newer report-level stamp would pass an out-of-band edit made
|
|
680
|
-
// in between off as live -- exactly the leak this gate exists to stop. Each
|
|
681
|
-
// distinct stamp is gated on its own; the ordinary single-stamp report
|
|
682
|
-
// takes one pass, as before, and the original file order is preserved.
|
|
683
|
-
const stamps = new Set(files.map((file) => file.generatedAt ?? generatedAt ?? ""));
|
|
684
|
-
if (stamps.size > 1) {
|
|
685
|
-
const gatedByPath = new Map();
|
|
686
|
-
for (const stamp of stamps) {
|
|
687
|
-
const group = files.filter((file) => (file.generatedAt ?? generatedAt ?? "") === stamp);
|
|
688
|
-
for (const gated of applyDeltaFreshnessGate(group.map((file) => ({
|
|
689
|
-
filePath: file.filePath,
|
|
690
|
-
warnings: file.warnings,
|
|
691
|
-
})), cwd, stamp === "" ? undefined : stamp)) {
|
|
692
|
-
gatedByPath.set(gated.filePath, gated);
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
return files
|
|
696
|
-
.map((file) => gatedByPath.get(file.filePath))
|
|
697
|
-
.filter((entry) => entry !== undefined);
|
|
698
|
-
}
|
|
699
|
-
// Every entry shares one stamp here; prefer the per-entry one when it has
|
|
700
|
-
// it, so a merged report's single surviving half is still aged by its own.
|
|
701
|
-
const stampedAt = [...stamps][0];
|
|
702
|
-
const effectiveAt = stampedAt !== undefined && stampedAt !== "" ? stampedAt : generatedAt;
|
|
703
|
-
if (!effectiveAt)
|
|
704
|
-
return files;
|
|
705
|
-
const flat = [];
|
|
706
|
-
for (const file of files) {
|
|
707
|
-
for (const warning of file.warnings)
|
|
708
|
-
flat.push({ filePath: file.filePath, warning });
|
|
709
|
-
}
|
|
710
|
-
if (flat.length === 0)
|
|
711
|
-
return files;
|
|
712
|
-
const { "lens-diagnostics-delta": gated } = gateFindingsByPathFreshness({
|
|
713
|
-
cwd,
|
|
714
|
-
sources: {
|
|
715
|
-
"lens-diagnostics-delta": {
|
|
716
|
-
findings: flat,
|
|
717
|
-
scannedAt: effectiveAt,
|
|
718
|
-
citedPath: (f) => f.filePath,
|
|
719
|
-
onMissing: "drop",
|
|
720
|
-
},
|
|
721
|
-
},
|
|
722
|
-
});
|
|
723
|
-
// Two passes (live, then stale) reorder a file's demoted rows to the end
|
|
724
|
-
// of its warnings array rather than the original report order — cosmetic
|
|
725
|
-
// only, nothing is dropped or duplicated.
|
|
726
|
-
const byFile = new Map();
|
|
727
|
-
for (const f of gated.live) {
|
|
728
|
-
const arr = byFile.get(f.filePath) ?? [];
|
|
729
|
-
arr.push(f.warning);
|
|
730
|
-
byFile.set(f.filePath, arr);
|
|
731
|
-
}
|
|
732
|
-
for (const f of gated.stale) {
|
|
733
|
-
const arr = byFile.get(f.filePath) ?? [];
|
|
734
|
-
// Fix B (#3167): carry the stamp the row was judged against so the render
|
|
735
|
-
// can emit one age label per file group — the row's own observation stamp,
|
|
736
|
-
// not the report-level one (the #2504 r4 multi-stamp case).
|
|
737
|
-
arr.push({
|
|
738
|
-
...f.warning,
|
|
739
|
-
stale: true,
|
|
740
|
-
line: undefined,
|
|
741
|
-
staleAsOf: effectiveAt,
|
|
742
|
-
});
|
|
743
|
-
byFile.set(f.filePath, arr);
|
|
744
|
-
}
|
|
745
|
-
return files
|
|
746
|
-
.map((file) => ({
|
|
747
|
-
filePath: file.filePath,
|
|
748
|
-
warnings: byFile.get(file.filePath) ?? [],
|
|
749
|
-
}))
|
|
750
|
-
.filter((file) => file.warnings.length > 0);
|
|
751
|
-
}
|
|
752
|
-
function getDeltaFileGroup(groups, cwd, filePath) {
|
|
753
|
-
const rel = path.relative(cwd, filePath);
|
|
754
|
-
let group = groups.get(rel);
|
|
755
|
-
if (!group) {
|
|
756
|
-
group = {
|
|
757
|
-
rel,
|
|
758
|
-
actionableLines: [],
|
|
759
|
-
qualityLines: [],
|
|
760
|
-
projectLines: [],
|
|
761
|
-
incomplete: false,
|
|
762
|
-
};
|
|
763
|
-
groups.set(rel, group);
|
|
764
|
-
}
|
|
765
|
-
return group;
|
|
766
|
-
}
|
|
767
|
-
// @delivery-surface: lens-diagnostics:mode-delta
|
|
768
|
-
function formatDeltaMode(cacheManager, cwd, severity, pathsScope) {
|
|
769
|
-
const actionableEntry = cacheManager.readCache("actionable-warnings", cwd);
|
|
770
|
-
const qualityEntry = cacheManager.readCache("code-quality-warnings", cwd);
|
|
771
|
-
// #3170: files whose re-verify pass could not complete inside its budget —
|
|
772
|
-
// their rows render as carried, plus this explicit gap label (never a
|
|
773
|
-
// false clean). Computed from the RAW actionable-warnings cache entries
|
|
774
|
-
// because the freshness pipeline below rebuilds the file shape. The
|
|
775
|
-
// re-verify only ever writes the actionable-warnings cache — the quality
|
|
776
|
-
// report's file shape carries no such marker.
|
|
777
|
-
const reverifyIncompletePaths = new Set((actionableEntry?.data?.files ?? [])
|
|
778
|
-
.filter((file) => file.reVerifyIncomplete)
|
|
779
|
-
.map((file) => normalizeMapKey(file.filePath)));
|
|
780
|
-
const actionable = actionableEntry?.data;
|
|
781
|
-
const quality = qualityEntry?.data;
|
|
782
|
-
// Project rule policy (`.pi-lens.json` `rules.<id>.disable` /
|
|
783
|
-
// `rules.<id>.select`) — output-only filtering applied consistently across
|
|
784
|
-
// every mode. The cache-only delta/all paths would otherwise leak project-
|
|
785
|
-
// policy oversight (the per-edit dispatch already filters, but the cache
|
|
786
|
-
// snapshots predate the user's project config edit and would replay
|
|
787
|
-
// filtered findings). Weak-anchored like weak disposition — zero I/O.
|
|
788
|
-
const policyMap = loadProjectRulePolicyMap(cwd);
|
|
789
|
-
const projectDelta = filterDeltaReportDispositions(loadProjectDiagnosticsDeltaReport(cwd), cwd, policyMap);
|
|
790
|
-
const includeFile = createScopedFileFilter(cwd, pathsScope);
|
|
791
|
-
// #755: delta re-serves the actionable/quality caches verbatim, but those
|
|
792
|
-
// were filtered at DISPATCH time — before any lens_diagnostic_mark. Re-apply
|
|
793
|
-
// dispositions here so a mark converges immediately, not only on the next
|
|
794
|
-
// edit — through the shared {@link applyCachedDispositions} seam, so strict
|
|
795
|
-
// false-positive marks land as fast as suppress/defer.
|
|
796
|
-
const visibleWarningFiles = (files) => (files ?? [])
|
|
797
|
-
.filter((file) => includeFile(file.filePath))
|
|
798
|
-
.map((file) => ({
|
|
799
|
-
filePath: file.filePath,
|
|
800
|
-
// #2504 r4 (F1): carried through, so a merged report's older
|
|
801
|
-
// entries keep their own age all the way to the freshness gate.
|
|
802
|
-
generatedAt: file.generatedAt,
|
|
803
|
-
warnings: applyRulePolicy(applyCachedDispositions(file.warnings ?? [], cwd, file.filePath), policyMap),
|
|
804
|
-
}))
|
|
805
|
-
.filter((file) => file.warnings.length > 0);
|
|
806
|
-
const actionableFiles = applyDeltaFreshnessGate(visibleWarningFiles(actionable?.files), cwd, actionable?.generatedAt);
|
|
807
|
-
const qualityFiles = applyDeltaFreshnessGate(visibleWarningFiles(quality?.files), cwd, quality?.generatedAt);
|
|
808
|
-
const matchingWarnings = (warnings) => warnings.filter((warning) => matchesRecordSeverity(warning.severity, severity));
|
|
809
|
-
const filteredActionableFiles = actionableFiles
|
|
810
|
-
.map((file) => ({
|
|
811
|
-
...file,
|
|
812
|
-
warnings: matchingWarnings(file.warnings),
|
|
813
|
-
}))
|
|
814
|
-
.filter((file) => file.warnings.length > 0);
|
|
815
|
-
const filteredQualityFiles = qualityFiles
|
|
816
|
-
.map((file) => ({
|
|
817
|
-
...file,
|
|
818
|
-
warnings: matchingWarnings(file.warnings),
|
|
819
|
-
}))
|
|
820
|
-
.filter((file) => file.warnings.length > 0);
|
|
821
|
-
// #3196: one grouping pass keyed by file — every tier below appends into
|
|
822
|
-
// the SAME per-file group (see `DeltaFileGroup`/`getDeltaFileGroup`), so a
|
|
823
|
-
// file present in more than one report always renders its rows under its
|
|
824
|
-
// own header, wherever that header ends up in the final buffer.
|
|
825
|
-
const groups = new Map();
|
|
826
|
-
// Fixable warnings from actionable-warnings and quality cache entries retain
|
|
827
|
-
// their own severity tier. Apply the same threshold semantics as the LSP path.
|
|
828
|
-
// F5/F10 (#3168): at most ONE label group (age and/or #3170's re-verify
|
|
829
|
-
// gap) per file. The actionable tier is folded first, so its stale row (if
|
|
830
|
-
// any) wins the group's age label; the quality tier only supplies one when
|
|
831
|
-
// the actionable tier had none for that file (#3168 F10(c)'s quality-only
|
|
832
|
-
// shape). The `(re-verify incomplete)` flag is a fact about the file (from
|
|
833
|
-
// the raw actionable cache, independent of which tier's rows happen to
|
|
834
|
-
// render it), so it is recorded on the group the first time either tier
|
|
835
|
-
// touches that file and never predicted.
|
|
836
|
-
if (filteredActionableFiles.length > 0) {
|
|
837
|
-
for (const file of filteredActionableFiles) {
|
|
838
|
-
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
839
|
-
for (const w of file.warnings) {
|
|
840
|
-
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
841
|
-
group.actionableLines.push(` ⚠ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
842
|
-
}
|
|
843
|
-
const key = normalizeMapKey(file.filePath);
|
|
844
|
-
if (reverifyIncompletePaths.has(key))
|
|
845
|
-
group.incomplete = true;
|
|
846
|
-
if (!group.staleRow) {
|
|
847
|
-
const staleWarning = file.warnings.find((w) => w.stale);
|
|
848
|
-
if (staleWarning)
|
|
849
|
-
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
}
|
|
853
|
-
// Quality issues
|
|
854
|
-
if (filteredQualityFiles.length > 0) {
|
|
855
|
-
for (const file of filteredQualityFiles) {
|
|
856
|
-
const group = getDeltaFileGroup(groups, cwd, file.filePath);
|
|
857
|
-
for (const w of file.warnings) {
|
|
858
|
-
const where = w.stale ? STALE_LINE_MARKER : `L${w.line ?? "?"}`;
|
|
859
|
-
group.qualityLines.push(` ℹ ${where} ${w.rule ?? w.code ?? w.tool} ${w.message}`);
|
|
860
|
-
}
|
|
861
|
-
const key = normalizeMapKey(file.filePath);
|
|
862
|
-
if (reverifyIncompletePaths.has(key))
|
|
863
|
-
group.incomplete = true;
|
|
864
|
-
if (!group.staleRow) {
|
|
865
|
-
const staleWarning = file.warnings.find((w) => w.stale);
|
|
866
|
-
if (staleWarning)
|
|
867
|
-
group.staleRow = { staleAsOf: staleWarning.staleAsOf };
|
|
868
|
-
}
|
|
869
|
-
}
|
|
870
|
-
}
|
|
871
|
-
const projectDeltaCount = appendProjectDiagnosticsDeltaLines(groups, cwd, projectDelta, severity, includeFile);
|
|
872
|
-
const lines = [];
|
|
873
|
-
for (const group of groups.values()) {
|
|
874
|
-
lines.push(group.rel);
|
|
875
|
-
lines.push(...group.actionableLines);
|
|
876
|
-
lines.push(...group.qualityLines);
|
|
877
|
-
lines.push(...group.projectLines);
|
|
878
|
-
if (group.staleRow) {
|
|
879
|
-
lines.push(` (${formatCacheAgeLabel(group.staleRow.staleAsOf)})`);
|
|
880
|
-
}
|
|
881
|
-
if (group.incomplete)
|
|
882
|
-
lines.push(" (re-verify incomplete)");
|
|
883
|
-
}
|
|
884
|
-
const selectedActionableFiles = filteredActionableFiles;
|
|
885
|
-
const selectedQualityFiles = filteredQualityFiles;
|
|
886
|
-
const aw = selectedActionableFiles.reduce((count, file) => count + file.warnings.length, 0);
|
|
887
|
-
const cq = selectedQualityFiles.reduce((count, file) => count + file.warnings.length, 0);
|
|
888
|
-
if (lines.length === 0) {
|
|
889
|
-
let text = `No ${severity === "all" ? "" : severity + " "}issues in the current turn delta.`;
|
|
890
|
-
// Discoverability (#190): `delta` is current-turn-scoped, so it's empty
|
|
891
|
-
// right after a resume even when prior findings were rehydrated into the
|
|
892
|
-
// session-wide view. Point the agent at `mode=all` when that's the case.
|
|
893
|
-
const carried = getFileDiagnosticSummaries()
|
|
894
|
-
.filter((f) => includeFile(f.filePath))
|
|
895
|
-
.map((f) => ({
|
|
896
|
-
...f,
|
|
897
|
-
diagnostics: applyRulePolicy(applyCachedDispositions(f.diagnostics, cwd, f.filePath), policyMap),
|
|
898
|
-
}))
|
|
899
|
-
.filter((f) => f.diagnostics.length > 0);
|
|
900
|
-
const carriedIssues = carried.reduce((n, f) => n + f.diagnostics.length, 0);
|
|
901
|
-
if (carried.length > 0) {
|
|
902
|
-
text += ` ${carriedIssues} finding${carriedIssues === 1 ? "" : "s"} across ${carried.length} file${carried.length === 1 ? "" : "s"} carried over from earlier this session — use mode=all to see them.`;
|
|
903
|
-
}
|
|
904
|
-
text += pathsScopeCacheOnlyNote(pathsScope);
|
|
905
|
-
return {
|
|
906
|
-
content: [{ type: "text", text }],
|
|
907
|
-
details: { mode: "delta", warnings: 0, carriedOverFiles: carried.length },
|
|
908
|
-
};
|
|
909
|
-
}
|
|
910
|
-
const summary = `\nSummary (turn delta): ${aw} actionable warning${aw === 1 ? "" : "s"} · ${cq} quality issue${cq === 1 ? "" : "s"} · ${projectDeltaCount} project diagnostic${projectDeltaCount === 1 ? "" : "s"}`;
|
|
911
|
-
return {
|
|
912
|
-
content: [{ type: "text", text: lines.join("\n") + summary }],
|
|
913
|
-
details: {
|
|
914
|
-
mode: "delta",
|
|
915
|
-
actionableWarnings: aw,
|
|
916
|
-
qualityIssues: cq,
|
|
917
|
-
projectDiagnostics: projectDeltaCount,
|
|
918
|
-
},
|
|
919
|
-
};
|
|
920
|
-
}
|
|
921
|
-
// ── all mode ──────────────────────────────────────────────────────────────────
|
|
922
|
-
function createCurrentIgnoreFilter(cwd) {
|
|
923
|
-
try {
|
|
924
|
-
const matcher = getProjectIgnoreMatcher(cwd);
|
|
925
|
-
return (filePath) => !matcher.isIgnored(path.resolve(filePath), false);
|
|
926
|
-
}
|
|
927
|
-
catch {
|
|
928
|
-
// Diagnostics should remain available even if an ignore config/root probe is
|
|
929
|
-
// temporarily unreadable. Walkers already treat config load failures as
|
|
930
|
-
// non-fatal; match that behavior for cached diagnostic presentation.
|
|
931
|
-
return () => true;
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
/**
|
|
935
|
-
* Resolves the `paths` param into a scope predicate. Returns `undefined` when
|
|
936
|
-
* `paths` was not provided (callers fall back to no restriction). Throws a
|
|
937
|
-
* plain `Error` when the cap is exceeded — the caller turns that into a
|
|
938
|
-
* clear tool error rather than silently truncating (a wrapper must not
|
|
939
|
-
* believe it checked files it didn't).
|
|
940
|
-
*/
|
|
941
|
-
function resolvePathsScope(cwd, rawPaths) {
|
|
942
|
-
if (!Array.isArray(rawPaths) || rawPaths.length === 0)
|
|
943
|
-
return undefined;
|
|
944
|
-
if (rawPaths.length > MAX_PATHS_ENTRIES) {
|
|
945
|
-
throw new Error(`paths has ${rawPaths.length} entries, over the ${MAX_PATHS_ENTRIES} cap. ` +
|
|
946
|
-
"Narrow the list or omit paths to scan without a restriction.");
|
|
947
|
-
}
|
|
948
|
-
const entries = rawPaths
|
|
949
|
-
.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
950
|
-
.map((entry) => path.resolve(path.isAbsolute(entry) ? entry : path.resolve(cwd, entry)));
|
|
951
|
-
const missing = [];
|
|
952
|
-
const fileEntries = [];
|
|
953
|
-
const existingEntries = [];
|
|
954
|
-
const dirEntries = [];
|
|
955
|
-
for (const entry of entries) {
|
|
956
|
-
let stat;
|
|
957
|
-
try {
|
|
958
|
-
stat = fsSync.statSync(entry);
|
|
959
|
-
}
|
|
960
|
-
catch {
|
|
961
|
-
// Nonexistent on disk right now — still treated as a file-key match
|
|
962
|
-
// candidate for the delta/all cache filter (not just dropped): the
|
|
963
|
-
// wrapper use case passes git output, where a deleted-but-staged path
|
|
964
|
-
// legitimately has no on-disk file yet may still have cached findings
|
|
965
|
-
// from before deletion. Excluded from `existingEntries` (nothing to
|
|
966
|
-
// actively scan) and recorded in `missing` for mode=full's skipped-note.
|
|
967
|
-
missing.push(entry);
|
|
968
|
-
fileEntries.push(entry);
|
|
969
|
-
continue;
|
|
970
|
-
}
|
|
971
|
-
if (stat.isDirectory()) {
|
|
972
|
-
dirEntries.push(entry);
|
|
973
|
-
}
|
|
974
|
-
else {
|
|
975
|
-
fileEntries.push(entry);
|
|
976
|
-
existingEntries.push(entry);
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
const fileKeys = new Set(fileEntries.map((f) => normalizeFilePath(f)));
|
|
980
|
-
const dirPrefixes = dirEntries.map((d) => normalizeFilePath(d));
|
|
981
|
-
const includeExplicitFile = (filePath) => fileKeys.has(normalizeFilePath(path.resolve(filePath)));
|
|
982
|
-
const includeFile = (filePath) => {
|
|
983
|
-
const key = normalizeFilePath(path.resolve(filePath));
|
|
984
|
-
if (fileKeys.has(key))
|
|
985
|
-
return true;
|
|
986
|
-
return dirPrefixes.some((prefix) => key === prefix || key.startsWith(`${prefix}/`));
|
|
987
|
-
};
|
|
988
|
-
return {
|
|
989
|
-
includeFile,
|
|
990
|
-
includeExplicitFile,
|
|
991
|
-
missing,
|
|
992
|
-
existingEntries,
|
|
993
|
-
hasDirectoryEntries: dirEntries.length > 0,
|
|
994
|
-
};
|
|
995
|
-
}
|
|
996
|
-
/** Short, honest note for delta/all when `paths` filtered everything out — these modes are cache-only, so an unseen file legitimately shows nothing. */
|
|
997
|
-
function pathsScopeCacheOnlyNote(scope) {
|
|
998
|
-
if (!scope)
|
|
999
|
-
return "";
|
|
1000
|
-
return ("\n\nNote: paths restricts this to cached findings for files pi-lens has " +
|
|
1001
|
-
"already dispatched this session — mode=delta/all can't see files it " +
|
|
1002
|
-
"hasn't touched. Use mode=full to actively scan exactly these paths.");
|
|
1003
|
-
}
|
|
1004
|
-
/** Short note listing paths entries that don't exist on disk (mode=full only — the wrapper use case passes git output, where a deleted-but-staged file WILL appear). */
|
|
1005
|
-
function pathsScopeMissingNote(scope) {
|
|
1006
|
-
if (!scope || scope.missing.length === 0)
|
|
1007
|
-
return "";
|
|
1008
|
-
const shown = scope.missing.slice(0, 10).map((p) => path.basename(p));
|
|
1009
|
-
const more = scope.missing.length > shown.length
|
|
1010
|
-
? ` (+${scope.missing.length - shown.length} more)`
|
|
1011
|
-
: "";
|
|
1012
|
-
return `\n\n⚠ Skipped ${scope.missing.length} path(s) not found on disk: ${shown.join(", ")}${more}`;
|
|
1013
|
-
}
|
|
1014
|
-
function createScopedFileFilter(cwd, pathsScope) {
|
|
1015
|
-
const ignoreFile = createCurrentIgnoreFilter(cwd);
|
|
1016
|
-
return (filePath) => {
|
|
1017
|
-
if (!pathsScope)
|
|
1018
|
-
return ignoreFile(filePath);
|
|
1019
|
-
if (!pathsScope.includeFile(filePath))
|
|
1020
|
-
return false;
|
|
1021
|
-
return pathsScope.includeExplicitFile(filePath) || ignoreFile(filePath);
|
|
1022
|
-
};
|
|
1023
|
-
}
|
|
1024
|
-
function filterProjectDiagnosticsSnapshot(snapshot, includeFile) {
|
|
1025
|
-
if (!snapshot)
|
|
1026
|
-
return undefined;
|
|
1027
|
-
return {
|
|
1028
|
-
...snapshot,
|
|
1029
|
-
diagnostics: snapshot.diagnostics.filter((d) => includeFile(d.filePath)),
|
|
1030
|
-
};
|
|
1031
|
-
}
|
|
1032
|
-
function filterProjectDiagnosticsDeltaReport(report, includeFile) {
|
|
1033
|
-
if (!report)
|
|
1034
|
-
return undefined;
|
|
1035
|
-
return {
|
|
1036
|
-
...report,
|
|
1037
|
-
diagnostics: report.diagnostics.filter((d) => includeFile(d.filePath)),
|
|
1038
|
-
};
|
|
1039
|
-
}
|
|
1040
|
-
/**
|
|
1041
|
-
* Apply the project rule policy to a project-diagnostics snapshot/delta
|
|
1042
|
-
* report's `diagnostics` list, in place of the collection's shape. Used by
|
|
1043
|
-
* `formatFullMode` so `projectSnapshot.diagnostics.length` /
|
|
1044
|
-
* `projectDelta.diagnostics.length` — which feed both the merged summaries
|
|
1045
|
-
* and the `details.projectDiagnostics`/`details.projectDiagnosticsDelta`
|
|
1046
|
-
* counts — already reflect the policy instead of reporting a pre-policy
|
|
1047
|
-
* count that disagrees with the rendered text.
|
|
1048
|
-
*/
|
|
1049
|
-
function applyProjectRulePolicy(value, policyMap) {
|
|
1050
|
-
if (!value)
|
|
1051
|
-
return undefined;
|
|
1052
|
-
return {
|
|
1053
|
-
...value,
|
|
1054
|
-
diagnostics: applyRulePolicy(value.diagnostics, policyMap),
|
|
1055
|
-
};
|
|
1056
|
-
}
|
|
1057
|
-
/** A diagnostic counts as error-like when it blocks or has error severity.
|
|
1058
|
-
* A `stale` (#1641 past-EOF) entry is excluded — its cited line no longer
|
|
1059
|
-
* exists in the current file, so it can no longer count toward a hard stop. */
|
|
1060
|
-
function isErrorLike(d) {
|
|
1061
|
-
if (d.stale)
|
|
1062
|
-
return false;
|
|
1063
|
-
return d.semantic === "blocking" || d.severity === "error";
|
|
1064
|
-
}
|
|
1065
|
-
function matchesSeverity(d, severity) {
|
|
1066
|
-
return matchesRecordSeverity(isErrorLike(d) ? "error" : d.severity, severity);
|
|
1067
|
-
}
|
|
1068
|
-
function matchesRecordSeverity(recordSeverity, requested) {
|
|
1069
|
-
if (requested === "all")
|
|
1070
|
-
return true;
|
|
1071
|
-
if (requested === "error")
|
|
1072
|
-
return recordSeverity === "error";
|
|
1073
|
-
if (requested === "warning")
|
|
1074
|
-
return recordSeverity === "error" || recordSeverity === "warning";
|
|
1075
|
-
if (requested === "information")
|
|
1076
|
-
return (recordSeverity === "error" ||
|
|
1077
|
-
recordSeverity === "warning" ||
|
|
1078
|
-
recordSeverity === "info" ||
|
|
1079
|
-
recordSeverity === "note" ||
|
|
1080
|
-
recordSeverity === "help");
|
|
1081
|
-
if (requested === "hint")
|
|
1082
|
-
return (recordSeverity === "error" ||
|
|
1083
|
-
recordSeverity === "warning" ||
|
|
1084
|
-
recordSeverity === "info" ||
|
|
1085
|
-
recordSeverity === "note" ||
|
|
1086
|
-
recordSeverity === "help" ||
|
|
1087
|
-
recordSeverity === "hint");
|
|
1088
|
-
return true;
|
|
1089
|
-
}
|
|
1090
|
-
/** Most-important first: blocking → error → warning/other, then by line. */
|
|
1091
|
-
function severityRank(d) {
|
|
1092
|
-
if (d.semantic === "blocking")
|
|
1093
|
-
return 0;
|
|
1094
|
-
if (d.severity === "error")
|
|
1095
|
-
return 1;
|
|
1096
|
-
if (d.severity === "warning")
|
|
1097
|
-
return 2;
|
|
1098
|
-
return 3;
|
|
1099
|
-
}
|
|
1100
|
-
function bySeverityThenLine(a, b) {
|
|
1101
|
-
return severityRank(a) - severityRank(b) || (a.line ?? 0) - (b.line ?? 0);
|
|
1102
|
-
}
|
|
1103
|
-
function lspSeverityName(severity) {
|
|
1104
|
-
if (severity === 1)
|
|
1105
|
-
return "error";
|
|
1106
|
-
if (severity === 2)
|
|
1107
|
-
return "warning";
|
|
1108
|
-
if (severity === 3)
|
|
1109
|
-
return "info";
|
|
1110
|
-
return "hint";
|
|
1111
|
-
}
|
|
1112
|
-
function lspRuleId(diagnostic) {
|
|
1113
|
-
const code = diagnostic.code === undefined ? undefined : String(diagnostic.code);
|
|
1114
|
-
if (diagnostic.source && code)
|
|
1115
|
-
return `${diagnostic.source}:${code}`;
|
|
1116
|
-
return diagnostic.source ?? code ?? "lsp";
|
|
1117
|
-
}
|
|
1118
|
-
function lspDiagnosticToWidget(diagnostic) {
|
|
1119
|
-
const severity = lspSeverityName(diagnostic.severity);
|
|
1120
|
-
const rule = lspRuleId(diagnostic);
|
|
1121
|
-
return {
|
|
1122
|
-
severity,
|
|
1123
|
-
semantic: diagnostic.severity === 1 ? "blocking" : "warning",
|
|
1124
|
-
message: diagnostic.message,
|
|
1125
|
-
line: diagnostic.range.start.line + 1,
|
|
1126
|
-
col: diagnostic.range.start.character + 1,
|
|
1127
|
-
rule,
|
|
1128
|
-
// #3041: keep auxiliary provenance. The footer-reconcile loop above already
|
|
1129
|
-
// gives a swept aux finding its real tool id via `retagAuxiliaryDiagnostics`
|
|
1130
|
-
// (#692); this second conversion of the SAME raw diagnostics did not, and
|
|
1131
|
-
// dispositions are anchored by `tool` — so a `false-positive`/`suppress`
|
|
1132
|
-
// mark recorded against the per-edit `ast-grep` finding never matched the
|
|
1133
|
-
// `lsp`-labelled copy mode=full renders.
|
|
1134
|
-
tool: findAuxiliaryProfileForSource(diagnostic.source)?.tool ?? "lsp",
|
|
1135
|
-
};
|
|
1136
|
-
}
|
|
1137
|
-
function projectDiagnosticToWidget(diagnostic, filePath) {
|
|
1138
|
-
return {
|
|
1139
|
-
severity: diagnostic.severity,
|
|
1140
|
-
semantic: diagnostic.semantic,
|
|
1141
|
-
message: diagnostic.message,
|
|
1142
|
-
line: diagnostic.line,
|
|
1143
|
-
col: diagnostic.column,
|
|
1144
|
-
rule: diagnostic.rule ?? diagnostic.code,
|
|
1145
|
-
tool: diagnostic.runner || diagnostic.tool,
|
|
1146
|
-
uri: widgetDiagnosticUri(filePath, diagnostic.line, diagnostic.column),
|
|
1147
|
-
};
|
|
1148
|
-
}
|
|
1149
|
-
// The ast-grep LSP keys its diagnostics `rule = "ast-grep:<id>"` (source:code,
|
|
1150
|
-
// lsp-diagnostics.ts), while the napi runner — per-edit AND the project scan
|
|
1151
|
-
// (#308) — emits the bare `<id>`. Both run the same shipped ruleset, so the same
|
|
1152
|
-
// violation must collapse to one finding in mode=full's merge regardless of which
|
|
1153
|
-
// engine produced it. Strip the `ast-grep:` source prefix and the `-js` language
|
|
1154
|
-
// suffix (the napi runner already treats `<id>` / `<id>-js` as one rule) so the
|
|
1155
|
-
// LSP sweep and the napi scan don't double-report the same line. The
|
|
1156
|
-
// normalization itself now lives in `clients/dispatch/rule-id-normalize.ts` so the
|
|
1157
|
-
// inline suppression parser, the project rule-policy matcher, and dedup all apply
|
|
1158
|
-
// the same canonical form.
|
|
1159
|
-
function diagnosticDedupKey(filePath, diagnostic) {
|
|
1160
|
-
const ruleId = normalizeRuleId(diagnostic.rule ?? diagnostic.tool ?? "");
|
|
1161
|
-
return [path.resolve(filePath), diagnostic.line ?? "?", ruleId].join(":");
|
|
1162
|
-
}
|
|
1163
|
-
/**
|
|
1164
|
-
* Which project runner produced a retained row (#2154). ONE derivation, used by
|
|
1165
|
-
* both the retirement filter and the record that counts what it retired — v2's
|
|
1166
|
-
* N2 was two verbatim copies of this expression drifting apart.
|
|
1167
|
-
*/
|
|
1168
|
-
function runnerIdOf(diagnostic) {
|
|
1169
|
-
return diagnostic.tool ?? diagnostic.rule?.split(":", 1)[0] ?? "";
|
|
1170
|
-
}
|
|
1171
|
-
/**
|
|
1172
|
-
* The one project-runner retirement decision. Coverage is authoritative when
|
|
1173
|
-
* present for the runner; the older id-only arm is used only when the runner
|
|
1174
|
-
* declared no coverage at all.
|
|
1175
|
-
*
|
|
1176
|
-
* #2962: an entry with an EMPTY file set is a declaration, not a missing one —
|
|
1177
|
-
* "this producer analysed the root and proved zero files". Selecting it out by
|
|
1178
|
-
* its own size (the pre-#2962 `entry.files.size > 0` filter) dropped it into the
|
|
1179
|
-
* whole-root id-only arm, so one complete opengrep report with
|
|
1180
|
-
* `paths.scanned: []` retired every retained opengrep finding in the tree even
|
|
1181
|
-
* though no file had been scanned. The id-only arm is unchanged for the eight
|
|
1182
|
-
* runners that declare no coverage.
|
|
1183
|
-
*/
|
|
1184
|
-
export function runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeCoverage) {
|
|
1185
|
-
const runnerId = runnerIdOf(diagnostic);
|
|
1186
|
-
const coverage = (authoritativeCoverage ?? []).filter((entry) => entry.runnerId === runnerId);
|
|
1187
|
-
if (coverage.length === 0) {
|
|
1188
|
-
return authoritativeRunnerIds?.has(runnerId) ? "retire" : "keep";
|
|
1189
|
-
}
|
|
1190
|
-
const realFilePath = realpathOrResolve(filePath);
|
|
1191
|
-
for (const entry of coverage) {
|
|
1192
|
-
const root = entry.root;
|
|
1193
|
-
const relative = path.relative(root, realFilePath);
|
|
1194
|
-
const underRoot = relative === "" ||
|
|
1195
|
-
(!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
1196
|
-
if (!underRoot)
|
|
1197
|
-
continue;
|
|
1198
|
-
if (entry.files.has(realFilePath)) {
|
|
1199
|
-
return "retire";
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
return "keep";
|
|
1203
|
-
}
|
|
1204
|
-
function summarizeDiagnostics(filePath, diagnostics, hasFinalSnapshot) {
|
|
1205
|
-
let blocking = 0;
|
|
1206
|
-
let errors = 0;
|
|
1207
|
-
let warnings = 0;
|
|
1208
|
-
let advisories = 0;
|
|
1209
|
-
for (const diagnostic of diagnostics) {
|
|
1210
|
-
// #1641: a demoted past-EOF entry keeps its severity for display but
|
|
1211
|
-
// drops out of the tallies — same reasoning as widget-state's `isBlocking`.
|
|
1212
|
-
if (diagnostic.stale)
|
|
1213
|
-
continue;
|
|
1214
|
-
if (diagnostic.semantic === "blocking")
|
|
1215
|
-
blocking++;
|
|
1216
|
-
// #2414: severity → tier classification is shared with
|
|
1217
|
-
// `countDiagnostics`/`countAdvisories` in `clients/widget-state.ts` via
|
|
1218
|
-
// `classifyDiagnosticTier` — mode=all reads that store's tally and
|
|
1219
|
-
// mode=full reads this one, so the two must agree. `hint`/`info` no
|
|
1220
|
-
// longer inflate `warnings` (they used to, matching a pre-#2414
|
|
1221
|
-
// `countDiagnostics`, so a hint-only file scored 0/0/0 there too and the
|
|
1222
|
-
// `withIssues` filter below dropped it — that gap is now closed via
|
|
1223
|
-
// `advisories`, not by re-folding hints into `warnings`).
|
|
1224
|
-
const tier = classifyDiagnosticTier(diagnostic);
|
|
1225
|
-
if (tier === "error")
|
|
1226
|
-
errors++;
|
|
1227
|
-
else if (tier === "warning")
|
|
1228
|
-
warnings++;
|
|
1229
|
-
else if (tier === "advisory")
|
|
1230
|
-
advisories++;
|
|
1231
|
-
}
|
|
1232
|
-
return {
|
|
1233
|
-
filePath,
|
|
1234
|
-
blocking,
|
|
1235
|
-
errors,
|
|
1236
|
-
warnings,
|
|
1237
|
-
advisories,
|
|
1238
|
-
hasFinalSnapshot,
|
|
1239
|
-
diagnostics,
|
|
1240
|
-
};
|
|
1241
|
-
}
|
|
1242
|
-
// #1618: sentence form used when exactly one reason accounts for every
|
|
1243
|
-
// unconfirmed file — mirrors the pre-#1618 two-case ternary's phrasing so
|
|
1244
|
-
// existing single-reason callers see unchanged text.
|
|
1245
|
-
const UNCONFIRMED_REASON_SENTENCE = {
|
|
1246
|
-
budget: "check didn't complete within budget",
|
|
1247
|
-
inconclusive: "check was inconclusive (notify-write or diagnostics wait timed out)",
|
|
1248
|
-
coverage_gap: "an auxiliary scanner coverage gap",
|
|
1249
|
-
service_destroyed: "the LSP service was reset mid-sweep",
|
|
1250
|
-
error: "check errored",
|
|
1251
|
-
// #1618 review round 2: the LSP layer considered this touch CONFIRMED —
|
|
1252
|
-
// the file's content changed under it (stale binding), which is a
|
|
1253
|
-
// completely different failure from a timeout. Must never collapse into
|
|
1254
|
-
// "within budget", the exact string this PR exists to stop misrendering.
|
|
1255
|
-
binding_mismatch: "the file changed on disk since this result was computed",
|
|
1256
|
-
// #2052: no server was asked. Say WHY and say it is permanent for this
|
|
1257
|
-
// path, so the reader does not read an empty result as "clean" or retry it.
|
|
1258
|
-
outside_project_root: "the file is outside every initialized session project root, so no language server was asked",
|
|
1259
|
-
};
|
|
1260
|
-
// Short per-reason label used only when MORE than one reason is present, so
|
|
1261
|
-
// the note can still say "N timed out, M errored" instead of collapsing to
|
|
1262
|
-
// one sentence that can't carry two counts.
|
|
1263
|
-
const UNCONFIRMED_REASON_COUNT_LABEL = {
|
|
1264
|
-
budget: "timed out",
|
|
1265
|
-
inconclusive: "inconclusive",
|
|
1266
|
-
coverage_gap: "coverage gap",
|
|
1267
|
-
service_destroyed: "service reset mid-sweep",
|
|
1268
|
-
error: "errored",
|
|
1269
|
-
binding_mismatch: "stale binding",
|
|
1270
|
-
outside_project_root: "outside project root",
|
|
1271
|
-
};
|
|
1272
|
-
/**
|
|
1273
|
-
* #1618: a result predating the `unconfirmedReason` field (a legacy/test
|
|
1274
|
-
* double, or `LSPWorkspaceDiagnosticResult`'s own pre-#1618 shape) is
|
|
1275
|
-
* classified from its OTHER fields exactly the way the dead
|
|
1276
|
-
* `unconfirmedErrored = length - unconfirmedTimedOut` subtraction meant to,
|
|
1277
|
-
* before that subtraction was structurally always 0 (the sweep's catch
|
|
1278
|
-
* block sets both `error` and `timedOut`, so `unconfirmedErrored` never had
|
|
1279
|
-
* a way to become nonzero). An explicit `result.error` now wins even when
|
|
1280
|
-
* `unconfirmedReason` is absent, so an errored file is never rendered as
|
|
1281
|
-
* "within budget".
|
|
1282
|
-
*
|
|
1283
|
-
* #1618 review round 2: `mismatched` MUST be checked first. A binding
|
|
1284
|
-
* mismatch is discovered HERE, after the sweep already returned the result
|
|
1285
|
-
* as confirmed (`!timedOut`, no `.error`, no `.unconfirmedReason`) — the
|
|
1286
|
-
* `?? "budget"` fallback below would otherwise silently claim a
|
|
1287
|
-
* stale-binding file as "within budget", the exact string this PR exists to
|
|
1288
|
-
* stop misrendering.
|
|
1289
|
-
*/
|
|
1290
|
-
function classifyUnconfirmedReason(result, mismatched) {
|
|
1291
|
-
if (mismatched.has(result))
|
|
1292
|
-
return "binding_mismatch";
|
|
1293
|
-
return result.unconfirmedReason ?? (result.error ? "error" : "budget");
|
|
1294
|
-
}
|
|
1295
|
-
/**
|
|
1296
|
-
* #1618: replaces the `unconfirmedTimedOut`/`unconfirmedErrored` dead
|
|
1297
|
-
* subtraction with a real per-reason tally read off each result's own
|
|
1298
|
-
* `unconfirmedReason` (falling back to `classifyUnconfirmedReason` for a
|
|
1299
|
-
* legacy result that predates the field). `mismatched` is threaded through
|
|
1300
|
-
* so a stale-binding file is never counted under a fallback reason it
|
|
1301
|
-
* doesn't have (review round 2).
|
|
1302
|
-
*/
|
|
1303
|
-
function tallyUnconfirmedReasons(results, mismatched) {
|
|
1304
|
-
const tally = new Map();
|
|
1305
|
-
for (const result of results) {
|
|
1306
|
-
const reason = classifyUnconfirmedReason(result, mismatched);
|
|
1307
|
-
tally.set(reason, (tally.get(reason) ?? 0) + 1);
|
|
1308
|
-
}
|
|
1309
|
-
return tally;
|
|
1310
|
-
}
|
|
1311
|
-
/** Render `tallyUnconfirmedReasons`' output as the clause inside the
|
|
1312
|
-
* "unconfirmed (...)" note — a single sentence when one reason accounts for
|
|
1313
|
-
* everything, else a comma-joined "N label" breakdown. */
|
|
1314
|
-
function formatUnconfirmedReasonClause(tally) {
|
|
1315
|
-
const entries = [...tally.entries()].filter(([, count]) => count > 0);
|
|
1316
|
-
if (entries.length === 0)
|
|
1317
|
-
return "";
|
|
1318
|
-
if (entries.length === 1) {
|
|
1319
|
-
return UNCONFIRMED_REASON_SENTENCE[entries[0][0]];
|
|
1320
|
-
}
|
|
1321
|
-
return entries
|
|
1322
|
-
.map(([reason, count]) => `${count} ${UNCONFIRMED_REASON_COUNT_LABEL[reason]}`)
|
|
1323
|
-
.join(", ");
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* #646: break the confirmed/unconfirmed LSP-sweep tally down PER primary
|
|
1327
|
-
* server id (typescript, pyright, marksman, ...) instead of one flat
|
|
1328
|
-
* aggregate count. Mirrors `tools/lsp-diagnostics.ts`'s `primaryServerId`/
|
|
1329
|
-
* `tallyConfirmation` split — both tools now use the SAME
|
|
1330
|
-
* `primaryServerId` (clients/lsp/config.ts) so a file's classification as
|
|
1331
|
-
* "primary language server" vs "auxiliary scanner" agrees between them.
|
|
1332
|
-
*
|
|
1333
|
-
* Motivating case (#646): a real sweep came back 34/155 unconfirmed, and
|
|
1334
|
-
* diagnosing WHICH server was responsible required grepping
|
|
1335
|
-
* `~/.pi-lens/latency.log` by hand — it turned out to be 100% one push-only
|
|
1336
|
-
* server (marksman). This tally makes that visible directly: `{ marksman:
|
|
1337
|
-
* { confirmed: 0, total: 34 }, typescript: { confirmed: 121, total: 121 } }`.
|
|
1338
|
-
*
|
|
1339
|
-
* Purely an additional reporting dimension — does NOT change what counts as
|
|
1340
|
-
* confirmed/unconfirmed (the existing `confirmedLspResults`/
|
|
1341
|
-
* `unconfirmedLspResults` partition in `formatFullMode` is unchanged and
|
|
1342
|
-
* still the source of truth this reads from).
|
|
1343
|
-
*/
|
|
1344
|
-
function tallyLspByPrimaryServer(confirmed, unconfirmed) {
|
|
1345
|
-
const byServer = new Map();
|
|
1346
|
-
const bump = (filePath, wasConfirmed) => {
|
|
1347
|
-
const id = primaryServerId(filePath) ?? "unknown";
|
|
1348
|
-
const entry = byServer.get(id) ?? { confirmed: 0, total: 0 };
|
|
1349
|
-
entry.total += 1;
|
|
1350
|
-
if (wasConfirmed)
|
|
1351
|
-
entry.confirmed += 1;
|
|
1352
|
-
byServer.set(id, entry);
|
|
1353
|
-
};
|
|
1354
|
-
for (const result of confirmed)
|
|
1355
|
-
bump(result.filePath, true);
|
|
1356
|
-
for (const result of unconfirmed)
|
|
1357
|
-
bump(result.filePath, false);
|
|
1358
|
-
return byServer;
|
|
1359
|
-
}
|
|
1360
|
-
/**
|
|
1361
|
-
* Render the per-server tally from `tallyLspByPrimaryServer` as a short
|
|
1362
|
-
* clause, e.g. "typescript: 121/121, marksman: 0/34" — sorted so the
|
|
1363
|
-
* worst-confirmed server (the one most likely to be why a sweep looks
|
|
1364
|
-
* unconfirmed) is called out first.
|
|
1365
|
-
*/
|
|
1366
|
-
function formatLspServerBreakdown(byServer) {
|
|
1367
|
-
return [...byServer.entries()]
|
|
1368
|
-
.sort((a, b) => {
|
|
1369
|
-
const rateA = a[1].confirmed / a[1].total;
|
|
1370
|
-
const rateB = b[1].confirmed / b[1].total;
|
|
1371
|
-
return rateA - rateB;
|
|
1372
|
-
})
|
|
1373
|
-
.map(([id, { confirmed, total }]) => `${id}: ${confirmed}/${total}`)
|
|
1374
|
-
.join(", ");
|
|
1375
|
-
}
|
|
1376
|
-
/**
|
|
1377
|
-
* #646: split a raw LSP-sweep result set's diagnostics into "primary" (the
|
|
1378
|
-
* real language server, e.g. typescript/pyright) vs "auxiliary" (cross-
|
|
1379
|
-
* cutting scanners attached via clientScope "all" — ast-grep, opengrep,
|
|
1380
|
-
* zizmor, typos, marksman, ...), the same separation `lsp_diagnostics`
|
|
1381
|
-
* already applies to its own findings rendering (see
|
|
1382
|
-
* `tools/lsp-diagnostics.ts`'s `collectBatchDiagnostics`). Computed from the
|
|
1383
|
-
* raw per-file LSP results (not the merged widget-state summaries further
|
|
1384
|
-
* down `formatFullMode`) so it doesn't disturb the existing merge/dedup
|
|
1385
|
-
* logic those summaries feed into.
|
|
1386
|
-
*/
|
|
1387
|
-
function tallyLspPrimaryVsAuxiliary(results) {
|
|
1388
|
-
let primary = 0;
|
|
1389
|
-
let auxiliary = 0;
|
|
1390
|
-
for (const result of results) {
|
|
1391
|
-
const primaryId = primaryServerId(result.filePath);
|
|
1392
|
-
for (const diagnostic of result.diagnostics ?? []) {
|
|
1393
|
-
if (diagnostic.serverId === primaryId)
|
|
1394
|
-
primary += 1;
|
|
1395
|
-
else
|
|
1396
|
-
auxiliary += 1;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
return { primary, auxiliary };
|
|
1400
|
-
}
|
|
1401
|
-
function mergeDiagnosticsWithWidgetSummaries(widgetSummaries, lspResults, projectSnapshot, projectDelta,
|
|
1402
|
-
/**
|
|
1403
|
-
* #1993: resolved paths covered by a CONFIRMED, fully-covered LSP result
|
|
1404
|
-
* in `lspResults`. The fresh sweep is AUTHORITATIVE for these files - any
|
|
1405
|
-
* widget-store diagnostics seeded for them predate the sweep and must not
|
|
1406
|
-
* survive it (an additive-only merge let mid-edit stale 🔴 entries render
|
|
1407
|
-
* forever beside a clean sweep). Files absent from this set keep today's
|
|
1408
|
-
* additive behavior (fail-open: unconfirmed/timed-out sweeps never retire
|
|
1409
|
-
* widget state they did not actually re-check).
|
|
1410
|
-
*/
|
|
1411
|
-
authoritativeLspFiles, authoritativeRunnerIds, authoritativeRunnerCoverage) {
|
|
1412
|
-
const byFile = new Map();
|
|
1413
|
-
const seen = new Set();
|
|
1414
|
-
for (const summary of widgetSummaries) {
|
|
1415
|
-
const filePath = path.resolve(summary.filePath);
|
|
1416
|
-
// NOTE (#1993 review): dispositions recorded against a CACHED copy's
|
|
1417
|
-
// message text may not match this file's fresh sweep copy (weak
|
|
1418
|
-
// anchors key on message). Both copies derive from the same LSP
|
|
1419
|
-
// diagnostic via convertLspDiagnostics, so identity is stable in
|
|
1420
|
-
// practice; inline pi-lens-ignore comments are the primary
|
|
1421
|
-
// suppression mechanism.
|
|
1422
|
-
if (authoritativeLspFiles?.has(filePath))
|
|
1423
|
-
continue;
|
|
1424
|
-
const retained = summary.diagnostics ?? [];
|
|
1425
|
-
const diagnostics = retained
|
|
1426
|
-
.filter((diagnostic) => runnerRetirementDecision(diagnostic, filePath, authoritativeRunnerIds, authoritativeRunnerCoverage) !== "retire")
|
|
1427
|
-
.map((d) => ({ ...d }));
|
|
1428
|
-
byFile.set(filePath,
|
|
1429
|
-
// #2154 v4: when the filter retires rows, the stored tallies describe
|
|
1430
|
-
// diagnostics that are no longer delivered — full mode rendered
|
|
1431
|
-
// `main.go 1W … 1 warning` with no row under it. Recompute from what
|
|
1432
|
-
// survived, and only then (an untouched file keeps its own counts).
|
|
1433
|
-
diagnostics.length === retained.length
|
|
1434
|
-
? { ...summary, filePath, diagnostics }
|
|
1435
|
-
: summarizeDiagnostics(filePath, diagnostics, summary.hasFinalSnapshot ?? true));
|
|
1436
|
-
for (const diagnostic of diagnostics) {
|
|
1437
|
-
seen.add(diagnosticDedupKey(filePath, diagnostic));
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
const addDiagnostic = (filePath, widgetDiagnostic) => {
|
|
1441
|
-
const existing = byFile.get(filePath);
|
|
1442
|
-
const diagnostics = existing ? [...existing.diagnostics] : [];
|
|
1443
|
-
const key = diagnosticDedupKey(filePath, widgetDiagnostic);
|
|
1444
|
-
if (seen.has(key))
|
|
1445
|
-
return;
|
|
1446
|
-
seen.add(key);
|
|
1447
|
-
diagnostics.push(widgetDiagnostic);
|
|
1448
|
-
byFile.set(filePath, summarizeDiagnostics(filePath, diagnostics, existing?.hasFinalSnapshot ?? true));
|
|
1449
|
-
};
|
|
1450
|
-
for (const result of lspResults) {
|
|
1451
|
-
const filePath = path.resolve(result.filePath);
|
|
1452
|
-
for (const diagnostic of result.diagnostics ?? []) {
|
|
1453
|
-
addDiagnostic(filePath, lspDiagnosticToWidget(diagnostic));
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
for (const diagnostic of projectSnapshot?.diagnostics ?? []) {
|
|
1457
|
-
addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic, diagnostic.filePath));
|
|
1458
|
-
}
|
|
1459
|
-
for (const diagnostic of projectDelta?.diagnostics ?? []) {
|
|
1460
|
-
addDiagnostic(path.resolve(diagnostic.filePath), projectDiagnosticToWidget(diagnostic, diagnostic.filePath));
|
|
1461
|
-
}
|
|
1462
|
-
return [...byFile.values()];
|
|
1463
|
-
}
|
|
1464
|
-
/**
|
|
1465
|
-
* Apply inline `pi-lens-ignore` suppression to the merged mode=full summaries so
|
|
1466
|
-
* the project-wide sweep honors the same comments as the per-edit path (#442) —
|
|
1467
|
-
* without it, a site cleanly suppressed in mode=all reappears as blocking here.
|
|
1468
|
-
* Reads each flagged file once (bounded to files that actually have diagnostics);
|
|
1469
|
-
* a read failure is fail-safe (keep the diagnostics rather than hide a finding on
|
|
1470
|
-
* an I/O error). Re-summarizes so the blocking/error/warning counts reflect the
|
|
1471
|
-
* suppression.
|
|
1472
|
-
*
|
|
1473
|
-
* Also applies the project's `.pi-lens.json` `rules.<id>.disable`/`select`
|
|
1474
|
-
* policy AFTER inline suppression / disposition so the same set of findings
|
|
1475
|
-
* the per-edit `dispatcher.ts` filters is what's rendered here. The policy
|
|
1476
|
-
* map is passed in (computed once per `formatFullMode` call) rather than
|
|
1477
|
-
* re-deriving per file — see `loadProjectRulePolicyMap(cwd)`.
|
|
1478
|
-
*/
|
|
1479
|
-
async function applyInlineSuppressionsToSummaries(summaries, cwd, policyMap) {
|
|
1480
|
-
return Promise.all(summaries.map(async (summary) => {
|
|
1481
|
-
if (!summary.diagnostics.length)
|
|
1482
|
-
return summary;
|
|
1483
|
-
let content;
|
|
1484
|
-
try {
|
|
1485
|
-
content = await fs.readFile(summary.filePath, "utf8");
|
|
1486
|
-
}
|
|
1487
|
-
catch {
|
|
1488
|
-
// Inline suppression/disposition remains fail-open when the file cannot
|
|
1489
|
-
// be read, but project policy does not depend on content and must still
|
|
1490
|
-
// be applied so a fresh full-mode result cannot bypass `disable`/`select`.
|
|
1491
|
-
const policyKept = applyRulePolicy(summary.diagnostics, policyMap);
|
|
1492
|
-
return policyKept.length === summary.diagnostics.length
|
|
1493
|
-
? summary
|
|
1494
|
-
: summarizeDiagnostics(summary.filePath, policyKept, summary.hasFinalSnapshot);
|
|
1495
|
-
}
|
|
1496
|
-
// #690/#3088: inline `pi-lens-ignore` → the same false-positive/
|
|
1497
|
-
// suppress/defer disposition filter the per-edit dispatch path applies
|
|
1498
|
-
// (dispatcher.ts) → the project's `.pi-lens.json` rule policy. mode=full
|
|
1499
|
-
// merges in diagnostics from a fresh LSP sweep/project scan that never
|
|
1500
|
-
// went through the dispatch path, so without this a disposed finding
|
|
1501
|
-
// reappears here. #3088 folded the three calls onto the shared
|
|
1502
|
-
// `applyFindingPolicy` seam, which the `source=lsp` probe lane now calls
|
|
1503
|
-
// too — one stack, one order, for every model-facing surface.
|
|
1504
|
-
const { kept: policyKept } = applyFindingPolicy(summary.diagnostics, {
|
|
1505
|
-
cwd,
|
|
1506
|
-
filePath: summary.filePath,
|
|
1507
|
-
content,
|
|
1508
|
-
policyMap,
|
|
1509
|
-
});
|
|
1510
|
-
// Tag `flagged` diagnostics for the render loop (formatAllMode). Content
|
|
1511
|
-
// is already in hand here (unlike mode=all/delta's cache-only path), so
|
|
1512
|
-
// this is the one place the tag can be computed without adding I/O to
|
|
1513
|
-
// the "instant" modes.
|
|
1514
|
-
for (const d of policyKept) {
|
|
1515
|
-
// flagged is weak-anchored (module doc, diagnostic-dispositions.ts) so
|
|
1516
|
-
// the tag survives incidental edits to the flagged line itself.
|
|
1517
|
-
const { weak } = anchorsForDiagnostic(cwd, summary.filePath, d, content);
|
|
1518
|
-
if (getDisposition(cwd, weak)?.disposition === "flagged") {
|
|
1519
|
-
d.flagged = true;
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1522
|
-
if (policyKept.length === summary.diagnostics.length)
|
|
1523
|
-
return summary;
|
|
1524
|
-
return summarizeDiagnostics(summary.filePath, policyKept, summary.hasFinalSnapshot);
|
|
1525
|
-
}));
|
|
1526
|
-
}
|
|
1527
|
-
function shouldUseCachedProjectDiagnostics(value) {
|
|
1528
|
-
return value === "cached";
|
|
1529
|
-
}
|
|
1530
|
-
function shouldRefreshProjectDiagnostics(value) {
|
|
1531
|
-
return value === "cheap" || value === "all";
|
|
1532
|
-
}
|
|
1533
|
-
/** True when full mode should include project-runner state at all (any non-none refreshRunners). */
|
|
1534
|
-
function shouldIncludeProjectRunners(value) {
|
|
1535
|
-
return shouldRefreshProjectDiagnostics(value) || value === "cached";
|
|
1536
|
-
}
|
|
1537
|
-
/**
|
|
1538
|
-
* Merge cache-derived diagnostics from the analyzer extractors (jscpd, madge,
|
|
1539
|
-
* gitleaks, knip …) into the scanned project snapshot: append to the existing
|
|
1540
|
-
* one (recording the runners), or synthesize a minimal snapshot when there was
|
|
1541
|
-
* no in-process scan. Returns the snapshot unchanged when there is nothing extra.
|
|
1542
|
-
*/
|
|
1543
|
-
function foldExtraDiagnosticsIntoSnapshot(snapshot, extra, runners, cwd) {
|
|
1544
|
-
if (extra.length === 0)
|
|
1545
|
-
return snapshot;
|
|
1546
|
-
if (snapshot) {
|
|
1547
|
-
const merged = new Set([...snapshot.runners, ...runners]);
|
|
1548
|
-
return {
|
|
1549
|
-
...snapshot,
|
|
1550
|
-
diagnostics: [...snapshot.diagnostics, ...extra],
|
|
1551
|
-
runners: [...merged],
|
|
1552
|
-
};
|
|
1553
|
-
}
|
|
1554
|
-
return {
|
|
1555
|
-
version: PROJECT_DIAGNOSTICS_CACHE_VERSION,
|
|
1556
|
-
cwd,
|
|
1557
|
-
tier: "all",
|
|
1558
|
-
scannedAt: new Date().toISOString(),
|
|
1559
|
-
diagnostics: extra,
|
|
1560
|
-
filesScanned: 0,
|
|
1561
|
-
runners,
|
|
1562
|
-
};
|
|
1563
|
-
}
|
|
1564
|
-
async function getProjectDiagnosticsSnapshotForFullMode(cwd, options) {
|
|
1565
|
-
if (shouldRefreshProjectDiagnostics(options.refreshRunners)) {
|
|
1566
|
-
return scanProjectDiagnostics({
|
|
1567
|
-
cwd,
|
|
1568
|
-
tier: "cheap",
|
|
1569
|
-
maxFiles: options.maxProjectFiles,
|
|
1570
|
-
signal: options.signal,
|
|
1571
|
-
files: options.files,
|
|
1572
|
-
includeGenerated: options.includeGenerated,
|
|
1573
|
-
});
|
|
1574
|
-
}
|
|
1575
|
-
if (shouldUseCachedProjectDiagnostics(options.refreshRunners)) {
|
|
1576
|
-
// The cached snapshot is a cross-session cache; drop diagnostics for files
|
|
1577
|
-
// edited/deleted since the scan so a stale entry isn't replayed (#298). A
|
|
1578
|
-
// fresh scan (above) is current by construction and needs no reconcile.
|
|
1579
|
-
const cached = loadProjectDiagnosticsSnapshot(cwd);
|
|
1580
|
-
if (!cached)
|
|
1581
|
-
return undefined;
|
|
1582
|
-
const reconciled = reconcileProjectDiagnosticsSnapshot(cached);
|
|
1583
|
-
// #2154: what this gate DROPS was invisible — the count was computed and
|
|
1584
|
-
// thrown away, so a session that silently retired another session's rows
|
|
1585
|
-
// (the whole point of the content axis added this round) left no record
|
|
1586
|
-
// of having done so. Bounded by construction: at most one row per
|
|
1587
|
-
// mode=full call, and only when rows were actually retired — the shape
|
|
1588
|
-
// `lsp_authoritative_widget_retire` uses for the sibling arm.
|
|
1589
|
-
if (reconciled.staleDropped > 0) {
|
|
1590
|
-
logLatency({
|
|
1591
|
-
type: "phase",
|
|
1592
|
-
toolName: "lens_diagnostics",
|
|
1593
|
-
filePath: cwd,
|
|
1594
|
-
phase: "project_snapshot_rows_retired",
|
|
1595
|
-
durationMs: 0,
|
|
1596
|
-
metadata: {
|
|
1597
|
-
files: reconciled.staleDropped,
|
|
1598
|
-
rows: cached.diagnostics.length - reconciled.snapshot.diagnostics.length,
|
|
1599
|
-
scannedAt: cached.scannedAt,
|
|
1600
|
-
},
|
|
1601
|
-
});
|
|
1602
|
-
}
|
|
1603
|
-
return reconciled.snapshot;
|
|
1604
|
-
}
|
|
1605
|
-
return undefined;
|
|
1606
|
-
}
|
|
1607
|
-
// @delivery-surface: lens-diagnostics:mode-full
|
|
1608
|
-
async function formatFullMode(cwd, severity, lspService, cacheManager, options = {}) {
|
|
1609
|
-
const runWorkspaceDiagnostics = lspService.runWorkspaceDiagnostics;
|
|
1610
|
-
if (typeof runWorkspaceDiagnostics !== "function") {
|
|
1611
|
-
return {
|
|
1612
|
-
content: [
|
|
1613
|
-
{
|
|
1614
|
-
type: "text",
|
|
1615
|
-
text: "LSP service does not support project-wide workspace diagnostics.",
|
|
1616
|
-
},
|
|
1617
|
-
],
|
|
1618
|
-
details: { mode: "full", filesChecked: 0, lspUnavailable: true },
|
|
1619
|
-
};
|
|
1620
|
-
}
|
|
1621
|
-
const { signal, pathsScope, nextWriteIndex } = options;
|
|
1622
|
-
// #2535: advisories name the delivery host's callable tools; pi callers
|
|
1623
|
-
// omit host and keep the pi spelling.
|
|
1624
|
-
const host = options.host ?? "pi";
|
|
1625
|
-
const includeFile = createScopedFileFilter(cwd, pathsScope);
|
|
1626
|
-
// `paths` (#461): route the active scans at exactly the requested files
|
|
1627
|
-
// instead of walking the whole project. Three cases:
|
|
1628
|
-
// - files only → pass them as the explicit list (skips the walk). An EMPTY
|
|
1629
|
-
// list (every requested file was missing) still passes through — both
|
|
1630
|
-
// seams treat [] as "scan nothing", where undefined would trigger a full
|
|
1631
|
-
// project walk that includeFile then filters to nothing (expensive no-op).
|
|
1632
|
-
// - any directory entry → fall back to the walk, narrowed by includeFile.
|
|
1633
|
-
// Passing only the file entries would silently skip everything under the
|
|
1634
|
-
// requested directories — an under-scan the caller can't detect.
|
|
1635
|
-
// - no paths → undefined, today's full walk.
|
|
1636
|
-
const explicitFiles = pathsScope && !pathsScope.hasDirectoryEntries
|
|
1637
|
-
? pathsScope.existingEntries
|
|
1638
|
-
: undefined;
|
|
1639
|
-
// #613: the heavyweight-analyzer fresh-fetch (below) used to be `await`ed
|
|
1640
|
-
// AFTER this Promise.all had already fully resolved — sequentially eating
|
|
1641
|
-
// into the SAME wall-clock ceiling (`FULL_SCAN_WALL_CLOCK_MS`) the LSP sweep
|
|
1642
|
-
// already spent, rather than sharing it concurrently as the old comment here
|
|
1643
|
-
// claimed. On a real project the LSP sweep alone can take 100+s, leaving the
|
|
1644
|
-
// analyzers almost no budget before the shared abort signal fires — killing
|
|
1645
|
-
// ALL of them (even unconditional ones like knip/jscpd/madge) before any
|
|
1646
|
-
// could complete. Building its promise here, alongside the sweep and cheap
|
|
1647
|
-
// scan, makes it genuinely concurrent — all three phases now race the SAME
|
|
1648
|
-
// signal from the same starting point instead of stacking.
|
|
1649
|
-
const projectRunnersRequested = shouldIncludeProjectRunners(options.refreshRunners);
|
|
1650
|
-
const analyzersPromise = projectRunnersRequested
|
|
1651
|
-
? loadBootstrapClients().then((clients) => fetchFreshProjectDiagnostics(cacheManager, cwd, clients, signal, {
|
|
1652
|
-
runtime: options.runtime,
|
|
1653
|
-
analysisRoot: options.analysisRoot,
|
|
1654
|
-
}))
|
|
1655
|
-
: Promise.resolve({
|
|
1656
|
-
diagnostics: [],
|
|
1657
|
-
runners: [],
|
|
1658
|
-
analyzed: [],
|
|
1659
|
-
authoritativeCoverage: [],
|
|
1660
|
-
// #1623: every heavyweight analyzer is ELIGIBLE for this project but
|
|
1661
|
-
// this call never asked for it (refreshRunners wasn't cheap/all/
|
|
1662
|
-
// cached) — the expensive fetch below deliberately never runs in
|
|
1663
|
-
// that case (see the comment above), but rendering total silence
|
|
1664
|
-
// here reads exactly as "ran clean", the false-empty #1623 exists to
|
|
1665
|
-
// close. `ANALYZER_IDS` (fresh-fetch.ts) is the single source of
|
|
1666
|
-
// truth for which lanes this covers — reuse it rather than
|
|
1667
|
-
// hand-listing ids here.
|
|
1668
|
-
cold: [...ANALYZER_IDS],
|
|
1669
|
-
coldReasons: Object.fromEntries(ANALYZER_IDS.map((id) => [id, notRequestedReason(host)])),
|
|
1670
|
-
failed: [],
|
|
1671
|
-
timings: {},
|
|
1672
|
-
});
|
|
1673
|
-
const [rawLspResults, rawProjectSnapshot, extracted] = await Promise.all([
|
|
1674
|
-
runWorkspaceDiagnostics.call(lspService, cwd, {
|
|
1675
|
-
maxFiles: options.maxLspFiles,
|
|
1676
|
-
signal,
|
|
1677
|
-
onProgress: options.onProgress,
|
|
1678
|
-
onServerReady: options.onServerReady,
|
|
1679
|
-
nextWriteIndex: options.nextWriteIndex,
|
|
1680
|
-
files: explicitFiles,
|
|
1681
|
-
}),
|
|
1682
|
-
getProjectDiagnosticsSnapshotForFullMode(cwd, {
|
|
1683
|
-
...options,
|
|
1684
|
-
files: explicitFiles,
|
|
1685
|
-
}),
|
|
1686
|
-
analyzersPromise,
|
|
1687
|
-
]);
|
|
1688
|
-
if (extracted.analysisRootError) {
|
|
1689
|
-
return {
|
|
1690
|
-
content: [{ type: "text", text: extracted.analysisRootError }],
|
|
1691
|
-
isError: true,
|
|
1692
|
-
details: {
|
|
1693
|
-
mode: "full",
|
|
1694
|
-
analysisRootError: extracted.analysisRootError,
|
|
1695
|
-
analysisRootValidation: extracted.analysisRootValidation,
|
|
1696
|
-
},
|
|
1697
|
-
};
|
|
1698
|
-
}
|
|
1699
|
-
const aborted = signal?.aborted ?? false;
|
|
1700
|
-
// #1640: before ANY consumer sees them — the footer reconcile, the widget
|
|
1701
|
-
// merge, the rendered counts — demote TypeScript errors on files tsserver
|
|
1702
|
-
// checked in its INFERRED project. Applied once, here, so a single seam
|
|
1703
|
-
// governs the whole sweep instead of each renderer learning the rule.
|
|
1704
|
-
const lspResults = await demoteInferredProjectSweepResults(rawLspResults.filter((result) => includeFile(result.filePath)), cwd, lspService,
|
|
1705
|
-
// #1645 review F1: the sweep that produced these results is signal-bounded,
|
|
1706
|
-
// so the probe loop that post-processes them must be too.
|
|
1707
|
-
signal);
|
|
1708
|
-
// A result bound to a different document must not replace the current
|
|
1709
|
-
// widget state, even when it contains real diagnostics. Pull results may carry
|
|
1710
|
-
// a content hash without the push binding verdict, so verify that hash against
|
|
1711
|
-
// disk through the bounded async fallback above; unreadable files remain
|
|
1712
|
-
// unknown and retain the existing fail-open policy.
|
|
1713
|
-
const mismatchedLspResults = await findFullScanBindingMismatches(lspResults);
|
|
1714
|
-
// #630: `timedOut`/`error` means the per-file check never actually
|
|
1715
|
-
// completed or was inconclusive (#570's `touchFile().inconclusive`, or
|
|
1716
|
-
// this sweep's own outer per-file deadline/throw — see
|
|
1717
|
-
// `LSPWorkspaceDiagnosticResult`'s doc comment) — `diagnostics` is a
|
|
1718
|
-
// default-EMPTY placeholder in that case, not a confirmed clean. Partition
|
|
1719
|
-
// once here so BOTH the footer write (below, #571) and the merge into
|
|
1720
|
-
// `summaries` (further down) share the same confirmed/unconfirmed split.
|
|
1721
|
-
// Previously only the footer write excluded these; the merge still fed
|
|
1722
|
-
// the unfiltered `lspResults` in, so a timed-out file's placeholder `[]`
|
|
1723
|
-
// read as "0 diagnostics" — false-clean — in the rendered/`details`
|
|
1724
|
-
// output (#630).
|
|
1725
|
-
const confirmedLspResults = lspResults.filter((result) => !result.timedOut && !result.error && !mismatchedLspResults.has(result));
|
|
1726
|
-
const fullyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) === 0);
|
|
1727
|
-
const partiallyCoveredLspResults = confirmedLspResults.filter((result) => (result.unconfirmedServerIds?.length ?? 0) > 0);
|
|
1728
|
-
const unconfirmedLspResults = lspResults.filter((result) => result.timedOut || result.error || mismatchedLspResults.has(result));
|
|
1729
|
-
const authoritativeLspFiles = new Set();
|
|
1730
|
-
const unreconciledFiles = new Set();
|
|
1731
|
-
// #571: reconcile this scan's fresh, CONFIRMED per-file results into the
|
|
1732
|
-
// footer cache. A footer write is never allowed to fail the tool call, so
|
|
1733
|
-
// any unexpected throw is swallowed.
|
|
1734
|
-
for (const result of fullyCoveredLspResults) {
|
|
1735
|
-
try {
|
|
1736
|
-
// #692: provenance label ONLY — must never affect `rule`/identity (see
|
|
1737
|
-
// `ConvertLspDiagnosticsOptions.scanOrigin`'s doc comment).
|
|
1738
|
-
const diagnostics = convertLspDiagnostics(result.diagnostics, result.filePath, { scanOrigin: "lens_diagnostics_full" });
|
|
1739
|
-
// #692: re-tag aux-sourced findings (ast-grep, opengrep, zizmor, typos)
|
|
1740
|
-
// with their real tool id + semantic policy — the same treatment the
|
|
1741
|
-
// per-edit dispatch runner gives them — so a scan-reconciled entry no
|
|
1742
|
-
// longer keeps tool "lsp". No file content is read here (this loop
|
|
1743
|
-
// only has `result.diagnostics`/`result.filePath` from the sweep, not
|
|
1744
|
-
// the file body) — `detectFileRole` still classifies test files
|
|
1745
|
-
// correctly from the path alone (its content-based checks only refine
|
|
1746
|
-
// generated/stub detection), and native inline-suppression comments
|
|
1747
|
-
// were already applied upstream by `runWorkspaceDiagnostics`
|
|
1748
|
-
// (`applyAuxiliarySuppressions`), so an empty content string here is a
|
|
1749
|
-
// safe no-op re-check rather than a behavior gap.
|
|
1750
|
-
const retagged = retagAuxiliaryDiagnostics(diagnostics, result.diagnostics, "", { cwd, fileRole: detectFileRole(result.filePath) });
|
|
1751
|
-
const retired = reconcileScanDiagnostics(result.filePath, retagged, true,
|
|
1752
|
-
// The service reserves this token when the file enters the scan. The
|
|
1753
|
-
// fallback preserves compatibility with older test doubles/services.
|
|
1754
|
-
result.writeIndex ?? nextWriteIndex?.(),
|
|
1755
|
-
// #1093: `observedAt` is set only when this result was served from
|
|
1756
|
-
// the workspace-diagnostics cache (a replay of an older scan) —
|
|
1757
|
-
// stamp `touchedAt` with when it was scanned, not now(), so a
|
|
1758
|
-
// mode=full that only re-serves the cache can't keep a resolved
|
|
1759
|
-
// finding on screen by re-arming the mtime gate. Undefined for
|
|
1760
|
-
// freshly-touched results (observed now).
|
|
1761
|
-
result.observedAt);
|
|
1762
|
-
// A result rejected by the shared ordering guard is not authoritative
|
|
1763
|
-
// for delivery. Otherwise full mode hides the old widget row while
|
|
1764
|
-
// mode=all still serves it, creating a false clean/full disagreement.
|
|
1765
|
-
// Strict `=== true`: `undefined` (a double that predates the boolean
|
|
1766
|
-
// return) takes the unreconciled arm, never the retiring one.
|
|
1767
|
-
if (retired === true) {
|
|
1768
|
-
authoritativeLspFiles.add(path.resolve(result.filePath));
|
|
1769
|
-
}
|
|
1770
|
-
else {
|
|
1771
|
-
unreconciledFiles.add(path.resolve(result.filePath));
|
|
1772
|
-
recordDegradationOnce({
|
|
1773
|
-
kind: "diagnostic-retained-unreconciled",
|
|
1774
|
-
subject: `${path.resolve(result.filePath)}:lsp`,
|
|
1775
|
-
reason: "confirmed diagnostic result could not replace retained widget state",
|
|
1776
|
-
});
|
|
1777
|
-
}
|
|
1778
|
-
}
|
|
1779
|
-
catch {
|
|
1780
|
-
// Never let a footer-reconciliation hiccup fail the scan itself.
|
|
1781
|
-
unreconciledFiles.add(path.resolve(result.filePath));
|
|
1782
|
-
recordDegradationOnce({
|
|
1783
|
-
kind: "diagnostic-retained-unreconciled",
|
|
1784
|
-
subject: `${path.resolve(result.filePath)}:lsp`,
|
|
1785
|
-
reason: "confirmed diagnostic result failed during widget reconciliation",
|
|
1786
|
-
});
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
// Project rule policy (`.pi-lens.json` `rules.<id>.disable`/`select`) —
|
|
1790
|
-
// loaded once per mode=full call, BEFORE `projectSnapshot`/`projectDelta`
|
|
1791
|
-
// are built below, so `.diagnostics.length` on both (which feeds
|
|
1792
|
-
// `details.projectDiagnostics`/`details.projectDiagnosticsDelta` as well
|
|
1793
|
-
// as the merge into `summaries`) is the POST-policy count. Threaded into
|
|
1794
|
-
// `applyInlineSuppressionsToSummaries` too (idempotent re-apply there) so
|
|
1795
|
-
// the merged summaries honor the same policy the per-edit dispatch path
|
|
1796
|
-
// applies. A project config read failure is treated as "no policy"
|
|
1797
|
-
// (defensive, matches the `applyInlineSuppressionsToSummaries` read-error
|
|
1798
|
-
// discipline).
|
|
1799
|
-
const policyMap = loadProjectRulePolicyMap(cwd);
|
|
1800
|
-
const scannedSnapshot = filterProjectDiagnosticsSnapshot(rawProjectSnapshot, includeFile);
|
|
1801
|
-
// Heavyweight-analyzer findings (jscpd, madge, gitleaks, knip, govulncheck,
|
|
1802
|
-
// trivy, dead-code) — a FRESH run of each (#585), not a cache-only read:
|
|
1803
|
-
// mode=full is the "authoritative full picture" call, so a session_start-only
|
|
1804
|
-
// snapshot that can be hours stale in a long session is no longer good
|
|
1805
|
-
// enough. Safe to run concurrently with the LSP sweep: every one of these
|
|
1806
|
-
// analyzers has its own in-flight de-dupe guard (KnipClient/JscpdClient/
|
|
1807
|
-
// DeadCodeClient's `inFlight` map, SecurityScanClient.dedupeScan for
|
|
1808
|
-
// gitleaks/govulncheck/trivy — see fresh-fetch.ts's header), so a fresh
|
|
1809
|
-
// fetch here racing a concurrent session_start/turn_end pass over the same
|
|
1810
|
-
// tool JOINS that run instead of double-spawning it. `extracted` was already
|
|
1811
|
-
// computed above, in the SAME `Promise.all` as the LSP sweep (#613) — only
|
|
1812
|
-
// when the caller opted into project-runner state (otherwise it's the
|
|
1813
|
-
// `Promise.resolve({...})` stub from `analyzersPromise` above).
|
|
1814
|
-
// #2154: only ids whose client said it parsed a scan of this root this call
|
|
1815
|
-
// (`FreshProjectDiagnosticsResult.analyzed`). Every exclusion — a skipped
|
|
1816
|
-
// runner, a crash that still reported success, the cache-read test-runner
|
|
1817
|
-
// lane — is decided at the record site in fresh-fetch.ts, not re-derived
|
|
1818
|
-
// here; a second list of "which ids don't count" would be the mirror this
|
|
1819
|
-
// repo's single-source-of-truth rule forbids.
|
|
1820
|
-
const authoritativeRunnerIds = new Set(extracted.analyzed ?? []);
|
|
1821
|
-
const authoritativeRunnerCoverage = extracted.authoritativeCoverage ?? [];
|
|
1822
|
-
const foldedProjectSnapshot = foldExtraDiagnosticsIntoSnapshot(scannedSnapshot, extracted.diagnostics.filter((d) => includeFile(d.filePath)), extracted.runners, cwd);
|
|
1823
|
-
const projectSnapshot = applyProjectRulePolicy(foldedProjectSnapshot, policyMap);
|
|
1824
|
-
const projectDelta = applyProjectRulePolicy(filterProjectDiagnosticsDeltaReport(loadProjectDiagnosticsDeltaReport(cwd), includeFile), policyMap);
|
|
1825
|
-
// #630: only the CONFIRMED LSP results contribute diagnostics to the merge
|
|
1826
|
-
// — an unconfirmed (timed-out/errored) file's placeholder `[]` must not be
|
|
1827
|
-
// read as "0 issues, clean" via its LSP contribution. It can still
|
|
1828
|
-
// legitimately show diagnostics from widgetSummaries/project-runner state
|
|
1829
|
-
// below if those independently have entries for it.
|
|
1830
|
-
// #1993: files with a CONFIRMED, fully-covered LSP result are authoritative
|
|
1831
|
-
// - the fresh sweep replaces their widget-store state instead of merging
|
|
1832
|
-
// additively beside it.
|
|
1833
|
-
// #1993 review: retirement must be observable - if a future regression
|
|
1834
|
-
// makes the set falsely authoritative, findings would vanish silently.
|
|
1835
|
-
const authoritativeRetiredCount = getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath) &&
|
|
1836
|
-
authoritativeLspFiles.has(path.resolve(summary.filePath)) &&
|
|
1837
|
-
(summary.diagnostics?.length ?? 0) > 0).length;
|
|
1838
|
-
if (authoritativeRetiredCount > 0) {
|
|
1839
|
-
logLatency({
|
|
1840
|
-
type: "phase",
|
|
1841
|
-
phase: "lsp_authoritative_widget_retire",
|
|
1842
|
-
filePath: "",
|
|
1843
|
-
durationMs: 0,
|
|
1844
|
-
metadata: { files: authoritativeRetiredCount },
|
|
1845
|
-
});
|
|
1846
|
-
}
|
|
1847
|
-
// #2154 v4 F2: the same rule for the runner arm, in the same shape as its
|
|
1848
|
-
// LSP sibling above — a retirement nobody can see is how a regression
|
|
1849
|
-
// deletes findings silently. Bounded by construction: at most one row per
|
|
1850
|
-
// mode=full call, and only when rows were actually retired.
|
|
1851
|
-
const retiredRunnerCounts = new Map();
|
|
1852
|
-
const runnerRetiredRows = getFileDiagnosticSummaries()
|
|
1853
|
-
.filter((summary) => includeFile(summary.filePath))
|
|
1854
|
-
.reduce((total, summary) => total +
|
|
1855
|
-
(summary.diagnostics ?? []).filter((diagnostic) => {
|
|
1856
|
-
const retired = runnerRetirementDecision(diagnostic, summary.filePath, authoritativeRunnerIds, authoritativeRunnerCoverage) === "retire";
|
|
1857
|
-
if (retired) {
|
|
1858
|
-
const runnerId = runnerIdOf(diagnostic);
|
|
1859
|
-
retiredRunnerCounts.set(runnerId, (retiredRunnerCounts.get(runnerId) ?? 0) + 1);
|
|
1860
|
-
}
|
|
1861
|
-
return retired;
|
|
1862
|
-
}).length, 0);
|
|
1863
|
-
if (runnerRetiredRows > 0) {
|
|
1864
|
-
logLatency({
|
|
1865
|
-
type: "phase",
|
|
1866
|
-
phase: "runner_authoritative_widget_retire",
|
|
1867
|
-
filePath: "",
|
|
1868
|
-
durationMs: 0,
|
|
1869
|
-
metadata: {
|
|
1870
|
-
rows: runnerRetiredRows,
|
|
1871
|
-
runners: [...retiredRunnerCounts.keys()].join(","),
|
|
1872
|
-
coverage: authoritativeRunnerCoverage.map((entry) => ({
|
|
1873
|
-
runnerId: entry.runnerId,
|
|
1874
|
-
root: entry.root,
|
|
1875
|
-
files: entry.files.size,
|
|
1876
|
-
})),
|
|
1877
|
-
},
|
|
1878
|
-
});
|
|
1879
|
-
}
|
|
1880
|
-
for (const entry of authoritativeRunnerCoverage) {
|
|
1881
|
-
// #2962: a zero-file declaration retires nothing, so it must neither
|
|
1882
|
-
// write this row (a `runner_coverage_retired` row with count 0 would
|
|
1883
|
-
// report the opposite of what happened) nor BURN the once-per-session
|
|
1884
|
-
// claim — the session's first real coverage row would then be suppressed
|
|
1885
|
-
// by the call that proved no coverage. Its own observation is the
|
|
1886
|
-
// `runner-coverage-empty` ledger row raised at the fresh-fetch seam.
|
|
1887
|
-
if (entry.files.size === 0)
|
|
1888
|
-
continue;
|
|
1889
|
-
if (!claimPhaseOncePerSession("runner_coverage_retired", entry.runnerId)) {
|
|
1890
|
-
continue;
|
|
1891
|
-
}
|
|
1892
|
-
logLatency({
|
|
1893
|
-
type: "phase",
|
|
1894
|
-
phase: "runner_coverage_retired",
|
|
1895
|
-
filePath: "",
|
|
1896
|
-
durationMs: 0,
|
|
1897
|
-
metadata: {
|
|
1898
|
-
runnerId: entry.runnerId,
|
|
1899
|
-
count: retiredRunnerCounts.get(entry.runnerId) ?? 0,
|
|
1900
|
-
},
|
|
1901
|
-
});
|
|
1902
|
-
}
|
|
1903
|
-
let summaries = await applyInlineSuppressionsToSummaries(mergeDiagnosticsWithWidgetSummaries(getFileDiagnosticSummaries().filter((summary) => includeFile(summary.filePath)), confirmedLspResults, projectSnapshot, projectDelta, authoritativeLspFiles, authoritativeRunnerIds, authoritativeRunnerCoverage), cwd, policyMap);
|
|
1904
|
-
if (unreconciledFiles.size > 0) {
|
|
1905
|
-
summaries = summaries.map((summary) => unreconciledFiles.has(path.resolve(summary.filePath))
|
|
1906
|
-
? summarizeDiagnostics(summary.filePath, markUnreconciledFindings(summary.diagnostics), summary.hasFinalSnapshot)
|
|
1907
|
-
: summary);
|
|
1908
|
-
}
|
|
1909
|
-
// #1888: the full-mode summary above is the first seam where every
|
|
1910
|
-
// producing lane is correlated. The earlier footer loop intentionally writes
|
|
1911
|
-
// only CONFIRMED LSP results, so an ast-grep backpressure failure left
|
|
1912
|
-
// project-scan (`ast-grep-napi`) findings visible to lens_diagnostics but
|
|
1913
|
-
// absent from the widget. Commit this same post-policy, post-suppression set
|
|
1914
|
-
// to the widget so its human-facing count cannot silently become single-lane.
|
|
1915
|
-
const parsedProjectScanObservedAt = projectSnapshot?.scannedAt
|
|
1916
|
-
? Date.parse(projectSnapshot.scannedAt)
|
|
1917
|
-
: undefined;
|
|
1918
|
-
const projectScanObservedAt = parsedProjectScanObservedAt !== undefined &&
|
|
1919
|
-
Number.isFinite(parsedProjectScanObservedAt)
|
|
1920
|
-
? parsedProjectScanObservedAt
|
|
1921
|
-
: undefined;
|
|
1922
|
-
for (const summary of summaries) {
|
|
1923
|
-
reconcileCorrelatedScanDiagnostics(summary.filePath, summary.diagnostics, nextWriteIndex?.(), projectScanObservedAt);
|
|
1924
|
-
}
|
|
1925
|
-
const result = await formatAllMode(cwd, severity, summaries, {
|
|
1926
|
-
mode: "full",
|
|
1927
|
-
lspFilesChecked: rawLspResults.length,
|
|
1928
|
-
partial: aborted,
|
|
1929
|
-
projectDiagnostics: projectSnapshot === undefined
|
|
1930
|
-
? undefined
|
|
1931
|
-
: {
|
|
1932
|
-
tier: projectSnapshot.tier,
|
|
1933
|
-
filesScanned: projectSnapshot.filesScanned,
|
|
1934
|
-
diagnostics: projectSnapshot.diagnostics.length,
|
|
1935
|
-
runners: projectSnapshot.runners,
|
|
1936
|
-
},
|
|
1937
|
-
projectDiagnosticsDelta: projectDelta === undefined
|
|
1938
|
-
? undefined
|
|
1939
|
-
: {
|
|
1940
|
-
diagnostics: projectDelta.diagnostics.length,
|
|
1941
|
-
sources: projectDelta.sources,
|
|
1942
|
-
turnIndex: projectDelta.turnIndex,
|
|
1943
|
-
},
|
|
1944
|
-
}, 0, pathsScope);
|
|
1945
|
-
const missingNote = pathsScopeMissingNote(pathsScope);
|
|
1946
|
-
// #630: mirrors `tools/lsp-diagnostics.ts`'s `unconfirmedReasonClause`/
|
|
1947
|
-
// `tallyConfirmation` semantic guarantee (never silently render
|
|
1948
|
-
// unconfirmed as clean), adapted to this tool's own aggregate-summary
|
|
1949
|
-
// shape rather than its per-file result shape. `confirmedLspResults` were
|
|
1950
|
-
// already excluded from the merge above, so an agent reading the summary
|
|
1951
|
-
// alone would otherwise see nothing for these files and could read that
|
|
1952
|
-
// as "0 diagnostics" — say explicitly which files the LSP sweep could not
|
|
1953
|
-
// confirm and why, distinguishing a hard error from a soft timeout the
|
|
1954
|
-
// same way #570 does upstream.
|
|
1955
|
-
const unconfirmedByReason = tallyUnconfirmedReasons(unconfirmedLspResults, mismatchedLspResults);
|
|
1956
|
-
// #646: per-primary-server breakdown of the same confirmed/unconfirmed
|
|
1957
|
-
// tally — lets an agent see AT A GLANCE which server is responsible for
|
|
1958
|
-
// any unconfirmed files (e.g. "marksman: 0/34") instead of having to
|
|
1959
|
-
// cross-reference latency.log by hand. Computed unconditionally so it's
|
|
1960
|
-
// always available in `details` even on an all-confirmed sweep; only
|
|
1961
|
-
// rendered in the text note when more than one server is involved (with
|
|
1962
|
-
// a single server the breakdown is redundant with the totals already
|
|
1963
|
-
// stated).
|
|
1964
|
-
const lspServerBreakdown = tallyLspByPrimaryServer(confirmedLspResults, unconfirmedLspResults);
|
|
1965
|
-
const serverBreakdownClause = lspServerBreakdown.size > 1
|
|
1966
|
-
? ` — by server: ${formatLspServerBreakdown(lspServerBreakdown)}.`
|
|
1967
|
-
: "";
|
|
1968
|
-
const unconfirmedLspNote = unconfirmedLspResults.length > 0
|
|
1969
|
-
? `\n\n⚠ LSP sweep: ${confirmedLspResults.length} file(s) confirmed via LSP, ` +
|
|
1970
|
-
`${unconfirmedLspResults.length} unconfirmed (${formatUnconfirmedReasonClause(unconfirmedByReason)})${serverBreakdownClause} — NOT the same as 0 diagnostics for: ${unconfirmedLspResults
|
|
1971
|
-
.slice(0, 20)
|
|
1972
|
-
.map((result) => result.filePath)
|
|
1973
|
-
.join(", ")}${unconfirmedLspResults.length > 20 ? ", …" : ""}. These files' LSP contribution is excluded from this result; re-run mode=full to retry them (they may still show findings above from cached/project-runner state).`
|
|
1974
|
-
: "";
|
|
1975
|
-
// Code-unit comparator (#1883): this list ships as the
|
|
1976
|
-
// `unconfirmedLspServerIds` structured field and as the lane names in the
|
|
1977
|
-
// agent-visible coverage note, so its order must be deterministic across
|
|
1978
|
-
// locales — localeCompare is deliberately avoided.
|
|
1979
|
-
const uncoveredScannerIds = [
|
|
1980
|
-
...new Set(partiallyCoveredLspResults.flatMap((result) => result.unconfirmedServerIds ?? [])),
|
|
1981
|
-
].sort((a, b) => Number(a > b) - Number(a < b));
|
|
1982
|
-
const auxiliaryCoverageNote = uncoveredScannerIds.length > 0
|
|
1983
|
-
? `\n\n⚠ Auxiliary coverage incomplete: ${uncoveredScannerIds.join(", ")} did not answer for ${partiallyCoveredLspResults.length} file(s). Findings from answering servers are included; this is not a clean verdict for the named scanner lane(s).`
|
|
1984
|
-
: "";
|
|
1985
|
-
// #646: primary-vs-auxiliary split of the raw LSP-sweep findings (before
|
|
1986
|
-
// they're merged into the widget-state summaries below), mirroring
|
|
1987
|
-
// `lsp_diagnostics`' "Primary findings"/"Auxiliary findings" separation —
|
|
1988
|
-
// so a page of ast-grep/opengrep/marksman noise never buries whether the
|
|
1989
|
-
// real language server itself found anything in this sweep.
|
|
1990
|
-
const lspPrimaryVsAuxiliary = tallyLspPrimaryVsAuxiliary(confirmedLspResults);
|
|
1991
|
-
logExtension({
|
|
1992
|
-
subsystem: "lsp-diagnostics",
|
|
1993
|
-
message: "lens_diagnostics verdict",
|
|
1994
|
-
metadata: {
|
|
1995
|
-
server: [
|
|
1996
|
-
...new Set(confirmedLspResults.map((result) => primaryServerId(result.filePath) ?? "unknown")),
|
|
1997
|
-
].join(","),
|
|
1998
|
-
primary: lspPrimaryVsAuxiliary.primary,
|
|
1999
|
-
auxiliary: lspPrimaryVsAuxiliary.auxiliary,
|
|
2000
|
-
total: lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary,
|
|
2001
|
-
sources: [
|
|
2002
|
-
...new Set(confirmedLspResults.flatMap((result) => (result.diagnostics ?? []).map((diagnostic) => diagnostic.source ?? "unknown"))),
|
|
2003
|
-
].slice(0, 5),
|
|
2004
|
-
},
|
|
2005
|
-
});
|
|
2006
|
-
const lspPrimaryVsAuxiliaryNote = lspPrimaryVsAuxiliary.primary + lspPrimaryVsAuxiliary.auxiliary > 0
|
|
2007
|
-
? `\n\nLSP sweep findings: ${lspPrimaryVsAuxiliary.primary} primary (language server), ` +
|
|
2008
|
-
`${lspPrimaryVsAuxiliary.auxiliary} auxiliary (ast-grep/opengrep/zizmor/typos/marksman/...).`
|
|
2009
|
-
: "";
|
|
2010
|
-
// #533/#585: a heavyweight analyzer (knip/jscpd/madge/gitleaks/govulncheck/
|
|
2011
|
-
// trivy/dead-code) that contributed nothing this run is either COLD (not
|
|
2012
|
-
// applicable to this project / tool unavailable — see fresh-fetch.ts's
|
|
2013
|
-
// per-analyzer gates) or genuinely clean; `cold` only ever lists the
|
|
2014
|
-
// former. Silently folding "not applicable" into "no issues found" would be
|
|
2015
|
-
// exactly the false-empty #533 is about. Named per #511/#514's
|
|
2016
|
-
// actionable-warning shape: say what's cold and what would warm it, only
|
|
2017
|
-
// when the caller actually asked for runner state (extracted.cold is
|
|
2018
|
-
// always [] otherwise).
|
|
2019
|
-
// #585: an aborted fresh-fetch (Escape / the mode=full wall-clock ceiling)
|
|
2020
|
-
// reports its still-in-flight analyzers separately from a genuine
|
|
2021
|
-
// not-applicable/unavailable skip — "stopped mid-scan, unknown" is a
|
|
2022
|
-
// different, more honest reason than "not applicable to this project", and
|
|
2023
|
-
// conflating them would suggest re-running mode=full is pointless when it
|
|
2024
|
-
// isn't (a re-run may well complete for that analyzer).
|
|
2025
|
-
const abortedIds = new Set(extracted.abortedIds ?? []);
|
|
2026
|
-
const genuinelyColdIds = extracted.cold.filter((id) => !abortedIds.has(id));
|
|
2027
|
-
const partialIds = extracted.partial ?? [];
|
|
2028
|
-
// #747: an unsafe analysis root (cwd at/above $HOME) skips ALL heavyweight
|
|
2029
|
-
// analyzers before anything spawns — rendering that as the per-analyzer
|
|
2030
|
-
// "not applicable / unavailable" list would send the caller chasing seven
|
|
2031
|
-
// wrong reasons. One note with the real one instead.
|
|
2032
|
-
// #1623: prefer the SPECIFIC reason captured at each gate
|
|
2033
|
-
// (`extracted.coldReasons`) over a generic guess — `formatNotRunEntry`
|
|
2034
|
-
// falls back to `warmTriggerFor` only for an id this call's `coldReasons`
|
|
2035
|
-
// doesn't cover (e.g. a caller-supplied `cold` list from an older cache
|
|
2036
|
-
// shape). Single formatter (extractors.ts) so this note's wording can't
|
|
2037
|
-
// drift from any other caller that renders the same `cold` list.
|
|
2038
|
-
// #1623 fix-round F5: the "not requested" (quick-mode) batch shares ONE
|
|
2039
|
-
// reason (`notRequestedReason`) across every id — `formatNotRunEntry`
|
|
2040
|
-
// repeating that same ~130-char sentence per id makes the note nearly
|
|
2041
|
-
// unreadable, and the "not applicable / unavailable this run" header
|
|
2042
|
-
// below is a dishonest label for it: these lanes ARE applicable and
|
|
2043
|
-
// available, this call just never asked for them. Render that batch as
|
|
2044
|
-
// its own compact note — bare ids, the shared reason stated once — and
|
|
2045
|
-
// keep the detailed per-id format for genuinely cold lanes, where each
|
|
2046
|
-
// id's reason really does differ (not a git repo vs binary unavailable
|
|
2047
|
-
// vs retry cooldown, ...).
|
|
2048
|
-
const coldNote = extracted.unsafeRoot
|
|
2049
|
-
? `\n\nheavyweight analyzers skipped: the analysis root resolves at or above the home directory, so a fresh knip/jscpd/madge/gitleaks/govulncheck/trivy/dead-code scan would walk every unrelated tree under it. Supply a project directory below the home ceiling. Absence of their findings is NOT a clean verdict.`
|
|
2050
|
-
: extracted.analysisRootError
|
|
2051
|
-
? `\n\nheavyweight analyzers skipped: ${extracted.analysisRootError}. Supply an existing project directory below the home ceiling. Absence of their findings is NOT a clean verdict.`
|
|
2052
|
-
: !projectRunnersRequested && genuinelyColdIds.length > 0
|
|
2053
|
-
? `\n\nnot run this call (quick mode): ${genuinelyColdIds.join(", ")}. ${notRequestedReason(host)}. Absence of their findings is NOT a clean verdict.`
|
|
2054
|
-
: genuinelyColdIds.length > 0
|
|
2055
|
-
? `\n\ncold (not applicable / unavailable this run): ${genuinelyColdIds
|
|
2056
|
-
.map((id) => formatNotRunEntry(id, extracted.coldReasons))
|
|
2057
|
-
.join(", ")}. These analyzers have not contributed to this result — absence of their findings is NOT a clean verdict.`
|
|
2058
|
-
: "";
|
|
2059
|
-
const partialNote = partialIds.length > 0
|
|
2060
|
-
? `\n\n⚠ partial coverage (findings included): ${partialIds
|
|
2061
|
-
.map((id) => `${id} — ${extracted.partialReasons?.[id] ?? "coverage is incomplete"}`)
|
|
2062
|
-
.join(", ")}. Coverage is incomplete, so absence of findings is NOT a clean verdict.`
|
|
2063
|
-
: "";
|
|
2064
|
-
// #1004: unlike every other analyzer above (knip/jscpd/madge/gitleaks/
|
|
2065
|
-
// govulncheck/opengrep/trivy/dead-code all run a FRESH whole-project scan
|
|
2066
|
-
// per the fresh-fetch.ts header, so "clean" there really does mean "the
|
|
2067
|
-
// whole project was checked this call"), test-runner has no whole-project
|
|
2068
|
-
// run to trigger: its cache only ever reflects the (targeted,
|
|
2069
|
-
// cascade-aware) test file(s) touched by the MOST RECENT edit's turn_end
|
|
2070
|
-
// fire (`runtime-turn.ts`). Folding it into `runners`/`diagnostics` with no
|
|
2071
|
-
// distinguishing note — as opposed to when it's cold, which already gets
|
|
2072
|
-
// `coldNote` above — would let a caller mistake "test-runner ran
|
|
2073
|
-
// project-wide and is clean" for "it only ever saw the 2 files edited an
|
|
2074
|
-
// hour ago", exactly the false-empty/false-confidence #533 this whole
|
|
2075
|
-
// module exists to prevent. Fires whenever test-runner CONTRIBUTED
|
|
2076
|
-
// (`runners`), independent of whether it also has cold/failed entries this
|
|
2077
|
-
// call — a caller needs this caveat on every mode=full response where
|
|
2078
|
-
// test-runner findings are present, not only some.
|
|
2079
|
-
const testRunnerEditScopedNote = extracted.runners.includes("test-runner")
|
|
2080
|
-
? `\n\ntest-runner coverage is edit-scoped, NOT a full-project run: its findings only reflect the test file(s) touched by the most recent edit's turn_end fire, not a fresh whole-project test suite run (mode=full never re-runs the suite — see fresh-fetch.ts). Absence of test-runner findings elsewhere in the project is NOT a clean verdict, and presence here does not mean "just checked" — it may be from several turns ago.`
|
|
2081
|
-
: "";
|
|
2082
|
-
const failedAnalyzers = extracted.failed ?? [];
|
|
2083
|
-
const failedNote = failedAnalyzers.length > 0
|
|
2084
|
-
? `\n\nfailed (ran, but no trustworthy result): ${failedAnalyzers
|
|
2085
|
-
.map(({ id, summary }) => `${id} — ${summary}`)
|
|
2086
|
-
.join(", ")}. These analyzers were not cached and will be retried; absence of their findings is NOT a clean verdict.`
|
|
2087
|
-
: "";
|
|
2088
|
-
// #1617: the #1616 suppressed-bucket rule applied to mode=full — a finding
|
|
2089
|
-
// an agent/user marked false-positive/won't-fix now drops out of
|
|
2090
|
-
// `diagnostics` (fresh-fetch.ts's `record()`), but that drop must stay
|
|
2091
|
-
// visible as a count rather than reading as "nothing was wrong here".
|
|
2092
|
-
// Review-round F4 (#1625): per-lane attribution — "gitleaks 2, knip 1" says
|
|
2093
|
-
// WHICH analyzer's marks are doing the suppressing, not just a bare total.
|
|
2094
|
-
// A lane that's 100% suppressed still won't appear in `runners`/`cold` as
|
|
2095
|
-
// distinct from "ran clean" — that gap is #1623's lane-status territory,
|
|
2096
|
-
// not fixed here.
|
|
2097
|
-
const dispositionSuppressedByLaneText = Object.entries(extracted.dispositionSuppressedByLane ?? {})
|
|
2098
|
-
.map(([id, count]) => `${id} ${count}`)
|
|
2099
|
-
.join(", ");
|
|
2100
|
-
const dispositionSuppressedNote = (extracted.dispositionSuppressed ?? 0) > 0
|
|
2101
|
-
? `\n\nsuppressed by disposition: ${extracted.dispositionSuppressed} finding(s) dropped from this result because they're marked false-positive or won't-fix (${dispositionSuppressedByLaneText}). Not a clean verdict for those locations — they were found, then intentionally hidden.`
|
|
2102
|
-
: "";
|
|
2103
|
-
// #747/#250: the cheap project-diagnostics scan (scanProjectDiagnostics) and
|
|
2104
|
-
// the LSP workspace sweep (collectWorkspaceDiagnosticFiles) both refuse to
|
|
2105
|
-
// WALK from a cwd at/above $HOME — from there, walking would enumerate every
|
|
2106
|
-
// unrelated tree under home. An explicit `paths` file list (explicitFiles)
|
|
2107
|
-
// bypasses both walks, so it is never unsafe. Their empty results in that
|
|
2108
|
-
// case are "walked nothing", not "clean" — say so, matching the fresh-fetch
|
|
2109
|
-
// unsafeRoot note above.
|
|
2110
|
-
const walkUnsafeRoot = explicitFiles === undefined && isAtOrAboveHomeDir(cwd);
|
|
2111
|
-
const walkUnsafeRootNote = walkUnsafeRoot
|
|
2112
|
-
? `\n\nproject file walk skipped: the working directory resolves at or above the home directory, so the cheap project scan and the LSP workspace sweep would each enumerate every unrelated tree under it. Re-run from inside a project directory. Absence of their findings is NOT a clean verdict.`
|
|
2113
|
-
: "";
|
|
2114
|
-
// #784: the cheap project scan's `scanTruncated` flag (#760) reached this
|
|
2115
|
-
// seam already but nothing rendered it, so a capped scan read as a
|
|
2116
|
-
// complete clean sweep. Say so explicitly, same wording the MCP
|
|
2117
|
-
// `pilens_project_scan` tool uses.
|
|
2118
|
-
const scanTruncatedNoticeText = projectSnapshot
|
|
2119
|
-
? scanTruncationNotice(projectSnapshot)
|
|
2120
|
-
: undefined;
|
|
2121
|
-
const scanTruncatedNote = scanTruncatedNoticeText
|
|
2122
|
-
? `\n\n${scanTruncatedNoticeText}`
|
|
2123
|
-
: "";
|
|
2124
|
-
// #1107 phase 2: same "reached the seam, nothing rendered it" gap as #784
|
|
2125
|
-
// above, for the generated-name/dir skip counters.
|
|
2126
|
-
const generatedSkipNoticeText = projectSnapshot
|
|
2127
|
-
? generatedSkipNotice(projectSnapshot, host)
|
|
2128
|
-
: undefined;
|
|
2129
|
-
const generatedSkipNote = generatedSkipNoticeText
|
|
2130
|
-
? `\n\n${generatedSkipNoticeText}`
|
|
2131
|
-
: "";
|
|
2132
|
-
// #1623: the cheap in-process project scan (tree-sitter/fact-rules/
|
|
2133
|
-
// ast-grep) is a THIRD lane `coldNote` never covers (it isn't one of
|
|
2134
|
-
// `ANALYZER_IDS` — that list is fetch-fetch.ts's heavyweight analyzers
|
|
2135
|
-
// only). It has three distinct states, all of which must render — the
|
|
2136
|
-
// fix-round F2 finding: with `refreshRunners=cached` and no snapshot ever
|
|
2137
|
-
// written yet, BOTH of the two notes below used to stay empty (one gated
|
|
2138
|
-
// on `scannedAt` being present, the other on the "not requested" branch
|
|
2139
|
-
// this call isn't in), so the original #1623 silence survived in exactly
|
|
2140
|
-
// this mode. `cheapScanScannedAtMs` centralizes the one bit every branch
|
|
2141
|
-
// needs: whether the stored snapshot has a usable timestamp (#1623
|
|
2142
|
-
// fix-round F4 — a corrupt/missing `scannedAt` must not compute a NaN
|
|
2143
|
-
// age, mirroring `./cache.ts`'s `Number.isFinite` guard).
|
|
2144
|
-
const cheapScanScannedAtMs = rawProjectSnapshot?.scannedAt
|
|
2145
|
-
? Date.parse(rawProjectSnapshot.scannedAt)
|
|
2146
|
-
: undefined;
|
|
2147
|
-
const cheapScanStatusNote = !projectRunnersRequested
|
|
2148
|
-
? `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): not run this call (${notRequestedReason(host)}).`
|
|
2149
|
-
: options.refreshRunners === "cached"
|
|
2150
|
-
? cheapScanScannedAtMs !== undefined &&
|
|
2151
|
-
Number.isFinite(cheapScanScannedAtMs)
|
|
2152
|
-
? `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): served from cache, ${formatCacheAgeOld(Date.now() - cheapScanScannedAtMs)} — not re-run this call.`
|
|
2153
|
-
: `\n\ncheap project scan (tree-sitter/fact-rules/ast-grep): not run (no cached scan; refresh with refreshRunners=cheap/all to populate).`
|
|
2154
|
-
: "";
|
|
2155
|
-
const abortedNote = abortedIds.size > 0
|
|
2156
|
-
? `\n\nstopped mid-scan (still running in the background, not reflected in this result): ${[
|
|
2157
|
-
...abortedIds,
|
|
2158
|
-
].join(", ")}. Not a clean verdict for these — re-run mode=full to pick up their result once it's cached.`
|
|
2159
|
-
: "";
|
|
2160
|
-
// #585: mode=full now fetches these analyzers fresh rather than reading a
|
|
2161
|
-
// possibly-stale session_start cache — say so honestly, with per-analyzer
|
|
2162
|
-
// elapsed time, since this can legitimately take a while (trivy's own
|
|
2163
|
-
// ~180s ceiling).
|
|
2164
|
-
// #1623: `extracted.cachedAgeMs` marks ids that are a cache-read BY DESIGN
|
|
2165
|
-
// (today, only test-runner — see fresh-fetch.ts) rather than a fresh
|
|
2166
|
-
// execution this call. Exclude those from "fetched fresh" below — folding
|
|
2167
|
-
// a cache replay into the same list as a genuine run is exactly the
|
|
2168
|
-
// misread the second #1623 dogfood pass reported (a cached result read as
|
|
2169
|
-
// "just ran"). They get their own note with an actual age instead.
|
|
2170
|
-
const cachedAgeMs = extracted.cachedAgeMs ?? {};
|
|
2171
|
-
const timingEntries = Object.entries(extracted.timings ?? {}).filter(([id]) => !(id in cachedAgeMs));
|
|
2172
|
-
const freshNote = timingEntries.length > 0
|
|
2173
|
-
? `\n\nfetched fresh this call: ${timingEntries
|
|
2174
|
-
.map(([id, ms]) => `${id} (${Math.round(ms)}ms)`)
|
|
2175
|
-
.join(", ")}.`
|
|
2176
|
-
: "";
|
|
2177
|
-
const cachedAgeEntries = Object.entries(cachedAgeMs);
|
|
2178
|
-
const cachedAgeNote = cachedAgeEntries.length > 0
|
|
2179
|
-
? `\n\nserved from cache this call (not re-run): ${cachedAgeEntries
|
|
2180
|
-
.map(([id, ms]) => `${id} (${formatCacheAgeOld(ms)})`)
|
|
2181
|
-
.join(", ")}.`
|
|
2182
|
-
: "";
|
|
2183
|
-
// coldRunners always lands in details (even when empty) so a caller can
|
|
2184
|
-
// reliably check "were any extractors cold" without a presence check.
|
|
2185
|
-
// analyzerTimingsMs surfaces the #585 fresh-fetch elapsed time per
|
|
2186
|
-
// analyzer that actually ran this call (empty when refreshRunners opted
|
|
2187
|
-
// out of runner state entirely).
|
|
2188
|
-
const resultWithCold = {
|
|
2189
|
-
...result,
|
|
2190
|
-
details: {
|
|
2191
|
-
...result.details,
|
|
2192
|
-
coldRunners: extracted.cold,
|
|
2193
|
-
partialRunners: partialIds,
|
|
2194
|
-
// #1623: the specific reason each cold id was skipped (single source
|
|
2195
|
-
// of truth: extractors.ts's `formatNotRunEntry` renders the same map
|
|
2196
|
-
// into the text note above) — lets a caller check WHY without parsing
|
|
2197
|
-
// text.
|
|
2198
|
-
coldReasons: extracted.coldReasons ?? {},
|
|
2199
|
-
failedAnalyzers,
|
|
2200
|
-
analyzerTimingsMs: extracted.timings,
|
|
2201
|
-
dispositionSuppressed: extracted.dispositionSuppressed ?? 0,
|
|
2202
|
-
dispositionSuppressedByLane: extracted.dispositionSuppressedByLane ?? {},
|
|
2203
|
-
// #1623: ms-old each cache-read-by-design lane's data was (today, only
|
|
2204
|
-
// test-runner) — lets a caller distinguish "just ran" from "served
|
|
2205
|
-
// from cache" without parsing the text note.
|
|
2206
|
-
analyzersCachedAgeMs: cachedAgeMs,
|
|
2207
|
-
analyzersAborted: extracted.aborted ?? false,
|
|
2208
|
-
analyzersAbortedIds: extracted.abortedIds ?? [],
|
|
2209
|
-
// #747: true when the fresh fetch refused an at-or-above-$HOME root —
|
|
2210
|
-
// lets a caller distinguish "skipped for safety" from per-analyzer
|
|
2211
|
-
// cold reasons without parsing the text note.
|
|
2212
|
-
// This is the fetch seam's explicit-root policy. The cwd walk below is
|
|
2213
|
-
// intentionally separate: it answers a different root question.
|
|
2214
|
-
analyzersUnsafeRoot: extracted.analysisRootValidation?.state === "unsafe" ||
|
|
2215
|
-
extracted.unsafeRoot === true,
|
|
2216
|
-
analysisRootValidation: extracted.analysisRootValidation,
|
|
2217
|
-
// #747: true when the cwd resolved at/above $HOME so the cheap project
|
|
2218
|
-
// scan and the LSP workspace sweep both refused to walk — lets a caller
|
|
2219
|
-
// distinguish "walked nothing for safety" from a genuinely clean sweep.
|
|
2220
|
-
projectWalkUnsafeRoot: walkUnsafeRoot,
|
|
2221
|
-
// #630: confirmed/unconfirmed LSP-sweep tally, mirroring
|
|
2222
|
-
// `lsp_diagnostics`' confirmation state — lets a caller check "were
|
|
2223
|
-
// any files unconfirmed" without re-deriving it from the text.
|
|
2224
|
-
lspFilesConfirmed: confirmedLspResults.length,
|
|
2225
|
-
lspFilesUnconfirmed: unconfirmedLspResults.length,
|
|
2226
|
-
lspFilesPartiallyCovered: partiallyCoveredLspResults.length,
|
|
2227
|
-
unconfirmedLspServerIds: uncoveredScannerIds,
|
|
2228
|
-
unconfirmedLspFiles: unconfirmedLspResults.map((result) => result.filePath),
|
|
2229
|
-
// #646: per-primary-server breakdown of the same tally, plus the
|
|
2230
|
-
// primary/auxiliary findings split — mirrors `lsp_diagnostics`'
|
|
2231
|
-
// per-server reporting granularity (always present, even when every
|
|
2232
|
-
// server confirmed cleanly, so a caller can check it unconditionally).
|
|
2233
|
-
lspServerBreakdown: Object.fromEntries(lspServerBreakdown),
|
|
2234
|
-
lspPrimaryDiagnosticsCount: lspPrimaryVsAuxiliary.primary,
|
|
2235
|
-
lspAuxiliaryDiagnosticsCount: lspPrimaryVsAuxiliary.auxiliary,
|
|
2236
|
-
// #784: lets a caller check "was the cheap project scan truncated"
|
|
2237
|
-
// without parsing the text note.
|
|
2238
|
-
projectScanTruncated: projectSnapshot?.scanTruncated ?? false,
|
|
2239
|
-
// #1107 phase 2: same machine-readable convention, for the
|
|
2240
|
-
// generated-name/dir skip counters.
|
|
2241
|
-
...(projectSnapshot?.generatedFileSkips
|
|
2242
|
-
? { projectGeneratedFileSkips: projectSnapshot.generatedFileSkips }
|
|
2243
|
-
: {}),
|
|
2244
|
-
...(projectSnapshot?.generatedNameOnlySkips
|
|
2245
|
-
? {
|
|
2246
|
-
projectGeneratedNameOnlySkips: projectSnapshot.generatedNameOnlySkips,
|
|
2247
|
-
}
|
|
2248
|
-
: {}),
|
|
2249
|
-
...(projectSnapshot?.generatedDirSkips
|
|
2250
|
-
? { projectGeneratedDirSkips: projectSnapshot.generatedDirSkips }
|
|
2251
|
-
: {}),
|
|
2252
|
-
},
|
|
2253
|
-
};
|
|
2254
|
-
// Stopped mid-scan: the results above are whatever completed before the abort.
|
|
2255
|
-
// Tell the agent so it doesn't read a partial sweep as "clean" (#341). The
|
|
2256
|
-
// abort is either a user/turn cancel (Escape) or the wall-clock ceiling firing
|
|
2257
|
-
// (AbortSignal.timeout → TimeoutError), which guarantees the scan can't hang
|
|
2258
|
-
// indefinitely — distinguish them so the agent knows whether to just re-run.
|
|
2259
|
-
if (aborted) {
|
|
2260
|
-
const timedOut = signal
|
|
2261
|
-
?.reason?.name === "TimeoutError";
|
|
2262
|
-
const note = timedOut
|
|
2263
|
-
? `\n\n⚠ Scan exceeded its ${Math.round((options.wallClockMs ?? 0) / 1000)}s time budget and was stopped — results are partial. ` +
|
|
2264
|
-
"Narrow it with maxLspFiles, or raise PI_LENS_LENS_DIAGNOSTICS_FULL_TIMEOUT_MS."
|
|
2265
|
-
: "\n\n⚠ Scan cancelled before completion — results are partial. " +
|
|
2266
|
-
"Re-run with a smaller maxLspFiles to finish within budget.";
|
|
2267
|
-
return {
|
|
2268
|
-
content: [
|
|
2269
|
-
{
|
|
2270
|
-
type: "text",
|
|
2271
|
-
text: result.content[0].text +
|
|
2272
|
-
note +
|
|
2273
|
-
unconfirmedLspNote +
|
|
2274
|
-
auxiliaryCoverageNote +
|
|
2275
|
-
lspPrimaryVsAuxiliaryNote +
|
|
2276
|
-
partialNote +
|
|
2277
|
-
coldNote +
|
|
2278
|
-
testRunnerEditScopedNote +
|
|
2279
|
-
failedNote +
|
|
2280
|
-
dispositionSuppressedNote +
|
|
2281
|
-
walkUnsafeRootNote +
|
|
2282
|
-
scanTruncatedNote +
|
|
2283
|
-
generatedSkipNote +
|
|
2284
|
-
abortedNote +
|
|
2285
|
-
freshNote +
|
|
2286
|
-
cachedAgeNote +
|
|
2287
|
-
cheapScanStatusNote +
|
|
2288
|
-
missingNote,
|
|
2289
|
-
},
|
|
2290
|
-
],
|
|
2291
|
-
details: { ...resultWithCold.details, timedOut },
|
|
2292
|
-
};
|
|
2293
|
-
}
|
|
2294
|
-
if (missingNote ||
|
|
2295
|
-
coldNote ||
|
|
2296
|
-
partialNote ||
|
|
2297
|
-
testRunnerEditScopedNote ||
|
|
2298
|
-
failedNote ||
|
|
2299
|
-
dispositionSuppressedNote ||
|
|
2300
|
-
walkUnsafeRootNote ||
|
|
2301
|
-
scanTruncatedNote ||
|
|
2302
|
-
generatedSkipNote ||
|
|
2303
|
-
abortedNote ||
|
|
2304
|
-
freshNote ||
|
|
2305
|
-
cachedAgeNote ||
|
|
2306
|
-
cheapScanStatusNote ||
|
|
2307
|
-
unconfirmedLspNote ||
|
|
2308
|
-
auxiliaryCoverageNote ||
|
|
2309
|
-
lspPrimaryVsAuxiliaryNote) {
|
|
2310
|
-
return {
|
|
2311
|
-
content: [
|
|
2312
|
-
{
|
|
2313
|
-
type: "text",
|
|
2314
|
-
text: result.content[0].text +
|
|
2315
|
-
unconfirmedLspNote +
|
|
2316
|
-
auxiliaryCoverageNote +
|
|
2317
|
-
lspPrimaryVsAuxiliaryNote +
|
|
2318
|
-
partialNote +
|
|
2319
|
-
coldNote +
|
|
2320
|
-
testRunnerEditScopedNote +
|
|
2321
|
-
failedNote +
|
|
2322
|
-
dispositionSuppressedNote +
|
|
2323
|
-
walkUnsafeRootNote +
|
|
2324
|
-
scanTruncatedNote +
|
|
2325
|
-
generatedSkipNote +
|
|
2326
|
-
abortedNote +
|
|
2327
|
-
freshNote +
|
|
2328
|
-
cachedAgeNote +
|
|
2329
|
-
cheapScanStatusNote +
|
|
2330
|
-
missingNote,
|
|
2331
|
-
},
|
|
2332
|
-
],
|
|
2333
|
-
details: resultWithCold.details,
|
|
2334
|
-
};
|
|
2335
|
-
}
|
|
2336
|
-
return resultWithCold;
|
|
2337
|
-
}
|
|
2338
|
-
async function projectResolvedCwd(summary, cwd) {
|
|
2339
|
-
// #2777 O1: the whole three-way lookup lives on the LSP seam now. A file
|
|
2340
|
-
// with no primary LSP server is `undefined` here by construction, and
|
|
2341
|
-
// formatAllMode renders that row without a cwd term (N1) — the literal
|
|
2342
|
-
// `cwd=undefined` cannot be produced.
|
|
2343
|
-
const resolvedCwd = await resolveLspCwdForFile(summary.filePath, cwd);
|
|
2344
|
-
return {
|
|
2345
|
-
...summary,
|
|
2346
|
-
...(resolvedCwd === undefined ? {} : { resolvedCwd }),
|
|
2347
|
-
diagnostics: summary.diagnostics ?? [],
|
|
2348
|
-
};
|
|
2349
|
-
}
|
|
2350
|
-
// @delivery-surface: lens-diagnostics:mode-all
|
|
2351
|
-
async function formatAllMode(cwd, severity, summaries = getFileDiagnosticSummaries(), detailOverrides = { mode: "all" }, staleDropped = 0, pathsScope, dependencyDemoted = 0) {
|
|
2352
|
-
// #2275 review F2: the widget footer stops DRAWING a dependency-drift
|
|
2353
|
-
// demotion once it hits `DEPENDENCY_DRIFT_MAX_DELIVERIES` unconfirmed
|
|
2354
|
-
// deliveries, but the record stays here (dropping it would make an
|
|
2355
|
-
// unconfirmed LSP error read as clean). Say so, through the same note
|
|
2356
|
-
// channel as the demotion itself, so the agent knows why the footer went
|
|
2357
|
-
// quiet and that a re-run can still confirm the finding.
|
|
2358
|
-
const footerCapped = summaries.reduce((n, s) => n + (s.diagnostics ?? []).filter((d) => d.footerRetired === true).length, 0);
|
|
2359
|
-
// Files changed/deleted since their diagnostics were recorded have already
|
|
2360
|
-
// been dropped by reconcileStaleWidgetFiles; note them so the agent knows
|
|
2361
|
-
// those aren't "clean", just un-rescanned (use mode=full to refresh).
|
|
2362
|
-
const staleNote = (staleDropped > 0
|
|
2363
|
-
? ` (${staleDropped} changed file${staleDropped === 1 ? "" : "s"} omitted as stale — use mode=full to rescan)`
|
|
2364
|
-
: "") +
|
|
2365
|
-
(dependencyDemoted > 0
|
|
2366
|
-
? ` (${dependencyDemoted} blocking finding${dependencyDemoted === 1 ? "" : "s"} demoted to [stale] — an imported file changed since; re-run to confirm)`
|
|
2367
|
-
: "") +
|
|
2368
|
-
(footerCapped > 0
|
|
2369
|
-
? ` (${footerCapped} demoted finding${footerCapped === 1 ? "" : "s"} capped after ${DEPENDENCY_DRIFT_MAX_DELIVERIES} unconfirmed deliveries — no longer shown in the pi-lens footer, still listed here; re-run to confirm)`
|
|
2370
|
-
: "");
|
|
2371
|
-
// mode=full already actively scanned exactly the requested paths, so a zero
|
|
2372
|
-
// result there IS a legitimate clean read — the cache-only note only applies
|
|
2373
|
-
// to delta/all, which is why this is gated on detailOverrides.mode !== "full".
|
|
2374
|
-
const isFullMode = detailOverrides.mode === "full";
|
|
2375
|
-
const includeFile = createScopedFileFilter(cwd, pathsScope);
|
|
2376
|
-
// Cached summaries predate marks. Every cache-only surface re-applies
|
|
2377
|
-
// dispositions through the shared applyCachedDispositions seam — strict
|
|
2378
|
-
// false-positive anchors against current content when the file is
|
|
2379
|
-
// unchanged since observation, weak-only fallback otherwise.
|
|
2380
|
-
const policyMap = loadProjectRulePolicyMap(cwd);
|
|
2381
|
-
const dispositioned = isFullMode
|
|
2382
|
-
? summaries
|
|
2383
|
-
: summaries.map((s) => {
|
|
2384
|
-
const diagnostics = s.diagnostics ?? [];
|
|
2385
|
-
const kept = applyCachedDispositions(diagnostics, cwd, s.filePath);
|
|
2386
|
-
const policyKept = applyRulePolicy(kept, policyMap);
|
|
2387
|
-
if (policyKept.length === kept.length &&
|
|
2388
|
-
kept.length === diagnostics.length)
|
|
2389
|
-
return s;
|
|
2390
|
-
return summarizeDiagnostics(s.filePath, policyKept, s.hasFinalSnapshot);
|
|
2391
|
-
});
|
|
2392
|
-
// #1641: the single chokepoint both mode=all (cache-only `summaries`) and
|
|
2393
|
-
// mode=full (widget cache merged with a FRESH LSP sweep, above) converge
|
|
2394
|
-
// through — a fresh sweep's server can still be citing its own stale
|
|
2395
|
-
// in-memory document, so gating only the cached half would miss exactly
|
|
2396
|
-
// the live incident this issue reports. Runs AFTER dispositions/rule
|
|
2397
|
-
// policy (not before): a finding a mark or a `.pi-lens.json` rule policy
|
|
2398
|
-
// already dropped is not being served, so it must never trigger a resync
|
|
2399
|
-
// or a `diagnostic_past_eof` telemetry record on this call.
|
|
2400
|
-
const eofGated = dispositioned.map((s) => {
|
|
2401
|
-
const { diagnostics, demotedCount } = demotePastEofDiagnostics({
|
|
2402
|
-
store: "lens_diagnostics",
|
|
2403
|
-
cwd,
|
|
2404
|
-
filePath: s.filePath,
|
|
2405
|
-
diagnostics: s.diagnostics ?? [],
|
|
2406
|
-
resync: resyncDocumentOnPastEof,
|
|
2407
|
-
});
|
|
2408
|
-
if (demotedCount === 0)
|
|
2409
|
-
return s;
|
|
2410
|
-
return summarizeDiagnostics(s.filePath, diagnostics, s.hasFinalSnapshot);
|
|
2411
|
-
});
|
|
2412
|
-
const visibleSummaries = eofGated.filter((s) => includeFile(s.filePath));
|
|
2413
|
-
// Filter to files with actual issues. A file whose only findings were
|
|
2414
|
-
// demoted by the #1641 past-EOF gate has blocking/errors/warnings at 0 —
|
|
2415
|
-
// counting it as "issue-free" would tell the agent the file is CLEAN when
|
|
2416
|
-
// it actually has an unconfirmed finding waiting on a rescan. Surface it
|
|
2417
|
-
// under `severity: "all"` (the gate's own advisory tier) so it stays
|
|
2418
|
-
// visible; a narrower `error`/`warning` filter still excludes it, matching
|
|
2419
|
-
// how those filters already treat any other non-matching severity.
|
|
2420
|
-
const withIssues = visibleSummaries.filter((s) => {
|
|
2421
|
-
if (severity === "error")
|
|
2422
|
-
return s.blocking > 0 || s.errors > 0;
|
|
2423
|
-
// #2414: a "warning" filter must not admit hint/info — that is exactly
|
|
2424
|
-
// the "present hints as warnings" defect this issue exists to close.
|
|
2425
|
-
if (severity === "warning")
|
|
2426
|
-
return s.blocking > 0 || s.errors > 0 || s.warnings > 0;
|
|
2427
|
-
// severity: "all" — a hint/info-only file (`advisories > 0`,
|
|
2428
|
-
// warnings === 0) must still surface here, or the #2414 fix that stops
|
|
2429
|
-
// hints inflating `warnings` would silently drop that file from the
|
|
2430
|
-
// detailed listing instead of just from the compact warning tally.
|
|
2431
|
-
return (s.blocking > 0 ||
|
|
2432
|
-
s.errors > 0 ||
|
|
2433
|
-
s.warnings > 0 ||
|
|
2434
|
-
s.advisories > 0 ||
|
|
2435
|
-
(s.diagnostics ?? []).some((d) => d.stale));
|
|
2436
|
-
});
|
|
2437
|
-
const pathsNote = isFullMode ? "" : pathsScopeCacheOnlyNote(pathsScope);
|
|
2438
|
-
if (withIssues.length === 0) {
|
|
2439
|
-
const text = (visibleSummaries.length === 0
|
|
2440
|
-
? "No files diagnosed yet this session."
|
|
2441
|
-
: `No ${severity === "all" ? "" : severity + " "}issues across ${visibleSummaries.length} file${visibleSummaries.length === 1 ? "" : "s"} diagnosed this session. ✓`) +
|
|
2442
|
-
staleNote +
|
|
2443
|
-
pathsNote;
|
|
2444
|
-
return {
|
|
2445
|
-
content: [{ type: "text", text }],
|
|
2446
|
-
details: {
|
|
2447
|
-
...detailOverrides,
|
|
2448
|
-
filesChecked: visibleSummaries.length,
|
|
2449
|
-
staleDropped,
|
|
2450
|
-
},
|
|
2451
|
-
};
|
|
2452
|
-
}
|
|
2453
|
-
// Sort: blocking first, then errors, then warnings
|
|
2454
|
-
const sorted = withIssues.sort((a, b) => b.blocking - a.blocking || b.errors - a.errors || b.warnings - a.warnings);
|
|
2455
|
-
// #2777 O2: only rendered rows resolve their root. The projection sits
|
|
2456
|
-
// below the withIssues filter, so a 500-file session pays the cwd
|
|
2457
|
-
// resolution for the handful of rows listed here, not for every summary
|
|
2458
|
-
// the cache holds.
|
|
2459
|
-
const rendered = await Promise.all(sorted.map((summary) => projectResolvedCwd(summary, cwd)));
|
|
2460
|
-
const lines = [];
|
|
2461
|
-
let totalBlocking = 0;
|
|
2462
|
-
let totalErrors = 0;
|
|
2463
|
-
let totalWarnings = 0;
|
|
2464
|
-
let totalAdvisories = 0;
|
|
2465
|
-
for (const s of rendered) {
|
|
2466
|
-
const rel = path.relative(cwd, s.filePath);
|
|
2467
|
-
const parts = [];
|
|
2468
|
-
if (s.blocking > 0)
|
|
2469
|
-
parts.push(`🔴 ${s.blocking} blocking`);
|
|
2470
|
-
if (s.errors > 0 && s.blocking === 0)
|
|
2471
|
-
parts.push(`${s.errors}E`);
|
|
2472
|
-
if (s.warnings > 0)
|
|
2473
|
-
parts.push(`${s.warnings}W`);
|
|
2474
|
-
// #2414: hint/info never inflate `warnings`, but a hint-only row still
|
|
2475
|
-
// needs a reason to be listed — otherwise it shows a bare filename with
|
|
2476
|
-
// no count at all.
|
|
2477
|
-
if (s.advisories > 0)
|
|
2478
|
-
parts.push(`${s.advisories} hint${s.advisories === 1 ? "" : "s"}`);
|
|
2479
|
-
if (!s.hasFinalSnapshot)
|
|
2480
|
-
parts.push(`(pending)`);
|
|
2481
|
-
const staleCount = (s.diagnostics ?? []).filter((d) => d.stale).length;
|
|
2482
|
-
if (staleCount > 0) {
|
|
2483
|
-
parts.push(`${staleCount} stale — re-run to confirm`);
|
|
2484
|
-
}
|
|
2485
|
-
// #2777 N1: files with no primary LSP server have no resolvedCwd —
|
|
2486
|
-
// omit the term rather than rendering the literal `cwd=undefined`.
|
|
2487
|
-
if (s.resolvedCwd !== undefined)
|
|
2488
|
-
parts.push(`cwd=${s.resolvedCwd}`);
|
|
2489
|
-
lines.push(`${rel} ${parts.join(" ")}`);
|
|
2490
|
-
// List the actual diagnostics (not just counts) so the agent can act on
|
|
2491
|
-
// them without re-running anything — same "L<line>: <message>" shape as the
|
|
2492
|
-
// inline blocker output. The widget state now exposes the FULL set (not the
|
|
2493
|
-
// TUI's 12-cap); the tool applies its own generous per-file budget purely to
|
|
2494
|
-
// avoid flooding context on a pathologically broken file.
|
|
2495
|
-
const matching = (s.diagnostics ?? [])
|
|
2496
|
-
.filter((d) => matchesSeverity(d, severity))
|
|
2497
|
-
.sort(bySeverityThenLine);
|
|
2498
|
-
const shown = matching.slice(0, MAX_DIAGNOSTICS_PER_FILE);
|
|
2499
|
-
for (const d of shown) {
|
|
2500
|
-
const marker = d.stale
|
|
2501
|
-
? ""
|
|
2502
|
-
: isErrorLike(d)
|
|
2503
|
-
? d.semantic === "blocking"
|
|
2504
|
-
? "🔴 "
|
|
2505
|
-
: ""
|
|
2506
|
-
: "";
|
|
2507
|
-
const label = d.rule ?? d.tool;
|
|
2508
|
-
const tag = label ? ` [${label}]` : "";
|
|
2509
|
-
const flaggedTag = d.flagged ? " 📌 flagged-to-fix" : "";
|
|
2510
|
-
const msg = d.message.replace(/\s+/g, " ").trim();
|
|
2511
|
-
// #1641: a demoted past-EOF entry no longer has a trustworthy line —
|
|
2512
|
-
// show the marker instead of a coordinate that doesn't exist in the
|
|
2513
|
-
// current file, matching the #1622/#1627 stale-line render convention.
|
|
2514
|
-
// Past-EOF demotions replace the untrustworthy coordinate; other
|
|
2515
|
-
// demotions (#1631 drift) keep their in-bounds line and append the
|
|
2516
|
-
// shared stale marker instead.
|
|
2517
|
-
const pastEof = d.stale && (d.staleReason ?? "past-eof") === "past-eof";
|
|
2518
|
-
const loc = pastEof ? PAST_EOF_STALE_MARKER : `L${d.line ?? "?"}`;
|
|
2519
|
-
const staleTag = d.stale && !pastEof ? ` ${STALE_LINE_MARKER}` : "";
|
|
2520
|
-
lines.push(` ${marker}${loc}: ${msg}${tag}${flaggedTag}${staleTag}`);
|
|
2521
|
-
}
|
|
2522
|
-
if (matching.length > shown.length) {
|
|
2523
|
-
lines.push(` … ${matching.length - shown.length} more in this file (showing ${shown.length} of ${matching.length})`);
|
|
2524
|
-
}
|
|
2525
|
-
totalBlocking += s.blocking;
|
|
2526
|
-
totalErrors += s.errors;
|
|
2527
|
-
totalWarnings += s.warnings;
|
|
2528
|
-
totalAdvisories += s.advisories;
|
|
2529
|
-
}
|
|
2530
|
-
// #1799: `totalBlocking` and `totalErrors` count the same findings UNLESS a
|
|
2531
|
-
// dependency-drift demotion (#1631) has revoked blocking authority from an
|
|
2532
|
-
// error while leaving it in the error tally (widget-state.ts `isBlocking`
|
|
2533
|
-
// vs `countDiagnostics`) — that's a real, live disagreement between the two
|
|
2534
|
-
// totals, not double-counting. So `errors` renders only when
|
|
2535
|
-
// `blocking === 0`, same guard as the per-file row above: it shows the
|
|
2536
|
-
// drift-demoted findings the blocking line can't, without ever printing the
|
|
2537
|
-
// same findings twice under two labels when blocking > 0.
|
|
2538
|
-
const summary = [
|
|
2539
|
-
`\nSummary (${visibleSummaries.length} files diagnosed this session):`,
|
|
2540
|
-
totalBlocking > 0
|
|
2541
|
-
? ` 🔴 ${totalBlocking} blocking error${totalBlocking === 1 ? "" : "s"}`
|
|
2542
|
-
: null,
|
|
2543
|
-
totalBlocking === 0 && totalErrors > 0
|
|
2544
|
-
? ` ${totalErrors} error${totalErrors === 1 ? "" : "s"}`
|
|
2545
|
-
: null,
|
|
2546
|
-
totalWarnings > 0
|
|
2547
|
-
? ` ${totalWarnings} warning${totalWarnings === 1 ? "" : "s"}`
|
|
2548
|
-
: null,
|
|
2549
|
-
// #2414: reported separately from `warnings` — hint/info are style
|
|
2550
|
-
// opinions, not defects, and must not read as unresolved code issues.
|
|
2551
|
-
totalAdvisories > 0
|
|
2552
|
-
? ` ${totalAdvisories} hint/info (style — not counted as warnings)`
|
|
2553
|
-
: null,
|
|
2554
|
-
]
|
|
2555
|
-
.filter(Boolean)
|
|
2556
|
-
.join("\n");
|
|
2557
|
-
return {
|
|
2558
|
-
content: [
|
|
2559
|
-
{ type: "text", text: lines.join("\n") + summary + staleNote },
|
|
2560
|
-
],
|
|
2561
|
-
details: {
|
|
2562
|
-
...detailOverrides,
|
|
2563
|
-
filesWithIssues: withIssues.length,
|
|
2564
|
-
totalBlocking,
|
|
2565
|
-
totalErrors,
|
|
2566
|
-
totalWarnings,
|
|
2567
|
-
totalAdvisories,
|
|
2568
|
-
staleDropped,
|
|
2569
|
-
},
|
|
2570
|
-
};
|
|
2571
|
-
}
|