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