@herbertgao/pi-extensions 2026.10.1 → 2026.10.2

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