pi-lens 4.3.0 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (650) hide show
  1. package/CHANGELOG.md +1064 -0
  2. package/README.md +115 -25
  3. package/config/dependency-cruiser-eager-allowlist.json +6 -2
  4. package/dist/chunk-22AVIGXS.js +560 -0
  5. package/dist/chunk-27BBHSZC.js +106 -0
  6. package/dist/chunk-2BEMIWEW.js +51 -0
  7. package/dist/chunk-2EECBNC5.js +89 -0
  8. package/dist/chunk-34U7PLGE.js +23 -0
  9. package/dist/chunk-3I5QFT3N.js +181 -0
  10. package/dist/chunk-3MED75HT.js +37 -0
  11. package/dist/chunk-47D2BO4S.js +87 -0
  12. package/dist/chunk-4RERDEXR.js +348 -0
  13. package/dist/chunk-4W2PLC4J.js +27 -0
  14. package/dist/chunk-52H5B5TC.js +202 -0
  15. package/dist/chunk-54B42YRX.js +27 -0
  16. package/dist/chunk-5RXBNSW2.js +266 -0
  17. package/dist/chunk-5THXD6X5.js +201 -0
  18. package/dist/chunk-5W3R6LFT.js +24 -0
  19. package/dist/chunk-5ZEJHV35.js +341 -0
  20. package/dist/chunk-6P62QVEA.js +118 -0
  21. package/dist/chunk-6QIUX4EP.js +459 -0
  22. package/dist/chunk-6VCD4VKZ.js +78 -0
  23. package/dist/chunk-7F72PXY2.js +326 -0
  24. package/dist/chunk-7FBF7NPR.js +998 -0
  25. package/dist/chunk-7U2IDNXN.js +37 -0
  26. package/dist/chunk-ABBOA7UD.js +157 -0
  27. package/dist/chunk-AHDW3GPE.js +50 -0
  28. package/dist/chunk-AJ3QXTRQ.js +756 -0
  29. package/dist/chunk-AJVLWNFN.js +1610 -0
  30. package/dist/chunk-AJXODZY6.js +112 -0
  31. package/dist/chunk-ARFCXG7S.js +84 -0
  32. package/dist/chunk-AS44CFQ4.js +708 -0
  33. package/dist/chunk-AWE5TMPO.js +38 -0
  34. package/dist/chunk-B3I2UUT4.js +4692 -0
  35. package/dist/chunk-BRJKOBZP.js +331 -0
  36. package/dist/chunk-BW5KFIQC.js +59 -0
  37. package/dist/chunk-C2AUYXQH.js +149 -0
  38. package/dist/chunk-CGGAIP7J.js +29 -0
  39. package/dist/chunk-D6QS3U6M.js +30 -0
  40. package/dist/chunk-DFVMIKMQ.js +349 -0
  41. package/dist/chunk-DIULIWKU.js +169 -0
  42. package/dist/chunk-DQ2NZ7C4.js +2317 -0
  43. package/dist/chunk-E2WFBRHM.js +213 -0
  44. package/dist/chunk-E4MAEDPM.js +50 -0
  45. package/dist/chunk-EOKRQ3SA.js +30 -0
  46. package/dist/chunk-F3AU2TMM.js +2626 -0
  47. package/dist/chunk-F3P7VQ7H.js +170 -0
  48. package/dist/chunk-FC4RP4I2.js +12 -0
  49. package/dist/chunk-FJNKANOS.js +32 -0
  50. package/dist/chunk-GUOGVTCQ.js +146 -0
  51. package/dist/chunk-GVXA6CCE.js +18 -0
  52. package/dist/chunk-HHYAELLJ.js +2334 -0
  53. package/dist/chunk-HMFXILPV.js +83 -0
  54. package/dist/chunk-HVUZXSWK.js +421 -0
  55. package/dist/chunk-HWBZIGMJ.js +62 -0
  56. package/dist/chunk-I3R7GKDR.js +17 -0
  57. package/dist/chunk-I7SPGOWI.js +55 -0
  58. package/dist/chunk-IIXOAGHR.js +38 -0
  59. package/dist/chunk-IVP5KAB7.js +2250 -0
  60. package/dist/chunk-IXEBJSUE.js +22 -0
  61. package/dist/chunk-JOLITGCC.js +27 -0
  62. package/dist/chunk-K6VYLB2U.js +145 -0
  63. package/dist/chunk-LJDODBBZ.js +78 -0
  64. package/dist/chunk-LNAKEO63.js +78 -0
  65. package/dist/chunk-LSMJ6KBE.js +4139 -0
  66. package/dist/chunk-LW6XKDZQ.js +5565 -0
  67. package/dist/chunk-N3YQJI6O.js +899 -0
  68. package/dist/chunk-NABLC7W3.js +148 -0
  69. package/dist/chunk-NKOUFPQF.js +659 -0
  70. package/dist/chunk-NL5GLMFI.js +211 -0
  71. package/dist/chunk-NMABWBZN.js +327 -0
  72. package/dist/chunk-NTHH2J74.js +27 -0
  73. package/dist/chunk-NTUJEGDZ.js +59 -0
  74. package/dist/chunk-NXL4FFQQ.js +47 -0
  75. package/dist/chunk-NXPS2L6U.js +466 -0
  76. package/dist/chunk-O5WILGMI.js +42 -0
  77. package/dist/chunk-O6TQT6RI.js +48 -0
  78. package/dist/chunk-OAXNDZJ7.js +323 -0
  79. package/dist/chunk-ODEEOXPH.js +44 -0
  80. package/dist/chunk-OQGFCZJ4.js +4533 -0
  81. package/dist/chunk-ORTMY4Q4.js +282 -0
  82. package/dist/chunk-OS32RNR2.js +442 -0
  83. package/dist/chunk-PAK6IDME.js +189 -0
  84. package/dist/chunk-PWFHBTSF.js +406 -0
  85. package/dist/chunk-Q5U6FMDI.js +2352 -0
  86. package/dist/chunk-QOZXJ4NA.js +3364 -0
  87. package/dist/chunk-QYF5S6P6.js +45 -0
  88. package/dist/chunk-RIISMHKN.js +36 -0
  89. package/dist/chunk-RPD4ZSB7.js +25 -0
  90. package/dist/chunk-RPD5K5NC.js +79 -0
  91. package/dist/chunk-RUOG5D2C.js +262 -0
  92. package/dist/chunk-T4ERT73Y.js +599 -0
  93. package/dist/chunk-T6D4Q4AQ.js +54 -0
  94. package/dist/chunk-TGPXLY2J.js +391 -0
  95. package/dist/chunk-TLGFBM3F.js +12116 -0
  96. package/dist/chunk-TO6WL3GM.js +88 -0
  97. package/dist/chunk-TT22N74J.js +517 -0
  98. package/dist/chunk-TVN5N6GY.js +21216 -0
  99. package/dist/chunk-U3VGR4VU.js +308 -0
  100. package/dist/chunk-UK3CMEAL.js +1250 -0
  101. package/dist/chunk-UOPVJQ4N.js +103 -0
  102. package/dist/chunk-UXVNL3D7.js +23 -0
  103. package/dist/chunk-UY752QSN.js +45 -0
  104. package/dist/chunk-V5PXFSZK.js +188 -0
  105. package/dist/chunk-V7R425XG.js +398 -0
  106. package/dist/chunk-VCSRSYLF.js +81 -0
  107. package/dist/chunk-VD4JQCQC.js +162 -0
  108. package/dist/chunk-VDBPVIKD.js +62 -0
  109. package/dist/chunk-VHQLPIRJ.js +114 -0
  110. package/dist/chunk-VN2AXLEX.js +5414 -0
  111. package/dist/chunk-VNMT7C64.js +478 -0
  112. package/dist/chunk-WONERJTP.js +309 -0
  113. package/dist/chunk-XECLWQZN.js +85 -0
  114. package/dist/chunk-XIMYZ3JA.js +27 -0
  115. package/dist/chunk-XK4CIILI.js +144 -0
  116. package/dist/chunk-Y4T27COV.js +562 -0
  117. package/dist/chunk-YGVYKNSN.js +1860 -0
  118. package/dist/chunk-YLF3PPT5.js +308 -0
  119. package/dist/chunk-YSNFMJKN.js +14 -0
  120. package/dist/chunk-Z3CCVDUB.js +76 -0
  121. package/dist/chunk-Z54ZSOBZ.js +61 -0
  122. package/dist/chunk-ZAWW3AF6.js +97 -0
  123. package/dist/chunk-ZMOL6RQC.js +167 -0
  124. package/dist/index.js +16456 -8335
  125. package/dist/mcp/analyze-cli.js +186 -232
  126. package/dist/mcp/cli.js +137 -98
  127. package/dist/mcp/server.js +22589 -1573
  128. package/dist/mcp/worker.js +97 -38
  129. package/dist/probes/installer.js +135 -0
  130. package/dist/workers/project-snapshot-persist-worker.js +22 -0
  131. package/dist/workers/review-graph-persist-worker.js +15 -0
  132. package/docs/agent-guide.md +13 -4
  133. package/docs/agent-tools.md +42 -0
  134. package/docs/ast-grep_rules_catalog.md +3 -2
  135. package/docs/configuration.md +20 -1
  136. package/docs/environment-variables.md +52 -2
  137. package/docs/features.md +8 -4
  138. package/docs/globalconfig.md +1 -0
  139. package/docs/io-bridge-v2-migration.md +94 -0
  140. package/docs/lsp-capability-matrix.md +36 -4
  141. package/docs/lsp-idle-eviction.md +124 -0
  142. package/docs/mcp.md +21 -5
  143. package/docs/merge-queue-rollout.md +77 -0
  144. package/docs/pi-lens-fixer.md +156 -88
  145. package/docs/pi-lens-investigator.md +74 -26
  146. package/docs/pi-lens-merge-policy.md +694 -0
  147. package/docs/pi-lens-monitor.md +73 -7
  148. package/docs/pi-lens-retro.md +6 -4
  149. package/docs/pi-lens-reviewer.md +188 -48
  150. package/docs/pi-lens-subagent.md +246 -45
  151. package/docs/pi-lens-warden.md +18 -7
  152. package/docs/public-api-stability.md +55 -0
  153. package/docs/release-qa-baseline.md +84 -17
  154. package/docs/rfcs/3654-unified-io-bridge-v2.md +316 -0
  155. package/docs/servercapabilities.md +53 -49
  156. package/docs/settings.md +1 -0
  157. package/docs/usage.md +5 -1
  158. package/grammars/tree-sitter-bash.wasm +0 -0
  159. package/grammars/tree-sitter-bash.wasm.json +3 -3
  160. package/package.json +19 -7
  161. package/rules/ast-grep-rules/rule-tests/advisory-registry-subset-test.yml +41 -0
  162. package/rules/ast-grep-rules/rules/advisory-registry-subset.yml +50 -0
  163. package/scripts/analyze-pi-lens-logs.mjs +1275 -62
  164. package/scripts/download-grammars.js +5 -0
  165. package/scripts/grammars.lock.json +6 -1
  166. package/scripts/install-selftest.mjs +33 -34
  167. package/scripts/lib/host-provided-deps.mjs +13 -4
  168. package/scripts/lib/web-tree-sitter-dir.mjs +18 -0
  169. package/dist/clients/actionable-warnings-logger.js +0 -35
  170. package/dist/clients/actionable-warnings.js +0 -1610
  171. package/dist/clients/advisory-provenance.js +0 -593
  172. package/dist/clients/agent-behavior-client.js +0 -147
  173. package/dist/clients/agent-nudge.js +0 -325
  174. package/dist/clients/analysed-root.js +0 -1
  175. package/dist/clients/ast-grep-client.js +0 -731
  176. package/dist/clients/ast-grep-rule-manager.js +0 -146
  177. package/dist/clients/ast-grep-tool-logger.js +0 -118
  178. package/dist/clients/ast-grep-types.js +0 -10
  179. package/dist/clients/ast-grep-yaml-synth.js +0 -154
  180. package/dist/clients/atomic-write-staging.js +0 -75
  181. package/dist/clients/atomic-write.js +0 -189
  182. package/dist/clients/availability-probe-flight.js +0 -12
  183. package/dist/clients/bash-file-access.js +0 -1056
  184. package/dist/clients/biome-client.js +0 -357
  185. package/dist/clients/blocker-freshness.js +0 -837
  186. package/dist/clients/blocker-past-eof.js +0 -129
  187. package/dist/clients/bootstrap.js +0 -569
  188. package/dist/clients/bounded-cache.js +0 -190
  189. package/dist/clients/bounded-pid-file-lock.js +0 -202
  190. package/dist/clients/bounded-telemetry.js +0 -295
  191. package/dist/clients/build-identity.js +0 -95
  192. package/dist/clients/bundled-resource-health.js +0 -113
  193. package/dist/clients/bus-events-logger.js +0 -105
  194. package/dist/clients/bus-publish.js +0 -171
  195. package/dist/clients/cache/rule-cache.js +0 -190
  196. package/dist/clients/cache-manager.js +0 -524
  197. package/dist/clients/cache-observability.js +0 -1313
  198. package/dist/clients/call-graph.js +0 -852
  199. package/dist/clients/cargo-manifest.js +0 -422
  200. package/dist/clients/cascade-budget.js +0 -196
  201. package/dist/clients/cascade-format.js +0 -315
  202. package/dist/clients/cascade-logger.js +0 -31
  203. package/dist/clients/cascade-types.js +0 -9
  204. package/dist/clients/child-unref.js +0 -124
  205. package/dist/clients/code-quality-warnings.js +0 -286
  206. package/dist/clients/codebase-model.js +0 -173
  207. package/dist/clients/collateral-test-role.js +0 -40
  208. package/dist/clients/complexity-client.js +0 -462
  209. package/dist/clients/config-core/deny.js +0 -221
  210. package/dist/clients/config-core/index.js +0 -50
  211. package/dist/clients/config-core/merge.js +0 -357
  212. package/dist/clients/config-core/normalize.js +0 -340
  213. package/dist/clients/config-core/process-spec.js +0 -248
  214. package/dist/clients/config-core/provenance.js +0 -164
  215. package/dist/clients/config-core/records.js +0 -218
  216. package/dist/clients/config-core/resolve.js +0 -125
  217. package/dist/clients/config-core/safe-object.js +0 -78
  218. package/dist/clients/config-core/schema.js +0 -165
  219. package/dist/clients/config-diagnostic-codes.js +0 -285
  220. package/dist/clients/config-locations.js +0 -388
  221. package/dist/clients/config-resolve.js +0 -798
  222. package/dist/clients/config-schema.js +0 -251
  223. package/dist/clients/config-warn.js +0 -407
  224. package/dist/clients/console-guard-install.js +0 -26
  225. package/dist/clients/cooperative-budget.js +0 -42
  226. package/dist/clients/dead-code-client.js +0 -510
  227. package/dist/clients/dead-code-logger.js +0 -30
  228. package/dist/clients/deadline-utils.js +0 -280
  229. package/dist/clients/debug-handles.js +0 -211
  230. package/dist/clients/debug-heap.js +0 -174
  231. package/dist/clients/deferred-lsp-work.js +0 -106
  232. package/dist/clients/degradation-ledger.js +0 -426
  233. package/dist/clients/demoted-finding-render.js +0 -169
  234. package/dist/clients/dependency-checker.js +0 -885
  235. package/dist/clients/deps/ast-grep-napi.js +0 -22
  236. package/dist/clients/deps/js-yaml.js +0 -9
  237. package/dist/clients/deps/minimatch.js +0 -4
  238. package/dist/clients/deps/pi-tui.js +0 -14
  239. package/dist/clients/deps/typebox.js +0 -14
  240. package/dist/clients/deps/vscode-jsonrpc.js +0 -1
  241. package/dist/clients/deps/web-tree-sitter.js +0 -24
  242. package/dist/clients/diagnostic-dispositions.js +0 -630
  243. package/dist/clients/diagnostic-line-freshness.js +0 -305
  244. package/dist/clients/diagnostic-logger.js +0 -73
  245. package/dist/clients/diagnostic-tracker.js +0 -105
  246. package/dist/clients/diagnostics-publish.js +0 -230
  247. package/dist/clients/dispatch/ast-grep-catalog.js +0 -65
  248. package/dist/clients/dispatch/auxiliary-lsp.js +0 -343
  249. package/dist/clients/dispatch/collect-later-tier.js +0 -35
  250. package/dist/clients/dispatch/diagnostic-taxonomy.js +0 -66
  251. package/dist/clients/dispatch/dispatcher.js +0 -1269
  252. package/dist/clients/dispatch/fact-provider-types.js +0 -1
  253. package/dist/clients/dispatch/fact-rule-runner.js +0 -17
  254. package/dist/clients/dispatch/fact-runner.js +0 -30
  255. package/dist/clients/dispatch/fact-scheduler.js +0 -68
  256. package/dist/clients/dispatch/fact-store.js +0 -377
  257. package/dist/clients/dispatch/facts/comment-facts.js +0 -22
  258. package/dist/clients/dispatch/facts/file-content.js +0 -19
  259. package/dist/clients/dispatch/facts/function-facts.js +0 -348
  260. package/dist/clients/dispatch/facts/import-facts.js +0 -204
  261. package/dist/clients/dispatch/facts/tree-sitter-facts.js +0 -34
  262. package/dist/clients/dispatch/facts/try-catch-facts.js +0 -119
  263. package/dist/clients/dispatch/finding-policy.js +0 -339
  264. package/dist/clients/dispatch/indent-detect.js +0 -417
  265. package/dist/clients/dispatch/inline-suppressions.js +0 -89
  266. package/dist/clients/dispatch/integration.js +0 -2377
  267. package/dist/clients/dispatch/lazy.js +0 -11
  268. package/dist/clients/dispatch/pending-runner-findings.js +0 -87
  269. package/dist/clients/dispatch/plan.js +0 -278
  270. package/dist/clients/dispatch/priorities.js +0 -20
  271. package/dist/clients/dispatch/rule-id-normalize.js +0 -50
  272. package/dist/clients/dispatch/rule-ignores.js +0 -69
  273. package/dist/clients/dispatch/rule-policy.js +0 -135
  274. package/dist/clients/dispatch/rules/async-noise.js +0 -40
  275. package/dist/clients/dispatch/rules/async-unnecessary-wrapper.js +0 -27
  276. package/dist/clients/dispatch/rules/cors-wildcard.js +0 -54
  277. package/dist/clients/dispatch/rules/error-obscuring.js +0 -32
  278. package/dist/clients/dispatch/rules/error-swallowing.js +0 -31
  279. package/dist/clients/dispatch/rules/framework-call-noise.js +0 -67
  280. package/dist/clients/dispatch/rules/high-complexity.js +0 -72
  281. package/dist/clients/dispatch/rules/high-fan-out.js +0 -74
  282. package/dist/clients/dispatch/rules/high-import-coupling.js +0 -37
  283. package/dist/clients/dispatch/rules/missing-error-propagation.js +0 -62
  284. package/dist/clients/dispatch/rules/no-commented-credentials.js +0 -52
  285. package/dist/clients/dispatch/rules/pass-through-wrappers.js +0 -42
  286. package/dist/clients/dispatch/rules/placeholder-comments.js +0 -40
  287. package/dist/clients/dispatch/rules/unsafe-boundary.js +0 -126
  288. package/dist/clients/dispatch/runner-context.js +0 -52
  289. package/dist/clients/dispatch/runners/actionlint.js +0 -111
  290. package/dist/clients/dispatch/runners/ast-grep-napi.js +0 -1196
  291. package/dist/clients/dispatch/runners/biome-check.js +0 -296
  292. package/dist/clients/dispatch/runners/cpp-check.js +0 -219
  293. package/dist/clients/dispatch/runners/credo.js +0 -82
  294. package/dist/clients/dispatch/runners/cue-vet.js +0 -416
  295. package/dist/clients/dispatch/runners/dart-analyze.js +0 -183
  296. package/dist/clients/dispatch/runners/detekt.js +0 -176
  297. package/dist/clients/dispatch/runners/dotnet-build.js +0 -164
  298. package/dist/clients/dispatch/runners/elixir-check.js +0 -195
  299. package/dist/clients/dispatch/runners/eslint.js +0 -137
  300. package/dist/clients/dispatch/runners/fact-rules.js +0 -29
  301. package/dist/clients/dispatch/runners/fish-indent.js +0 -69
  302. package/dist/clients/dispatch/runners/gleam-check.js +0 -167
  303. package/dist/clients/dispatch/runners/go-vet.js +0 -104
  304. package/dist/clients/dispatch/runners/golangci-lint.js +0 -135
  305. package/dist/clients/dispatch/runners/hadolint.js +0 -91
  306. package/dist/clients/dispatch/runners/helm-lint.js +0 -171
  307. package/dist/clients/dispatch/runners/helm-render.js +0 -940
  308. package/dist/clients/dispatch/runners/htmlhint.js +0 -117
  309. package/dist/clients/dispatch/runners/index.js +0 -108
  310. package/dist/clients/dispatch/runners/javac.js +0 -99
  311. package/dist/clients/dispatch/runners/ktlint.js +0 -110
  312. package/dist/clients/dispatch/runners/lsp.js +0 -379
  313. package/dist/clients/dispatch/runners/markdownlint.js +0 -167
  314. package/dist/clients/dispatch/runners/mypy.js +0 -102
  315. package/dist/clients/dispatch/runners/oxlint.js +0 -414
  316. package/dist/clients/dispatch/runners/php-lint.js +0 -85
  317. package/dist/clients/dispatch/runners/phpstan.js +0 -120
  318. package/dist/clients/dispatch/runners/prisma-validate.js +0 -62
  319. package/dist/clients/dispatch/runners/psscriptanalyzer.js +0 -445
  320. package/dist/clients/dispatch/runners/pyright.js +0 -169
  321. package/dist/clients/dispatch/runners/rubocop.js +0 -99
  322. package/dist/clients/dispatch/runners/ruff.js +0 -88
  323. package/dist/clients/dispatch/runners/rust-clippy.js +0 -256
  324. package/dist/clients/dispatch/runners/shellcheck.js +0 -173
  325. package/dist/clients/dispatch/runners/shfmt.js +0 -102
  326. package/dist/clients/dispatch/runners/spellcheck.js +0 -128
  327. package/dist/clients/dispatch/runners/spotbugs.js +0 -259
  328. package/dist/clients/dispatch/runners/sqlfluff.js +0 -183
  329. package/dist/clients/dispatch/runners/stylelint.js +0 -166
  330. package/dist/clients/dispatch/runners/swiftlint.js +0 -163
  331. package/dist/clients/dispatch/runners/taplo.js +0 -174
  332. package/dist/clients/dispatch/runners/terragrunt.js +0 -168
  333. package/dist/clients/dispatch/runners/tflint.js +0 -125
  334. package/dist/clients/dispatch/runners/tree-sitter.js +0 -643
  335. package/dist/clients/dispatch/runners/trivy-config.js +0 -253
  336. package/dist/clients/dispatch/runners/utils/availability-policy.js +0 -496
  337. package/dist/clients/dispatch/runners/utils/candidate-probe.js +0 -90
  338. package/dist/clients/dispatch/runners/utils/diagnostic-parsers.js +0 -95
  339. package/dist/clients/dispatch/runners/utils/lazy-installer.js +0 -274
  340. package/dist/clients/dispatch/runners/utils/runner-helpers.js +0 -1701
  341. package/dist/clients/dispatch/runners/utils/spawn-outcome.js +0 -101
  342. package/dist/clients/dispatch/runners/utils/tool-failure.js +0 -235
  343. package/dist/clients/dispatch/runners/utils/toolchain-availability.js +0 -169
  344. package/dist/clients/dispatch/runners/utils.js +0 -15
  345. package/dist/clients/dispatch/runners/vale.js +0 -128
  346. package/dist/clients/dispatch/runners/yaml-rule-parser.js +0 -242
  347. package/dist/clients/dispatch/runners/yamllint.js +0 -95
  348. package/dist/clients/dispatch/runners/zig-check.js +0 -79
  349. package/dist/clients/dispatch/suppress-writer.js +0 -79
  350. package/dist/clients/dispatch/tool-profile.js +0 -30
  351. package/dist/clients/dispatch/types.js +0 -21
  352. package/dist/clients/dispatch/utils/format-utils.js +0 -45
  353. package/dist/clients/dispatch/utils/lsp-diagnostics.js +0 -35
  354. package/dist/clients/disposition-logger.js +0 -32
  355. package/dist/clients/disposition-publish.js +0 -124
  356. package/dist/clients/durable-store.js +0 -97
  357. package/dist/clients/effective-config.js +0 -415
  358. package/dist/clients/env-utils.js +0 -83
  359. package/dist/clients/error-class.js +0 -23
  360. package/dist/clients/eval-timestamp.js +0 -17
  361. package/dist/clients/event-loop-hold.js +0 -274
  362. package/dist/clients/event-loop-monitor.js +0 -250
  363. package/dist/clients/extension-log.js +0 -504
  364. package/dist/clients/extension-mode.js +0 -73
  365. package/dist/clients/feature-hints.js +0 -103
  366. package/dist/clients/file-kinds.js +0 -412
  367. package/dist/clients/file-role.js +0 -134
  368. package/dist/clients/file-time.js +0 -160
  369. package/dist/clients/file-utils.js +0 -1032
  370. package/dist/clients/finding-delivery-gate.js +0 -494
  371. package/dist/clients/finding-identity.js +0 -88
  372. package/dist/clients/fix-worklog.js +0 -103
  373. package/dist/clients/format-events-publish.js +0 -343
  374. package/dist/clients/format-service.js +0 -222
  375. package/dist/clients/formatters-lazy.js +0 -9
  376. package/dist/clients/formatters.js +0 -2032
  377. package/dist/clients/freshness-cadence.js +0 -17
  378. package/dist/clients/freshness.js +0 -38
  379. package/dist/clients/generated-artifacts.js +0 -394
  380. package/dist/clients/generation-guard.js +0 -215
  381. package/dist/clients/git-guard.js +0 -1136
  382. package/dist/clients/git-tracked-ignore.js +0 -243
  383. package/dist/clients/gitleaks-client.js +0 -509
  384. package/dist/clients/go-client.js +0 -104
  385. package/dist/clients/govulncheck-client.js +0 -605
  386. package/dist/clients/gradle-ktfmt-style.js +0 -348
  387. package/dist/clients/grammar-source.js +0 -465
  388. package/dist/clients/gzip-stage-write.js +0 -135
  389. package/dist/clients/hashline-anchor.js +0 -424
  390. package/dist/clients/hook-budgets.js +0 -76
  391. package/dist/clients/host-edit-normalize.js +0 -140
  392. package/dist/clients/host-ports.js +0 -39
  393. package/dist/clients/indent-retarget.js +0 -154
  394. package/dist/clients/inline-blocker-dispositions.js +0 -186
  395. package/dist/clients/install-diagnostics.js +0 -156
  396. package/dist/clients/installer/index.js +0 -5416
  397. package/dist/clients/installer/managed-tool-refresh-session.js +0 -58
  398. package/dist/clients/installer/managed-tool-refresh.js +0 -783
  399. package/dist/clients/instance-reaper-state.js +0 -7
  400. package/dist/clients/instance-reaper.js +0 -1137
  401. package/dist/clients/instance-registry-lock.js +0 -145
  402. package/dist/clients/instance-registry.js +0 -773
  403. package/dist/clients/jscpd-client.js +0 -378
  404. package/dist/clients/json-cache-read.js +0 -65
  405. package/dist/clients/knip-client.js +0 -788
  406. package/dist/clients/language-policy.js +0 -252
  407. package/dist/clients/language-profile.js +0 -361
  408. package/dist/clients/language-registry.js +0 -603
  409. package/dist/clients/latency-logger.js +0 -727
  410. package/dist/clients/lazy-import.js +0 -21
  411. package/dist/clients/ledger-bounds.js +0 -54
  412. package/dist/clients/lens-config.js +0 -404
  413. package/dist/clients/lens-engine.js +0 -358
  414. package/dist/clients/lens-events.js +0 -125
  415. package/dist/clients/lens-flag-registry.js +0 -467
  416. package/dist/clients/lens-map.js +0 -1156
  417. package/dist/clients/live-bus-emitter.js +0 -74
  418. package/dist/clients/log-cleanup.js +0 -222
  419. package/dist/clients/lsp/aggregation.js +0 -178
  420. package/dist/clients/lsp/cascade-tier.js +0 -421
  421. package/dist/clients/lsp/client.js +0 -4593
  422. package/dist/clients/lsp/config.js +0 -691
  423. package/dist/clients/lsp/content-limits.js +0 -41
  424. package/dist/clients/lsp/diagnostic-binding.js +0 -225
  425. package/dist/clients/lsp/document-drift.js +0 -467
  426. package/dist/clients/lsp/edits.js +0 -1219
  427. package/dist/clients/lsp/index.js +0 -8305
  428. package/dist/clients/lsp/inferred-project.js +0 -216
  429. package/dist/clients/lsp/jvm-runtime.js +0 -298
  430. package/dist/clients/lsp/language.js +0 -63
  431. package/dist/clients/lsp/launch.js +0 -746
  432. package/dist/clients/lsp/lombok.js +0 -158
  433. package/dist/clients/lsp/path-utils.js +0 -5
  434. package/dist/clients/lsp/pending-aux-coverage.js +0 -251
  435. package/dist/clients/lsp/position-encoding.js +0 -61
  436. package/dist/clients/lsp/ruby-drive-dirs.js +0 -83
  437. package/dist/clients/lsp/server.js +0 -3290
  438. package/dist/clients/lsp/session-roots.js +0 -192
  439. package/dist/clients/lsp/spawn-history.js +0 -17
  440. package/dist/clients/lsp/sync-kind.js +0 -56
  441. package/dist/clients/lsp/tsserver-sync.js +0 -332
  442. package/dist/clients/lsp/wait-policy/capability-snapshot.js +0 -1
  443. package/dist/clients/lsp/wait-policy/classification.js +0 -85
  444. package/dist/clients/lsp/wait-policy/index.js +0 -10
  445. package/dist/clients/lsp/wait-policy/strategies.js +0 -392
  446. package/dist/clients/lsp/watch-queue.js +0 -61
  447. package/dist/clients/lsp/workspace-diagnostics-cache.js +0 -639
  448. package/dist/clients/lsp/workspace-diagnostics-session.js +0 -30
  449. package/dist/clients/lsp/workspace-sweep-hold.js +0 -184
  450. package/dist/clients/lsp-budget.js +0 -186
  451. package/dist/clients/lsp-document-symbols.js +0 -110
  452. package/dist/clients/lsp-lazy.js +0 -9
  453. package/dist/clients/lsp-mutation.js +0 -396
  454. package/dist/clients/lsp-status.js +0 -59
  455. package/dist/clients/map-with-concurrency.js +0 -40
  456. package/dist/clients/mcp/analyze.js +0 -420
  457. package/dist/clients/mcp/host-shim.js +0 -31
  458. package/dist/clients/mcp/ipc.js +0 -523
  459. package/dist/clients/mcp/review.js +0 -197
  460. package/dist/clients/mcp/session.js +0 -412
  461. package/dist/clients/memory-sampler.js +0 -208
  462. package/dist/clients/message-end-attribution.js +0 -30
  463. package/dist/clients/metrics-client.js +0 -107
  464. package/dist/clients/metrics-history.js +0 -327
  465. package/dist/clients/middle-man-analysis.js +0 -263
  466. package/dist/clients/model-provider.js +0 -53
  467. package/dist/clients/module-report-lsp.js +0 -189
  468. package/dist/clients/module-report.js +0 -2150
  469. package/dist/clients/mutating-tool.js +0 -651
  470. package/dist/clients/mutation-attribution.js +0 -368
  471. package/dist/clients/mutation-bridge.js +0 -240
  472. package/dist/clients/ndjson-logger.js +0 -734
  473. package/dist/clients/observed-mutation-sources.js +0 -101
  474. package/dist/clients/observed-mutation.js +0 -1253
  475. package/dist/clients/oldtext-autopatch.js +0 -128
  476. package/dist/clients/opaque-mutation-scan.js +0 -496
  477. package/dist/clients/opengrep-client.js +0 -333
  478. package/dist/clients/opengrep-config.js +0 -76
  479. package/dist/clients/package-manager.js +0 -607
  480. package/dist/clients/package-root.js +0 -38
  481. package/dist/clients/partial-edit-apply.js +0 -439
  482. package/dist/clients/path-attribution-telemetry.js +0 -43
  483. package/dist/clients/path-keyed-map.js +0 -108
  484. package/dist/clients/path-utils.js +0 -1244
  485. package/dist/clients/performance-report.js +0 -331
  486. package/dist/clients/persist-debounce.js +0 -70
  487. package/dist/clients/persistent-reverify.js +0 -271
  488. package/dist/clients/php-cs-fixer-config.js +0 -119
  489. package/dist/clients/pipeline.js +0 -1480
  490. package/dist/clients/post-autofix-notice.js +0 -37
  491. package/dist/clients/print-mode.js +0 -21
  492. package/dist/clients/probe-home-state.js +0 -228
  493. package/dist/clients/process-bridge.js +0 -66
  494. package/dist/clients/process-singletons.js +0 -210
  495. package/dist/clients/process-snapshot.js +0 -68
  496. package/dist/clients/project-changes.js +0 -181
  497. package/dist/clients/project-conventions.js +0 -171
  498. package/dist/clients/project-diagnostics/cache.js +0 -137
  499. package/dist/clients/project-diagnostics/extractors.js +0 -109
  500. package/dist/clients/project-diagnostics/fresh-fetch.js +0 -668
  501. package/dist/clients/project-diagnostics/runner-adapters/call-graph-impact.js +0 -118
  502. package/dist/clients/project-diagnostics/runner-adapters/dead-code.js +0 -47
  503. package/dist/clients/project-diagnostics/runner-adapters/gitleaks.js +0 -40
  504. package/dist/clients/project-diagnostics/runner-adapters/govulncheck.js +0 -34
  505. package/dist/clients/project-diagnostics/runner-adapters/jscpd.js +0 -38
  506. package/dist/clients/project-diagnostics/runner-adapters/knip.js +0 -47
  507. package/dist/clients/project-diagnostics/runner-adapters/madge.js +0 -48
  508. package/dist/clients/project-diagnostics/runner-adapters/opengrep.js +0 -40
  509. package/dist/clients/project-diagnostics/runner-adapters/runner-findings.js +0 -127
  510. package/dist/clients/project-diagnostics/runner-adapters/trivy.js +0 -81
  511. package/dist/clients/project-diagnostics/scanner.js +0 -520
  512. package/dist/clients/project-diagnostics/types.js +0 -1
  513. package/dist/clients/project-lens-config.js +0 -807
  514. package/dist/clients/project-report.js +0 -701
  515. package/dist/clients/project-scale.js +0 -237
  516. package/dist/clients/project-scan-policy.js +0 -11
  517. package/dist/clients/project-snapshot-fingerprint.js +0 -48
  518. package/dist/clients/project-snapshot-persist-worker.js +0 -20
  519. package/dist/clients/project-snapshot.js +0 -1653
  520. package/dist/clients/project-trust.js +0 -181
  521. package/dist/clients/python-environment.js +0 -217
  522. package/dist/clients/python-provenance.js +0 -639
  523. package/dist/clients/quiet-window-config.js +0 -47
  524. package/dist/clients/quiet-window.js +0 -236
  525. package/dist/clients/read-bridge.js +0 -139
  526. package/dist/clients/read-expansion.js +0 -266
  527. package/dist/clients/read-guard-logger.js +0 -251
  528. package/dist/clients/read-guard-tool-lines.js +0 -1306
  529. package/dist/clients/read-guard.js +0 -1561
  530. package/dist/clients/recent-touches.js +0 -240
  531. package/dist/clients/redact/secrets.js +0 -449
  532. package/dist/clients/resource-sampler.js +0 -628
  533. package/dist/clients/reverse-deps.js +0 -270
  534. package/dist/clients/review-graph/builder.js +0 -5126
  535. package/dist/clients/review-graph/format.js +0 -33
  536. package/dist/clients/review-graph/git-identity.js +0 -163
  537. package/dist/clients/review-graph/import-resolvers.js +0 -481
  538. package/dist/clients/review-graph/persist-worker.js +0 -10
  539. package/dist/clients/review-graph/query.js +0 -244
  540. package/dist/clients/review-graph/revision-drift.js +0 -21
  541. package/dist/clients/review-graph/service.js +0 -96
  542. package/dist/clients/review-graph/shared-extraction-ir.js +0 -82
  543. package/dist/clients/review-graph/symbol-id.js +0 -103
  544. package/dist/clients/review-graph/tsconfig-paths.js +0 -299
  545. package/dist/clients/review-graph/types.js +0 -1
  546. package/dist/clients/review-graph/workspace-modules.js +0 -449
  547. package/dist/clients/review-graph-logger.js +0 -81
  548. package/dist/clients/ruff-client.js +0 -179
  549. package/dist/clients/rules-scanner.js +0 -131
  550. package/dist/clients/run-duration.js +0 -55
  551. package/dist/clients/runtime-agent-end.js +0 -746
  552. package/dist/clients/runtime-config.js +0 -77
  553. package/dist/clients/runtime-context.js +0 -211
  554. package/dist/clients/runtime-coordinator.js +0 -1324
  555. package/dist/clients/runtime-session.js +0 -2485
  556. package/dist/clients/runtime-tool-call.js +0 -1295
  557. package/dist/clients/runtime-tool-result.js +0 -2049
  558. package/dist/clients/runtime-turn.js +0 -3767
  559. package/dist/clients/rust-client.js +0 -97
  560. package/dist/clients/safe-spawn.js +0 -2357
  561. package/dist/clients/sanitize.js +0 -286
  562. package/dist/clients/scan-utils.js +0 -25
  563. package/dist/clients/scratch-tree-policy.js +0 -165
  564. package/dist/clients/search-read-registration.js +0 -94
  565. package/dist/clients/secret-findings.js +0 -114
  566. package/dist/clients/security-scan-client.js +0 -352
  567. package/dist/clients/session-event-guard.js +0 -297
  568. package/dist/clients/session-lifecycle.js +0 -484
  569. package/dist/clients/session-start-observability.js +0 -94
  570. package/dist/clients/session-state-store.js +0 -101
  571. package/dist/clients/session-summary.js +0 -15
  572. package/dist/clients/sessionstart-logger.js +0 -27
  573. package/dist/clients/sg-runner.js +0 -876
  574. package/dist/clients/sgconfig.js +0 -347
  575. package/dist/clients/shared-checkout-guard.js +0 -338
  576. package/dist/clients/single-flight.js +0 -158
  577. package/dist/clients/situational-tool-telemetry.js +0 -108
  578. package/dist/clients/skills-resolver.js +0 -141
  579. package/dist/clients/slow-fs.js +0 -138
  580. package/dist/clients/smells-rollup.js +0 -231
  581. package/dist/clients/source-filter.js +0 -596
  582. package/dist/clients/source-walker.js +0 -211
  583. package/dist/clients/spawn-output-cap.js +0 -120
  584. package/dist/clients/spawn-timeout-cooldown.js +0 -110
  585. package/dist/clients/stale-marker.js +0 -17
  586. package/dist/clients/startup-scan.js +0 -400
  587. package/dist/clients/startup-timing.js +0 -130
  588. package/dist/clients/string-utils.js +0 -33
  589. package/dist/clients/subagent-mode.js +0 -111
  590. package/dist/clients/symbol-containment.js +0 -24
  591. package/dist/clients/symbol-types.js +0 -5
  592. package/dist/clients/test-runner-client.js +0 -2507
  593. package/dist/clients/test-runner-delivery.js +0 -287
  594. package/dist/clients/todo-scanner.js +0 -229
  595. package/dist/clients/tool-agreement.js +0 -423
  596. package/dist/clients/tool-config.js +0 -249
  597. package/dist/clients/tool-cwd.js +0 -270
  598. package/dist/clients/tool-definition.js +0 -47
  599. package/dist/clients/tool-event.js +0 -105
  600. package/dist/clients/tool-policy.js +0 -2755
  601. package/dist/clients/tool-probe.js +0 -53
  602. package/dist/clients/tool-render.js +0 -156
  603. package/dist/clients/tool-set-policy.js +0 -118
  604. package/dist/clients/tree-sitter-cache.js +0 -364
  605. package/dist/clients/tree-sitter-client.js +0 -4195
  606. package/dist/clients/tree-sitter-logger.js +0 -100
  607. package/dist/clients/tree-sitter-navigator.js +0 -269
  608. package/dist/clients/tree-sitter-query-loader.js +0 -569
  609. package/dist/clients/tree-sitter-shared.js +0 -158
  610. package/dist/clients/tree-sitter-symbol-extractor.js +0 -1265
  611. package/dist/clients/trivy-client.js +0 -467
  612. package/dist/clients/tui-fit.js +0 -50
  613. package/dist/clients/turn-context.js +0 -52
  614. package/dist/clients/turn-end/lane.js +0 -51
  615. package/dist/clients/turn-end/lanes/govulncheck.js +0 -112
  616. package/dist/clients/turn-end/lanes/secrets.js +0 -204
  617. package/dist/clients/turn-summary-render.js +0 -72
  618. package/dist/clients/turn-summary.js +0 -132
  619. package/dist/clients/typos-config.js +0 -29
  620. package/dist/clients/user-notify.js +0 -51
  621. package/dist/clients/vanished-instance-marker.js +0 -57
  622. package/dist/clients/warm-attach.js +0 -305
  623. package/dist/clients/widget-state.js +0 -1808
  624. package/dist/clients/word-index-logger.js +0 -49
  625. package/dist/clients/word-index-store.js +0 -459
  626. package/dist/clients/word-index.js +0 -2080
  627. package/dist/clients/workspace-topology.js +0 -443
  628. package/dist/clients/write-ordering-guard.js +0 -65
  629. package/dist/clients/zizmor-config.js +0 -340
  630. package/dist/i18n.js +0 -69
  631. package/dist/mcp/build-staleness.js +0 -124
  632. package/dist/scripts/lib/process-scan.mjs +0 -583
  633. package/dist/scripts/lib/skills-predicate.mjs +0 -129
  634. package/dist/scripts/lib/web-tree-sitter-dir.mjs +0 -123
  635. package/dist/tools/activate-tools.js +0 -87
  636. package/dist/tools/ast-grep-outline.js +0 -137
  637. package/dist/tools/ast-grep-replace.js +0 -244
  638. package/dist/tools/ast-grep-search.js +0 -762
  639. package/dist/tools/effective-config.js +0 -90
  640. package/dist/tools/lens-diagnostic-mark.js +0 -375
  641. package/dist/tools/lens-diagnostics.js +0 -2571
  642. package/dist/tools/lsp-diagnostics.js +0 -1745
  643. package/dist/tools/lsp-navigation.js +0 -1367
  644. package/dist/tools/lsp-structured-output.js +0 -294
  645. package/dist/tools/module-report.js +0 -382
  646. package/dist/tools/project-report.js +0 -86
  647. package/dist/tools/render-compact.js +0 -374
  648. package/dist/tools/scan-progress.js +0 -53
  649. package/dist/tools/shared.js +0 -29
  650. package/dist/tools/symbol-search.js +0 -155
@@ -1,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
- }