@herbertgao/pi-extensions 2026.10.1 → 2026.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (707) hide show
  1. package/README.md +13 -9
  2. package/THIRD_PARTY_NOTICES.md +2 -2
  3. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-audit.md +2 -2
  4. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-debt.md +2 -2
  5. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-gain.md +1 -1
  6. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-help.md +1 -1
  7. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail-review.md +2 -2
  8. package/node_modules/@dietrichgebert/ponytail/.opencode/command/ponytail.md +1 -1
  9. package/node_modules/@dietrichgebert/ponytail/.qoder/rules/ponytail.md +21 -21
  10. package/node_modules/@dietrichgebert/ponytail/.qoder-plugin/plugin.json +1 -1
  11. package/node_modules/@dietrichgebert/ponytail/AGENTS.md +21 -21
  12. package/node_modules/@dietrichgebert/ponytail/README.md +68 -72
  13. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-activate.js +7 -0
  14. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-instructions.js +30 -30
  15. package/node_modules/@dietrichgebert/ponytail/hooks/ponytail-map.js +103 -0
  16. package/node_modules/@dietrichgebert/ponytail/package.json +2 -2
  17. package/node_modules/@dietrichgebert/ponytail/skills/ponytail/SKILL.md +33 -101
  18. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-audit/SKILL.md +89 -32
  19. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-debt/SKILL.md +13 -12
  20. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-gain/SKILL.md +17 -16
  21. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-help/SKILL.md +9 -10
  22. package/node_modules/@dietrichgebert/ponytail/skills/ponytail-review/SKILL.md +96 -57
  23. package/node_modules/@herbertgao/pi-agency/LICENSE +21 -0
  24. package/node_modules/@herbertgao/pi-agency/README.md +40 -0
  25. package/node_modules/@herbertgao/pi-agency/package.json +53 -0
  26. package/node_modules/@herbertgao/pi-agency/src/index.ts +441 -0
  27. package/node_modules/@herbertgao/pi-cc-extensions/extensions/feature/compact-thinking.ts +12 -12
  28. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/compact-mode.ts +30 -6
  29. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/default-mode.ts +15 -9
  30. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/mouse/interaction.ts +27 -9
  31. package/node_modules/@herbertgao/pi-cc-extensions/extensions/renderer/tool/codemode.ts +4 -1
  32. package/node_modules/@herbertgao/pi-cc-extensions/package.json +2 -2
  33. package/node_modules/@narumitw/pi-caffeinate/package.json +3 -3
  34. package/node_modules/@tifan/pi-mermaid-open/package.json +5 -1
  35. package/node_modules/@tifan/pi-mermaid-open/src/index.ts +26 -9
  36. package/node_modules/@tifan/pi-recap/package.json +1 -1
  37. package/node_modules/@tifan/pi-recap/src/index.ts +104 -20
  38. package/node_modules/@tifan/pi-recap/src/models.ts +46 -4
  39. package/node_modules/pi-antigravity/README.md +23 -2
  40. package/node_modules/pi-antigravity/package.json +5 -3
  41. package/node_modules/pi-antigravity/src/diagnostics/diagnostics.ts +3 -2
  42. package/node_modules/pi-antigravity/src/features/generate-image.ts +72 -0
  43. package/node_modules/pi-antigravity/src/features/google-search.ts +71 -0
  44. package/node_modules/pi-antigravity/src/image/image.ts +86 -47
  45. package/node_modules/pi-antigravity/src/image/native.ts +69 -0
  46. package/node_modules/pi-antigravity/src/index.ts +52 -137
  47. package/node_modules/pi-antigravity/src/search/search.ts +130 -31
  48. package/node_modules/pi-antigravity/src/stream/stream.ts +162 -113
  49. package/node_modules/pi-antigravity/src/types/enums.ts +1 -0
  50. package/node_modules/pi-antigravity/src/types/types.ts +1 -1
  51. package/node_modules/pi-antigravity/src/utils/security.ts +47 -1
  52. package/node_modules/pi-antigravity/src/utils/util.ts +38 -6
  53. package/node_modules/pi-lens/CHANGELOG.md +1064 -0
  54. package/node_modules/pi-lens/README.md +115 -25
  55. package/node_modules/pi-lens/config/dependency-cruiser-eager-allowlist.json +6 -2
  56. package/node_modules/pi-lens/dist/chunk-22AVIGXS.js +560 -0
  57. package/node_modules/pi-lens/dist/chunk-27BBHSZC.js +106 -0
  58. package/node_modules/pi-lens/dist/chunk-2BEMIWEW.js +51 -0
  59. package/node_modules/pi-lens/dist/chunk-2EECBNC5.js +89 -0
  60. package/node_modules/pi-lens/dist/chunk-34U7PLGE.js +23 -0
  61. package/node_modules/pi-lens/dist/chunk-3I5QFT3N.js +181 -0
  62. package/node_modules/pi-lens/dist/chunk-3MED75HT.js +37 -0
  63. package/node_modules/pi-lens/dist/chunk-47D2BO4S.js +87 -0
  64. package/node_modules/pi-lens/dist/chunk-4RERDEXR.js +348 -0
  65. package/node_modules/pi-lens/dist/chunk-4W2PLC4J.js +27 -0
  66. package/node_modules/pi-lens/dist/chunk-52H5B5TC.js +202 -0
  67. package/node_modules/pi-lens/dist/chunk-54B42YRX.js +27 -0
  68. package/node_modules/pi-lens/dist/chunk-5RXBNSW2.js +266 -0
  69. package/node_modules/pi-lens/dist/chunk-5THXD6X5.js +201 -0
  70. package/node_modules/pi-lens/dist/chunk-5W3R6LFT.js +24 -0
  71. package/node_modules/pi-lens/dist/chunk-5ZEJHV35.js +341 -0
  72. package/node_modules/pi-lens/dist/chunk-6P62QVEA.js +118 -0
  73. package/node_modules/pi-lens/dist/chunk-6QIUX4EP.js +459 -0
  74. package/node_modules/pi-lens/dist/chunk-6VCD4VKZ.js +78 -0
  75. package/node_modules/pi-lens/dist/chunk-7F72PXY2.js +326 -0
  76. package/node_modules/pi-lens/dist/chunk-7FBF7NPR.js +998 -0
  77. package/node_modules/pi-lens/dist/chunk-7U2IDNXN.js +37 -0
  78. package/node_modules/pi-lens/dist/chunk-ABBOA7UD.js +157 -0
  79. package/node_modules/pi-lens/dist/chunk-AHDW3GPE.js +50 -0
  80. package/node_modules/pi-lens/dist/chunk-AJ3QXTRQ.js +756 -0
  81. package/node_modules/pi-lens/dist/chunk-AJVLWNFN.js +1610 -0
  82. package/node_modules/pi-lens/dist/chunk-AJXODZY6.js +112 -0
  83. package/node_modules/pi-lens/dist/chunk-ARFCXG7S.js +84 -0
  84. package/node_modules/pi-lens/dist/chunk-AS44CFQ4.js +708 -0
  85. package/node_modules/pi-lens/dist/chunk-AWE5TMPO.js +38 -0
  86. package/node_modules/pi-lens/dist/chunk-B3I2UUT4.js +4692 -0
  87. package/node_modules/pi-lens/dist/chunk-BRJKOBZP.js +331 -0
  88. package/node_modules/pi-lens/dist/chunk-BW5KFIQC.js +59 -0
  89. package/node_modules/pi-lens/dist/chunk-C2AUYXQH.js +149 -0
  90. package/node_modules/pi-lens/dist/chunk-CGGAIP7J.js +29 -0
  91. package/node_modules/pi-lens/dist/chunk-D6QS3U6M.js +30 -0
  92. package/node_modules/pi-lens/dist/chunk-DFVMIKMQ.js +349 -0
  93. package/node_modules/pi-lens/dist/chunk-DIULIWKU.js +169 -0
  94. package/node_modules/pi-lens/dist/chunk-DQ2NZ7C4.js +2317 -0
  95. package/node_modules/pi-lens/dist/chunk-E2WFBRHM.js +213 -0
  96. package/node_modules/pi-lens/dist/chunk-E4MAEDPM.js +50 -0
  97. package/node_modules/pi-lens/dist/chunk-EOKRQ3SA.js +30 -0
  98. package/node_modules/pi-lens/dist/chunk-F3AU2TMM.js +2626 -0
  99. package/node_modules/pi-lens/dist/chunk-F3P7VQ7H.js +170 -0
  100. package/node_modules/pi-lens/dist/chunk-FC4RP4I2.js +12 -0
  101. package/node_modules/pi-lens/dist/chunk-FJNKANOS.js +32 -0
  102. package/node_modules/pi-lens/dist/chunk-GUOGVTCQ.js +146 -0
  103. package/node_modules/pi-lens/dist/chunk-GVXA6CCE.js +18 -0
  104. package/node_modules/pi-lens/dist/chunk-HHYAELLJ.js +2334 -0
  105. package/node_modules/pi-lens/dist/chunk-HMFXILPV.js +83 -0
  106. package/node_modules/pi-lens/dist/chunk-HVUZXSWK.js +421 -0
  107. package/node_modules/pi-lens/dist/chunk-HWBZIGMJ.js +62 -0
  108. package/node_modules/pi-lens/dist/chunk-I3R7GKDR.js +17 -0
  109. package/node_modules/pi-lens/dist/chunk-I7SPGOWI.js +55 -0
  110. package/node_modules/pi-lens/dist/chunk-IIXOAGHR.js +38 -0
  111. package/node_modules/pi-lens/dist/chunk-IVP5KAB7.js +2250 -0
  112. package/node_modules/pi-lens/dist/chunk-IXEBJSUE.js +22 -0
  113. package/node_modules/pi-lens/dist/chunk-JOLITGCC.js +27 -0
  114. package/node_modules/pi-lens/dist/chunk-K6VYLB2U.js +145 -0
  115. package/node_modules/pi-lens/dist/chunk-LJDODBBZ.js +78 -0
  116. package/node_modules/pi-lens/dist/chunk-LNAKEO63.js +78 -0
  117. package/node_modules/pi-lens/dist/chunk-LSMJ6KBE.js +4139 -0
  118. package/node_modules/pi-lens/dist/chunk-LW6XKDZQ.js +5565 -0
  119. package/node_modules/pi-lens/dist/chunk-N3YQJI6O.js +899 -0
  120. package/node_modules/pi-lens/dist/chunk-NABLC7W3.js +148 -0
  121. package/node_modules/pi-lens/dist/chunk-NKOUFPQF.js +659 -0
  122. package/node_modules/pi-lens/dist/chunk-NL5GLMFI.js +211 -0
  123. package/node_modules/pi-lens/dist/chunk-NMABWBZN.js +327 -0
  124. package/node_modules/pi-lens/dist/chunk-NTHH2J74.js +27 -0
  125. package/node_modules/pi-lens/dist/chunk-NTUJEGDZ.js +59 -0
  126. package/node_modules/pi-lens/dist/chunk-NXL4FFQQ.js +47 -0
  127. package/node_modules/pi-lens/dist/chunk-NXPS2L6U.js +466 -0
  128. package/node_modules/pi-lens/dist/chunk-O5WILGMI.js +42 -0
  129. package/node_modules/pi-lens/dist/chunk-O6TQT6RI.js +48 -0
  130. package/node_modules/pi-lens/dist/chunk-OAXNDZJ7.js +323 -0
  131. package/node_modules/pi-lens/dist/chunk-ODEEOXPH.js +44 -0
  132. package/node_modules/pi-lens/dist/chunk-OQGFCZJ4.js +4533 -0
  133. package/node_modules/pi-lens/dist/chunk-ORTMY4Q4.js +282 -0
  134. package/node_modules/pi-lens/dist/chunk-OS32RNR2.js +442 -0
  135. package/node_modules/pi-lens/dist/chunk-PAK6IDME.js +189 -0
  136. package/node_modules/pi-lens/dist/chunk-PWFHBTSF.js +406 -0
  137. package/node_modules/pi-lens/dist/chunk-Q5U6FMDI.js +2352 -0
  138. package/node_modules/pi-lens/dist/chunk-QOZXJ4NA.js +3364 -0
  139. package/node_modules/pi-lens/dist/chunk-QYF5S6P6.js +45 -0
  140. package/node_modules/pi-lens/dist/chunk-RIISMHKN.js +36 -0
  141. package/node_modules/pi-lens/dist/chunk-RPD4ZSB7.js +25 -0
  142. package/node_modules/pi-lens/dist/chunk-RPD5K5NC.js +79 -0
  143. package/node_modules/pi-lens/dist/chunk-RUOG5D2C.js +262 -0
  144. package/node_modules/pi-lens/dist/chunk-T4ERT73Y.js +599 -0
  145. package/node_modules/pi-lens/dist/chunk-T6D4Q4AQ.js +54 -0
  146. package/node_modules/pi-lens/dist/chunk-TGPXLY2J.js +391 -0
  147. package/node_modules/pi-lens/dist/chunk-TLGFBM3F.js +12116 -0
  148. package/node_modules/pi-lens/dist/chunk-TO6WL3GM.js +88 -0
  149. package/node_modules/pi-lens/dist/chunk-TT22N74J.js +517 -0
  150. package/node_modules/pi-lens/dist/chunk-TVN5N6GY.js +21216 -0
  151. package/node_modules/pi-lens/dist/chunk-U3VGR4VU.js +308 -0
  152. package/node_modules/pi-lens/dist/chunk-UK3CMEAL.js +1250 -0
  153. package/node_modules/pi-lens/dist/chunk-UOPVJQ4N.js +103 -0
  154. package/node_modules/pi-lens/dist/chunk-UXVNL3D7.js +23 -0
  155. package/node_modules/pi-lens/dist/chunk-UY752QSN.js +45 -0
  156. package/node_modules/pi-lens/dist/chunk-V5PXFSZK.js +188 -0
  157. package/node_modules/pi-lens/dist/chunk-V7R425XG.js +398 -0
  158. package/node_modules/pi-lens/dist/chunk-VCSRSYLF.js +81 -0
  159. package/node_modules/pi-lens/dist/chunk-VD4JQCQC.js +162 -0
  160. package/node_modules/pi-lens/dist/chunk-VDBPVIKD.js +62 -0
  161. package/node_modules/pi-lens/dist/chunk-VHQLPIRJ.js +114 -0
  162. package/node_modules/pi-lens/dist/chunk-VN2AXLEX.js +5414 -0
  163. package/node_modules/pi-lens/dist/chunk-VNMT7C64.js +478 -0
  164. package/node_modules/pi-lens/dist/chunk-WONERJTP.js +309 -0
  165. package/node_modules/pi-lens/dist/chunk-XECLWQZN.js +85 -0
  166. package/node_modules/pi-lens/dist/chunk-XIMYZ3JA.js +27 -0
  167. package/node_modules/pi-lens/dist/chunk-XK4CIILI.js +144 -0
  168. package/node_modules/pi-lens/dist/chunk-Y4T27COV.js +562 -0
  169. package/node_modules/pi-lens/dist/chunk-YGVYKNSN.js +1860 -0
  170. package/node_modules/pi-lens/dist/chunk-YLF3PPT5.js +308 -0
  171. package/node_modules/pi-lens/dist/chunk-YSNFMJKN.js +14 -0
  172. package/node_modules/pi-lens/dist/chunk-Z3CCVDUB.js +76 -0
  173. package/node_modules/pi-lens/dist/chunk-Z54ZSOBZ.js +61 -0
  174. package/node_modules/pi-lens/dist/chunk-ZAWW3AF6.js +97 -0
  175. package/node_modules/pi-lens/dist/chunk-ZMOL6RQC.js +167 -0
  176. package/node_modules/pi-lens/dist/index.js +16456 -8335
  177. package/node_modules/pi-lens/dist/mcp/analyze-cli.js +186 -232
  178. package/node_modules/pi-lens/dist/mcp/cli.js +137 -98
  179. package/node_modules/pi-lens/dist/mcp/server.js +22589 -1573
  180. package/node_modules/pi-lens/dist/mcp/worker.js +97 -38
  181. package/node_modules/pi-lens/dist/probes/installer.js +135 -0
  182. package/node_modules/pi-lens/dist/workers/project-snapshot-persist-worker.js +22 -0
  183. package/node_modules/pi-lens/dist/workers/review-graph-persist-worker.js +15 -0
  184. package/node_modules/pi-lens/docs/agent-guide.md +13 -4
  185. package/node_modules/pi-lens/docs/agent-tools.md +42 -0
  186. package/node_modules/pi-lens/docs/ast-grep_rules_catalog.md +3 -2
  187. package/node_modules/pi-lens/docs/configuration.md +20 -1
  188. package/node_modules/pi-lens/docs/environment-variables.md +52 -2
  189. package/node_modules/pi-lens/docs/features.md +8 -4
  190. package/node_modules/pi-lens/docs/globalconfig.md +1 -0
  191. package/node_modules/pi-lens/docs/io-bridge-v2-migration.md +94 -0
  192. package/node_modules/pi-lens/docs/lsp-capability-matrix.md +36 -4
  193. package/node_modules/pi-lens/docs/lsp-idle-eviction.md +124 -0
  194. package/node_modules/pi-lens/docs/mcp.md +21 -5
  195. package/node_modules/pi-lens/docs/merge-queue-rollout.md +77 -0
  196. package/node_modules/pi-lens/docs/pi-lens-fixer.md +156 -88
  197. package/node_modules/pi-lens/docs/pi-lens-investigator.md +74 -26
  198. package/node_modules/pi-lens/docs/pi-lens-merge-policy.md +694 -0
  199. package/node_modules/pi-lens/docs/pi-lens-monitor.md +73 -7
  200. package/node_modules/pi-lens/docs/pi-lens-retro.md +6 -4
  201. package/node_modules/pi-lens/docs/pi-lens-reviewer.md +188 -48
  202. package/node_modules/pi-lens/docs/pi-lens-subagent.md +246 -45
  203. package/node_modules/pi-lens/docs/pi-lens-warden.md +18 -7
  204. package/node_modules/pi-lens/docs/public-api-stability.md +55 -0
  205. package/node_modules/pi-lens/docs/release-qa-baseline.md +84 -17
  206. package/node_modules/pi-lens/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  207. package/node_modules/pi-lens/docs/servercapabilities.md +53 -49
  208. package/node_modules/pi-lens/docs/settings.md +1 -0
  209. package/node_modules/pi-lens/docs/usage.md +5 -1
  210. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm +0 -0
  211. package/node_modules/pi-lens/grammars/tree-sitter-bash.wasm.json +3 -3
  212. package/node_modules/pi-lens/package.json +18 -6
  213. package/node_modules/pi-lens/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  214. package/node_modules/pi-lens/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  215. package/node_modules/pi-lens/scripts/analyze-pi-lens-logs.mjs +1275 -62
  216. package/node_modules/pi-lens/scripts/download-grammars.js +5 -0
  217. package/node_modules/pi-lens/scripts/grammars.lock.json +6 -1
  218. package/node_modules/pi-lens/scripts/install-selftest.mjs +33 -34
  219. package/node_modules/pi-lens/scripts/lib/host-provided-deps.mjs +13 -4
  220. package/node_modules/pi-lens/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  221. package/package.json +15 -10
  222. package/node_modules/@dietrichgebert/ponytail/README.es.md +0 -212
  223. package/node_modules/@dietrichgebert/ponytail/README.ja.md +0 -212
  224. package/node_modules/@dietrichgebert/ponytail/README.ko.md +0 -212
  225. package/node_modules/@dietrichgebert/ponytail/README.zh-CN.md +0 -212
  226. package/node_modules/pi-lens/dist/clients/actionable-warnings-logger.js +0 -35
  227. package/node_modules/pi-lens/dist/clients/actionable-warnings.js +0 -1610
  228. package/node_modules/pi-lens/dist/clients/advisory-provenance.js +0 -593
  229. package/node_modules/pi-lens/dist/clients/agent-behavior-client.js +0 -147
  230. package/node_modules/pi-lens/dist/clients/agent-nudge.js +0 -325
  231. package/node_modules/pi-lens/dist/clients/analysed-root.js +0 -1
  232. package/node_modules/pi-lens/dist/clients/ast-grep-client.js +0 -731
  233. package/node_modules/pi-lens/dist/clients/ast-grep-rule-manager.js +0 -146
  234. package/node_modules/pi-lens/dist/clients/ast-grep-tool-logger.js +0 -118
  235. package/node_modules/pi-lens/dist/clients/ast-grep-types.js +0 -10
  236. package/node_modules/pi-lens/dist/clients/ast-grep-yaml-synth.js +0 -154
  237. package/node_modules/pi-lens/dist/clients/atomic-write-staging.js +0 -75
  238. package/node_modules/pi-lens/dist/clients/atomic-write.js +0 -189
  239. package/node_modules/pi-lens/dist/clients/availability-probe-flight.js +0 -12
  240. package/node_modules/pi-lens/dist/clients/bash-file-access.js +0 -1056
  241. package/node_modules/pi-lens/dist/clients/biome-client.js +0 -357
  242. package/node_modules/pi-lens/dist/clients/blocker-freshness.js +0 -837
  243. package/node_modules/pi-lens/dist/clients/blocker-past-eof.js +0 -129
  244. package/node_modules/pi-lens/dist/clients/bootstrap.js +0 -569
  245. package/node_modules/pi-lens/dist/clients/bounded-cache.js +0 -190
  246. package/node_modules/pi-lens/dist/clients/bounded-pid-file-lock.js +0 -202
  247. package/node_modules/pi-lens/dist/clients/bounded-telemetry.js +0 -295
  248. package/node_modules/pi-lens/dist/clients/build-identity.js +0 -95
  249. package/node_modules/pi-lens/dist/clients/bundled-resource-health.js +0 -113
  250. package/node_modules/pi-lens/dist/clients/bus-events-logger.js +0 -105
  251. package/node_modules/pi-lens/dist/clients/bus-publish.js +0 -171
  252. package/node_modules/pi-lens/dist/clients/cache/rule-cache.js +0 -190
  253. package/node_modules/pi-lens/dist/clients/cache-manager.js +0 -524
  254. package/node_modules/pi-lens/dist/clients/cache-observability.js +0 -1313
  255. package/node_modules/pi-lens/dist/clients/call-graph.js +0 -852
  256. package/node_modules/pi-lens/dist/clients/cargo-manifest.js +0 -422
  257. package/node_modules/pi-lens/dist/clients/cascade-budget.js +0 -196
  258. package/node_modules/pi-lens/dist/clients/cascade-format.js +0 -315
  259. package/node_modules/pi-lens/dist/clients/cascade-logger.js +0 -31
  260. package/node_modules/pi-lens/dist/clients/cascade-types.js +0 -9
  261. package/node_modules/pi-lens/dist/clients/child-unref.js +0 -124
  262. package/node_modules/pi-lens/dist/clients/code-quality-warnings.js +0 -286
  263. package/node_modules/pi-lens/dist/clients/codebase-model.js +0 -173
  264. package/node_modules/pi-lens/dist/clients/collateral-test-role.js +0 -40
  265. package/node_modules/pi-lens/dist/clients/complexity-client.js +0 -462
  266. package/node_modules/pi-lens/dist/clients/config-core/deny.js +0 -221
  267. package/node_modules/pi-lens/dist/clients/config-core/index.js +0 -50
  268. package/node_modules/pi-lens/dist/clients/config-core/merge.js +0 -357
  269. package/node_modules/pi-lens/dist/clients/config-core/normalize.js +0 -340
  270. package/node_modules/pi-lens/dist/clients/config-core/process-spec.js +0 -248
  271. package/node_modules/pi-lens/dist/clients/config-core/provenance.js +0 -164
  272. package/node_modules/pi-lens/dist/clients/config-core/records.js +0 -218
  273. package/node_modules/pi-lens/dist/clients/config-core/resolve.js +0 -125
  274. package/node_modules/pi-lens/dist/clients/config-core/safe-object.js +0 -78
  275. package/node_modules/pi-lens/dist/clients/config-core/schema.js +0 -165
  276. package/node_modules/pi-lens/dist/clients/config-diagnostic-codes.js +0 -285
  277. package/node_modules/pi-lens/dist/clients/config-locations.js +0 -388
  278. package/node_modules/pi-lens/dist/clients/config-resolve.js +0 -798
  279. package/node_modules/pi-lens/dist/clients/config-schema.js +0 -251
  280. package/node_modules/pi-lens/dist/clients/config-warn.js +0 -407
  281. package/node_modules/pi-lens/dist/clients/console-guard-install.js +0 -26
  282. package/node_modules/pi-lens/dist/clients/cooperative-budget.js +0 -42
  283. package/node_modules/pi-lens/dist/clients/dead-code-client.js +0 -510
  284. package/node_modules/pi-lens/dist/clients/dead-code-logger.js +0 -30
  285. package/node_modules/pi-lens/dist/clients/deadline-utils.js +0 -280
  286. package/node_modules/pi-lens/dist/clients/debug-handles.js +0 -211
  287. package/node_modules/pi-lens/dist/clients/debug-heap.js +0 -174
  288. package/node_modules/pi-lens/dist/clients/deferred-lsp-work.js +0 -106
  289. package/node_modules/pi-lens/dist/clients/degradation-ledger.js +0 -426
  290. package/node_modules/pi-lens/dist/clients/demoted-finding-render.js +0 -169
  291. package/node_modules/pi-lens/dist/clients/dependency-checker.js +0 -885
  292. package/node_modules/pi-lens/dist/clients/deps/ast-grep-napi.js +0 -22
  293. package/node_modules/pi-lens/dist/clients/deps/js-yaml.js +0 -9
  294. package/node_modules/pi-lens/dist/clients/deps/minimatch.js +0 -4
  295. package/node_modules/pi-lens/dist/clients/deps/pi-tui.js +0 -14
  296. package/node_modules/pi-lens/dist/clients/deps/typebox.js +0 -14
  297. package/node_modules/pi-lens/dist/clients/deps/vscode-jsonrpc.js +0 -1
  298. package/node_modules/pi-lens/dist/clients/deps/web-tree-sitter.js +0 -24
  299. package/node_modules/pi-lens/dist/clients/diagnostic-dispositions.js +0 -630
  300. package/node_modules/pi-lens/dist/clients/diagnostic-line-freshness.js +0 -305
  301. package/node_modules/pi-lens/dist/clients/diagnostic-logger.js +0 -73
  302. package/node_modules/pi-lens/dist/clients/diagnostic-tracker.js +0 -105
  303. package/node_modules/pi-lens/dist/clients/diagnostics-publish.js +0 -230
  304. package/node_modules/pi-lens/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  305. package/node_modules/pi-lens/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  306. package/node_modules/pi-lens/dist/clients/dispatch/collect-later-tier.js +0 -35
  307. package/node_modules/pi-lens/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  308. package/node_modules/pi-lens/dist/clients/dispatch/dispatcher.js +0 -1269
  309. package/node_modules/pi-lens/dist/clients/dispatch/fact-provider-types.js +0 -1
  310. package/node_modules/pi-lens/dist/clients/dispatch/fact-rule-runner.js +0 -17
  311. package/node_modules/pi-lens/dist/clients/dispatch/fact-runner.js +0 -30
  312. package/node_modules/pi-lens/dist/clients/dispatch/fact-scheduler.js +0 -68
  313. package/node_modules/pi-lens/dist/clients/dispatch/fact-store.js +0 -377
  314. package/node_modules/pi-lens/dist/clients/dispatch/facts/comment-facts.js +0 -22
  315. package/node_modules/pi-lens/dist/clients/dispatch/facts/file-content.js +0 -19
  316. package/node_modules/pi-lens/dist/clients/dispatch/facts/function-facts.js +0 -348
  317. package/node_modules/pi-lens/dist/clients/dispatch/facts/import-facts.js +0 -204
  318. package/node_modules/pi-lens/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  319. package/node_modules/pi-lens/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  320. package/node_modules/pi-lens/dist/clients/dispatch/finding-policy.js +0 -339
  321. package/node_modules/pi-lens/dist/clients/dispatch/indent-detect.js +0 -417
  322. package/node_modules/pi-lens/dist/clients/dispatch/inline-suppressions.js +0 -89
  323. package/node_modules/pi-lens/dist/clients/dispatch/integration.js +0 -2377
  324. package/node_modules/pi-lens/dist/clients/dispatch/lazy.js +0 -11
  325. package/node_modules/pi-lens/dist/clients/dispatch/pending-runner-findings.js +0 -87
  326. package/node_modules/pi-lens/dist/clients/dispatch/plan.js +0 -278
  327. package/node_modules/pi-lens/dist/clients/dispatch/priorities.js +0 -20
  328. package/node_modules/pi-lens/dist/clients/dispatch/rule-id-normalize.js +0 -50
  329. package/node_modules/pi-lens/dist/clients/dispatch/rule-ignores.js +0 -69
  330. package/node_modules/pi-lens/dist/clients/dispatch/rule-policy.js +0 -135
  331. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-noise.js +0 -40
  332. package/node_modules/pi-lens/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  333. package/node_modules/pi-lens/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  334. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  335. package/node_modules/pi-lens/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  336. package/node_modules/pi-lens/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  337. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-complexity.js +0 -72
  338. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  339. package/node_modules/pi-lens/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  340. package/node_modules/pi-lens/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  341. package/node_modules/pi-lens/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  342. package/node_modules/pi-lens/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  343. package/node_modules/pi-lens/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  344. package/node_modules/pi-lens/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  345. package/node_modules/pi-lens/dist/clients/dispatch/runner-context.js +0 -52
  346. package/node_modules/pi-lens/dist/clients/dispatch/runners/actionlint.js +0 -111
  347. package/node_modules/pi-lens/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  348. package/node_modules/pi-lens/dist/clients/dispatch/runners/biome-check.js +0 -296
  349. package/node_modules/pi-lens/dist/clients/dispatch/runners/cpp-check.js +0 -219
  350. package/node_modules/pi-lens/dist/clients/dispatch/runners/credo.js +0 -82
  351. package/node_modules/pi-lens/dist/clients/dispatch/runners/cue-vet.js +0 -416
  352. package/node_modules/pi-lens/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  353. package/node_modules/pi-lens/dist/clients/dispatch/runners/detekt.js +0 -176
  354. package/node_modules/pi-lens/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  355. package/node_modules/pi-lens/dist/clients/dispatch/runners/elixir-check.js +0 -195
  356. package/node_modules/pi-lens/dist/clients/dispatch/runners/eslint.js +0 -137
  357. package/node_modules/pi-lens/dist/clients/dispatch/runners/fact-rules.js +0 -29
  358. package/node_modules/pi-lens/dist/clients/dispatch/runners/fish-indent.js +0 -69
  359. package/node_modules/pi-lens/dist/clients/dispatch/runners/gleam-check.js +0 -167
  360. package/node_modules/pi-lens/dist/clients/dispatch/runners/go-vet.js +0 -104
  361. package/node_modules/pi-lens/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  362. package/node_modules/pi-lens/dist/clients/dispatch/runners/hadolint.js +0 -91
  363. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-lint.js +0 -171
  364. package/node_modules/pi-lens/dist/clients/dispatch/runners/helm-render.js +0 -940
  365. package/node_modules/pi-lens/dist/clients/dispatch/runners/htmlhint.js +0 -117
  366. package/node_modules/pi-lens/dist/clients/dispatch/runners/index.js +0 -108
  367. package/node_modules/pi-lens/dist/clients/dispatch/runners/javac.js +0 -99
  368. package/node_modules/pi-lens/dist/clients/dispatch/runners/ktlint.js +0 -110
  369. package/node_modules/pi-lens/dist/clients/dispatch/runners/lsp.js +0 -379
  370. package/node_modules/pi-lens/dist/clients/dispatch/runners/markdownlint.js +0 -167
  371. package/node_modules/pi-lens/dist/clients/dispatch/runners/mypy.js +0 -102
  372. package/node_modules/pi-lens/dist/clients/dispatch/runners/oxlint.js +0 -414
  373. package/node_modules/pi-lens/dist/clients/dispatch/runners/php-lint.js +0 -85
  374. package/node_modules/pi-lens/dist/clients/dispatch/runners/phpstan.js +0 -120
  375. package/node_modules/pi-lens/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  376. package/node_modules/pi-lens/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  377. package/node_modules/pi-lens/dist/clients/dispatch/runners/pyright.js +0 -169
  378. package/node_modules/pi-lens/dist/clients/dispatch/runners/rubocop.js +0 -99
  379. package/node_modules/pi-lens/dist/clients/dispatch/runners/ruff.js +0 -88
  380. package/node_modules/pi-lens/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  381. package/node_modules/pi-lens/dist/clients/dispatch/runners/shellcheck.js +0 -173
  382. package/node_modules/pi-lens/dist/clients/dispatch/runners/shfmt.js +0 -102
  383. package/node_modules/pi-lens/dist/clients/dispatch/runners/spellcheck.js +0 -128
  384. package/node_modules/pi-lens/dist/clients/dispatch/runners/spotbugs.js +0 -259
  385. package/node_modules/pi-lens/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  386. package/node_modules/pi-lens/dist/clients/dispatch/runners/stylelint.js +0 -166
  387. package/node_modules/pi-lens/dist/clients/dispatch/runners/swiftlint.js +0 -163
  388. package/node_modules/pi-lens/dist/clients/dispatch/runners/taplo.js +0 -174
  389. package/node_modules/pi-lens/dist/clients/dispatch/runners/terragrunt.js +0 -168
  390. package/node_modules/pi-lens/dist/clients/dispatch/runners/tflint.js +0 -125
  391. package/node_modules/pi-lens/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  392. package/node_modules/pi-lens/dist/clients/dispatch/runners/trivy-config.js +0 -253
  393. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  394. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  395. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  396. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  397. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  398. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  399. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  400. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  401. package/node_modules/pi-lens/dist/clients/dispatch/runners/utils.js +0 -15
  402. package/node_modules/pi-lens/dist/clients/dispatch/runners/vale.js +0 -128
  403. package/node_modules/pi-lens/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  404. package/node_modules/pi-lens/dist/clients/dispatch/runners/yamllint.js +0 -95
  405. package/node_modules/pi-lens/dist/clients/dispatch/runners/zig-check.js +0 -79
  406. package/node_modules/pi-lens/dist/clients/dispatch/suppress-writer.js +0 -79
  407. package/node_modules/pi-lens/dist/clients/dispatch/tool-profile.js +0 -30
  408. package/node_modules/pi-lens/dist/clients/dispatch/types.js +0 -21
  409. package/node_modules/pi-lens/dist/clients/dispatch/utils/format-utils.js +0 -45
  410. package/node_modules/pi-lens/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  411. package/node_modules/pi-lens/dist/clients/disposition-logger.js +0 -32
  412. package/node_modules/pi-lens/dist/clients/disposition-publish.js +0 -124
  413. package/node_modules/pi-lens/dist/clients/durable-store.js +0 -97
  414. package/node_modules/pi-lens/dist/clients/effective-config.js +0 -415
  415. package/node_modules/pi-lens/dist/clients/env-utils.js +0 -83
  416. package/node_modules/pi-lens/dist/clients/error-class.js +0 -23
  417. package/node_modules/pi-lens/dist/clients/eval-timestamp.js +0 -17
  418. package/node_modules/pi-lens/dist/clients/event-loop-hold.js +0 -274
  419. package/node_modules/pi-lens/dist/clients/event-loop-monitor.js +0 -250
  420. package/node_modules/pi-lens/dist/clients/extension-log.js +0 -504
  421. package/node_modules/pi-lens/dist/clients/extension-mode.js +0 -73
  422. package/node_modules/pi-lens/dist/clients/feature-hints.js +0 -103
  423. package/node_modules/pi-lens/dist/clients/file-kinds.js +0 -412
  424. package/node_modules/pi-lens/dist/clients/file-role.js +0 -134
  425. package/node_modules/pi-lens/dist/clients/file-time.js +0 -160
  426. package/node_modules/pi-lens/dist/clients/file-utils.js +0 -1032
  427. package/node_modules/pi-lens/dist/clients/finding-delivery-gate.js +0 -494
  428. package/node_modules/pi-lens/dist/clients/finding-identity.js +0 -88
  429. package/node_modules/pi-lens/dist/clients/fix-worklog.js +0 -103
  430. package/node_modules/pi-lens/dist/clients/format-events-publish.js +0 -343
  431. package/node_modules/pi-lens/dist/clients/format-service.js +0 -222
  432. package/node_modules/pi-lens/dist/clients/formatters-lazy.js +0 -9
  433. package/node_modules/pi-lens/dist/clients/formatters.js +0 -2032
  434. package/node_modules/pi-lens/dist/clients/freshness-cadence.js +0 -17
  435. package/node_modules/pi-lens/dist/clients/freshness.js +0 -38
  436. package/node_modules/pi-lens/dist/clients/generated-artifacts.js +0 -394
  437. package/node_modules/pi-lens/dist/clients/generation-guard.js +0 -215
  438. package/node_modules/pi-lens/dist/clients/git-guard.js +0 -1136
  439. package/node_modules/pi-lens/dist/clients/git-tracked-ignore.js +0 -243
  440. package/node_modules/pi-lens/dist/clients/gitleaks-client.js +0 -509
  441. package/node_modules/pi-lens/dist/clients/go-client.js +0 -104
  442. package/node_modules/pi-lens/dist/clients/govulncheck-client.js +0 -605
  443. package/node_modules/pi-lens/dist/clients/gradle-ktfmt-style.js +0 -348
  444. package/node_modules/pi-lens/dist/clients/grammar-source.js +0 -465
  445. package/node_modules/pi-lens/dist/clients/gzip-stage-write.js +0 -135
  446. package/node_modules/pi-lens/dist/clients/hashline-anchor.js +0 -424
  447. package/node_modules/pi-lens/dist/clients/hook-budgets.js +0 -76
  448. package/node_modules/pi-lens/dist/clients/host-edit-normalize.js +0 -140
  449. package/node_modules/pi-lens/dist/clients/host-ports.js +0 -39
  450. package/node_modules/pi-lens/dist/clients/indent-retarget.js +0 -154
  451. package/node_modules/pi-lens/dist/clients/inline-blocker-dispositions.js +0 -186
  452. package/node_modules/pi-lens/dist/clients/install-diagnostics.js +0 -156
  453. package/node_modules/pi-lens/dist/clients/installer/index.js +0 -5416
  454. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  455. package/node_modules/pi-lens/dist/clients/installer/managed-tool-refresh.js +0 -783
  456. package/node_modules/pi-lens/dist/clients/instance-reaper-state.js +0 -7
  457. package/node_modules/pi-lens/dist/clients/instance-reaper.js +0 -1137
  458. package/node_modules/pi-lens/dist/clients/instance-registry-lock.js +0 -145
  459. package/node_modules/pi-lens/dist/clients/instance-registry.js +0 -773
  460. package/node_modules/pi-lens/dist/clients/jscpd-client.js +0 -378
  461. package/node_modules/pi-lens/dist/clients/json-cache-read.js +0 -65
  462. package/node_modules/pi-lens/dist/clients/knip-client.js +0 -788
  463. package/node_modules/pi-lens/dist/clients/language-policy.js +0 -252
  464. package/node_modules/pi-lens/dist/clients/language-profile.js +0 -361
  465. package/node_modules/pi-lens/dist/clients/language-registry.js +0 -603
  466. package/node_modules/pi-lens/dist/clients/latency-logger.js +0 -727
  467. package/node_modules/pi-lens/dist/clients/lazy-import.js +0 -21
  468. package/node_modules/pi-lens/dist/clients/ledger-bounds.js +0 -54
  469. package/node_modules/pi-lens/dist/clients/lens-config.js +0 -404
  470. package/node_modules/pi-lens/dist/clients/lens-engine.js +0 -358
  471. package/node_modules/pi-lens/dist/clients/lens-events.js +0 -125
  472. package/node_modules/pi-lens/dist/clients/lens-flag-registry.js +0 -467
  473. package/node_modules/pi-lens/dist/clients/lens-map.js +0 -1156
  474. package/node_modules/pi-lens/dist/clients/live-bus-emitter.js +0 -74
  475. package/node_modules/pi-lens/dist/clients/log-cleanup.js +0 -222
  476. package/node_modules/pi-lens/dist/clients/lsp/aggregation.js +0 -178
  477. package/node_modules/pi-lens/dist/clients/lsp/cascade-tier.js +0 -421
  478. package/node_modules/pi-lens/dist/clients/lsp/client.js +0 -4593
  479. package/node_modules/pi-lens/dist/clients/lsp/config.js +0 -691
  480. package/node_modules/pi-lens/dist/clients/lsp/content-limits.js +0 -41
  481. package/node_modules/pi-lens/dist/clients/lsp/diagnostic-binding.js +0 -225
  482. package/node_modules/pi-lens/dist/clients/lsp/document-drift.js +0 -467
  483. package/node_modules/pi-lens/dist/clients/lsp/edits.js +0 -1219
  484. package/node_modules/pi-lens/dist/clients/lsp/index.js +0 -8305
  485. package/node_modules/pi-lens/dist/clients/lsp/inferred-project.js +0 -216
  486. package/node_modules/pi-lens/dist/clients/lsp/jvm-runtime.js +0 -298
  487. package/node_modules/pi-lens/dist/clients/lsp/language.js +0 -63
  488. package/node_modules/pi-lens/dist/clients/lsp/launch.js +0 -746
  489. package/node_modules/pi-lens/dist/clients/lsp/lombok.js +0 -158
  490. package/node_modules/pi-lens/dist/clients/lsp/path-utils.js +0 -5
  491. package/node_modules/pi-lens/dist/clients/lsp/pending-aux-coverage.js +0 -251
  492. package/node_modules/pi-lens/dist/clients/lsp/position-encoding.js +0 -61
  493. package/node_modules/pi-lens/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  494. package/node_modules/pi-lens/dist/clients/lsp/server.js +0 -3290
  495. package/node_modules/pi-lens/dist/clients/lsp/session-roots.js +0 -192
  496. package/node_modules/pi-lens/dist/clients/lsp/spawn-history.js +0 -17
  497. package/node_modules/pi-lens/dist/clients/lsp/sync-kind.js +0 -56
  498. package/node_modules/pi-lens/dist/clients/lsp/tsserver-sync.js +0 -332
  499. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  500. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/classification.js +0 -85
  501. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/index.js +0 -10
  502. package/node_modules/pi-lens/dist/clients/lsp/wait-policy/strategies.js +0 -392
  503. package/node_modules/pi-lens/dist/clients/lsp/watch-queue.js +0 -61
  504. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  505. package/node_modules/pi-lens/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  506. package/node_modules/pi-lens/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  507. package/node_modules/pi-lens/dist/clients/lsp-budget.js +0 -186
  508. package/node_modules/pi-lens/dist/clients/lsp-document-symbols.js +0 -110
  509. package/node_modules/pi-lens/dist/clients/lsp-lazy.js +0 -9
  510. package/node_modules/pi-lens/dist/clients/lsp-mutation.js +0 -396
  511. package/node_modules/pi-lens/dist/clients/lsp-status.js +0 -59
  512. package/node_modules/pi-lens/dist/clients/map-with-concurrency.js +0 -40
  513. package/node_modules/pi-lens/dist/clients/mcp/analyze.js +0 -420
  514. package/node_modules/pi-lens/dist/clients/mcp/host-shim.js +0 -31
  515. package/node_modules/pi-lens/dist/clients/mcp/ipc.js +0 -523
  516. package/node_modules/pi-lens/dist/clients/mcp/review.js +0 -197
  517. package/node_modules/pi-lens/dist/clients/mcp/session.js +0 -412
  518. package/node_modules/pi-lens/dist/clients/memory-sampler.js +0 -208
  519. package/node_modules/pi-lens/dist/clients/message-end-attribution.js +0 -30
  520. package/node_modules/pi-lens/dist/clients/metrics-client.js +0 -107
  521. package/node_modules/pi-lens/dist/clients/metrics-history.js +0 -327
  522. package/node_modules/pi-lens/dist/clients/middle-man-analysis.js +0 -263
  523. package/node_modules/pi-lens/dist/clients/model-provider.js +0 -53
  524. package/node_modules/pi-lens/dist/clients/module-report-lsp.js +0 -189
  525. package/node_modules/pi-lens/dist/clients/module-report.js +0 -2150
  526. package/node_modules/pi-lens/dist/clients/mutating-tool.js +0 -651
  527. package/node_modules/pi-lens/dist/clients/mutation-attribution.js +0 -368
  528. package/node_modules/pi-lens/dist/clients/mutation-bridge.js +0 -240
  529. package/node_modules/pi-lens/dist/clients/ndjson-logger.js +0 -734
  530. package/node_modules/pi-lens/dist/clients/observed-mutation-sources.js +0 -101
  531. package/node_modules/pi-lens/dist/clients/observed-mutation.js +0 -1253
  532. package/node_modules/pi-lens/dist/clients/oldtext-autopatch.js +0 -128
  533. package/node_modules/pi-lens/dist/clients/opaque-mutation-scan.js +0 -496
  534. package/node_modules/pi-lens/dist/clients/opengrep-client.js +0 -333
  535. package/node_modules/pi-lens/dist/clients/opengrep-config.js +0 -76
  536. package/node_modules/pi-lens/dist/clients/package-manager.js +0 -607
  537. package/node_modules/pi-lens/dist/clients/package-root.js +0 -38
  538. package/node_modules/pi-lens/dist/clients/partial-edit-apply.js +0 -439
  539. package/node_modules/pi-lens/dist/clients/path-attribution-telemetry.js +0 -43
  540. package/node_modules/pi-lens/dist/clients/path-keyed-map.js +0 -108
  541. package/node_modules/pi-lens/dist/clients/path-utils.js +0 -1244
  542. package/node_modules/pi-lens/dist/clients/performance-report.js +0 -331
  543. package/node_modules/pi-lens/dist/clients/persist-debounce.js +0 -70
  544. package/node_modules/pi-lens/dist/clients/persistent-reverify.js +0 -271
  545. package/node_modules/pi-lens/dist/clients/php-cs-fixer-config.js +0 -119
  546. package/node_modules/pi-lens/dist/clients/pipeline.js +0 -1480
  547. package/node_modules/pi-lens/dist/clients/post-autofix-notice.js +0 -37
  548. package/node_modules/pi-lens/dist/clients/print-mode.js +0 -21
  549. package/node_modules/pi-lens/dist/clients/probe-home-state.js +0 -228
  550. package/node_modules/pi-lens/dist/clients/process-bridge.js +0 -66
  551. package/node_modules/pi-lens/dist/clients/process-singletons.js +0 -210
  552. package/node_modules/pi-lens/dist/clients/process-snapshot.js +0 -68
  553. package/node_modules/pi-lens/dist/clients/project-changes.js +0 -181
  554. package/node_modules/pi-lens/dist/clients/project-conventions.js +0 -171
  555. package/node_modules/pi-lens/dist/clients/project-diagnostics/cache.js +0 -137
  556. package/node_modules/pi-lens/dist/clients/project-diagnostics/extractors.js +0 -109
  557. package/node_modules/pi-lens/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  558. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  559. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  560. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  561. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  562. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  563. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  564. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  565. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  566. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  567. package/node_modules/pi-lens/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  568. package/node_modules/pi-lens/dist/clients/project-diagnostics/scanner.js +0 -520
  569. package/node_modules/pi-lens/dist/clients/project-diagnostics/types.js +0 -1
  570. package/node_modules/pi-lens/dist/clients/project-lens-config.js +0 -807
  571. package/node_modules/pi-lens/dist/clients/project-report.js +0 -701
  572. package/node_modules/pi-lens/dist/clients/project-scale.js +0 -237
  573. package/node_modules/pi-lens/dist/clients/project-scan-policy.js +0 -11
  574. package/node_modules/pi-lens/dist/clients/project-snapshot-fingerprint.js +0 -48
  575. package/node_modules/pi-lens/dist/clients/project-snapshot-persist-worker.js +0 -20
  576. package/node_modules/pi-lens/dist/clients/project-snapshot.js +0 -1653
  577. package/node_modules/pi-lens/dist/clients/project-trust.js +0 -181
  578. package/node_modules/pi-lens/dist/clients/python-environment.js +0 -217
  579. package/node_modules/pi-lens/dist/clients/python-provenance.js +0 -639
  580. package/node_modules/pi-lens/dist/clients/quiet-window-config.js +0 -47
  581. package/node_modules/pi-lens/dist/clients/quiet-window.js +0 -236
  582. package/node_modules/pi-lens/dist/clients/read-bridge.js +0 -139
  583. package/node_modules/pi-lens/dist/clients/read-expansion.js +0 -266
  584. package/node_modules/pi-lens/dist/clients/read-guard-logger.js +0 -251
  585. package/node_modules/pi-lens/dist/clients/read-guard-tool-lines.js +0 -1306
  586. package/node_modules/pi-lens/dist/clients/read-guard.js +0 -1561
  587. package/node_modules/pi-lens/dist/clients/recent-touches.js +0 -240
  588. package/node_modules/pi-lens/dist/clients/redact/secrets.js +0 -449
  589. package/node_modules/pi-lens/dist/clients/resource-sampler.js +0 -628
  590. package/node_modules/pi-lens/dist/clients/reverse-deps.js +0 -270
  591. package/node_modules/pi-lens/dist/clients/review-graph/builder.js +0 -5126
  592. package/node_modules/pi-lens/dist/clients/review-graph/format.js +0 -33
  593. package/node_modules/pi-lens/dist/clients/review-graph/git-identity.js +0 -163
  594. package/node_modules/pi-lens/dist/clients/review-graph/import-resolvers.js +0 -481
  595. package/node_modules/pi-lens/dist/clients/review-graph/persist-worker.js +0 -10
  596. package/node_modules/pi-lens/dist/clients/review-graph/query.js +0 -244
  597. package/node_modules/pi-lens/dist/clients/review-graph/revision-drift.js +0 -21
  598. package/node_modules/pi-lens/dist/clients/review-graph/service.js +0 -96
  599. package/node_modules/pi-lens/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  600. package/node_modules/pi-lens/dist/clients/review-graph/symbol-id.js +0 -103
  601. package/node_modules/pi-lens/dist/clients/review-graph/tsconfig-paths.js +0 -299
  602. package/node_modules/pi-lens/dist/clients/review-graph/types.js +0 -1
  603. package/node_modules/pi-lens/dist/clients/review-graph/workspace-modules.js +0 -449
  604. package/node_modules/pi-lens/dist/clients/review-graph-logger.js +0 -81
  605. package/node_modules/pi-lens/dist/clients/ruff-client.js +0 -179
  606. package/node_modules/pi-lens/dist/clients/rules-scanner.js +0 -131
  607. package/node_modules/pi-lens/dist/clients/run-duration.js +0 -55
  608. package/node_modules/pi-lens/dist/clients/runtime-agent-end.js +0 -746
  609. package/node_modules/pi-lens/dist/clients/runtime-config.js +0 -77
  610. package/node_modules/pi-lens/dist/clients/runtime-context.js +0 -211
  611. package/node_modules/pi-lens/dist/clients/runtime-coordinator.js +0 -1324
  612. package/node_modules/pi-lens/dist/clients/runtime-session.js +0 -2485
  613. package/node_modules/pi-lens/dist/clients/runtime-tool-call.js +0 -1295
  614. package/node_modules/pi-lens/dist/clients/runtime-tool-result.js +0 -2049
  615. package/node_modules/pi-lens/dist/clients/runtime-turn.js +0 -3767
  616. package/node_modules/pi-lens/dist/clients/rust-client.js +0 -97
  617. package/node_modules/pi-lens/dist/clients/safe-spawn.js +0 -2357
  618. package/node_modules/pi-lens/dist/clients/sanitize.js +0 -286
  619. package/node_modules/pi-lens/dist/clients/scan-utils.js +0 -25
  620. package/node_modules/pi-lens/dist/clients/scratch-tree-policy.js +0 -165
  621. package/node_modules/pi-lens/dist/clients/search-read-registration.js +0 -94
  622. package/node_modules/pi-lens/dist/clients/secret-findings.js +0 -114
  623. package/node_modules/pi-lens/dist/clients/security-scan-client.js +0 -352
  624. package/node_modules/pi-lens/dist/clients/session-event-guard.js +0 -297
  625. package/node_modules/pi-lens/dist/clients/session-lifecycle.js +0 -484
  626. package/node_modules/pi-lens/dist/clients/session-start-observability.js +0 -94
  627. package/node_modules/pi-lens/dist/clients/session-state-store.js +0 -101
  628. package/node_modules/pi-lens/dist/clients/session-summary.js +0 -15
  629. package/node_modules/pi-lens/dist/clients/sessionstart-logger.js +0 -27
  630. package/node_modules/pi-lens/dist/clients/sg-runner.js +0 -876
  631. package/node_modules/pi-lens/dist/clients/sgconfig.js +0 -347
  632. package/node_modules/pi-lens/dist/clients/shared-checkout-guard.js +0 -338
  633. package/node_modules/pi-lens/dist/clients/single-flight.js +0 -158
  634. package/node_modules/pi-lens/dist/clients/situational-tool-telemetry.js +0 -108
  635. package/node_modules/pi-lens/dist/clients/skills-resolver.js +0 -141
  636. package/node_modules/pi-lens/dist/clients/slow-fs.js +0 -138
  637. package/node_modules/pi-lens/dist/clients/smells-rollup.js +0 -231
  638. package/node_modules/pi-lens/dist/clients/source-filter.js +0 -596
  639. package/node_modules/pi-lens/dist/clients/source-walker.js +0 -211
  640. package/node_modules/pi-lens/dist/clients/spawn-output-cap.js +0 -120
  641. package/node_modules/pi-lens/dist/clients/spawn-timeout-cooldown.js +0 -110
  642. package/node_modules/pi-lens/dist/clients/stale-marker.js +0 -17
  643. package/node_modules/pi-lens/dist/clients/startup-scan.js +0 -400
  644. package/node_modules/pi-lens/dist/clients/startup-timing.js +0 -130
  645. package/node_modules/pi-lens/dist/clients/string-utils.js +0 -33
  646. package/node_modules/pi-lens/dist/clients/subagent-mode.js +0 -111
  647. package/node_modules/pi-lens/dist/clients/symbol-containment.js +0 -24
  648. package/node_modules/pi-lens/dist/clients/symbol-types.js +0 -5
  649. package/node_modules/pi-lens/dist/clients/test-runner-client.js +0 -2507
  650. package/node_modules/pi-lens/dist/clients/test-runner-delivery.js +0 -287
  651. package/node_modules/pi-lens/dist/clients/todo-scanner.js +0 -229
  652. package/node_modules/pi-lens/dist/clients/tool-agreement.js +0 -423
  653. package/node_modules/pi-lens/dist/clients/tool-config.js +0 -249
  654. package/node_modules/pi-lens/dist/clients/tool-cwd.js +0 -270
  655. package/node_modules/pi-lens/dist/clients/tool-definition.js +0 -47
  656. package/node_modules/pi-lens/dist/clients/tool-event.js +0 -105
  657. package/node_modules/pi-lens/dist/clients/tool-policy.js +0 -2755
  658. package/node_modules/pi-lens/dist/clients/tool-probe.js +0 -53
  659. package/node_modules/pi-lens/dist/clients/tool-render.js +0 -156
  660. package/node_modules/pi-lens/dist/clients/tool-set-policy.js +0 -118
  661. package/node_modules/pi-lens/dist/clients/tree-sitter-cache.js +0 -364
  662. package/node_modules/pi-lens/dist/clients/tree-sitter-client.js +0 -4195
  663. package/node_modules/pi-lens/dist/clients/tree-sitter-logger.js +0 -100
  664. package/node_modules/pi-lens/dist/clients/tree-sitter-navigator.js +0 -269
  665. package/node_modules/pi-lens/dist/clients/tree-sitter-query-loader.js +0 -569
  666. package/node_modules/pi-lens/dist/clients/tree-sitter-shared.js +0 -158
  667. package/node_modules/pi-lens/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  668. package/node_modules/pi-lens/dist/clients/trivy-client.js +0 -467
  669. package/node_modules/pi-lens/dist/clients/tui-fit.js +0 -50
  670. package/node_modules/pi-lens/dist/clients/turn-context.js +0 -52
  671. package/node_modules/pi-lens/dist/clients/turn-end/lane.js +0 -51
  672. package/node_modules/pi-lens/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  673. package/node_modules/pi-lens/dist/clients/turn-end/lanes/secrets.js +0 -204
  674. package/node_modules/pi-lens/dist/clients/turn-summary-render.js +0 -72
  675. package/node_modules/pi-lens/dist/clients/turn-summary.js +0 -132
  676. package/node_modules/pi-lens/dist/clients/typos-config.js +0 -29
  677. package/node_modules/pi-lens/dist/clients/user-notify.js +0 -51
  678. package/node_modules/pi-lens/dist/clients/vanished-instance-marker.js +0 -57
  679. package/node_modules/pi-lens/dist/clients/warm-attach.js +0 -305
  680. package/node_modules/pi-lens/dist/clients/widget-state.js +0 -1808
  681. package/node_modules/pi-lens/dist/clients/word-index-logger.js +0 -49
  682. package/node_modules/pi-lens/dist/clients/word-index-store.js +0 -459
  683. package/node_modules/pi-lens/dist/clients/word-index.js +0 -2080
  684. package/node_modules/pi-lens/dist/clients/workspace-topology.js +0 -443
  685. package/node_modules/pi-lens/dist/clients/write-ordering-guard.js +0 -65
  686. package/node_modules/pi-lens/dist/clients/zizmor-config.js +0 -340
  687. package/node_modules/pi-lens/dist/i18n.js +0 -69
  688. package/node_modules/pi-lens/dist/mcp/build-staleness.js +0 -124
  689. package/node_modules/pi-lens/dist/scripts/lib/process-scan.mjs +0 -583
  690. package/node_modules/pi-lens/dist/scripts/lib/skills-predicate.mjs +0 -129
  691. package/node_modules/pi-lens/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  692. package/node_modules/pi-lens/dist/tools/activate-tools.js +0 -87
  693. package/node_modules/pi-lens/dist/tools/ast-grep-outline.js +0 -137
  694. package/node_modules/pi-lens/dist/tools/ast-grep-replace.js +0 -244
  695. package/node_modules/pi-lens/dist/tools/ast-grep-search.js +0 -762
  696. package/node_modules/pi-lens/dist/tools/effective-config.js +0 -90
  697. package/node_modules/pi-lens/dist/tools/lens-diagnostic-mark.js +0 -375
  698. package/node_modules/pi-lens/dist/tools/lens-diagnostics.js +0 -2571
  699. package/node_modules/pi-lens/dist/tools/lsp-diagnostics.js +0 -1745
  700. package/node_modules/pi-lens/dist/tools/lsp-navigation.js +0 -1367
  701. package/node_modules/pi-lens/dist/tools/lsp-structured-output.js +0 -294
  702. package/node_modules/pi-lens/dist/tools/module-report.js +0 -382
  703. package/node_modules/pi-lens/dist/tools/project-report.js +0 -86
  704. package/node_modules/pi-lens/dist/tools/render-compact.js +0 -374
  705. package/node_modules/pi-lens/dist/tools/scan-progress.js +0 -53
  706. package/node_modules/pi-lens/dist/tools/shared.js +0 -29
  707. package/node_modules/pi-lens/dist/tools/symbol-search.js +0 -155
@@ -1,2032 +0,0 @@
1
- /**
2
- * Formatter Definitions for pi-lens
3
- *
4
- * Auto-detects formatters based on:
5
- * - Config files (biome.json, .prettierrc, etc.)
6
- * - Dependencies (package.json, requirements.txt, etc.)
7
- * - Binary availability (which/where)
8
- *
9
- * Inspired by OpenCode's formatter.ts pattern
10
- */
11
- import { logExtension } from "./extension-log.js";
12
- import { AsyncLocalStorage } from "node:async_hooks";
13
- import * as fs from "node:fs/promises";
14
- import * as os from "node:os";
15
- import * as path from "node:path";
16
- import { BoundedLruCache } from "./bounded-cache.js";
17
- import { createGenerationSource } from "./generation-guard.js";
18
- import { findNearestContaining, normalizeMapKey } from "./path-utils.js";
19
- import { resolveToolCwd } from "./tool-cwd.js";
20
- import { resolveCargoPackageEdition } from "./cargo-manifest.js";
21
- import { resolveKtfmtGradleStyle } from "./gradle-ktfmt-style.js";
22
- import { resolvePhpCsFixerConfig } from "./php-cs-fixer-config.js";
23
- import { TERRAGRUNT_FILENAMES } from "./file-kinds.js";
24
- import { stripAnsi } from "./sanitize.js";
25
- import { detectIndentation, hasDetectableIndentation, } from "./dispatch/indent-detect.js";
26
- import { logLatency } from "./latency-logger.js";
27
- import { compareOrdinal } from "./string-utils.js";
28
- import { classifyProbeFailure, createAvailabilityLatch, logAvailabilityDecision, startHostStallSampler, } from "./dispatch/runners/utils/availability-policy.js";
29
- import { findGlobalBinary, findLocalBinUpwards, VENDOR_BIN_DIRS, VENV_BIN_DIRS, } from "./package-manager.js";
30
- import { safeSpawnAsync } from "./safe-spawn.js";
31
- import { establishToolAgreement } from "./tool-agreement.js";
32
- import { recordDegradationOnce } from "./degradation-ledger.js";
33
- import { probeToolAsync } from "./tool-probe.js";
34
- import { assertInstallAllowed } from "./project-trust.js";
35
- import { tryLazyInstallForFormatter } from "./dispatch/runners/utils/lazy-installer.js";
36
- import { getToolPath } from "./installer/index.js";
37
- import { findPSScriptAnalyzerConfigPath, getAutoInstallToolIdForFormatter, getFormatterPolicyForFile, getSmartDefaultFormatterName, hasBiomeConfig, hasBlackConfig, hasClangFormatConfig, hasCljfmtConfig, hasCmakeFormatConfig, hasCsharpierConfig, hasFantomasConfig, hasGoogleJavaFormatConfig, hasKtfmtConfig, hasKtlintConfig, hasMixFormatConfig, hasNearestPackageJsonDependency, hasNearestPackageJsonField, hasOcamlformatConfig, hasOrmoluConfig, hasOxfmtConfig, hasOxfmtSvelteConfig, hasPhpCsFixerConfig, hasPrettierConfig, hasPSScriptAnalyzerConfig, hasRubocopConfig, hasRuffConfig, hasSqlfluffConfig, hasStandardrbConfig, hasStyluaConfig, hasSwiftformatConfig, hasTaploConfig, hasTerraformConfig, hasVitePlusConfig, OXFMT_SUPPORTED_EXTENSIONS, } from "./tool-policy.js";
38
- /**
39
- * Lazy-install a formatter's tool, through the shared seam (#1537).
40
- *
41
- * This used to own a second copy of the attempt guard — `_lazyInstallAttempts`,
42
- * a Set keyed before the install ran and never cleared — so a `gem install
43
- * rubocop` that died on a network blip was never retried for the session. The
44
- * state, the transient/durable classification and the retry ladder now live in
45
- * `lazy-installer.ts`; what stays here is the one thing that is formatter
46
- * business: a fresh binary on PATH invalidates every "not found" verdict (#1495).
47
- */
48
- export async function tryLazyInstallFormatterTool(tool, cwd) {
49
- const ok = await tryLazyInstallForFormatter(tool, cwd);
50
- if (ok)
51
- resetWhichLatches();
52
- return ok;
53
- }
54
- // --- Types ---
55
- /**
56
- * Sentinel a `resolveCommand` returns to mean "do not format this file at
57
- * all" — distinct from `null`, which means "fall back to the static
58
- * `command`". #1144's style-preserving resolvers return this when the repo
59
- * has no config AND the file's indentation is undetectable: running the
60
- * stock command there is exactly the stock-style imposition the fix bans.
61
- */
62
- export const SKIP_FORMATTING = "skip-formatting";
63
- /**
64
- * Sentinel a `resolveCommand` returns when it has PROVEN the selected
65
- * formatter has no executable — the binary is absent from every place the
66
- * resolver probed (node_modules/.bin, a venv, a managed install dir, PATH) and
67
- * no alternative resolved (#2413). Distinct from `null` ("no override, fall
68
- * back to the static `command`") and from SKIP_FORMATTING ("style-preserving
69
- * refusal"): the static fallback for these formatters is the SAME bare binary
70
- * the resolver just proved missing, so spawning it only re-triggers the ENOENT
71
- * the resolver already observed — and reporting that as a formatting failure is
72
- * the exact defect this fixes (recurring defect shape 10: an unavailable
73
- * producer collapsed into an ordinary failed result).
74
- */
75
- export const FORMATTER_UNAVAILABLE = "formatter-unavailable";
76
- export const FORMATTER_AGREEMENT_UNAVAILABLE = "formatter-agreement-unavailable";
77
- // --- Utility Functions ---
78
- async function fileExists(filePath) {
79
- try {
80
- await fs.access(filePath);
81
- return true;
82
- }
83
- catch {
84
- return false;
85
- }
86
- }
87
- async function findUp(targets, startDir, stopDir = path.parse(startDir).root) {
88
- const found = [];
89
- let currentDir = startDir;
90
- while (currentDir !== stopDir) {
91
- // One `readdir` per directory instead of one `access` per target: the
92
- // per-target probe made this loop's cost O(targets), so a long target
93
- // list (e.g. `FORMATTER_CONFIG_FILES`) paid for every entry at every
94
- // ancestor directory even when almost none of them exist (#1603).
95
- let entries;
96
- try {
97
- entries = await fs.readdir(currentDir);
98
- }
99
- catch {
100
- entries = [];
101
- }
102
- if (entries.length > 0) {
103
- // On default-case-insensitive platforms the old `fs.access` probe
104
- // matched a config file regardless of on-disk casing; fold so the
105
- // readdir membership check keeps that behavior.
106
- const foldCase = process.platform === "win32" || process.platform === "darwin";
107
- const entrySet = new Set(foldCase ? entries.map((e) => e.toLowerCase()) : entries);
108
- for (const target of targets) {
109
- // Nested candidates (for example node_modules/.bin/biome) do
110
- // not appear as direct readdir members. They retain the old
111
- // bounded candidate probe; direct names use the O(depth + matches)
112
- // directory-membership path above.
113
- if (target.includes("/") || target.includes("\\"))
114
- continue;
115
- if (entrySet.has(foldCase ? target.toLowerCase() : target)) {
116
- const candidate = path.join(currentDir, target);
117
- // Directory membership is only a cheap candidate filter. Keep the
118
- // old access check for matched entries so dangling links and entries
119
- // that cannot be read do not become config evidence (#1603 R2).
120
- if (await fileExists(candidate))
121
- found.push(candidate);
122
- }
123
- }
124
- }
125
- for (const target of targets) {
126
- if (!target.includes("/") && !target.includes("\\"))
127
- continue;
128
- const candidate = path.join(currentDir, target);
129
- if (await fileExists(candidate))
130
- found.push(candidate);
131
- }
132
- const parent = path.dirname(currentDir);
133
- if (parent === currentDir)
134
- break;
135
- currentDir = parent;
136
- }
137
- return found;
138
- }
139
- /** Test-only access to the real filesystem walker and its candidate filter. */
140
- export function _findUpForTests(targets, startDir, stopDir) {
141
- return findUp(targets, startDir, stopDir);
142
- }
143
- const WHICH_BUDGET_MS = 5000;
144
- /**
145
- * PATH lookups, governed by the shared availability policy (#1495).
146
- *
147
- * `which()` is a spawn on a 5 s budget, not a filesystem check, and around a
148
- * dozen `detect*` implementations gate on it. A transient timeout used to drop
149
- * the formatter AND get written to `detectionCache` as an empty enabled-list —
150
- * a cache invalidated only by a config file's mtime or size, never by time. One
151
- * stalled `which rustfmt` therefore disabled Rust formatting for the session
152
- * unless the user happened to edit a config file.
153
- *
154
- * Now: only a genuine absence latches, a stall expires on a cooldown, and the
155
- * detection pass refuses to cache an empty result a stall caused.
156
- */
157
- const whichLatchByCommand = new Map();
158
- /**
159
- * Commands whose CURRENT `which` verdict is transient — a probe that never got a
160
- * fair hearing, still inside its cooldown.
161
- *
162
- * Scoped per command on purpose (#1495 review). The first cut counted transient
163
- * verdicts process-wide and compared the count across a detection pass, which
164
- * over-suppressed: one stalled `which rustfmt` stopped an unrelated Python or
165
- * shell detection from caching, and stamped `reason: "probe-timeout"` on
166
- * selections where nothing had timed out. A command leaves the set as soon as
167
- * its verdict becomes trustworthy — a resolved path or a genuine absence.
168
- */
169
- const whichTransientCommands = new Set();
170
- /**
171
- * The binaries `which()` was asked about inside the current `detect()` call
172
- * (#1539).
173
- *
174
- * This replaces the `command[0]` approximation the poison guard used to run on.
175
- * `command[0]` is the formatter's own binary, which is exact for most
176
- * `detect()`s and wrong for the ones that consult a second binary — and the
177
- * "extras are harmless" argument does not hold: `rustfmt` answering a GENUINE
178
- * absence while `which rustup` stalls skips the lazy install and leaves nothing
179
- * in the transient set for `command[0]` to match. Recording the actual probes
180
- * makes the guard exact in both directions: it also stops a leftover transient
181
- * verdict for some other pass's binary from being blamed on this decision.
182
- *
183
- * `AsyncLocalStorage` because `detect()` is async and two selection passes can
184
- * be in flight at once (`tests/clients/formatters-which-latch.test.ts` runs
185
- * exactly that); a module-level set would let one pass's probes leak into the
186
- * other's verdict. Built eagerly — unlike `extension-log.ts`'s lazy console
187
- * capture, this store is entered on every detection pass, so there is no window
188
- * where paying for it is avoidable.
189
- */
190
- const probedCommandsStorage = new AsyncLocalStorage();
191
- /**
192
- * Run one candidate's `detect()` and read the per-candidate verdict back out of
193
- * the PATH latch (#1539). `detected` is the plain boolean the caller still needs
194
- * for selection; `verdict` is why.
195
- */
196
- async function detectCandidate(formatter, cwd) {
197
- const probed = new Set();
198
- let detected = false;
199
- let error;
200
- try {
201
- detected = await probedCommandsStorage.run(probed, () => formatter.detect(cwd));
202
- }
203
- catch (err) {
204
- // A `detect()` that threw still probed whatever it probed first, and those
205
- // probes are the fact the guard needs. Swallowing them here would let a
206
- // stall inside a throwing detection cache as a clean "not this project".
207
- error = err;
208
- }
209
- const stalledCommands = [...probed].filter((command) => whichTransientCommands.has(command));
210
- return {
211
- detected,
212
- ...(error !== undefined && { error }),
213
- outcome: {
214
- name: formatter.name,
215
- verdict: detected
216
- ? "available"
217
- : stalledCommands.length > 0
218
- ? "unreachable"
219
- : "missing",
220
- stalledCommands,
221
- },
222
- };
223
- }
224
- /**
225
- * Drop the PATH verdicts, so a newly installed binary is visible at once.
226
- *
227
- * Module-private on purpose. Clearing the latches alone does NOT re-arm
228
- * formatter availability: `getFormattersForFile` answers from `detectionCache`
229
- * before it ever probes PATH, so a caller that wants a genuine re-probe must
230
- * drop both. `clearFormatterCache` is that pair, and it is the only reset a
231
- * session boundary should call (#1895 review round).
232
- */
233
- function resetWhichLatches() {
234
- whichLatchByCommand.clear();
235
- whichTransientCommands.clear();
236
- cooldownRecordedForRetryAtMs.clear();
237
- }
238
- /**
239
- * Last `retryAtMs` a cooldown-served record was emitted for, per command.
240
- *
241
- * The bound (#1539): the memo branch used to return before
242
- * `logAvailabilityDecision`, so a formatter held off by a transient cooldown
243
- * produced ONE record for arbitrarily many decisions — a reader counting
244
- * `availability_decision` rows undercounted how long it had been off. Logging
245
- * every cache hit is not an option either: this seam is consulted per save.
246
- * One extra row per cooldown WINDOW is the compromise, and `retryAtMs` is the
247
- * window's identity, so the ladder itself does the rate limiting.
248
- */
249
- const cooldownRecordedForRetryAtMs = new Map();
250
- /**
251
- * Record that a still-cooling verdict was served from the latch, at most once
252
- * per cooldown window (#1539's second defect).
253
- */
254
- function noteCooldownServedVerdict(command, latch) {
255
- if (latch.getOutcome() !== "transient")
256
- return;
257
- const retryAtMs = latch.getRetryAtMs();
258
- if (retryAtMs <= 0)
259
- return;
260
- if (cooldownRecordedForRetryAtMs.get(command) === retryAtMs)
261
- return;
262
- cooldownRecordedForRetryAtMs.set(command, retryAtMs);
263
- const cause = latch.getCause();
264
- if (cause === null) {
265
- // `getOutcome() === "transient"` is only ever set by `noteUnavailable` in
266
- // the same call that sets `cause`, so this is unreachable today. It is
267
- // still not a throw (#1539 review F2): this runs inside `which()`, which
268
- // runs inside `detect()`, and the smart-default branch of the selection
269
- // pass rethrows a detection failure — so an invariant break in a LOGGING
270
- // helper could take down formatting for the file. Nothing is fabricated
271
- // either (#1535's rule: a made-up cause would mislabel WHY a formatter is
272
- // off, in the record this fix exists to make honest). The row is dropped
273
- // and the anomaly is reported, bounded by the same once-per-window gate
274
- // this function already passed.
275
- logExtension({
276
- subsystem: "format",
277
- message: `which latch: transient outcome with no cause for ${command}; cooldown-served record dropped`,
278
- metadata: { tool: command, retryAtMs },
279
- });
280
- return;
281
- }
282
- logAvailabilityDecision({
283
- tool: command,
284
- verdict: "unavailable",
285
- outcome: "transient",
286
- cause,
287
- elapsedMs: 0,
288
- latched: false,
289
- retryAfterMs: Math.max(1, retryAtMs - Date.now()),
290
- budgetMs: WHICH_BUDGET_MS,
291
- servedFromCooldown: true,
292
- // No probe ran here: the latch's own remembered cause is replayed
293
- // as-is, so the call site is the one asserting it (#2209).
294
- classifiedBy: "caller",
295
- });
296
- }
297
- async function which(command) {
298
- probedCommandsStorage.getStore()?.add(command);
299
- let entry = whichLatchByCommand.get(command);
300
- if (!entry) {
301
- entry = { latch: createAvailabilityLatch(), resolved: null };
302
- whichLatchByCommand.set(command, entry);
303
- }
304
- const memo = entry.latch.read();
305
- if (memo !== null) {
306
- if (!memo)
307
- noteCooldownServedVerdict(command, entry.latch);
308
- return memo ? entry.resolved : null;
309
- }
310
- const stallSampler = startHostStallSampler();
311
- const startedAt = Date.now();
312
- const result = await safeSpawnAsync(process.platform === "win32" ? "where" : "which", [command], { timeout: WHICH_BUDGET_MS });
313
- const hostStallMs = stallSampler.stop();
314
- const elapsedMs = Date.now() - startedAt;
315
- const resolved = !result.error && result.status === 0
316
- ? (result.stdout?.trim().split(/\r?\n/)[0] ?? null)
317
- : null;
318
- if (resolved) {
319
- entry.resolved = resolved;
320
- entry.latch.noteAvailable();
321
- whichTransientCommands.delete(command);
322
- cooldownRecordedForRetryAtMs.delete(command);
323
- logAvailabilityDecision({
324
- tool: command,
325
- verdict: "available",
326
- outcome: "success",
327
- cause: "ok",
328
- elapsedMs,
329
- latched: true,
330
- hostStallMs,
331
- budgetMs: WHICH_BUDGET_MS,
332
- classifiedBy: "probe",
333
- });
334
- return resolved;
335
- }
336
- entry.resolved = null;
337
- // A `which`/`where` that RAN and found nothing exits nonzero with NO spawn
338
- // error, and only that is a genuine absence an install would fix.
339
- //
340
- // An UNSPAWNABLE prober is the opposite (#1495 review): EACCES, `spawn
341
- // UNKNOWN`, EMFILE, an unresolvable cwd, or `where`/`which` itself missing
342
- // says nothing about the tool that was asked for. Claiming `missing` there
343
- // was the worst version of this bug, because the prober is shared by every
344
- // which-gated formatter — one EACCES would have latched a dozen of them off
345
- // for the session. So the `missing` override is granted only to a prober that
346
- // ran, and anything that stopped it from running stays transient.
347
- const proberRan = !result.error;
348
- const classified = classifyProbeFailure(result, {
349
- hostStallMs,
350
- ...(proberRan && { unclassifiedFailureOutcome: "missing" }),
351
- });
352
- let { outcome, cause } = classified;
353
- // This call site OVERRIDES classifyProbeFailure's own answer below, so a
354
- // row it forced is a caller assertion, not a probe classification — the
355
- // scrutiny availability-policy.ts:253-258 reserves for "caller" rows must
356
- // not be laundered away by mislabeling this one "probe" (#2226 review F1).
357
- let outcomeForced = false;
358
- if (!proberRan && outcome !== "transient") {
359
- outcome = "transient";
360
- cause = "probe-rejected";
361
- outcomeForced = true;
362
- }
363
- const retryAfterMs = entry.latch.noteUnavailable(outcome, cause);
364
- if (outcome === "transient")
365
- whichTransientCommands.add(command);
366
- else
367
- whichTransientCommands.delete(command);
368
- // A fresh probe opens a new cooldown window (or ends the cooldown), so the
369
- // next cache hit is entitled to its own record.
370
- cooldownRecordedForRetryAtMs.delete(command);
371
- logAvailabilityDecision({
372
- tool: command,
373
- verdict: "unavailable",
374
- outcome,
375
- cause,
376
- elapsedMs,
377
- latched: retryAfterMs === 0,
378
- hostStallMs,
379
- ...(retryAfterMs > 0 && { retryAfterMs }),
380
- budgetMs: WHICH_BUDGET_MS,
381
- classifiedBy: outcomeForced ? "caller" : "probe",
382
- });
383
- return null;
384
- }
385
- async function resolveGoFmtBinary() {
386
- const inPath = await which("gofmt");
387
- if (inPath)
388
- return inPath;
389
- const goCheck = await probeToolAsync("go", ["env", "GOROOT"], {
390
- timeout: 5000,
391
- });
392
- if (goCheck.error || goCheck.status !== 0)
393
- return null;
394
- const goroot = (goCheck.stdout ?? "").trim();
395
- if (!goroot)
396
- return null;
397
- const binary = path.join(goroot, "bin", process.platform === "win32" ? "gofmt.exe" : "gofmt");
398
- return (await fileExists(binary)) ? binary : null;
399
- }
400
- // --- Venv / Local Binary Helpers ---
401
- /**
402
- * Walk up from cwd looking for a binary in .venv or venv.
403
- * Returns the absolute path if found, null otherwise.
404
- *
405
- * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
406
- * the project upward past HOME means STOP, not keep reading — a `.venv`/`venv`
407
- * bin found at or above HOME can never be THIS project's own virtualenv. The
408
- * ceiling lives in `findLocalBinUpwards` and is default-on there; the tests
409
- * that pin a fake HOME drive that walker directly rather than threading a
410
- * `homeDir` no production caller ever passed (#2544 review F3).
411
- *
412
- * On Windows the candidate order is EXT-OUTER, DIR-INNER — `.venv/Scripts/x.exe`,
413
- * `venv/Scripts/x.exe`, then the bare names — which is what the pre-fold copy
414
- * did and what `findLocalBinUpwards` preserves.
415
- */
416
- async function findInVenv(binary, cwd) {
417
- return (findLocalBinUpwards(binary, cwd, {
418
- windowsExt: ".exe",
419
- binDirs: VENV_BIN_DIRS,
420
- }) ?? null);
421
- }
422
- /**
423
- * Check vendor/bin for PHP Composer-managed tools.
424
- * Walks up from cwd to find vendor/bin/<binary>.
425
- *
426
- * Tool-resolution walker, not a config lookup (#2514/#2517 policy): escaping
427
- * the project upward past HOME means STOP, not keep reading — a `vendor/bin`
428
- * match found at or above HOME can never be THIS project's own Composer
429
- * install. Same single walker as `findInVenv` above; only `binDirs` and the
430
- * Windows extension differ.
431
- */
432
- async function findInVendorBin(binary, cwd) {
433
- return (findLocalBinUpwards(binary, cwd, {
434
- windowsExt: ".bat",
435
- binDirs: VENDOR_BIN_DIRS,
436
- }) ?? null);
437
- }
438
- /**
439
- * Check node_modules/.bin for locally installed Node tools.
440
- * Walks up from cwd to find node_modules/.bin/<binary>.
441
- *
442
- * #2514: this WAS a private, byte-for-byte duplicate of
443
- * `findLocalBinUpwards` (`package-manager.ts`) — the same walk `stylua`
444
- * below, `taplo.ts`, `knip-client.ts`, and (via `findNodeToolBinary`)
445
- * `dependency-checker.ts`/`jscpd-client.ts` already reuse. A stray
446
- * `~/node_modules/.bin/oxfmt.cmd` (the home-level pi-extensions manifest
447
- * installs its own bins) was picked up as the project's formatter because
448
- * this copy had no HOME ceiling; fixing only this copy would have left every
449
- * other caller of `findLocalBinUpwards` with the same defect. Delegating
450
- * here instead of hand-rolling a second ceiling keeps there being exactly one
451
- * `node_modules/.bin` walker — see that function for the tool-resolution
452
- * "escaping the project means STOP" policy (#2517).
453
- */
454
- async function findInNodeModules(binary, cwd) {
455
- return findLocalBinUpwards(binary, cwd) ?? null;
456
- }
457
- /**
458
- * Returns true if `bundle exec <gem>` should be used:
459
- * bundle binary is available AND Gemfile.lock exists in the tree.
460
- */
461
- async function canUseBundleExec(cwd) {
462
- if ((await which("bundle")) === null)
463
- return false;
464
- const lockfiles = await findUp(["Gemfile.lock"], cwd);
465
- return lockfiles.length > 0;
466
- }
467
- async function resolveManagedSmartDefaultCommand(formatterName, filePath, args) {
468
- const toolId = getAutoInstallToolIdForFormatter(formatterName);
469
- if (!toolId)
470
- return FORMATTER_UNAVAILABLE;
471
- const { ensureTool } = await import("./installer/index.js");
472
- const installed = await ensureTool(toolId);
473
- if (!installed) {
474
- recordDegradationOnce({
475
- kind: "formatter-unavailable",
476
- subject: formatterName,
477
- reason: "managed formatter lookup and installation returned no executable",
478
- });
479
- return FORMATTER_UNAVAILABLE;
480
- }
481
- return [installed, ...args, filePath];
482
- }
483
- /** Resolve an explicitly selected formatter from the shared managed-tool seam. */
484
- async function resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd) {
485
- const local = findLocal && cwd ? await findLocal(toolId, cwd) : null;
486
- const installed = local ?? (await which(toolId)) ?? (await getToolPath(toolId));
487
- return installed ? [installed, ...args, filePath] : FORMATTER_UNAVAILABLE;
488
- }
489
- function managedFormatterResolver(toolId, args, findLocal) {
490
- return (filePath, cwd) => resolveManagedFormatterCommand(toolId, filePath, args, findLocal, cwd);
491
- }
492
- function managedToolDetect(toolId, configDetect, nativeDetect) {
493
- return async (cwd) => {
494
- const available = (nativeDetect && (await nativeDetect(cwd))) ||
495
- Boolean(await getToolPath(toolId));
496
- return available && (configDetect ? await configDetect(cwd) : true);
497
- };
498
- }
499
- /**
500
- * One entry per formatter that can be selected via explicit project config
501
- * (the `formatterPolicy` "explicit-config" branch of `getFormattersForFile`).
502
- * A formatter with NO entry here can never win that branch — the switch this
503
- * table replaced had exactly that failure mode for `psscriptanalyzer-format`
504
- * (#1572: `.ps1`'s policy sets `defaultWhenUnconfigured: false` AND the
505
- * formatter had no config check, so neither selection branch could ever pick
506
- * it).
507
- *
508
- * This table IS the source of truth for "which formatters have an explicit-
509
- * config check" — `FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK` below derives its
510
- * membership from these keys rather than hand-listing them a second time, so
511
- * the coverage guard in formatter-policy-consistency.test.ts (#1572) can never
512
- * drift from what this function actually does.
513
- */
514
- // A `Map`, not an object literal: an object literal's lookup inherits
515
- // `Object.prototype`, so `checks["toString"]` or `checks["constructor"]`
516
- // resolves to a real (non-formatter) function instead of `undefined` —
517
- // a formatter genuinely NAMED one of those prototype properties would read
518
- // as "has an explicit-config check" when it has none (review finding, #1572).
519
- // `Map.prototype.get` has no such inherited-key hazard.
520
- const EXPLICIT_FORMATTER_CONFIG_CHECKS = new Map([
521
- ["biome", (cwd) => hasBiomeConfig(cwd)],
522
- [
523
- "prettier",
524
- (cwd) => hasPrettierConfig(cwd) || hasNearestPackageJsonField(cwd, "prettier"),
525
- ],
526
- // .svelte is conditional beyond "an oxfmt config exists": oxfmt requires the
527
- // `svelte` package installed AND the config's `svelte` flag enabled
528
- // (verified empirically — see hasOxfmtSvelteConfig). The generic checks
529
- // below are NOT sufficient for .svelte — an oxfmt.toml with no svelte flag,
530
- // or an oxfmt dependency alone, both fail at runtime for .svelte
531
- // specifically (other extensions are unaffected by this stricter gate).
532
- [
533
- "oxfmt",
534
- (cwd, ext) => ext === ".svelte"
535
- ? hasOxfmtSvelteConfig(cwd)
536
- : hasOxfmtConfig(cwd) ||
537
- hasVitePlusConfig(cwd) ||
538
- // The published package is `oxfmt`; `@oxc-project/oxfmt` does not
539
- // exist on npm. Accept both (scoped kept for forward-compat).
540
- hasNearestPackageJsonDependency(cwd, "oxfmt") ||
541
- hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"),
542
- ],
543
- ["ruff", (cwd) => hasRuffConfig(cwd)],
544
- ["black", (cwd) => hasBlackConfig(cwd)],
545
- ["sqlfluff", (cwd) => hasSqlfluffConfig(cwd)],
546
- ["rubocop", (cwd) => hasRubocopConfig(cwd)],
547
- ["standardrb", (cwd) => hasStandardrbConfig(cwd)],
548
- ["clang-format", (cwd) => hasClangFormatConfig(cwd)],
549
- ["php-cs-fixer", (cwd) => hasPhpCsFixerConfig(cwd)],
550
- ["stylua", (cwd) => hasStyluaConfig(cwd)],
551
- ["ocamlformat", (cwd) => hasOcamlformatConfig(cwd)],
552
- ["google-java-format", (cwd) => hasGoogleJavaFormatConfig(cwd)],
553
- ["ktfmt", (cwd) => hasKtfmtConfig(cwd)],
554
- ["ktlint", (cwd) => hasKtlintConfig(cwd)],
555
- ["cljfmt", (cwd) => hasCljfmtConfig(cwd)],
556
- ["cmake-format", (cwd) => hasCmakeFormatConfig(cwd)],
557
- ["psscriptanalyzer-format", (cwd) => hasPSScriptAnalyzerConfig(cwd)],
558
- // #1595 sweep — see the comment above hasCsharpierConfig et al. in
559
- // tool-policy.ts for why nixfmt (the 8th formatter #1572 flagged) is NOT
560
- // wired here: it has no config-file convention and no manifest-marker
561
- // equivalent to `.terraform.lock.hcl`, so there is no honest opt-in signal.
562
- ["csharpier", (cwd) => hasCsharpierConfig(cwd)],
563
- ["ormolu", (cwd) => hasOrmoluConfig(cwd)],
564
- ["taplo", (cwd) => hasTaploConfig(cwd)],
565
- ["terraform", (cwd) => hasTerraformConfig(cwd)],
566
- ["swiftformat", (cwd) => hasSwiftformatConfig(cwd)],
567
- ["fantomas", (cwd) => hasFantomasConfig(cwd)],
568
- ["mix", (cwd) => hasMixFormatConfig(cwd)],
569
- ]);
570
- function hasExplicitFormatterConfig(formatterName, cwd, ext) {
571
- return (EXPLICIT_FORMATTER_CONFIG_CHECKS.get(formatterName)?.(cwd, ext) ?? false);
572
- }
573
- // Exported for the "every registered formatter is selectable" coverage guard
574
- // (formatter-policy-consistency.test.ts, #1572): derived from the table above,
575
- // not hand-listed, so it cannot drift from what `hasExplicitFormatterConfig`
576
- // actually checks.
577
- export const FORMATTERS_WITH_EXPLICIT_CONFIG_CHECK = new Set(EXPLICIT_FORMATTER_CONFIG_CHECKS.keys());
578
- // --- Formatter Definitions ---
579
- async function hasEditorConfig(cwd) {
580
- return findNearestContaining(cwd, [".editorconfig"]) !== undefined;
581
- }
582
- async function indentationArgs(filePath, tool, cwd) {
583
- if ((await hasEditorConfig(cwd)) ||
584
- hasExplicitFormatterConfig(tool, cwd, path.extname(filePath))) {
585
- return [];
586
- }
587
- let content;
588
- try {
589
- content = await fs.readFile(filePath, "utf8");
590
- }
591
- catch {
592
- // Resolution tests and callers may probe a path before it exists.
593
- return [];
594
- }
595
- if (!hasDetectableIndentation(content))
596
- return null;
597
- const indentation = detectIndentation(content);
598
- if (!indentation) {
599
- // #3038: the file HAS indentation, but only nested runs, so no unit can be
600
- // proven. The caller turns this into SKIP_FORMATTING — a file the user
601
- // expected to be formatted silently is not — so the refusal gets one
602
- // bounded row. Subject is the tool (four possible values, recorded once
603
- // per session each) rather than the path: a session that edits many such
604
- // files must not write one ledger key per file.
605
- recordDegradationOnce({
606
- kind: "formatter-skip",
607
- subject: tool,
608
- reason: "indentation evidence is ambiguous; formatter style not pinned",
609
- });
610
- return null;
611
- }
612
- if (tool === "shfmt")
613
- return indentation.style === "tab"
614
- ? ["-i", "0"]
615
- : ["-i", String(indentation.width)];
616
- if (tool === "prettier") {
617
- return [
618
- indentation.style === "tab" ? "--use-tabs" : "--no-use-tabs",
619
- "--tab-width",
620
- String(indentation.width),
621
- ];
622
- }
623
- if (tool === "ruff") {
624
- // `ruff format` has NO --indent-style/--indent-width flags (it errors with
625
- // "unexpected argument" and exits 2, which formatFile reported as a silent
626
- // clean no-op back when exit-code strictness was opt-in). Style is pinned
627
- // through inline TOML overrides instead (#1144 follow-up).
628
- return [
629
- "--config",
630
- `indent-width=${indentation.width}`,
631
- "--config",
632
- `format.indent-style='${indentation.style}'`,
633
- ];
634
- }
635
- return [
636
- "--indent-style",
637
- indentation.style,
638
- "--indent-width",
639
- String(indentation.width),
640
- ];
641
- }
642
- /**
643
- * Resolve the cwd for the formatter child, capped at the user's home
644
- * directory. Files outside a project retain the historical file-directory
645
- * cwd. The optional homeDir is only for the ceiling regression test.
646
- */
647
- export function resolveFormatterCwd(absolutePath, formatterName, homeDir = os.homedir()) {
648
- return resolveToolCwd("formatter", formatterName ?? "unknown", absolutePath, {
649
- // Formatter discovery historically walks above the file directory. The
650
- // filesystem root is the neutral dispatch boundary; the seam's `$HOME`
651
- // ceiling prevents it from escaping the user's workspace.
652
- cwd: path.parse(path.resolve(absolutePath)).root,
653
- homeDir,
654
- }).cwd;
655
- }
656
- /**
657
- * Every biome invocation must carry this. Biome exits 1 with "No files were
658
- * processed in the specified paths" when the path is ignored by the repo's own
659
- * biome.json or carries an extension biome does not handle — a benign outcome
660
- * that, under #1337's strict default, would otherwise report a formatting
661
- * failure on every edit under an ignored directory. Verified against biome
662
- * 2.4.12: ignored path → exit 1, +flag → exit 0; unsupported extension → exit
663
- * 1, +flag → exit 0; and a real syntax error still exits 1 WITH the flag, so
664
- * strictness is preserved for actual failures.
665
- *
666
- * `clients/dispatch/runners/biome-check.ts:108` already passes this on the lint
667
- * path; the formatter path did not, which is what made biome the one
668
- * misclassified formatter in the #1337 audit.
669
- */
670
- const BIOME_UNMATCHED_FLAG = "--no-errors-on-unmatched";
671
- export const biomeFormatter = {
672
- name: "biome",
673
- command: [
674
- "npx",
675
- "@biomejs/biome",
676
- "format",
677
- "--write",
678
- BIOME_UNMATCHED_FLAG,
679
- "$FILE",
680
- ],
681
- async resolveCommand(filePath, cwd) {
682
- const editorConfigFlag = (await hasEditorConfig(cwd))
683
- ? ["--use-editorconfig=true"]
684
- : [];
685
- const styleArgs = await indentationArgs(filePath, "biome", cwd);
686
- if (styleArgs === null)
687
- return SKIP_FORMATTING;
688
- const args = [
689
- "format",
690
- "--write",
691
- BIOME_UNMATCHED_FLAG,
692
- ...editorConfigFlag,
693
- ...styleArgs,
694
- ];
695
- const local = await findInNodeModules("biome", cwd);
696
- if (local)
697
- return [local, ...args, filePath];
698
- // Any package manager's global bin dir (npm/pnpm/yarn/bun) before we
699
- // auto-install — catches a `pnpm add -g @biomejs/biome` PATH misses (#375).
700
- const global = await findGlobalBinary("biome");
701
- if (global)
702
- return [global, ...args, filePath];
703
- const toolId = getAutoInstallToolIdForFormatter("biome");
704
- if (!toolId)
705
- return null;
706
- const { ensureTool } = await import("./installer/index.js");
707
- const installed = await ensureTool(toolId);
708
- if (installed)
709
- return [installed, ...args, filePath];
710
- return null;
711
- },
712
- extensions: [
713
- ".js",
714
- ".jsx",
715
- ".mjs",
716
- ".cjs",
717
- ".ts",
718
- ".tsx",
719
- ".mts",
720
- ".cts",
721
- ".json",
722
- ".jsonc",
723
- ".css",
724
- ".scss",
725
- ".sass",
726
- ".vue",
727
- ".svelte",
728
- ".html",
729
- ".htm",
730
- ],
731
- async detect(cwd) {
732
- return (hasBiomeConfig(cwd) ||
733
- hasNearestPackageJsonDependency(cwd, "@biomejs/biome"));
734
- },
735
- };
736
- export const prettierFormatter = {
737
- name: "prettier",
738
- command: ["npx", "prettier", "--write", "$FILE"],
739
- async resolveCommand(filePath, cwd) {
740
- const styleArgs = await indentationArgs(filePath, "prettier", cwd);
741
- if (styleArgs === null)
742
- return SKIP_FORMATTING;
743
- const args = ["--write", ...styleArgs];
744
- const local = await findInNodeModules("prettier", cwd);
745
- if (local)
746
- return [local, ...args, filePath];
747
- // Global bin of any manager (npm/pnpm/yarn/bun) before auto-install (#375).
748
- const global = await findGlobalBinary("prettier");
749
- if (global)
750
- return [global, ...args, filePath];
751
- return resolveManagedSmartDefaultCommand("prettier", filePath, args);
752
- },
753
- extensions: [
754
- ".js",
755
- ".jsx",
756
- ".mjs",
757
- ".cjs",
758
- ".ts",
759
- ".tsx",
760
- ".mts",
761
- ".cts",
762
- ".json",
763
- ".jsonc",
764
- ".css",
765
- ".scss",
766
- ".sass",
767
- ".less",
768
- ".vue",
769
- ".svelte",
770
- ".html",
771
- ".htm",
772
- ".md",
773
- ".mdx",
774
- ".yaml",
775
- ".yml",
776
- ".graphql",
777
- ".gql",
778
- ],
779
- async detect(cwd) {
780
- return (hasPrettierConfig(cwd) ||
781
- hasNearestPackageJsonDependency(cwd, "prettier") ||
782
- hasNearestPackageJsonField(cwd, "prettier"));
783
- },
784
- };
785
- /**
786
- * Turns "no target files left after ignore rules" into a clean exit 0.
787
- *
788
- * oxfmt is offered for every extension in `OXFMT_SUPPORTED_EXTENSIONS` as soon
789
- * as a config exists, but a config's `ignorePatterns` can exclude any of them.
790
- * Handed a path it has been told to ignore, oxfmt 0.64.0 exits 2 with
791
- * "Expected at least one target file. All matched files may have been excluded
792
- * by ignore rules." Under the #1337 strict default that reads as a formatting
793
- * failure, so a user whose oxfmt config ignores (say) Markdown sees an error on
794
- * every Markdown edit.
795
- *
796
- * Same class as biome's "No files were processed in the specified paths", and
797
- * the same fix: ask the tool not to treat an empty target set as an error,
798
- * rather than teaching `formatFile` to forgive an exit code. This is the
799
- * narrower repair — a flag cannot mask anything else, whereas status- or
800
- * message-keyed leniency would have to be trusted to stay tight across oxfmt
801
- * releases. Verified against oxfmt 0.64.0: with the flag, an ignored path exits
802
- * 0 untouched, a normal file is still rewritten, and an unparseable file still
803
- * exits 2. `tests/clients/dispatch/oxfmt-ignored-file-noop.test.ts` pins all
804
- * three against the real binary.
805
- *
806
- * The flag also silences a genuinely missing file, which `formatFile` never
807
- * reaches: it reads the file before it spawns anything, so a missing path
808
- * fails there.
809
- */
810
- const OXFMT_NO_ERROR_ON_UNMATCHED = "--no-error-on-unmatched-pattern";
811
- export const oxfmtFormatter = {
812
- name: "oxfmt",
813
- command: ["oxfmt", OXFMT_NO_ERROR_ON_UNMATCHED, "$FILE"],
814
- // #1337 audit: oxfmt (and the `vp fmt --write` path) publish no exit-code
815
- // table. `--write` is the default and `--check` is the separate verification
816
- // mode, so there is no documented nonzero-on-reformat. Absent a documented
817
- // benign-nonzero mode, it stays strict — the safe direction, since the failure
818
- // mode of guessing wrong the other way is a silent no-op (#1336).
819
- async resolveCommand(filePath, cwd) {
820
- if (hasVitePlusConfig(cwd)) {
821
- // No unmatched-pattern flag here: this is `vp`, a different CLI with
822
- // its own arguments. Whether `vp fmt` has the same empty-target
823
- // behavior is unverified, so nothing is claimed about it.
824
- const localVp = await findInNodeModules("vp", cwd);
825
- if (localVp)
826
- return [localVp, "fmt", filePath, "--write"];
827
- const globalVp = await which("vp");
828
- if (globalVp)
829
- return [globalVp, "fmt", filePath, "--write"];
830
- }
831
- const local = await findInNodeModules("oxfmt", cwd);
832
- if (local)
833
- return [local, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
834
- const found = await which("oxfmt");
835
- if (found)
836
- return [found, OXFMT_NO_ERROR_ON_UNMATCHED, filePath];
837
- return ((await resolveManagedFormatterCommand("oxfmt", filePath, [OXFMT_NO_ERROR_ON_UNMATCHED], findInNodeModules, cwd)) ?? FORMATTER_UNAVAILABLE);
838
- },
839
- // Single source of truth: OXFMT_SUPPORTED_EXTENSIONS in tool-policy.ts.
840
- // Do not hand-maintain a second copy of this list (#1134 — previously two
841
- // parallel hand-maintained lists, the #883 single-source-of-truth class).
842
- extensions: [...OXFMT_SUPPORTED_EXTENSIONS],
843
- async detect(cwd) {
844
- return (hasOxfmtConfig(cwd) ||
845
- hasVitePlusConfig(cwd) ||
846
- // Published package is `oxfmt` (the scoped name does not exist on npm).
847
- hasNearestPackageJsonDependency(cwd, "oxfmt") ||
848
- hasNearestPackageJsonDependency(cwd, "@oxc-project/oxfmt"));
849
- },
850
- };
851
- export const ruffFormatter = {
852
- name: "ruff",
853
- command: ["ruff", "format", "$FILE"],
854
- extensions: [".py", ".pyi"],
855
- // Strict (the #1337 default): `ruff format` exits 0 on a successful in-place
856
- // rewrite and 2 on argument rejection / syntax error (verified, ruff 0.x:
857
- // well-formed → 0, reformatted → 0, unparseable → 2). The exit-2 no-op is
858
- // exactly what hid the bad --indent-style flags for a full release cycle.
859
- async resolveCommand(filePath, cwd) {
860
- const styleArgs = await indentationArgs(filePath, "ruff", cwd);
861
- if (styleArgs === null)
862
- return SKIP_FORMATTING;
863
- const args = ["format", ...styleArgs];
864
- const venv = await findInVenv("ruff", cwd);
865
- if (venv)
866
- return [venv, ...args, filePath];
867
- const toolId = getAutoInstallToolIdForFormatter("ruff");
868
- if (!toolId)
869
- return null;
870
- const { ensureTool } = await import("./installer/index.js");
871
- const installed = await ensureTool(toolId);
872
- if (installed)
873
- return [installed, ...args, filePath];
874
- // #2413: `ensureTool` already resolves PATH, global bins and the managed
875
- // dir (and attempts an install) before returning falsy, so reaching here
876
- // means ruff is genuinely nowhere. `detect()`'s `hasRuffConfig` branch has
877
- // no binary check, so a config-only project selects ruff with nothing
878
- // installed; the static bare `ruff` would then ENOENT. Prove it unavailable.
879
- return FORMATTER_UNAVAILABLE;
880
- },
881
- async detect(cwd) {
882
- if (hasRuffConfig(cwd))
883
- return true;
884
- // No-config fallback: if Ruff is already available, allow formatter usage.
885
- // This keeps Python default behavior consistent with startup defaults.
886
- return Boolean(await getToolPath("ruff"));
887
- },
888
- };
889
- export const blackFormatter = {
890
- name: "black",
891
- command: ["black", "$FILE"],
892
- extensions: [".py", ".pyi"],
893
- resolveCommand: managedFormatterResolver("black", [], findInVenv),
894
- async detect(cwd) {
895
- return hasBlackConfig(cwd);
896
- },
897
- };
898
- export const sqlfluffFormatter = {
899
- name: "sqlfluff",
900
- command: ["sqlfluff", "fix", "--force", "$FILE"],
901
- extensions: [".sql"],
902
- lenientExitCode: "lint-autofix: `sqlfluff fix` writes the corrected file and then exits 1 " +
903
- "when unfixable violations remain (its documented exit codes are 0 = all " +
904
- "clean, 1 = violations remain, 2 = command/config failure), so a nonzero " +
905
- "exit routinely accompanies a successful rewrite.",
906
- lenientStatuses: [1],
907
- async resolveCommand(filePath, cwd) {
908
- const venv = await findInVenv("sqlfluff", cwd);
909
- if (venv)
910
- return [venv, "fix", "--force", filePath];
911
- return null;
912
- },
913
- async detect(cwd) {
914
- return hasSqlfluffConfig(cwd);
915
- },
916
- };
917
- export const gofmtFormatter = {
918
- name: "gofmt",
919
- command: ["gofmt", "-w", "$FILE"],
920
- extensions: [".go"],
921
- async resolveCommand(filePath, _cwd) {
922
- const gofmtBinary = await resolveGoFmtBinary();
923
- if (!gofmtBinary)
924
- return null;
925
- return [gofmtBinary, "-w", filePath];
926
- },
927
- async detect(_cwd) {
928
- return (await resolveGoFmtBinary()) !== null;
929
- },
930
- };
931
- export const rustfmtFormatter = {
932
- name: "rustfmt",
933
- command: ["rustfmt", "$FILE"],
934
- extensions: [".rs"],
935
- // #2466: rustfmt defaults to an OLDER edition than the file's actual Cargo
936
- // package when invoked bare, so it can reject valid newer-edition syntax
937
- // (e.g. Rust 2024). Carry the nearest package's `edition` (honoring
938
- // `edition.workspace = true` inheritance) through `--edition`; `undefined`
939
- // (unreadable/unparseable manifest) falls back to the static command
940
- // above, unchanged from pre-#2466 behavior.
941
- async resolveCommand(filePath, _cwd) {
942
- const edition = await resolveCargoPackageEdition(filePath);
943
- if (edition === undefined)
944
- return null;
945
- return ["rustfmt", "--edition", edition, filePath];
946
- },
947
- async detect(cwd) {
948
- if ((await which("rustfmt")) !== null)
949
- return true;
950
- // If we're in a Rust project, attempt one lazy install of rustfmt component.
951
- const rustProject = (await findUp(["Cargo.toml"], cwd)).length > 0;
952
- if (!rustProject)
953
- return false;
954
- if ((await which("rustup")) === null)
955
- return false;
956
- await tryLazyInstallFormatterTool("rustfmt", cwd);
957
- return (await which("rustfmt")) !== null;
958
- },
959
- };
960
- export const zigFormatter = {
961
- name: "zig",
962
- command: ["zig", "fmt", "$FILE"],
963
- extensions: [".zig", ".zon"],
964
- async detect(_cwd) {
965
- return (await which("zig")) !== null;
966
- },
967
- };
968
- export const dartFormatter = {
969
- name: "dart",
970
- command: ["dart", "format", "$FILE"],
971
- extensions: [".dart"],
972
- async detect(_cwd) {
973
- return (await which("dart")) !== null;
974
- },
975
- };
976
- export const shfmtFormatter = {
977
- name: "shfmt",
978
- command: ["shfmt", "-w", "$FILE"],
979
- extensions: [".sh", ".bash"],
980
- async resolveCommand(filePath, cwd) {
981
- const styleArgs = await indentationArgs(filePath, "shfmt", cwd);
982
- if (styleArgs === null)
983
- return SKIP_FORMATTING;
984
- const inPath = await which("shfmt");
985
- if (inPath)
986
- return [inPath, "-w", ...styleArgs, filePath];
987
- return resolveManagedSmartDefaultCommand("shfmt", filePath, [
988
- "-w",
989
- ...styleArgs,
990
- ]);
991
- },
992
- detect: managedToolDetect("shfmt", undefined, async () => (await which("shfmt")) !== null),
993
- };
994
- export const nixfmtFormatter = {
995
- name: "nixfmt",
996
- command: ["nixfmt", "$FILE"],
997
- // #1337 audit: nixfmt's README documents in-place formatting but has no
998
- // exit-code section and no change-detection mode, so nothing documents a
999
- // benign nonzero. Strict by default (see oxfmt above for the rationale).
1000
- extensions: [".nix"],
1001
- async detect(_cwd) {
1002
- return (await which("nixfmt")) !== null;
1003
- },
1004
- };
1005
- export const mixFormatter = {
1006
- name: "mix",
1007
- command: ["mix", "format", "$FILE"],
1008
- extensions: [".ex", ".exs", ".eex", ".heex", ".leex"],
1009
- async detect(_cwd) {
1010
- return (await which("mix")) !== null;
1011
- },
1012
- };
1013
- export const ocamlformatFormatter = {
1014
- name: "ocamlformat",
1015
- command: ["ocamlformat", "-i", "$FILE"],
1016
- extensions: [".ml", ".mli"],
1017
- async detect(cwd) {
1018
- const hasBinary = (await which("ocamlformat")) !== null;
1019
- if (!hasBinary)
1020
- return false;
1021
- const configs = [".ocamlformat"];
1022
- const found = await findUp(configs, cwd);
1023
- return found.length > 0;
1024
- },
1025
- };
1026
- export const clangFormatFormatter = {
1027
- name: "clang-format",
1028
- command: ["clang-format", "-i", "$FILE"],
1029
- extensions: [".c", ".cc", ".cpp", ".cxx", ".h", ".hpp", ".ino"],
1030
- async detect(cwd) {
1031
- const hasBinary = (await which("clang-format")) !== null;
1032
- if (!hasBinary)
1033
- return false;
1034
- const configs = [".clang-format", "_clang-format"];
1035
- const found = await findUp(configs, cwd);
1036
- return found.length > 0;
1037
- },
1038
- };
1039
- export const ktlintFormatter = {
1040
- name: "ktlint",
1041
- command: ["ktlint", "-F", "$FILE"],
1042
- extensions: [".kt", ".kts"],
1043
- lenientExitCode: "lint-autofix: `ktlint -F` autocorrects what it can and then exits 1 if " +
1044
- "any lint error remains unfixed — the documented CLI contract (ktlint " +
1045
- "exits nonzero whenever violations are reported, and -F does not suppress " +
1046
- "the ones it cannot correct).",
1047
- lenientStatuses: [1],
1048
- async resolveCommand(filePath, _cwd) {
1049
- const inPath = await which("ktlint");
1050
- if (inPath)
1051
- return [inPath, "-F", filePath];
1052
- return resolveManagedSmartDefaultCommand("ktlint", filePath, ["-F"]);
1053
- },
1054
- detect: managedToolDetect("ktlint", undefined, async () => (await which("ktlint")) !== null),
1055
- };
1056
- export const ktfmtFormatter = {
1057
- name: "ktfmt",
1058
- // ktfmt formats in place when given a file path (no flag needed).
1059
- command: ["ktfmt", "$FILE"],
1060
- extensions: [".kt", ".kts"],
1061
- // #2468: ktfmt's CLI never reads a project's Gradle `ktfmt { googleStyle()
1062
- // | kotlinLangStyle() }` selection — style is CLI-flag-only
1063
- // (`--google-style`/`--kotlinlang-style`, verified against ktfmt v0.63's
1064
- // own arg parser). Carry the nearest module's declared style through;
1065
- // `undefined` (no declaration, unreadable/unparseable manifest, or an
1066
- // unsupported style like the removed `dropboxStyle()`) falls back to the
1067
- // bare invocation, unchanged from pre-#2468 behavior.
1068
- async resolveCommand(filePath, _cwd) {
1069
- const styleFlag = await resolveKtfmtGradleStyle(filePath);
1070
- const inPath = await which("ktfmt");
1071
- if (inPath)
1072
- return styleFlag ? [inPath, styleFlag, filePath] : [inPath, filePath];
1073
- const { ensureTool } = await import("./installer/index.js");
1074
- const installed = await ensureTool("ktfmt");
1075
- // #2413: which() and ensureTool (PATH/global/managed + install) both
1076
- // failed, and `detect()` gates only on hasKtfmtConfig — no binary probe —
1077
- // so a config-only project reaches here with ktfmt absent. The static
1078
- // command is bare `ktfmt`; spawning it reproduces the ENOENT class.
1079
- if (!installed)
1080
- return FORMATTER_UNAVAILABLE;
1081
- return styleFlag ? [installed, styleFlag, filePath] : [installed, filePath];
1082
- },
1083
- async detect(cwd) {
1084
- // Opt-in only: ktfmt becomes the formatter when the project elects it,
1085
- // otherwise ktlint stays the Kotlin smart-default (#129).
1086
- return hasKtfmtConfig(cwd);
1087
- },
1088
- };
1089
- export const rubocopFormatter = {
1090
- name: "rubocop",
1091
- command: ["rubocop", "-a", "--no-color", "$FILE"],
1092
- extensions: [".rb", ".rake", ".gemspec", ".ru"],
1093
- lenientExitCode: "lint-autofix: `rubocop -a` exits 1 whenever ANY offense remains after it " +
1094
- "has already rewritten the file. Verified locally (rubocop on Ruby 3.4): a " +
1095
- "file with an unfixable Lint/UselessAssignment exits 1, and even a " +
1096
- "tidy file exits 1 on an unfixable Style/Documentation offense — nonzero " +
1097
- "is the normal outcome, not a failure.",
1098
- lenientStatuses: [1],
1099
- async resolveCommand(filePath, cwd) {
1100
- if (await canUseBundleExec(cwd))
1101
- return ["bundle", "exec", "rubocop", "-a", "--no-color", filePath];
1102
- return null;
1103
- },
1104
- async detect(cwd) {
1105
- if (!hasRubocopConfig(cwd))
1106
- return false;
1107
- if ((await which("rubocop")) !== null)
1108
- return true;
1109
- await tryLazyInstallFormatterTool("rubocop", cwd);
1110
- return (await which("rubocop")) !== null;
1111
- },
1112
- };
1113
- export const standardrbFormatter = {
1114
- name: "standardrb",
1115
- command: ["standardrb", "--fix", "$FILE"],
1116
- extensions: [".rb", ".rake"],
1117
- lenientExitCode: "lint-autofix: standardrb is a RuboCop wrapper and inherits its exit " +
1118
- "contract — `--fix` exits 1 when offenses remain after the rewrite.",
1119
- lenientStatuses: [1],
1120
- async resolveCommand(filePath, cwd) {
1121
- if (await canUseBundleExec(cwd))
1122
- return ["bundle", "exec", "standardrb", "--fix", filePath];
1123
- return null;
1124
- },
1125
- async detect(cwd) {
1126
- if (!hasStandardrbConfig(cwd))
1127
- return false;
1128
- return (await which("standardrb")) !== null;
1129
- },
1130
- };
1131
- export const gleamFormatter = {
1132
- name: "gleam",
1133
- command: ["gleam", "format", "$FILE"],
1134
- extensions: [".gleam"],
1135
- async detect(cwd) {
1136
- // Present if gleam.toml exists (any Gleam project)
1137
- const found = await findUp(["gleam.toml"], cwd);
1138
- if (found.length > 0)
1139
- return (await which("gleam")) !== null;
1140
- return false;
1141
- },
1142
- };
1143
- export const typstyleFormatter = {
1144
- name: "typstyle",
1145
- command: ["typstyle", "-i", "$FILE"],
1146
- extensions: [".typ", ".typc"],
1147
- async resolveCommand(filePath) {
1148
- const inPath = await which("typstyle");
1149
- if (inPath)
1150
- return [inPath, "-i", filePath];
1151
- return resolveManagedSmartDefaultCommand("typstyle", filePath, ["-i"]);
1152
- },
1153
- detect: managedToolDetect("typstyle", undefined, async () => (await which("typstyle")) !== null),
1154
- };
1155
- export const terraformFormatter = {
1156
- name: "terraform",
1157
- command: ["terraform", "fmt", "$FILE"],
1158
- extensions: [".tf", ".tfvars"],
1159
- async detect(_cwd) {
1160
- return (await which("terraform")) !== null;
1161
- },
1162
- };
1163
- export const terragruntHclFormatter = {
1164
- name: "terragrunt-hcl",
1165
- command: ["terragrunt", "hcl", "fmt", "--file", "$FILE"],
1166
- extensions: [],
1167
- filenames: TERRAGRUNT_FILENAMES,
1168
- // Strict (the #1337 default): verified exit 0 on a successful in-place format
1169
- // against terragrunt v1.1.2. A binary predating the `hcl` command group exits
1170
- // nonzero and touches nothing, which unguarded reads as "already formatted".
1171
- async detect(_cwd) {
1172
- return (await which("terragrunt")) !== null;
1173
- },
1174
- };
1175
- export const phpCsFixerFormatter = {
1176
- name: "php-cs-fixer",
1177
- command: ["php-cs-fixer", "fix", "$FILE"],
1178
- extensions: [".php"],
1179
- // #2472: php-cs-fixer does NOT walk up parent directories looking for its
1180
- // own config the way prettier/biome/eslint do (verified against upstream
1181
- // `computeConfigFiles()` — see `resolvePhpCsFixerConfig`'s doc comment),
1182
- // and `formatFile` spawns with cwd = the FILE's own directory, which is
1183
- // not necessarily where the ancestor config `detect()` found actually
1184
- // lives. Always resolve the binary explicitly here (vendor/bin first,
1185
- // then global) rather than falling through to the static `command` above
1186
- // — that static command can never carry `--config`, so a config found at
1187
- // an ancestor would silently be dropped whenever the vendor lookup here
1188
- // missed but a global binary still resolved. `--config` is attached
1189
- // whenever a config resolves, even when it sits in the file's own
1190
- // directory (AC3): unlike the pre-#2472 code, correctness no longer
1191
- // depends on php-cs-fixer's own (nonexistent) upward search.
1192
- async resolveCommand(filePath, cwd) {
1193
- const configPath = resolvePhpCsFixerConfig(filePath);
1194
- const binary = (await findInVendorBin("php-cs-fixer", cwd)) ??
1195
- (await which("php-cs-fixer"));
1196
- const managed = await resolveManagedFormatterCommand("php-cs-fixer", filePath, [], findInVendorBin, cwd);
1197
- if (managed === FORMATTER_UNAVAILABLE)
1198
- return FORMATTER_UNAVAILABLE;
1199
- const resolved = binary ?? managed[0];
1200
- // #2413/#2472 review F4: both probes (vendor/bin, then PATH) have
1201
- // PROVEN the binary is absent — returning `null` here would fall back
1202
- // to the static `command` above, which is the SAME bare
1203
- // `php-cs-fixer` this just failed to find, spawning it only to
1204
- // re-observe the ENOENT already known. Report the proven-missing
1205
- // state instead so `formatFile` skips the wasted spawn.
1206
- if (!resolved)
1207
- return FORMATTER_UNAVAILABLE;
1208
- return configPath
1209
- ? [resolved, "fix", "--config", configPath, filePath]
1210
- : [resolved, "fix", filePath];
1211
- },
1212
- detect: managedToolDetect("php-cs-fixer", async (cwd) => (await findUp([".php-cs-fixer.php", ".php-cs-fixer.dist.php"], cwd))
1213
- .length > 0, async (cwd) => Boolean((await findInVendorBin("php-cs-fixer", cwd)) ||
1214
- (await which("php-cs-fixer")))),
1215
- };
1216
- export const csharpierFormatter = {
1217
- name: "csharpier",
1218
- // CSharpier ≥1.0 is a standalone `csharpier format <file>`; the `dotnet
1219
- // csharpier <file>` form was removed (a bare `dotnet csharpier` now errors
1220
- // "a dotnet-prefixed executable with this name could not be found"). Keep the
1221
- // legacy form as a fallback for CSharpier 0.x via resolveCommand.
1222
- command: ["csharpier", "format", "$FILE"],
1223
- extensions: [".cs"],
1224
- async resolveCommand(filePath, _cwd) {
1225
- if ((await which("csharpier")) !== null) {
1226
- return ["csharpier", "format", filePath];
1227
- }
1228
- // CSharpier 0.x: invoked through the dotnet driver.
1229
- if ((await which("dotnet")) !== null) {
1230
- const legacy = await probeToolAsync("dotnet", ["csharpier", "--version"], { timeout: 5000 });
1231
- if (!legacy.error && legacy.status === 0) {
1232
- return ["dotnet", "csharpier", filePath];
1233
- }
1234
- }
1235
- return null;
1236
- },
1237
- async detect(_cwd) {
1238
- // CSharpier ≥1.0 standalone binary …
1239
- if ((await which("csharpier")) !== null)
1240
- return true;
1241
- // … or the legacy dotnet-driver form (CSharpier 0.x).
1242
- if ((await which("dotnet")) === null)
1243
- return false;
1244
- const result = await probeToolAsync("dotnet", ["csharpier", "--version"], {
1245
- timeout: 5000,
1246
- });
1247
- return !result.error && result.status === 0;
1248
- },
1249
- };
1250
- export const fantomasFormatter = {
1251
- name: "fantomas",
1252
- command: ["fantomas", "$FILE"],
1253
- extensions: [".fs", ".fsi", ".fsx"],
1254
- async detect(_cwd) {
1255
- return (await which("fantomas")) !== null;
1256
- },
1257
- };
1258
- export const swiftformatFormatter = {
1259
- name: "swiftformat",
1260
- command: ["swiftformat", "$FILE"],
1261
- extensions: [".swift"],
1262
- async detect(_cwd) {
1263
- return (await which("swiftformat")) !== null;
1264
- },
1265
- };
1266
- export const styluaFormatter = {
1267
- name: "stylua",
1268
- command: ["stylua", "$FILE"],
1269
- extensions: [".lua"],
1270
- resolveCommand: managedFormatterResolver("stylua", [], findLocalBinUpwards),
1271
- detect: managedToolDetect("stylua", async (cwd) => (await findUp(["stylua.toml", ".stylua.toml"], cwd)).length > 0, async (cwd) => Boolean(findLocalBinUpwards("stylua", cwd) || (await which("stylua")))),
1272
- };
1273
- export const ormoluFormatter = {
1274
- name: "ormolu",
1275
- command: ["ormolu", "--mode", "inplace", "$FILE"],
1276
- extensions: [".hs", ".lhs"],
1277
- async detect(_cwd) {
1278
- return (await which("ormolu")) !== null;
1279
- },
1280
- };
1281
- export const taploFormatter = {
1282
- name: "taplo",
1283
- command: ["taplo", "fmt", "$FILE"],
1284
- extensions: [".toml"],
1285
- async resolveCommand(filePath, _cwd) {
1286
- const inPath = await which("taplo");
1287
- if (inPath)
1288
- return [inPath, "fmt", filePath];
1289
- return resolveManagedSmartDefaultCommand("taplo", filePath, ["fmt"]);
1290
- },
1291
- detect: managedToolDetect("taplo", undefined, async () => (await which("taplo")) !== null),
1292
- };
1293
- export const googleJavaFormatFormatter = {
1294
- name: "google-java-format",
1295
- command: ["google-java-format", "--replace", "$FILE"],
1296
- extensions: [".java"],
1297
- resolveCommand: managedFormatterResolver("google-java-format", ["--replace"]),
1298
- detect: managedToolDetect("google-java-format", hasGoogleJavaFormatConfig, async () => (await which("google-java-format")) !== null),
1299
- };
1300
- export const cljfmtFormatter = {
1301
- name: "cljfmt",
1302
- command: ["cljfmt", "fix", "$FILE"],
1303
- extensions: [".clj", ".cljc", ".cljs"],
1304
- resolveCommand: managedFormatterResolver("cljfmt", ["fix"]),
1305
- detect: managedToolDetect("cljfmt", hasCljfmtConfig, async () => (await which("cljfmt")) !== null),
1306
- };
1307
- export const cmakeFormatFormatter = {
1308
- name: "cmake-format",
1309
- command: ["cmake-format", "-i", "$FILE"],
1310
- extensions: [".cmake"],
1311
- resolveCommand: managedFormatterResolver("cmake-format", ["-i"]),
1312
- detect: managedToolDetect("cmake-format", hasCmakeFormatConfig, async () => (await which("cmake-format")) !== null),
1313
- };
1314
- export const cueFormatter = {
1315
- name: "cue",
1316
- // `cue fmt <file>` rewrites in place already. There is no `-w`: the only
1317
- // flags are --check, -d/--diff and --files, so `cue fmt -w` aborts with
1318
- // "unknown shorthand flag: 'w'" on every .cue write (verified on cue
1319
- // v0.17.1).
1320
- command: ["cue", "fmt", "$FILE"],
1321
- extensions: [".cue"],
1322
- async detect(_cwd) {
1323
- return (await which("cue")) !== null;
1324
- },
1325
- };
1326
- /**
1327
- * The PowerShell one-liner behind `psscriptanalyzer-format`.
1328
- *
1329
- * #1337 audit finding: without `$ErrorActionPreference = 'Stop'`, a failing
1330
- * `Invoke-Formatter` is a NON-TERMINATING error — pwsh still exits 0, so this
1331
- * formatter could never report a failure through the strict seam and the #1336
1332
- * silent-no-op class survived intact for `.ps1/.psm1/.psd1`. Verified: an
1333
- * `Invoke-Formatter` argument-validation failure exits 0 under the old script.
1334
- *
1335
- * Two more defects fixed here rather than left as landmines:
1336
- * - the old script ran `Set-Content -Value $formatted` even when `$formatted`
1337
- * was `$null`, which TRUNCATES the file it was asked to format;
1338
- * - single-quoted interpolation broke on any path containing an apostrophe.
1339
- * An empty/whitespace-only file exits 0 without touching anything, so "nothing
1340
- * to format" stays a clean no-op rather than a reported failure.
1341
- *
1342
- * `settingsPath`, when the project has one (#1572 review F2), is passed
1343
- * straight to `Invoke-Formatter -Settings`. Gating selection on the file's
1344
- * presence without ever reading its rules would run the project through the
1345
- * stock `CodeFormatting` ruleset regardless of what it declared — the same
1346
- * stock-style imposition #1144 banned for the other config-first formatters.
1347
- */
1348
- function psScriptAnalyzerCommand(filePath, settingsPath) {
1349
- // PowerShell single-quoted strings escape an apostrophe by doubling it.
1350
- const quoted = filePath.replace(/'/g, "''");
1351
- const settingsArg = settingsPath
1352
- ? ` -Settings '${settingsPath.replace(/'/g, "''")}'`
1353
- : "";
1354
- return [
1355
- "$ErrorActionPreference = 'Stop'",
1356
- `$p = '${quoted}'`,
1357
- "$content = Get-Content -Raw -LiteralPath $p",
1358
- "if ([string]::IsNullOrWhiteSpace($content)) { exit 0 }",
1359
- `$formatted = Invoke-Formatter -ScriptDefinition $content${settingsArg}`,
1360
- "if ($null -eq $formatted) { throw 'Invoke-Formatter returned no output' }",
1361
- "Set-Content -LiteralPath $p -Value $formatted",
1362
- ].join("; ");
1363
- }
1364
- export const psscriptanalyzerFormatFormatter = {
1365
- name: "psscriptanalyzer-format",
1366
- command: ["pwsh", "-NoProfile", "-Command", psScriptAnalyzerCommand("$FILE")],
1367
- extensions: [".ps1", ".psm1", ".psd1"],
1368
- async resolveCommand(filePath, cwd) {
1369
- const pwsh = (await which("pwsh")) ?? (await which("powershell"));
1370
- if (!pwsh)
1371
- return null;
1372
- const settingsPath = findPSScriptAnalyzerConfigPath(cwd);
1373
- return [
1374
- pwsh,
1375
- "-NoProfile",
1376
- "-Command",
1377
- psScriptAnalyzerCommand(filePath, settingsPath),
1378
- ];
1379
- },
1380
- async detect(_cwd) {
1381
- const pwsh = (await which("pwsh")) ?? (await which("powershell"));
1382
- if (!pwsh)
1383
- return false;
1384
- // Check PSScriptAnalyzer module is available
1385
- const result = await probeToolAsync(pwsh, [
1386
- "-NoProfile",
1387
- "-Command",
1388
- "Get-Module -ListAvailable PSScriptAnalyzer | Select-Object -First 1 -ExpandProperty Name",
1389
- ], { timeout: 5_000 });
1390
- return (result.stdout ?? "").includes("PSScriptAnalyzer");
1391
- },
1392
- };
1393
- // --- Registry ---
1394
- // Exported for the formatter/policy drift guard (tests/clients/
1395
- // formatter-policy-consistency.test.ts, #1135): the test cross-checks these
1396
- // definitions against tool-policy.ts's FORMATTER_POLICY_BY_EXTENSION so the two
1397
- // hand-maintained inverse mappings can never silently diverge.
1398
- export const ALL_FORMATTERS = [
1399
- biomeFormatter,
1400
- prettierFormatter,
1401
- oxfmtFormatter,
1402
- ruffFormatter,
1403
- blackFormatter,
1404
- sqlfluffFormatter,
1405
- gofmtFormatter,
1406
- rustfmtFormatter,
1407
- zigFormatter,
1408
- dartFormatter,
1409
- shfmtFormatter,
1410
- nixfmtFormatter,
1411
- mixFormatter,
1412
- ocamlformatFormatter,
1413
- clangFormatFormatter,
1414
- ktlintFormatter,
1415
- ktfmtFormatter,
1416
- terraformFormatter,
1417
- terragruntHclFormatter,
1418
- phpCsFixerFormatter,
1419
- csharpierFormatter,
1420
- fantomasFormatter,
1421
- swiftformatFormatter,
1422
- styluaFormatter,
1423
- ormoluFormatter,
1424
- rubocopFormatter,
1425
- standardrbFormatter,
1426
- gleamFormatter,
1427
- typstyleFormatter,
1428
- taploFormatter,
1429
- googleJavaFormatFormatter,
1430
- cljfmtFormatter,
1431
- cmakeFormatFormatter,
1432
- cueFormatter,
1433
- psscriptanalyzerFormatFormatter,
1434
- ];
1435
- // Basenames claimed by a filename-keyed formatter, e.g. terragrunt.hcl.
1436
- const FILENAME_FORMATTER_BASENAMES = new Set(ALL_FORMATTERS.flatMap((f) => f.filenames ?? []));
1437
- // Cache for detection results - stores array of enabled formatter names per cwd+ext
1438
- const detectionCache = new BoundedLruCache(32);
1439
- // The signature is immutable for a cache generation. This memo is separate
1440
- // from detectionCache because a cwd can have several extension entries, and a
1441
- // warm lookup must not repeat the ancestor walk or stat matched configs.
1442
- const formatterSignatureFlights = new BoundedLruCache(32);
1443
- const formatterCacheGeneration = createGenerationSource("formatter-cache");
1444
- // These are the formatter configuration files consulted by the policy helpers
1445
- // above. Their metadata is captured by the cold detection signature. The
1446
- // write-result seam invalidates that signature when a config path changes, so
1447
- // detection re-runs even when PATH and installed tools are unchanged. A
1448
- // filename a `has*Config` check reads but this list omits is invisible to the
1449
- // cache: the signature never moves when that file is added, so a project that
1450
- // opts in AFTER the first `getFormattersForFile` call for its cwd keeps
1451
- // getting the stale (pre-opt-in) cached answer for the rest of the session
1452
- // (#1572 review F1 — proved for psscriptanalyzer-format's settings file;
1453
- // swept against every `EXPLICIT_FORMATTER_CONFIG_CHECKS` entry below, which
1454
- // turned up the same gap for google-java-format, cljfmt, cmake-format, the
1455
- // Kotlin/Spotless gradle files, sqlfluff's setup.cfg, and oxfmt's
1456
- // vite-plus.json / additional vite.config extensions).
1457
- const FORMATTER_CONFIG_FILES = [
1458
- "package.json",
1459
- "biome.json",
1460
- "biome.jsonc",
1461
- ".prettierrc",
1462
- ".prettierrc.json",
1463
- ".prettierrc.yml",
1464
- ".prettierrc.yaml",
1465
- ".prettierrc.js",
1466
- ".prettierrc.cjs",
1467
- ".prettierrc.mjs",
1468
- "prettier.config.js",
1469
- "prettier.config.cjs",
1470
- "prettier.config.mjs",
1471
- "prettier.config.ts",
1472
- "pyproject.toml",
1473
- "ruff.toml",
1474
- ".ruff.toml",
1475
- "black.toml",
1476
- ".black",
1477
- "tox.ini",
1478
- "setup.cfg",
1479
- "requirements.txt",
1480
- "Pipfile",
1481
- ".sqlfluff",
1482
- ".rubocop.yml",
1483
- ".rubocop.yaml",
1484
- "Gemfile",
1485
- ".clang-format",
1486
- "_clang-format",
1487
- ".php-cs-fixer.php",
1488
- ".php-cs-fixer.dist.php",
1489
- "stylua.toml",
1490
- ".stylua.toml",
1491
- ".ocamlformat",
1492
- ".editorconfig",
1493
- ".google-java-format",
1494
- ".ktfmt",
1495
- ".ktfmt.kts",
1496
- "build.gradle.kts",
1497
- "build.gradle",
1498
- "settings.gradle.kts",
1499
- "settings.gradle",
1500
- ".cljfmt.edn",
1501
- "cljfmt.edn",
1502
- ".cljfmt",
1503
- ".cmake-format",
1504
- ".cmake-format.yaml",
1505
- ".cmake-format.yml",
1506
- ".cmake-format.json",
1507
- ".cmake-format.py",
1508
- "cmake-format.py",
1509
- "cmake-format.yaml",
1510
- "cmake-format.yml",
1511
- "oxfmt.toml",
1512
- ".oxfmtrc.json",
1513
- "vite-plus.json",
1514
- "vite.config.ts",
1515
- "vite.config.mts",
1516
- "vite.config.cts",
1517
- "vite.config.js",
1518
- "vite.config.mjs",
1519
- "vite.config.cjs",
1520
- "PSScriptAnalyzerSettings.psd1",
1521
- "ScriptAnalyzerSettings.psd1",
1522
- // #1595 sweep additions.
1523
- ".csharpierrc",
1524
- ".csharpierrc.json",
1525
- ".csharpierrc.yaml",
1526
- ".csharpierrc.yml",
1527
- ".ormolu",
1528
- "taplo.toml",
1529
- ".taplo.toml",
1530
- ".terraform.lock.hcl",
1531
- ".swiftformat",
1532
- ".fantomasignore",
1533
- ".formatter.exs",
1534
- ];
1535
- async function formatterConfigSignature(cwd) {
1536
- const paths = await findUp(FORMATTER_CONFIG_FILES, cwd);
1537
- const parts = await Promise.all(paths.sort(compareOrdinal).map(async (filePath) => {
1538
- try {
1539
- const stat = await fs.stat(filePath);
1540
- return `${filePath}:${stat.mtimeMs}:${stat.size}`;
1541
- }
1542
- catch {
1543
- return `${filePath}:missing`;
1544
- }
1545
- }));
1546
- return parts.join("|");
1547
- }
1548
- async function getFormatterConfigSignature(cwd, normalizedCwd) {
1549
- const existing = formatterSignatureFlights.get(normalizedCwd);
1550
- if (existing)
1551
- return existing.promise;
1552
- const promise = formatterConfigSignature(cwd).finally(() => {
1553
- const current = formatterSignatureFlights.get(normalizedCwd);
1554
- if (current?.promise === promise)
1555
- formatterSignatureFlights.delete(normalizedCwd);
1556
- });
1557
- formatterSignatureFlights.set(normalizedCwd, { promise });
1558
- return promise;
1559
- }
1560
- // --- Public API ---
1561
- export async function getFormattersForFile(filePath, cwd) {
1562
- const ext = path.extname(filePath).toLowerCase();
1563
- const base = path.basename(filePath).toLowerCase();
1564
- // Filename-keyed formatters (e.g. terragrunt.hcl) can share an extension
1565
- // with unrelated files in the same dir (.terraform.lock.hcl next to
1566
- // terragrunt.hcl). Fold the basename into the cache key only when a
1567
- // filename-based formatter actually applies, so a plain .hcl file cached
1568
- // first doesn't poison the cache for terragrunt.hcl/root.hcl, or vice versa.
1569
- const normalizedCwd = normalizeMapKey(cwd);
1570
- const cacheKey = FILENAME_FORMATTER_BASENAMES.has(base)
1571
- ? `${normalizedCwd}:${ext}:${base}`
1572
- : `${normalizedCwd}:${ext}`;
1573
- // A warm entry is authoritative until the write-result seam invalidates its
1574
- // config path. This is the only way to make the warm path free of config
1575
- // polling while still reacting immediately to pi-authored create/remove/
1576
- // change events. External editor changes remain a documented session-boundary
1577
- // limitation, like other write-result-owned freshness seams.
1578
- let cached = detectionCache.get(normalizedCwd);
1579
- if (cached?.entries.has(cacheKey)) {
1580
- const enabledNames = cached.entries.get(cacheKey);
1581
- const selectedFormatterName = enabledNames && enabledNames.length > 0 ? enabledNames[0] : null;
1582
- logLatency({
1583
- type: "phase",
1584
- phase: "formatter_selected",
1585
- filePath,
1586
- durationMs: 0,
1587
- metadata: {
1588
- formatter: selectedFormatterName,
1589
- reason: "cache",
1590
- cached: true,
1591
- outcome: "hit",
1592
- cwd,
1593
- },
1594
- });
1595
- if (!enabledNames || enabledNames.length === 0)
1596
- return [];
1597
- return ALL_FORMATTERS.filter((f) => enabledNames.includes(f.name));
1598
- }
1599
- if (!cached) {
1600
- const generation = formatterCacheGeneration.capture();
1601
- const configSignature = await getFormatterConfigSignature(cwd, normalizedCwd);
1602
- // An invalidation can happen while the first signature walk is in flight.
1603
- // Do not publish a pre-invalidation signature into the new generation.
1604
- if (!generation.isCurrent()) {
1605
- return getFormattersForFile(filePath, cwd);
1606
- }
1607
- // Another cold caller for this cwd can finish the shared signature flight
1608
- // first. Re-read the LRU before installing an object so the later caller
1609
- // merges its extension entry into the existing object instead of replacing
1610
- // the earlier entry.
1611
- cached = detectionCache.get(normalizedCwd);
1612
- if (!cached) {
1613
- cached = { signature: configSignature, entries: new Map() };
1614
- detectionCache.set(normalizedCwd, cached);
1615
- }
1616
- }
1617
- // Detect formatters for this extension (or exact filename, e.g. terragrunt.hcl)
1618
- const matching = ALL_FORMATTERS.filter((f) => f.extensions.includes(ext) || f.filenames?.includes(base));
1619
- const formatterPolicy = getFormatterPolicyForFile(filePath);
1620
- const smartDefaultFormatterName = getSmartDefaultFormatterName(filePath);
1621
- const candidateFormatters = formatterPolicy?.formatterNames?.length
1622
- ? matching.filter((f) => formatterPolicy.formatterNames.includes(f.name))
1623
- : matching;
1624
- let selected;
1625
- /**
1626
- * One entry per candidate this pass actually ASKED, in the order it asked
1627
- * them (#1539). Candidates eliminated without a probe — an explicit-config
1628
- * decision is pure filesystem — contribute nothing, which is the point: a
1629
- * decision that never probed cannot have been degraded by a probe.
1630
- */
1631
- const candidateOutcomes = [];
1632
- if (formatterPolicy) {
1633
- const explicitlyConfigured = candidateFormatters.filter((formatter) => hasExplicitFormatterConfig(formatter.name, cwd, ext));
1634
- if (explicitlyConfigured.length > 0) {
1635
- // A formatter with explicit project config was found — use it.
1636
- // Prefer the policy's defaultFormatter only if it has explicit config,
1637
- // otherwise pick the first explicitly-configured formatter.
1638
- selected = formatterPolicy.defaultFormatter
1639
- ? (explicitlyConfigured.find((f) => f.name === formatterPolicy.defaultFormatter) ?? explicitlyConfigured[0])
1640
- : explicitlyConfigured[0];
1641
- }
1642
- else if (smartDefaultFormatterName) {
1643
- // Reached only when explicitlyConfigured is empty, so no candidate
1644
- // has explicit config. Safe to activate the smart-default.
1645
- const smartDefaultFormatter = candidateFormatters.find((f) => f.name === smartDefaultFormatterName);
1646
- if (smartDefaultFormatter) {
1647
- const autoInstallToolId = getAutoInstallToolIdForFormatter(smartDefaultFormatter.name);
1648
- if (autoInstallToolId) {
1649
- selected = smartDefaultFormatter;
1650
- }
1651
- else {
1652
- const { detected, error, outcome } = await detectCandidate(smartDefaultFormatter, cwd);
1653
- candidateOutcomes.push(outcome);
1654
- // This branch never caught a throwing detection, and still
1655
- // does not: `detectCandidate` catches only so the probes survive.
1656
- if (error !== undefined)
1657
- throw error;
1658
- if (detected)
1659
- selected = smartDefaultFormatter;
1660
- }
1661
- }
1662
- }
1663
- }
1664
- else {
1665
- for (const formatter of candidateFormatters) {
1666
- const { detected, error, outcome } = await detectCandidate(formatter, cwd);
1667
- candidateOutcomes.push(outcome);
1668
- if (error !== undefined) {
1669
- // pi-lens-ignore: missing-error-propagation — optional formatter detection, skip on failure
1670
- logExtension({
1671
- subsystem: "format",
1672
- message: `Detection failed for ${formatter.name}: ${error instanceof Error ? error.message : String(error)}`,
1673
- metadata: { formatter: formatter.name, cwd },
1674
- });
1675
- continue;
1676
- }
1677
- if (detected) {
1678
- selected = formatter;
1679
- break;
1680
- }
1681
- }
1682
- }
1683
- const enabled = selected ? [selected] : [];
1684
- // The #925/#1467 `wouldPoisonCache` shape: an empty result produced while a
1685
- // PATH probe was timing out is not a finding about this project. Caching it
1686
- // would survive until a config file's mtime or size changed, so leave the
1687
- // cache untouched and let the next turn re-detect.
1688
- // Which of THIS file's candidates are currently un-answered? Read off the
1689
- // binaries the candidates' own `detect()`s actually probed (#1539), so a
1690
- // stalled `which rustfmt` cannot stop a shell or Python detection from
1691
- // caching (#1495 review) AND a leftover transient verdict for a binary this
1692
- // pass never consulted cannot be blamed on this decision.
1693
- //
1694
- // This replaces the `command[0]` approximation, which missed both shapes of
1695
- // extra probe: a co-equal ALTERNATIVE (`pwsh` ?? `powershell`) and — the
1696
- // case the old comment wrongly called harmless — an install FALLBACK reached
1697
- // after the primary answered a GENUINE absence. `rustfmt` missing plus a
1698
- // stalled `which rustup` skips the lazy install, and nothing named `rustfmt`
1699
- // is transient, so the empty result used to cache for the session.
1700
- //
1701
- // Residual: the record covers this module's own `which()` only. A detection
1702
- // that reaches PATH some other way is invisible to it — ktlint's `detect()`
1703
- // falls back to the installer's `getToolPath("ktlint")`, which has its own
1704
- // probe and its own 24-hour cache. Widening the record to those is deferred to
1705
- // the installer-side follow-up rather than bolted on here.
1706
- const stalledCandidateCommands = [
1707
- ...new Set(candidateOutcomes.flatMap((outcome) => outcome.stalledCommands)),
1708
- ];
1709
- const poisonedByTransientProbe = enabled.length === 0 && stalledCandidateCommands.length > 0;
1710
- // A NON-empty result the poison guard cannot see (#1539): the winner won only
1711
- // because a candidate ahead of it was never asked. `candidateOutcomes` is in
1712
- // ask order and the loop stops at the winner, so every entry before the
1713
- // winner's is a candidate that lost — and an `unreachable` one did not lose
1714
- // on the merits. Caching this would hand the session to the runner-up until
1715
- // a config file's mtime or size changed.
1716
- const winnerIndex = selected
1717
- ? candidateOutcomes.findIndex((outcome) => outcome.name === selected.name)
1718
- : -1;
1719
- const unreachablePreferred = candidateOutcomes
1720
- .slice(0, winnerIndex === -1 ? 0 : winnerIndex)
1721
- .filter((outcome) => outcome.verdict === "unreachable")
1722
- .map((outcome) => outcome.name);
1723
- const degradedSelection = unreachablePreferred.length > 0;
1724
- let selectionReason;
1725
- if (poisonedByTransientProbe) {
1726
- selectionReason = "probe-timeout";
1727
- }
1728
- else if (degradedSelection) {
1729
- // The reason now describes how the winner WON, not just what the config
1730
- // looks like. "explicit-config" on a selection whose preferred candidate
1731
- // was never reachable was the most misleading record in this seam.
1732
- selectionReason = "preferred-unreachable";
1733
- }
1734
- else if (!selected) {
1735
- selectionReason = "none";
1736
- }
1737
- else if (!formatterPolicy) {
1738
- selectionReason = "detect";
1739
- }
1740
- else {
1741
- selectionReason = candidateFormatters.some((f) => hasExplicitFormatterConfig(f.name, cwd, ext))
1742
- ? "explicit-config"
1743
- : "smart-default";
1744
- }
1745
- const provisional = poisonedByTransientProbe || degradedSelection;
1746
- logLatency({
1747
- type: "phase",
1748
- phase: "formatter_selected",
1749
- filePath: filePath,
1750
- durationMs: 0,
1751
- metadata: {
1752
- formatter: selected?.name ?? null,
1753
- reason: selectionReason,
1754
- outcome: "miss",
1755
- cwd,
1756
- ...(provisional && {
1757
- cached: false,
1758
- stalledProbes: stalledCandidateCommands,
1759
- }),
1760
- ...(degradedSelection && { unreachablePreferred }),
1761
- },
1762
- });
1763
- // Provisional either way: not cached, so the next pass re-detects once the
1764
- // cooldown expires and the preferred formatter's recovery takes effect
1765
- // without waiting on a config-file edit.
1766
- if (provisional)
1767
- return enabled;
1768
- // Store the list of enabled formatter names in cache
1769
- const enabledNames = enabled.map((f) => f.name);
1770
- // An invalidation may have removed or replaced this object while detection
1771
- // awaited a tool probe. Never repopulate the old generation.
1772
- if (detectionCache.get(normalizedCwd) === cached) {
1773
- cached.entries.set(cacheKey, enabledNames);
1774
- }
1775
- return enabled;
1776
- }
1777
- /**
1778
- * Re-arm formatter availability: drop both the per-cwd selection cache and the
1779
- * PATH latches.
1780
- *
1781
- * #1895: this is the session-boundary reset. Both halves are required. The
1782
- * latches decide whether `which` runs at all, but `detectionCache` short-
1783
- * circuits ahead of them — a same-cwd lookup returns the previous verdict's
1784
- * formatter names without reaching a probe. Clearing only the latches
1785
- * therefore re-arms every directory EXCEPT the one the user is working in.
1786
- */
1787
- export function clearFormatterCache() {
1788
- formatterCacheGeneration.bump();
1789
- formatterSignatureFlights.clear();
1790
- detectionCache.clear();
1791
- resetWhichLatches();
1792
- }
1793
- const formatterConfigBasenames = new Set(FORMATTER_CONFIG_FILES.map((fileName) => fileName.toLowerCase()));
1794
- /** Invalidate selection when the write-result seam reports a config path. */
1795
- export function invalidateFormatterCacheForPath(filePath) {
1796
- const basename = filePath.includes("\\")
1797
- ? path.win32.basename(filePath)
1798
- : path.basename(filePath);
1799
- if (!formatterConfigBasenames.has(basename.toLowerCase()))
1800
- return;
1801
- clearFormatterCache();
1802
- }
1803
- /**
1804
- * Test-only internals access for the session-state registry probe (#1895):
1805
- * the conformance suite must arm all four pieces of state this reset claims
1806
- * to cover and prove none survives.
1807
- */
1808
- export function _getFormatterResetStateForTests() {
1809
- return {
1810
- whichLatchByCommand,
1811
- whichTransientCommands,
1812
- cooldownRecordedForRetryAtMs,
1813
- detectionCache,
1814
- formatterSignatureFlights,
1815
- };
1816
- }
1817
- export function clearFormatterRuntimeState() {
1818
- clearFormatterCache();
1819
- // NO `resetLazyInstallAttempts()` here (#1537 review F1). This function runs
1820
- // from `resetFormatService()`, which `handleTurnEnd` calls every turn — so
1821
- // clearing the lazy-install hold here made "held for the session" mean "held
1822
- // for a turn", and a failing install re-ran every turn. The hold's only reset
1823
- // is `session_start`'s block in runtime-session.ts.
1824
- }
1825
- const BOX_DRAWING_GLOBAL = /[\u2500-\u257F]/g;
1826
- /** Blank, or a rule made of box-drawing characters and spacing alone. */
1827
- const DECORATION_RULE = /^[\u2500-\u257F\s]*$/;
1828
- /** The renderer's section heading: one word, then box-drawing to end of line. */
1829
- const DECORATION_HEADING = /^\S+[ \t]+[\u2500-\u257F][\u2500-\u257F\s]*$/;
1830
- /**
1831
- * First line of `text` that actually carries a diagnostic.
1832
- *
1833
- * #1337 made nonzero exits user-visible, which put whatever this returns in
1834
- * front of the agent (`clients/pipeline.ts`) and in the end-of-turn summary
1835
- * (`clients/runtime-agent-end.ts`) — so "first line of stderr" is no longer
1836
- * good enough. Biome opens stderr with a decorated section rule
1837
- * (`format ━━━━━━━━━━━`), which as an error message is pure noise.
1838
- *
1839
- * Skips blank lines, pure box-drawing rules, and short banner headings; strips
1840
- * ANSI colour so the message stays readable in a plain-text surface.
1841
- */
1842
- export function firstDiagnosticLine(text) {
1843
- for (const raw of (text ?? "").split("\n")) {
1844
- const line = stripAnsi(raw).trimEnd();
1845
- if (isDecorativeLine(line))
1846
- continue;
1847
- return line.replace(BOX_DRAWING_GLOBAL, "").trim().slice(0, 300);
1848
- }
1849
- return undefined;
1850
- }
1851
- /**
1852
- * The two decoration SHAPES a renderer emits, and nothing else: a rule made of
1853
- * box-drawing characters alone (blank lines included), and a `<word> ━━━━`
1854
- * section heading whose remainder after the word is box-drawing only.
1855
- *
1856
- * The single normalization both readers above and below share, so the bounded
1857
- * tail cannot surface what the first-line reader already rejects (#3312 review
1858
- * R3-4: the tail put biome's `format ━━━━` banner back in front of the strict
1859
- * #1337 seam). Matching SHAPES rather than "contains a box character and is
1860
- * short" is #3312 review R4-1: the length heuristic deleted
1861
- * `━ traceback source excerpt`, a diagnostic whose own text opens with a box
1862
- * character. A line carrying any non-decorative text is never decoration.
1863
- */
1864
- function isDecorativeLine(line) {
1865
- return DECORATION_RULE.test(line) || DECORATION_HEADING.test(line);
1866
- }
1867
- const FORMATTER_ERROR_TAIL_LINES = 20;
1868
- /**
1869
- * Preserve a bounded tail of a formatter's diagnostic output.
1870
- *
1871
- * A traceback's first line is not actionable on its own (#3312). Keep the
1872
- * final lines, which include the exception and its message, while bounding
1873
- * the model-facing error when a formatter emits an unexpectedly large log.
1874
- *
1875
- * Decorative lines are dropped FIRST, by the same predicate `firstDiagnosticLine`
1876
- * uses: a tail that kept them would hand the strict #1337 seam the banner this
1877
- * module already decided is not a diagnostic, and every banner line it kept
1878
- * would evict a real traceback line from the bound.
1879
- */
1880
- export function diagnosticTail(text, maxLines = FORMATTER_ERROR_TAIL_LINES) {
1881
- const lines = [];
1882
- for (const raw of (text ?? "").split("\n")) {
1883
- const line = stripAnsi(raw).trimEnd();
1884
- if (isDecorativeLine(line))
1885
- continue;
1886
- lines.push(line.slice(0, 300));
1887
- }
1888
- if (lines.length === 0)
1889
- return undefined;
1890
- return lines.slice(-maxLines).join("\n");
1891
- }
1892
- /**
1893
- * Resolve a formatter command without allowing the static command fallback to
1894
- * bypass a resolver's style-preservation refusal (#1345). `null` means the
1895
- * primary command is unavailable; the explicit sentinel means formatting is
1896
- * forbidden for this file and must be returned before the static command is
1897
- * materialized.
1898
- */
1899
- async function resolveFormatterCommand(formatter, absolutePath, cwd) {
1900
- const agreement = establishToolAgreement(formatter.name, cwd);
1901
- if (agreement.decision === "decline") {
1902
- recordDegradationOnce({
1903
- kind: "formatter-agreement-unavailable",
1904
- subject: agreement.subject,
1905
- reason: agreement.reason,
1906
- });
1907
- return FORMATTER_AGREEMENT_UNAVAILABLE;
1908
- }
1909
- const resolved = formatter.resolveCommand
1910
- ? await formatter.resolveCommand(absolutePath, cwd)
1911
- : null;
1912
- if (resolved === SKIP_FORMATTING)
1913
- return SKIP_FORMATTING;
1914
- // The resolver proved the executable absent — DO NOT fall through to the
1915
- // static bare command, which is the same binary it just probed and missing
1916
- // (#2413). The caller turns this into a typed `unavailable` outcome, never a
1917
- // spawn and never a failure.
1918
- if (resolved === FORMATTER_UNAVAILABLE)
1919
- return FORMATTER_UNAVAILABLE;
1920
- if (resolved !== null)
1921
- return resolved;
1922
- const fallback = formatter.command.map((c) => c.replace("$FILE", absolutePath));
1923
- // Trust gate on the install-capable static fallback (#1334 S5): npx can
1924
- // DOWNLOAD packages, so an untrusted project treats the fallback as
1925
- // unavailable -- a skip, not a formatter failure; may converge next turn.
1926
- if (fallback[0] === "npx" &&
1927
- !assertInstallAllowed(`formatter npx fallback: ${formatter.name}`)) {
1928
- // No second ledger entry here (#1366 review): assertInstallAllowed just
1929
- // recorded the trust-refusal with this formatter's context — recording
1930
- // formatter-skip too would count one user-visible degradation twice.
1931
- return SKIP_FORMATTING;
1932
- }
1933
- return fallback;
1934
- }
1935
- export async function formatFile(filePath, formatter) {
1936
- try {
1937
- const absolutePath = path.resolve(filePath);
1938
- const cwd = path.dirname(absolutePath);
1939
- const formatterCwd = resolveToolCwd("formatter", formatter.name, absolutePath, { cwd: path.parse(absolutePath).root }).cwd;
1940
- const contentBefore = await fs.readFile(absolutePath, "utf-8");
1941
- // Resolve command: prefer local (venv/vendor/node_modules) over global.
1942
- // The shared seam must honor SKIP_FORMATTING before selecting the static
1943
- // command, including its npx fallback (#1345).
1944
- const cmd = await resolveFormatterCommand(formatter, absolutePath, cwd);
1945
- if (cmd === SKIP_FORMATTING) {
1946
- // Style-preserving refusal (#1144): no repo config and no detectable
1947
- // indentation to pin — formatting would impose the tool's stock style.
1948
- return { success: true, changed: false, outcome: "skipped" };
1949
- }
1950
- if (cmd === FORMATTER_UNAVAILABLE) {
1951
- // The resolver proved the executable absent (#2413): the oxfmt ENOENT
1952
- // trap and its siblings. This is unavailable infrastructure, not a code
1953
- // failure — success stays true so it is never counted as a failed file
1954
- // or requeued, and the typed outcome keeps it out of the failure bucket.
1955
- return {
1956
- success: true,
1957
- changed: false,
1958
- outcome: "unavailable",
1959
- error: `${formatter.name}: formatter executable not found`,
1960
- };
1961
- }
1962
- if (cmd === FORMATTER_AGREEMENT_UNAVAILABLE) {
1963
- return {
1964
- success: true,
1965
- changed: false,
1966
- outcome: "unavailable",
1967
- error: `${formatter.name}: project tool agreement could not be established`,
1968
- };
1969
- }
1970
- // Run formatter without blocking the event loop.
1971
- const result = await safeSpawnAsync(cmd[0], cmd.slice(1), {
1972
- timeout: 15000,
1973
- cwd: formatterCwd,
1974
- });
1975
- // A resolver that could NOT prove absence (it never probed PATH — e.g.
1976
- // black/sqlfluff, which only look in a venv) legitimately falls back to the
1977
- // bare static command. When that bare command is missing, the spawn boundary
1978
- // reports a typed `tool-not-found` failure — never a nonzero exit. Treat it
1979
- // as unavailable, not a formatting failure, closing the same #2413 class for
1980
- // the formatters whose `null` correctly means "static untried" (no
1981
- // oxfmt-specific string matching — this reads the typed spawn-failure kind).
1982
- if (result.spawnFailure?.kind === "tool-not-found") {
1983
- return {
1984
- success: true,
1985
- changed: false,
1986
- outcome: "unavailable",
1987
- error: result.spawnFailure.message,
1988
- };
1989
- }
1990
- // Strict by default (#1337): only a formatter with a documented
1991
- // benign-nonzero mode (`lenientExitCode`) may exit nonzero and still be
1992
- // read as a successful run. Everything else that exits nonzero never
1993
- // rewrote the file, and reporting {success: true, changed: false} there is
1994
- // indistinguishable from "already formatted" — the #1336 silent no-op.
1995
- const lenientOk = formatter.lenientExitCode !== undefined &&
1996
- result.status !== null &&
1997
- (formatter.lenientStatuses ?? []).includes(result.status);
1998
- if (result.error || (result.status !== 0 && !lenientOk)) {
1999
- return {
2000
- success: false,
2001
- changed: false,
2002
- outcome: "failed",
2003
- error: result.error?.message ||
2004
- diagnosticTail(result.stderr) ||
2005
- // biome, ktlint and `mix format` report on STDOUT; without this
2006
- // their diagnostic is discarded and the user is told only that
2007
- // the tool "exited with status 1".
2008
- diagnosticTail(result.stdout) ||
2009
- `${formatter.name} exited with status ${result.status}`,
2010
- };
2011
- }
2012
- // Check if content changed
2013
- const contentAfter = await fs.readFile(absolutePath, "utf-8");
2014
- const changed = contentBefore !== contentAfter;
2015
- return {
2016
- success: true,
2017
- changed,
2018
- outcome: changed ? "formatted" : "unchanged",
2019
- };
2020
- }
2021
- catch (err) {
2022
- return {
2023
- success: false,
2024
- changed: false,
2025
- outcome: "failed",
2026
- error: err instanceof Error ? err.message : String(err),
2027
- };
2028
- }
2029
- }
2030
- export function listAllFormatters() {
2031
- return ALL_FORMATTERS.map((f) => f.name);
2032
- }