@herbertgao/pi-extensions 2026.10.0 → 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 +16 -12
- package/THIRD_PARTY_NOTICES.md +3 -3
- 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/.opencode/plugins/index.js +4 -0
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
- package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
- 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 +52 -242
- package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
- 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/hooks/ponytail-mode-tracker.js +46 -23
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
- package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
- package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
- package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
- package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
- package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
- package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
- 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 +4 -4
- 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/node_modules/pi-typesafe/README.md +1 -1
- package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
- package/node_modules/pi-typesafe/dist/backends.js +18 -0
- package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
- package/node_modules/pi-typesafe/dist/client.js +22 -7
- package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
- package/node_modules/pi-typesafe/dist/index.js +1 -1
- package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
- package/node_modules/pi-typesafe/dist/usage.js +147 -42
- package/node_modules/pi-typesafe/package.json +1 -1
- package/node_modules/pi-web-access/CHANGELOG.md +48 -0
- package/node_modules/pi-web-access/README.md +57 -11
- package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
- package/node_modules/pi-web-access/curator-page.ts +5 -1
- package/node_modules/pi-web-access/curator-server.ts +2 -0
- package/node_modules/pi-web-access/declared-web-links.ts +2 -2
- package/node_modules/pi-web-access/degoog.ts +323 -0
- package/node_modules/pi-web-access/dist/index.js +3483 -2745
- package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
- package/node_modules/pi-web-access/exa.ts +5 -1
- package/node_modules/pi-web-access/extract.ts +41 -3
- package/node_modules/pi-web-access/firecrawl.ts +35 -4
- package/node_modules/pi-web-access/gemini-search.ts +34 -11
- package/node_modules/pi-web-access/gemini-web.ts +14 -9
- package/node_modules/pi-web-access/index.ts +153 -1202
- package/node_modules/pi-web-access/keenable.ts +221 -0
- package/node_modules/pi-web-access/mcp-cli.ts +40 -0
- package/node_modules/pi-web-access/mcp-server.ts +81 -0
- package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
- package/node_modules/pi-web-access/openai-search.ts +34 -1
- package/node_modules/pi-web-access/package.json +23 -5
- package/node_modules/pi-web-access/perplexity.ts +64 -56
- package/node_modules/pi-web-access/tool-arguments.ts +20 -0
- package/node_modules/pi-web-access/video-extract.ts +1 -1
- package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
- package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
- package/node_modules/pi-web-access/youtube-extract.ts +2 -5
- package/package.json +17 -12
- package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
- package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
- package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
- package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
- package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
- package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
- 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,1808 +0,0 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
2
|
-
import { stat } from "node:fs/promises";
|
|
3
|
-
import * as path from "node:path";
|
|
4
|
-
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { BoundedFifoMap } from "./bounded-cache.js";
|
|
6
|
-
import { demotePastEofDiagnostics, } from "./diagnostic-line-freshness.js";
|
|
7
|
-
import { visibleWidth } from "./deps/pi-tui.js";
|
|
8
|
-
import { normalizeEphemeralMapKey, normalizeMapKey } from "./path-utils.js";
|
|
9
|
-
import { fitLine } from "./tui-fit.js";
|
|
10
|
-
import { WriteOrderingGuard } from "./write-ordering-guard.js";
|
|
11
|
-
import { collectForwardImportMtimes } from "./blocker-freshness.js";
|
|
12
|
-
import { normalizeMessage } from "./finding-identity.js";
|
|
13
|
-
import { freshnessFromMtime } from "./freshness.js";
|
|
14
|
-
import { PAST_EOF_STALE_MARKER } from "./diagnostic-line-freshness.js";
|
|
15
|
-
import { STALE_LINE_MARKER } from "./stale-marker.js";
|
|
16
|
-
import { anchorsForDiagnostic, applyDispositions, getDisposition, registerWidgetDispositionReconciler, strictAnchorSpan, strictAnchorSpansIn, } from "./diagnostic-dispositions.js";
|
|
17
|
-
import { recordDegradationOnce } from "./degradation-ledger.js";
|
|
18
|
-
/**
|
|
19
|
-
* Canonical key for the `files` map (and `diagnosticsWriteGuard`) — #1020.
|
|
20
|
-
*
|
|
21
|
-
* The SAME file reaches this module under DIFFERENT path forms in one session:
|
|
22
|
-
* forward-slash (`C:/…/x.ts`) from the LSP client + cascade fold via
|
|
23
|
-
* `normalizeFilePath`, and backslash (`C:\…\x.ts`) from mode=full's reconcile
|
|
24
|
-
* writing `result.filePath` and from `path.resolve`/event inputs on Windows.
|
|
25
|
-
* Keyed raw, those coexisted as two entries: `mode=full` re-keyed on read and
|
|
26
|
-
* the clean entry hid the stale one, but `mode=all`'s `formatAllMode` reads the
|
|
27
|
-
* summaries verbatim and rendered the stale `blocking:1` as a 🔴 (#1020) — a
|
|
28
|
-
* resolved state that replayed as still-broken on every `mode=all`.
|
|
29
|
-
*
|
|
30
|
-
* `normalizeEphemeralMapKey` (slash-fold + win32-lowercase, NO filesystem I/O)
|
|
31
|
-
* is chosen over `normalizeMapKey`/`normalizeFilePath`, which call
|
|
32
|
-
* `realpathSync.native()` — real disk I/O on EVERY diagnostic/runner/formatter
|
|
33
|
-
* write, far too heavy for this hot path. The key only needs to be a stable
|
|
34
|
-
* syntactic fold that collapses `\`↔`/` and Windows drive-letter case, which
|
|
35
|
-
* this does; on-disk canonical casing is irrelevant for merely deduplicating a
|
|
36
|
-
* process-local footer cache. The human-readable path is preserved separately
|
|
37
|
-
* on the record's `filePath` (see `toDisplayPath`) for rendering/summaries.
|
|
38
|
-
*/
|
|
39
|
-
function fileMapKey(filePath) {
|
|
40
|
-
return normalizeEphemeralMapKey(filePath);
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* A diagnostic is "blocking" when pi-lens classifies it as a hard stop
|
|
44
|
-
* (`semantic === "blocking"`). Falls back to severity for sources that
|
|
45
|
-
* don't set `semantic` (raw tsc/eslint diagnostics) so the red dot still
|
|
46
|
-
* fires on traditional compile errors.
|
|
47
|
-
*
|
|
48
|
-
* #1561: exported so the blocker-retire decision in `tools/lsp-diagnostics.ts`
|
|
49
|
-
* asks THIS predicate rather than growing a second severity rule that can drift
|
|
50
|
-
* from what the footer counts.
|
|
51
|
-
*/
|
|
52
|
-
export function isBlocking(d) {
|
|
53
|
-
// #3158 round 2 F1: a finding the agent marked `false-positive`/`suppress` is
|
|
54
|
-
// not a hard stop — it is an explicitly dismissed one. Before this line only
|
|
55
|
-
// `countDiagnostics` knew that (it skips tagged entries), so the footer header
|
|
56
|
-
// counted zero errors while every consumer that asks THIS predicate still
|
|
57
|
-
// treated the row as blocking: `renderWidget` drew a red ● row for it,
|
|
58
|
-
// `capStoredDiagnostics` hoisted it ahead of live findings (evicting one at the
|
|
59
|
-
// display cap), and `getWidgetBlockingFilesForSweep`,
|
|
60
|
-
// `markWidgetFileBlockersStale` and `reconcileStaleWidgetDependencyBlockers`
|
|
61
|
-
// admitted it to the turn-end blocker sweep. The mismatch was latent while a
|
|
62
|
-
// tagged row lived only between a mark and the next scan; retention makes it
|
|
63
|
-
// durable, so the predicate has to answer for the tag.
|
|
64
|
-
if (d.disposition)
|
|
65
|
-
return false;
|
|
66
|
-
// #1631: a dependency-drift-demoted finding is no longer a hard stop. The gate
|
|
67
|
-
// sets `stale` rather than dropping the entry (#1419 demote-not-drop), so every
|
|
68
|
-
// tally and render that asks "is this blocking?" must answer no once demoted.
|
|
69
|
-
if (d.stale)
|
|
70
|
-
return false;
|
|
71
|
-
if (d.semantic === "blocking")
|
|
72
|
-
return true;
|
|
73
|
-
if (d.semantic == null && d.severity === "error")
|
|
74
|
-
return true;
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
// ── Module state ─────────────────────────────────────────────────────────────
|
|
78
|
-
const files = new Map();
|
|
79
|
-
let sessionLanguages = [];
|
|
80
|
-
let requestRenderFn = null;
|
|
81
|
-
/**
|
|
82
|
-
* Guards `recordDiagnostics` writes against the same race class fixed for
|
|
83
|
-
* `clients/lsp/client.ts` in #555: pi-lens allows concurrent pipeline runs
|
|
84
|
-
* for the same file across different same-turn edits, so an older edit's
|
|
85
|
-
* (slower) pipeline can finish its `recordDiagnostics` call AFTER a newer
|
|
86
|
-
* edit's (faster) pipeline already recorded fresher diagnostics for that
|
|
87
|
-
* path. Keyed by `filePath`, tokened by `writeIndex` (see
|
|
88
|
-
* `clients/runtime-tool-result.ts:nextWriteIndex`).
|
|
89
|
-
*/
|
|
90
|
-
const diagnosticsWriteGuard = new WriteOrderingGuard();
|
|
91
|
-
/**
|
|
92
|
-
* Runner completions also mutate the shared file record: they mark the
|
|
93
|
-
* diagnostics snapshot pending. Keep that mutation in the same per-file order
|
|
94
|
-
* as the final diagnostic replacement, or an older pipeline can set a newer
|
|
95
|
-
* confirmed-clean record back to `(pending)` (#1198).
|
|
96
|
-
*/
|
|
97
|
-
const runnerWriteGuard = new WriteOrderingGuard();
|
|
98
|
-
const MAX_STORED_DIAGNOSTICS_PER_FILE = 12;
|
|
99
|
-
/**
|
|
100
|
-
* #3158 / AGENTS.md shape 9: the per-file bound on
|
|
101
|
-
* {@link WidgetDiagnostic.suppressedRetained} rows. Retention is the only rule
|
|
102
|
-
* that lets a record hold findings the latest scan did not report, so it is the
|
|
103
|
-
* only axis of `allDiagnostics` that can grow while the live set shrinks — every
|
|
104
|
-
* other row is replaced wholesale by the next write. Its own constant rather
|
|
105
|
-
* than `MAX_STORED_DIAGNOSTICS_PER_FILE`: that one is the TUI's display cap
|
|
106
|
-
* (`capStoredDiagnostics`) and the two would drift the moment either policy
|
|
107
|
-
* moved. Twelve because the chip is a COUNT, not a list — past a dozen
|
|
108
|
-
* suppressed findings on one unchanged file the exact total stops informing the
|
|
109
|
-
* footer, and the truncation is stated once per file through the ledger rather
|
|
110
|
-
* than dropped silently.
|
|
111
|
-
*/
|
|
112
|
-
const MAX_RETAINED_SUPPRESSED_PER_FILE = 12;
|
|
113
|
-
const MAX_INACTIVE_FILE_RECORDS = 1024;
|
|
114
|
-
const ACTIVE_FILE_IDLE_MS = 30 * 60_000;
|
|
115
|
-
export const MAX_LSP_SERVER_RECORDS = 128;
|
|
116
|
-
const lspServers = new BoundedFifoMap(MAX_LSP_SERVER_RECORDS);
|
|
117
|
-
// Pruning is a cold-size-boundary operation. Do not walk the whole file map
|
|
118
|
-
// for every record in a large diagnostics reconciliation; the full-scan path
|
|
119
|
-
// can legitimately create thousands of records in one synchronous batch.
|
|
120
|
-
let nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
|
|
121
|
-
function pruneInactiveFileRecords(now = Date.now()) {
|
|
122
|
-
if (files.size <= MAX_INACTIVE_FILE_RECORDS)
|
|
123
|
-
return;
|
|
124
|
-
const victims = [...files.entries()]
|
|
125
|
-
.filter(([, rec]) => now - rec.touchedAt > ACTIVE_FILE_IDLE_MS && !hasLiveDiagnostic(rec))
|
|
126
|
-
.sort(([, a], [, b]) => a.touchedAt - b.touchedAt);
|
|
127
|
-
for (const [key] of victims) {
|
|
128
|
-
if (files.size <= MAX_INACTIVE_FILE_RECORDS)
|
|
129
|
-
break;
|
|
130
|
-
files.delete(key);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
function maybePruneInactiveFileRecords() {
|
|
134
|
-
if (files.size < nextInactivePruneSize)
|
|
135
|
-
return;
|
|
136
|
-
pruneInactiveFileRecords();
|
|
137
|
-
// A live-heavy map may remain above the soft bound. Do not rescan it for
|
|
138
|
-
// every subsequent file; the next lifecycle starts with a fresh state map.
|
|
139
|
-
nextInactivePruneSize = Number.POSITIVE_INFINITY;
|
|
140
|
-
}
|
|
141
|
-
// ── Public API ────────────────────────────────────────────────────────────────
|
|
142
|
-
export function setRenderCallback(fn) {
|
|
143
|
-
requestRenderFn = fn;
|
|
144
|
-
}
|
|
145
|
-
/** Reconcile a mark through the same apply/count/commit path as dispatch. */
|
|
146
|
-
export function reconcileWidgetDisposition(cwd, target, _disposition, content) {
|
|
147
|
-
const current = getFileDiagnostics(target.filePath);
|
|
148
|
-
if (!current) {
|
|
149
|
-
recordDegradationOnce({
|
|
150
|
-
kind: "widget-disposition-reconcile-fallback",
|
|
151
|
-
subject: target.filePath,
|
|
152
|
-
reason: "the marked finding's widget record is absent",
|
|
153
|
-
});
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
let source = content ?? target.content;
|
|
157
|
-
if (source === undefined) {
|
|
158
|
-
try {
|
|
159
|
-
source = readFileSync(target.filePath, "utf8");
|
|
160
|
-
}
|
|
161
|
-
catch {
|
|
162
|
-
recordDegradationOnce({
|
|
163
|
-
kind: "widget-disposition-reconcile-fallback",
|
|
164
|
-
subject: target.filePath,
|
|
165
|
-
reason: "the disposition anchor content could not be read",
|
|
166
|
-
});
|
|
167
|
-
source = "";
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
const active = new Set(applyDispositions(current, cwd, target.filePath, source));
|
|
171
|
-
const normalized = current.flatMap((diagnostic) => {
|
|
172
|
-
const { strict, weak } = anchorsForDiagnostic(cwd, target.filePath, diagnostic, source);
|
|
173
|
-
const entry = getDisposition(cwd, strict) ?? getDisposition(cwd, weak);
|
|
174
|
-
if (!active.has(diagnostic) &&
|
|
175
|
-
(entry?.disposition === "false-positive" ||
|
|
176
|
-
entry?.disposition === "suppress")) {
|
|
177
|
-
// #3183: stamp the strict anchor's own SPAN so the row's lifetime can
|
|
178
|
-
// later ask the mark instead of the file's mtime. `source` is the content
|
|
179
|
-
// the anchors above were derived from, so the span is the one the mark
|
|
180
|
-
// itself binds to. Only the STRICT arm has a span — a weak `suppress`
|
|
181
|
-
// mark hashes no line — and re-tagging always re-derives it, so a stale
|
|
182
|
-
// span from an earlier tag can never survive into a new disposition.
|
|
183
|
-
const { anchorSpan: _previousSpan, ...base } = diagnostic;
|
|
184
|
-
const span = entry.disposition === "false-positive"
|
|
185
|
-
? strictAnchorSpan(source, diagnostic.line)
|
|
186
|
-
: undefined;
|
|
187
|
-
return [
|
|
188
|
-
{
|
|
189
|
-
...base,
|
|
190
|
-
disposition: entry.disposition,
|
|
191
|
-
flagged: false,
|
|
192
|
-
...(span === undefined ? {} : { anchorSpan: span }),
|
|
193
|
-
},
|
|
194
|
-
];
|
|
195
|
-
}
|
|
196
|
-
// #3158 retirement rule 3: a row the store holds ONLY because it was
|
|
197
|
-
// suppressed (`suppressedRetained`) has no live scan behind it — the last
|
|
198
|
-
// scan of this file did not report it. Once its mark stops suppressing it
|
|
199
|
-
// (a strict `false-positive` anchor whose marked line changed), retire it
|
|
200
|
-
// rather than re-arming it below as a finding the agent would read as
|
|
201
|
-
// currently present.
|
|
202
|
-
if (diagnostic.suppressedRetained)
|
|
203
|
-
return [];
|
|
204
|
-
const { disposition: _disposition, flagged: _flagged,
|
|
205
|
-
// #3183: a row that is no longer disposition-tagged has no mark to
|
|
206
|
-
// anchor, so its span goes with the tag — leaving it would exempt a LIVE
|
|
207
|
-
// row from the sweep's mtime gate.
|
|
208
|
-
anchorSpan: _anchorSpan, ...baseDiagnostic } = diagnostic;
|
|
209
|
-
return entry?.disposition === "flagged"
|
|
210
|
-
? [{ ...baseDiagnostic, flagged: true }]
|
|
211
|
-
: [baseDiagnostic];
|
|
212
|
-
});
|
|
213
|
-
const rec = getOrCreate(target.filePath);
|
|
214
|
-
const writeIndex = admitWidgetDiagnosticsWrite(target.filePath);
|
|
215
|
-
if (!diagnosticsWriteGuard.shouldWrite(fileMapKey(target.filePath), writeIndex))
|
|
216
|
-
return;
|
|
217
|
-
commitDiagnostics(rec, target.filePath, normalized, Date.now(), fileMapKey(target.filePath), false);
|
|
218
|
-
}
|
|
219
|
-
/** Reserve a widget write for a producer that has no runtime write index. */
|
|
220
|
-
export function admitWidgetDiagnosticsWrite(filePath, writeIndex) {
|
|
221
|
-
const key = fileMapKey(filePath);
|
|
222
|
-
const token = writeIndex ?? diagnosticsWriteGuard.nextToken(key);
|
|
223
|
-
diagnosticsWriteGuard.shouldWrite(key, token);
|
|
224
|
-
runnerWriteGuard.shouldWrite(key, token);
|
|
225
|
-
return token;
|
|
226
|
-
}
|
|
227
|
-
registerWidgetDispositionReconciler((cwd, target, disposition) => reconcileWidgetDisposition(cwd, target, disposition, target.content));
|
|
228
|
-
/** Consume the host-side disposition event and refresh the real widget store. */
|
|
229
|
-
export function wireWidgetDispositionSubscriber(args) {
|
|
230
|
-
args.events?.on?.("pilens:diagnostic:disposition", (data) => {
|
|
231
|
-
if (!data || typeof data !== "object")
|
|
232
|
-
return;
|
|
233
|
-
const payload = data;
|
|
234
|
-
if (payload.source !== "pi-lens" || typeof payload.cwd !== "string")
|
|
235
|
-
return;
|
|
236
|
-
if (typeof payload.filePath !== "string" ||
|
|
237
|
-
typeof payload.disposition !== "string")
|
|
238
|
-
return;
|
|
239
|
-
if (typeof payload.message !== "string")
|
|
240
|
-
return;
|
|
241
|
-
if (!["false-positive", "suppress", "defer", "flagged"].includes(payload.disposition))
|
|
242
|
-
return;
|
|
243
|
-
reconcileWidgetDisposition(payload.cwd, {
|
|
244
|
-
cwd: payload.cwd,
|
|
245
|
-
filePath: payload.filePath,
|
|
246
|
-
message: typeof payload.message === "string" ? payload.message : "",
|
|
247
|
-
...(typeof payload.tool === "string" ? { tool: payload.tool } : {}),
|
|
248
|
-
...(typeof payload.rule === "string" ? { rule: payload.rule } : {}),
|
|
249
|
-
...(typeof payload.line === "number" ? { line: payload.line } : {}),
|
|
250
|
-
}, payload.disposition);
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* #2275 review F1: files whose dependency-drift-demoted rows the footer has
|
|
255
|
-
* ACTUALLY DRAWN since the last turn end — the delivery population
|
|
256
|
-
* `runtime-turn.ts`'s cap step drains.
|
|
257
|
-
*
|
|
258
|
-
* The footer is not a broadcast of the whole store: `renderWidget` picks ONE
|
|
259
|
-
* record (`withBlocking[0]`) and its first five qualifying entries, and it
|
|
260
|
-
* may not be drawn at all (headless, or a turn with no repaint). Counting a
|
|
261
|
-
* delivery per turn end therefore charged the cap for rows the agent never
|
|
262
|
-
* saw, and retired them a delivery early. A Set (not a counter) is the right
|
|
263
|
-
* shape: the widget repaints many times per turn, and all of those repaints
|
|
264
|
-
* are the SAME one delivery.
|
|
265
|
-
*/
|
|
266
|
-
const renderedDependencyDriftFiles = new Set();
|
|
267
|
-
export function clearWidgetState() {
|
|
268
|
-
files.clear();
|
|
269
|
-
renderedDependencyDriftFiles.clear();
|
|
270
|
-
lspServers.clear();
|
|
271
|
-
sessionLanguages = [];
|
|
272
|
-
requestRenderFn = null;
|
|
273
|
-
diagnosticsWriteGuard.clear();
|
|
274
|
-
runnerWriteGuard.clear();
|
|
275
|
-
nextInactivePruneSize = MAX_INACTIVE_FILE_RECORDS + 1;
|
|
276
|
-
}
|
|
277
|
-
// v1 → v2 (#1186): per-entry `WidgetDiagnostic.observedAt`. v2 is a SUPERSET of
|
|
278
|
-
// v1 (the field is additive/optional), so `importWidgetState` accepts either and
|
|
279
|
-
// migrates a v1 record by inheriting each entry's `observedAt` from the record's
|
|
280
|
-
// `touchedAt`. A v1 file must never be rejected (that would silently drop resume
|
|
281
|
-
// diagnostics) nor crash.
|
|
282
|
-
//
|
|
283
|
-
// v2 → v3 (#1631 review F3): a dependency-drift `stale` demotion is a verdict
|
|
284
|
-
// about THIS session's disk state — the next session starts with a fresh (and
|
|
285
|
-
// possibly different) dependency graph, so a demotion from the last session
|
|
286
|
-
// must not outlive it. `exportWidgetState` had been serializing `stale: true`
|
|
287
|
-
// verbatim; on restore, a genuine type error stayed non-blocking across a
|
|
288
|
-
// resume with the dependency pinned back into the past. Same shape as the
|
|
289
|
-
// #1348 failed-formatter precedent one field over: `importWidgetState` strips
|
|
290
|
-
// `stale` on every restored entry so a resumed session re-evaluates from
|
|
291
|
-
// scratch instead of inheriting a possibly-stale-itself verdict.
|
|
292
|
-
export const WIDGET_STATE_VERSION = 3;
|
|
293
|
-
/**
|
|
294
|
-
* Snapshot the per-file widget diagnostics for persistence (#190). Excludes
|
|
295
|
-
* `lspServers` — those are process-bound (servers re-spawn fresh on the next
|
|
296
|
-
* launch), so restoring their "ready" status would be misleading.
|
|
297
|
-
*/
|
|
298
|
-
export function exportWidgetState() {
|
|
299
|
-
return {
|
|
300
|
-
version: WIDGET_STATE_VERSION,
|
|
301
|
-
sessionLanguages: [...sessionLanguages],
|
|
302
|
-
files: [...files.values()].map((rec) => ({
|
|
303
|
-
filePath: rec.filePath,
|
|
304
|
-
runners: [...rec.runners.entries()],
|
|
305
|
-
formatters: [...rec.formatters.entries()],
|
|
306
|
-
diagnostics: rec.diagnostics,
|
|
307
|
-
allDiagnostics: rec.allDiagnostics,
|
|
308
|
-
diagnosticCounts: rec.diagnosticCounts,
|
|
309
|
-
hasFinalDiagnosticsSnapshot: rec.hasFinalDiagnosticsSnapshot,
|
|
310
|
-
touchedAt: rec.touchedAt,
|
|
311
|
-
})),
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Restore a {@link PersistedWidgetState} snapshot (#190 resume rehydration).
|
|
316
|
-
* Replaces the in-memory `files` map; ignores snapshots from a different
|
|
317
|
-
* version. Triggers a re-render if a callback is registered.
|
|
318
|
-
*/
|
|
319
|
-
/**
|
|
320
|
-
* #1186 v1→v2 migration: stamp each entry that lacks a per-entry `observedAt`
|
|
321
|
-
* with the record's `touchedAt` (the single stamp the whole record shared under
|
|
322
|
-
* v1). Non-mutating; entries that already carry a stamp (a v2 record) pass
|
|
323
|
-
* through untouched.
|
|
324
|
-
*
|
|
325
|
-
* #1631 review F3 (v2→v3): also strips `stale` from every entry. A dependency
|
|
326
|
-
* drift demotion is scoped to the session that observed the drift; restoring it
|
|
327
|
-
* verbatim would let a real blocking finding resume as permanently non-blocking
|
|
328
|
-
* (#1348 precedent — see `WIDGET_STATE_VERSION`'s doc comment).
|
|
329
|
-
*/
|
|
330
|
-
function migrateEntryStamps(entries, recordTouchedAt) {
|
|
331
|
-
return (entries ?? []).map((d) => {
|
|
332
|
-
// #2275: strip staleDeliveryCount and footerRetired alongside stale — a
|
|
333
|
-
// delivery count (and the footer-hide it earned) without the demotion
|
|
334
|
-
// it counts is meaningless, and a resumed session re-evaluates
|
|
335
|
-
// dependency-drift from scratch (#1631 review F3) same as the `stale`
|
|
336
|
-
// bit itself. A restored footer has shown the row zero times.
|
|
337
|
-
// Fix-round 3 (#2275 review F2): `staleReason` left the round trip
|
|
338
|
-
// unstripped, so a resumed row could carry `staleReason:
|
|
339
|
-
// "dependency-drift"` with `stale` gone — `isDependencyDriftDemoted`
|
|
340
|
-
// reads `stale === true`, so that combination reads as clean today, but
|
|
341
|
-
// the stray reason is still a demotion label with no demotion behind
|
|
342
|
-
// it, the same "meaningless without the bit it counts" shape as the
|
|
343
|
-
// other three fields. Strip it alongside them.
|
|
344
|
-
const { stale: _stale, staleDeliveryCount: _staleDeliveryCount, footerRetired: _footerRetired, staleReason: _staleReason, ...rest } = d;
|
|
345
|
-
return rest.observedAt == null
|
|
346
|
-
? { ...rest, observedAt: recordTouchedAt }
|
|
347
|
-
: rest;
|
|
348
|
-
});
|
|
349
|
-
}
|
|
350
|
-
export function importWidgetState(state) {
|
|
351
|
-
// Accept any known-or-older version and migrate (#1186): reject a missing
|
|
352
|
-
// snapshot, a missing/non-numeric `version` (NaN/undefined/null — the
|
|
353
|
-
// pre-#1186 guard `version !== WIDGET_STATE_VERSION` rejected these, and
|
|
354
|
-
// loosening that would silently admit a malformed/foreign snapshot), or a
|
|
355
|
-
// FUTURE version this build can't understand. Rejecting a v1
|
|
356
|
-
// (pre-per-entry-stamp) file, by contrast, would silently drop all resume
|
|
357
|
-
// diagnostics — so v1..current are accepted and migrated.
|
|
358
|
-
if (!state ||
|
|
359
|
-
typeof state.version !== "number" ||
|
|
360
|
-
state.version < 1 ||
|
|
361
|
-
state.version > WIDGET_STATE_VERSION) {
|
|
362
|
-
return false;
|
|
363
|
-
}
|
|
364
|
-
files.clear();
|
|
365
|
-
// A resumed session's writeIndex counter starts fresh (#190 rehydration is
|
|
366
|
-
// process-bound like lspServers, see the export above) — any ordering
|
|
367
|
-
// tokens tracked before the restore no longer correspond to anything, so
|
|
368
|
-
// drop them rather than risk a legitimate post-resume write being read as
|
|
369
|
-
// "superseded" against a stale token.
|
|
370
|
-
diagnosticsWriteGuard.clear();
|
|
371
|
-
runnerWriteGuard.clear();
|
|
372
|
-
for (const f of state.files ?? []) {
|
|
373
|
-
// Fold persisted keys through the same normalizer as live writes (#1020),
|
|
374
|
-
// or a persisted forward-slash key stays split from a fresh backslash key
|
|
375
|
-
// across a resumed session — a primary repro condition. Keep a readable
|
|
376
|
-
// display path on the record.
|
|
377
|
-
// #1186 migration: a v1 record's entries have no per-entry `observedAt`.
|
|
378
|
-
// Inherit the record's `touchedAt` (a safe, over-conservative default —
|
|
379
|
-
// the whole record shared that one stamp before), so the per-entry stale
|
|
380
|
-
// gate has a concrete observation time and never treats `undefined` as
|
|
381
|
-
// epoch-0 (which would drop every migrated entry on the first sweep).
|
|
382
|
-
const recordTouchedAt = f.touchedAt ?? Date.now();
|
|
383
|
-
// #1631 review V1: `diagnosticCounts` is DERIVED from `allDiagnostics`
|
|
384
|
-
// everywhere else in this module (see the `countDiagnostics(rec.allDiagnostics)`
|
|
385
|
-
// call sites) — it must be recomputed here too, from the just-migrated
|
|
386
|
-
// (stale-stripped) entries, not restored verbatim from the snapshot. The
|
|
387
|
-
// persisted counts were computed while a finding was still demoted
|
|
388
|
-
// (`blocking: 0`); once F3 strips `stale` so `isBlocking` reports true
|
|
389
|
-
// again, a verbatim count would still say `blocking: 0` for a record
|
|
390
|
-
// whose entries ARE blocking — inverting the one-predicate invariant
|
|
391
|
-
// `isBlocking` exists to hold. Every consumer (`getFileDiagnosticSummaries`,
|
|
392
|
-
// the record-tier classifier, the footer) trusts `diagnosticCounts`, not a
|
|
393
|
-
// live re-scan of the entries, so a derived count out of sync with its own
|
|
394
|
-
// entries is silently wrong everywhere at once.
|
|
395
|
-
const migratedAllDiagnostics = migrateEntryStamps(f.allDiagnostics, recordTouchedAt);
|
|
396
|
-
files.set(fileMapKey(f.filePath), {
|
|
397
|
-
filePath: f.filePath,
|
|
398
|
-
runners: new Map(f.runners ?? []),
|
|
399
|
-
// Failure entries do NOT survive a session restore (#1348 review):
|
|
400
|
-
// a fmt-failed marker is live advice about THIS session's last
|
|
401
|
-
// attempt; rehydrating one from a snapshot shows a stale failure the
|
|
402
|
-
// current session never observed (and same-mtime fixes would never
|
|
403
|
-
// clear it). Successes rehydrate as before.
|
|
404
|
-
formatters: new Map((f.formatters ?? []).filter(([, outcome]) => outcome?.success !== false)),
|
|
405
|
-
diagnostics: migrateEntryStamps(f.diagnostics, recordTouchedAt),
|
|
406
|
-
allDiagnostics: migratedAllDiagnostics,
|
|
407
|
-
diagnosticCounts: countDiagnostics(migratedAllDiagnostics),
|
|
408
|
-
hasFinalDiagnosticsSnapshot: f.hasFinalDiagnosticsSnapshot ?? false,
|
|
409
|
-
touchedAt: recordTouchedAt,
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
pruneInactiveFileRecords();
|
|
413
|
-
sessionLanguages = state.sessionLanguages ?? [];
|
|
414
|
-
requestRenderFn?.();
|
|
415
|
-
return true;
|
|
416
|
-
}
|
|
417
|
-
export function setSessionLanguages(langs) {
|
|
418
|
-
sessionLanguages = langs;
|
|
419
|
-
requestRender();
|
|
420
|
-
}
|
|
421
|
-
/** File-kinds detected in use this session (#170 staleness scope). */
|
|
422
|
-
export function getSessionLanguages() {
|
|
423
|
-
return [...sessionLanguages];
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Distinct serverIds with a failed spawn record (#170). Raw — the per-language
|
|
427
|
-
* coverage check (a live sibling) and the in-use staleness filter live in
|
|
428
|
-
* `selectLspStatus`, which joins this against the alive set and session kinds.
|
|
429
|
-
*/
|
|
430
|
-
export function getFailedLspServerIds() {
|
|
431
|
-
const ids = [];
|
|
432
|
-
const seen = new Set();
|
|
433
|
-
for (const rec of lspServers.values()) {
|
|
434
|
-
if (rec.status !== "failed" || seen.has(rec.serverId))
|
|
435
|
-
continue;
|
|
436
|
-
seen.add(rec.serverId);
|
|
437
|
-
ids.push(rec.serverId);
|
|
438
|
-
}
|
|
439
|
-
return ids;
|
|
440
|
-
}
|
|
441
|
-
export function recordFormatter(filePath, formatter, changed, success, outcome) {
|
|
442
|
-
const rec = getOrCreate(filePath);
|
|
443
|
-
rec.formatters.set(formatter, { changed, success, outcome });
|
|
444
|
-
rec.touchedAt = Date.now();
|
|
445
|
-
files.set(fileMapKey(filePath), rec);
|
|
446
|
-
requestRender();
|
|
447
|
-
}
|
|
448
|
-
export function recordRunner(filePath, runnerId, status, diagnosticCount, durationMs, writeIndex) {
|
|
449
|
-
const key = fileMapKey(filePath);
|
|
450
|
-
if (!runnerWriteGuard.shouldWrite(key, writeIndex))
|
|
451
|
-
return;
|
|
452
|
-
// Advance the final-diagnostics guard too. A runner completion is part of
|
|
453
|
-
// this pipeline's ordered write, even though its immediate effect is only to
|
|
454
|
-
// mark the record pending. This prevents an older final replacement from
|
|
455
|
-
// landing after a newer runner state (and vice versa).
|
|
456
|
-
diagnosticsWriteGuard.shouldWrite(key, writeIndex);
|
|
457
|
-
const rec = getOrCreate(filePath);
|
|
458
|
-
rec.runners.set(runnerId, { status, count: diagnosticCount, durationMs });
|
|
459
|
-
rec.hasFinalDiagnosticsSnapshot = false;
|
|
460
|
-
rec.touchedAt = Date.now();
|
|
461
|
-
files.set(fileMapKey(filePath), rec);
|
|
462
|
-
requestRender();
|
|
463
|
-
}
|
|
464
|
-
/**
|
|
465
|
-
* Collapse a (possibly multi-line) diagnostic message to a single line.
|
|
466
|
-
* TS2769 / "no overload matches" and many compiler errors are multi-line;
|
|
467
|
-
* embedded newlines/tabs would otherwise render across several widget rows
|
|
468
|
-
* (and break the `L<line>: <message>` inline-blocker format), so flatten all
|
|
469
|
-
* whitespace runs to a single space before storing.
|
|
470
|
-
*/
|
|
471
|
-
function toSingleLineMessage(message) {
|
|
472
|
-
return (message ?? "").replace(/\s+/g, " ").trim();
|
|
473
|
-
}
|
|
474
|
-
/** Build the OSC-8 target used by every stored diagnostic row. */
|
|
475
|
-
export function widgetDiagnosticUri(filePath, line, column) {
|
|
476
|
-
const base = pathToFileURL(filePath).href;
|
|
477
|
-
return line != null
|
|
478
|
-
? `${base}#L${line}${column != null ? `:${column}` : ""}`
|
|
479
|
-
: base;
|
|
480
|
-
}
|
|
481
|
-
export function recordDiagnostics(filePath, diagnostics, writeIndex,
|
|
482
|
-
// #1093: when the truth was OBSERVED, not when it's being written. Defaults
|
|
483
|
-
// to `Date.now()` for the per-edit/live path (observed now). A reconcile
|
|
484
|
-
// replaying a CACHED view (e.g. the workspace-diagnostics cache-hit branch in
|
|
485
|
-
// `tools/lsp-diagnostics.ts`) must pass the cache entry's own scan timestamp
|
|
486
|
-
// here — otherwise a repeat "fresh check" that merely re-serves a stale
|
|
487
|
-
// cached view keeps bumping `touchedAt` to now(), permanently disarming
|
|
488
|
-
// `reconcileStaleWidgetFiles`'s `mtimeMs > touchedAt` gate so a resolved
|
|
489
|
-
// finding renders forever (the #1092 touchedAt-re-arming defect).
|
|
490
|
-
observedAt) {
|
|
491
|
-
// Drop a write that's superseded by a later same-turn edit to this file
|
|
492
|
-
// whose pipeline finished first (same race class as #555). No cache write,
|
|
493
|
-
// no count/timestamp update, no render trigger — the recorded state must
|
|
494
|
-
// stay exactly as the fresher write left it. `writeIndex` omitted (e.g.
|
|
495
|
-
// the `clients/mcp/analyze.ts` on-demand call site, which has no per-edit
|
|
496
|
-
// ordering token) always proceeds, same as version-less LSP servers in the
|
|
497
|
-
// #555 guard.
|
|
498
|
-
const key = fileMapKey(filePath);
|
|
499
|
-
if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
|
|
500
|
-
return false;
|
|
501
|
-
// Keep runner state ordered with the final diagnostic replacement. The
|
|
502
|
-
// guards are deliberately advanced in both directions because either verb
|
|
503
|
-
// may be the first completion from a pipeline.
|
|
504
|
-
runnerWriteGuard.shouldWrite(key, writeIndex);
|
|
505
|
-
// Resolve the observation time ONCE (#1186): every incoming entry is stamped
|
|
506
|
-
// with it, and it also seeds the record's `touchedAt`. A fresh write (no
|
|
507
|
-
// `observedAt`) is observed now.
|
|
508
|
-
const observedTs = observedAt ?? Date.now();
|
|
509
|
-
const rec = getOrCreate(filePath, key);
|
|
510
|
-
commitDiagnostics(rec, filePath, normalizeDiagnostics(filePath, diagnostics, observedTs), observedTs, key);
|
|
511
|
-
return true;
|
|
512
|
-
}
|
|
513
|
-
/** Map the raw diagnostic shape callers pass into stored {@link WidgetDiagnostic}s.
|
|
514
|
-
* Every produced entry is stamped with `observedTs` (#1186) — the time THIS batch
|
|
515
|
-
* of diagnostics was observed — so the per-entry stale gate can later drop just
|
|
516
|
-
* the entries older than the file's mtime rather than the whole record. */
|
|
517
|
-
function normalizeDiagnostics(filePath, diagnostics, observedTs) {
|
|
518
|
-
return diagnostics.map((d) => {
|
|
519
|
-
const rule = d.rule ?? d.id;
|
|
520
|
-
return {
|
|
521
|
-
severity: d.severity ?? "info",
|
|
522
|
-
semantic: d.semantic,
|
|
523
|
-
message: toSingleLineMessage(d.message),
|
|
524
|
-
line: d.line,
|
|
525
|
-
col: d.column,
|
|
526
|
-
rule,
|
|
527
|
-
tool: d.tool,
|
|
528
|
-
uri: widgetDiagnosticUri(filePath, d.line, d.column),
|
|
529
|
-
observedAt: observedTs,
|
|
530
|
-
};
|
|
531
|
-
});
|
|
532
|
-
}
|
|
533
|
-
/**
|
|
534
|
-
* The identity a retained suppressed row is matched against a later scan's
|
|
535
|
-
* entries by (#3158), so a scan that reports the finding AGAIN replaces the
|
|
536
|
-
* retained row instead of standing beside it as a duplicate.
|
|
537
|
-
*
|
|
538
|
-
* These are the WEAK ANCHOR's identity parts — `computeWeakAnchor`
|
|
539
|
-
* (`clients/diagnostic-dispositions.ts`) hashes `[tool, rule,
|
|
540
|
-
* normalizeMessage(message)]` over the file — deliberately, because the weak
|
|
541
|
-
* anchor is how the disposition store itself decides which findings a mark
|
|
542
|
-
* covers. Matching at any finer granularity means the store can disagree with
|
|
543
|
-
* the mark that created the row.
|
|
544
|
-
*
|
|
545
|
-
* Round 2 F2: `line` USED to be part of it, and that was the finer granularity
|
|
546
|
-
* the paragraph above forbids. A writer that re-reports the marked finding one
|
|
547
|
-
* line down — `clients/pipeline.ts`'s per-edit `recordDiagnostics`, or
|
|
548
|
-
* `reconcileCascadeNeighborLspErrors`, neither of which runs
|
|
549
|
-
* `reconcileStaleWidgetFiles` — left the retained row standing beside the live
|
|
550
|
-
* one, so a single finding was counted live AND in `suppressed: N`.
|
|
551
|
-
*
|
|
552
|
-
* `computeWeakAnchor` itself is not called here: it needs `cwd` to build its
|
|
553
|
-
* path component, which `commitDiagnostics` does not have and could only get by
|
|
554
|
-
* widening `recordDiagnostics`, `reconcileScanDiagnostics` and
|
|
555
|
-
* `reconcileCorrelatedScanDiagnostics` up through `tools/`. That component is
|
|
556
|
-
* constant across one file record — both sides of this comparison are the same
|
|
557
|
-
* file — so it would discriminate nothing. `normalizeMessage` is the shared
|
|
558
|
-
* derivation from `clients/finding-identity.ts` that the anchor uses, imported
|
|
559
|
-
* rather than re-spelled.
|
|
560
|
-
*
|
|
561
|
-
* #3183: this identity is OCCURRENCE-BLIND, and for a STRICT (`false-positive`)
|
|
562
|
-
* row that is not enough on its own. Two findings of one rule with identical
|
|
563
|
-
* messages on different lines collapse onto it, so a scan reporting only the
|
|
564
|
-
* UNMARKED one matched the marked row and replaced it — the chip lost a mark
|
|
565
|
-
* that was still applying. A collision is therefore resolved by the second half
|
|
566
|
-
* of {@link WidgetDiagnostic.anchorSpan}'s contract, in `retainSuppressedRows`:
|
|
567
|
-
* only a colliding incoming finding sitting ON the marked line is the same
|
|
568
|
-
* finding. That test, not this one, is what keeps the round-2 F2 case above
|
|
569
|
-
* closed — the marked line's TEXT moved, so its span still matches.
|
|
570
|
-
*/
|
|
571
|
-
function retentionIdentity(d) {
|
|
572
|
-
return JSON.stringify([
|
|
573
|
-
d.tool ?? "",
|
|
574
|
-
d.rule ?? "",
|
|
575
|
-
normalizeMessage(d.message),
|
|
576
|
-
]);
|
|
577
|
-
}
|
|
578
|
-
/** `filePath`'s current content, or `undefined` when it cannot be read. */
|
|
579
|
-
function readContentOrUndefined(filePath) {
|
|
580
|
-
try {
|
|
581
|
-
return readFileSync(filePath, "utf8");
|
|
582
|
-
}
|
|
583
|
-
catch {
|
|
584
|
-
// Only reached once this module has established there IS a span-carrying row
|
|
585
|
-
// to ask about, so the record is bounded by that population — and worth
|
|
586
|
-
// making: with no content both callers fall back to the pre-#3183 rules (the
|
|
587
|
-
// per-entry mtime gate, the coarse retention identity), which is the
|
|
588
|
-
// conservative direction but can retire a mark that still stands.
|
|
589
|
-
recordDegradationOnce({
|
|
590
|
-
kind: "widget-mark-anchor-unreadable",
|
|
591
|
-
subject: filePath,
|
|
592
|
-
reason: "the marked file could not be read, so its suppressed rows fall back to the file-mtime retirement gate and the footer chip may under-count it",
|
|
593
|
-
});
|
|
594
|
-
return undefined;
|
|
595
|
-
}
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* {@link strictAnchorSpansIn} over `filePath`'s current content (#3183), or
|
|
599
|
-
* `undefined` when it cannot be read.
|
|
600
|
-
*
|
|
601
|
-
* Synchronous in both callers, matching `applyCachedDispositions`
|
|
602
|
-
* (`tools/lens-diagnostics.ts`) — the seam that asks the very same question on
|
|
603
|
-
* the very same read path, with `statSync`/`readFileSync`. An async read here
|
|
604
|
-
* would add two unbounded awaits to a module a registered hook handler imports
|
|
605
|
-
* directly (`tests/config/hook-await-bounds.test.ts`) for no behavioural gain.
|
|
606
|
-
*/
|
|
607
|
-
function readAnchorSpans(filePath) {
|
|
608
|
-
const content = readContentOrUndefined(filePath);
|
|
609
|
-
return content === undefined ? undefined : strictAnchorSpansIn(content);
|
|
610
|
-
}
|
|
611
|
-
/**
|
|
612
|
-
* #3158: carry the record's disposition-tagged rows across a whole-replace
|
|
613
|
-
* write when the incoming set no longer reports them, so a `false-positive` /
|
|
614
|
-
* `suppress` mark keeps contributing to the footer's `suppressed: N` chip. See
|
|
615
|
-
* {@link WidgetDiagnostic.suppressedRetained} for the full lifetime statement.
|
|
616
|
-
*
|
|
617
|
-
* Retained rows are APPENDED after the incoming ones: `capStoredDiagnostics`
|
|
618
|
-
* fills the TUI's display list from the front, so a retained row never evicts a
|
|
619
|
-
* live finding from the rendered set.
|
|
620
|
-
*
|
|
621
|
-
* The tagged-row check comes FIRST so a project with no disposition marks — the
|
|
622
|
-
* overwhelmingly common case, and this runs on the per-edit write path — pays
|
|
623
|
-
* one predicate per stored row and never builds the identity set.
|
|
624
|
-
*/
|
|
625
|
-
function retainSuppressedRows(previous, incoming, filePath) {
|
|
626
|
-
const tagged = previous.filter((d) => d.disposition !== undefined);
|
|
627
|
-
if (tagged.length === 0)
|
|
628
|
-
return incoming;
|
|
629
|
-
const reported = new Map();
|
|
630
|
-
for (const d of incoming) {
|
|
631
|
-
const id = retentionIdentity(d);
|
|
632
|
-
const bucket = reported.get(id);
|
|
633
|
-
if (bucket)
|
|
634
|
-
bucket.push(d);
|
|
635
|
-
else
|
|
636
|
-
reported.set(id, [d]);
|
|
637
|
-
}
|
|
638
|
-
// #3183: the ONE file read on this path, and only for the state the coarse
|
|
639
|
-
// identity genuinely cannot resolve — a strict-anchored row whose identity an
|
|
640
|
-
// incoming finding collides with. Every other write (no marks at all, a mark
|
|
641
|
-
// with no collision, a weak `suppress` mark) stays zero-I/O, which is what
|
|
642
|
-
// keeps this off the per-edit `recordDiagnostics` cost.
|
|
643
|
-
const content = tagged.some((d) => d.anchorSpan !== undefined && reported.has(retentionIdentity(d)))
|
|
644
|
-
? readContentOrUndefined(filePath)
|
|
645
|
-
: undefined;
|
|
646
|
-
const spans = content === undefined ? undefined : strictAnchorSpansIn(content);
|
|
647
|
-
const retained = tagged.filter((d) => {
|
|
648
|
-
const collisions = reported.get(retentionIdentity(d));
|
|
649
|
-
// Nothing re-reported this finding, so retention rule 2 has nothing to say.
|
|
650
|
-
if (collisions === undefined)
|
|
651
|
-
return true;
|
|
652
|
-
// No span to ask (a weak `suppress` mark, or a row from an older build), or
|
|
653
|
-
// the file is unreadable: fall back to the pre-#3183 coarse rule. The safe
|
|
654
|
-
// direction is to UNDER-count, never to leave a phantom duplicate standing
|
|
655
|
-
// beside a live finding.
|
|
656
|
-
if (d.anchorSpan === undefined || spans === undefined)
|
|
657
|
-
return false;
|
|
658
|
-
// The mark stopped applying — its line is gone from the file — so the
|
|
659
|
-
// colliding finding IS this one, re-reported now that the filter no longer
|
|
660
|
-
// drops it (retirement rule 2, and the same verdict the sweep reaches).
|
|
661
|
-
if (!spans.has(d.anchorSpan))
|
|
662
|
-
return false;
|
|
663
|
-
// The mark still applies. Only a collision sitting ON the marked line is
|
|
664
|
-
// the same finding (its line number may have shifted under an edit — #3158
|
|
665
|
-
// round 2 F2); any other is a DIFFERENT occurrence this strict anchor never
|
|
666
|
-
// covered, and replacing the row with it loses a live mark (#3183).
|
|
667
|
-
return !collisions.some((i) => strictAnchorSpan(content, i.line) === d.anchorSpan);
|
|
668
|
-
});
|
|
669
|
-
if (retained.length > MAX_RETAINED_SUPPRESSED_PER_FILE) {
|
|
670
|
-
// Freshest observations win the cap. One record per FILE, never one per
|
|
671
|
-
// dropped row (AGENTS.md "bounded observability"): the chip under-counts
|
|
672
|
-
// by exactly `dropped` for this file until its content changes.
|
|
673
|
-
retained.sort((a, b) => (b.observedAt ?? 0) - (a.observedAt ?? 0));
|
|
674
|
-
recordDegradationOnce({
|
|
675
|
-
kind: "widget-suppressed-retention-capped",
|
|
676
|
-
subject: filePath,
|
|
677
|
-
reason: `${retained.length - MAX_RETAINED_SUPPRESSED_PER_FILE} suppressed row(s) beyond the ${MAX_RETAINED_SUPPRESSED_PER_FILE}-row per-file retention cap are no longer counted by the footer chip`,
|
|
678
|
-
});
|
|
679
|
-
retained.length = MAX_RETAINED_SUPPRESSED_PER_FILE;
|
|
680
|
-
}
|
|
681
|
-
return [
|
|
682
|
-
...incoming,
|
|
683
|
-
...retained.map((d) => d.suppressedRetained ? d : { ...d, suppressedRetained: true }),
|
|
684
|
-
];
|
|
685
|
-
}
|
|
686
|
-
/** Store `normalized` as the record's complete diagnostic set: recompute counts,
|
|
687
|
-
* cap the display list, stamp `touchedAt` (at `observedAt` when given, else now
|
|
688
|
-
* — #1093), persist, and re-render. The caller decides what `normalized`
|
|
689
|
-
* contains (a full replace, or a merge — see `reconcileCascadeNeighborLspErrors`). */
|
|
690
|
-
function commitDiagnostics(rec, filePath, incoming, observedAt, key = fileMapKey(filePath),
|
|
691
|
-
/**
|
|
692
|
-
* #3158: apply the suppressed-row retention rule to this write. True for
|
|
693
|
-
* every seam that hands in a fresh SCAN of the file (`recordDiagnostics` and
|
|
694
|
-
* both reconciles), false for `reconcileWidgetDisposition`, which re-maps the
|
|
695
|
-
* record's own current rows and is the seam that RETIRES a retained row —
|
|
696
|
-
* re-adding what it just dropped would make retirement rule 3 inert.
|
|
697
|
-
*/
|
|
698
|
-
retainSuppressed = true) {
|
|
699
|
-
const normalized = retainSuppressed
|
|
700
|
-
? retainSuppressedRows(rec.allDiagnostics, incoming, filePath)
|
|
701
|
-
: incoming;
|
|
702
|
-
rec.diagnosticCounts = countDiagnostics(normalized);
|
|
703
|
-
rec.diagnostics = capStoredDiagnostics(normalized);
|
|
704
|
-
rec.allDiagnostics = normalized;
|
|
705
|
-
rec.hasFinalDiagnosticsSnapshot = true;
|
|
706
|
-
// Record-level `touchedAt` is the FRESHEST per-entry observation in the merged
|
|
707
|
-
// set (#1186) — drives render recency and the empty-record stale gate. On a
|
|
708
|
-
// merge (`reconcileCascadeNeighborLspErrors`) this is the newest of the
|
|
709
|
-
// preserved + incoming entries, not the (possibly aging) incoming stamp, so a
|
|
710
|
-
// record holding a fresh preserved entry doesn't sort/gate as stale. Empty set
|
|
711
|
-
// falls back to the passed `observedAt` (or now).
|
|
712
|
-
rec.touchedAt = freshestObservation(normalized, observedAt ?? Date.now());
|
|
713
|
-
files.set(key, rec);
|
|
714
|
-
requestRender();
|
|
715
|
-
}
|
|
716
|
-
export function classifyDiagnosticTier(d) {
|
|
717
|
-
if (d.severity === "error")
|
|
718
|
-
return "error";
|
|
719
|
-
if (d.severity === "warning")
|
|
720
|
-
return "warning";
|
|
721
|
-
if (d.severity === "hint" || d.severity === "info")
|
|
722
|
-
return "advisory";
|
|
723
|
-
return undefined;
|
|
724
|
-
}
|
|
725
|
-
/** Recompute the {blocking, errors, warnings} tally for a diagnostic set.
|
|
726
|
-
* `hint`/`info` tier findings are tallied separately — see
|
|
727
|
-
* {@link classifyDiagnosticTier} and {@link countAdvisories} — and never
|
|
728
|
-
* inflate `warnings` (#2414). */
|
|
729
|
-
function countDiagnostics(diags) {
|
|
730
|
-
let blocking = 0;
|
|
731
|
-
let errors = 0;
|
|
732
|
-
let warnings = 0;
|
|
733
|
-
for (const diagnostic of diags) {
|
|
734
|
-
if (diagnostic.disposition)
|
|
735
|
-
continue;
|
|
736
|
-
if (isBlocking(diagnostic))
|
|
737
|
-
blocking++;
|
|
738
|
-
// A past-EOF stale entry keeps its severity for display purposes but is
|
|
739
|
-
// excluded from the error/warning tallies alongside blocking — its cited
|
|
740
|
-
// coordinate is no longer trustworthy, same reasoning as `isBlocking`.
|
|
741
|
-
// A dependency-drift demotion (#1631 criterion 3) is different: the
|
|
742
|
-
// finding itself is still real evidence, only its BLOCKING authority is
|
|
743
|
-
// revoked until re-confirmed, so it stays in the error/warning tally.
|
|
744
|
-
if (diagnostic.stale &&
|
|
745
|
-
(diagnostic.staleReason ?? "past-eof") === "past-eof")
|
|
746
|
-
continue;
|
|
747
|
-
const tier = classifyDiagnosticTier(diagnostic);
|
|
748
|
-
if (tier === "error")
|
|
749
|
-
errors++;
|
|
750
|
-
else if (tier === "warning")
|
|
751
|
-
warnings++;
|
|
752
|
-
// tier === "advisory" (hint/info): intentionally excluded from the
|
|
753
|
-
// footer's warning tally (#2414) — see `countAdvisories` for the
|
|
754
|
-
// parallel count `getFileDiagnosticSummaries` exposes to
|
|
755
|
-
// `lens_diagnostics` so a hint-only file still shows up as "has issues".
|
|
756
|
-
}
|
|
757
|
-
return { blocking, errors, warnings };
|
|
758
|
-
}
|
|
759
|
-
/** Count `hint`/`info` tier findings using the same past-EOF exclusion as
|
|
760
|
-
* {@link countDiagnostics} (#2414). Kept as a standalone accessor rather than
|
|
761
|
-
* a new `FileRecord.diagnosticCounts` field — the footer never renders this
|
|
762
|
-
* number, only `getFileDiagnosticSummaries` (mode=all) needs it, to keep a
|
|
763
|
-
* hint-only file from being silently dropped from a detailed listing. */
|
|
764
|
-
function countAdvisories(diags) {
|
|
765
|
-
let advisories = 0;
|
|
766
|
-
for (const diagnostic of diags) {
|
|
767
|
-
if (diagnostic.stale &&
|
|
768
|
-
(diagnostic.staleReason ?? "past-eof") === "past-eof")
|
|
769
|
-
continue;
|
|
770
|
-
if (classifyDiagnosticTier(diagnostic) === "advisory")
|
|
771
|
-
advisories++;
|
|
772
|
-
}
|
|
773
|
-
return advisories;
|
|
774
|
-
}
|
|
775
|
-
/**
|
|
776
|
-
* Apply the #1641 past-EOF gate to `rec` in place: demote (never drop) any
|
|
777
|
-
* stored diagnostic whose cited line exceeds the file's CURRENT on-disk line
|
|
778
|
-
* count, then recompute the capped/full diagnostic lists and the counts so
|
|
779
|
-
* every reader (the TUI render loop, `getFileDiagnosticSummaries`,
|
|
780
|
-
* `getFileDiagnostics`) sees one consistent, already-gated record instead of
|
|
781
|
-
* each re-deriving its own verdict. Cheap on the common case: one memoized
|
|
782
|
-
* stat per record (see `getCachedLineCount`), a full recount only when the
|
|
783
|
-
* file's mtime moved since the last check.
|
|
784
|
-
*
|
|
785
|
-
* No `resync` callback here deliberately: this gate runs on the TUI's render
|
|
786
|
-
* path and the bus-publish/mark-tool read accessors, all of which fire far
|
|
787
|
-
* more often than an agent's own edit/tool cadence. Triggering a document
|
|
788
|
-
* resync from every one of those reads would storm the LSP with didOpen calls
|
|
789
|
-
* for a file whose drift hasn't yet been fixed. `tools/lens-diagnostics.ts`'s
|
|
790
|
-
* `formatAllMode` — an explicit, agent-invoked tool call — is the resync
|
|
791
|
-
* trigger point (#1641 criterion 2); this gate still demotes/logs on its own.
|
|
792
|
-
*
|
|
793
|
-
* The gate RE-DERIVES every entry's `stale` flag on every call — it is never
|
|
794
|
-
* a one-way latch. A transient shrink that later restores (truncate-then-
|
|
795
|
-
* write, a formatter pass, a checkout) un-demotes on its own next read, so
|
|
796
|
-
* this always persists the freshly-derived array back into the record, not
|
|
797
|
-
* only when `demotedCount` (which counts RISING edges only, for telemetry)
|
|
798
|
-
* is nonzero.
|
|
799
|
-
*/
|
|
800
|
-
function applyPastEofGate(rec, lineCountCache) {
|
|
801
|
-
if (rec.allDiagnostics.length === 0)
|
|
802
|
-
return;
|
|
803
|
-
const { diagnostics } = demotePastEofDiagnostics({
|
|
804
|
-
store: "widget-state",
|
|
805
|
-
cwd: process.cwd(),
|
|
806
|
-
filePath: rec.filePath,
|
|
807
|
-
diagnostics: rec.allDiagnostics,
|
|
808
|
-
lineCountCache,
|
|
809
|
-
});
|
|
810
|
-
rec.allDiagnostics = diagnostics;
|
|
811
|
-
rec.diagnostics = capStoredDiagnostics(diagnostics);
|
|
812
|
-
rec.diagnosticCounts = countDiagnostics(diagnostics);
|
|
813
|
-
}
|
|
814
|
-
/** The newest per-entry `observedAt` in `diags`, or `fallback` when empty (or no
|
|
815
|
-
* entry carries a stamp). Used as the record-level `touchedAt` (#1186). */
|
|
816
|
-
function freshestObservation(diags, fallback) {
|
|
817
|
-
let newest;
|
|
818
|
-
for (const d of diags) {
|
|
819
|
-
if (d.observedAt != null &&
|
|
820
|
-
(newest === undefined || d.observedAt > newest)) {
|
|
821
|
-
newest = d.observedAt;
|
|
822
|
-
}
|
|
823
|
-
}
|
|
824
|
-
return newest ?? fallback;
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* A stored diagnostic that came from a language SERVER (tsserver, pyright, …)
|
|
828
|
-
* AND is an error/blocking finding. `convertLspDiagnostics` tags every
|
|
829
|
-
* language-server diagnostic with `tool: "lsp"` and `retagAuxiliaryDiagnostics`
|
|
830
|
-
* re-tags auxiliary-LSP findings (opengrep/ast-grep/zizmor/typos) to their real
|
|
831
|
-
* tool id, so `tool === "lsp"` uniquely identifies a genuine language-server
|
|
832
|
-
* entry. Used by the cascade merge below to decide which existing entries an
|
|
833
|
-
* errors-only LSP re-check is entitled to replace.
|
|
834
|
-
*/
|
|
835
|
-
function isLspErrorEntry(d) {
|
|
836
|
-
return (d.tool === "lsp" && (d.semantic === "blocking" || d.severity === "error"));
|
|
837
|
-
}
|
|
838
|
-
/**
|
|
839
|
-
* Reconcile a CONFIRMED cascade neighbor re-check (#1093) into the footer,
|
|
840
|
-
* MERGING rather than whole-replacing (unlike `recordDiagnostics`).
|
|
841
|
-
*
|
|
842
|
-
* The cascade only re-checks a neighbor through its LANGUAGE SERVER, and only
|
|
843
|
-
* for ERRORS (`severity === 1`; see `clients/dispatch/integration.ts`). A plain
|
|
844
|
-
* full-replace would therefore erase the neighbor's live findings from OTHER
|
|
845
|
-
* sources that the cascade never re-examined — biome/ruff/ast-grep runner
|
|
846
|
-
* findings, and even the language server's own WARNINGS — turning a
|
|
847
|
-
* cross-file-error re-check into a silent false-clean for everything else
|
|
848
|
-
* (#533). So we replace ONLY the existing LSP-error entries
|
|
849
|
-
* (`isLspErrorEntry`) with the cascade's fresh LSP errors and preserve
|
|
850
|
-
* everything else verbatim. A stale LSP warning or biome finding therefore
|
|
851
|
-
* survives an errors-only cascade — correct, because this check never looked at
|
|
852
|
-
* it; it self-corrects on the next per-edit dispatch or a `lens_diagnostics`
|
|
853
|
-
* scan (which DO re-examine every source).
|
|
854
|
-
*
|
|
855
|
-
* Only ever call this for a CONFIRMED result (a valid passive snapshot or a
|
|
856
|
-
* completed, NON-inconclusive active touch — #571). `writeIndex` and
|
|
857
|
-
* `observedAt` behave exactly as in `recordDiagnostics`.
|
|
858
|
-
*/
|
|
859
|
-
export function reconcileCascadeNeighborLspErrors(filePath, lspErrorDiagnostics, writeIndex, observedAt) {
|
|
860
|
-
const key = fileMapKey(filePath);
|
|
861
|
-
if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
|
|
862
|
-
return;
|
|
863
|
-
runnerWriteGuard.shouldWrite(key, writeIndex);
|
|
864
|
-
// #1186: the INCOMING LSP errors are stamped at THIS observation time
|
|
865
|
-
// (`observedAt`, e.g. an aging passive snapshot's `entry.ts`, or now for a
|
|
866
|
-
// fresh active touch). The PRESERVED entries keep their OWN prior per-entry
|
|
867
|
-
// `observedAt` — a fresh per-edit finding preserved through this errors-only
|
|
868
|
-
// merge is NOT re-aged to the incoming stamp. That per-entry split is exactly
|
|
869
|
-
// what lets `reconcileStaleWidgetFiles` drop the stale incoming entry while
|
|
870
|
-
// keeping the newer preserved one, instead of dropping the whole record.
|
|
871
|
-
const observedTs = observedAt ?? Date.now();
|
|
872
|
-
const rec = getOrCreate(filePath);
|
|
873
|
-
const incoming = normalizeDiagnostics(filePath, lspErrorDiagnostics, observedTs);
|
|
874
|
-
const preserved = rec.allDiagnostics.filter((d) => !isLspErrorEntry(d));
|
|
875
|
-
commitDiagnostics(rec, filePath, [...incoming, ...preserved], observedTs);
|
|
876
|
-
}
|
|
877
|
-
/**
|
|
878
|
-
* Reconcile a diagnostics result obtained OUTSIDE the per-edit dispatch
|
|
879
|
-
* pipeline — a `lens_diagnostics` mode=full workspace scan, or a standalone
|
|
880
|
-
* `lsp_diagnostics` on-demand check — into the footer cache (#571).
|
|
881
|
-
*
|
|
882
|
-
* `recordDiagnostics` is otherwise only reachable from `pipeline.ts`'s
|
|
883
|
-
* per-edit dispatch, so a file that becomes stale/fresh purely because of a
|
|
884
|
-
* change to some OTHER file it depends on (and is never itself re-edited
|
|
885
|
-
* through pi-lens) has no path to correct the footer — a full scan proves
|
|
886
|
-
* the fresher truth but had nowhere to put it. This is that path, shared by
|
|
887
|
-
* both call sites so there's exactly one place that decides whether a scan
|
|
888
|
-
* result is trustworthy enough to write.
|
|
889
|
-
*
|
|
890
|
-
* `confirmed` MUST be false for any result the caller can't vouch for — a
|
|
891
|
-
* timed-out/inconclusive LSP check (see #570) must never present as
|
|
892
|
-
* "confirmed clean" in the footer, and must not clobber a real prior
|
|
893
|
-
* confirmed-dirty entry either. Non-confirmed results are silently skipped,
|
|
894
|
-
* leaving whatever the footer already had (stale-but-real beats
|
|
895
|
-
* fresh-but-fabricated).
|
|
896
|
-
*
|
|
897
|
-
* `writeIndex` should be a freshly-drawn token from the same monotonic
|
|
898
|
-
* source the per-edit pipeline uses (`RuntimeCoordinator.nextWriteIndex()`)
|
|
899
|
-
* so `recordDiagnostics`'s existing `WriteOrderingGuard` (#555) can tell a
|
|
900
|
-
* scan-originated write apart from a concurrent, genuinely newer per-edit
|
|
901
|
-
* write for the same file — an omitted `writeIndex` always proceeds (same
|
|
902
|
-
* version-less fallback `recordDiagnostics` already documents), which is
|
|
903
|
-
* only safe for callers with no ordering token to give (e.g. tests).
|
|
904
|
-
*
|
|
905
|
-
* `observedAt` (#1093) is the wall-clock time the diagnostics were actually
|
|
906
|
-
* OBSERVED — pass it whenever the reconciled result is a replay of an older
|
|
907
|
-
* CACHED observation (the workspace-diagnostics cache-hit branch), so
|
|
908
|
-
* `touchedAt` records when the truth was seen, not when it was written. Omit
|
|
909
|
-
* it for genuinely fresh observations (a just-completed touch/scan), which are
|
|
910
|
-
* observed now.
|
|
911
|
-
*/
|
|
912
|
-
export function reconcileScanDiagnostics(filePath, diagnostics, confirmed, writeIndex, observedAt) {
|
|
913
|
-
if (!confirmed)
|
|
914
|
-
return false;
|
|
915
|
-
return recordDiagnostics(filePath, diagnostics, writeIndex, observedAt);
|
|
916
|
-
}
|
|
917
|
-
/**
|
|
918
|
-
* Commit the already-correlated result of a full diagnostics scan.
|
|
919
|
-
*
|
|
920
|
-
* Unlike {@link reconcileScanDiagnostics}, this seam accepts widget diagnostics
|
|
921
|
-
* that have already been merged across producing lanes. `lens_diagnostics`
|
|
922
|
-
* uses it after correlating the confirmed LSP sweep, the cheap project scan,
|
|
923
|
-
* and retained widget rows. This prevents a broken LSP lane from hiding
|
|
924
|
-
* independently-produced `ast-grep-napi` findings in the widget count (#1888).
|
|
925
|
-
* Existing per-entry observation times survive the merge; newly-added project
|
|
926
|
-
* rows inherit the scan's observation time.
|
|
927
|
-
*/
|
|
928
|
-
export function reconcileCorrelatedScanDiagnostics(filePath, diagnostics, writeIndex, observedAt) {
|
|
929
|
-
const key = fileMapKey(filePath);
|
|
930
|
-
if (!diagnosticsWriteGuard.shouldWrite(key, writeIndex))
|
|
931
|
-
return;
|
|
932
|
-
runnerWriteGuard.shouldWrite(key, writeIndex);
|
|
933
|
-
const observedTs = observedAt ?? Date.now();
|
|
934
|
-
const correlated = diagnostics.map((diagnostic) => ({
|
|
935
|
-
...diagnostic,
|
|
936
|
-
observedAt: diagnostic.observedAt ?? observedTs,
|
|
937
|
-
}));
|
|
938
|
-
commitDiagnostics(getOrCreate(filePath, key), filePath, correlated, observedTs, key);
|
|
939
|
-
}
|
|
940
|
-
/**
|
|
941
|
-
* Drop widget entries whose file changed on disk after pi-lens last recorded
|
|
942
|
-
* them (`mtimeMs > touchedAt` → the recorded diagnostics predate the current
|
|
943
|
-
* content → stale) or that no longer exist. Keeps `lens_diagnostics` from
|
|
944
|
-
* surfacing findings the agent already fixed (or that an external edit
|
|
945
|
-
* invalidated). Async with concurrent stats — call on read, never on the typing
|
|
946
|
-
* path. Returns how many entries were dropped (so callers can tell the agent
|
|
947
|
-
* those files changed and need a `mode=full` rescan rather than reading as
|
|
948
|
-
* clean).
|
|
949
|
-
*/
|
|
950
|
-
export async function reconcileStaleWidgetFiles() {
|
|
951
|
-
const entries = [...files.entries()];
|
|
952
|
-
const verdicts = await Promise.all(
|
|
953
|
-
// `mapKey` is the normalized `files` key (used for deletion); stat the
|
|
954
|
-
// record's real display path, not the lowercased key (#1020).
|
|
955
|
-
entries.map(async ([mapKey, rec]) => {
|
|
956
|
-
let mtimeMs;
|
|
957
|
-
try {
|
|
958
|
-
mtimeMs = (await stat(rec.filePath)).mtimeMs;
|
|
959
|
-
}
|
|
960
|
-
catch {
|
|
961
|
-
return { mapKey, action: "drop" }; // deleted / unreadable → drop
|
|
962
|
-
}
|
|
963
|
-
// A clean record (no findings) has no per-entry stamps to consult —
|
|
964
|
-
// gate it on the record's own `touchedAt` exactly as before, so a ✓
|
|
965
|
-
// entry for a file that changed on disk still drops.
|
|
966
|
-
//
|
|
967
|
-
// #1631 review F11 follow-up: a THIRD site hitting the same Windows
|
|
968
|
-
// mtime-vs-`Date.now()` skew F2 named for `blocker-freshness.ts` and
|
|
969
|
-
// `reconcileStaleWidgetDependencyBlockers` — a file's mtime can lead
|
|
970
|
-
// `Date.now()` by up to ~11.4ms measured (#1491/#1498 precedent), so a
|
|
971
|
-
// record touched immediately after its own write dropped here at +1ms
|
|
972
|
-
// with zero real drift. Found investigating F11: `lens-diagnostics-
|
|
973
|
-
// mode-all-freshness.test.ts`'s "control" case (and its sibling) failed
|
|
974
|
-
// under this gate, not from cross-test state. Same tolerance, same
|
|
975
|
-
// constant, for the same reason.
|
|
976
|
-
if (rec.allDiagnostics.length === 0) {
|
|
977
|
-
return freshnessFromMtime({ mtimeMs, referenceMs: rec.touchedAt })
|
|
978
|
-
.verdict === "stale"
|
|
979
|
-
? { mapKey, action: "drop" }
|
|
980
|
-
: { mapKey, action: "keep" };
|
|
981
|
-
}
|
|
982
|
-
// #1186 per-ENTRY gate: drop only the entries observed BEFORE the file's
|
|
983
|
-
// current mtime; keep the rest. A merged record can hold a fresh
|
|
984
|
-
// preserved entry beside an entry replayed from an aging snapshot, so a
|
|
985
|
-
// per-RECORD gate over-cleared the whole record (the residual documented
|
|
986
|
-
// at dispatch/integration.ts). A missing per-entry stamp (a migrated
|
|
987
|
-
// pre-#1186 record) inherits the record's `touchedAt`. Tolerance matches
|
|
988
|
-
// the Windows host-clock skew rationale above.
|
|
989
|
-
const mtimeStale = (d) => freshnessFromMtime({
|
|
990
|
-
mtimeMs,
|
|
991
|
-
referenceMs: d.observedAt ?? rec.touchedAt,
|
|
992
|
-
}).verdict === "stale";
|
|
993
|
-
// #3183: a disposition-tagged row carrying a strict anchor span is gated
|
|
994
|
-
// on its MARK, not on the file's mtime (retirement rule 1 — see
|
|
995
|
-
// `WidgetDiagnostic.suppressedRetained`): it survives while the marked
|
|
996
|
-
// LINE is still somewhere in the file, and is retired when it is not.
|
|
997
|
-
// The content read is gated on a span-carrying row the mtime gate WOULD
|
|
998
|
-
// have dropped — the only state where reading can change an outcome — so
|
|
999
|
-
// the debounced render sweep stays off the disk both for a file with no
|
|
1000
|
-
// marks and for a marked file nothing has touched.
|
|
1001
|
-
const spans = rec.allDiagnostics.some((d) => d.anchorSpan !== undefined && mtimeStale(d))
|
|
1002
|
-
? readAnchorSpans(rec.filePath)
|
|
1003
|
-
: undefined;
|
|
1004
|
-
const survivors = rec.allDiagnostics.filter((d) => d.anchorSpan !== undefined && spans !== undefined
|
|
1005
|
-
? spans.has(d.anchorSpan)
|
|
1006
|
-
: !mtimeStale(d));
|
|
1007
|
-
if (survivors.length === rec.allDiagnostics.length) {
|
|
1008
|
-
return { mapKey, action: "keep" }; // nothing stale
|
|
1009
|
-
}
|
|
1010
|
-
if (survivors.length === 0) {
|
|
1011
|
-
return { mapKey, action: "drop" }; // every entry stale → drop record
|
|
1012
|
-
}
|
|
1013
|
-
return { mapKey, action: "prune", survivors };
|
|
1014
|
-
}));
|
|
1015
|
-
let dropped = 0;
|
|
1016
|
-
for (const v of verdicts) {
|
|
1017
|
-
if (v.action === "keep")
|
|
1018
|
-
continue;
|
|
1019
|
-
if (v.action === "drop") {
|
|
1020
|
-
files.delete(v.mapKey);
|
|
1021
|
-
dropped += 1;
|
|
1022
|
-
continue;
|
|
1023
|
-
}
|
|
1024
|
-
// prune: the file changed and shed its stale entries but retains fresher
|
|
1025
|
-
// ones — keep the record, recompute counts/cap from the survivors, and
|
|
1026
|
-
// still count it as a changed file so the agent is told to rescan.
|
|
1027
|
-
const rec = files.get(v.mapKey);
|
|
1028
|
-
if (rec) {
|
|
1029
|
-
rec.allDiagnostics = v.survivors;
|
|
1030
|
-
rec.diagnostics = capStoredDiagnostics(v.survivors);
|
|
1031
|
-
rec.diagnosticCounts = countDiagnostics(v.survivors);
|
|
1032
|
-
}
|
|
1033
|
-
dropped += 1;
|
|
1034
|
-
}
|
|
1035
|
-
if (dropped > 0)
|
|
1036
|
-
requestRenderFn?.();
|
|
1037
|
-
return dropped;
|
|
1038
|
-
}
|
|
1039
|
-
export async function reconcileStaleWidgetDependencyBlockers(cwd, turnIndex) {
|
|
1040
|
-
let demoted = 0;
|
|
1041
|
-
let truncatedImports = 0;
|
|
1042
|
-
for (const [, rec] of files.entries()) {
|
|
1043
|
-
// #1631 review F4: narrow demotion to LSP-sourced findings. The import-closure
|
|
1044
|
-
// drift check only knows how to reason about IMPORT graphs — an ast-grep
|
|
1045
|
-
// hardcoded-secret or a govulncheck CVE finding doesn't stop being true because
|
|
1046
|
-
// a file this diagnostic's file imports changed; only a language-server verdict
|
|
1047
|
-
// (`tool === "lsp"`, the same predicate `isLspErrorEntry` above uses) is actually
|
|
1048
|
-
// invalidated by that shape of drift. Documented trade: a non-LSP blocking
|
|
1049
|
-
// finding on a file with drifted imports stays fully authoritative until its own
|
|
1050
|
-
// content changes or it is explicitly re-run.
|
|
1051
|
-
if (!rec.allDiagnostics.some((d) => isBlocking(d) && d.tool === "lsp"))
|
|
1052
|
-
continue;
|
|
1053
|
-
let importMtimes = [];
|
|
1054
|
-
try {
|
|
1055
|
-
const result = await collectForwardImportMtimes(cwd, rec.filePath, undefined, turnIndex);
|
|
1056
|
-
importMtimes = result.mtimes;
|
|
1057
|
-
if (result.truncated)
|
|
1058
|
-
truncatedImports += 1;
|
|
1059
|
-
}
|
|
1060
|
-
catch {
|
|
1061
|
-
importMtimes = [];
|
|
1062
|
-
}
|
|
1063
|
-
if (importMtimes.length === 0)
|
|
1064
|
-
continue;
|
|
1065
|
-
let changed = false;
|
|
1066
|
-
for (const d of rec.allDiagnostics) {
|
|
1067
|
-
if (!isBlocking(d) || d.tool !== "lsp")
|
|
1068
|
-
continue;
|
|
1069
|
-
const baseline = d.observedAt ?? rec.touchedAt;
|
|
1070
|
-
// +50ms tolerance (#1631 review F2): a whole-millisecond `Date.now()`
|
|
1071
|
-
// baseline vs. sub-millisecond mtime precision only needs +1ms, but on
|
|
1072
|
-
// Windows a file's mtime can LEAD `Date.now()` by up to ~11.4ms (measured
|
|
1073
|
-
// across 200 writes; #1491/#1498 precedent for the same host-clock skew).
|
|
1074
|
-
// +1ms produced 42 false demotions in 50 runs on Windows; +50ms clears the
|
|
1075
|
-
// measured skew while staying far below the gap between real edits.
|
|
1076
|
-
if (importMtimes.some((im) => freshnessFromMtime({
|
|
1077
|
-
mtimeMs: im.mtimeMs,
|
|
1078
|
-
referenceMs: baseline,
|
|
1079
|
-
}).verdict === "stale")) {
|
|
1080
|
-
d.stale = true;
|
|
1081
|
-
d.staleReason = "dependency-drift";
|
|
1082
|
-
changed = true;
|
|
1083
|
-
demoted += 1;
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
if (changed) {
|
|
1087
|
-
rec.diagnosticCounts = countDiagnostics(rec.allDiagnostics);
|
|
1088
|
-
rec.diagnostics = capStoredDiagnostics(rec.allDiagnostics);
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
if (demoted > 0)
|
|
1092
|
-
requestRenderFn?.();
|
|
1093
|
-
return { demoted, truncatedImports };
|
|
1094
|
-
}
|
|
1095
|
-
/**
|
|
1096
|
-
* #1790: sweep-population feed for `sweepInlineBlockerFreshness` (the turn-end
|
|
1097
|
-
* gate in `clients/blocker-freshness.ts`). That sweep's population was built
|
|
1098
|
-
* solely from `RuntimeCoordinator`'s inline-blocker map — a live-dispatch-only
|
|
1099
|
-
* store. A workspace-diagnostics CACHE HIT never touches that map; it calls
|
|
1100
|
-
* `reconcileScanDiagnostics` straight into `files` here (the cache-serve branch
|
|
1101
|
-
* in `tools/lsp-diagnostics.ts`), so a cache-served blocking row could render in
|
|
1102
|
-
* the widget while the turn-end sweep's `total` count never saw it (the live
|
|
1103
|
-
* 2026-08-20 dogfood: `total:1 kept:1` against five stale cache-served blocking
|
|
1104
|
-
* rows on screen).
|
|
1105
|
-
*
|
|
1106
|
-
* One entry per file with at least one CURRENTLY blocking (non-stale),
|
|
1107
|
-
* LSP-sourced diagnostic — the same provenance narrowing
|
|
1108
|
-
* `reconcileStaleWidgetDependencyBlockers` already applies, so every row this
|
|
1109
|
-
* feeds into the sweep is one its `isLspSourced` gate would keep anyway.
|
|
1110
|
-
* `recordedAtMs` is the EARLIEST `observedAt` among them: the conservative
|
|
1111
|
-
* baseline, since using the latest could hide drift that predates an earlier
|
|
1112
|
-
* diagnostic's own observation.
|
|
1113
|
-
*/
|
|
1114
|
-
export function getWidgetBlockingFilesForSweep() {
|
|
1115
|
-
const out = [];
|
|
1116
|
-
for (const rec of files.values()) {
|
|
1117
|
-
let earliest;
|
|
1118
|
-
for (const d of rec.allDiagnostics) {
|
|
1119
|
-
if (!isBlocking(d) || d.tool !== "lsp")
|
|
1120
|
-
continue;
|
|
1121
|
-
const observedAt = d.observedAt ?? rec.touchedAt;
|
|
1122
|
-
if (earliest === undefined || observedAt < earliest)
|
|
1123
|
-
earliest = observedAt;
|
|
1124
|
-
}
|
|
1125
|
-
if (earliest !== undefined)
|
|
1126
|
-
out.push({ filePath: rec.filePath, recordedAtMs: earliest });
|
|
1127
|
-
}
|
|
1128
|
-
return out;
|
|
1129
|
-
}
|
|
1130
|
-
/**
|
|
1131
|
-
* #1790: demote every currently-blocking, LSP-sourced diagnostic for `filePath`
|
|
1132
|
-
* to stale (#1419 demote-not-drop). Called by the turn-end sweep once its own
|
|
1133
|
-
* drift check (`detectDrift`/`collectForwardImportMtimes`, shared with the
|
|
1134
|
-
* inline-blocker branch) independently confirms drift for a file that reached
|
|
1135
|
-
* the sweep's population through `getWidgetBlockingFilesForSweep` rather than an
|
|
1136
|
-
* inline blocker. Same store and same write `reconcileStaleWidgetDependencyBlockers`
|
|
1137
|
-
* already performs per-diagnostic — this is the single-file entry point the
|
|
1138
|
-
* sweep drives once ITS check (not a second implementation of the check) says
|
|
1139
|
-
* to. Returns true iff something changed.
|
|
1140
|
-
*
|
|
1141
|
-
* #1790 review F4 (latent): unlike `reconcileStaleWidgetDependencyBlockers`, which
|
|
1142
|
-
* checks drift per-diagnostic against EACH diagnostic's own `observedAt`, this
|
|
1143
|
-
* demotes every currently-blocking LSP diagnostic for the file against the ONE
|
|
1144
|
-
* file-level baseline `getWidgetBlockingFilesForSweep` computed (the earliest
|
|
1145
|
-
* `observedAt` among them) — because the sweep's `detectDrift` contract is
|
|
1146
|
-
* file-level, not per-diagnostic. That is safe ONLY as long as every LSP
|
|
1147
|
-
* diagnostic on a `FileRecord` shares (or is more conservative than) that
|
|
1148
|
-
* earliest stamp, which holds today because every writer that populates
|
|
1149
|
-
* `allDiagnostics` (`recordDiagnostics`, `reconcileCascadeNeighborLspErrors`)
|
|
1150
|
-
* replaces the file's LSP errors wholesale with one `observedAt` per write. If a
|
|
1151
|
-
* future writer ever merges LSP diagnostics with genuinely MIXED per-entry
|
|
1152
|
-
* stamps on one record, this function would over-demote entries observed after
|
|
1153
|
-
* the earliest one drifted — re-derive per-diagnostic (mirroring
|
|
1154
|
-
* `reconcileStaleWidgetDependencyBlockers`) rather than assume the single
|
|
1155
|
-
* baseline still holds.
|
|
1156
|
-
*/
|
|
1157
|
-
export function markWidgetFileBlockersStale(filePath, reason) {
|
|
1158
|
-
const rec = files.get(fileMapKey(filePath));
|
|
1159
|
-
if (!rec)
|
|
1160
|
-
return false;
|
|
1161
|
-
let changed = false;
|
|
1162
|
-
for (const d of rec.allDiagnostics) {
|
|
1163
|
-
if (!isBlocking(d) || d.tool !== "lsp")
|
|
1164
|
-
continue;
|
|
1165
|
-
d.stale = true;
|
|
1166
|
-
d.staleReason = reason;
|
|
1167
|
-
changed = true;
|
|
1168
|
-
}
|
|
1169
|
-
if (changed) {
|
|
1170
|
-
rec.diagnosticCounts = countDiagnostics(rec.allDiagnostics);
|
|
1171
|
-
rec.diagnostics = capStoredDiagnostics(rec.allDiagnostics);
|
|
1172
|
-
requestRenderFn?.();
|
|
1173
|
-
}
|
|
1174
|
-
return changed;
|
|
1175
|
-
}
|
|
1176
|
-
/**
|
|
1177
|
-
* #2275: a dependency-drift demotion the footer is still allowed to draw —
|
|
1178
|
-
* the one predicate the render marker, the delivery counter and the
|
|
1179
|
-
* footer-hide all ask, so "which rows does this cap govern" cannot drift
|
|
1180
|
-
* apart between them.
|
|
1181
|
-
*/
|
|
1182
|
-
function isDependencyDriftDemoted(d) {
|
|
1183
|
-
return (d.stale === true &&
|
|
1184
|
-
d.staleReason === "dependency-drift" &&
|
|
1185
|
-
d.footerRetired !== true);
|
|
1186
|
-
}
|
|
1187
|
-
/**
|
|
1188
|
-
* #2275 review F1: take (and clear) the files whose dependency-drift
|
|
1189
|
-
* demotions the footer actually RENDERED since the last call — the delivery
|
|
1190
|
-
* population `runtime-turn.ts`'s per-turn cap step walks at turn end.
|
|
1191
|
-
*
|
|
1192
|
-
* Deliberately NOT a store-wide query over everything currently demoted.
|
|
1193
|
-
* `renderWidget` serves one record per pass, so a store-wide walk charged a
|
|
1194
|
-
* delivery to every demoted file every turn — including files the footer had
|
|
1195
|
-
* never shown and one demoted moments earlier by the same turn end, which
|
|
1196
|
-
* also made the ledger's "capped after N deliveries" overstate N by one.
|
|
1197
|
-
* Draining here is what makes many repaints within one turn count as the one
|
|
1198
|
-
* delivery they are, and a turn with no repaint count as none.
|
|
1199
|
-
*/
|
|
1200
|
-
export function drainRenderedDependencyDriftFilePaths() {
|
|
1201
|
-
const out = [...renderedDependencyDriftFiles];
|
|
1202
|
-
renderedDependencyDriftFiles.clear();
|
|
1203
|
-
return out;
|
|
1204
|
-
}
|
|
1205
|
-
/**
|
|
1206
|
-
* #2275: commit one more RENDERED delivery of `filePath`'s
|
|
1207
|
-
* dependency-drift-demoted diagnostics. Every qualifying entry on the record
|
|
1208
|
-
* advances together — `markWidgetFileBlockersStale` demotes them as one
|
|
1209
|
-
* batch per file, so they share one delivery history, mirroring that
|
|
1210
|
-
* write's own file-level scope. Returns the new count, or 0 when the file
|
|
1211
|
-
* has nothing this cap governs (never demoted, or already footer-retired —
|
|
1212
|
-
* a hidden row is delivered no more, so it must stop advancing).
|
|
1213
|
-
*/
|
|
1214
|
-
export function incrementWidgetDependencyDriftDelivery(filePath) {
|
|
1215
|
-
const rec = files.get(fileMapKey(filePath));
|
|
1216
|
-
if (!rec)
|
|
1217
|
-
return 0;
|
|
1218
|
-
let next = 0;
|
|
1219
|
-
for (const d of rec.allDiagnostics) {
|
|
1220
|
-
if (isDependencyDriftDemoted(d)) {
|
|
1221
|
-
next = (d.staleDeliveryCount ?? 0) + 1;
|
|
1222
|
-
d.staleDeliveryCount = next;
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
return next;
|
|
1226
|
-
}
|
|
1227
|
-
/**
|
|
1228
|
-
* #2275 review F2: retire every dependency-drift-demoted diagnostic on
|
|
1229
|
-
* `filePath` once its delivery cap is reached — by HIDING it from the
|
|
1230
|
-
* footer, not by dropping it.
|
|
1231
|
-
*
|
|
1232
|
-
* `RuntimeCoordinator.retireDemotedDependencyDriftBlocker` can delete its
|
|
1233
|
-
* whole `_pendingInlineBlockers` record because that store backs exactly one
|
|
1234
|
-
* surface (the turn-end advisory text). This store does not: `allDiagnostics`
|
|
1235
|
-
* is also what `getFileDiagnosticSummaries` serves to `lens_diagnostics
|
|
1236
|
-
* mode=all` and what `lens_diagnostic_mark` cross-checks to reanchor. Removing
|
|
1237
|
-
* the entry there turned an unconfirmed LSP error into a CLEAN read and took
|
|
1238
|
-
* it out of the error tally #1631 requires it to stay in. So the cap marks
|
|
1239
|
-
* `footerRetired` and leaves everything else — severity, counts, the
|
|
1240
|
-
* `stale`/`dependency-drift` demotion — exactly as it was; mode=all carries
|
|
1241
|
-
* the "capped, still confirmable" wording in its own note. Returns true iff
|
|
1242
|
-
* something changed (idempotent on a second call).
|
|
1243
|
-
*/
|
|
1244
|
-
export function retireWidgetDependencyDriftBlockers(filePath) {
|
|
1245
|
-
const rec = files.get(fileMapKey(filePath));
|
|
1246
|
-
if (!rec)
|
|
1247
|
-
return false;
|
|
1248
|
-
let changed = false;
|
|
1249
|
-
for (const d of rec.allDiagnostics) {
|
|
1250
|
-
if (isDependencyDriftDemoted(d)) {
|
|
1251
|
-
d.footerRetired = true;
|
|
1252
|
-
changed = true;
|
|
1253
|
-
}
|
|
1254
|
-
}
|
|
1255
|
-
if (!changed)
|
|
1256
|
-
return false;
|
|
1257
|
-
// The record's severity tallies are unchanged by design (the finding is
|
|
1258
|
-
// still an error, still non-blocking) — only what the footer draws moves.
|
|
1259
|
-
// Fix-round 3 (#2275 review F1): this delete is live only for the
|
|
1260
|
-
// multi-file drain re-render case — `drainRenderedDependencyDriftFilePaths`
|
|
1261
|
-
// already clears the whole Set before this loop starts, so by the time a
|
|
1262
|
-
// given `wPath` reaches this call its own entry is normally long gone. It
|
|
1263
|
-
// only does something when a render for a DIFFERENT file interleaves
|
|
1264
|
-
// between the drain and this retire (adding `rec.filePath` back to the
|
|
1265
|
-
// Set mid-loop) or a render fires between retirement and the caller's next
|
|
1266
|
-
// drain — either way, defensive: it keeps a just-retired file from being
|
|
1267
|
-
// double-drained (and double-charged) on the next pass.
|
|
1268
|
-
renderedDependencyDriftFiles.delete(rec.filePath);
|
|
1269
|
-
requestRenderFn?.();
|
|
1270
|
-
return true;
|
|
1271
|
-
}
|
|
1272
|
-
/**
|
|
1273
|
-
* Keep the TUI honest (#298 follow-up). `reconcileStaleWidgetFiles` drops
|
|
1274
|
-
* widget entries whose file changed on disk after they were last recorded
|
|
1275
|
-
* (i.e. diagnostics the agent already fixed) — but it was only ever wired
|
|
1276
|
-
* into the `lens_diagnostics` tool, so the widget rendered cached diagnostics
|
|
1277
|
-
* verbatim and kept showing fixed errors until `lens_diagnostics` was run by
|
|
1278
|
-
* hand. This debounced scheduler fires it from the widget render path (see
|
|
1279
|
-
* `mountLensWidget` in index.ts) so stale entries self-correct. The debounce
|
|
1280
|
-
* collapses the burst of renders that accompany a save into a single sweep.
|
|
1281
|
-
*/
|
|
1282
|
-
let staleReconcileTimer = null;
|
|
1283
|
-
export const STALE_RECONCILE_DEBOUNCE_MS = 1500;
|
|
1284
|
-
export function scheduleStaleReconcile() {
|
|
1285
|
-
if (staleReconcileTimer !== null)
|
|
1286
|
-
return;
|
|
1287
|
-
staleReconcileTimer = setTimeout(() => {
|
|
1288
|
-
staleReconcileTimer = null;
|
|
1289
|
-
void reconcileStaleWidgetFiles().catch(() => { });
|
|
1290
|
-
}, STALE_RECONCILE_DEBOUNCE_MS);
|
|
1291
|
-
// Don't keep the process alive solely for this background sweep.
|
|
1292
|
-
staleReconcileTimer?.unref?.();
|
|
1293
|
-
}
|
|
1294
|
-
/**
|
|
1295
|
-
* Return current diagnostics for every file pi-lens has seen this session.
|
|
1296
|
-
* Used by lens_diagnostics tool (mode: "all"). Exposes the FULL per-file
|
|
1297
|
-
* diagnostic set — decoupled from the widget's display cap — so the agent sees
|
|
1298
|
-
* everything, not just the 12 the TUI keeps for rendering.
|
|
1299
|
-
*
|
|
1300
|
-
* #3183: "everything" means every row a live scan stands behind.
|
|
1301
|
-
* `disposition`-tagged rows are the store's own bookkeeping for a mark — the
|
|
1302
|
-
* scan that produced this record did not report them — and they are excluded
|
|
1303
|
-
* here. That is the same predicate `countDiagnostics` and `isBlocking` have
|
|
1304
|
-
* always used, so this makes the projected array agree with the counts beside
|
|
1305
|
-
* it; before #3183 the array disagreed, and once retirement rule 1 let a row
|
|
1306
|
-
* outlive the file's mtime, `applyCachedDispositions` reached it in the
|
|
1307
|
-
* WEAK-fallback branch — which can never match a STRICT `false-positive`
|
|
1308
|
-
* anchor — and `mode=all` served a marked finding as a live warning (#3158 AC3).
|
|
1309
|
-
* The exclusion lives HERE, at the one projection every cache-only surface and
|
|
1310
|
-
* the mode=full merge read, rather than as a second disposition rule inside
|
|
1311
|
-
* `summarizeDiagnostics`' tally: that tally still skips `stale` only, and the
|
|
1312
|
-
* disposition filter still reaches it pre-applied through the shared
|
|
1313
|
-
* `applyCachedDispositions` seam.
|
|
1314
|
-
*
|
|
1315
|
-
* Deliberately NOT applied to {@link getFileDiagnostics}, the per-file read:
|
|
1316
|
-
* `reconcileWidgetDisposition` needs the tagged rows to enforce retirement
|
|
1317
|
-
* rule 3, and `lens_diagnostic_mark`'s cross-check re-marks through them.
|
|
1318
|
-
*/
|
|
1319
|
-
export function getFileDiagnosticSummaries() {
|
|
1320
|
-
return [...files.values()].map((rec) => {
|
|
1321
|
-
applyPastEofGate(rec);
|
|
1322
|
-
const live = rec.allDiagnostics.filter((d) => d.disposition === undefined);
|
|
1323
|
-
return {
|
|
1324
|
-
filePath: rec.filePath,
|
|
1325
|
-
blocking: rec.diagnosticCounts.blocking,
|
|
1326
|
-
errors: rec.diagnosticCounts.errors,
|
|
1327
|
-
warnings: rec.diagnosticCounts.warnings,
|
|
1328
|
-
advisories: countAdvisories(live),
|
|
1329
|
-
hasFinalSnapshot: rec.hasFinalDiagnosticsSnapshot,
|
|
1330
|
-
diagnostics: live.map((d) => ({ ...d })),
|
|
1331
|
-
};
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1334
|
-
/**
|
|
1335
|
-
* How many {@link WidgetDiagnostic.suppressedRetained} rows this file's record
|
|
1336
|
-
* is currently carrying (#3158 round 2 F4).
|
|
1337
|
-
*
|
|
1338
|
-
* The success-path counterpart of `widget-suppressed-retention-capped`: the
|
|
1339
|
-
* ledger only hears about retention when the per-file cap TRUNCATES it, so
|
|
1340
|
-
* nothing observed the healthy case. `tools/lsp-diagnostics.ts` folds this into
|
|
1341
|
-
* the `lsp_probe_disposition_filter` phase it already emits once per filtered
|
|
1342
|
-
* file per scan, so the count is bounded by that record's existing cardinality
|
|
1343
|
-
* — never one row per retained finding.
|
|
1344
|
-
*
|
|
1345
|
-
* Deliberately not {@link getFileDiagnostics}: that applies the past-EOF gate,
|
|
1346
|
-
* which can `stat` the file. This is a map lookup plus a count over rows the
|
|
1347
|
-
* caller's own scan just filtered, so it adds no I/O to the probe path.
|
|
1348
|
-
*/
|
|
1349
|
-
export function countRetainedSuppressedRows(filePath) {
|
|
1350
|
-
const rec = files.get(fileMapKey(filePath));
|
|
1351
|
-
if (!rec)
|
|
1352
|
-
return 0;
|
|
1353
|
-
let n = 0;
|
|
1354
|
-
for (const d of rec.allDiagnostics)
|
|
1355
|
-
if (d.suppressedRetained)
|
|
1356
|
-
n += 1;
|
|
1357
|
-
return n;
|
|
1358
|
-
}
|
|
1359
|
-
/**
|
|
1360
|
-
* Return the current FULL (uncapped) diagnostic set for a single file, as
|
|
1361
|
-
* last recorded by {@link recordDiagnostics} — the same `allDiagnostics`
|
|
1362
|
-
* store `getFileDiagnosticSummaries` exposes per-file, without paying for a
|
|
1363
|
-
* whole-session snapshot. Used by the #502 `pilens:diagnostics` bus producer
|
|
1364
|
-
* (`clients/bus-publish.ts`), which reads this immediately after
|
|
1365
|
-
* `recordDiagnostics` writes it so the emitted event reflects the write
|
|
1366
|
-
* batch's FINAL diagnostic state (post-format, post-autofix, post-dispatch —
|
|
1367
|
-
* see pipeline.ts call order). Returns `undefined` when the file has never
|
|
1368
|
-
* been recorded (caller must not confuse "never seen" with "seen and clean";
|
|
1369
|
-
* an explicit `[]` from `recordDiagnostics` is a real empty array here).
|
|
1370
|
-
*
|
|
1371
|
-
* The `files` map key is normalized through `fileMapKey` (#1020), so any path
|
|
1372
|
-
* form of the same file — forward-slash, backslash, or a different Windows
|
|
1373
|
-
* drive-letter case — resolves to the same record. This read-side fold MUST
|
|
1374
|
-
* stay identical to the write-side fold, or a file recorded under one form
|
|
1375
|
-
* would silently read as `undefined` under another (e.g. via bus-publish).
|
|
1376
|
-
*/
|
|
1377
|
-
export function getFileDiagnostics(filePath) {
|
|
1378
|
-
const rec = files.get(fileMapKey(filePath));
|
|
1379
|
-
if (!rec)
|
|
1380
|
-
return undefined;
|
|
1381
|
-
applyPastEofGate(rec);
|
|
1382
|
-
return rec.allDiagnostics.map((d) => ({ ...d }));
|
|
1383
|
-
}
|
|
1384
|
-
/** @internal Test-only helpers. Do not use in production code. */
|
|
1385
|
-
export const __testing = {
|
|
1386
|
-
getWidgetStateSnapshot() {
|
|
1387
|
-
return {
|
|
1388
|
-
files: [...files.values()].map((rec) => ({
|
|
1389
|
-
filePath: rec.filePath,
|
|
1390
|
-
storedDiagnostics: rec.diagnostics.length,
|
|
1391
|
-
blocking: rec.diagnosticCounts.blocking,
|
|
1392
|
-
errors: rec.diagnosticCounts.errors,
|
|
1393
|
-
warnings: rec.diagnosticCounts.warnings,
|
|
1394
|
-
})),
|
|
1395
|
-
};
|
|
1396
|
-
},
|
|
1397
|
-
};
|
|
1398
|
-
export function recordLsp(serverId, root, status, durationMs) {
|
|
1399
|
-
const normalizedRoot = normalizeMapKey(root);
|
|
1400
|
-
const key = `${serverId}@${normalizedRoot}`;
|
|
1401
|
-
const mapped = status === "spawn_start"
|
|
1402
|
-
? "spawning"
|
|
1403
|
-
: status === "spawn_success"
|
|
1404
|
-
? "ready"
|
|
1405
|
-
: "failed";
|
|
1406
|
-
lspServers.set(key, { serverId, root, status: mapped, durationMs });
|
|
1407
|
-
requestRender();
|
|
1408
|
-
}
|
|
1409
|
-
// ── Render ────────────────────────────────────────────────────────────────────
|
|
1410
|
-
const HORIZONTAL_MIN_WIDTH = 70;
|
|
1411
|
-
export function renderWidget(width, theme) {
|
|
1412
|
-
const dim = (s) => theme.fg("dim", s);
|
|
1413
|
-
const red = (s) => theme.fg("error", s);
|
|
1414
|
-
const yellow = (s) => theme.fg("warning", s);
|
|
1415
|
-
const green = (s) => theme.fg("success", s);
|
|
1416
|
-
const cyan = (s) => theme.fg("accent", s);
|
|
1417
|
-
const w = Math.max(1, width || 80);
|
|
1418
|
-
const useHorizontal = w >= HORIZONTAL_MIN_WIDTH;
|
|
1419
|
-
if (files.size === 0 && lspServers.size === 0)
|
|
1420
|
-
return [];
|
|
1421
|
-
const lines = [];
|
|
1422
|
-
// Header — counts from deduplicated files only
|
|
1423
|
-
const deduped = dedupeByBasename([...files.values()]);
|
|
1424
|
-
const recencySorted = deduped.filter(shouldRenderFile).slice(0, 5);
|
|
1425
|
-
const langStr = sessionLanguages.slice(0, 6).join(" ");
|
|
1426
|
-
const totalBlocking = countBlockingIn(deduped);
|
|
1427
|
-
const totalErrors = countTotalIn("error", deduped);
|
|
1428
|
-
const totalWarnings = countTotalIn("warning", deduped);
|
|
1429
|
-
const totalSuppressed = countSuppressedIn(deduped);
|
|
1430
|
-
const hasPendingAnalysis = deduped.some(isPendingAnalysis);
|
|
1431
|
-
const errorChunk = totalErrors > 0
|
|
1432
|
-
? (totalBlocking > 0 ? red : yellow)(`●${totalErrors}E`)
|
|
1433
|
-
: "";
|
|
1434
|
-
const warningChunk = totalWarnings > 0 ? yellow(`!${totalWarnings}W`) : "";
|
|
1435
|
-
const summary = errorChunk
|
|
1436
|
-
? errorChunk + (warningChunk ? " " + warningChunk : "")
|
|
1437
|
-
: warningChunk
|
|
1438
|
-
? warningChunk
|
|
1439
|
-
: files.size > 0 && !hasPendingAnalysis
|
|
1440
|
-
? green("✓ clean")
|
|
1441
|
-
: "";
|
|
1442
|
-
// LSP spawning — folded into the header in horizontal mode, tail line otherwise
|
|
1443
|
-
const spawning = [...lspServers.values()].filter((s) => s.status === "spawning");
|
|
1444
|
-
const lspChip = useHorizontal && spawning.length > 0 ? " " + dim("LSP↑") : "";
|
|
1445
|
-
const header = ` ${cyan("pi-lens")}${langStr ? " " + dim(langStr) : ""}${lspChip}${summary ? " " + summary : ""}`;
|
|
1446
|
-
lines.push(fitLine(header, w));
|
|
1447
|
-
if (totalSuppressed > 0) {
|
|
1448
|
-
lines.push(fitLine(` ${dim(`suppressed: ${totalSuppressed}`)}`, w));
|
|
1449
|
-
}
|
|
1450
|
-
// File list — display order varies by mode
|
|
1451
|
-
if (useHorizontal) {
|
|
1452
|
-
const displayOrder = sortByTierThenRecency(recencySorted);
|
|
1453
|
-
const rowLine = packHorizontalRow(displayOrder, w, theme);
|
|
1454
|
-
if (rowLine.length > 0)
|
|
1455
|
-
lines.push(rowLine);
|
|
1456
|
-
}
|
|
1457
|
-
else {
|
|
1458
|
-
for (const rec of recencySorted) {
|
|
1459
|
-
lines.push(fitLine(formatFileRowVertical(rec, theme), w));
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
// Diagnostics — blocking (or dependency-drift-demoted) only, from the most
|
|
1463
|
-
// recently touched file that has them. Vertical mode keeps the divider/filename
|
|
1464
|
-
// context; horizontal already shows the filename on the packed row above, so
|
|
1465
|
-
// we drop the extra header noise there.
|
|
1466
|
-
//
|
|
1467
|
-
// #1631 review F10: `isBlocking` answers false for a demoted (`stale`) finding
|
|
1468
|
-
// by design (#1419 demote-not-drop) — but this render loop used that same
|
|
1469
|
-
// predicate to pick which findings to SHOW, so a demoted finding vanished from
|
|
1470
|
-
// the footer entirely instead of demoting visibly. `isBlockingOrDemoted` keeps
|
|
1471
|
-
// it in the list; the per-entry render below distinguishes the two with a
|
|
1472
|
-
// dimmed marker instead of the red dot.
|
|
1473
|
-
//
|
|
1474
|
-
// #2275: a demotion that reached its delivery cap is hidden from THIS
|
|
1475
|
-
// surface only (`footerRetired`) — it stays in `allDiagnostics` for
|
|
1476
|
-
// mode=all and lens_diagnostic_mark, which is where the agent is told it
|
|
1477
|
-
// was capped and can still be confirmed by a re-run.
|
|
1478
|
-
const isBlockingOrDemoted = (d) => (isBlocking(d) || d.stale === true) && d.footerRetired !== true;
|
|
1479
|
-
const withBlocking = recencySorted.filter((r) => r.diagnostics.some(isBlockingOrDemoted));
|
|
1480
|
-
if (withBlocking.length > 0) {
|
|
1481
|
-
const rec = withBlocking[0];
|
|
1482
|
-
// #1641: gate only the ONE record whose line numbers are about to be
|
|
1483
|
-
// rendered, not the whole (potentially session-long) `deduped` list the
|
|
1484
|
-
// header counts read — one memoized stat per redraw, never a per-file
|
|
1485
|
-
// scan of every file pi-lens has touched this session. A past-EOF
|
|
1486
|
-
// citation demoted here can leave the header's aggregate blocking count
|
|
1487
|
-
// one turn stale; `reconcileStaleWidgetFiles`'s existing debounced sweep
|
|
1488
|
-
// (scheduleStaleReconcile) already re-derives that aggregate on its own
|
|
1489
|
-
// cadence, so this is a bounded, self-correcting gap, not a silent one.
|
|
1490
|
-
applyPastEofGate(rec);
|
|
1491
|
-
if (!useHorizontal) {
|
|
1492
|
-
lines.push(fitLine(dim("─".repeat(Math.min(w, 60))), w));
|
|
1493
|
-
lines.push(fitLine(` ${dim(path.basename(rec.filePath))}`, w));
|
|
1494
|
-
}
|
|
1495
|
-
const blockers = rec.diagnostics.filter(isBlockingOrDemoted).slice(0, 5);
|
|
1496
|
-
// #2275 review F1: THIS is the point where a demoted row is delivered to
|
|
1497
|
-
// a human — after the `withBlocking[0]` pick and the top-five slice, not
|
|
1498
|
-
// before them. Mark the file so the next turn end can charge exactly one
|
|
1499
|
-
// delivery against its cap. File-level, matching
|
|
1500
|
-
// `markWidgetFileBlockersStale`'s own file-level demotion batch (all of a
|
|
1501
|
-
// file's drift demotions share one delivery history); a file holding more
|
|
1502
|
-
// than five demoted rows delivers the visible ones and counts the batch,
|
|
1503
|
-
// which is the conservative direction — it retires no LATER than the rows
|
|
1504
|
-
// the agent actually read.
|
|
1505
|
-
if (blockers.some(isDependencyDriftDemoted)) {
|
|
1506
|
-
renderedDependencyDriftFiles.add(rec.filePath);
|
|
1507
|
-
}
|
|
1508
|
-
for (const d of blockers) {
|
|
1509
|
-
// A past-EOF demotion's coordinate is untrustworthy — render the
|
|
1510
|
-
// marker instead of the line (#1641); drift demotions keep theirs.
|
|
1511
|
-
const pastEof = d.stale && (d.staleReason ?? "past-eof") === "past-eof";
|
|
1512
|
-
// No link for a past-EOF demotion: the anchor would carry the same
|
|
1513
|
-
// untrustworthy line the marker exists to replace.
|
|
1514
|
-
const loc = pastEof
|
|
1515
|
-
? PAST_EOF_STALE_MARKER
|
|
1516
|
-
: d.line != null
|
|
1517
|
-
? osc8(d.uri ?? "", `L${d.line}`)
|
|
1518
|
-
: "";
|
|
1519
|
-
const rule = d.rule ? dim(` ${d.rule}`) : "";
|
|
1520
|
-
const staleTag = d.stale ? dim(` ${STALE_LINE_MARKER}`) : "";
|
|
1521
|
-
const prefix = ` ${d.stale ? dim("○") : red("●")} ${loc}${rule} `;
|
|
1522
|
-
const msgWidth = Math.max(1, w - visibleWidth(prefix) - visibleWidth(staleTag));
|
|
1523
|
-
const msg = fitLine(d.message, msgWidth, "…");
|
|
1524
|
-
lines.push(fitLine(`${prefix}${msg}${staleTag}`, w));
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
// LSP status tail — only in vertical mode; horizontal folds into header
|
|
1528
|
-
if (!useHorizontal && spawning.length > 0) {
|
|
1529
|
-
const ids = spawning.map((s) => s.serverId).join(" ");
|
|
1530
|
-
lines.push(fitLine(` ${dim(`LSP spawning: ${ids}`)}`, w));
|
|
1531
|
-
}
|
|
1532
|
-
return lines;
|
|
1533
|
-
}
|
|
1534
|
-
function classifyFileTier(rec) {
|
|
1535
|
-
if (rec.diagnosticCounts.blocking > 0)
|
|
1536
|
-
return "blocking";
|
|
1537
|
-
if (rec.diagnosticCounts.errors > 0 || rec.diagnosticCounts.warnings > 0) {
|
|
1538
|
-
return "warning";
|
|
1539
|
-
}
|
|
1540
|
-
return "clean";
|
|
1541
|
-
}
|
|
1542
|
-
function sortByTierThenRecency(recs) {
|
|
1543
|
-
const order = {
|
|
1544
|
-
blocking: 0,
|
|
1545
|
-
warning: 1,
|
|
1546
|
-
clean: 2,
|
|
1547
|
-
};
|
|
1548
|
-
return [...recs].sort((a, b) => {
|
|
1549
|
-
const ta = order[classifyFileTier(a)];
|
|
1550
|
-
const tb = order[classifyFileTier(b)];
|
|
1551
|
-
if (ta !== tb)
|
|
1552
|
-
return ta - tb;
|
|
1553
|
-
return b.touchedAt - a.touchedAt;
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
function formatFileRowVertical(rec, theme) {
|
|
1557
|
-
const dim = (s) => theme.fg("dim", s);
|
|
1558
|
-
const red = (s) => theme.fg("error", s);
|
|
1559
|
-
const yellow = (s) => theme.fg("warning", s);
|
|
1560
|
-
const green = (s) => theme.fg("success", s);
|
|
1561
|
-
const base = path.basename(rec.filePath);
|
|
1562
|
-
const blocking = rec.diagnosticCounts.blocking;
|
|
1563
|
-
const errors = rec.diagnosticCounts.errors;
|
|
1564
|
-
const warnings = rec.diagnosticCounts.warnings;
|
|
1565
|
-
const formatterFailed = hasFailedFormatter(rec);
|
|
1566
|
-
// Diagnostic severity outranks formatter failure (#1348 review): a file
|
|
1567
|
-
// with blocking diagnostics shows the blocking dot even if a format also
|
|
1568
|
-
// failed -- same precedence as the horizontal renderer.
|
|
1569
|
-
const dot = blocking > 0
|
|
1570
|
-
? red("●")
|
|
1571
|
-
: formatterFailed
|
|
1572
|
-
? red("x")
|
|
1573
|
-
: warnings > 0 || errors > 0
|
|
1574
|
-
? yellow("!")
|
|
1575
|
-
: green("✓");
|
|
1576
|
-
const runnerNames = [...rec.runners.entries()]
|
|
1577
|
-
.filter(([, r]) => r.status !== "skipped")
|
|
1578
|
-
.map(([id]) => id)
|
|
1579
|
-
.join(" ");
|
|
1580
|
-
const counts = errors > 0
|
|
1581
|
-
? " " +
|
|
1582
|
-
(blocking > 0 ? red : yellow)(`${errors}E`) +
|
|
1583
|
-
(warnings > 0 ? " " + yellow(`${warnings}W`) : "")
|
|
1584
|
-
: warnings > 0
|
|
1585
|
-
? " " + yellow(`${warnings}W`)
|
|
1586
|
-
: " " + dim("clean");
|
|
1587
|
-
const changedFormatters = [...rec.formatters.entries()]
|
|
1588
|
-
.filter(([, f]) => f.changed && f.success)
|
|
1589
|
-
.map(([name]) => name);
|
|
1590
|
-
const failedFormatters = [...rec.formatters.entries()]
|
|
1591
|
-
// An `unavailable` tool is not a code failure (#2413): it never earns a red
|
|
1592
|
-
// `fmt-failed` marker. Its result already carries success:true, so this
|
|
1593
|
-
// guard is a belt that keeps the intent explicit at the render seam.
|
|
1594
|
-
.filter(([, f]) => !f.success && f.outcome !== "unavailable")
|
|
1595
|
-
.map(([name]) => name);
|
|
1596
|
-
const formatMark = (failedFormatters.length > 0
|
|
1597
|
-
? red(` fmt-failed:${failedFormatters.join(",")}`)
|
|
1598
|
-
: "") +
|
|
1599
|
-
(changedFormatters.length > 0
|
|
1600
|
-
? dim(` fmt:${changedFormatters.join(",")}`)
|
|
1601
|
-
: "");
|
|
1602
|
-
return ` ${dot} ${base} ${dim(runnerNames)}${formatMark}${counts}`;
|
|
1603
|
-
}
|
|
1604
|
-
function packHorizontalRow(recs, totalWidth, theme) {
|
|
1605
|
-
if (recs.length === 0)
|
|
1606
|
-
return "";
|
|
1607
|
-
const dim = (s) => theme.fg("dim", s);
|
|
1608
|
-
const indent = " ";
|
|
1609
|
-
const sep = " ";
|
|
1610
|
-
// Reserve worst-case overflow space upfront so the marker always fits.
|
|
1611
|
-
// " +NN" — 4 visible chars covers up to two-digit overflow.
|
|
1612
|
-
const overflowReserve = 4;
|
|
1613
|
-
let used = visibleWidth(indent);
|
|
1614
|
-
const parts = [indent];
|
|
1615
|
-
const addedTokenWidths = [];
|
|
1616
|
-
let droppedAt = -1;
|
|
1617
|
-
for (let i = 0; i < recs.length; i++) {
|
|
1618
|
-
const sepWidth = parts.length > 1 ? visibleWidth(sep) : 0;
|
|
1619
|
-
const willOverflow = i < recs.length - 1;
|
|
1620
|
-
const reserve = willOverflow ? overflowReserve : 0;
|
|
1621
|
-
const remaining = totalWidth - used - sepWidth - reserve;
|
|
1622
|
-
if (remaining < 4) {
|
|
1623
|
-
droppedAt = i;
|
|
1624
|
-
break;
|
|
1625
|
-
}
|
|
1626
|
-
const token = formatFileTokenHorizontal(recs[i], remaining, theme);
|
|
1627
|
-
const tokenWidth = visibleWidth(token);
|
|
1628
|
-
if (token.length === 0 || used + sepWidth + tokenWidth > totalWidth) {
|
|
1629
|
-
droppedAt = i;
|
|
1630
|
-
break;
|
|
1631
|
-
}
|
|
1632
|
-
if (sepWidth > 0) {
|
|
1633
|
-
parts.push(sep);
|
|
1634
|
-
used += sepWidth;
|
|
1635
|
-
}
|
|
1636
|
-
parts.push(token);
|
|
1637
|
-
used += tokenWidth;
|
|
1638
|
-
addedTokenWidths.push(tokenWidth + sepWidth);
|
|
1639
|
-
}
|
|
1640
|
-
if (droppedAt >= 0) {
|
|
1641
|
-
let dropped = recs.length - droppedAt;
|
|
1642
|
-
let overflow = " " + dim(`+${dropped}`);
|
|
1643
|
-
// If reservation was insufficient (e.g. last token grew because no
|
|
1644
|
-
// reserve was applied), shed accepted tokens until overflow fits.
|
|
1645
|
-
while (used + visibleWidth(overflow) > totalWidth &&
|
|
1646
|
-
addedTokenWidths.length > 0) {
|
|
1647
|
-
const lastWidth = addedTokenWidths.pop();
|
|
1648
|
-
used -= lastWidth;
|
|
1649
|
-
parts.pop(); // token
|
|
1650
|
-
if (parts.length > 1)
|
|
1651
|
-
parts.pop(); // preceding separator
|
|
1652
|
-
dropped++;
|
|
1653
|
-
overflow = " " + dim(`+${dropped}`);
|
|
1654
|
-
}
|
|
1655
|
-
if (used + visibleWidth(overflow) <= totalWidth) {
|
|
1656
|
-
parts.push(overflow);
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
return fitLine(parts.join(""), totalWidth);
|
|
1660
|
-
}
|
|
1661
|
-
function formatFileTokenHorizontal(rec, remainingWidth, theme) {
|
|
1662
|
-
const dim = (s) => theme.fg("dim", s);
|
|
1663
|
-
const red = (s) => theme.fg("error", s);
|
|
1664
|
-
const yellow = (s) => theme.fg("warning", s);
|
|
1665
|
-
const blocking = rec.diagnosticCounts.blocking;
|
|
1666
|
-
const errors = rec.diagnosticCounts.errors;
|
|
1667
|
-
const warnings = rec.diagnosticCounts.warnings;
|
|
1668
|
-
const formatterChanged = hasChangedFormatter(rec);
|
|
1669
|
-
const formatterFailed = hasFailedFormatter(rec);
|
|
1670
|
-
let dotChar;
|
|
1671
|
-
if (blocking > 0)
|
|
1672
|
-
dotChar = red("●");
|
|
1673
|
-
else if (errors > 0 || warnings > 0)
|
|
1674
|
-
dotChar = yellow("!");
|
|
1675
|
-
else if (formatterChanged)
|
|
1676
|
-
dotChar = dim("✎");
|
|
1677
|
-
else
|
|
1678
|
-
dotChar = dim("·");
|
|
1679
|
-
if (formatterFailed && blocking === 0 && errors === 0 && warnings === 0) {
|
|
1680
|
-
dotChar = red("x");
|
|
1681
|
-
}
|
|
1682
|
-
let countsStyled = "";
|
|
1683
|
-
if (errors > 0 && warnings > 0) {
|
|
1684
|
-
const eColor = blocking > 0 ? red : yellow;
|
|
1685
|
-
countsStyled = " " + eColor(`${errors}E`) + yellow(`${warnings}W`);
|
|
1686
|
-
}
|
|
1687
|
-
else if (errors > 0) {
|
|
1688
|
-
const eColor = blocking > 0 ? red : yellow;
|
|
1689
|
-
countsStyled = " " + eColor(`${errors}E`);
|
|
1690
|
-
}
|
|
1691
|
-
else if (warnings > 0) {
|
|
1692
|
-
countsStyled = " " + yellow(`${warnings}W`);
|
|
1693
|
-
}
|
|
1694
|
-
const fullBasename = path.basename(rec.filePath);
|
|
1695
|
-
const fixedWidth = visibleWidth(dotChar) + 1 + visibleWidth(countsStyled);
|
|
1696
|
-
const basenameBudget = remainingWidth - fixedWidth;
|
|
1697
|
-
if (basenameBudget < 3)
|
|
1698
|
-
return "";
|
|
1699
|
-
const truncated = truncateBasename(fullBasename, basenameBudget);
|
|
1700
|
-
const linked = osc8(pathToFileURL(rec.filePath).href, truncated);
|
|
1701
|
-
return `${dotChar} ${linked}${countsStyled}`;
|
|
1702
|
-
}
|
|
1703
|
-
function truncateBasename(name, maxWidth) {
|
|
1704
|
-
if (visibleWidth(name) <= maxWidth)
|
|
1705
|
-
return name;
|
|
1706
|
-
if (maxWidth < 2)
|
|
1707
|
-
return "…";
|
|
1708
|
-
const ext = path.extname(name);
|
|
1709
|
-
const stem = name.slice(0, name.length - ext.length);
|
|
1710
|
-
const keep = maxWidth - ext.length - 1;
|
|
1711
|
-
if (keep < 1) {
|
|
1712
|
-
// Extension alone wouldn't fit; truncate the whole name.
|
|
1713
|
-
return name.slice(0, maxWidth - 1) + "…";
|
|
1714
|
-
}
|
|
1715
|
-
return stem.slice(0, keep) + "…" + ext;
|
|
1716
|
-
}
|
|
1717
|
-
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
1718
|
-
function getOrCreate(filePath, key = fileMapKey(filePath)) {
|
|
1719
|
-
maybePruneInactiveFileRecords();
|
|
1720
|
-
// Look up by the normalized key so mixed path forms of the same file share
|
|
1721
|
-
// ONE record (#1020); keep the caller's verbatim path as the display path.
|
|
1722
|
-
return (files.get(key) ?? {
|
|
1723
|
-
filePath,
|
|
1724
|
-
runners: new Map(),
|
|
1725
|
-
formatters: new Map(),
|
|
1726
|
-
diagnostics: [],
|
|
1727
|
-
allDiagnostics: [],
|
|
1728
|
-
diagnosticCounts: { blocking: 0, errors: 0, warnings: 0 },
|
|
1729
|
-
hasFinalDiagnosticsSnapshot: false,
|
|
1730
|
-
touchedAt: Date.now(),
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
function hasChangedFormatter(rec) {
|
|
1734
|
-
return [...rec.formatters.values()].some((f) => f.changed && f.success);
|
|
1735
|
-
}
|
|
1736
|
-
function hasFailedFormatter(rec) {
|
|
1737
|
-
// `unavailable` (#2413) is success:true and must never count as a failure —
|
|
1738
|
-
// the explicit guard keeps that true even if a future path mislabels it.
|
|
1739
|
-
return [...rec.formatters.values()].some((f) => !f.success && f.outcome !== "unavailable");
|
|
1740
|
-
}
|
|
1741
|
-
function shouldRenderFile(rec) {
|
|
1742
|
-
return (rec.hasFinalDiagnosticsSnapshot ||
|
|
1743
|
-
hasChangedFormatter(rec) ||
|
|
1744
|
-
hasFailedFormatter(rec));
|
|
1745
|
-
}
|
|
1746
|
-
function hasLiveDiagnostic(rec) {
|
|
1747
|
-
return rec.hasFinalDiagnosticsSnapshot && rec.diagnostics.length > 0;
|
|
1748
|
-
}
|
|
1749
|
-
function isPendingAnalysis(rec) {
|
|
1750
|
-
return rec.runners.size > 0 && !rec.hasFinalDiagnosticsSnapshot;
|
|
1751
|
-
}
|
|
1752
|
-
function capStoredDiagnostics(diagnostics) {
|
|
1753
|
-
if (diagnostics.length <= MAX_STORED_DIAGNOSTICS_PER_FILE)
|
|
1754
|
-
return diagnostics;
|
|
1755
|
-
const blockers = diagnostics.filter(isBlocking);
|
|
1756
|
-
if (blockers.length >= MAX_STORED_DIAGNOSTICS_PER_FILE) {
|
|
1757
|
-
return blockers.slice(0, MAX_STORED_DIAGNOSTICS_PER_FILE);
|
|
1758
|
-
}
|
|
1759
|
-
const rest = diagnostics.filter((d) => !isBlocking(d));
|
|
1760
|
-
return [
|
|
1761
|
-
...blockers,
|
|
1762
|
-
...rest.slice(0, MAX_STORED_DIAGNOSTICS_PER_FILE - blockers.length),
|
|
1763
|
-
];
|
|
1764
|
-
}
|
|
1765
|
-
function countTotalIn(severity, recs) {
|
|
1766
|
-
let n = 0;
|
|
1767
|
-
for (const rec of recs) {
|
|
1768
|
-
if (severity === "error")
|
|
1769
|
-
n += rec.diagnosticCounts.errors;
|
|
1770
|
-
else
|
|
1771
|
-
n += rec.diagnosticCounts.warnings;
|
|
1772
|
-
}
|
|
1773
|
-
return n;
|
|
1774
|
-
}
|
|
1775
|
-
function countSuppressedIn(recs) {
|
|
1776
|
-
let n = 0;
|
|
1777
|
-
for (const rec of recs) {
|
|
1778
|
-
for (const diagnostic of rec.allDiagnostics) {
|
|
1779
|
-
if (diagnostic.disposition)
|
|
1780
|
-
n++;
|
|
1781
|
-
}
|
|
1782
|
-
}
|
|
1783
|
-
return n;
|
|
1784
|
-
}
|
|
1785
|
-
function countBlockingIn(recs) {
|
|
1786
|
-
let n = 0;
|
|
1787
|
-
for (const rec of recs)
|
|
1788
|
-
n += rec.diagnosticCounts.blocking;
|
|
1789
|
-
return n;
|
|
1790
|
-
}
|
|
1791
|
-
function requestRender() {
|
|
1792
|
-
requestRenderFn?.();
|
|
1793
|
-
}
|
|
1794
|
-
function osc8(uri, label) {
|
|
1795
|
-
if (!uri)
|
|
1796
|
-
return label;
|
|
1797
|
-
return `\x1b]8;;${uri}\x1b\\${label}\x1b]8;;\x1b\\`;
|
|
1798
|
-
}
|
|
1799
|
-
// Dual-signature truncateToWidth handling lives in tui-fit.ts (shared with the
|
|
1800
|
-
// turn-summary message renderer, which learned the hard way that pi-tui crashes
|
|
1801
|
-
// the host on over-width lines — #513).
|
|
1802
|
-
function dedupeByBasename(recs) {
|
|
1803
|
-
const seen = new Map();
|
|
1804
|
-
for (const r of [...recs].sort((a, b) => a.touchedAt - b.touchedAt)) {
|
|
1805
|
-
seen.set(path.basename(r.filePath), r);
|
|
1806
|
-
}
|
|
1807
|
-
return [...seen.values()].sort((a, b) => b.touchedAt - a.touchedAt);
|
|
1808
|
-
}
|