pi-lens 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (671) hide show
  1. package/CHANGELOG.md +1562 -0
  2. package/README.md +157 -24
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -2
  4. package/dist/chunk-22AVIGXS.js +560 -0
  5. package/dist/chunk-27BBHSZC.js +106 -0
  6. package/dist/chunk-2BEMIWEW.js +51 -0
  7. package/dist/chunk-2EECBNC5.js +89 -0
  8. package/dist/chunk-34U7PLGE.js +23 -0
  9. package/dist/chunk-3I5QFT3N.js +181 -0
  10. package/dist/chunk-3MED75HT.js +37 -0
  11. package/dist/chunk-47D2BO4S.js +87 -0
  12. package/dist/chunk-4RERDEXR.js +348 -0
  13. package/dist/chunk-4W2PLC4J.js +27 -0
  14. package/dist/chunk-52H5B5TC.js +202 -0
  15. package/dist/chunk-54B42YRX.js +27 -0
  16. package/dist/chunk-5RXBNSW2.js +266 -0
  17. package/dist/chunk-5THXD6X5.js +201 -0
  18. package/dist/chunk-5W3R6LFT.js +24 -0
  19. package/dist/chunk-5ZEJHV35.js +341 -0
  20. package/dist/chunk-6P62QVEA.js +118 -0
  21. package/dist/chunk-6QIUX4EP.js +459 -0
  22. package/dist/chunk-6VCD4VKZ.js +78 -0
  23. package/dist/chunk-7F72PXY2.js +326 -0
  24. package/dist/chunk-7FBF7NPR.js +998 -0
  25. package/dist/chunk-7U2IDNXN.js +37 -0
  26. package/dist/chunk-ABBOA7UD.js +157 -0
  27. package/dist/chunk-AHDW3GPE.js +50 -0
  28. package/dist/chunk-AJ3QXTRQ.js +756 -0
  29. package/dist/chunk-AJVLWNFN.js +1610 -0
  30. package/dist/chunk-AJXODZY6.js +112 -0
  31. package/dist/chunk-ARFCXG7S.js +84 -0
  32. package/dist/chunk-AS44CFQ4.js +708 -0
  33. package/dist/chunk-AWE5TMPO.js +38 -0
  34. package/dist/chunk-B3I2UUT4.js +4692 -0
  35. package/dist/chunk-BRJKOBZP.js +331 -0
  36. package/dist/chunk-BW5KFIQC.js +59 -0
  37. package/dist/chunk-C2AUYXQH.js +149 -0
  38. package/dist/chunk-CGGAIP7J.js +29 -0
  39. package/dist/chunk-D6QS3U6M.js +30 -0
  40. package/dist/chunk-DFVMIKMQ.js +349 -0
  41. package/dist/chunk-DIULIWKU.js +169 -0
  42. package/dist/chunk-DQ2NZ7C4.js +2317 -0
  43. package/dist/chunk-E2WFBRHM.js +213 -0
  44. package/dist/chunk-E4MAEDPM.js +50 -0
  45. package/dist/chunk-EOKRQ3SA.js +30 -0
  46. package/dist/chunk-F3AU2TMM.js +2626 -0
  47. package/dist/chunk-F3P7VQ7H.js +170 -0
  48. package/dist/chunk-FC4RP4I2.js +12 -0
  49. package/dist/chunk-FJNKANOS.js +32 -0
  50. package/dist/chunk-GUOGVTCQ.js +146 -0
  51. package/dist/chunk-GVXA6CCE.js +18 -0
  52. package/dist/chunk-HHYAELLJ.js +2334 -0
  53. package/dist/chunk-HMFXILPV.js +83 -0
  54. package/dist/chunk-HVUZXSWK.js +421 -0
  55. package/dist/chunk-HWBZIGMJ.js +62 -0
  56. package/dist/chunk-I3R7GKDR.js +17 -0
  57. package/dist/chunk-I7SPGOWI.js +55 -0
  58. package/dist/chunk-IIXOAGHR.js +38 -0
  59. package/dist/chunk-IVP5KAB7.js +2250 -0
  60. package/dist/chunk-IXEBJSUE.js +22 -0
  61. package/dist/chunk-JOLITGCC.js +27 -0
  62. package/dist/chunk-K6VYLB2U.js +145 -0
  63. package/dist/chunk-LJDODBBZ.js +78 -0
  64. package/dist/chunk-LNAKEO63.js +78 -0
  65. package/dist/chunk-LSMJ6KBE.js +4139 -0
  66. package/dist/chunk-LW6XKDZQ.js +5565 -0
  67. package/dist/chunk-N3YQJI6O.js +899 -0
  68. package/dist/chunk-NABLC7W3.js +148 -0
  69. package/dist/chunk-NKOUFPQF.js +659 -0
  70. package/dist/chunk-NL5GLMFI.js +211 -0
  71. package/dist/chunk-NMABWBZN.js +327 -0
  72. package/dist/chunk-NTHH2J74.js +27 -0
  73. package/dist/chunk-NTUJEGDZ.js +59 -0
  74. package/dist/chunk-NXL4FFQQ.js +47 -0
  75. package/dist/chunk-NXPS2L6U.js +466 -0
  76. package/dist/chunk-O5WILGMI.js +42 -0
  77. package/dist/chunk-O6TQT6RI.js +48 -0
  78. package/dist/chunk-OAXNDZJ7.js +323 -0
  79. package/dist/chunk-ODEEOXPH.js +44 -0
  80. package/dist/chunk-OQGFCZJ4.js +4533 -0
  81. package/dist/chunk-ORTMY4Q4.js +282 -0
  82. package/dist/chunk-OS32RNR2.js +442 -0
  83. package/dist/chunk-PAK6IDME.js +189 -0
  84. package/dist/chunk-PWFHBTSF.js +406 -0
  85. package/dist/chunk-Q5U6FMDI.js +2352 -0
  86. package/dist/chunk-QOZXJ4NA.js +3364 -0
  87. package/dist/chunk-QYF5S6P6.js +45 -0
  88. package/dist/chunk-RIISMHKN.js +36 -0
  89. package/dist/chunk-RPD4ZSB7.js +25 -0
  90. package/dist/chunk-RPD5K5NC.js +79 -0
  91. package/dist/chunk-RUOG5D2C.js +262 -0
  92. package/dist/chunk-T4ERT73Y.js +599 -0
  93. package/dist/chunk-T6D4Q4AQ.js +54 -0
  94. package/dist/chunk-TGPXLY2J.js +391 -0
  95. package/dist/chunk-TLGFBM3F.js +12116 -0
  96. package/dist/chunk-TO6WL3GM.js +88 -0
  97. package/dist/chunk-TT22N74J.js +517 -0
  98. package/dist/chunk-TVN5N6GY.js +21216 -0
  99. package/dist/chunk-U3VGR4VU.js +308 -0
  100. package/dist/chunk-UK3CMEAL.js +1250 -0
  101. package/dist/chunk-UOPVJQ4N.js +103 -0
  102. package/dist/chunk-UXVNL3D7.js +23 -0
  103. package/dist/chunk-UY752QSN.js +45 -0
  104. package/dist/chunk-V5PXFSZK.js +188 -0
  105. package/dist/chunk-V7R425XG.js +398 -0
  106. package/dist/chunk-VCSRSYLF.js +81 -0
  107. package/dist/chunk-VD4JQCQC.js +162 -0
  108. package/dist/chunk-VDBPVIKD.js +62 -0
  109. package/dist/chunk-VHQLPIRJ.js +114 -0
  110. package/dist/chunk-VN2AXLEX.js +5414 -0
  111. package/dist/chunk-VNMT7C64.js +478 -0
  112. package/dist/chunk-WONERJTP.js +309 -0
  113. package/dist/chunk-XECLWQZN.js +85 -0
  114. package/dist/chunk-XIMYZ3JA.js +27 -0
  115. package/dist/chunk-XK4CIILI.js +144 -0
  116. package/dist/chunk-Y4T27COV.js +562 -0
  117. package/dist/chunk-YGVYKNSN.js +1860 -0
  118. package/dist/chunk-YLF3PPT5.js +308 -0
  119. package/dist/chunk-YSNFMJKN.js +14 -0
  120. package/dist/chunk-Z3CCVDUB.js +76 -0
  121. package/dist/chunk-Z54ZSOBZ.js +61 -0
  122. package/dist/chunk-ZAWW3AF6.js +97 -0
  123. package/dist/chunk-ZMOL6RQC.js +167 -0
  124. package/dist/index.js +19039 -8956
  125. package/dist/mcp/analyze-cli.js +186 -228
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22592 -1564
  128. package/dist/mcp/worker.js +97 -38
  129. package/dist/probes/installer.js +135 -0
  130. package/dist/workers/project-snapshot-persist-worker.js +22 -0
  131. package/dist/workers/review-graph-persist-worker.js +15 -0
  132. package/docs/adr/0001-stale-advisory-live-arm.md +29 -0
  133. package/docs/adr/0002-workspace-hash-rendezvous.md +30 -0
  134. package/docs/adr/0003-git-guard-latch-writer.md +203 -0
  135. package/docs/adr/0004-disposition-policy-seam.md +38 -0
  136. package/docs/adr/0005-tool-availability-enforcement-seam.md +29 -0
  137. package/docs/adr/0006-derived-state-benchmark-first.md +31 -0
  138. package/docs/adr/0007-end-to-end-witness-per-seam-slice.md +38 -0
  139. package/docs/adr/0008-turn-end-lane-interface.md +165 -0
  140. package/docs/adr/0009-reported-path-attribution.md +114 -0
  141. package/docs/adr/README.md +26 -0
  142. package/docs/agent-guide.md +13 -4
  143. package/docs/agent-tools.md +60 -1
  144. package/docs/ast-grep_rules_catalog.md +6 -4
  145. package/docs/configuration.md +75 -2
  146. package/docs/engineering-principles.md +202 -0
  147. package/docs/environment-variables.md +113 -6
  148. package/docs/features.md +10 -6
  149. package/docs/globalconfig.md +7 -1
  150. package/docs/io-bridge-v2-migration.md +94 -0
  151. package/docs/language-coverage.md +17 -0
  152. package/docs/lsp-capability-matrix.md +131 -32
  153. package/docs/lsp-idle-eviction.md +124 -0
  154. package/docs/mcp.md +21 -5
  155. package/docs/merge-queue-rollout.md +77 -0
  156. package/docs/pi-lens-fixer.md +161 -39
  157. package/docs/pi-lens-investigator.md +77 -20
  158. package/docs/pi-lens-merge-policy.md +694 -0
  159. package/docs/pi-lens-monitor.md +73 -7
  160. package/docs/pi-lens-retro.md +6 -4
  161. package/docs/pi-lens-reviewer.md +190 -34
  162. package/docs/pi-lens-subagent.md +247 -37
  163. package/docs/pi-lens-warden.md +18 -7
  164. package/docs/public-api-stability.md +56 -0
  165. package/docs/release-qa-baseline.md +86 -17
  166. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  167. package/docs/servercapabilities.md +66 -52
  168. package/docs/settings.md +13 -3
  169. package/docs/subagent-compat.md +1 -1
  170. package/docs/usage.md +5 -1
  171. package/grammars/tree-sitter-bash.wasm +0 -0
  172. package/grammars/tree-sitter-bash.wasm.json +3 -3
  173. package/package.json +23 -9
  174. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  175. package/rules/ast-grep-rules/rule-tests/no-assert-tuple-test.yml +13 -0
  176. package/rules/ast-grep-rules/rule-tests/no-boolean-in-except-test.yml +26 -0
  177. package/rules/ast-grep-rules/rule-tests/no-http-headers-bracket-access-test.yml +10 -0
  178. package/rules/ast-grep-rules/rule-tests/unchecked-numeric-parse-python-test.yml +18 -0
  179. package/rules/ast-grep-rules/rule-tests/unchecked-throwing-call-python-test.yml +4 -3
  180. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  181. package/rules/ast-grep-rules/rules/go-no-fmt-println.yml +2 -2
  182. package/rules/ast-grep-rules/rules/no-assert-tuple.yml +23 -13
  183. package/rules/ast-grep-rules/rules/no-boolean-in-except.yml +7 -3
  184. package/rules/ast-grep-rules/rules/no-console-except-error-js.yml +2 -2
  185. package/rules/ast-grep-rules/rules/no-console-except-error.yml +2 -2
  186. package/rules/ast-grep-rules/rules/no-http-headers-bracket-access.yml +12 -7
  187. package/rules/ast-grep-rules/rules/unchecked-numeric-parse-python.yml +44 -0
  188. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +5 -3
  189. package/rules/tree-sitter-queries/typescript/console-statement.yml +2 -2
  190. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  191. package/scripts/download-grammars.js +5 -0
  192. package/scripts/grammars.lock.json +6 -1
  193. package/scripts/install-selftest.mjs +38 -35
  194. package/scripts/lib/host-provided-deps.mjs +13 -4
  195. package/scripts/lib/web-tree-sitter-dir.mjs +141 -0
  196. package/dist/clients/actionable-warnings-logger.js +0 -35
  197. package/dist/clients/actionable-warnings.js +0 -1594
  198. package/dist/clients/advisory-provenance.js +0 -457
  199. package/dist/clients/agent-behavior-client.js +0 -147
  200. package/dist/clients/agent-nudge.js +0 -325
  201. package/dist/clients/analysed-root.js +0 -1
  202. package/dist/clients/ast-grep-client.js +0 -731
  203. package/dist/clients/ast-grep-rule-manager.js +0 -146
  204. package/dist/clients/ast-grep-tool-logger.js +0 -118
  205. package/dist/clients/ast-grep-types.js +0 -10
  206. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  207. package/dist/clients/atomic-write-staging.js +0 -75
  208. package/dist/clients/atomic-write.js +0 -189
  209. package/dist/clients/availability-probe-flight.js +0 -12
  210. package/dist/clients/bash-file-access.js +0 -1056
  211. package/dist/clients/biome-client.js +0 -357
  212. package/dist/clients/blocker-freshness.js +0 -837
  213. package/dist/clients/blocker-past-eof.js +0 -129
  214. package/dist/clients/bootstrap.js +0 -569
  215. package/dist/clients/bounded-cache.js +0 -190
  216. package/dist/clients/bounded-pid-file-lock.js +0 -202
  217. package/dist/clients/bounded-telemetry.js +0 -295
  218. package/dist/clients/build-identity.js +0 -95
  219. package/dist/clients/bundled-resource-health.js +0 -113
  220. package/dist/clients/bus-events-logger.js +0 -105
  221. package/dist/clients/bus-publish.js +0 -171
  222. package/dist/clients/cache/rule-cache.js +0 -190
  223. package/dist/clients/cache-manager.js +0 -465
  224. package/dist/clients/cache-observability.js +0 -1313
  225. package/dist/clients/call-graph.js +0 -852
  226. package/dist/clients/cargo-manifest.js +0 -422
  227. package/dist/clients/cascade-budget.js +0 -196
  228. package/dist/clients/cascade-format.js +0 -315
  229. package/dist/clients/cascade-logger.js +0 -31
  230. package/dist/clients/cascade-types.js +0 -9
  231. package/dist/clients/child-unref.js +0 -115
  232. package/dist/clients/code-quality-warnings.js +0 -286
  233. package/dist/clients/codebase-model.js +0 -173
  234. package/dist/clients/collateral-test-role.js +0 -40
  235. package/dist/clients/complexity-client.js +0 -462
  236. package/dist/clients/config-core/deny.js +0 -221
  237. package/dist/clients/config-core/index.js +0 -50
  238. package/dist/clients/config-core/merge.js +0 -357
  239. package/dist/clients/config-core/normalize.js +0 -340
  240. package/dist/clients/config-core/process-spec.js +0 -248
  241. package/dist/clients/config-core/provenance.js +0 -164
  242. package/dist/clients/config-core/records.js +0 -218
  243. package/dist/clients/config-core/resolve.js +0 -125
  244. package/dist/clients/config-core/safe-object.js +0 -78
  245. package/dist/clients/config-core/schema.js +0 -165
  246. package/dist/clients/config-diagnostic-codes.js +0 -283
  247. package/dist/clients/config-locations.js +0 -160
  248. package/dist/clients/config-resolve.js +0 -792
  249. package/dist/clients/config-schema.js +0 -251
  250. package/dist/clients/config-warn.js +0 -407
  251. package/dist/clients/console-guard-install.js +0 -26
  252. package/dist/clients/cooperative-budget.js +0 -42
  253. package/dist/clients/dead-code-client.js +0 -510
  254. package/dist/clients/dead-code-logger.js +0 -30
  255. package/dist/clients/deadline-utils.js +0 -280
  256. package/dist/clients/debug-handles.js +0 -211
  257. package/dist/clients/debug-heap.js +0 -174
  258. package/dist/clients/deferred-lsp-work.js +0 -106
  259. package/dist/clients/degradation-ledger.js +0 -396
  260. package/dist/clients/demoted-finding-render.js +0 -169
  261. package/dist/clients/dependency-checker.js +0 -862
  262. package/dist/clients/deps/ast-grep-napi.js +0 -22
  263. package/dist/clients/deps/js-yaml.js +0 -9
  264. package/dist/clients/deps/minimatch.js +0 -4
  265. package/dist/clients/deps/pi-tui.js +0 -14
  266. package/dist/clients/deps/typebox.js +0 -14
  267. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  268. package/dist/clients/deps/web-tree-sitter.js +0 -24
  269. package/dist/clients/diagnostic-dispositions.js +0 -594
  270. package/dist/clients/diagnostic-line-freshness.js +0 -305
  271. package/dist/clients/diagnostic-logger.js +0 -73
  272. package/dist/clients/diagnostic-tracker.js +0 -105
  273. package/dist/clients/diagnostics-publish.js +0 -230
  274. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  275. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  276. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  277. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  278. package/dist/clients/dispatch/dispatcher.js +0 -1269
  279. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  280. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  281. package/dist/clients/dispatch/fact-runner.js +0 -30
  282. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  283. package/dist/clients/dispatch/fact-store.js +0 -377
  284. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  285. package/dist/clients/dispatch/facts/file-content.js +0 -19
  286. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  287. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  288. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  289. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  290. package/dist/clients/dispatch/finding-policy.js +0 -222
  291. package/dist/clients/dispatch/indent-detect.js +0 -411
  292. package/dist/clients/dispatch/inline-suppressions.js +0 -70
  293. package/dist/clients/dispatch/integration.js +0 -2377
  294. package/dist/clients/dispatch/lazy.js +0 -11
  295. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  296. package/dist/clients/dispatch/plan.js +0 -278
  297. package/dist/clients/dispatch/priorities.js +0 -20
  298. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  299. package/dist/clients/dispatch/rule-ignores.js +0 -62
  300. package/dist/clients/dispatch/rule-policy.js +0 -135
  301. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  302. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  303. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  304. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  305. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  306. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  307. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  308. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  309. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  310. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  311. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  312. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  313. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  314. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  315. package/dist/clients/dispatch/runner-context.js +0 -52
  316. package/dist/clients/dispatch/runners/actionlint.js +0 -113
  317. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  318. package/dist/clients/dispatch/runners/biome-check.js +0 -283
  319. package/dist/clients/dispatch/runners/cpp-check.js +0 -236
  320. package/dist/clients/dispatch/runners/credo.js +0 -80
  321. package/dist/clients/dispatch/runners/cue-vet.js +0 -398
  322. package/dist/clients/dispatch/runners/dart-analyze.js +0 -202
  323. package/dist/clients/dispatch/runners/detekt.js +0 -169
  324. package/dist/clients/dispatch/runners/dotnet-build.js +0 -159
  325. package/dist/clients/dispatch/runners/elixir-check.js +0 -210
  326. package/dist/clients/dispatch/runners/eslint.js +0 -154
  327. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  328. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  329. package/dist/clients/dispatch/runners/gleam-check.js +0 -87
  330. package/dist/clients/dispatch/runners/go-vet.js +0 -90
  331. package/dist/clients/dispatch/runners/golangci-lint.js +0 -120
  332. package/dist/clients/dispatch/runners/hadolint.js +0 -79
  333. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  334. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  335. package/dist/clients/dispatch/runners/htmlhint.js +0 -108
  336. package/dist/clients/dispatch/runners/index.js +0 -108
  337. package/dist/clients/dispatch/runners/javac.js +0 -89
  338. package/dist/clients/dispatch/runners/ktlint.js +0 -136
  339. package/dist/clients/dispatch/runners/lsp.js +0 -378
  340. package/dist/clients/dispatch/runners/markdownlint.js +0 -160
  341. package/dist/clients/dispatch/runners/mypy.js +0 -101
  342. package/dist/clients/dispatch/runners/oxlint.js +0 -398
  343. package/dist/clients/dispatch/runners/php-lint.js +0 -62
  344. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  345. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  346. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  347. package/dist/clients/dispatch/runners/pyright.js +0 -169
  348. package/dist/clients/dispatch/runners/rubocop.js +0 -90
  349. package/dist/clients/dispatch/runners/ruff.js +0 -89
  350. package/dist/clients/dispatch/runners/rust-clippy.js +0 -251
  351. package/dist/clients/dispatch/runners/shellcheck.js +0 -169
  352. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  353. package/dist/clients/dispatch/runners/spellcheck.js +0 -121
  354. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  355. package/dist/clients/dispatch/runners/sqlfluff.js +0 -174
  356. package/dist/clients/dispatch/runners/stylelint.js +0 -158
  357. package/dist/clients/dispatch/runners/swiftlint.js +0 -154
  358. package/dist/clients/dispatch/runners/taplo.js +0 -168
  359. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  360. package/dist/clients/dispatch/runners/tflint.js +0 -110
  361. package/dist/clients/dispatch/runners/tree-sitter.js +0 -620
  362. package/dist/clients/dispatch/runners/trivy-config.js +0 -247
  363. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  364. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  365. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -85
  366. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  367. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  368. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  369. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  370. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  371. package/dist/clients/dispatch/runners/utils.js +0 -15
  372. package/dist/clients/dispatch/runners/vale.js +0 -116
  373. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  374. package/dist/clients/dispatch/runners/yamllint.js +0 -85
  375. package/dist/clients/dispatch/runners/zig-check.js +0 -91
  376. package/dist/clients/dispatch/suppress-writer.js +0 -69
  377. package/dist/clients/dispatch/tool-profile.js +0 -30
  378. package/dist/clients/dispatch/types.js +0 -21
  379. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  380. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  381. package/dist/clients/disposition-logger.js +0 -32
  382. package/dist/clients/disposition-publish.js +0 -124
  383. package/dist/clients/durable-store.js +0 -97
  384. package/dist/clients/effective-config.js +0 -413
  385. package/dist/clients/env-utils.js +0 -83
  386. package/dist/clients/error-class.js +0 -23
  387. package/dist/clients/eval-timestamp.js +0 -17
  388. package/dist/clients/event-loop-hold.js +0 -274
  389. package/dist/clients/event-loop-monitor.js +0 -250
  390. package/dist/clients/extension-log.js +0 -504
  391. package/dist/clients/extension-mode.js +0 -73
  392. package/dist/clients/feature-hints.js +0 -103
  393. package/dist/clients/file-kinds.js +0 -412
  394. package/dist/clients/file-role.js +0 -134
  395. package/dist/clients/file-time.js +0 -160
  396. package/dist/clients/file-utils.js +0 -1032
  397. package/dist/clients/finding-delivery-gate.js +0 -463
  398. package/dist/clients/finding-identity.js +0 -88
  399. package/dist/clients/fix-worklog.js +0 -103
  400. package/dist/clients/format-events-publish.js +0 -343
  401. package/dist/clients/format-service.js +0 -222
  402. package/dist/clients/formatters-lazy.js +0 -9
  403. package/dist/clients/formatters.js +0 -1994
  404. package/dist/clients/freshness-cadence.js +0 -17
  405. package/dist/clients/freshness.js +0 -38
  406. package/dist/clients/generated-artifacts.js +0 -394
  407. package/dist/clients/generation-guard.js +0 -215
  408. package/dist/clients/git-guard.js +0 -923
  409. package/dist/clients/git-tracked-ignore.js +0 -243
  410. package/dist/clients/gitleaks-client.js +0 -509
  411. package/dist/clients/go-client.js +0 -104
  412. package/dist/clients/govulncheck-client.js +0 -605
  413. package/dist/clients/gradle-ktfmt-style.js +0 -252
  414. package/dist/clients/grammar-source.js +0 -465
  415. package/dist/clients/gzip-stage-write.js +0 -135
  416. package/dist/clients/hashline-anchor.js +0 -424
  417. package/dist/clients/hook-budgets.js +0 -76
  418. package/dist/clients/host-edit-normalize.js +0 -140
  419. package/dist/clients/host-ports.js +0 -39
  420. package/dist/clients/indent-retarget.js +0 -154
  421. package/dist/clients/install-diagnostics.js +0 -143
  422. package/dist/clients/installer/index.js +0 -5167
  423. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  424. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  425. package/dist/clients/instance-reaper-state.js +0 -7
  426. package/dist/clients/instance-reaper.js +0 -1137
  427. package/dist/clients/instance-registry-lock.js +0 -145
  428. package/dist/clients/instance-registry.js +0 -773
  429. package/dist/clients/jscpd-client.js +0 -370
  430. package/dist/clients/json-cache-read.js +0 -65
  431. package/dist/clients/knip-client.js +0 -788
  432. package/dist/clients/language-policy.js +0 -252
  433. package/dist/clients/language-profile.js +0 -361
  434. package/dist/clients/language-registry.js +0 -603
  435. package/dist/clients/latency-logger.js +0 -719
  436. package/dist/clients/lazy-import.js +0 -21
  437. package/dist/clients/ledger-bounds.js +0 -54
  438. package/dist/clients/lens-config.js +0 -346
  439. package/dist/clients/lens-engine.js +0 -358
  440. package/dist/clients/lens-events.js +0 -125
  441. package/dist/clients/lens-flag-registry.js +0 -467
  442. package/dist/clients/lens-map.js +0 -1156
  443. package/dist/clients/live-bus-emitter.js +0 -74
  444. package/dist/clients/log-cleanup.js +0 -222
  445. package/dist/clients/lsp/aggregation.js +0 -178
  446. package/dist/clients/lsp/cascade-tier.js +0 -421
  447. package/dist/clients/lsp/client.js +0 -4465
  448. package/dist/clients/lsp/config.js +0 -689
  449. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  450. package/dist/clients/lsp/document-drift.js +0 -467
  451. package/dist/clients/lsp/edits.js +0 -1219
  452. package/dist/clients/lsp/index.js +0 -8305
  453. package/dist/clients/lsp/inferred-project.js +0 -216
  454. package/dist/clients/lsp/jvm-runtime.js +0 -298
  455. package/dist/clients/lsp/language.js +0 -63
  456. package/dist/clients/lsp/launch.js +0 -725
  457. package/dist/clients/lsp/lombok.js +0 -158
  458. package/dist/clients/lsp/path-utils.js +0 -5
  459. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  460. package/dist/clients/lsp/position-encoding.js +0 -61
  461. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  462. package/dist/clients/lsp/server.js +0 -3209
  463. package/dist/clients/lsp/session-roots.js +0 -192
  464. package/dist/clients/lsp/spawn-history.js +0 -17
  465. package/dist/clients/lsp/sync-kind.js +0 -42
  466. package/dist/clients/lsp/tsserver-sync.js +0 -332
  467. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  468. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  469. package/dist/clients/lsp/wait-policy/index.js +0 -10
  470. package/dist/clients/lsp/wait-policy/strategies.js +0 -303
  471. package/dist/clients/lsp/watch-queue.js +0 -61
  472. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  473. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  474. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  475. package/dist/clients/lsp-budget.js +0 -186
  476. package/dist/clients/lsp-document-symbols.js +0 -110
  477. package/dist/clients/lsp-lazy.js +0 -9
  478. package/dist/clients/lsp-mutation.js +0 -396
  479. package/dist/clients/lsp-status.js +0 -59
  480. package/dist/clients/map-with-concurrency.js +0 -40
  481. package/dist/clients/mcp/analyze.js +0 -420
  482. package/dist/clients/mcp/host-shim.js +0 -31
  483. package/dist/clients/mcp/ipc.js +0 -335
  484. package/dist/clients/mcp/review.js +0 -151
  485. package/dist/clients/mcp/session.js +0 -412
  486. package/dist/clients/memory-sampler.js +0 -208
  487. package/dist/clients/message-end-attribution.js +0 -30
  488. package/dist/clients/metrics-client.js +0 -107
  489. package/dist/clients/metrics-history.js +0 -327
  490. package/dist/clients/middle-man-analysis.js +0 -263
  491. package/dist/clients/model-provider.js +0 -53
  492. package/dist/clients/module-report-lsp.js +0 -189
  493. package/dist/clients/module-report.js +0 -2150
  494. package/dist/clients/mutating-tool.js +0 -651
  495. package/dist/clients/mutation-attribution.js +0 -368
  496. package/dist/clients/mutation-bridge.js +0 -240
  497. package/dist/clients/ndjson-logger.js +0 -734
  498. package/dist/clients/observed-mutation-sources.js +0 -101
  499. package/dist/clients/observed-mutation.js +0 -1253
  500. package/dist/clients/oldtext-autopatch.js +0 -128
  501. package/dist/clients/opaque-mutation-scan.js +0 -496
  502. package/dist/clients/opengrep-client.js +0 -328
  503. package/dist/clients/opengrep-config.js +0 -76
  504. package/dist/clients/package-manager.js +0 -607
  505. package/dist/clients/package-root.js +0 -38
  506. package/dist/clients/partial-edit-apply.js +0 -439
  507. package/dist/clients/path-attribution-telemetry.js +0 -43
  508. package/dist/clients/path-keyed-map.js +0 -108
  509. package/dist/clients/path-utils.js +0 -1244
  510. package/dist/clients/performance-report.js +0 -331
  511. package/dist/clients/persist-debounce.js +0 -70
  512. package/dist/clients/persistent-reverify.js +0 -271
  513. package/dist/clients/php-cs-fixer-config.js +0 -119
  514. package/dist/clients/pipeline.js +0 -1425
  515. package/dist/clients/post-autofix-notice.js +0 -37
  516. package/dist/clients/print-mode.js +0 -21
  517. package/dist/clients/probe-home-state.js +0 -228
  518. package/dist/clients/process-bridge.js +0 -66
  519. package/dist/clients/process-singletons.js +0 -210
  520. package/dist/clients/process-snapshot.js +0 -68
  521. package/dist/clients/project-changes.js +0 -181
  522. package/dist/clients/project-conventions.js +0 -171
  523. package/dist/clients/project-diagnostics/cache.js +0 -137
  524. package/dist/clients/project-diagnostics/extractors.js +0 -109
  525. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -650
  526. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  527. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  528. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  529. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  530. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  531. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  532. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  533. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  534. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  535. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  536. package/dist/clients/project-diagnostics/scanner.js +0 -520
  537. package/dist/clients/project-diagnostics/types.js +0 -1
  538. package/dist/clients/project-lens-config.js +0 -799
  539. package/dist/clients/project-report.js +0 -701
  540. package/dist/clients/project-scale.js +0 -237
  541. package/dist/clients/project-scan-policy.js +0 -11
  542. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  543. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  544. package/dist/clients/project-snapshot.js +0 -1653
  545. package/dist/clients/project-trust.js +0 -181
  546. package/dist/clients/python-environment.js +0 -217
  547. package/dist/clients/python-provenance.js +0 -639
  548. package/dist/clients/quiet-window-config.js +0 -47
  549. package/dist/clients/quiet-window.js +0 -236
  550. package/dist/clients/read-bridge.js +0 -139
  551. package/dist/clients/read-expansion.js +0 -266
  552. package/dist/clients/read-guard-logger.js +0 -251
  553. package/dist/clients/read-guard-tool-lines.js +0 -1306
  554. package/dist/clients/read-guard.js +0 -1561
  555. package/dist/clients/recent-touches.js +0 -240
  556. package/dist/clients/redact/secrets.js +0 -449
  557. package/dist/clients/resource-sampler.js +0 -628
  558. package/dist/clients/reverse-deps.js +0 -270
  559. package/dist/clients/review-graph/builder.js +0 -5118
  560. package/dist/clients/review-graph/format.js +0 -33
  561. package/dist/clients/review-graph/git-identity.js +0 -157
  562. package/dist/clients/review-graph/import-resolvers.js +0 -481
  563. package/dist/clients/review-graph/persist-worker.js +0 -10
  564. package/dist/clients/review-graph/query.js +0 -244
  565. package/dist/clients/review-graph/revision-drift.js +0 -21
  566. package/dist/clients/review-graph/service.js +0 -96
  567. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  568. package/dist/clients/review-graph/symbol-id.js +0 -103
  569. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  570. package/dist/clients/review-graph/types.js +0 -1
  571. package/dist/clients/review-graph/workspace-modules.js +0 -449
  572. package/dist/clients/review-graph-logger.js +0 -81
  573. package/dist/clients/ruff-client.js +0 -152
  574. package/dist/clients/rules-scanner.js +0 -131
  575. package/dist/clients/run-duration.js +0 -55
  576. package/dist/clients/runtime-agent-end.js +0 -746
  577. package/dist/clients/runtime-config.js +0 -77
  578. package/dist/clients/runtime-context.js +0 -211
  579. package/dist/clients/runtime-coordinator.js +0 -1290
  580. package/dist/clients/runtime-session.js +0 -2485
  581. package/dist/clients/runtime-tool-call.js +0 -1284
  582. package/dist/clients/runtime-tool-result.js +0 -2034
  583. package/dist/clients/runtime-turn.js +0 -3600
  584. package/dist/clients/rust-client.js +0 -97
  585. package/dist/clients/safe-spawn.js +0 -2166
  586. package/dist/clients/sanitize.js +0 -286
  587. package/dist/clients/scan-utils.js +0 -25
  588. package/dist/clients/scratch-tree-policy.js +0 -165
  589. package/dist/clients/search-read-registration.js +0 -94
  590. package/dist/clients/secret-findings.js +0 -114
  591. package/dist/clients/security-scan-client.js +0 -352
  592. package/dist/clients/session-event-guard.js +0 -297
  593. package/dist/clients/session-lifecycle.js +0 -484
  594. package/dist/clients/session-start-observability.js +0 -94
  595. package/dist/clients/session-state-store.js +0 -101
  596. package/dist/clients/session-summary.js +0 -15
  597. package/dist/clients/sessionstart-logger.js +0 -27
  598. package/dist/clients/sg-runner.js +0 -876
  599. package/dist/clients/sgconfig.js +0 -347
  600. package/dist/clients/shared-checkout-guard.js +0 -338
  601. package/dist/clients/single-flight.js +0 -158
  602. package/dist/clients/situational-tool-telemetry.js +0 -108
  603. package/dist/clients/skills-resolver.js +0 -141
  604. package/dist/clients/slow-fs.js +0 -138
  605. package/dist/clients/smells-rollup.js +0 -231
  606. package/dist/clients/source-filter.js +0 -596
  607. package/dist/clients/source-walker.js +0 -211
  608. package/dist/clients/spawn-output-cap.js +0 -37
  609. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  610. package/dist/clients/stale-marker.js +0 -17
  611. package/dist/clients/startup-scan.js +0 -400
  612. package/dist/clients/startup-timing.js +0 -130
  613. package/dist/clients/string-utils.js +0 -33
  614. package/dist/clients/subagent-mode.js +0 -111
  615. package/dist/clients/symbol-containment.js +0 -24
  616. package/dist/clients/symbol-types.js +0 -5
  617. package/dist/clients/test-runner-client.js +0 -2507
  618. package/dist/clients/test-runner-delivery.js +0 -287
  619. package/dist/clients/todo-scanner.js +0 -229
  620. package/dist/clients/tool-agreement.js +0 -423
  621. package/dist/clients/tool-config.js +0 -249
  622. package/dist/clients/tool-cwd.js +0 -270
  623. package/dist/clients/tool-definition.js +0 -47
  624. package/dist/clients/tool-event.js +0 -105
  625. package/dist/clients/tool-policy.js +0 -2755
  626. package/dist/clients/tool-probe.js +0 -53
  627. package/dist/clients/tool-render.js +0 -156
  628. package/dist/clients/tool-set-policy.js +0 -118
  629. package/dist/clients/tree-sitter-cache.js +0 -364
  630. package/dist/clients/tree-sitter-client.js +0 -4134
  631. package/dist/clients/tree-sitter-logger.js +0 -100
  632. package/dist/clients/tree-sitter-navigator.js +0 -269
  633. package/dist/clients/tree-sitter-query-loader.js +0 -569
  634. package/dist/clients/tree-sitter-shared.js +0 -158
  635. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  636. package/dist/clients/trivy-client.js +0 -467
  637. package/dist/clients/tui-fit.js +0 -50
  638. package/dist/clients/turn-context.js +0 -52
  639. package/dist/clients/turn-summary-render.js +0 -72
  640. package/dist/clients/turn-summary.js +0 -132
  641. package/dist/clients/typos-config.js +0 -29
  642. package/dist/clients/user-notify.js +0 -51
  643. package/dist/clients/vanished-instance-marker.js +0 -57
  644. package/dist/clients/warm-attach.js +0 -246
  645. package/dist/clients/widget-state.js +0 -1677
  646. package/dist/clients/word-index-logger.js +0 -49
  647. package/dist/clients/word-index-store.js +0 -459
  648. package/dist/clients/word-index.js +0 -2080
  649. package/dist/clients/workspace-topology.js +0 -389
  650. package/dist/clients/write-ordering-guard.js +0 -65
  651. package/dist/clients/zizmor-config.js +0 -327
  652. package/dist/i18n.js +0 -69
  653. package/dist/mcp/build-staleness.js +0 -124
  654. package/dist/scripts/lib/process-scan.mjs +0 -583
  655. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  656. package/dist/tools/activate-tools.js +0 -87
  657. package/dist/tools/ast-grep-outline.js +0 -137
  658. package/dist/tools/ast-grep-replace.js +0 -244
  659. package/dist/tools/ast-grep-search.js +0 -762
  660. package/dist/tools/effective-config.js +0 -90
  661. package/dist/tools/lens-diagnostic-mark.js +0 -375
  662. package/dist/tools/lens-diagnostics.js +0 -2544
  663. package/dist/tools/lsp-diagnostics.js +0 -1634
  664. package/dist/tools/lsp-navigation.js +0 -1367
  665. package/dist/tools/lsp-structured-output.js +0 -294
  666. package/dist/tools/module-report.js +0 -382
  667. package/dist/tools/project-report.js +0 -86
  668. package/dist/tools/render-compact.js +0 -374
  669. package/dist/tools/scan-progress.js +0 -53
  670. package/dist/tools/shared.js +0 -29
  671. package/dist/tools/symbol-search.js +0 -155
@@ -1,1244 +0,0 @@
1
- /**
2
- * Path utilities for pi-lens
3
- *
4
- * Handles cross-platform path normalization, particularly
5
- * Windows case-insensitivity issues when using paths as Map keys.
6
- *
7
- * Approach (inspired by OpenCode's Filesystem.normalizePath):
8
- * - On Windows: try realpathSync.native() for canonical casing
9
- * - Falls back to lowercase for files that don't exist yet
10
- * - On non-Windows: adopt the on-disk casing of an existing path (#3098 — a
11
- * case-insensitive POSIX filesystem otherwise derives two Map keys for one
12
- * file); a path that does not exist is returned as-is, case-preserving
13
- * - Always convert backslashes to forward slashes for Map key consistency
14
- */
15
- import { existsSync, readFileSync, realpathSync, statSync, } from "node:fs";
16
- import * as os from "node:os";
17
- import * as path from "node:path";
18
- import { win32 } from "node:path";
19
- import { fileURLToPath, pathToFileURL } from "node:url";
20
- import { minimatch } from "./deps/minimatch.js";
21
- /**
22
- * Detect a positively Windows-shaped path, regardless of the host OS.
23
- *
24
- * A backslash anywhere in a path is not enough: it is a legal character in a
25
- * POSIX filename. Only a drive-letter prefix (`X:`), a UNC root (`\\`), or a
26
- * rooted backslash at position zero (`\`) selects Windows parsing.
27
- */
28
- export function isWindowsPath(filePath) {
29
- return /^[A-Za-z]:/.test(filePath) || filePath.startsWith("\\");
30
- }
31
- /**
32
- * Canonical backslash→forward-slash fold — the single sanctioned form of the
33
- * `p.replace(/\\/g, "/")` idiom otherwise hand-rolled across the codebase
34
- * (~138 sites, #1193). PURE separator normalization: it does NOT resolve,
35
- * canonicalize, lowercase, or collapse repeated slashes — reach for
36
- * `normalizeFilePath`/`normalizeMapKey`/`normalizeEphemeralMapKey` when a
37
- * canonical map *key* (case-fold / realpath) is what you need. Consolidating on
38
- * this funnels the scattered transform and makes a shape-2 lint/ast-grep rule
39
- * possible for the first time: today a bare inline `.replace(/\\/g, "/")` is
40
- * byte-identical to the sanctioned use so it can't be ruled (#1158); once
41
- * everything routes through `toPosix`, an *un-migrated* inline `.replace`
42
- * becomes detectable.
43
- */
44
- export function toPosix(filePath) {
45
- return filePath.replace(/\\/g, "/");
46
- }
47
- /** Return whether `filePath` is fully qualified under Windows semantics. */
48
- export function isFullyQualifiedWin32(filePath) {
49
- return win32.isAbsolute(filePath) && win32.parse(filePath).root.length > 1;
50
- }
51
- /** Return whether `filePath` is fully qualified under POSIX semantics. */
52
- export function isFullyQualifiedPosix(filePath) {
53
- return path.posix.isAbsolute(filePath) && !isWindowsPath(filePath);
54
- }
55
- /**
56
- * Return whether `filePath` is fully qualified under the host's semantics.
57
- *
58
- * In particular, `/foo` is rooted-relative under Win32 (ambient-drive
59
- * dependent) but fully qualified under POSIX.
60
- */
61
- export function isFullyQualified(filePath) {
62
- return process.platform === "win32"
63
- ? isFullyQualifiedWin32(filePath)
64
- : isFullyQualifiedPosix(filePath);
65
- }
66
- /**
67
- * Shape-based absolute check (mirrors `toProjectRelativePath`'s idiom): a
68
- * Windows-shaped path (drive-letter/UNC) is parsed with `win32.isAbsolute`
69
- * regardless of host OS, since the host-default `path.isAbsolute` returns
70
- * FALSE for a Windows-shaped path on POSIX (#1150 class) and would let a
71
- * cross-platform-persisted relative path slip past this guard on Linux CI.
72
- * MUST run on the RAW `filePath`, before `normalizeMapKey`: on Windows,
73
- * `normalizeMapKey`'s nonexistent-path fallback (`resolveNonExisting`) calls
74
- * `win32.resolve`, which silently makes any relative path absolute against
75
- * `process.cwd()` — checking the normalized value would defeat this guard
76
- * entirely on Windows.
77
- *
78
- * Promoted from a private copy in `clients/review-graph/service.ts` (#2477)
79
- * so `clients/dispatch/dispatcher.ts`'s baseline-key guard (#2489) can share
80
- * the same sanctioned check instead of hand-rolling a second one.
81
- */
82
- export function isAbsoluteFilePath(filePath) {
83
- const p = isWindowsPath(filePath) ? win32 : path;
84
- return p.isAbsolute(filePath);
85
- }
86
- /**
87
- * Split a path into its non-empty segments on EITHER separator (`\` or `/`),
88
- * regardless of the running OS — the shape-safe form of `p.split(path.sep)` /
89
- * an inline `p.split(/[\\/]+/)`, which #1161/#1163 showed must not assume the
90
- * host separator for a possibly-cross-shaped path. Drops empty segments
91
- * (leading slash, drive-root, doubled separators).
92
- */
93
- export function splitPathSegments(filePath) {
94
- return filePath.split(/[\\/]+/).filter(Boolean);
95
- }
96
- /**
97
- * Adopt `canonical`'s CASING for the trailing segments of `held`, stopping at
98
- * the first segment that differs by more than case.
99
- *
100
- * `realpathSync.native` answers a different question than the one the POSIX
101
- * arm of `normalizeFilePath` asks: it resolves symlinks AND reports on-disk
102
- * casing, and we want only the second half. Walking from the tail and halting
103
- * at the first structural divergence separates them with no extra syscall:
104
- *
105
- * held /var/folders/T/x/SUB/a.ts (macOS tmpdir, mis-cased segment)
106
- * canonical /private/var/folders/T/x/sub/a.ts
107
- * result /var/folders/T/x/sub/a.ts (case fixed, symlink prefix kept)
108
- *
109
- * The symlink prefix matters: macOS's `os.tmpdir()` is `/var/folders/...`,
110
- * a symlink into `/private/var`, so a whole-string "is this a case variant"
111
- * test would decline to canonicalize exactly the paths the #1024 regression
112
- * test (and any macOS temp-dir workflow) runs on. Keeping the prefix is also
113
- * what bounds this change: on POSIX, `normalizeFilePath` still never resolves
114
- * a symlink, so a symlinked monorepo package keeps keying under the path the
115
- * caller held (refs #2490 — a cwd fold for its own sake broke every monorepo).
116
- *
117
- * PURE string algebra: this pins what the rewrite DOES, never what the kernel
118
- * reports — and from two strings alone it cannot tell "the same file, spelled
119
- * with different case" from "a different file whose name happens to be a case
120
- * variant". Any symlink whose BASENAME is a case variant of its target's
121
- * basename (`<root>/MyProject` → `work/myproject`, `node_modules/Foo` →
122
- * `../pkgs/foo`) has its name replaced while its own parent is kept, so the
123
- * result names a different place — or no place at all. Measured in #3159
124
- * review round 2 (F1): two different inodes collapsed onto ONE key (the #1024
125
- * defect inverted — false suppression), and a symlinked package keyed under a
126
- * path that does not exist on disk, breaking the #2490 bound above. The caller
127
- * therefore CONFIRMS every rewrite against the filesystem before adopting it
128
- * (see `normalizeFilePath`); do not use this function without that step.
129
- */
130
- function adoptCanonicalCasing(held, canonical) {
131
- const heldParts = held.split("/");
132
- const realParts = canonical.split("/");
133
- let i = heldParts.length - 1;
134
- let j = realParts.length - 1;
135
- let changed = false;
136
- while (i >= 0 && j >= 0) {
137
- const a = heldParts[i];
138
- const b = realParts[j];
139
- if (a !== b) {
140
- if (a.toLowerCase() !== b.toLowerCase())
141
- break;
142
- heldParts[i] = b;
143
- changed = true;
144
- }
145
- i--;
146
- j--;
147
- }
148
- // Identity-preserving when nothing moved — this runs on every POSIX map-key
149
- // derivation and the overwhelmingly common answer is "already canonical".
150
- // NOT a behavioural branch (both arms produce an equal string): it is the
151
- // same allocation guard `normalizeEphemeralMapKey` documents below.
152
- return changed ? heldParts.join("/") : held;
153
- }
154
- /**
155
- * Normalize a file path for consistent Map key usage.
156
- *
157
- * On Windows:
158
- * - If the file exists: uses realpathSync.native() to get the canonical
159
- * filesystem path (actual casing, resolved symlinks)
160
- * - If the file doesn't exist: resolves the path and lowercases
161
- * (needed for new files where we haven't written yet)
162
- *
163
- * On POSIX:
164
- * - Folds `.`, `..` and duplicate separators first (#3184, see below).
165
- * - If the file exists: adopts the on-disk CASING of the trailing segments
166
- * (see `adoptCanonicalCasing`) — no lowercasing, no symlink resolution.
167
- * - If the file doesn't exist: returns the path as-is, case-PRESERVING. On a
168
- * case-sensitive filesystem `SUB/a.ts` and `sub/a.ts` are two different
169
- * files, and nothing may fold one into the other; only the filesystem's own
170
- * answer for a path that EXISTS can tell the two apart, and for a path that
171
- * does not exist there is no such answer to ask for.
172
- *
173
- * Why the POSIX arm folds dot segments (#3184): the casing arm returns the
174
- * caller's own spelling whenever `adoptCanonicalCasing` changes nothing, and
175
- * for `<base>/src/../src/a.ts` it always changes nothing — `realpath` answers
176
- * a string with FEWER segments, which a casing-only rewrite cannot express, so
177
- * it declines and the caller's un-folded spelling came back as the map key.
178
- * Every canonical writer keys through `path.resolve` first (`ctx.filePath` =
179
- * `normalizeMapKey(resolveAgainstAncestors(...))`, `clients/dispatch/
180
- * runner-context.ts:49`), so a consumer that passes an ALREADY-absolute
181
- * agent-typed path straight in (`tools/lens-diagnostic-mark.ts`,
182
- * `clients/mcp/analyze.ts`) derived an orphan key that no reader could reach.
183
- * Folding here is pure string algebra — no cwd, no filesystem — so a relative
184
- * path stays relative (`src/../x` → `x`, `../x` → `../x`, never resolved
185
- * against `process.cwd()`; refs #2490, where a cwd fold broke every monorepo)
186
- * and a symlinked package still keys under the path the caller held. These
187
- * are `path.resolve`'s own TEXTUAL `..` semantics, which is exactly what
188
- * makes a folded reader key equal to the canonical writer's key: where a
189
- * `..` sits right after a symlinked directory, textual folding and the
190
- * kernel disagree, and both sides of every comparison take the textual
191
- * answer because every canonical writer already resolved that way.
192
- *
193
- * Why POSIX canonicalizes casing at all (#3098, the #1024 defect's live half):
194
- * a case-insensitive POSIX filesystem — macOS's default APFS, `nocase` vfat /
195
- * ntfs3 / cifs mounts — makes `SUB/a.ts` and `sub/a.ts` ONE file, so a raw
196
- * mis-cased write (`lens_diagnostic_mark` anchors under `path.resolve(cwd,
197
- * arg)`) and a `normalizeMapKey` read derived two anchors for one file and the
198
- * agent's own disposition mark silently never applied. A case-preserving POSIX
199
- * arm cannot close that: the two keys only become one if the normalizer asks
200
- * the filesystem which name is really on disk.
201
- *
202
- * `realpathSync.native` is `realpath(3)`. On Darwin it rebuilds every
203
- * component from the filesystem's own `ATTR_CMN_NAME` — Libc-1669.0.4
204
- * `stdlib/FreeBSD/realpath.c:233` (`getattrlist(resolved, &_rp_alist, …,
205
- * FSOPT_NOFOLLOW)` with `ATTR_CMN_NAME`) and `:348-354` ("attrs already has
206
- * the real name") — which is why it returns on-disk casing on APFS/HFS+.
207
- * MEASURED counter-example (#3098): a Linux ext4/tmpfs `chattr +F` casefold
208
- * directory aliases the two spellings but `realpath(3)` there returns the
209
- * spelling the caller asked with, so this arm is a no-op on casefolded Linux
210
- * directories — filed with the transcript as #3154.
211
- *
212
- * Always converts backslashes to forward slashes for consistent Map keys.
213
- */
214
- export function normalizeFilePath(filePath) {
215
- // Convert backslashes to forward slashes first
216
- const normalized = filePath.replace(/\\/g, "/");
217
- if (process.platform !== "win32" && !isWindowsPath(normalized)) {
218
- // #3184. `path.posix`, not the host default: this branch is already
219
- // committed to POSIX parsing of a slash-folded string (shape 2). Only
220
- // the POSIX arm needs this — the win32 arm below reaches `realpath` or
221
- // `win32.resolve`/`win32.normalize` on every path, all of which fold
222
- // dot segments already (measured: `C:\repo\src\..\src\a.ts` →
223
- // `c:/repo/src/a.ts` on this POSIX host, before this change) — and
224
- // folding BEFORE the arms would also move which arm a degenerate
225
- // drive-letter path selects (`path.posix.normalize("C:/repo/../..")`
226
- // is `"."`, no longer Windows-shaped).
227
- // Two inputs keep the caller's spelling instead:
228
- // - "" is a non-path sentinel in this codebase's path-typed fields
229
- // (see `normalizeLoggedPath`'s doc); `posix.normalize("")` invents
230
- // ".", the process cwd.
231
- // - a UNC root (`\\server\share`, slash-folded to `//server/share`)
232
- // reaches THIS arm on a POSIX host, because `isWindowsPath` tests
233
- // the already-folded string and sees no backslash; POSIX
234
- // `normalize` collapses its leading `//` to `/`, renaming the path
235
- // to an unrelated local one.
236
- const folded = normalized === "" || normalized.startsWith("//")
237
- ? normalized
238
- : path.posix.normalize(normalized);
239
- try {
240
- const canonical = realpathSync.native(folded);
241
- // Fast path, not a guard: both arms answer `folded` when the
242
- // strings match, but skipping the two `split`s there is a measured
243
- // 1.9 vs 2.3 microseconds per call on the per-edit seam (#3098).
244
- if (canonical === folded)
245
- return folded;
246
- const adopted = adoptCanonicalCasing(folded, canonical);
247
- if (adopted === folded)
248
- return folded;
249
- // The rewrite is string algebra and can land on a DIFFERENT file
250
- // (#3159 review round 2, F1 — see `adoptCanonicalCasing`). Adopt it
251
- // only once the filesystem agrees it still names the file the caller
252
- // held: `canonical` IS `realpath(normalized)`, so this asks exactly
253
- // "does the rewritten spelling resolve to the same file?". One extra
254
- // syscall, and only on the rare branch where casing actually moved —
255
- // never on an already-canonical path. A throw here (the rewritten
256
- // path does not exist, the #2490 monorepo case) lands in the catch
257
- // below and keeps the caller's spelling, which is the same answer.
258
- return realpathSync.native(adopted) === canonical ? adopted : folded;
259
- }
260
- catch {
261
- // Does not exist (or is unreadable): case-preserving, as above.
262
- return folded;
263
- }
264
- }
265
- // Windows: try realpathSync.native() for canonical casing
266
- // This resolves symlinks and returns the actual filesystem casing
267
- try {
268
- const canonical = realpathSync.native(filePath);
269
- return canonical.replace(/\\/g, "/");
270
- }
271
- catch {
272
- // File doesn't exist yet (new file) — resolve path and lowercase
273
- // We need to walk up the directory tree to find the nearest existing
274
- // parent, resolve its casing, then append the non-existent parts
275
- try {
276
- return resolveNonExisting(filePath);
277
- }
278
- catch {
279
- // Last resort: just lowercase the resolved path
280
- const resolved = win32.normalize(win32.resolve(filePath));
281
- return resolved.replace(/\\/g, "/").toLowerCase();
282
- }
283
- }
284
- }
285
- /** Resolve a filesystem identity once, retaining a usable absolute fallback. */
286
- export function realpathOrResolve(filePath) {
287
- const resolved = path.resolve(filePath);
288
- try {
289
- return realpathSync(resolved);
290
- }
291
- catch {
292
- return resolved;
293
- }
294
- }
295
- /**
296
- * Normalize a logged `filePath`/`cwd` value, but ONLY when it is already a
297
- * fully-qualified path (#2219, the #2141 class's sibling loggers). Several
298
- * NDJSON log-entry types reuse a `filePath`-typed field for non-path
299
- * sentinels alongside genuine paths — `"<quiet-window>"` in
300
- * `cascade-logger.ts`, `"<tree-sitter>"` in `tree-sitter-logger.ts`, a shell
301
- * command or an empty placeholder in `latency-logger.ts`. `normalizeFilePath`
302
- * resolves a relative-looking string against the CURRENT process cwd (see
303
- * `resolveNonExisting` above), so running it over one of those sentinels
304
- * would silently corrupt it into `"<repoRoot>/<quiet-window>"` instead of
305
- * normalizing it. Only a value that is already fully qualified can be the
306
- * #2141 mixed-raw/normalized-path defect; anything else is passed through
307
- * unchanged.
308
- *
309
- * #2229 review round 1, F1: this classifier checks BOTH host shapes
310
- * (`isFullyQualifiedWin32(value) || isFullyQualifiedPosix(value)`), not
311
- * `isFullyQualified(value)` (host-dispatched on `process.platform`). A
312
- * Windows-shaped absolute path (`C:\Users\...`) is exactly the log payload
313
- * this fix exists to normalize, but `isFullyQualified` on Linux CI routes to
314
- * `isFullyQualifiedPosix`, which rejects it (no leading `/`) — so on Linux
315
- * the guard silently no-ops for the very inputs the #2141 defect produces,
316
- * passing the raw backslash form straight through. Checking both shapes
317
- * makes the classifier's answer for a given STRING independent of which OS
318
- * is asking; `normalizeFilePath` itself still branches on `process.platform`
319
- * for how the file is resolved, but whether to normalize at all no longer
320
- * does (AGENTS.md shape 2, the #1024/#1150 OS-divergence class).
321
- */
322
- export function normalizeLoggedPath(value) {
323
- return isFullyQualifiedWin32(value) || isFullyQualifiedPosix(value)
324
- ? normalizeFilePath(value)
325
- : value;
326
- }
327
- /**
328
- * Resolve a non-existing path by finding the nearest existing parent,
329
- * getting its canonical casing, then appending the non-existent parts lowercased.
330
- *
331
- * Example: C:\Users\Foo\newdir\file.ts
332
- * - C:\Users\Foo exists → realpathSync gives C:\Users\Foo
333
- * - newdir\file.ts doesn't exist → lowercased
334
- * - Result: C:/Users/Foo/newdir/file.ts
335
- */
336
- function resolveNonExisting(filePath) {
337
- const resolved = win32.resolve(filePath);
338
- let current = resolved;
339
- const nonExistentParts = [];
340
- // Walk up until we find an existing directory
341
- while (true) {
342
- if (existsSync(current)) {
343
- // Found existing ancestor — get its canonical casing
344
- const canonical = realpathSync.native(current);
345
- if (nonExistentParts.length === 0) {
346
- return canonical.replace(/\\/g, "/");
347
- }
348
- // Append non-existent parts (lowercased for consistency)
349
- const tail = nonExistentParts.reverse().join("/").toLowerCase();
350
- const base = canonical.replace(/\\/g, "/");
351
- return base.endsWith("/") ? base + tail : `${base}/${tail}`;
352
- }
353
- // Use win32.dirname (not the platform-default dirname) so a
354
- // Windows-shaped path is parsed with win32 semantics regardless of the
355
- // running OS — consistent with the win32.resolve/win32.normalize this
356
- // branch already commits to. The platform-default POSIX dirname would
357
- // find no separator in a win32-resolved "C:\repo\..." path (its only
358
- // separators are backslashes), collapse to ".", stop the upward walk at
359
- // cwd, and mangle the key on Linux CI (refs #1150, the #1024
360
- // OS-divergence class).
361
- const parent = win32.dirname(current);
362
- if (parent === current) {
363
- // Reached filesystem root without finding existing dir
364
- // Fall back to full lowercase
365
- throw new Error("No existing parent found");
366
- }
367
- nonExistentParts.push(win32.basename(current));
368
- current = parent;
369
- }
370
- }
371
- /**
372
- * Convert a file:// URI to a normalized path.
373
- * Handles URL decoding and Windows drive letter normalization.
374
- */
375
- export function uriToPath(uri) {
376
- try {
377
- const filePath = fileURLToPath(uri);
378
- return normalizeFilePath(filePath);
379
- }
380
- catch {
381
- // Not a valid file:// URI, treat as plain path
382
- return normalizeFilePath(uri);
383
- }
384
- }
385
- /**
386
- * Decode a file:// URI to an on-disk path WITHOUT map-key normalization.
387
- *
388
- * `uriToPath` runs its result through `normalizeFilePath`, which on win32
389
- * lowercases the nonexistent tail of a path (see `resolveNonExisting`) and
390
- * canonicalizes an existing path to its real casing. That is correct for Map
391
- * keys, but DESTRUCTIVE for a real create/rename target: creating `NewFile.txt`
392
- * would write `newfile.txt`, and a legitimate case-only rename would collapse
393
- * to a no-op ("source and destination must differ"). Disk mutations must honor
394
- * the caller's intended casing, so they resolve their target through this
395
- * decode-only path while confinement/validation keep using the normalized
396
- * `uriToPath`. Non-win32 is unaffected either way (normalizeFilePath is a
397
- * near-identity there).
398
- */
399
- export function uriToDiskPath(uri) {
400
- try {
401
- return fileURLToPath(uri);
402
- }
403
- catch {
404
- // Not a valid file:// URI — treat as a plain path (matches uriToPath).
405
- return uri;
406
- }
407
- }
408
- /**
409
- * Convert a path to a file:// URI.
410
- * Does NOT normalize the path - URIs preserve original casing.
411
- */
412
- export function pathToUri(filePath) {
413
- return pathToFileURL(filePath).href;
414
- }
415
- /**
416
- * Normalize a Map key lookup for file paths.
417
- * Use this when getting/setting values in Maps that use file paths as keys.
418
- */
419
- export function normalizeMapKey(filePath) {
420
- return normalizeFilePath(filePath);
421
- }
422
- /**
423
- * Human-facing path relative to a project root when the file is inside it.
424
- *
425
- * Parses by path SHAPE, not host OS (refs #1150/#1152, shape-2 class #1163):
426
- * a Windows-shaped `filePath` (drive-letter/UNC — e.g. a persisted call-graph
427
- * symbol-key path `C:\repo\src\x.ts` rehydrated on a Linux CI run) is split
428
- * with `win32.*` regardless of `process.platform`. The host-default
429
- * `isAbsolute`/`relative` find no drive-letter anchor in a win32 path on POSIX:
430
- * `path.isAbsolute("C:\\repo\\x.ts")` returns FALSE on Linux, short-circuiting
431
- * to the raw absolute path instead of ever relativizing it — so a file that IS
432
- * under the project root renders as a full absolute path on Linux but the
433
- * expected `src/x.ts` on Windows (green-locally / wrong-on-CI, the #1024
434
- * divergence class). `win32.*` on a native POSIX path (Windows never sees one;
435
- * Linux native paths aren't Windows-shaped) is never selected, so same-OS
436
- * native paths are unchanged either way.
437
- */
438
- export function toProjectRelativePath(filePath, projectRoot) {
439
- const p = isWindowsPath(filePath) ? win32 : path;
440
- if (!p.isAbsolute(filePath))
441
- return filePath.replace(/\\/g, "/");
442
- const relative = p.relative(p.resolve(projectRoot), filePath);
443
- return relative && !relative.startsWith("..") && !p.isAbsolute(relative)
444
- ? relative.replace(/\\/g, "/")
445
- : filePath.replace(/\\/g, "/");
446
- }
447
- /**
448
- * Cheap, syntactic-only Map key normalization: slash-fold + (on Windows)
449
- * lowercase. No `realpathSync` / filesystem I/O.
450
- *
451
- * `normalizeMapKey` (via `normalizeFilePath`) calls `realpathSync.native()` to
452
- * get canonical on-disk casing — correct for maps that key long-lived state
453
- * shared across call sites (e.g. LSP/read-guard caches), but expensive when
454
- * the *point* of the cache is to avoid filesystem calls in the first place:
455
- * for a candidate path that does NOT exist (the common case for sibling-probe
456
- * memos), `normalizeFilePath` walks up the directory tree doing its own
457
- * `existsSync` calls to resolve the nearest existing ancestor — measured at
458
- * ~11x slower than the single `existsSync` probe such a cache is trying to
459
- * save (refs #191).
460
- *
461
- * Safe to use ONLY for ephemeral, single-process, single-walk caches whose
462
- * keys are produced by this process's own `path.join`/`path.resolve` calls
463
- * within the same run (so separators and casing are already consistent
464
- * modulo simple slash direction) — never for state shared across processes,
465
- * persisted, or compared against externally-supplied paths where symlink /
466
- * real-casing resolution actually matters.
467
- */
468
- export function normalizeEphemeralMapKey(filePath) {
469
- // Most hot-path keys on POSIX are already canonical slash-separated strings.
470
- // Preserve that identity instead of allocating a replacement string for each
471
- // file in a large diagnostics reconciliation.
472
- if (process.platform !== "win32" && !filePath.includes("\\"))
473
- return filePath;
474
- const slashed = filePath.replace(/\\/g, "/");
475
- return process.platform === "win32" ? slashed.toLowerCase() : slashed;
476
- }
477
- /**
478
- * Compare two file paths for equality, handling Windows case-insensitivity
479
- * and mixed separators (backslash vs forward slash).
480
- */
481
- export function pathsEqual(a, b) {
482
- return normalizeFilePath(a) === normalizeFilePath(b);
483
- }
484
- /**
485
- * Check if `child` is under `parent` directory.
486
- * Separator-agnostic and case-insensitive on Windows.
487
- */
488
- /**
489
- * Yield each directory from `startDir` up to (and including) the filesystem
490
- * root. Terminates when `path.dirname(current) === current` so it works on
491
- * Windows drive roots and POSIX `/` alike.
492
- *
493
- * Single source of truth for the half-dozen "walk up the directory tree
494
- * looking for X" loops that have accumulated across the codebase. Callers
495
- * that need an "is there a file named Y anywhere on the way up" check
496
- * should use `findNearestContaining` instead.
497
- */
498
- export function* walkUpDirs(startDir) {
499
- let current = path.resolve(startDir);
500
- while (true) {
501
- yield current;
502
- const parent = path.dirname(current);
503
- if (parent === current)
504
- return;
505
- current = parent;
506
- }
507
- }
508
- /**
509
- * Walk up from `startDir` and return the first directory that contains any
510
- * of `candidates` on disk. Returns `undefined` if none match.
511
- *
512
- * @example
513
- * findNearestContaining("/repo/pkg/src", ["package.json", "tsconfig.json"]);
514
- * // → "/repo/pkg" if pkg/package.json exists, "/repo" if only /repo/package.json
515
- */
516
- export function findNearestContaining(startDir, candidates) {
517
- for (const dir of walkUpDirs(startDir)) {
518
- for (const name of candidates) {
519
- if (existsSync(path.join(dir, name)))
520
- return dir;
521
- }
522
- }
523
- return undefined;
524
- }
525
- /**
526
- * Walk up from `startDir` and return the first matching FILE path (not just
527
- * the containing directory) for any of `names`, first-match-wins within each
528
- * directory in `names` order. Single source of truth for the "walk up
529
- * looking for one of these config filenames" loop that `opengrep-config.ts`,
530
- * `typos-config.ts`, `zizmor-config.ts`, and `sgconfig.ts` each hand-rolled
531
- * independently (refs #680), and that `php-cs-fixer-config.ts` now delegates
532
- * to as well (refs #2472 review F2).
533
- *
534
- * UNCEILINGED by default (refs #2472 review round 3, F1) — `options.homeDir`
535
- * is opt-in, not default-on. A prior version applied the SAME `$HOME`
536
- * ceiling as `findNearestMarkerRoot` unconditionally, which broke every one
537
- * of these tools' actual discovery contract: each of them treats a config
538
- * living directly at `$HOME` (`~/typos.toml`, `~/sgconfig.yml`, …) as the
539
- * user's legitimate GLOBAL config, and reads it itself regardless of pi-lens
540
- * — the ceiling didn't stop pi-lens from seeing an unrelated ancestor
541
- * config, it stopped pi-lens from seeing the SAME config the tool was about
542
- * to read on its own, so pi-lens silently fell back to (or, for typos,
543
- * injected and let its own shipped `_typos.toml` merge over) the user's
544
- * config where the tool's own resolver would have honored it. `php-cs-fixer`
545
- * makes the same mismatch concrete: its detection gates
546
- * (`hasPhpCsFixerConfig` via `findNearestContaining`, `phpCsFixerFormatter
547
- * .detect` via its own `findUp`) are both unceilinged, so a ceilinged
548
- * carriage here disagreed with its own gate — "config exists" from the gate,
549
- * "config not found" from the resolver — and dropped the very `--config`
550
- * argv #2472 exists to carry. Pass `options.homeDir` only when a caller
551
- * affirmatively wants the ceiling (a config found at or above THAT directory
552
- * is never returned); omitting it walks all the way to the filesystem root,
553
- * matching `findNearestContaining`'s unceilinged behavior and every
554
- * underlying tool's own discovery.
555
- *
556
- * Distinct from `findNearestContaining`, which returns the containing
557
- * directory rather than the matched file path — use that one when the caller
558
- * only needs "is one of these present nearby", not which file it is.
559
- *
560
- * @example
561
- * findLocalToolConfig(cwd, ["typos.toml", "_typos.toml", ".typos.toml"]);
562
- * // → "/repo/typos.toml" if present, else undefined
563
- */
564
- export function findLocalToolConfig(startDir, names, options = {}) {
565
- const homeDir = options.homeDir !== undefined ? path.resolve(options.homeDir) : undefined;
566
- for (const dir of walkUpDirs(startDir || process.cwd())) {
567
- if (homeDir !== undefined && isAtOrAboveHomeDir(dir, homeDir)) {
568
- return undefined;
569
- }
570
- for (const name of names) {
571
- const candidate = path.join(dir, name);
572
- if (existsSync(candidate))
573
- return candidate;
574
- }
575
- }
576
- return undefined;
577
- }
578
- /**
579
- * Accept only a real Git repository marker: a directory with HEAD, or a
580
- * worktree/submodule marker file whose first line starts with `gitdir:`.
581
- */
582
- export function isRealGitMarker(markerPath) {
583
- try {
584
- const marker = statSync(markerPath);
585
- if (marker.isDirectory())
586
- return existsSync(path.join(markerPath, "HEAD"));
587
- if (!marker.isFile())
588
- return false;
589
- return readFileSync(markerPath, "utf8")
590
- .split(/\r?\n/, 1)[0]
591
- .startsWith("gitdir:");
592
- }
593
- catch {
594
- return false;
595
- }
596
- }
597
- /**
598
- * Walk up from `startDir` looking for a directory containing any of
599
- * `markers`, the same containment-aware climb `knip-client.ts` and
600
- * `dead-code-client.ts` each used to hand-roll independently (refs #625):
601
- *
602
- * - Never resolves at or above `$HOME` (via `isAtOrAboveHomeDir`) — a
603
- * marker found there has escaped the user's workspace.
604
- * - If `options.boundaries` is given and one is found before any `marker`,
605
- * stops and returns `null` rather than continuing past it.
606
- * - Depth-capped at 64 climbs, matching the callers' existing safety bound
607
- * (guards a pathological symlink loop; real depths are ~10).
608
- * - Returns `null` — never `startDir` — when nothing is found. Callers
609
- * must treat `null` as "no project here", not fall back to the start
610
- * directory (a `null`-swallowing fallback was the #250/#296 bug class:
611
- * scanning $HOME wholesale from a bare cwd).
612
- *
613
- * For a plain "find nearest containing directory" with no boundary concept,
614
- * use `findNearestContaining` instead. Distinct from `startup-scan.ts`'s
615
- * `findNearestProjectRoot` (fixed marker list, no boundaries, no home-check —
616
- * that caller applies `isAtOrAboveHomeDir` itself afterward); named
617
- * differently here to avoid confusion between the two.
618
- */
619
- export function findNearestMarkerRoot(startDir, markers, options = {}) {
620
- const boundaries = options.boundaries ?? [];
621
- const homeDir = path.resolve(options.homeDir ?? os.homedir());
622
- const markerPredicate = options.markerPredicate ?? (() => true);
623
- let current = path.resolve(startDir);
624
- for (let depth = 0; depth < 64; depth++) {
625
- if (isAtOrAboveHomeDir(current, homeDir))
626
- return null;
627
- if (markers.some((m) => existsSync(path.join(current, m)) &&
628
- markerPredicate(path.join(current, m))))
629
- return current;
630
- if (boundaries.some((m) => existsSync(path.join(current, m))))
631
- return null;
632
- const parent = path.dirname(current);
633
- if (parent === current)
634
- return null;
635
- current = parent;
636
- }
637
- return null;
638
- }
639
- /**
640
- * True when `dir` is the home directory OR an ancestor of it (`/home`,
641
- * `C:\Users`, the filesystem root, …). A project-root search that climbs to
642
- * such a directory has escaped the user's workspace — walking down from it
643
- * scans unrelated trees (the #250 runaway). Use this as the single shared
644
- * ceiling on any upward project-root resolution, instead of an exact
645
- * `=== os.homedir()` check (which a marker found *above* `$HOME` slips past).
646
- * A normal project *under* home (e.g. `~/code/app`) is NOT at-or-above home,
647
- * so it still resolves fine. Refs #253.
648
- *
649
- * ONE expression, deliberately: `path.relative` alone answers "is home inside
650
- * `dir`, or the same directory as `dir`", and it answers with the PLATFORM's
651
- * path-equality semantics — case-folding on win32, case-sensitive on POSIX.
652
- * The `resolvedDir === resolvedHome` shortcut that used to precede it was
653
- * case-SENSITIVE on every platform, so `c:\Users\jane` (the lowercase-drive
654
- * form VS Code URIs produce, and the form 46 records in a real `latency.log`
655
- * carry) missed the equality test AND was then rejected by a `rel !== ""`
656
- * clause — reporting "not at home" for the home directory itself and letting
657
- * every ceilinged walker climb straight past HOME (#2544 review F1).
658
- *
659
- * rel === "" ⇢ `dir` IS home
660
- * rel relative, no leading `..` ⇢ home is INSIDE `dir` (an ancestor)
661
- * rel starts with `..` ⇢ home is elsewhere — sibling, or the
662
- * prefix trap `C:\Users\jane2` → home
663
- * `C:\Users\jane` gives `..\jane`
664
- * rel absolute ⇢ different Windows drive
665
- *
666
- * Same shape as `isUnderDir` above; keep the two in step.
667
- *
668
- * `pathImpl` exists so the ubuntu Unit tests lane — the authoritative one —
669
- * can exercise the WIN32 semantics this helper's whole bug was about. Every
670
- * assertion that matters here (drive-letter folding, cross-drive `rel`) is
671
- * win32-only, so with the ambient `path` hardcoded the ceiling was enforced
672
- * only on a maintainer's dev box and CI's diff was structurally zero (#2544
673
- * review F2). Production never passes it; `path.win32`/`path.posix` are pure,
674
- * host-independent implementations, so `tests/clients/path-utils.test.ts` runs
675
- * the same table under both on every lane. Lane-level fix tracked as #2536.
676
- */
677
- export function isAtOrAboveHomeDir(dir, homeDir = os.homedir(), pathImpl = path) {
678
- const rel = pathImpl.relative(pathImpl.resolve(dir), pathImpl.resolve(homeDir));
679
- return !rel.startsWith("..") && !pathImpl.isAbsolute(rel);
680
- }
681
- /**
682
- * Rewrite a `$HOME`-anchored path to its `~` form for a DIAGNOSTIC surface.
683
- *
684
- * `~/.pi-lens/config.json` says everything an operator needs about which file
685
- * won, and `C:/Users/jane.doe/.pi-lens/config.json` says that plus the
686
- * account name. The second is the shape a global config path always has, so any
687
- * projection that names one leaks an identifier by default rather than by
688
- * accident (#2440 review finding F5).
689
- *
690
- * Deliberately NOT built on `normalizeFilePath`/`isUnderDir`: those resolve
691
- * through `realpathSync`, and a redaction helper on a diagnostic path must be
692
- * pure, total, and unable to throw for a file that no longer exists. This is a
693
- * string rewrite and nothing else.
694
- *
695
- * A path that is not under home is returned UNCHANGED — separators included.
696
- * Normalizing unrelated paths on the way past would make the helper's blast
697
- * radius every path any projection ever carries, for no redaction benefit.
698
- */
699
- export function homeRelativePath(filePath, homeDir = os.homedir()) {
700
- if (filePath.length === 0)
701
- return filePath;
702
- const candidate = toPosix(filePath);
703
- const home = toPosix(homeDir).replace(/\/+$/, "");
704
- if (home.length === 0)
705
- return filePath;
706
- // Windows paths are case-insensitive, so the COMPARISON folds case there
707
- // while the returned tail keeps the caller's own casing.
708
- const fold = (text) => process.platform === "win32" ? text.toLowerCase() : text;
709
- const foldedHome = fold(home);
710
- const foldedCandidate = fold(candidate);
711
- if (foldedCandidate === foldedHome)
712
- return "~";
713
- if (!foldedCandidate.startsWith(`${foldedHome}/`))
714
- return filePath;
715
- return `~${candidate.slice(home.length)}`;
716
- }
717
- export function isUnderDir(child, parent) {
718
- const normChild = normalizeFilePath(child);
719
- const normParent = normalizeFilePath(parent);
720
- // Ensure parent ends with / for prefix matching
721
- const parentPrefix = normParent.endsWith("/") ? normParent : `${normParent}/`;
722
- return normChild === normParent || normChild.startsWith(parentPrefix);
723
- }
724
- const VENDOR_DIR_NAMES = new Set([
725
- "node_modules",
726
- "vendor",
727
- "vendors",
728
- "third_party",
729
- "third-party",
730
- ]);
731
- /**
732
- * Returns true when a file should be treated as external/vendor and excluded
733
- * from pipelines (LSP, diagnostics, complexity, read-guard, etc.).
734
- *
735
- * Cases:
736
- * 1. Outside the project root entirely (e.g. global npm packages, system files)
737
- * 2. Inside the project but under a vendor directory (node_modules, vendor, third_party, etc.)
738
- */
739
- export function isExternalOrVendorFile(filePath, projectRoot) {
740
- if (!isUnderDir(filePath, projectRoot))
741
- return true;
742
- const normalized = normalizeFilePath(filePath);
743
- const rootNorm = normalizeFilePath(projectRoot);
744
- const rel = normalized.startsWith(rootNorm + "/")
745
- ? normalized.slice(rootNorm.length + 1)
746
- : normalized;
747
- return rel.split("/").some((seg) => VENDOR_DIR_NAMES.has(seg));
748
- }
749
- /**
750
- * Shared marker-glob semantics for every "does this directory contain a file
751
- * matching this glob" probe (#895 review): match against the entry NAME only,
752
- * `dot: true` so dotfile markers match, `nocase` on win32 to match the
753
- * filesystem (and the project ignore matcher). The three marker probes —
754
- * language-profile.ts `hasProjectMarker`, workspace-topology.ts
755
- * `hasBasenameMarker`, lsp/server.ts `markerExists` — must all route their
756
- * glob matching through here rather than call minimatch with hand-copied
757
- * options.
758
- */
759
- export function nameMatchesMarkerGlob(name, pattern) {
760
- return minimatch(name, pattern, {
761
- dot: true,
762
- nocase: process.platform === "win32",
763
- });
764
- }
765
- /**
766
- * Files/symlinks-only marker-glob probe over a directory listing — a
767
- * *directory* named like a marker (e.g. a `Foo.csproj/` dir) is not a project
768
- * file (#201).
769
- */
770
- export function direntsHaveMarkerGlobMatch(entries, pattern) {
771
- return entries.some((entry) => (entry.isFile() || entry.isSymbolicLink()) &&
772
- nameMatchesMarkerGlob(entry.name, pattern));
773
- }
774
- /**
775
- * Cargo `[workspace] members`/`exclude` entries.
776
- *
777
- * KNOWN LIMITATION (#1671 F6, documented rather than implemented, preserved
778
- * byte-for-byte by #2591's fold): a recursive `**` component is NOT supported
779
- * and a pattern containing one never matches — cargo workspaces that rely on
780
- * `**` to pull in an arbitrarily-nested crate tree under-hoist (the crate stays
781
- * independently rooted instead of joining the workspace). Folding cargo onto
782
- * uv's `crosses-components` globstar would silently change Rust-LSP root
783
- * selection, so the divergence is kept as a dialect value, not resolved.
784
- *
785
- * `characterClasses: false` is likewise a preserved divergence, not a
786
- * considered choice: cargo's pre-fold segment compiler escaped `[` and `]`
787
- * into literals, so `crates/[ab]` names a directory literally called `[ab]`.
788
- * Real cargo (the `glob` crate) would read it as a class; changing that here
789
- * would be the same unreviewed Rust-hoisting change.
790
- */
791
- export const CARGO_WORKSPACE_MEMBER_DIALECT = {
792
- name: "cargo",
793
- globstar: "pattern-never-matches",
794
- wildcardCrossesSeparator: false,
795
- characterClasses: false,
796
- normalizePattern: (pattern) => pattern.replace(/\/+$/, ""),
797
- };
798
- /**
799
- * uv `[tool.uv.workspace] members`, pinned to
800
- * `astral-sh/uv@3c979abda4530fe9bf3d92e9bcf5c5575e3b3126`,
801
- * `crates/uv-workspace/src/workspace.rs` `is_included_in_workspace`: the glob is
802
- * `normalize_path`d first (so a leading `./` and any interior `.` component are
803
- * not part of the pattern) and matched with
804
- * `MatchOptions { require_literal_separator: true, ..MatchOptions::new() }`.
805
- */
806
- export const UV_WORKSPACE_MEMBERS_DIALECT = {
807
- name: "uv-members",
808
- globstar: "crosses-components",
809
- wildcardCrossesSeparator: false,
810
- characterClasses: true,
811
- normalizePattern: (pattern) => path.posix.normalize(toPosix(pattern)),
812
- };
813
- /**
814
- * uv `[tool.uv.workspace] exclude`, same upstream file and SHA
815
- * (`WorkspaceExclusions::matches`): an exclusion is matched with
816
- * `Pattern::matches_path`, i.e. `MatchOptions::new()` defaults, where
817
- * `require_literal_separator` is FALSE — so a `*` in an exclusion DOES cross
818
- * `/` (`exclude = ['packages/a*c']` excludes `packages/a/b/c`). Two different
819
- * option sets inside one tool, which is why the dialect is an object and the
820
- * two uv constants are not one.
821
- *
822
- * Before #2591 this was a documented limitation: minimatch cannot express a
823
- * separator-crossing `*`, so pi-lens under-excluded. The dialect object can, so
824
- * it is now implemented rather than documented.
825
- */
826
- export const UV_WORKSPACE_EXCLUDE_DIALECT = {
827
- name: "uv-exclude",
828
- globstar: "crosses-components",
829
- wildcardCrossesSeparator: true,
830
- characterClasses: true,
831
- normalizePattern: (pattern) => path.posix.normalize(toPosix(pattern)),
832
- };
833
- /**
834
- * Index of the `]` closing the character class opened at `open`, or `-1` when
835
- * the run is unterminated (in which case the `[` is a literal). A leading `!`
836
- * or `^` negates, and a `]` in first position is a literal class member — the
837
- * shape both minimatch and rust `glob` accept.
838
- */
839
- function findCharacterClassEnd(segment, open) {
840
- let i = open + 1;
841
- if (segment[i] === "!" || segment[i] === "^")
842
- i += 1;
843
- if (segment[i] === "]")
844
- i += 1;
845
- for (; i < segment.length; i += 1)
846
- if (segment[i] === "]")
847
- return i;
848
- return -1;
849
- }
850
- /**
851
- * May a wildcard with this separator policy consume `ch`?
852
- *
853
- * A separator-confined wildcard (`[^/]` in the regex this replaced) takes
854
- * anything but `/`. A separator-CROSSING one — uv `exclude`'s `*`/`?`, and
855
- * every `**` in either uv dialect — spelled `.` in that regex, which is every
856
- * code unit EXCEPT the four line terminators. That exclusion is preserved
857
- * deliberately rather than "fixed": minimatch's globstar is `.`-based too, so
858
- * a `\n` inside a directory name has never matched across a `**` in either
859
- * implementation, and the differential oracle would flag a change here as a
860
- * divergence rather than an improvement (#2603).
861
- */
862
- function stepAcceptsChar(crossesSeparator, ch) {
863
- return crossesSeparator
864
- ? ch !== "\n" && ch !== "\r" && ch !== "\u2028" && ch !== "\u2029"
865
- : ch !== "/";
866
- }
867
- /**
868
- * Compile a normalized workspace-member pattern into a step list, or
869
- * `undefined` when it cannot be compiled at all.
870
- *
871
- * A `**` component consumes zero or more path components — except as the LAST
872
- * component, where it requires at least one (`a/**` matches `a/b`, not `a`),
873
- * reproducing both minimatch's and rust `glob`'s answer. It is emitted as
874
- * `split → one → star → literal "/"`, i.e. "nothing, or one-or-more characters
875
- * followed by the `/` that ends them", after the separator that precedes it.
876
- * That is the same language as the `(?:/.+)?/` group the previous compiler
877
- * emitted — `X(?:/.+)?/Y` and `X/(?:.+/)?Y` both denote `X/Y` ∪ `X/.+/Y` — in a
878
- * form with no nested quantifier.
879
- */
880
- function compileWorkspaceMemberPattern(pattern, dialect) {
881
- const isGlobstar = (component) => component === "**" && dialect.globstar === "crosses-components";
882
- const components = pattern.split("/");
883
- const steps = [];
884
- let needSeparator = false;
885
- for (let c = 0; c < components.length; c += 1) {
886
- if (needSeparator)
887
- steps.push({ kind: "literal", char: "/" });
888
- needSeparator = false;
889
- const component = components[c];
890
- if (isGlobstar(component)) {
891
- if (c === components.length - 1) {
892
- // `.+` — a trailing `**` requires at least one character.
893
- steps.push({ kind: "one", crossesSeparator: true });
894
- steps.push({ kind: "star", crossesSeparator: true });
895
- continue;
896
- }
897
- // `(?:.+/)?` — zero or more whole components. The group already ends
898
- // at a `/`, so the next component must NOT emit one.
899
- const split = { kind: "split", alternative: -1 };
900
- steps.push(split);
901
- steps.push({ kind: "one", crossesSeparator: true });
902
- steps.push({ kind: "star", crossesSeparator: true });
903
- steps.push({ kind: "literal", char: "/" });
904
- split.alternative = steps.length;
905
- continue;
906
- }
907
- for (let i = 0; i < component.length; i += 1) {
908
- const ch = component[i];
909
- if (ch === "*") {
910
- steps.push({
911
- kind: "star",
912
- crossesSeparator: dialect.wildcardCrossesSeparator,
913
- });
914
- continue;
915
- }
916
- if (ch === "?") {
917
- steps.push({
918
- kind: "one",
919
- crossesSeparator: dialect.wildcardCrossesSeparator,
920
- });
921
- continue;
922
- }
923
- if (ch === "[" && dialect.characterClasses) {
924
- const close = findCharacterClassEnd(component, i);
925
- if (close !== -1) {
926
- // `\` and a first-position `]` are literal class MEMBERS in glob;
927
- // left alone they would be a regex escape and an empty-class
928
- // terminator (`[]ab]` is `[]` + `ab]` in JS), so both are escaped.
929
- const body = component.slice(i + 1, close).replace(/[\\\]]/g, "\\$&");
930
- const source = body.startsWith("!")
931
- ? `[^${body.slice(1)}]`
932
- : `[${body}]`;
933
- let match;
934
- try {
935
- match = new RegExp(`^${source}$`);
936
- }
937
- catch {
938
- // A glob character class is not a JS character class: `[z-a]`
939
- // is a legal glob (matching nothing, since the range is empty)
940
- // and an illegal RegExp ("Range out of order"). Fail CLOSED —
941
- // an uncompilable pattern declares no member and excludes
942
- // nothing — which is also the answer the deleted minimatch call
943
- // gave (#2591 review round 2, F2).
944
- return undefined;
945
- }
946
- steps.push({ kind: "class", match });
947
- i = close;
948
- continue;
949
- }
950
- }
951
- steps.push({ kind: "literal", char: ch });
952
- }
953
- needSeparator = true;
954
- }
955
- return steps;
956
- }
957
- /**
958
- * Does the whole of `subject` match the whole of `steps`?
959
- *
960
- * A memoized (step index, path index) table, filled once, bottom-up:
961
- * `table[s][p]` is "steps `s…` match `subject[p…]`". Every cell reads only
962
- * cells with a larger step index or a larger path index, so one backward
963
- * double loop fills the table with no recursion and no re-entry — the match is
964
- * O(steps x characters) in time and space, with no path through it that can
965
- * take exponential time (#2603).
966
- *
967
- * The compiled whole-path regex this replaced was correct but backtracking:
968
- * every `**` emitted its own nullable `.+`, and N of them explored 2^N splits
969
- * of a non-matching subject. #2591 collapsed CONSECUTIVE `**`s, which is a
970
- * normalization that cannot fire across a separating component, so
971
- * `("**\/*" x12)/zzz` against a 40-component path still took 124900 ms (#2603);
972
- * the same shapes are microseconds here. There is nothing left to collapse for
973
- * speed, so no collapse is done: `a/**\/**\/b` compiles to two adjacent
974
- * `(?:.+/)?` groups, which denote the same language as one and cost the same
975
- * table.
976
- */
977
- function matchesWorkspaceMemberSteps(steps, subject) {
978
- const stepCount = steps.length;
979
- const width = subject.length + 1;
980
- // One byte per (step, position) cell; `1` means "the rest matches from here".
981
- const table = new Uint8Array((stepCount + 1) * width);
982
- for (let p = subject.length; p >= 0; p -= 1) {
983
- const atEnd = p === subject.length;
984
- // The empty step list matches only the empty remainder.
985
- table[stepCount * width + p] = atEnd ? 1 : 0;
986
- for (let s = stepCount - 1; s >= 0; s -= 1) {
987
- const step = steps[s];
988
- const next = (s + 1) * width;
989
- let matched = 0;
990
- switch (step.kind) {
991
- case "literal":
992
- matched =
993
- !atEnd && subject[p] === step.char ? table[next + p + 1] : 0;
994
- break;
995
- case "one":
996
- matched =
997
- !atEnd && stepAcceptsChar(step.crossesSeparator, subject[p])
998
- ? table[next + p + 1]
999
- : 0;
1000
- break;
1001
- case "class":
1002
- matched =
1003
- !atEnd && step.match.test(subject[p]) ? table[next + p + 1] : 0;
1004
- break;
1005
- case "star":
1006
- // Consume nothing, or one more character and stay on this step.
1007
- matched =
1008
- table[next + p] === 1 ||
1009
- (!atEnd &&
1010
- stepAcceptsChar(step.crossesSeparator, subject[p]) &&
1011
- table[s * width + p + 1] === 1)
1012
- ? 1
1013
- : 0;
1014
- break;
1015
- case "split":
1016
- matched =
1017
- table[step.alternative * width + p] === 1 || table[next + p] === 1
1018
- ? 1
1019
- : 0;
1020
- break;
1021
- }
1022
- table[s * width + p] = matched;
1023
- }
1024
- }
1025
- return table[0] === 1;
1026
- }
1027
- /**
1028
- * Does `relativePath` — a `/`-separated path relative to the workspace root —
1029
- * match one declared workspace-member (or workspace-exclude) `pattern` under
1030
- * `dialect`?
1031
- *
1032
- * This is the ONE workspace-member matcher (#2591). `clients/lsp/server.ts`'s
1033
- * `cargoWorkspaceDeclaresMember` and `clients/python-environment.ts`'s
1034
- * `isUvWorkspaceMember` are thin callers of it; neither keeps a private segment
1035
- * compiler or minimatch options block any more.
1036
- *
1037
- * `clients/review-graph/workspace-modules.ts`'s `expandWorkspacePattern` is
1038
- * deliberately NOT a caller: npm/pnpm `workspaces` entries are EXPANDED against
1039
- * the filesystem (one `readdir` of the directory preceding the first `*`,
1040
- * yielding the directories that exist and hold a manifest) rather than tested
1041
- * against a candidate path. It answers "which directories does this pattern
1042
- * name", not "does this pattern name this directory"; folding it in would mean
1043
- * giving this pure function a filesystem.
1044
- *
1045
- * The supported syntax is the intersection of the two upstream dialects: `*`,
1046
- * `?`, `**`, `[abc]`/`[!abc]` (dialect-gated), and literals. minimatch-only
1047
- * extensions the pre-fold uv path inherited by accident — brace expansion,
1048
- * extglobs, leading-`!` negation, leading-`#` comments — are NOT honored, and
1049
- * uv's own `glob` crate does not honor them either (same pinned SHA), so
1050
- * dropping them moves uv toward upstream rather than away from it.
1051
- *
1052
- * Matching is NON-BACKTRACKING (#2603): the pattern compiles to a list of
1053
- * bounded steps and {@link matchesWorkspaceMemberSteps} decides it with one
1054
- * memoized (step, position) table, O(steps x characters), no matter how many
1055
- * `**`s the pattern carries. The anchored whole-path regex this replaced was
1056
- * correct but explored 2^N splits of a non-matching path for N `**`
1057
- * components — 124900 ms for `("**\/*" x12)/zzz` against a 40-component path,
1058
- * on a call `detectPythonEnvironment` awaits with no timeout, i.e. a wedged
1059
- * turn rather than a slow answer. The wall-clock half of that fix lives in
1060
- * `tests/clients/workspace-glob-nonbacktracking-budget.test.ts`; the answers
1061
- * are unchanged, pinned by the dialect table and the minimatch differential in
1062
- * `tests/clients/path-utils.test.ts`.
1063
- *
1064
- * A pattern that cannot be compiled at all answers `false` for every path —
1065
- * it declares no member and excludes nothing (#2591 review round 2, F2). The
1066
- * only such patterns today carry an empty character-class range (`[z-a]`:
1067
- * legal glob, illegal JS RegExp). Reachability note, recorded rather than
1068
- * assumed away: no such pattern can reach here through a manifest right now,
1069
- * because `parseTomlStringArray` (`clients/cargo-manifest.ts`) captures an
1070
- * array body non-greedily up to the FIRST `]`, so any entry containing a `]`
1071
- * loses its closing quote and is dropped before it becomes a pattern. That is
1072
- * a property of the TOML reader, not of this matcher, and it is not this
1073
- * function's to rely on — the character-class axis stays because upstream
1074
- * cargo and uv both support classes, and the reader's gap may be closed later.
1075
- */
1076
- export function matchesWorkspaceMemberPattern(pattern, relativePath, dialect) {
1077
- const normalized = dialect.normalizePattern(pattern);
1078
- if (dialect.globstar === "pattern-never-matches" &&
1079
- normalized.includes("**")) {
1080
- return false;
1081
- }
1082
- const steps = compileWorkspaceMemberPattern(normalized, dialect);
1083
- return (steps !== undefined && matchesWorkspaceMemberSteps(steps, relativePath));
1084
- }
1085
- /**
1086
- * Narrow no-break space, U+202F. macOS writes it before AM/PM in screenshot
1087
- * file names; users type an ordinary space.
1088
- */
1089
- const NARROW_NO_BREAK_SPACE = "\u202F";
1090
- /** Right single quotation mark, U+2019. macOS writes it; users type U+0027. */
1091
- const RIGHT_SINGLE_QUOTE = "\u2019";
1092
- /**
1093
- * The unicode space class pi folds to U+0020, copied character-for-character
1094
- * from `@earendil-works/pi-coding-agent/dist/utils/paths.js:6`
1095
- * (`UNICODE_SPACES`, source `src/utils/paths.ts`). Widening or narrowing this
1096
- * set makes pi-lens resolve a different file than pi does.
1097
- */
1098
- const HOST_UNICODE_SPACES = /[\u00A0\u2000-\u200A\u202F\u205F\u3000]/g;
1099
- /**
1100
- * Mirror pi's `normalizeWindowsShellPath`
1101
- * (`@earendil-works/pi-coding-agent/dist/utils/paths.js:47-56`): convert Git
1102
- * Bash, MSYS, Cygwin, and WSL drive paths to a form native Windows APIs
1103
- * accept.
1104
- */
1105
- function hostNormalizeWindowsShellPath(filePath) {
1106
- if (!filePath.startsWith("/") ||
1107
- filePath.startsWith("//") ||
1108
- filePath.includes("\\")) {
1109
- return filePath;
1110
- }
1111
- const match = filePath.match(/^\/(?:mnt\/|cygdrive\/)?([a-z])(?:\/(.*))?$/i);
1112
- if (!match)
1113
- return filePath;
1114
- const suffix = match[2]?.replaceAll("/", "\\");
1115
- return `${match[1].toUpperCase()}:\\${suffix ?? ""}`;
1116
- }
1117
- /**
1118
- * Mirror pi's `normalizePath`
1119
- * (`@earendil-works/pi-coding-agent/dist/utils/paths.js:57-79`, source
1120
- * `src/utils/paths.ts`), in pi's order, which is load-bearing: the unicode
1121
- * fold runs BEFORE the `@` strip, and the tilde expansion before the
1122
- * `file://` conversion.
1123
- *
1124
- * The `win32` step is gated on `process.platform`, exactly as pi gates it.
1125
- * This is a deliberate exception to the usual probe-the-filesystem rule
1126
- * (AGENTS.md shape 2): pi-lens runs in pi's own process, so mirroring the
1127
- * host's own platform branch is what keeps the two resolvers in agreement.
1128
- * Shape-based parsing here would DIVERGE from the host, not protect against
1129
- * it.
1130
- */
1131
- export function normalizeHostToolPath(input, options = {}) {
1132
- let normalized = input;
1133
- if (options.normalizeUnicodeSpaces) {
1134
- normalized = normalized.replace(HOST_UNICODE_SPACES, " ");
1135
- }
1136
- if (options.stripAtPrefix && normalized.startsWith("@")) {
1137
- normalized = normalized.slice(1);
1138
- }
1139
- if (process.platform === "win32") {
1140
- normalized = hostNormalizeWindowsShellPath(normalized);
1141
- }
1142
- // `homedir()` is resolved inside the branch, as pi does. This runs on every
1143
- // tool_call and a `~` path is the rare case.
1144
- if (normalized === "~")
1145
- return os.homedir();
1146
- if (normalized.startsWith("~/") ||
1147
- (process.platform === "win32" && normalized.startsWith("~\\"))) {
1148
- return path.join(os.homedir(), normalized.slice(2));
1149
- }
1150
- if (normalized.startsWith("file://")) {
1151
- try {
1152
- return fileURLToPath(normalized);
1153
- }
1154
- catch {
1155
- // pi lets a malformed file: URL throw out of normalizePath, but pi-lens
1156
- // is advisory instrumentation on the same event: a URL pi rejects must
1157
- // degrade to "no path" here, never take down the tool_call handler.
1158
- return normalized;
1159
- }
1160
- }
1161
- return normalized;
1162
- }
1163
- /**
1164
- * Mirror pi's `resolveToCwd`
1165
- * (`@earendil-works/pi-coding-agent/dist/core/tools/path-utils.js:42-44`,
1166
- * source `src/core/tools/path-utils.ts:~44-46`) \u2014 the BASE resolution every
1167
- * read/edit/write path goes through before the variant ladder below ever
1168
- * runs.
1169
- *
1170
- * Two details are pi's, not ours, and both matter:
1171
- * - the input is normalized with `normalizeUnicodeSpaces` + `stripAtPrefix`;
1172
- * the BASE DIR is normalized with neither (`resolvePath`,
1173
- * `dist/utils/paths.js:80-84`).
1174
- * - an already-absolute input is re-resolved on its own, ignoring the cwd.
1175
- */
1176
- export function resolveHostToolPath(input, baseDir) {
1177
- const normalized = normalizeHostToolPath(input, {
1178
- normalizeUnicodeSpaces: true,
1179
- stripAtPrefix: true,
1180
- });
1181
- const normalizedBaseDir = normalizeHostToolPath(baseDir);
1182
- return path.isAbsolute(normalized)
1183
- ? path.resolve(normalized)
1184
- : path.resolve(normalizedBaseDir, normalized);
1185
- }
1186
- /**
1187
- * Mirror pi's read-path fallback ladder (#1655 item 5).
1188
- *
1189
- * pi does NOT open `resolve(cwd, input.path)`. `resolveReadPath`
1190
- * (`@earendil-works/pi-coding-agent/dist/core/tools/path-utils.js:45-70`,
1191
- * source `src/core/tools/path-utils.ts:52-83`) resolves, and when that path
1192
- * does not exist it silently retries four unicode/spacing variants in this
1193
- * exact order:
1194
- *
1195
- * 1. narrow no-break space before `AM.`/`PM.` (`tryMacOSScreenshotPath`)
1196
- * 2. NFD normalization (`tryNFDVariant`) — macOS stores names decomposed
1197
- * 3. U+0027 → U+2019 (`tryCurlyQuoteVariant`)
1198
- * 4. NFD + curly quote combined
1199
- *
1200
- * Each candidate is used only when it DIFFERS from the resolved path and the
1201
- * file exists; otherwise pi falls back to the resolved path. So the file pi
1202
- * actually read can differ from what a naive `path.resolve` produces, and
1203
- * pi-lens keyed its read guard, LSP touch, and dispatch off the naive form —
1204
- * silently doing nothing for exactly those files.
1205
- *
1206
- * Order matters: it is pi's, so pi-lens picks the same file pi did when more
1207
- * than one variant happens to exist.
1208
- *
1209
- * @param resolvedPath an already-resolved absolute path (the naive form)
1210
- * @param fileExists injectable existence probe; defaults to `existsSync`
1211
- */
1212
- export function resolveHostPathVariants(resolvedPath, fileExists = existsSync) {
1213
- if (fileExists(resolvedPath)) {
1214
- return { path: resolvedPath, unresolved: false, triedVariants: [] };
1215
- }
1216
- const nfd = resolvedPath.normalize("NFD");
1217
- const candidates = [
1218
- {
1219
- variant: "narrow-nbsp",
1220
- candidate: resolvedPath.replace(/ (AM|PM)\./gi, `${NARROW_NO_BREAK_SPACE}$1.`),
1221
- },
1222
- { variant: "nfd", candidate: nfd },
1223
- {
1224
- variant: "curly-quote",
1225
- candidate: resolvedPath.replaceAll("'", RIGHT_SINGLE_QUOTE),
1226
- },
1227
- {
1228
- variant: "nfd-curly-quote",
1229
- candidate: nfd.replaceAll("'", RIGHT_SINGLE_QUOTE),
1230
- },
1231
- ];
1232
- const triedVariants = [];
1233
- for (const { variant, candidate } of candidates) {
1234
- // pi skips a candidate identical to the resolved path, so pi-lens does
1235
- // too — otherwise a no-op "variant" would be reported as a match.
1236
- if (candidate === resolvedPath)
1237
- continue;
1238
- triedVariants.push(variant);
1239
- if (fileExists(candidate)) {
1240
- return { path: candidate, variant, unresolved: false, triedVariants };
1241
- }
1242
- }
1243
- return { path: resolvedPath, unresolved: true, triedVariants };
1244
- }