pi-lens 4.3.0 → 4.4.0

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 (650) hide show
  1. package/CHANGELOG.md +1064 -0
  2. package/README.md +115 -25
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -2
  4. package/dist/chunk-22AVIGXS.js +560 -0
  5. package/dist/chunk-27BBHSZC.js +106 -0
  6. package/dist/chunk-2BEMIWEW.js +51 -0
  7. package/dist/chunk-2EECBNC5.js +89 -0
  8. package/dist/chunk-34U7PLGE.js +23 -0
  9. package/dist/chunk-3I5QFT3N.js +181 -0
  10. package/dist/chunk-3MED75HT.js +37 -0
  11. package/dist/chunk-47D2BO4S.js +87 -0
  12. package/dist/chunk-4RERDEXR.js +348 -0
  13. package/dist/chunk-4W2PLC4J.js +27 -0
  14. package/dist/chunk-52H5B5TC.js +202 -0
  15. package/dist/chunk-54B42YRX.js +27 -0
  16. package/dist/chunk-5RXBNSW2.js +266 -0
  17. package/dist/chunk-5THXD6X5.js +201 -0
  18. package/dist/chunk-5W3R6LFT.js +24 -0
  19. package/dist/chunk-5ZEJHV35.js +341 -0
  20. package/dist/chunk-6P62QVEA.js +118 -0
  21. package/dist/chunk-6QIUX4EP.js +459 -0
  22. package/dist/chunk-6VCD4VKZ.js +78 -0
  23. package/dist/chunk-7F72PXY2.js +326 -0
  24. package/dist/chunk-7FBF7NPR.js +998 -0
  25. package/dist/chunk-7U2IDNXN.js +37 -0
  26. package/dist/chunk-ABBOA7UD.js +157 -0
  27. package/dist/chunk-AHDW3GPE.js +50 -0
  28. package/dist/chunk-AJ3QXTRQ.js +756 -0
  29. package/dist/chunk-AJVLWNFN.js +1610 -0
  30. package/dist/chunk-AJXODZY6.js +112 -0
  31. package/dist/chunk-ARFCXG7S.js +84 -0
  32. package/dist/chunk-AS44CFQ4.js +708 -0
  33. package/dist/chunk-AWE5TMPO.js +38 -0
  34. package/dist/chunk-B3I2UUT4.js +4692 -0
  35. package/dist/chunk-BRJKOBZP.js +331 -0
  36. package/dist/chunk-BW5KFIQC.js +59 -0
  37. package/dist/chunk-C2AUYXQH.js +149 -0
  38. package/dist/chunk-CGGAIP7J.js +29 -0
  39. package/dist/chunk-D6QS3U6M.js +30 -0
  40. package/dist/chunk-DFVMIKMQ.js +349 -0
  41. package/dist/chunk-DIULIWKU.js +169 -0
  42. package/dist/chunk-DQ2NZ7C4.js +2317 -0
  43. package/dist/chunk-E2WFBRHM.js +213 -0
  44. package/dist/chunk-E4MAEDPM.js +50 -0
  45. package/dist/chunk-EOKRQ3SA.js +30 -0
  46. package/dist/chunk-F3AU2TMM.js +2626 -0
  47. package/dist/chunk-F3P7VQ7H.js +170 -0
  48. package/dist/chunk-FC4RP4I2.js +12 -0
  49. package/dist/chunk-FJNKANOS.js +32 -0
  50. package/dist/chunk-GUOGVTCQ.js +146 -0
  51. package/dist/chunk-GVXA6CCE.js +18 -0
  52. package/dist/chunk-HHYAELLJ.js +2334 -0
  53. package/dist/chunk-HMFXILPV.js +83 -0
  54. package/dist/chunk-HVUZXSWK.js +421 -0
  55. package/dist/chunk-HWBZIGMJ.js +62 -0
  56. package/dist/chunk-I3R7GKDR.js +17 -0
  57. package/dist/chunk-I7SPGOWI.js +55 -0
  58. package/dist/chunk-IIXOAGHR.js +38 -0
  59. package/dist/chunk-IVP5KAB7.js +2250 -0
  60. package/dist/chunk-IXEBJSUE.js +22 -0
  61. package/dist/chunk-JOLITGCC.js +27 -0
  62. package/dist/chunk-K6VYLB2U.js +145 -0
  63. package/dist/chunk-LJDODBBZ.js +78 -0
  64. package/dist/chunk-LNAKEO63.js +78 -0
  65. package/dist/chunk-LSMJ6KBE.js +4139 -0
  66. package/dist/chunk-LW6XKDZQ.js +5565 -0
  67. package/dist/chunk-N3YQJI6O.js +899 -0
  68. package/dist/chunk-NABLC7W3.js +148 -0
  69. package/dist/chunk-NKOUFPQF.js +659 -0
  70. package/dist/chunk-NL5GLMFI.js +211 -0
  71. package/dist/chunk-NMABWBZN.js +327 -0
  72. package/dist/chunk-NTHH2J74.js +27 -0
  73. package/dist/chunk-NTUJEGDZ.js +59 -0
  74. package/dist/chunk-NXL4FFQQ.js +47 -0
  75. package/dist/chunk-NXPS2L6U.js +466 -0
  76. package/dist/chunk-O5WILGMI.js +42 -0
  77. package/dist/chunk-O6TQT6RI.js +48 -0
  78. package/dist/chunk-OAXNDZJ7.js +323 -0
  79. package/dist/chunk-ODEEOXPH.js +44 -0
  80. package/dist/chunk-OQGFCZJ4.js +4533 -0
  81. package/dist/chunk-ORTMY4Q4.js +282 -0
  82. package/dist/chunk-OS32RNR2.js +442 -0
  83. package/dist/chunk-PAK6IDME.js +189 -0
  84. package/dist/chunk-PWFHBTSF.js +406 -0
  85. package/dist/chunk-Q5U6FMDI.js +2352 -0
  86. package/dist/chunk-QOZXJ4NA.js +3364 -0
  87. package/dist/chunk-QYF5S6P6.js +45 -0
  88. package/dist/chunk-RIISMHKN.js +36 -0
  89. package/dist/chunk-RPD4ZSB7.js +25 -0
  90. package/dist/chunk-RPD5K5NC.js +79 -0
  91. package/dist/chunk-RUOG5D2C.js +262 -0
  92. package/dist/chunk-T4ERT73Y.js +599 -0
  93. package/dist/chunk-T6D4Q4AQ.js +54 -0
  94. package/dist/chunk-TGPXLY2J.js +391 -0
  95. package/dist/chunk-TLGFBM3F.js +12116 -0
  96. package/dist/chunk-TO6WL3GM.js +88 -0
  97. package/dist/chunk-TT22N74J.js +517 -0
  98. package/dist/chunk-TVN5N6GY.js +21216 -0
  99. package/dist/chunk-U3VGR4VU.js +308 -0
  100. package/dist/chunk-UK3CMEAL.js +1250 -0
  101. package/dist/chunk-UOPVJQ4N.js +103 -0
  102. package/dist/chunk-UXVNL3D7.js +23 -0
  103. package/dist/chunk-UY752QSN.js +45 -0
  104. package/dist/chunk-V5PXFSZK.js +188 -0
  105. package/dist/chunk-V7R425XG.js +398 -0
  106. package/dist/chunk-VCSRSYLF.js +81 -0
  107. package/dist/chunk-VD4JQCQC.js +162 -0
  108. package/dist/chunk-VDBPVIKD.js +62 -0
  109. package/dist/chunk-VHQLPIRJ.js +114 -0
  110. package/dist/chunk-VN2AXLEX.js +5414 -0
  111. package/dist/chunk-VNMT7C64.js +478 -0
  112. package/dist/chunk-WONERJTP.js +309 -0
  113. package/dist/chunk-XECLWQZN.js +85 -0
  114. package/dist/chunk-XIMYZ3JA.js +27 -0
  115. package/dist/chunk-XK4CIILI.js +144 -0
  116. package/dist/chunk-Y4T27COV.js +562 -0
  117. package/dist/chunk-YGVYKNSN.js +1860 -0
  118. package/dist/chunk-YLF3PPT5.js +308 -0
  119. package/dist/chunk-YSNFMJKN.js +14 -0
  120. package/dist/chunk-Z3CCVDUB.js +76 -0
  121. package/dist/chunk-Z54ZSOBZ.js +61 -0
  122. package/dist/chunk-ZAWW3AF6.js +97 -0
  123. package/dist/chunk-ZMOL6RQC.js +167 -0
  124. package/dist/index.js +16456 -8335
  125. package/dist/mcp/analyze-cli.js +186 -232
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22589 -1573
  128. package/dist/mcp/worker.js +97 -38
  129. package/dist/probes/installer.js +135 -0
  130. package/dist/workers/project-snapshot-persist-worker.js +22 -0
  131. package/dist/workers/review-graph-persist-worker.js +15 -0
  132. package/docs/agent-guide.md +13 -4
  133. package/docs/agent-tools.md +42 -0
  134. package/docs/ast-grep_rules_catalog.md +3 -2
  135. package/docs/configuration.md +20 -1
  136. package/docs/environment-variables.md +52 -2
  137. package/docs/features.md +8 -4
  138. package/docs/globalconfig.md +1 -0
  139. package/docs/io-bridge-v2-migration.md +94 -0
  140. package/docs/lsp-capability-matrix.md +36 -4
  141. package/docs/lsp-idle-eviction.md +124 -0
  142. package/docs/mcp.md +21 -5
  143. package/docs/merge-queue-rollout.md +77 -0
  144. package/docs/pi-lens-fixer.md +156 -88
  145. package/docs/pi-lens-investigator.md +74 -26
  146. package/docs/pi-lens-merge-policy.md +694 -0
  147. package/docs/pi-lens-monitor.md +73 -7
  148. package/docs/pi-lens-retro.md +6 -4
  149. package/docs/pi-lens-reviewer.md +188 -48
  150. package/docs/pi-lens-subagent.md +246 -45
  151. package/docs/pi-lens-warden.md +18 -7
  152. package/docs/public-api-stability.md +55 -0
  153. package/docs/release-qa-baseline.md +84 -17
  154. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  155. package/docs/servercapabilities.md +53 -49
  156. package/docs/settings.md +1 -0
  157. package/docs/usage.md +5 -1
  158. package/grammars/tree-sitter-bash.wasm +0 -0
  159. package/grammars/tree-sitter-bash.wasm.json +3 -3
  160. package/package.json +19 -7
  161. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  162. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  163. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  164. package/scripts/download-grammars.js +5 -0
  165. package/scripts/grammars.lock.json +6 -1
  166. package/scripts/install-selftest.mjs +33 -34
  167. package/scripts/lib/host-provided-deps.mjs +13 -4
  168. package/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  169. package/dist/clients/actionable-warnings-logger.js +0 -35
  170. package/dist/clients/actionable-warnings.js +0 -1610
  171. package/dist/clients/advisory-provenance.js +0 -593
  172. package/dist/clients/agent-behavior-client.js +0 -147
  173. package/dist/clients/agent-nudge.js +0 -325
  174. package/dist/clients/analysed-root.js +0 -1
  175. package/dist/clients/ast-grep-client.js +0 -731
  176. package/dist/clients/ast-grep-rule-manager.js +0 -146
  177. package/dist/clients/ast-grep-tool-logger.js +0 -118
  178. package/dist/clients/ast-grep-types.js +0 -10
  179. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  180. package/dist/clients/atomic-write-staging.js +0 -75
  181. package/dist/clients/atomic-write.js +0 -189
  182. package/dist/clients/availability-probe-flight.js +0 -12
  183. package/dist/clients/bash-file-access.js +0 -1056
  184. package/dist/clients/biome-client.js +0 -357
  185. package/dist/clients/blocker-freshness.js +0 -837
  186. package/dist/clients/blocker-past-eof.js +0 -129
  187. package/dist/clients/bootstrap.js +0 -569
  188. package/dist/clients/bounded-cache.js +0 -190
  189. package/dist/clients/bounded-pid-file-lock.js +0 -202
  190. package/dist/clients/bounded-telemetry.js +0 -295
  191. package/dist/clients/build-identity.js +0 -95
  192. package/dist/clients/bundled-resource-health.js +0 -113
  193. package/dist/clients/bus-events-logger.js +0 -105
  194. package/dist/clients/bus-publish.js +0 -171
  195. package/dist/clients/cache/rule-cache.js +0 -190
  196. package/dist/clients/cache-manager.js +0 -524
  197. package/dist/clients/cache-observability.js +0 -1313
  198. package/dist/clients/call-graph.js +0 -852
  199. package/dist/clients/cargo-manifest.js +0 -422
  200. package/dist/clients/cascade-budget.js +0 -196
  201. package/dist/clients/cascade-format.js +0 -315
  202. package/dist/clients/cascade-logger.js +0 -31
  203. package/dist/clients/cascade-types.js +0 -9
  204. package/dist/clients/child-unref.js +0 -124
  205. package/dist/clients/code-quality-warnings.js +0 -286
  206. package/dist/clients/codebase-model.js +0 -173
  207. package/dist/clients/collateral-test-role.js +0 -40
  208. package/dist/clients/complexity-client.js +0 -462
  209. package/dist/clients/config-core/deny.js +0 -221
  210. package/dist/clients/config-core/index.js +0 -50
  211. package/dist/clients/config-core/merge.js +0 -357
  212. package/dist/clients/config-core/normalize.js +0 -340
  213. package/dist/clients/config-core/process-spec.js +0 -248
  214. package/dist/clients/config-core/provenance.js +0 -164
  215. package/dist/clients/config-core/records.js +0 -218
  216. package/dist/clients/config-core/resolve.js +0 -125
  217. package/dist/clients/config-core/safe-object.js +0 -78
  218. package/dist/clients/config-core/schema.js +0 -165
  219. package/dist/clients/config-diagnostic-codes.js +0 -285
  220. package/dist/clients/config-locations.js +0 -388
  221. package/dist/clients/config-resolve.js +0 -798
  222. package/dist/clients/config-schema.js +0 -251
  223. package/dist/clients/config-warn.js +0 -407
  224. package/dist/clients/console-guard-install.js +0 -26
  225. package/dist/clients/cooperative-budget.js +0 -42
  226. package/dist/clients/dead-code-client.js +0 -510
  227. package/dist/clients/dead-code-logger.js +0 -30
  228. package/dist/clients/deadline-utils.js +0 -280
  229. package/dist/clients/debug-handles.js +0 -211
  230. package/dist/clients/debug-heap.js +0 -174
  231. package/dist/clients/deferred-lsp-work.js +0 -106
  232. package/dist/clients/degradation-ledger.js +0 -426
  233. package/dist/clients/demoted-finding-render.js +0 -169
  234. package/dist/clients/dependency-checker.js +0 -885
  235. package/dist/clients/deps/ast-grep-napi.js +0 -22
  236. package/dist/clients/deps/js-yaml.js +0 -9
  237. package/dist/clients/deps/minimatch.js +0 -4
  238. package/dist/clients/deps/pi-tui.js +0 -14
  239. package/dist/clients/deps/typebox.js +0 -14
  240. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  241. package/dist/clients/deps/web-tree-sitter.js +0 -24
  242. package/dist/clients/diagnostic-dispositions.js +0 -630
  243. package/dist/clients/diagnostic-line-freshness.js +0 -305
  244. package/dist/clients/diagnostic-logger.js +0 -73
  245. package/dist/clients/diagnostic-tracker.js +0 -105
  246. package/dist/clients/diagnostics-publish.js +0 -230
  247. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  248. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  249. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  250. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  251. package/dist/clients/dispatch/dispatcher.js +0 -1269
  252. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  253. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  254. package/dist/clients/dispatch/fact-runner.js +0 -30
  255. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  256. package/dist/clients/dispatch/fact-store.js +0 -377
  257. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  258. package/dist/clients/dispatch/facts/file-content.js +0 -19
  259. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  260. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  261. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  262. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  263. package/dist/clients/dispatch/finding-policy.js +0 -339
  264. package/dist/clients/dispatch/indent-detect.js +0 -417
  265. package/dist/clients/dispatch/inline-suppressions.js +0 -89
  266. package/dist/clients/dispatch/integration.js +0 -2377
  267. package/dist/clients/dispatch/lazy.js +0 -11
  268. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  269. package/dist/clients/dispatch/plan.js +0 -278
  270. package/dist/clients/dispatch/priorities.js +0 -20
  271. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  272. package/dist/clients/dispatch/rule-ignores.js +0 -69
  273. package/dist/clients/dispatch/rule-policy.js +0 -135
  274. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  275. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  276. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  277. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  278. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  279. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  280. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  281. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  282. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  283. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  284. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  285. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  286. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  287. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  288. package/dist/clients/dispatch/runner-context.js +0 -52
  289. package/dist/clients/dispatch/runners/actionlint.js +0 -111
  290. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  291. package/dist/clients/dispatch/runners/biome-check.js +0 -296
  292. package/dist/clients/dispatch/runners/cpp-check.js +0 -219
  293. package/dist/clients/dispatch/runners/credo.js +0 -82
  294. package/dist/clients/dispatch/runners/cue-vet.js +0 -416
  295. package/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  296. package/dist/clients/dispatch/runners/detekt.js +0 -176
  297. package/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  298. package/dist/clients/dispatch/runners/elixir-check.js +0 -195
  299. package/dist/clients/dispatch/runners/eslint.js +0 -137
  300. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  301. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  302. package/dist/clients/dispatch/runners/gleam-check.js +0 -167
  303. package/dist/clients/dispatch/runners/go-vet.js +0 -104
  304. package/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  305. package/dist/clients/dispatch/runners/hadolint.js +0 -91
  306. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  307. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  308. package/dist/clients/dispatch/runners/htmlhint.js +0 -117
  309. package/dist/clients/dispatch/runners/index.js +0 -108
  310. package/dist/clients/dispatch/runners/javac.js +0 -99
  311. package/dist/clients/dispatch/runners/ktlint.js +0 -110
  312. package/dist/clients/dispatch/runners/lsp.js +0 -379
  313. package/dist/clients/dispatch/runners/markdownlint.js +0 -167
  314. package/dist/clients/dispatch/runners/mypy.js +0 -102
  315. package/dist/clients/dispatch/runners/oxlint.js +0 -414
  316. package/dist/clients/dispatch/runners/php-lint.js +0 -85
  317. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  318. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  319. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  320. package/dist/clients/dispatch/runners/pyright.js +0 -169
  321. package/dist/clients/dispatch/runners/rubocop.js +0 -99
  322. package/dist/clients/dispatch/runners/ruff.js +0 -88
  323. package/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  324. package/dist/clients/dispatch/runners/shellcheck.js +0 -173
  325. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  326. package/dist/clients/dispatch/runners/spellcheck.js +0 -128
  327. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  328. package/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  329. package/dist/clients/dispatch/runners/stylelint.js +0 -166
  330. package/dist/clients/dispatch/runners/swiftlint.js +0 -163
  331. package/dist/clients/dispatch/runners/taplo.js +0 -174
  332. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  333. package/dist/clients/dispatch/runners/tflint.js +0 -125
  334. package/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  335. package/dist/clients/dispatch/runners/trivy-config.js +0 -253
  336. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  337. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  338. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  339. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  340. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  341. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  342. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  343. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  344. package/dist/clients/dispatch/runners/utils.js +0 -15
  345. package/dist/clients/dispatch/runners/vale.js +0 -128
  346. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  347. package/dist/clients/dispatch/runners/yamllint.js +0 -95
  348. package/dist/clients/dispatch/runners/zig-check.js +0 -79
  349. package/dist/clients/dispatch/suppress-writer.js +0 -79
  350. package/dist/clients/dispatch/tool-profile.js +0 -30
  351. package/dist/clients/dispatch/types.js +0 -21
  352. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  353. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  354. package/dist/clients/disposition-logger.js +0 -32
  355. package/dist/clients/disposition-publish.js +0 -124
  356. package/dist/clients/durable-store.js +0 -97
  357. package/dist/clients/effective-config.js +0 -415
  358. package/dist/clients/env-utils.js +0 -83
  359. package/dist/clients/error-class.js +0 -23
  360. package/dist/clients/eval-timestamp.js +0 -17
  361. package/dist/clients/event-loop-hold.js +0 -274
  362. package/dist/clients/event-loop-monitor.js +0 -250
  363. package/dist/clients/extension-log.js +0 -504
  364. package/dist/clients/extension-mode.js +0 -73
  365. package/dist/clients/feature-hints.js +0 -103
  366. package/dist/clients/file-kinds.js +0 -412
  367. package/dist/clients/file-role.js +0 -134
  368. package/dist/clients/file-time.js +0 -160
  369. package/dist/clients/file-utils.js +0 -1032
  370. package/dist/clients/finding-delivery-gate.js +0 -494
  371. package/dist/clients/finding-identity.js +0 -88
  372. package/dist/clients/fix-worklog.js +0 -103
  373. package/dist/clients/format-events-publish.js +0 -343
  374. package/dist/clients/format-service.js +0 -222
  375. package/dist/clients/formatters-lazy.js +0 -9
  376. package/dist/clients/formatters.js +0 -2032
  377. package/dist/clients/freshness-cadence.js +0 -17
  378. package/dist/clients/freshness.js +0 -38
  379. package/dist/clients/generated-artifacts.js +0 -394
  380. package/dist/clients/generation-guard.js +0 -215
  381. package/dist/clients/git-guard.js +0 -1136
  382. package/dist/clients/git-tracked-ignore.js +0 -243
  383. package/dist/clients/gitleaks-client.js +0 -509
  384. package/dist/clients/go-client.js +0 -104
  385. package/dist/clients/govulncheck-client.js +0 -605
  386. package/dist/clients/gradle-ktfmt-style.js +0 -348
  387. package/dist/clients/grammar-source.js +0 -465
  388. package/dist/clients/gzip-stage-write.js +0 -135
  389. package/dist/clients/hashline-anchor.js +0 -424
  390. package/dist/clients/hook-budgets.js +0 -76
  391. package/dist/clients/host-edit-normalize.js +0 -140
  392. package/dist/clients/host-ports.js +0 -39
  393. package/dist/clients/indent-retarget.js +0 -154
  394. package/dist/clients/inline-blocker-dispositions.js +0 -186
  395. package/dist/clients/install-diagnostics.js +0 -156
  396. package/dist/clients/installer/index.js +0 -5416
  397. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  398. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  399. package/dist/clients/instance-reaper-state.js +0 -7
  400. package/dist/clients/instance-reaper.js +0 -1137
  401. package/dist/clients/instance-registry-lock.js +0 -145
  402. package/dist/clients/instance-registry.js +0 -773
  403. package/dist/clients/jscpd-client.js +0 -378
  404. package/dist/clients/json-cache-read.js +0 -65
  405. package/dist/clients/knip-client.js +0 -788
  406. package/dist/clients/language-policy.js +0 -252
  407. package/dist/clients/language-profile.js +0 -361
  408. package/dist/clients/language-registry.js +0 -603
  409. package/dist/clients/latency-logger.js +0 -727
  410. package/dist/clients/lazy-import.js +0 -21
  411. package/dist/clients/ledger-bounds.js +0 -54
  412. package/dist/clients/lens-config.js +0 -404
  413. package/dist/clients/lens-engine.js +0 -358
  414. package/dist/clients/lens-events.js +0 -125
  415. package/dist/clients/lens-flag-registry.js +0 -467
  416. package/dist/clients/lens-map.js +0 -1156
  417. package/dist/clients/live-bus-emitter.js +0 -74
  418. package/dist/clients/log-cleanup.js +0 -222
  419. package/dist/clients/lsp/aggregation.js +0 -178
  420. package/dist/clients/lsp/cascade-tier.js +0 -421
  421. package/dist/clients/lsp/client.js +0 -4593
  422. package/dist/clients/lsp/config.js +0 -691
  423. package/dist/clients/lsp/content-limits.js +0 -41
  424. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  425. package/dist/clients/lsp/document-drift.js +0 -467
  426. package/dist/clients/lsp/edits.js +0 -1219
  427. package/dist/clients/lsp/index.js +0 -8305
  428. package/dist/clients/lsp/inferred-project.js +0 -216
  429. package/dist/clients/lsp/jvm-runtime.js +0 -298
  430. package/dist/clients/lsp/language.js +0 -63
  431. package/dist/clients/lsp/launch.js +0 -746
  432. package/dist/clients/lsp/lombok.js +0 -158
  433. package/dist/clients/lsp/path-utils.js +0 -5
  434. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  435. package/dist/clients/lsp/position-encoding.js +0 -61
  436. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  437. package/dist/clients/lsp/server.js +0 -3290
  438. package/dist/clients/lsp/session-roots.js +0 -192
  439. package/dist/clients/lsp/spawn-history.js +0 -17
  440. package/dist/clients/lsp/sync-kind.js +0 -56
  441. package/dist/clients/lsp/tsserver-sync.js +0 -332
  442. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  443. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  444. package/dist/clients/lsp/wait-policy/index.js +0 -10
  445. package/dist/clients/lsp/wait-policy/strategies.js +0 -392
  446. package/dist/clients/lsp/watch-queue.js +0 -61
  447. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  448. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  449. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  450. package/dist/clients/lsp-budget.js +0 -186
  451. package/dist/clients/lsp-document-symbols.js +0 -110
  452. package/dist/clients/lsp-lazy.js +0 -9
  453. package/dist/clients/lsp-mutation.js +0 -396
  454. package/dist/clients/lsp-status.js +0 -59
  455. package/dist/clients/map-with-concurrency.js +0 -40
  456. package/dist/clients/mcp/analyze.js +0 -420
  457. package/dist/clients/mcp/host-shim.js +0 -31
  458. package/dist/clients/mcp/ipc.js +0 -523
  459. package/dist/clients/mcp/review.js +0 -197
  460. package/dist/clients/mcp/session.js +0 -412
  461. package/dist/clients/memory-sampler.js +0 -208
  462. package/dist/clients/message-end-attribution.js +0 -30
  463. package/dist/clients/metrics-client.js +0 -107
  464. package/dist/clients/metrics-history.js +0 -327
  465. package/dist/clients/middle-man-analysis.js +0 -263
  466. package/dist/clients/model-provider.js +0 -53
  467. package/dist/clients/module-report-lsp.js +0 -189
  468. package/dist/clients/module-report.js +0 -2150
  469. package/dist/clients/mutating-tool.js +0 -651
  470. package/dist/clients/mutation-attribution.js +0 -368
  471. package/dist/clients/mutation-bridge.js +0 -240
  472. package/dist/clients/ndjson-logger.js +0 -734
  473. package/dist/clients/observed-mutation-sources.js +0 -101
  474. package/dist/clients/observed-mutation.js +0 -1253
  475. package/dist/clients/oldtext-autopatch.js +0 -128
  476. package/dist/clients/opaque-mutation-scan.js +0 -496
  477. package/dist/clients/opengrep-client.js +0 -333
  478. package/dist/clients/opengrep-config.js +0 -76
  479. package/dist/clients/package-manager.js +0 -607
  480. package/dist/clients/package-root.js +0 -38
  481. package/dist/clients/partial-edit-apply.js +0 -439
  482. package/dist/clients/path-attribution-telemetry.js +0 -43
  483. package/dist/clients/path-keyed-map.js +0 -108
  484. package/dist/clients/path-utils.js +0 -1244
  485. package/dist/clients/performance-report.js +0 -331
  486. package/dist/clients/persist-debounce.js +0 -70
  487. package/dist/clients/persistent-reverify.js +0 -271
  488. package/dist/clients/php-cs-fixer-config.js +0 -119
  489. package/dist/clients/pipeline.js +0 -1480
  490. package/dist/clients/post-autofix-notice.js +0 -37
  491. package/dist/clients/print-mode.js +0 -21
  492. package/dist/clients/probe-home-state.js +0 -228
  493. package/dist/clients/process-bridge.js +0 -66
  494. package/dist/clients/process-singletons.js +0 -210
  495. package/dist/clients/process-snapshot.js +0 -68
  496. package/dist/clients/project-changes.js +0 -181
  497. package/dist/clients/project-conventions.js +0 -171
  498. package/dist/clients/project-diagnostics/cache.js +0 -137
  499. package/dist/clients/project-diagnostics/extractors.js +0 -109
  500. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  501. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  502. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  503. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  504. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  505. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  506. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  507. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  508. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  509. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  510. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  511. package/dist/clients/project-diagnostics/scanner.js +0 -520
  512. package/dist/clients/project-diagnostics/types.js +0 -1
  513. package/dist/clients/project-lens-config.js +0 -807
  514. package/dist/clients/project-report.js +0 -701
  515. package/dist/clients/project-scale.js +0 -237
  516. package/dist/clients/project-scan-policy.js +0 -11
  517. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  518. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  519. package/dist/clients/project-snapshot.js +0 -1653
  520. package/dist/clients/project-trust.js +0 -181
  521. package/dist/clients/python-environment.js +0 -217
  522. package/dist/clients/python-provenance.js +0 -639
  523. package/dist/clients/quiet-window-config.js +0 -47
  524. package/dist/clients/quiet-window.js +0 -236
  525. package/dist/clients/read-bridge.js +0 -139
  526. package/dist/clients/read-expansion.js +0 -266
  527. package/dist/clients/read-guard-logger.js +0 -251
  528. package/dist/clients/read-guard-tool-lines.js +0 -1306
  529. package/dist/clients/read-guard.js +0 -1561
  530. package/dist/clients/recent-touches.js +0 -240
  531. package/dist/clients/redact/secrets.js +0 -449
  532. package/dist/clients/resource-sampler.js +0 -628
  533. package/dist/clients/reverse-deps.js +0 -270
  534. package/dist/clients/review-graph/builder.js +0 -5126
  535. package/dist/clients/review-graph/format.js +0 -33
  536. package/dist/clients/review-graph/git-identity.js +0 -163
  537. package/dist/clients/review-graph/import-resolvers.js +0 -481
  538. package/dist/clients/review-graph/persist-worker.js +0 -10
  539. package/dist/clients/review-graph/query.js +0 -244
  540. package/dist/clients/review-graph/revision-drift.js +0 -21
  541. package/dist/clients/review-graph/service.js +0 -96
  542. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  543. package/dist/clients/review-graph/symbol-id.js +0 -103
  544. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  545. package/dist/clients/review-graph/types.js +0 -1
  546. package/dist/clients/review-graph/workspace-modules.js +0 -449
  547. package/dist/clients/review-graph-logger.js +0 -81
  548. package/dist/clients/ruff-client.js +0 -179
  549. package/dist/clients/rules-scanner.js +0 -131
  550. package/dist/clients/run-duration.js +0 -55
  551. package/dist/clients/runtime-agent-end.js +0 -746
  552. package/dist/clients/runtime-config.js +0 -77
  553. package/dist/clients/runtime-context.js +0 -211
  554. package/dist/clients/runtime-coordinator.js +0 -1324
  555. package/dist/clients/runtime-session.js +0 -2485
  556. package/dist/clients/runtime-tool-call.js +0 -1295
  557. package/dist/clients/runtime-tool-result.js +0 -2049
  558. package/dist/clients/runtime-turn.js +0 -3767
  559. package/dist/clients/rust-client.js +0 -97
  560. package/dist/clients/safe-spawn.js +0 -2357
  561. package/dist/clients/sanitize.js +0 -286
  562. package/dist/clients/scan-utils.js +0 -25
  563. package/dist/clients/scratch-tree-policy.js +0 -165
  564. package/dist/clients/search-read-registration.js +0 -94
  565. package/dist/clients/secret-findings.js +0 -114
  566. package/dist/clients/security-scan-client.js +0 -352
  567. package/dist/clients/session-event-guard.js +0 -297
  568. package/dist/clients/session-lifecycle.js +0 -484
  569. package/dist/clients/session-start-observability.js +0 -94
  570. package/dist/clients/session-state-store.js +0 -101
  571. package/dist/clients/session-summary.js +0 -15
  572. package/dist/clients/sessionstart-logger.js +0 -27
  573. package/dist/clients/sg-runner.js +0 -876
  574. package/dist/clients/sgconfig.js +0 -347
  575. package/dist/clients/shared-checkout-guard.js +0 -338
  576. package/dist/clients/single-flight.js +0 -158
  577. package/dist/clients/situational-tool-telemetry.js +0 -108
  578. package/dist/clients/skills-resolver.js +0 -141
  579. package/dist/clients/slow-fs.js +0 -138
  580. package/dist/clients/smells-rollup.js +0 -231
  581. package/dist/clients/source-filter.js +0 -596
  582. package/dist/clients/source-walker.js +0 -211
  583. package/dist/clients/spawn-output-cap.js +0 -120
  584. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  585. package/dist/clients/stale-marker.js +0 -17
  586. package/dist/clients/startup-scan.js +0 -400
  587. package/dist/clients/startup-timing.js +0 -130
  588. package/dist/clients/string-utils.js +0 -33
  589. package/dist/clients/subagent-mode.js +0 -111
  590. package/dist/clients/symbol-containment.js +0 -24
  591. package/dist/clients/symbol-types.js +0 -5
  592. package/dist/clients/test-runner-client.js +0 -2507
  593. package/dist/clients/test-runner-delivery.js +0 -287
  594. package/dist/clients/todo-scanner.js +0 -229
  595. package/dist/clients/tool-agreement.js +0 -423
  596. package/dist/clients/tool-config.js +0 -249
  597. package/dist/clients/tool-cwd.js +0 -270
  598. package/dist/clients/tool-definition.js +0 -47
  599. package/dist/clients/tool-event.js +0 -105
  600. package/dist/clients/tool-policy.js +0 -2755
  601. package/dist/clients/tool-probe.js +0 -53
  602. package/dist/clients/tool-render.js +0 -156
  603. package/dist/clients/tool-set-policy.js +0 -118
  604. package/dist/clients/tree-sitter-cache.js +0 -364
  605. package/dist/clients/tree-sitter-client.js +0 -4195
  606. package/dist/clients/tree-sitter-logger.js +0 -100
  607. package/dist/clients/tree-sitter-navigator.js +0 -269
  608. package/dist/clients/tree-sitter-query-loader.js +0 -569
  609. package/dist/clients/tree-sitter-shared.js +0 -158
  610. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  611. package/dist/clients/trivy-client.js +0 -467
  612. package/dist/clients/tui-fit.js +0 -50
  613. package/dist/clients/turn-context.js +0 -52
  614. package/dist/clients/turn-end/lane.js +0 -51
  615. package/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  616. package/dist/clients/turn-end/lanes/secrets.js +0 -204
  617. package/dist/clients/turn-summary-render.js +0 -72
  618. package/dist/clients/turn-summary.js +0 -132
  619. package/dist/clients/typos-config.js +0 -29
  620. package/dist/clients/user-notify.js +0 -51
  621. package/dist/clients/vanished-instance-marker.js +0 -57
  622. package/dist/clients/warm-attach.js +0 -305
  623. package/dist/clients/widget-state.js +0 -1808
  624. package/dist/clients/word-index-logger.js +0 -49
  625. package/dist/clients/word-index-store.js +0 -459
  626. package/dist/clients/word-index.js +0 -2080
  627. package/dist/clients/workspace-topology.js +0 -443
  628. package/dist/clients/write-ordering-guard.js +0 -65
  629. package/dist/clients/zizmor-config.js +0 -340
  630. package/dist/i18n.js +0 -69
  631. package/dist/mcp/build-staleness.js +0 -124
  632. package/dist/scripts/lib/process-scan.mjs +0 -583
  633. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  634. package/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  635. package/dist/tools/activate-tools.js +0 -87
  636. package/dist/tools/ast-grep-outline.js +0 -137
  637. package/dist/tools/ast-grep-replace.js +0 -244
  638. package/dist/tools/ast-grep-search.js +0 -762
  639. package/dist/tools/effective-config.js +0 -90
  640. package/dist/tools/lens-diagnostic-mark.js +0 -375
  641. package/dist/tools/lens-diagnostics.js +0 -2571
  642. package/dist/tools/lsp-diagnostics.js +0 -1745
  643. package/dist/tools/lsp-navigation.js +0 -1367
  644. package/dist/tools/lsp-structured-output.js +0 -294
  645. package/dist/tools/module-report.js +0 -382
  646. package/dist/tools/project-report.js +0 -86
  647. package/dist/tools/render-compact.js +0 -374
  648. package/dist/tools/scan-progress.js +0 -53
  649. package/dist/tools/shared.js +0 -29
  650. package/dist/tools/symbol-search.js +0 -155
@@ -1,1745 +0,0 @@
1
- /**
2
- * lsp_diagnostics tool definition
3
- *
4
- * Proactive LSP diagnostics check — single files or directories.
5
- * Adopted from code-yeongyu/pi-lsp-client design.
6
- */
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
- import { getProjectIgnoreMatcher, isExcludedDirName, } from "../clients/file-utils.js";
10
- import { getLSPService, groupFilesByPrimaryServer, runPerServerGroups, } from "../clients/lsp/index.js";
11
- import { buildScopeKey, createWorkspaceDiagnosticsCacheContext, } from "../clients/lsp/workspace-diagnostics-cache.js";
12
- import { getServersForFileWithConfig, primaryServerId, } from "../clients/lsp/config.js";
13
- import { mapWithConcurrency } from "../clients/map-with-concurrency.js";
14
- import { combineAbortSignals, withDeadline, } from "../clients/deadline-utils.js";
15
- import { applyAuxiliarySuppressions, retagAuxiliaryDiagnostics, } from "../clients/dispatch/auxiliary-lsp.js";
16
- import { applyLspFindingPolicy, loadProjectRulePolicyMap, } from "../clients/dispatch/finding-policy.js";
17
- import { hasStrictDispositionMarks } from "../clients/diagnostic-dispositions.js";
18
- import { recordDegradationOnce } from "../clients/degradation-ledger.js";
19
- import { logLatency } from "../clients/latency-logger.js";
20
- import { detectFileRole } from "../clients/file-role.js";
21
- import { hashDiagnosticContent, touchCompletedConfirmationPolicy, touchCoverageGap, } from "../clients/lsp/diagnostic-binding.js";
22
- import { classifyCascadeWaitTier } from "../clients/lsp/wait-policy/index.js";
23
- import { attemptTsserverSyncDiagnostics } from "../clients/lsp/tsserver-sync.js";
24
- import { convertLspDiagnostics } from "../clients/dispatch/utils/lsp-diagnostics.js";
25
- import { demoteInferredProjectDiagnostics } from "../clients/lsp/inferred-project.js";
26
- import { normalizeMapKey } from "../clients/path-utils.js";
27
- import { countRetainedSuppressedRows, isBlocking, reconcileScanDiagnostics, } from "../clients/widget-state.js";
28
- import { makeProgressReporter } from "./scan-progress.js";
29
- import { isWarmAttached, tryWarmAttachedDiagnostics, } from "../clients/warm-attach.js";
30
- import { extensionsForLanguage, SCAN_LANGUAGE_PRIORITY, } from "../clients/language-registry.js";
31
- import { exceedsLspSyncLimits } from "../clients/lsp/content-limits.js";
32
- const MAX_FILES = 100;
33
- export const MAX_BATCH_FILES = 100;
34
- const MAX_DIAGNOSTICS = 200;
35
- const DEFAULT_BATCH_CONCURRENCY = 8;
36
- const MAX_BATCH_CONCURRENCY = 16;
37
- const DEFAULT_BATCH_FILE_DEADLINE_MS = 15_000;
38
- // LSP severities: 1=Error, 2=Warning, 3=Information, 4=Hint
39
- export const SEVERITY_NAMES = {
40
- 1: "error",
41
- 2: "warning",
42
- 3: "information",
43
- 4: "hint",
44
- };
45
- export const LSP_SEVERITY_FILTERS = [
46
- ...Object.values(SEVERITY_NAMES),
47
- "all",
48
- ];
49
- function assertNeverBatchFileOutcome(value) {
50
- throw new Error(`Unhandled batch file outcome: ${String(value)}`);
51
- }
52
- /** Render the bounded per-file outcomes shared by batch, directory, and fold surfaces. */
53
- export function renderBatchOutcomeLines(outcomes, formatFile = (file) => file) {
54
- const lines = [];
55
- for (const outcome of outcomes) {
56
- switch (outcome.outcome) {
57
- case "too_large":
58
- lines.push(`${formatFile(outcome.file)}: ${outcome.reason ?? "file too large for LSP diagnostics"}`);
59
- break;
60
- case "clean":
61
- case "findings":
62
- case "unsupported":
63
- case "unavailable":
64
- case "failed":
65
- case "inconclusive":
66
- break;
67
- default:
68
- assertNeverBatchFileOutcome(outcome.outcome);
69
- }
70
- }
71
- return lines;
72
- }
73
- function batchFileDeadlineMs() {
74
- const raw = Number(process.env.PI_LENS_LSP_BATCH_FILE_MS);
75
- return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_BATCH_FILE_DEADLINE_MS;
76
- }
77
- // #646: `primaryServerId` moved to clients/lsp/config.ts so this tool and
78
- // tools/lens-diagnostics.ts's mode=full sweep share the exact same
79
- // primary-vs-auxiliary classification instead of each keeping its own copy.
80
- function lspUnavailableMessage(filePath, health) {
81
- if (!health || !String(health.health ?? "").startsWith("no_clients")) {
82
- return undefined;
83
- }
84
- const candidates = health.candidateServerIds?.length
85
- ? ` candidates=${health.candidateServerIds.join(",")}`
86
- : "";
87
- const reason = (health.serverCountAttempted ?? 0) === 0
88
- ? "no LSP server configured"
89
- : "no LSP client is currently ready";
90
- const stale = (health.mergedCount ?? 0) > 0
91
- ? " Showing stale last-known diagnostics below."
92
- : " No diagnostics were collected.";
93
- return `LSP unavailable for ${filePath}: ${reason}; ready=${health.serverCountReady ?? 0}/${health.serverCountAttempted ?? 0}.${candidates}.${stale}`;
94
- }
95
- function boundedPositiveInt(value, fallback, min, max) {
96
- const parsed = typeof value === "number" ? Math.floor(value) : Number.NaN;
97
- if (!Number.isFinite(parsed))
98
- return fallback;
99
- return Math.max(min, Math.min(max, parsed));
100
- }
101
- /**
102
- * #631: fan `mapper` out across `items` (a batch/directory file list) while
103
- * respecting per-LSP-server affinity — previously this was a flat,
104
- * server-oblivious bounded-concurrency pool (up to `concurrency` files
105
- * in flight at once, regardless of which server they belonged to). That let
106
- * a single-language batch (the common case) fire many concurrent touches at
107
- * the SAME shared, single-threaded LSP server — exactly the pattern #387
108
- * found doesn't parallelize (it queues server-side and cascades per-file
109
- * timeouts by queue position) and that `runWorkspaceDiagnostics` (the engine
110
- * behind `lens_diagnostics mode=full`) has been protected against since #387.
111
- *
112
- * Groups `items` by primary server via `groupFilesByPrimaryServer` (the same
113
- * grouping key `runWorkspaceDiagnostics` uses) and schedules them with
114
- * `runPerServerGroups` (both extracted from `clients/lsp/index.ts` so this
115
- * tool shares the real primitive instead of a second hand-copied
116
- * implementation): at most one in-flight `mapper` call per server group,
117
- * parallelized across distinct groups up to `concurrency`. A single-language
118
- * batch collapses to one group and runs effectively serially regardless of
119
- * `concurrency` — the CORRECT, intended #387 behavior, not a regression.
120
- *
121
- * Result order matches `items`' original order (not completion order),
122
- * matching the old flat pool's positional-assignment behavior — callers may
123
- * depend on `results[i]` corresponding to `items[i]`.
124
- *
125
- * #667: before a group's own per-file loop starts, calls the shared
126
- * `LSPService.ensureWarmForSweep` warm-check/ensure-warm step (the same one
127
- * `runWorkspaceDiagnostics` uses for `lens_diagnostics mode=full`) against
128
- * the group's first file — a no-op when that group's primary server already
129
- * demonstrated readiness earlier this session, one bounded warm-up round
130
- * trip otherwise. Fixes the first-few-files-eat-cold-start-timeouts pattern
131
- * for THIS tool's batch/directory sweep the same way #667 fixed it for the
132
- * workspace-diagnostics sweep.
133
- */
134
- /**
135
- * Project-ignore predicate rooted at `root`, fail-open. Lets a directory scan
136
- * honor the user's `.pi-lens.json` / `.gitignore` patterns — not just the
137
- * canonical dir-name list — so `lsp_diagnostics` stays consistent with the
138
- * workspace-diagnostics walk and every other scan surface (#243/#297/#298). A
139
- * config-probe error never blocks a scan (matches the walkers' behaviour).
140
- */
141
- function projectIgnorePredicate(root) {
142
- try {
143
- const matcher = getProjectIgnoreMatcher(root);
144
- return (fullPath, isDir) => matcher.isIgnored(fullPath, isDir);
145
- }
146
- catch {
147
- return () => false;
148
- }
149
- }
150
- /**
151
- * #1137: async so a directory read never blocks the event loop (and pi's TUI).
152
- *
153
- * This walk was fully synchronous with NO yielding of any kind, bounded only by
154
- * `maxFiles` *kept* — an ignored-heavy or cloud-backed (OneDrive/network) tree
155
- * could traverse unboundedly many entries, and a single stalled `readdirSync`
156
- * held the loop for the whole stall. It is also called once PER FAMILY in
157
- * `runDirectoryDiagnostics`'s `SCAN_LANGUAGE_PRIORITY` loop (#2434, grouped by
158
- * family since #2458 fix-round F1), so a directory-mode `lsp_diagnostics`
159
- * could pay that cost several times over.
160
- *
161
- * The traversal is deliberately still **depth-first with immediate descent**
162
- * (not the shared stack-based `walkTreeStackAsync`): the `maxFiles` cap makes
163
- * traversal ORDER observable — a stack walk would keep a different subset of
164
- * files on an over-large tree. Only scheduling changes here; the returned list
165
- * is identical. Awaiting each directory read is itself the yield point.
166
- */
167
- async function collectFiles(dir, extensions, maxFiles, isIgnored = () => false) {
168
- const files = [];
169
- async function walk(current) {
170
- if (files.length >= maxFiles)
171
- return;
172
- let entries;
173
- try {
174
- entries = await fs.promises.readdir(current, { withFileTypes: true });
175
- }
176
- catch {
177
- return;
178
- }
179
- for (const entry of entries) {
180
- if (files.length >= maxFiles)
181
- return;
182
- if (entry.isSymbolicLink())
183
- continue;
184
- const full = path.join(current, entry.name);
185
- if (entry.isDirectory()) {
186
- if (!isExcludedDirName(entry.name) && !isIgnored(full, true))
187
- await walk(full);
188
- }
189
- else if (entry.isFile() && extensions.includes(path.extname(full))) {
190
- if (isIgnored(full, false))
191
- continue;
192
- files.push(full);
193
- }
194
- }
195
- }
196
- await walk(dir);
197
- return files;
198
- }
199
- export function createLspDiagnosticsTool(
200
- // #571/#1198: same shared write-ordering token source `lens_diagnostics`
201
- // mode=full uses (index.ts injects `() => runtime.nextWriteIndex()`). A
202
- // confirmed result reconciled into the footer reserves its token when the
203
- // per-file check starts, so settlement order cannot make an older result look
204
- // newer. Optional/undefined in tests.
205
- nextWriteIndex,
206
- // #1561: #571 wired this tool's confirmed result into the widget footer and
207
- // stopped there. The inline-blocker map is a SECOND agent-facing store fed by
208
- // the same dispatch verdict, and nothing retired it — so pi-lens kept
209
- // injecting "Unresolved from this turn" for three more turns after answering
210
- // "confirmed clean" for the same file. index.ts injects
211
- // `runtime.retireInlineBlockerOnConfirmedClean`. Optional/undefined in tests.
212
- onConfirmedNoBlockers, getService = getLSPService) {
213
- return {
214
- async execute(_toolCallId, params, _signal, onUpdate, ctx) {
215
- // Escape aborts the turn via ctx.signal; honor both it and the tool-call
216
- // signal so a batch/directory scan cancels rather than grinding on.
217
- const signal = combineAbortSignals(_signal, ctx.signal);
218
- // Stream a throttled progress bar for batch/directory scans (opaque for
219
- // seconds-to-minutes otherwise).
220
- const onProgress = makeProgressReporter(onUpdate, "Scanning LSP diagnostics");
221
- const typedParams = params;
222
- const severity = (typedParams.severity ?? "all");
223
- const cwd = ctx.cwd ?? process.cwd();
224
- const concurrency = boundedPositiveInt(typedParams.concurrency, DEFAULT_BATCH_CONCURRENCY, 1, MAX_BATCH_CONCURRENCY);
225
- const waitMs = typeof typedParams.waitMs === "number" && typedParams.waitMs >= 0
226
- ? Math.floor(typedParams.waitMs)
227
- : undefined;
228
- const serverScope = typedParams.serverScope === "primary" ? "primary" : "all";
229
- const lspService = getService();
230
- if (!lspService) {
231
- return {
232
- content: [
233
- { type: "text", text: "LSP service not available." },
234
- ],
235
- isError: true,
236
- details: {},
237
- };
238
- }
239
- if (Array.isArray(typedParams.paths) && typedParams.paths.length > 0) {
240
- if (typedParams.paths.length > MAX_BATCH_FILES) {
241
- return {
242
- content: [
243
- {
244
- type: "text",
245
- text: `paths accepts at most ${MAX_BATCH_FILES} files; received ${typedParams.paths.length}.`,
246
- },
247
- ],
248
- isError: true,
249
- details: { mode: "batch", filesChecked: 0 },
250
- };
251
- }
252
- const rawPaths = typedParams.paths.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
253
- if (rawPaths.length !== typedParams.paths.length) {
254
- return {
255
- content: [
256
- {
257
- type: "text",
258
- text: "paths must contain non-empty file paths.",
259
- },
260
- ],
261
- isError: true,
262
- details: { mode: "batch", filesChecked: 0 },
263
- };
264
- }
265
- // Preserve input order (including duplicate entries); normalize each
266
- // path before grouping so Windows separators and dot segments cannot
267
- // change cache/group identity. Explicit lists never enter the walker.
268
- // #3160/#3182: `path.resolve(cwd, entry)` folds dot segments (and
269
- // separators) structurally — it ignores `cwd` whenever `entry` is
270
- // already absolute, so the old `path.isAbsolute` ternary is
271
- // redundant. `normalizeMapKey` then adopts on-disk CASING where the
272
- // path exists — an agent-typed, possibly mis-cased `paths` entry
273
- // must key `reconcileScanDiagnostics`'s widget-state write the same
274
- // way a canonical writer (clients/pipeline.ts's ctx.filePath) or the
275
- // #3160-fixed lens_diagnostic_mark reader would. `path.resolve`
276
- // stays required for the relative→absolute step: `normalizeMapKey`
277
- // folds dot segments itself since #3184, but never resolves against
278
- // `cwd` (a cwd fold there broke every monorepo, #2490).
279
- const absPaths = rawPaths.map((entry) => normalizeMapKey(path.resolve(cwd, entry)));
280
- return runBatchFileDiagnostics(absPaths, severity, lspService, {
281
- concurrency,
282
- waitMs,
283
- signal,
284
- onProgress,
285
- nextWriteIndex,
286
- serverScope,
287
- cwd,
288
- onConfirmedNoBlockers,
289
- });
290
- }
291
- const rawPath = typedParams.path;
292
- if (!rawPath || rawPath.trim().length === 0) {
293
- return {
294
- content: [
295
- {
296
- type: "text",
297
- text: "path or paths is required.",
298
- },
299
- ],
300
- isError: true,
301
- details: {},
302
- };
303
- }
304
- // #3184: the single-`path` sibling of the `paths` batch above, and
305
- // keyed the same way — `runFileDiagnostics` hands this straight to
306
- // `reconcileScanDiagnostics`, whose `normalizeEphemeralMapKey` key
307
- // derivation folds neither dot segments nor casing. Same expression as
308
- // :444 so both modes of this tool write under ONE key per file.
309
- const absPath = normalizeMapKey(path.resolve(cwd, rawPath));
310
- let stat;
311
- try {
312
- stat = fs.statSync(absPath);
313
- }
314
- catch {
315
- return {
316
- content: [
317
- { type: "text", text: `Path not found: ${absPath}` },
318
- ],
319
- isError: true,
320
- details: {},
321
- };
322
- }
323
- if (stat.isDirectory()) {
324
- return runDirectoryDiagnostics(absPath, severity, lspService, {
325
- concurrency,
326
- waitMs,
327
- signal,
328
- onProgress,
329
- nextWriteIndex,
330
- serverScope,
331
- cwd,
332
- onConfirmedNoBlockers,
333
- });
334
- }
335
- return runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope, cwd, onConfirmedNoBlockers);
336
- },
337
- };
338
- }
339
- async function collectDiagnosticsForFile(absPath, lspService,
340
- /**
341
- * #3041: project root the per-rule `ignores` globs (#965) are resolved
342
- * against. ast-grep's LSP publishes per-document diagnostics without
343
- * applying them, so this standalone `source=lsp` query has to — the same
344
- * carve-out the NAPI runner applies on the same file. Positioned before the
345
- * optional parameters so it is structurally REQUIRED: an optional
346
- * `string | undefined` would silently disable the carve-out for whichever
347
- * caller forgot to pass it.
348
- */
349
- scanRoot, waitMs, serverScope = "all") {
350
- let timedOut = false;
351
- let content;
352
- // `touchFile` is the authoritative collection boundary for every scope: it
353
- // preserves per-touch timeout, content-binding, and silent-clean confirmation
354
- // metadata while returning diagnostics from only the requested clients. The
355
- // getDiagnostics fallback below remains for the two states a REAL service
356
- // still reaches: the file read threw (no content to touch with), or the
357
- // touch resolved no clients and returned `undefined` (clients/lsp/index.ts
358
- // `touchFile`'s `no_clients`/`destroyed` returns). #2598 deleted the third
359
- // reason — "an older/mock service without touchFile" — because the real
360
- // `LSPService` has always defined the method unconditionally, so that arm
361
- // was reachable only from a partial test double (AGENTS.md shape 7).
362
- // #1179: the result is an explicit wrapper so side-channel fields survive
363
- // array copies; confirmation was added when Marksman's lower-level clean verdict
364
- // proved that a successful empty collection also needs explicit provenance.
365
- let touched;
366
- try {
367
- content = fs.readFileSync(absPath, "utf-8");
368
- const contentLimit = exceedsLspSyncLimits(content);
369
- if (contentLimit.tooLarge) {
370
- const reason = `file too large for LSP diagnostics (${contentLimit.reason})`;
371
- recordDegradationOnce({
372
- kind: "lsp-diagnostics-file-too-large",
373
- subject: absPath,
374
- reason,
375
- });
376
- return {
377
- diagnostics: [],
378
- timedOut: false,
379
- confirmedByTouch: false,
380
- unconfirmedServerIds: [],
381
- diagnosticsUnsupportedServerIds: [],
382
- content,
383
- tooLargeReason: reason,
384
- };
385
- }
386
- if (isWarmAttached()) {
387
- // The local sweep's default service wait is bounded to the same
388
- // 15-second per-file envelope used by the workspace sweep. An explicit
389
- // waitMs remains the tighter caller-selected cap.
390
- const attachedBudgetMs = waitMs ?? 15_000;
391
- const attached = await tryWarmAttachedDiagnostics(absPath, content, attachedBudgetMs, "sweep");
392
- if (attached?.available) {
393
- const scopedDiagnostics = serverScope === "primary"
394
- ? attached.response.diagnostics.filter((item) => item.serverId === primaryServerId(absPath))
395
- : attached.response.diagnostics;
396
- const filtered = applyAuxiliarySuppressions(scopedDiagnostics, content, {
397
- fileRole: detectFileRole(absPath, content),
398
- filePath: absPath,
399
- scanRoot,
400
- });
401
- return {
402
- diagnostics: filtered,
403
- timedOut: false,
404
- // #1253: the incumbent's touch carries the same confirmation
405
- // provenance a local touch does (an `available: true` answer is
406
- // already gated on `fresh && !inconclusive`, but that is NOT
407
- // evidence a silent-on-clean server was confirmed — only the
408
- // explicit flag is). The incumbent always touches with
409
- // `clientScope: "with-auxiliary"`, so this is an AGGREGATE
410
- // confirmation: it carries the same caveat all-scope local
411
- // touches do, and classic TypeScript still needs the primary-only
412
- // tsserver sync check below rather than this verdict.
413
- // #1470: `"partial"` counts, for the same reason the local touch
414
- // branch below accepts it — the incumbent's primary confirmed; only
415
- // a named auxiliary did not.
416
- confirmedByTouch: attached.response.confirmation !== undefined &&
417
- primaryServerId(absPath) !== "typescript",
418
- // #1470: the incumbent's narrowed verdict, carried as an explicit DTO
419
- // field across the socket. An older incumbent omits it → empty → the
420
- // pre-#1470 handling, unchanged.
421
- unconfirmedServerIds: attached.response.unconfirmedServerIds ?? [],
422
- diagnosticsUnsupportedServerIds: [],
423
- content,
424
- };
425
- }
426
- }
427
- touched = await lspService.touchFile(absPath, content, {
428
- diagnostics: "document",
429
- collectDiagnostics: true,
430
- maxClientWaitMs: waitMs,
431
- source: "lsp_diagnostics",
432
- clientScope: serverScope,
433
- // #3405: the caller asked whether THIS file is clean right now, and the
434
- // content above was just read from disk — so it is the file's saved
435
- // state. Without the save a save-triggered server answers zero
436
- // diagnostics for every file in the query, which this path would report
437
- // as clean.
438
- saved: true,
439
- });
440
- timedOut = touched?.inconclusive === true;
441
- }
442
- catch {
443
- // Non-fatal: getDiagnostics may still have stale/health information.
444
- }
445
- // Only fall through to the unscoped getDiagnostics() read when the touch
446
- // produced nothing to read — it threw, the file read threw before it ran, or
447
- // it resolved no clients at all. When touched IS defined it's already the
448
- // answer — reusing it is what makes serverScope:"primary" actually skip
449
- // auxiliary scanners and drops the common case back to a single LSP round
450
- // trip instead of two.
451
- const diagnostics = touched !== undefined
452
- ? touched.diags
453
- : await lspService.getDiagnostics(absPath, waitMs !== undefined ? "document" : "full");
454
- // #586: honor each auxiliary profile's native inline-suppression comment
455
- // (e.g. opengrep's `// nosemgrep`, #441) the same way the per-edit dispatch
456
- // runner does — previously this standalone query path ignored it entirely.
457
- // #692: also honor a profile's `skipTestFiles` gate (e.g. ast-grep, #687) —
458
- // this standalone-query path had no test-file gating of its own, so an
459
- // `lsp_diagnostics` check on a test file surfaced ast-grep findings the
460
- // per-edit dispatch runner would have suppressed. `content` is only unset
461
- // if the read itself failed above; fail-open (no filtering) rather than
462
- // lose diagnostics over an unrelated read error.
463
- const filtered = content !== undefined
464
- ? applyAuxiliarySuppressions(diagnostics, content, {
465
- fileRole: detectFileRole(absPath, content),
466
- filePath: absPath,
467
- scanRoot,
468
- })
469
- : diagnostics;
470
- // #1095: surface the touch's content binding (only a touch that resolved
471
- // clients carries one; the getDiagnostics fallback leaves it undefined →
472
- // "unknown", no demotion).
473
- const binding = touched?.binding;
474
- // #1470: `"partial"` counts here. `confirmedByTouch` feeds
475
- // `canTrustTouchConfirmation`, which asks about the PRIMARY's own verdict —
476
- // and a partial touch is one whose primary confirmed while an auxiliary was
477
- // cut off. Excluding it would render "Primary LSP: unconfirmed" for a primary
478
- // that did confirm. The coverage gap is carried separately, below.
479
- const confirmedByTouch = touchCompletedConfirmationPolicy(touched);
480
- return {
481
- diagnostics: filtered,
482
- timedOut,
483
- skipReason: touched?.skipReason,
484
- confirmedByTouch,
485
- // #1470: only a touch that resolved clients contributes a coverage gap;
486
- // the getDiagnostics fallback never reports one, which is honest — that
487
- // path claims no confirmation at all.
488
- unconfirmedServerIds: touchCoverageGap(touched),
489
- diagnosticsUnsupportedServerIds: touched?.diagnosticsUnsupportedServerIds ?? [],
490
- content,
491
- binding,
492
- };
493
- }
494
- function diagnosticsToFileDiags(file, diagnostics) {
495
- return diagnostics.map((d) => ({
496
- file,
497
- line: d.range?.start?.line,
498
- character: d.range?.start?.character,
499
- severity: d.severity,
500
- message: d.message,
501
- source: d.source,
502
- serverId: d.serverId,
503
- code: d.code,
504
- }));
505
- }
506
- /**
507
- * #533: classify an EMPTY diagnostic result as "clean" (the server actually
508
- * confirmed no issues) or "unconfirmed" (came from a push-only,
509
- * silent-on-clean server — classic typescript-language-server — that
510
- * publishes nothing on a clean→clean transition, so an empty result here is
511
- * indistinguishable from "still analyzing" or "never asked"). Reuses the same
512
- * capability-snapshot classifier the #458 cascade lane already trusts
513
- * (`classifyCascadeWaitTier`) so this tool's notion of "silent tier-3" stays
514
- * in lockstep with the rest of the LSP layer instead of drifting via a second
515
- * copy of the server-strategy table. Fail-safe: any error or missing snapshot
516
- * (server not alive, capability probe failure) reads as "clean" — the same
517
- * default this tool has always had — rather than manufacturing a new failure
518
- * mode from a best-effort classification.
519
- */
520
- async function classifyEmptyResult(file, lspService) {
521
- try {
522
- const snapshots = await lspService.getCapabilitySnapshots(file);
523
- const tier = classifyCascadeWaitTier(lspService, file, snapshots);
524
- return tier === "tier3-silent" ? "unconfirmed" : "clean";
525
- }
526
- catch {
527
- return "clean";
528
- }
529
- }
530
- // --- #611/#707: tier-3 silent escape hatch (typescript.tsserverRequest sync
531
- // commands) — implementation extracted to clients/lsp/tsserver-sync.ts and
532
- // re-used from there by the per-edit dispatch path (#707). ---
533
- /**
534
- * #611: resolve an EMPTY diagnostic result for a Tier-3 silent server (see
535
- * `classifyCascadeWaitTier` — today only classic typescript-language-server,
536
- * native-ts7 is explicitly excluded there) with a definitive answer instead of
537
- * defaulting straight to "unconfirmed". `confirmed: true` with an empty
538
- * `diagnostics` array is a genuinely confirmed clean result; `confirmed: true`
539
- * with a non-empty array means the sync command surfaced real diagnostics the
540
- * server had computed but never published (silentOnClean) — these must be
541
- * surfaced to the caller, not discarded. `confirmed: false` is the existing
542
- * "unconfirmed" fallback (command unavailable, error, or the file isn't part
543
- * of any project). Fail-safe: any error in the tier classification itself
544
- * (missing snapshot, server not alive) reads as `confirmed: true` with no
545
- * diagnostics — the same "clean" default this tool has always had.
546
- */
547
- async function resolveEmptyResult(file, lspService) {
548
- try {
549
- const snapshots = await lspService.getCapabilitySnapshots(file);
550
- const tier = classifyCascadeWaitTier(lspService, file, snapshots);
551
- if (tier !== "tier3-silent") {
552
- return { confirmed: true, diagnostics: [] };
553
- }
554
- const syncDiagnostics = await attemptTsserverSyncDiagnostics(file, lspService);
555
- if (syncDiagnostics === undefined) {
556
- return { confirmed: false, diagnostics: [] };
557
- }
558
- return { confirmed: true, diagnostics: syncDiagnostics };
559
- }
560
- catch {
561
- return { confirmed: true, diagnostics: [] };
562
- }
563
- }
564
- /**
565
- * A primary-scope touch is authoritative because `touchFile` runs that
566
- * primary's own confirmation path (including TypeScript's sync fallback). For
567
- * all-scope TypeScript touches, the aggregate silent-clean gate can settle
568
- * without that primary-only sync request, so preserve `resolveEmptyResult`'s
569
- * existing tsserver fallback instead of accepting the aggregate verdict.
570
- */
571
- function canTrustTouchConfirmation(file, serverScope, confirmedByTouch) {
572
- return (confirmedByTouch &&
573
- (serverScope === "primary" || primaryServerId(file) !== "typescript"));
574
- }
575
- /**
576
- * #1561 F1: what this check is entitled to speak for.
577
- *
578
- * Inline blockers are raised by the whole dispatch — eslint, biome-check,
579
- * actionlint, ast-grep security rules — while this tool only ever consults
580
- * language servers. Retiring one therefore needs an explicit coverage claim,
581
- * and the honest claim is exactly the servers this check consulted and got an
582
- * answer from. `serverScope: "primary"` consults only the primary, so its claim
583
- * is `"lsp"` alone; an all-scope check adds each auxiliary server by id, which
584
- * is the same vocabulary `retagAuxiliaryDiagnostics` tags their findings with.
585
- *
586
- * A server named in `unconfirmedServerIds` is dropped: it produced no evidence
587
- * for this content, so claiming coverage for it would be the #1470/#1493 lie one
588
- * layer up. (Today an aux gap also demotes the whole verdict to "unconfirmed",
589
- * so the hook does not fire at all — this stays correct if that ever narrows.)
590
- */
591
- function coveredSourcesForCheck(file, serverScope, unconfirmedServerIds) {
592
- const unconfirmed = new Set(unconfirmedServerIds);
593
- const covered = new Set();
594
- let servers = [];
595
- try {
596
- servers = (getServersForFileWithConfig(file) ?? []);
597
- }
598
- catch {
599
- // A registry we cannot read is not a coverage claim. Returning an empty
600
- // set makes every retire fail closed, which is the safe direction for a
601
- // commit gate.
602
- return [];
603
- }
604
- for (const server of servers) {
605
- const auxiliary = server.role === "auxiliary";
606
- if (auxiliary && serverScope === "primary")
607
- continue;
608
- if (unconfirmed.has(server.id))
609
- continue;
610
- // The dispatch runner tags the primary language server's findings `lsp`;
611
- // auxiliaries keep their own tool id.
612
- covered.add(auxiliary ? server.id : "lsp");
613
- }
614
- return [...covered];
615
- }
616
- /**
617
- * #1645 review F3: `lsp_diagnostics` writes into the SAME widget store as
618
- * `lens_diagnostics mode=full`. Two tools writing one store must apply ONE rule
619
- * for "is this file's diagnostics demoted" — otherwise an orphan file blocks or
620
- * does not depending purely on which tool ran last, which is the #1633-V1
621
- * lesson. Every path in this file that reaches `reconcileWidgetFromLspResult`
622
- * or the rendered output routes its raw diagnostics through here first, so the
623
- * store, the counts, and the text all agree.
624
- *
625
- * Applied AFTER the confirmation verdict is computed, never before: confirmation
626
- * asks "did the server answer for this content", which the demotion must not
627
- * influence.
628
- */
629
- function demoteInferredForFile(file, diagnostics, cwd, lspService) {
630
- return demoteInferredProjectDiagnostics(diagnostics, {
631
- filePath: file,
632
- cwd,
633
- service: lspService,
634
- });
635
- }
636
- /**
637
- * #3088: the probe lane's stored-disposition / rule-policy / inline-suppression
638
- * gate — the same `clients/dispatch/finding-policy.ts` stack `mode=full` and
639
- * the per-edit dispatcher apply, called once per file at the position
640
- * `demoteInferredForFile` already established: AFTER the confirmation verdict
641
- * (which asks "did the server answer for this content" and must not be
642
- * influenced by a policy drop) and BEFORE the rendered output, the severity
643
- * filter and `reconcileWidgetFromLspResult`. One filtered set for all three, so
644
- * the text, the counts and the widget footer cannot disagree — and so the
645
- * footer reconcile can no longer re-arm a finding the mark-time
646
- * `reconcileWidgetDisposition` demoted.
647
- *
648
- * Fails OPEN: a filter that throws returns the unfiltered set plus one bounded
649
- * degradation record. A finding that stays visible is recoverable; one silently
650
- * hidden by a broken filter is the harm this lane exists to prevent
651
- * (AGENTS.md shape 48).
652
- */
653
- function applyProbeFindingPolicy(file, diagnostics, cwd, content,
654
- /**
655
- * Round 2 F1: the caller's severity threshold. The count reported to the
656
- * agent must be scoped to the SAME threshold the rendered result is, or a
657
- * `severity: "error"` probe of a repo carrying warning-level marks prints
658
- * "suppressed by disposition: N … Not a clean verdict" on a result that IS
659
- * clean at that threshold — the skill's own folder-check recipe would emit
660
- * the banner on every check. The threshold is applied HERE, in the tool that
661
- * owns both the render and the record, rather than in the three call sites
662
- * (three copies of one subtraction) or in
663
- * `clients/dispatch/finding-policy.ts`, which must stay threshold-free: its
664
- * other caller (`mode=full`) has no severity axis at all.
665
- */
666
- severity) {
667
- const started = Date.now();
668
- try {
669
- const result = applyLspFindingPolicy(diagnostics, {
670
- cwd,
671
- filePath: file,
672
- content,
673
- policyMap: loadProjectRulePolicyMap(cwd),
674
- fileRole: detectFileRole(file, content),
675
- });
676
- const inScopeBefore = applySeverityFilter(diagnostics, severity).length;
677
- const suppressed = inScopeBefore - applySeverityFilter(result.kept, severity).length;
678
- if (suppressed > 0) {
679
- // One record per FILE that actually dropped something the caller would
680
- // otherwise have SEEN, never one per finding (AGENTS.md "bounded
681
- // observability"). `total` is the in-scope population for the same
682
- // reason the count is, so the record and the rendered line agree.
683
- //
684
- // #3158 round 2 F4: `retainedSuppressed` is the widget store's
685
- // SUCCESS-path number — how many suppressed rows the store is carrying
686
- // INTO this scan, written by the scans before it. Until this, retention
687
- // was observable only when its per-file cap truncated
688
- // (`widget-suppressed-retention-capped`), so a healthy footer chip had no
689
- // record behind it at all. Folded into the record this lane already emits
690
- // rather than added as a second one: same cardinality (one per filtered
691
- // file per scan, never one per row), no new sink, and the two numbers are
692
- // read together — `suppressed` is what THIS scan dropped,
693
- // `retainedSuppressed` is what the footer chip is still counting.
694
- logLatency({
695
- type: "phase",
696
- toolName: "lsp_diagnostics",
697
- filePath: file,
698
- phase: "lsp_probe_disposition_filter",
699
- durationMs: Date.now() - started,
700
- metadata: {
701
- suppressed,
702
- total: inScopeBefore,
703
- retainedSuppressed: countRetainedSuppressedRows(file),
704
- },
705
- });
706
- }
707
- return { kept: result.kept, inlineKept: result.inlineKept, suppressed };
708
- }
709
- catch (err) {
710
- recordDegradationOnce({
711
- kind: "lsp-probe-finding-policy",
712
- subject: cwd,
713
- reason: err instanceof Error ? err.message : String(err),
714
- });
715
- return { kept: diagnostics, inlineKept: diagnostics, suppressed: 0 };
716
- }
717
- }
718
- /**
719
- * #3088 AC5/AC7: the content a cache REPLAY needs to evaluate STRICT
720
- * (`false-positive`) disposition anchors, which hash the finding's own line.
721
- * A cache hit means the file is unchanged since the entry was recorded, so this
722
- * is the same content the recording touch read — but it is a disk read the
723
- * replay path exists to avoid, so it is paid ONLY when the store actually holds
724
- * a strict mark. With no marks (the overwhelmingly common case) the probe path
725
- * adds zero I/O and the weak-anchored marks apply without content at all.
726
- */
727
- function replayContentForStrictMarks(file, cwd) {
728
- if (!hasStrictDispositionMarks(cwd))
729
- return undefined;
730
- try {
731
- return fs.readFileSync(file, "utf-8");
732
- }
733
- catch {
734
- // Fail open — an unreadable file degrades to the weak-anchored filter
735
- // rather than hiding or dropping anything.
736
- return undefined;
737
- }
738
- }
739
- function reconcileWidgetFromLspResult(file, rawDiags, confirmation, writeIndex, cwd, content,
740
- // #1095: true when the result's content binding demonstrably mismatches disk
741
- // (boundToCurrentDisk === false). Such a result — even a NON-EMPTY one, which
742
- // the pre-#1095 "non-empty is definitionally confirmed" doctrine would have
743
- // written straight through — must NOT re-cement the footer with a stale view
744
- // (#1092's window-1 transcript: 17 live-looking diagnostics while tsc exits
745
- // 0). "unknown"/true bindings leave the doctrine unchanged (fallback preserved
746
- // for servers that never bind a version).
747
- boundMismatch = false,
748
- // #1093: when these `rawDiags` were actually OBSERVED. Fresh touches observe
749
- // now (`undefined` → `Date.now()`); a cache HIT replays diagnostics scanned
750
- // earlier, so its caller passes the cache entry's `scannedAt` here so the
751
- // footer's `touchedAt` isn't re-armed to now() and the mtime-staleness gate
752
- // stays live (the #1092 re-arming defect).
753
- observedAt) {
754
- const confirmed = (rawDiags.length > 0 || confirmation !== "unconfirmed") && !boundMismatch;
755
- if (!confirmed)
756
- return { confirmed: false, blocking: true };
757
- try {
758
- // #692: provenance label ONLY — must never affect `rule`/identity (see
759
- // `ConvertLspDiagnosticsOptions.scanOrigin`'s doc comment).
760
- const diagnostics = convertLspDiagnostics(rawDiags, file, {
761
- scanOrigin: "lsp_diagnostics",
762
- });
763
- const retagged = retagAuxiliaryDiagnostics(diagnostics, rawDiags, content ?? "", { cwd, fileRole: detectFileRole(file, content) });
764
- const reconciled = reconcileScanDiagnostics(file, retagged, true, writeIndex, observedAt);
765
- if (!reconciled)
766
- return { confirmed: false, blocking: true };
767
- // #1561: the blocking tally comes from the SAME `isBlocking` predicate the
768
- // footer counts with — no second severity rule for the blocker-retire
769
- // decision to drift away from.
770
- return { confirmed: true, blocking: retagged.some(isBlocking) };
771
- }
772
- catch {
773
- // Never let a footer-reconciliation hiccup fail the diagnostics check.
774
- // A verdict we could not compute is NOT evidence of clean: report it as
775
- // still-blocking so no caller retires a blocker off a failed reconcile.
776
- return { confirmed: false, blocking: true };
777
- }
778
- }
779
- async function collectFileDiagnosticResult(file, severity, lspService, waitMs, nextWriteIndex, serverScope = "all",
780
- // #671: shared workspace-diagnostics cache (see `createWorkspaceDiagnostics
781
- // CacheContext`'s doc) — optional so single-file callers of this function
782
- // (there are none today, but keep it non-breaking) can omit it and simply
783
- // always touch. Only `collectBatchDiagnostics` (the batch/directory sweep)
784
- // passes these.
785
- cacheCtx, scopeKey,
786
- // #692: threaded through so `reconcileWidgetFromLspResult` can compute
787
- // `allowBlocking(cwd)` for a scan-reconciled aux finding — defaults to
788
- // `process.cwd()` for any call site that predates this (there are none
789
- // today besides the two below, both of which now pass it explicitly).
790
- cwd = process.cwd(),
791
- // #1561: see `createLspDiagnosticsTool`'s parameter doc. Called only for a
792
- // FRESH observation — never on the cache-hit replay below, whose `rawDiags`
793
- // are an OLD observation (#1093) and therefore no evidence about now.
794
- onConfirmedNoBlockers) {
795
- // Reserve the ordering token when this diagnostic operation starts, not when
796
- // its LSP promise settles. A slower old result must not receive a newer token
797
- // merely because it completed later (#1198).
798
- const writeIndex = nextWriteIndex?.();
799
- let stat;
800
- try {
801
- stat = fs.statSync(file);
802
- if (!stat.isFile()) {
803
- return { file, diagnostics: [], error: `${file}: not a file` };
804
- }
805
- }
806
- catch {
807
- return { file, diagnostics: [], error: `${file}: path not found` };
808
- }
809
- if (cacheCtx && scopeKey !== undefined) {
810
- const cached = cacheCtx.lookup(file, scopeKey);
811
- // #1095: a cached entry whose content binding demonstrably mismatches disk
812
- // (bytes changed without the mtime bump the freshness gate would catch) is
813
- // NOT served — fall through to a fresh touch instead of replaying a stale
814
- // cached result. "unknown"/true bindings serve as before.
815
- if (cached && cached.binding.boundToCurrentDisk !== false) {
816
- const confirmation = cached.diagnostics.length === 0 ? "clean" : undefined;
817
- // #1645 review F3: a cache REPLAY writes the widget store exactly like a
818
- // fresh touch does, so it gets the same demotion. Without this, replaying
819
- // yesterday's cached blocker would re-cement an orphan file as blocking
820
- // after mode=full had demoted it.
821
- const demotedCached = await demoteInferredForFile(file, cached.diagnostics, cwd, lspService);
822
- // #3088 AC5: a replay is served to the agent exactly like a fresh
823
- // observation, so it passes the same filter. The cached set is already
824
- // inline-suppression-clean (the record below stores `inlineKept`), so
825
- // only the revocable store/config filters run here.
826
- const cachedPolicy = applyProbeFindingPolicy(file, demotedCached, cwd, replayContentForStrictMarks(file, cwd), severity);
827
- const cachedDiags = cachedPolicy.kept;
828
- const filteredDiags = applySeverityFilter(cachedDiags, severity);
829
- // #692: cached.diagnostics were already suppression-/skipTestFiles-
830
- // filtered at write time (this same code path); no file content was
831
- // cached alongside them, so `undefined` here is a safe re-check, not
832
- // a gap.
833
- // #1093: this is a CACHE HIT — a replay of an OLD observation. Stamp the
834
- // footer's `touchedAt` with the cache entry's original `scannedAt`, NOT
835
- // now(), or a repeat check that only re-serves the cache would keep
836
- // re-arming the mtime-staleness gate and a resolved finding would render
837
- // forever (the #1092 defect).
838
- reconcileWidgetFromLspResult(file, cachedDiags, confirmation, writeIndex, cwd, undefined,
839
- // This branch only runs when the cache binding is not a mismatch
840
- // (guarded above), so boundMismatch is false; #1093's observedAt is
841
- // the cache entry's original scan time.
842
- false, cached.scannedAt);
843
- return {
844
- file,
845
- diagnostics: diagnosticsToFileDiags(file, filteredDiags),
846
- confirmation,
847
- primaryServerId: primaryServerId(file),
848
- ...(cachedPolicy.suppressed > 0 && {
849
- dispositionSuppressed: cachedPolicy.suppressed,
850
- }),
851
- };
852
- }
853
- }
854
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(file, lspService, cwd, waitMs, serverScope);
855
- if (tooLargeReason !== undefined) {
856
- return { file, diagnostics: [], outcome: "too_large", tooLargeReason };
857
- }
858
- const health = lspService.getDiagnosticsHealth?.(file);
859
- // #570: a timed-out priming check is never a confirmed "clean" — treat it
860
- // as unconfirmed without consulting the (unrelated) silent-tier
861
- // classifier, and remember why so the rendered text is accurate.
862
- // #611: a genuinely empty (not just severity-filtered-away) push-based
863
- // result gets a shot at the tier-3 sync escape hatch before "unconfirmed"
864
- // — it may surface real diagnostics the server never published, which must
865
- // be merged in rather than discarded.
866
- let effectiveRawDiags = rawDiags;
867
- let confirmation;
868
- if (diagnosticsUnsupportedServerIds.length > 0) {
869
- // No diagnostic provider and no push evidence is a capability boundary,
870
- // not a clean result and not a timeout.
871
- confirmation = undefined;
872
- }
873
- else if (skipReason !== undefined) {
874
- confirmation = "unconfirmed";
875
- }
876
- else if (timedOut) {
877
- if (applySeverityFilter(rawDiags, severity).length === 0) {
878
- confirmation = "unconfirmed";
879
- }
880
- }
881
- else if (canTrustTouchConfirmation(file, serverScope, confirmedByTouch)) {
882
- if (applySeverityFilter(rawDiags, severity).length === 0) {
883
- confirmation = "clean";
884
- }
885
- }
886
- else if (rawDiags.length === 0) {
887
- const resolved = await resolveEmptyResult(file, lspService);
888
- effectiveRawDiags = resolved.diagnostics;
889
- confirmation = resolved.confirmed
890
- ? resolved.diagnostics.length === 0
891
- ? "clean"
892
- : undefined
893
- : "unconfirmed";
894
- }
895
- else if (applySeverityFilter(rawDiags, severity).length === 0) {
896
- confirmation = await classifyEmptyResult(file, lspService);
897
- }
898
- // #1095: a result whose content binding demonstrably mismatches disk is
899
- // demoted to "unconfirmed" — it must neither confirm the footer nor be cached
900
- // as clean, regardless of how many diagnostics it carries (the #1092
901
- // re-cementing path). "unknown"/true bindings leave the verdict untouched.
902
- const boundMismatch = binding?.boundToCurrentDisk === false;
903
- if (boundMismatch)
904
- confirmation = "unconfirmed";
905
- // #1470/#1493: an auxiliary that never reported — cut off by the aux grace timer,
906
- // or silent with nothing published for this content — contributed no evidence
907
- // about this file, so a "clean" verdict computed from the merged result would
908
- // be claiming coverage this batch does not have. Demote it — that also keeps
909
- // the entry out of the workspace cache below, which would otherwise replay the
910
- // partial answer as a confirmed clean on every later sweep.
911
- if (unconfirmedServerIds.length > 0 && confirmation === "clean") {
912
- confirmation = "unconfirmed";
913
- }
914
- // #1645 review F3: one demotion rule for every writer of the widget store.
915
- effectiveRawDiags = await demoteInferredForFile(file, effectiveRawDiags, cwd, lspService);
916
- // #3088: one filter for the output, the counts and the footer reconcile.
917
- const policy = applyProbeFindingPolicy(file, effectiveRawDiags, cwd, collectedContent, severity);
918
- effectiveRawDiags = policy.kept;
919
- const filteredDiags = applySeverityFilter(effectiveRawDiags, severity);
920
- const verdict = reconcileWidgetFromLspResult(file, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
921
- if (verdict.confirmed && !verdict.blocking) {
922
- onConfirmedNoBlockers?.({
923
- filePath: file,
924
- writeIndex,
925
- coveredSources: coveredSourcesForCheck(file, serverScope, unconfirmedServerIds),
926
- cwd,
927
- });
928
- }
929
- // #671: only a CONFIRMED outcome ("clean", or a non-empty result — either
930
- // is definitionally confirmed per this function's own doctrine above) is
931
- // safe to cache; "unconfirmed" (timeout OR a silent-tier server's
932
- // unescapable empty push) must never be persisted as a cacheable clean
933
- // result — same false-clean bug class `runWorkspaceDiagnostics`'s cache
934
- // wiring guards against. #1095: the entry carries the content fingerprint so a
935
- // later lookup can verify it against disk beyond the mtime proxy.
936
- if (cacheCtx &&
937
- scopeKey !== undefined &&
938
- confirmation !== "unconfirmed" &&
939
- diagnosticsUnsupportedServerIds.length === 0 &&
940
- unconfirmedServerIds.length === 0) {
941
- // #3088: the entry records the INLINE-suppressed set, never the
942
- // disposition/rule-policy-filtered one. An inline `pi-lens-ignore` comment
943
- // cannot change without invalidating this content-keyed entry, so baking it
944
- // in is safe and makes every later replay consistent with a fresh probe; a
945
- // disposition mark or a `.pi-lens.json` rule can be revoked at any moment,
946
- // so caching THEIR effect would keep a finding hidden after its mark was
947
- // removed. The replay branch above re-applies those two on every serve.
948
- cacheCtx.record(file, scopeKey, policy.inlineKept, stat.mtimeMs, collectedContent !== undefined
949
- ? hashDiagnosticContent(collectedContent)
950
- : undefined, stat.size);
951
- }
952
- return {
953
- file,
954
- diagnostics: diagnosticsToFileDiags(file, filteredDiags),
955
- unavailable: lspUnavailableMessage(file, health),
956
- confirmation,
957
- timedOut: confirmation === "unconfirmed" ? timedOut : undefined,
958
- ...(skipReason !== undefined && { skipReason }),
959
- primaryServerId: primaryServerId(file),
960
- diagnosticsUnsupported: diagnosticsUnsupportedServerIds.length > 0,
961
- ...(policy.suppressed > 0 && {
962
- dispositionSuppressed: policy.suppressed,
963
- }),
964
- };
965
- }
966
- async function runFileDiagnostics(absPath, severity, lspService, waitMs, nextWriteIndex, serverScope = "all", cwd = process.cwd(),
967
- // #1561: see `createLspDiagnosticsTool`'s parameter doc. This is the path the
968
- // live incident took (`mode: "file"`).
969
- onConfirmedNoBlockers) {
970
- // Reserve the token before awaiting this file's LSP result. The direct-file
971
- // path performs its own confirmation/reconciliation below (#1198).
972
- const writeIndex = nextWriteIndex?.();
973
- const { diagnostics: rawDiags, timedOut, confirmedByTouch, unconfirmedServerIds, diagnosticsUnsupportedServerIds, content: collectedContent, binding, tooLargeReason, skipReason, } = await collectDiagnosticsForFile(absPath, lspService, cwd, waitMs, serverScope);
974
- if (tooLargeReason !== undefined) {
975
- return {
976
- content: [{ type: "text", text: tooLargeReason }],
977
- details: {
978
- filePath: absPath,
979
- mode: "file",
980
- severity,
981
- serverScope,
982
- outcome: "too_large",
983
- tooLargeReason,
984
- diagnostics: [],
985
- totalDiagnostics: 0,
986
- },
987
- };
988
- }
989
- const lspHealth = lspService.getDiagnosticsHealth?.(absPath);
990
- const unavailable = lspUnavailableMessage(absPath, lspHealth);
991
- // #533: an empty result needs a confirmed/unconfirmed verdict — a push-only,
992
- // silent-on-clean server (classic typescript) publishes nothing on a
993
- // clean→clean edit, so "0 diagnostics" from it is unverifiable, not clean.
994
- // #570: a timed-out priming check is a second, distinct reason a result
995
- // can be unconfirmed — checked first since it's a property of THIS check,
996
- // not a general server-capability classification.
997
- // #611: a genuinely empty (not just severity-filtered-away) result gets a
998
- // shot at the tier-3 sync escape hatch before "unconfirmed" — real
999
- // diagnostics it surfaces are merged in, not discarded.
1000
- let effectiveRawDiags = rawDiags;
1001
- let confirmation;
1002
- if (diagnosticsUnsupportedServerIds.length > 0) {
1003
- confirmation = undefined;
1004
- }
1005
- else if (skipReason !== undefined) {
1006
- confirmation = "unconfirmed";
1007
- }
1008
- else if (timedOut) {
1009
- if (applySeverityFilter(rawDiags, severity).length === 0) {
1010
- confirmation = "unconfirmed";
1011
- }
1012
- }
1013
- else if (canTrustTouchConfirmation(absPath, serverScope, confirmedByTouch)) {
1014
- if (applySeverityFilter(rawDiags, severity).length === 0) {
1015
- confirmation = "clean";
1016
- }
1017
- }
1018
- else if (rawDiags.length === 0) {
1019
- const resolved = await resolveEmptyResult(absPath, lspService);
1020
- effectiveRawDiags = resolved.diagnostics;
1021
- confirmation = resolved.confirmed
1022
- ? resolved.diagnostics.length === 0
1023
- ? "clean"
1024
- : undefined
1025
- : "unconfirmed";
1026
- }
1027
- else if (applySeverityFilter(rawDiags, severity).length === 0) {
1028
- confirmation = await classifyEmptyResult(absPath, lspService);
1029
- }
1030
- // #1095: demote a result whose content binding mismatches disk to
1031
- // "unconfirmed" (the #1092 re-cementing path) — a non-empty stale result is no
1032
- // longer "definitionally confirmed". "unknown"/true bindings are unchanged.
1033
- const boundMismatch = binding?.boundToCurrentDisk === false;
1034
- if (boundMismatch)
1035
- confirmation = "unconfirmed";
1036
- // #1470/#1493: NARROWED, not collapsed. An auxiliary that never reported — the aux
1037
- // grace timer cut it off, or it stayed silent with nothing published for this
1038
- // content — makes the FILE-level verdict unconfirmed. The merged result is missing
1039
- // whatever that scanner would have said, and this tool is the security lane's read surface. The
1040
- // PRIMARY's own verdict is untouched (`primaryCoverageGapOnly` below), so a
1041
- // TypeScript answer stays "confirmed clean" on its own line while an explicit
1042
- // line names the scanner whose coverage is absent.
1043
- const primaryCoverageGapOnly = unconfirmedServerIds.length > 0 && confirmation === "clean";
1044
- if (primaryCoverageGapOnly)
1045
- confirmation = "unconfirmed";
1046
- // #1645 review F3: one demotion rule for every writer of the widget store.
1047
- effectiveRawDiags = await demoteInferredForFile(absPath, effectiveRawDiags, cwd, lspService);
1048
- // #3088: one filter for the output, the counts and the footer reconcile.
1049
- const policy = applyProbeFindingPolicy(absPath, effectiveRawDiags, cwd, collectedContent, severity);
1050
- effectiveRawDiags = policy.kept;
1051
- const filtered = applySeverityFilter(effectiveRawDiags, severity);
1052
- const total = filtered.length;
1053
- const truncated = total > MAX_DIAGNOSTICS;
1054
- const limited = truncated ? filtered.slice(0, MAX_DIAGNOSTICS) : filtered;
1055
- const unconfirmed = confirmation === "unconfirmed";
1056
- const verdict = reconcileWidgetFromLspResult(absPath, effectiveRawDiags, confirmation, writeIndex, cwd, collectedContent, boundMismatch);
1057
- // #1561: a confirmed current view with nothing at the blocking tier retires
1058
- // this file's stale inline blocker — the store #571 corrected the footer for
1059
- // but never reached, which is how a resolved cross-file finding kept being
1060
- // injected as "Unresolved from this turn" for the rest of the session.
1061
- if (verdict.confirmed && !verdict.blocking) {
1062
- onConfirmedNoBlockers?.({
1063
- filePath: absPath,
1064
- writeIndex,
1065
- coveredSources: coveredSourcesForCheck(absPath, serverScope, unconfirmedServerIds),
1066
- cwd,
1067
- });
1068
- }
1069
- const primaryId = primaryServerId(absPath);
1070
- const primaryDiags = limited.filter((d) => d.serverId === primaryId);
1071
- const auxiliaryDiags = limited.filter((d) => d.serverId !== primaryId);
1072
- // Primary confirmation is always its own line, independent of how many
1073
- // auxiliary findings exist — a wall of ast-grep/opengrep noise must never
1074
- // bury whether the actual language server confirmed the file clean.
1075
- const primaryLine = (() => {
1076
- if (diagnosticsUnsupportedServerIds.length > 0) {
1077
- return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: navigation-only — diagnostics unsupported (no pull provider or publish evidence).`;
1078
- }
1079
- if (timedOut) {
1080
- return ("Primary LSP: check timed out — NOT the same as 0 diagnostics; the " +
1081
- "file may still have errors that just hadn't been reported yet. " +
1082
- "Re-check after the server settles, or increase waitMs.");
1083
- }
1084
- // #1470: a file demoted ONLY because an auxiliary was cut off must not render
1085
- // the silent-on-clean text — the primary did confirm, and saying otherwise is
1086
- // the same overclaim in the opposite direction. The coverage line below names
1087
- // what is actually missing.
1088
- if (unconfirmed && !primaryCoverageGapOnly) {
1089
- return (`Primary LSP${primaryId ? ` (${primaryId})` : ""}: unconfirmed — ` +
1090
- "cannot confirm clean (push-only, silent-on-clean, e.g. classic " +
1091
- "typescript-language-server never publishes on a clean re-check). " +
1092
- "NOT the same as 0 diagnostics; re-check after an edit, or use " +
1093
- "waitMs to wait longer.");
1094
- }
1095
- if (primaryDiags.length === 0) {
1096
- return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: confirmed clean.`;
1097
- }
1098
- return `Primary LSP${primaryId ? ` (${primaryId})` : ""}: ${primaryDiags.length} diagnostic${primaryDiags.length === 1 ? "" : "s"}.`;
1099
- })();
1100
- // #1470: this is the narrowing made readable. It states exactly which servers
1101
- // this result does NOT speak for, so "no auxiliary findings" can never be read
1102
- // as "the security scanners found nothing".
1103
- const coverageLine = unconfirmedServerIds.length > 0
1104
- ? `Auxiliary coverage INCOMPLETE — ${[...unconfirmedServerIds].join(", ")} did not answer within the wait budget, so ${unconfirmedServerIds.length === 1 ? "its findings are" : "their findings are"} NOT included here. This is not a clean bill of health for ${unconfirmedServerIds.length === 1 ? "that scanner" : "those scanners"}; re-check after the next edit. waitMs can extend an ordinary wait, but it cannot override a session-demoted scanner.`
1105
- : undefined;
1106
- // #3088: the #1616 suppressed-bucket rule on this lane — a policy drop is
1107
- // stated as a count, never left to read as "nothing was wrong here"
1108
- // (AGENTS.md shape 10). Bounded: one line per call, whatever the count.
1109
- const suppressedLine = dispositionSuppressedLine(policy.suppressed);
1110
- let text;
1111
- if (total === 0) {
1112
- text = [
1113
- primaryLine,
1114
- "",
1115
- unavailable ?? "No auxiliary findings.",
1116
- ...(coverageLine ? [coverageLine] : []),
1117
- ...(suppressedLine ? [suppressedLine] : []),
1118
- ].join("\n");
1119
- }
1120
- else {
1121
- const lines = [primaryLine, ""];
1122
- if (primaryDiags.length > 0) {
1123
- lines.push(...primaryDiags.map(formatDiag), "");
1124
- }
1125
- if (auxiliaryDiags.length > 0) {
1126
- lines.push(`Auxiliary findings (${auxiliaryDiags.length}):`);
1127
- lines.push(...auxiliaryDiags.map(formatDiag));
1128
- }
1129
- if (coverageLine)
1130
- lines.push("", coverageLine);
1131
- if (suppressedLine)
1132
- lines.push("", suppressedLine);
1133
- if (unavailable)
1134
- lines.unshift(unavailable, "");
1135
- if (truncated) {
1136
- lines.unshift(`Found ${total} diagnostics (showing first ${MAX_DIAGNOSTICS}):`);
1137
- }
1138
- text = lines.join("\n");
1139
- }
1140
- return {
1141
- content: [{ type: "text", text }],
1142
- details: {
1143
- filePath: absPath,
1144
- mode: "file",
1145
- severity,
1146
- serverScope,
1147
- ...(unavailable ? { unavailable } : {}),
1148
- primaryServerId: primaryId,
1149
- primaryDiagnosticsCount: primaryDiags.length,
1150
- auxiliaryDiagnosticsCount: auxiliaryDiags.length,
1151
- diagnostics: limited.map((d) => ({
1152
- line: d.range?.start?.line,
1153
- character: d.range?.start?.character,
1154
- severity: d.severity,
1155
- message: d.message,
1156
- source: d.source,
1157
- code: d.code,
1158
- })),
1159
- totalDiagnostics: total,
1160
- ...(policy.suppressed > 0 && {
1161
- dispositionSuppressed: policy.suppressed,
1162
- }),
1163
- truncated,
1164
- unconfirmed,
1165
- timedOut: unconfirmed ? timedOut : undefined,
1166
- navigationOnlyFiles: diagnosticsUnsupportedServerIds.length > 0 ? 1 : undefined,
1167
- ...(skipReason !== undefined && { skipReason }),
1168
- // #1470: which servers this result does NOT speak for. Absent when it
1169
- // speaks for all of them.
1170
- ...(unconfirmedServerIds.length > 0 && {
1171
- unconfirmedServerIds: [...unconfirmedServerIds],
1172
- }),
1173
- lspHealth,
1174
- waitMs,
1175
- },
1176
- };
1177
- }
1178
- /**
1179
- * #533: tally the per-file discriminated outcome across a batch/directory
1180
- * result set. `unconfirmed` files are those whose diagnostics collapsed to an
1181
- * empty array from a push-only, silent-on-clean server (see
1182
- * `classifyEmptyResult`) — they must never be folded into "clean" in the
1183
- * aggregate render, or a majority-unconfirmed result reads as a false "0
1184
- * diagnostics across N files".
1185
- */
1186
- function tallyConfirmation(results) {
1187
- let clean = 0;
1188
- let unconfirmed = 0;
1189
- let timedOut = 0;
1190
- let navigationOnly = 0;
1191
- let tooLarge = 0;
1192
- for (const result of results) {
1193
- if (result.tooLargeReason) {
1194
- tooLarge += 1;
1195
- continue;
1196
- }
1197
- if (result.diagnosticsUnsupported) {
1198
- navigationOnly += 1;
1199
- continue;
1200
- }
1201
- if (result.diagnostics.length > 0)
1202
- continue;
1203
- if (result.confirmation === "unconfirmed") {
1204
- unconfirmed += 1;
1205
- // #570: timed-out checks are a subset of "unconfirmed" — tallied
1206
- // separately so the aggregate text can say WHY, not just THAT.
1207
- if (result.timedOut)
1208
- timedOut += 1;
1209
- }
1210
- else {
1211
- clean += 1;
1212
- }
1213
- }
1214
- return { clean, unconfirmed, timedOut, navigationOnly, tooLarge };
1215
- }
1216
- function classifyBatchFileOutcome(result) {
1217
- if (result.error)
1218
- return "failed";
1219
- if (result.tooLargeReason)
1220
- return "too_large";
1221
- if (result.diagnosticsUnsupported)
1222
- return "unsupported";
1223
- // A timed-out/unconfirmed answer may contain partial findings, but it cannot
1224
- // honestly be called a complete findings result. Keep the raw findings for
1225
- // investigation while making the aggregate incomplete.
1226
- if (result.timedOut || result.confirmation === "unconfirmed") {
1227
- return "inconclusive";
1228
- }
1229
- if (result.diagnostics.length > 0)
1230
- return "findings";
1231
- if (result.unavailable) {
1232
- return result.primaryServerId ? "unavailable" : "unsupported";
1233
- }
1234
- if (!result.primaryServerId)
1235
- return "unsupported";
1236
- return "clean";
1237
- }
1238
- function inconclusiveBatchResult(file, reason) {
1239
- return {
1240
- file,
1241
- diagnostics: [],
1242
- confirmation: "unconfirmed",
1243
- timedOut: true,
1244
- outcome: "inconclusive",
1245
- inconclusiveReason: reason,
1246
- };
1247
- }
1248
- /**
1249
- * #570: build the explanatory clause for a batch/directory result that has
1250
- * unconfirmed files, distinguishing timed-out checks from the pre-existing
1251
- * #533 silent-on-clean-server bucket — both are "unconfirmed" for counting,
1252
- * but the reason differs and misreporting a timeout as "server can't confirm
1253
- * clean" would itself be misleading.
1254
- */
1255
- function unconfirmedReasonClause(unconfirmed, timedOut) {
1256
- const silent = unconfirmed - timedOut;
1257
- if (timedOut > 0 && silent > 0) {
1258
- return (`${timedOut} timed out (check didn't complete within budget) and ` +
1259
- `${silent} from a server that cannot confirm clean (push-only, ` +
1260
- "silent-on-clean).");
1261
- }
1262
- if (timedOut > 0) {
1263
- return `${timedOut} timed out (check didn't complete within the wait budget).`;
1264
- }
1265
- return ("from a server that cannot confirm clean (push-only, silent-on-clean; " +
1266
- "e.g. classic typescript-language-server does not publish on a clean " +
1267
- "re-check).");
1268
- }
1269
- /**
1270
- * Fan out `collectFileDiagnosticResult` across a file list at bounded
1271
- * concurrency and reduce the results into the shape both batch-style callers
1272
- * (`runBatchFileDiagnostics`/`runDirectoryDiagnostics`) render from —
1273
- * previously duplicated identically between them (SonarCloud
1274
- * `new_duplicated_lines_density` gate, surfaced when #571 added the
1275
- * `nextWriteIndex` threading to both call sites). Purely mechanical
1276
- * extraction: no behavior change, and does NOT touch the confirmed/
1277
- * unconfirmed semantics `collectFileDiagnosticResult`/`tallyConfirmation`
1278
- * already encode — those, and `lens_diagnostics` mode=full's separate,
1279
- * deliberately different confirmation gate in `tools/lens-diagnostics.ts`,
1280
- * are unrelated to this file's internal duplication and are left exactly
1281
- * as they were.
1282
- */
1283
- async function collectBatchDiagnostics(files, severity, lspService, options) {
1284
- // #671: one cache context for this whole batch/directory sweep — loaded
1285
- // once, written back once after every file has been processed (see the
1286
- // `persist()` call below), rather than round-tripping the on-disk cache
1287
- // file per-file. Shared store with `runWorkspaceDiagnostics` (`lens_
1288
- // diagnostics mode=full`'s engine); `scopeKey` keeps the two tools'
1289
- // differently-scoped touches (this tool never excludes any server, that
1290
- // one excludes opengrep — see `buildScopeKey`'s doc) from cross-serving
1291
- // entries that wouldn't actually match what each asked for.
1292
- const resolvedCwd = options.cwd ?? process.cwd();
1293
- const cacheCtx = createWorkspaceDiagnosticsCacheContext(resolvedCwd);
1294
- const scopeKey = buildScopeKey(options.serverScope ?? "all");
1295
- const resultsByIndex = new Map();
1296
- let completed = 0;
1297
- const pendingIndices = new Map();
1298
- files.forEach((file, index) => {
1299
- const queue = pendingIndices.get(file);
1300
- if (queue)
1301
- queue.push(index);
1302
- else
1303
- pendingIndices.set(file, [index]);
1304
- });
1305
- const groups = groupFilesByPrimaryServer(files);
1306
- await runPerServerGroups(groups, options.concurrency, async (group) => {
1307
- if (options.signal?.aborted)
1308
- return;
1309
- const first = group.files[0];
1310
- if (first &&
1311
- lspService &&
1312
- !isWarmAttached() &&
1313
- typeof lspService.ensureWarmForSweep === "function") {
1314
- await lspService.ensureWarmForSweep(first, {
1315
- signal: options.signal,
1316
- });
1317
- if (options.signal?.aborted)
1318
- return;
1319
- }
1320
- await mapWithConcurrency(group.files, 1, async (file) => {
1321
- // Keep duplicate explicit paths in their original result slots.
1322
- const index = pendingIndices.get(file).shift();
1323
- const work = collectFileDiagnosticResult(file, severity, lspService, options.waitMs, options.nextWriteIndex, options.serverScope, cacheCtx, scopeKey, resolvedCwd, options.onConfirmedNoBlockers);
1324
- const bounded = withDeadline(work, {
1325
- ms: batchFileDeadlineMs(),
1326
- onTimeout: "undefined",
1327
- onReject: "undefined",
1328
- });
1329
- const result = options.signal
1330
- ? await Promise.race([
1331
- bounded,
1332
- new Promise((resolve) => {
1333
- if (options.signal?.aborted) {
1334
- resolve(undefined);
1335
- return;
1336
- }
1337
- options.signal?.addEventListener("abort", () => resolve(undefined), {
1338
- once: true,
1339
- });
1340
- }),
1341
- ])
1342
- : await bounded;
1343
- resultsByIndex.set(index, result ??
1344
- inconclusiveBatchResult(file, options.signal?.aborted
1345
- ? "Batch aborted before this file completed."
1346
- : `File check exceeded ${batchFileDeadlineMs()}ms.`));
1347
- completed += 1;
1348
- options.onProgress?.(completed, files.length);
1349
- }, options.signal);
1350
- }, options.signal);
1351
- // The retired local pool returned only slots whose mapper had run. Keep that
1352
- // dense abort contract while restoring the original file order for callers.
1353
- const results = [...resultsByIndex.entries()]
1354
- .sort(([left], [right]) => left - right)
1355
- .map(([, result]) => result);
1356
- // Persist whatever was recorded, including a partial/aborted sweep's
1357
- // already-completed files — same "don't throw away confirmed work"
1358
- // posture as `runWorkspaceDiagnostics`.
1359
- cacheCtx.persist();
1360
- const fileErrors = results.flatMap((result) => result.error ? [result.error] : []);
1361
- const lspHealthWarnings = results.flatMap((result) => result.unavailable ? [result.unavailable] : []);
1362
- const allDiags = results.flatMap((result) => result.diagnostics);
1363
- const total = allDiags.length;
1364
- const truncated = total > MAX_DIAGNOSTICS;
1365
- const display = truncated ? allDiags.slice(0, MAX_DIAGNOSTICS) : allDiags;
1366
- const { clean, unconfirmed, timedOut } = tallyConfirmation(results);
1367
- for (const result of results) {
1368
- result.outcome = classifyBatchFileOutcome(result);
1369
- }
1370
- const outcomeCounts = Object.fromEntries([
1371
- "clean",
1372
- "findings",
1373
- "unsupported",
1374
- "unavailable",
1375
- "failed",
1376
- "too_large",
1377
- "inconclusive",
1378
- ].map((outcome) => [
1379
- outcome,
1380
- results.filter((result) => result.outcome === outcome).length,
1381
- ]));
1382
- // Per-file primary-server lookup so a flattened multi-file `display` list
1383
- // can still be split into "primary findings" vs "auxiliary findings" —
1384
- // `clean`/`unconfirmed` above already reflect ONLY the primary server's
1385
- // confirmation; this split does the same job for the listed diagnostics.
1386
- const primaryIdByFile = new Map(results.map((r) => [r.file, r.primaryServerId]));
1387
- const primaryDisplay = display.filter((d) => d.serverId === primaryIdByFile.get(d.file));
1388
- const auxiliaryDisplay = display.filter((d) => d.serverId !== primaryIdByFile.get(d.file));
1389
- return {
1390
- results,
1391
- fileErrors,
1392
- lspHealthWarnings,
1393
- total,
1394
- truncated,
1395
- display,
1396
- primaryDisplay,
1397
- auxiliaryDisplay,
1398
- clean,
1399
- unconfirmed,
1400
- timedOut,
1401
- outcomeCounts,
1402
- incompleteFiles: Math.max(0, files.length - results.length),
1403
- };
1404
- }
1405
- async function runBatchFileDiagnostics(absPaths, severity, lspService, options) {
1406
- if (absPaths.length === 0) {
1407
- return {
1408
- content: [{ type: "text", text: "No file paths provided." }],
1409
- isError: true,
1410
- details: { mode: "batch", severity, filesChecked: 0 },
1411
- };
1412
- }
1413
- const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, incompleteFiles, } = await collectBatchDiagnostics(absPaths, severity, lspService, options);
1414
- const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
1415
- // #3088: the batch's own suppressed bucket — one summed line, never one per
1416
- // file or per finding.
1417
- const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
1418
- const lines = [
1419
- `Files checked: ${results.length}`,
1420
- `Total diagnostics: ${total}`,
1421
- `Concurrency: ${options.concurrency}`,
1422
- ];
1423
- if (options.waitMs !== undefined)
1424
- lines.push(`Wait budget: ${options.waitMs}ms`);
1425
- lines.push(`Outcomes: ${Object.entries(outcomeCounts)
1426
- .map(([outcome, count]) => `${outcome}=${count}`)
1427
- .join(", ")}`);
1428
- const notConfirmed = outcomeCounts.inconclusive +
1429
- outcomeCounts.unavailable +
1430
- outcomeCounts.unsupported +
1431
- outcomeCounts.failed +
1432
- outcomeCounts.too_large +
1433
- incompleteFiles;
1434
- if (notConfirmed > 0) {
1435
- lines.push("", `Checks not confirmed: ${notConfirmed} (inconclusive/unavailable/unsupported/failed or not started).`);
1436
- }
1437
- if (incompleteFiles > 0) {
1438
- lines.push("", `Batch incomplete: ${incompleteFiles} file${incompleteFiles === 1 ? "" : "s"} were not confirmed because the batch was aborted.`);
1439
- }
1440
- if (fileErrors.length > 0)
1441
- lines.push("", "File errors:", ...fileErrors);
1442
- const outcomeDetails = results.map((result) => ({
1443
- file: result.file,
1444
- outcome: result.outcome,
1445
- reason: result.tooLargeReason ??
1446
- result.inconclusiveReason ??
1447
- result.error ??
1448
- result.unavailable,
1449
- }));
1450
- const tooLargeLines = renderBatchOutcomeLines(outcomeDetails);
1451
- if (tooLargeLines.length > 0) {
1452
- lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
1453
- }
1454
- if (lspHealthWarnings.length > 0) {
1455
- lines.push("", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10));
1456
- }
1457
- // #533/#570: surface unconfirmed files regardless of whether OTHER files in
1458
- // the batch found real diagnostics — a mixed found/unconfirmed result must
1459
- // not let the unconfirmed files silently pass as clean just because the
1460
- // batch as a whole isn't "0 diagnostics". This tally is primary-server-only
1461
- // (see collectFileDiagnosticResult) — it's the batch-level equivalent of
1462
- // the single-file "Primary LSP: ..." line, always reported on its own.
1463
- if (unconfirmed > 0) {
1464
- lines.push("", `${clean} file${clean === 1 ? "" : "s"} confirmed clean, ${unconfirmed} unconfirmed: ` +
1465
- `${unconfirmedReasonClause(unconfirmed, timedOut)} NOT the same as 0 diagnostics.`);
1466
- }
1467
- if (display.length === 0) {
1468
- if (unconfirmed === 0 && outcomeCounts.clean === results.length) {
1469
- lines.push("", "No diagnostics found.");
1470
- }
1471
- }
1472
- else {
1473
- if (primaryDisplay.length > 0) {
1474
- lines.push("", `Primary findings (${primaryDisplay.length}):`);
1475
- lines.push(...primaryDisplay.map(formatDisplayDiag));
1476
- }
1477
- if (auxiliaryDisplay.length > 0) {
1478
- lines.push("", `Auxiliary findings (${auxiliaryDisplay.length}):`);
1479
- lines.push(...auxiliaryDisplay.map(formatDisplayDiag));
1480
- }
1481
- if (truncated) {
1482
- lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
1483
- }
1484
- }
1485
- const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
1486
- if (suppressedLine)
1487
- lines.push("", suppressedLine);
1488
- return {
1489
- content: [{ type: "text", text: lines.join("\n") }],
1490
- details: {
1491
- mode: "batch",
1492
- severity,
1493
- serverScope: options.serverScope ?? "all",
1494
- filesChecked: results.length,
1495
- concurrency: options.concurrency,
1496
- waitMs: options.waitMs,
1497
- diagnostics: display,
1498
- primaryDiagnosticsCount: primaryDisplay.length,
1499
- auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
1500
- totalDiagnostics: total,
1501
- ...(dispositionSuppressed > 0 && { dispositionSuppressed }),
1502
- truncated,
1503
- cleanFiles: clean,
1504
- unconfirmedFiles: unconfirmed,
1505
- navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : undefined,
1506
- timedOutFiles: timedOut > 0 ? timedOut : undefined,
1507
- outcomes: results.map((result) => ({
1508
- file: result.file,
1509
- outcome: result.outcome,
1510
- reason: result.tooLargeReason ??
1511
- result.inconclusiveReason ??
1512
- result.error ??
1513
- result.unavailable,
1514
- primaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId === result.primaryServerId).length,
1515
- auxiliaryDiagnosticsCount: result.diagnostics.filter((diagnostic) => diagnostic.serverId !== result.primaryServerId).length,
1516
- })),
1517
- outcomeCounts,
1518
- incompleteFiles: incompleteFiles > 0 ? incompleteFiles : undefined,
1519
- fileErrors: fileErrors.length > 0 ? fileErrors : undefined,
1520
- lspHealthWarnings: lspHealthWarnings.length > 0 ? lspHealthWarnings : undefined,
1521
- },
1522
- };
1523
- }
1524
- /**
1525
- * The scan-language DECISION `runDirectoryDiagnostics` makes, pulled out as
1526
- * its own function so `tests/tools/lsp-diagnostics-scan-family.test.ts` can
1527
- * pin it against the golden `LANG_EXTENSIONS` table for every 1- and
1528
- * 2-extension combination in that table's universe without spinning up one
1529
- * real directory (and the full LSP-mocked tool) per combination — `hasMatch`
1530
- * is the only I/O-shaped seam, so the caller decides whether it is backed by
1531
- * a real `collectFiles` walk (production) or an in-memory extension-set probe
1532
- * (tests).
1533
- *
1534
- * Walks `SCAN_LANGUAGE_PRIORITY`'s FAMILIES in order and unions each member
1535
- * id's {@link extensionsForLanguage} into ONE `hasMatch` call — the first
1536
- * family with any match wins the whole directory for this pass. Grouping by
1537
- * family (not trying each id alone) is what makes a directory mixing BOTH
1538
- * extensions of a registry-split pair (`.ts`+`.tsx`, `.css`+`.scss`, ...)
1539
- * behavior-preserving vs the pre-#2434 bundled-key table: a flat per-id loop
1540
- * stopped at the first id with ANY match, silently losing the sibling half of
1541
- * a split pair present in the same directory (#2458 fix-round F1).
1542
- */
1543
- export async function resolveDirectoryScanExtensions(hasMatch) {
1544
- for (const family of SCAN_LANGUAGE_PRIORITY) {
1545
- const exts = family.flatMap((languageId) => extensionsForLanguage(languageId));
1546
- if (await hasMatch(exts)) {
1547
- return exts;
1548
- }
1549
- }
1550
- return undefined;
1551
- }
1552
- async function runDirectoryDiagnostics(absPath, severity, lspService, options) {
1553
- let collectedFiles = [];
1554
- const isIgnored = projectIgnorePredicate(absPath);
1555
- await resolveDirectoryScanExtensions(async (exts) => {
1556
- collectedFiles = await collectFiles(absPath, [...exts], MAX_FILES + 1, isIgnored);
1557
- return collectedFiles.length > 0;
1558
- });
1559
- if (collectedFiles.length === 0) {
1560
- return {
1561
- content: [
1562
- {
1563
- type: "text",
1564
- text: `No supported source files found in: ${absPath}`,
1565
- },
1566
- ],
1567
- details: {
1568
- filePath: absPath,
1569
- mode: "directory",
1570
- severity,
1571
- filesScanned: 0,
1572
- },
1573
- };
1574
- }
1575
- const wasCapped = collectedFiles.length > MAX_FILES;
1576
- const filesToProcess = collectedFiles.slice(0, MAX_FILES);
1577
- const { results, fileErrors, lspHealthWarnings, total, truncated, display, primaryDisplay, auxiliaryDisplay, clean, unconfirmed, timedOut, outcomeCounts, } = await collectBatchDiagnostics(filesToProcess, severity, lspService, options);
1578
- const navigationOnly = results.filter((result) => result.diagnosticsUnsupported).length;
1579
- // #3088: the directory scan's own suppressed bucket — one summed line.
1580
- const dispositionSuppressed = results.reduce((sum, result) => sum + (result.dispositionSuppressed ?? 0), 0);
1581
- const suppressedLine = dispositionSuppressedLine(dispositionSuppressed);
1582
- const outcomeDetails = results.map((result) => ({
1583
- file: result.file,
1584
- outcome: result.outcome,
1585
- reason: result.tooLargeReason ??
1586
- result.inconclusiveReason ??
1587
- result.error ??
1588
- result.unavailable,
1589
- }));
1590
- const tooLargeLines = renderBatchOutcomeLines(outcomeDetails, (file) => path.relative(absPath, file));
1591
- let text;
1592
- if (total === 0) {
1593
- // #533/#570: an unconfirmed-containing directory result must never
1594
- // render as a bare "no diagnostics" — that reads as an affirmative
1595
- // clean scan the server never actually gave for those files.
1596
- const cleanLine = unconfirmed > 0
1597
- ? `${clean} clean · ${unconfirmed} unconfirmed: ` +
1598
- `${unconfirmedReasonClause(unconfirmed, timedOut)} NOT the same as 0 diagnostics.`
1599
- : "No diagnostics found.";
1600
- text = [
1601
- `Directory: ${absPath}`,
1602
- `Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${MAX_FILES})` : ""}`,
1603
- ...(lspHealthWarnings.length > 0
1604
- ? [
1605
- "LSP unavailable for one or more files:",
1606
- ...lspHealthWarnings.slice(0, 10),
1607
- ]
1608
- : unconfirmed > 0 || outcomeCounts.clean === filesToProcess.length
1609
- ? [cleanLine]
1610
- : []),
1611
- ...(tooLargeLines.length > 0
1612
- ? ["", "Files too large for LSP diagnostics:", ...tooLargeLines]
1613
- : []),
1614
- ...(suppressedLine ? ["", suppressedLine] : []),
1615
- ].join("\n");
1616
- }
1617
- else {
1618
- const lines = [
1619
- `Directory: ${absPath}`,
1620
- `Files scanned: ${filesToProcess.length}${wasCapped ? ` (capped at ${MAX_FILES})` : ""}`,
1621
- `Files with errors: ${new Set(display.map((d) => d.file)).size}`,
1622
- `Total diagnostics: ${total}`,
1623
- ...(lspHealthWarnings.length > 0
1624
- ? ["", "LSP health warnings:", ...lspHealthWarnings.slice(0, 10)]
1625
- : []),
1626
- // #533/#570: the remaining clean-looking files in a mixed scan may
1627
- // still be unconfirmed — say so even though the directory as a
1628
- // whole found diagnostics elsewhere.
1629
- ...(unconfirmed > 0
1630
- ? [
1631
- "",
1632
- `${clean} other file${clean === 1 ? "" : "s"} confirmed clean, ${unconfirmed} unconfirmed: ` +
1633
- unconfirmedReasonClause(unconfirmed, timedOut),
1634
- ]
1635
- : []),
1636
- "",
1637
- ];
1638
- const toRelative = (d) => ({
1639
- ...d,
1640
- file: path.relative(absPath, d.file),
1641
- });
1642
- if (primaryDisplay.length > 0) {
1643
- lines.push(`Primary findings (${primaryDisplay.length}):`);
1644
- lines.push(...primaryDisplay.map(toRelative).map(formatDisplayDiag));
1645
- lines.push("");
1646
- }
1647
- if (auxiliaryDisplay.length > 0) {
1648
- lines.push(`Auxiliary findings (${auxiliaryDisplay.length}):`);
1649
- lines.push(...auxiliaryDisplay.map(toRelative).map(formatDisplayDiag));
1650
- }
1651
- if (truncated) {
1652
- lines.push("", `... (${total - MAX_DIAGNOSTICS} more diagnostics not shown)`);
1653
- }
1654
- if (suppressedLine)
1655
- lines.push("", suppressedLine);
1656
- if (tooLargeLines.length > 0) {
1657
- lines.push("", "Files too large for LSP diagnostics:", ...tooLargeLines);
1658
- }
1659
- text = lines.join("\n");
1660
- }
1661
- return {
1662
- content: [{ type: "text", text }],
1663
- details: {
1664
- filePath: absPath,
1665
- mode: "directory",
1666
- severity,
1667
- serverScope: options.serverScope ?? "all",
1668
- filesScanned: filesToProcess.length,
1669
- capped: wasCapped,
1670
- diagnostics: display.map((d) => ({
1671
- file: path.relative(absPath, d.file),
1672
- line: d.line,
1673
- character: d.character,
1674
- severity: d.severity,
1675
- message: d.message,
1676
- source: d.source,
1677
- code: d.code,
1678
- })),
1679
- primaryDiagnosticsCount: primaryDisplay.length,
1680
- auxiliaryDiagnosticsCount: auxiliaryDisplay.length,
1681
- totalDiagnostics: total,
1682
- ...(dispositionSuppressed > 0 && { dispositionSuppressed }),
1683
- truncated,
1684
- cleanFiles: clean,
1685
- unconfirmedFiles: unconfirmed,
1686
- outcomes: outcomeDetails,
1687
- outcomeCounts,
1688
- navigationOnlyFiles: navigationOnly > 0 ? navigationOnly : undefined,
1689
- timedOutFiles: timedOut > 0 ? timedOut : undefined,
1690
- fileErrors: fileErrors.length > 0 ? fileErrors : undefined,
1691
- lspHealthWarnings: lspHealthWarnings.length > 0 ? lspHealthWarnings : undefined,
1692
- concurrency: options.concurrency,
1693
- waitMs: options.waitMs,
1694
- },
1695
- };
1696
- }
1697
- // ── helpers ─────────────────────────────────────────────────────────────
1698
- /**
1699
- * #3088: the one rendering of the probe lane's policy drop, shared by the
1700
- * single-file, batch and directory renders so the three cannot word the same
1701
- * fact differently. Returns `undefined` when nothing was dropped — the #1616
1702
- * rule asks for a count when there IS one, not a "0 suppressed" line on every
1703
- * clean probe.
1704
- */
1705
- function dispositionSuppressedLine(suppressed) {
1706
- if (suppressed <= 0)
1707
- return undefined;
1708
- return (`suppressed by disposition: ${suppressed} finding(s) dropped from this ` +
1709
- "result because they're marked false-positive/won't-fix, disabled by " +
1710
- "`.pi-lens.json` rule policy, or suppressed by an inline `pi-lens-ignore` " +
1711
- "comment. Not a clean verdict for those locations — they were found, " +
1712
- "then intentionally hidden.");
1713
- }
1714
- function applySeverityFilter(diags, severity) {
1715
- if (severity === "all")
1716
- return diags;
1717
- const maxLevel = {
1718
- error: 1,
1719
- warning: 2,
1720
- information: 3,
1721
- hint: 4,
1722
- };
1723
- const max = maxLevel[severity] ?? 0;
1724
- if (max === 0)
1725
- return diags;
1726
- return diags.filter((d) => (d.severity ?? 3) <= max);
1727
- }
1728
- function formatDisplayDiag(d) {
1729
- const sevName = SEVERITY_NAMES[d.severity] ?? "unknown";
1730
- const loc = d.line !== undefined
1731
- ? `${d.file}:${d.line + 1}:${(d.character ?? 0) + 1}`
1732
- : d.file;
1733
- const src = d.source ? `[${d.source}]` : "";
1734
- const code = d.code ? ` (${d.code})` : "";
1735
- return `${loc}: ${sevName}${src}${code}: ${d.message}`;
1736
- }
1737
- function formatDiag(diag) {
1738
- const loc = diag.range?.start?.line !== undefined
1739
- ? `L${diag.range.start.line + 1}:${(diag.range.start.character ?? 0) + 1}`
1740
- : "";
1741
- const src = diag.source ? `[${diag.source}]` : "";
1742
- const code = diag.code ? ` (${diag.code})` : "";
1743
- const sevName = SEVERITY_NAMES[diag.severity] ?? "unknown";
1744
- return `${loc}: ${sevName}${src}${code}: ${diag.message}`;
1745
- }