@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.
Files changed (769) hide show
  1. package/README.md +16 -12
  2. package/THIRD_PARTY_NOTICES.md +3 -3
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/index.js +4 -0
  10. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail-frontmatter.cjs +43 -3
  11. package/node_modules/@dietrichgebert/ponytail/.opencode/plugins/ponytail.mjs +142 -59
  12. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  13. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  14. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  15. package/node_modules/@dietrichgebert/ponytail/README.md +52 -242
  16. package/node_modules/@dietrichgebert/ponytail/hooks/claude-codex-hooks.json +3 -3
  17. package/node_modules/@dietrichgebert/ponytail/hooks/copilot-hooks.json +4 -4
  18. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +70 -16
  19. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-config.js +8 -7
  20. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  21. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  22. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-mode-tracker.js +46 -23
  23. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-runtime.js +39 -7
  24. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-subagent.js +20 -3
  25. package/node_modules/@dietrichgebert/ponytail/hooks/qoder-hooks.json +3 -3
  26. package/node_modules/@dietrichgebert/ponytail/package.json +3 -3
  27. package/node_modules/@dietrichgebert/ponytail/pi-extension/index.js +47 -17
  28. package/node_modules/@dietrichgebert/ponytail/scripts/uninstall.js +21 -7
  29. package/node_modules/@dietrichgebert/ponytail/skills/plugin.json +5 -0
  30. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  31. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -27
  32. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  33. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +21 -18
  34. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +12 -13
  35. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -52
  36. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  37. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  38. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  39. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  40. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  41. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  42. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  43. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  44. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  45. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  46. package/node_modules/@narumitw/pi-caffeinate/package.json +4 -4
  47. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  48. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  49. package/node_modules/@tifan/pi-recap/package.json +1 -1
  50. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  51. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  52. package/node_modules/pi-antigravity/README.md +23 -2
  53. package/node_modules/pi-antigravity/package.json +5 -3
  54. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  55. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  56. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  57. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  58. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  59. package/node_modules/pi-antigravity/src/index.ts +52 -137
  60. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  61. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  62. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  63. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  64. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  65. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  66. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  67. package/node_modules/pi-lens/README.md +115 -25
  68. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  69. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  70. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  71. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  72. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  73. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  74. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  75. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  76. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  77. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  78. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  79. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  80. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  81. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  82. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  83. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  84. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  85. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  86. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  87. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  88. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  89. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  90. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  91. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  92. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  93. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  94. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  95. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  96. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  97. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  98. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  99. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  100. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  101. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  102. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  103. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  104. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  105. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  106. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  107. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  108. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  109. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  110. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  111. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  112. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  113. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  114. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  115. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  116. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  117. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  118. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  119. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  120. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  121. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  122. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  123. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  124. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  125. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  126. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  127. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  128. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  129. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  130. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  131. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  132. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  133. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  134. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  135. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  136. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  137. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  138. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  139. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  140. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  141. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  142. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  143. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  144. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  145. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  146. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  147. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  148. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  149. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  150. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  151. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  152. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  153. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  154. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  155. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  156. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  157. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  158. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  159. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  160. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  161. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  162. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  163. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  164. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  165. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  166. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  167. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  168. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  169. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  170. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  171. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  172. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  173. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  174. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  175. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  176. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  177. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  178. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  179. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  180. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  181. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  182. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  183. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  184. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  185. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  186. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  187. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  188. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  189. package/node_modules/pi-lens/dist/index.js +16456 -8335
  190. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  191. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  192. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  193. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  194. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  195. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  196. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  197. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  198. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  199. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  200. package/node_modules/pi-lens/docs/configuration.md +20 -1
  201. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  202. package/node_modules/pi-lens/docs/features.md +8 -4
  203. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  204. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  205. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  206. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  207. package/node_modules/pi-lens/docs/mcp.md +21 -5
  208. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  209. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  210. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  211. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  212. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  213. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  214. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  215. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  216. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  217. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  218. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  219. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  220. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  221. package/node_modules/pi-lens/docs/settings.md +1 -0
  222. package/node_modules/pi-lens/docs/usage.md +5 -1
  223. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  224. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  225. package/node_modules/pi-lens/package.json +18 -6
  226. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  227. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  228. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  229. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  230. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  231. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  232. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  233. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  234. package/node_modules/pi-typesafe/README.md +1 -1
  235. package/node_modules/pi-typesafe/dist/backends.d.ts +15 -5
  236. package/node_modules/pi-typesafe/dist/backends.js +18 -0
  237. package/node_modules/pi-typesafe/dist/client.d.ts +4 -3
  238. package/node_modules/pi-typesafe/dist/client.js +22 -7
  239. package/node_modules/pi-typesafe/dist/index.d.ts +2 -2
  240. package/node_modules/pi-typesafe/dist/index.js +1 -1
  241. package/node_modules/pi-typesafe/dist/usage.d.ts +14 -4
  242. package/node_modules/pi-typesafe/dist/usage.js +147 -42
  243. package/node_modules/pi-typesafe/package.json +1 -1
  244. package/node_modules/pi-web-access/CHANGELOG.md +48 -0
  245. package/node_modules/pi-web-access/README.md +57 -11
  246. package/node_modules/pi-web-access/brave-rate-limit.ts +8 -2
  247. package/node_modules/pi-web-access/curator-page.ts +5 -1
  248. package/node_modules/pi-web-access/curator-server.ts +2 -0
  249. package/node_modules/pi-web-access/declared-web-links.ts +2 -2
  250. package/node_modules/pi-web-access/degoog.ts +323 -0
  251. package/node_modules/pi-web-access/dist/index.js +3483 -2745
  252. package/node_modules/pi-web-access/dist/mcp-cli.js +27443 -0
  253. package/node_modules/pi-web-access/exa.ts +5 -1
  254. package/node_modules/pi-web-access/extract.ts +41 -3
  255. package/node_modules/pi-web-access/firecrawl.ts +35 -4
  256. package/node_modules/pi-web-access/gemini-search.ts +34 -11
  257. package/node_modules/pi-web-access/gemini-web.ts +14 -9
  258. package/node_modules/pi-web-access/index.ts +153 -1202
  259. package/node_modules/pi-web-access/keenable.ts +221 -0
  260. package/node_modules/pi-web-access/mcp-cli.ts +40 -0
  261. package/node_modules/pi-web-access/mcp-server.ts +81 -0
  262. package/node_modules/pi-web-access/mcp-tool-definitions.ts +117 -0
  263. package/node_modules/pi-web-access/openai-search.ts +34 -1
  264. package/node_modules/pi-web-access/package.json +23 -5
  265. package/node_modules/pi-web-access/perplexity.ts +64 -56
  266. package/node_modules/pi-web-access/tool-arguments.ts +20 -0
  267. package/node_modules/pi-web-access/video-extract.ts +1 -1
  268. package/node_modules/pi-web-access/web-tool-contract.ts +86 -0
  269. package/node_modules/pi-web-access/web-tool-core.ts +1502 -0
  270. package/node_modules/pi-web-access/youtube-extract.ts +2 -5
  271. package/package.json +17 -12
  272. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -329
  273. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -346
  274. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-3model.svg +0 -21
  275. package/node_modules/@dietrichgebert/ponytail/assets/benchmark-agentic.svg +0 -62
  276. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.png +0 -0
  277. package/node_modules/@dietrichgebert/ponytail/assets/logo-dark.svg +0 -115
  278. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt-dark.svg +0 -27
  279. package/node_modules/@dietrichgebert/ponytail/assets/logo-greenpt.svg +0 -27
  280. package/node_modules/@dietrichgebert/ponytail/assets/logo.png +0 -0
  281. package/node_modules/@dietrichgebert/ponytail/assets/retriever-icon.png +0 -0
  282. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-dark.svg +0 -4
  283. package/node_modules/@dietrichgebert/ponytail/assets/retriever-logo-light.svg +0 -4
  284. package/node_modules/@dietrichgebert/ponytail/assets/social-preview.png +0 -0
  285. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-es.png +0 -0
  286. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner-ko.png +0 -0
  287. package/node_modules/@dietrichgebert/ponytail/assets/waitlist-banner.png +0 -0
  288. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  289. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  290. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  291. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  292. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  293. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  294. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  295. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  296. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  297. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  298. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  299. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  300. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  301. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  302. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  303. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  304. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  305. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  306. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  307. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  308. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  309. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  310. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  311. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  312. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  313. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  314. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  315. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  316. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  317. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  318. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  319. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  320. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  321. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  322. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  323. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  324. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  325. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  326. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  327. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  328. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  329. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  330. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  331. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  332. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  333. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  334. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  335. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  336. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  337. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  338. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  339. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  340. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  341. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  342. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  343. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  344. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  345. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  346. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  347. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  348. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  349. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  350. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  351. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  352. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  353. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  354. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  355. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  356. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  357. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  358. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  359. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  360. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  361. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  362. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  363. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  364. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  365. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  366. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  367. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  368. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  369. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  370. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  371. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  372. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  373. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  374. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  375. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  376. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  377. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  378. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  379. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  380. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  381. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  382. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  383. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  384. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  385. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  386. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  387. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  388. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  389. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  390. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  391. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  392. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  393. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  394. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  395. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  396. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  397. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  398. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  399. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  400. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  401. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  402. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  403. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  404. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  405. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  406. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  407. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  408. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  409. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  410. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  411. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  412. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  413. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  414. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  415. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  416. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  417. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  418. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  419. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  420. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  421. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  422. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  423. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  424. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  425. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  426. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  427. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  428. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  429. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  430. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  431. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  432. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  433. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  434. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  435. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  436. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  437. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  438. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  439. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  440. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  441. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  442. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  443. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  444. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  445. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  446. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  447. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  448. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  449. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  450. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  451. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  452. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  453. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  454. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  455. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  456. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  457. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  458. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  459. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  460. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  461. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  462. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  463. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  464. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  465. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  466. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  467. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  468. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  469. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  470. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  471. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  472. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  473. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  474. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  475. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  476. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  477. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  478. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  479. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  480. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  481. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  482. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  483. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  484. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  485. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  486. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  487. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  488. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  489. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  490. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  491. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  492. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  493. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  494. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  495. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  496. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  498. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  499. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  500. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  501. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  502. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  503. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  504. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  505. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  506. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  507. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  508. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  509. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  510. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  511. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  512. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  513. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  514. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  515. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  516. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  517. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  518. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  519. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  520. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  521. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  522. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  523. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  524. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  525. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  526. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  527. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  528. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  529. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  530. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  531. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  532. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  533. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  534. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  535. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  536. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  537. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  538. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  539. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  540. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  541. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  542. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  543. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  544. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  545. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  546. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  547. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  548. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  549. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  550. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  551. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  552. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  553. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  554. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  555. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  556. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  557. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  558. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  559. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  560. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  561. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  562. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  563. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  564. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  565. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  566. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  567. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  568. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  569. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  570. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  571. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  572. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  573. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  574. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  575. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  576. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  577. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  578. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  579. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  580. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  581. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  582. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  583. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  584. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  585. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  586. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  587. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  588. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  589. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  590. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  591. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  592. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  593. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  594. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  595. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  596. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  597. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  598. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  599. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  600. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  601. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  602. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  603. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  604. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  605. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  606. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  607. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  608. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  609. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  610. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  611. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  612. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  613. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  614. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  615. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  616. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  617. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  618. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  619. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  620. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  621. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  622. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  623. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  624. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  625. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  626. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  627. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  628. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  629. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  630. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  631. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  632. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  633. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  634. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  635. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  636. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  637. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  638. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  639. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  640. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  641. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  642. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  643. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  644. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  645. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  646. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  647. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  648. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  649. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  650. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  651. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  652. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  653. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  654. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  655. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  656. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  657. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  658. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  659. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  660. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  661. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  662. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  663. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  664. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  665. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  666. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  667. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  668. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  669. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  670. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  671. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  672. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  673. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  674. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  675. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  676. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  677. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  678. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  679. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  680. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  681. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  682. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  683. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  684. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  685. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  686. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  687. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  688. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  689. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  690. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  691. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  692. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  693. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  694. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  695. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  696. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  697. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  698. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  699. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  700. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  701. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  702. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  703. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  704. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  705. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  706. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  707. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  708. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  709. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  710. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  711. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  712. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  713. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  714. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  715. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  716. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  717. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  718. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  719. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  720. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  721. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  722. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  723. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  724. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  725. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  726. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  727. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  728. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  729. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  730. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  731. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  732. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  733. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  734. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  735. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  736. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  737. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  738. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  739. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  740. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  741. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  742. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  743. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  744. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  745. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  746. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  747. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  748. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  749. package/node_modules/pi-lens/dist/i18n.js +0 -69
  750. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  751. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  752. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  753. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  754. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  755. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  756. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  757. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  758. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  759. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  760. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  761. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  762. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  763. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  764. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  765. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  766. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  767. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  768. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  769. 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
- }