@animalabs/agent-framework 0.23.2 → 0.24.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 (338) hide show
  1. package/README.md +212 -42
  2. package/dist/src/agent.d.ts +60 -9
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +92 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +14 -12
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/code-execution/engine.d.ts +177 -0
  10. package/dist/src/code-execution/engine.d.ts.map +1 -0
  11. package/dist/src/code-execution/engine.js +494 -0
  12. package/dist/src/code-execution/engine.js.map +1 -0
  13. package/dist/src/code-execution/index.d.ts +5 -10
  14. package/dist/src/code-execution/index.d.ts.map +1 -1
  15. package/dist/src/code-execution/index.js +5 -9
  16. package/dist/src/code-execution/index.js.map +1 -1
  17. package/dist/src/code-execution/isolate/calls.d.ts +114 -0
  18. package/dist/src/code-execution/isolate/calls.d.ts.map +1 -0
  19. package/dist/src/code-execution/isolate/calls.js +145 -0
  20. package/dist/src/code-execution/isolate/calls.js.map +1 -0
  21. package/dist/src/code-execution/isolate/child-entry.d.ts +2 -0
  22. package/dist/src/code-execution/isolate/child-entry.d.ts.map +1 -0
  23. package/dist/src/code-execution/isolate/child-entry.js +5 -0
  24. package/dist/src/code-execution/isolate/child-entry.js.map +1 -0
  25. package/dist/src/code-execution/isolate/child-main.d.ts +26 -0
  26. package/dist/src/code-execution/isolate/child-main.d.ts.map +1 -0
  27. package/dist/src/code-execution/isolate/child-main.js +132 -0
  28. package/dist/src/code-execution/isolate/child-main.js.map +1 -0
  29. package/dist/src/code-execution/isolate/child.d.ts +44 -0
  30. package/dist/src/code-execution/isolate/child.d.ts.map +1 -0
  31. package/dist/src/code-execution/isolate/child.js +315 -0
  32. package/dist/src/code-execution/isolate/child.js.map +1 -0
  33. package/dist/src/code-execution/isolate/frames.d.ts +80 -0
  34. package/dist/src/code-execution/isolate/frames.d.ts.map +1 -0
  35. package/dist/src/code-execution/isolate/frames.js +210 -0
  36. package/dist/src/code-execution/isolate/frames.js.map +1 -0
  37. package/dist/src/code-execution/isolate/output.d.ts +68 -0
  38. package/dist/src/code-execution/isolate/output.d.ts.map +1 -0
  39. package/dist/src/code-execution/isolate/output.js +143 -0
  40. package/dist/src/code-execution/isolate/output.js.map +1 -0
  41. package/dist/src/code-execution/isolate/prelude.d.ts +109 -0
  42. package/dist/src/code-execution/isolate/prelude.d.ts.map +1 -0
  43. package/dist/src/code-execution/isolate/prelude.js +401 -0
  44. package/dist/src/code-execution/isolate/prelude.js.map +1 -0
  45. package/dist/src/code-execution/isolate/protocol.d.ts +151 -0
  46. package/dist/src/code-execution/isolate/protocol.d.ts.map +1 -0
  47. package/dist/src/code-execution/isolate/protocol.js +150 -0
  48. package/dist/src/code-execution/isolate/protocol.js.map +1 -0
  49. package/dist/src/code-execution/isolate/render.d.ts +57 -0
  50. package/dist/src/code-execution/isolate/render.d.ts.map +1 -0
  51. package/dist/src/code-execution/isolate/render.js +214 -0
  52. package/dist/src/code-execution/isolate/render.js.map +1 -0
  53. package/dist/src/code-execution/isolate/runner.d.ts +250 -0
  54. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -0
  55. package/dist/src/code-execution/isolate/runner.js +854 -0
  56. package/dist/src/code-execution/isolate/runner.js.map +1 -0
  57. package/dist/src/code-execution/isolate/scripts.d.ts +175 -0
  58. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -0
  59. package/dist/src/code-execution/isolate/scripts.js +357 -0
  60. package/dist/src/code-execution/isolate/scripts.js.map +1 -0
  61. package/dist/src/code-execution/quickjs.wasm +0 -0
  62. package/dist/src/code-execution/tool-definition.d.ts +93 -43
  63. package/dist/src/code-execution/tool-definition.d.ts.map +1 -1
  64. package/dist/src/code-execution/tool-definition.js +176 -142
  65. package/dist/src/code-execution/tool-definition.js.map +1 -1
  66. package/dist/src/deadline-timer.d.ts +3 -0
  67. package/dist/src/deadline-timer.d.ts.map +1 -1
  68. package/dist/src/deadline-timer.js +1 -1
  69. package/dist/src/deadline-timer.js.map +1 -1
  70. package/dist/src/framework.d.ts +1027 -440
  71. package/dist/src/framework.d.ts.map +1 -1
  72. package/dist/src/framework.js +4093 -2238
  73. package/dist/src/framework.js.map +1 -1
  74. package/dist/src/gate/event-gate.d.ts +28 -1
  75. package/dist/src/gate/event-gate.d.ts.map +1 -1
  76. package/dist/src/gate/event-gate.js +88 -10
  77. package/dist/src/gate/event-gate.js.map +1 -1
  78. package/dist/src/held-writes.d.ts +16 -3
  79. package/dist/src/held-writes.d.ts.map +1 -1
  80. package/dist/src/held-writes.js +16 -3
  81. package/dist/src/held-writes.js.map +1 -1
  82. package/dist/src/index.d.ts +6 -6
  83. package/dist/src/index.d.ts.map +1 -1
  84. package/dist/src/index.js +9 -8
  85. package/dist/src/index.js.map +1 -1
  86. package/dist/src/mcpl/channel-registry.d.ts +471 -75
  87. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  88. package/dist/src/mcpl/channel-registry.js +1673 -337
  89. package/dist/src/mcpl/channel-registry.js.map +1 -1
  90. package/dist/src/mcpl/conversation-router.d.ts +12 -7
  91. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  92. package/dist/src/mcpl/conversation-router.js +12 -7
  93. package/dist/src/mcpl/conversation-router.js.map +1 -1
  94. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  95. package/dist/src/mcpl/convert-block.js +9 -5
  96. package/dist/src/mcpl/convert-block.js.map +1 -1
  97. package/dist/src/mcpl/inbound-source.d.ts +50 -0
  98. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  99. package/dist/src/mcpl/inbound-source.js +130 -0
  100. package/dist/src/mcpl/inbound-source.js.map +1 -1
  101. package/dist/src/mcpl/index.d.ts +2 -1
  102. package/dist/src/mcpl/index.d.ts.map +1 -1
  103. package/dist/src/mcpl/index.js +3 -2
  104. package/dist/src/mcpl/index.js.map +1 -1
  105. package/dist/src/mcpl/log-value.d.ts +6 -0
  106. package/dist/src/mcpl/log-value.d.ts.map +1 -1
  107. package/dist/src/mcpl/log-value.js +8 -0
  108. package/dist/src/mcpl/log-value.js.map +1 -1
  109. package/dist/src/mcpl/modern-connection.d.ts +15 -6
  110. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  111. package/dist/src/mcpl/modern-connection.js +168 -53
  112. package/dist/src/mcpl/modern-connection.js.map +1 -1
  113. package/dist/src/mcpl/prose-grammar.d.ts +4 -5
  114. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  115. package/dist/src/mcpl/prose-grammar.js +4 -5
  116. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  117. package/dist/src/mcpl/protocol-family.d.ts +97 -10
  118. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  119. package/dist/src/mcpl/protocol-family.js +173 -16
  120. package/dist/src/mcpl/protocol-family.js.map +1 -1
  121. package/dist/src/mcpl/push-coalescer.d.ts +132 -14
  122. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  123. package/dist/src/mcpl/push-coalescer.js +227 -50
  124. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  125. package/dist/src/mcpl/push-handler.d.ts +6 -1
  126. package/dist/src/mcpl/push-handler.d.ts.map +1 -1
  127. package/dist/src/mcpl/push-handler.js +4 -1
  128. package/dist/src/mcpl/push-handler.js.map +1 -1
  129. package/dist/src/mcpl/reference-fetcher.d.ts +2 -2
  130. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  131. package/dist/src/mcpl/reference-fetcher.js +4 -4
  132. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  133. package/dist/src/mcpl/server-connection.d.ts +50 -21
  134. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  135. package/dist/src/mcpl/server-connection.js +158 -74
  136. package/dist/src/mcpl/server-connection.js.map +1 -1
  137. package/dist/src/mcpl/tags.d.ts +24 -0
  138. package/dist/src/mcpl/tags.d.ts.map +1 -1
  139. package/dist/src/mcpl/tags.js +33 -0
  140. package/dist/src/mcpl/tags.js.map +1 -1
  141. package/dist/src/mcpl/tool-result-normalize.d.ts +2 -1
  142. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  143. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  144. package/dist/src/mcpl/transport.d.ts +55 -7
  145. package/dist/src/mcpl/transport.d.ts.map +1 -1
  146. package/dist/src/mcpl/transport.js +163 -34
  147. package/dist/src/mcpl/transport.js.map +1 -1
  148. package/dist/src/mcpl/types.d.ts +22 -6
  149. package/dist/src/mcpl/types.d.ts.map +1 -1
  150. package/dist/src/mcpl/types.js.map +1 -1
  151. package/dist/src/module-registry.d.ts +4 -2
  152. package/dist/src/module-registry.d.ts.map +1 -1
  153. package/dist/src/module-registry.js +100 -13
  154. package/dist/src/module-registry.js.map +1 -1
  155. package/dist/src/modules/discord/index.d.ts.map +1 -1
  156. package/dist/src/modules/discord/index.js +8 -13
  157. package/dist/src/modules/discord/index.js.map +1 -1
  158. package/dist/src/modules/history/index.d.ts.map +1 -1
  159. package/dist/src/modules/history/index.js +27 -4
  160. package/dist/src/modules/history/index.js.map +1 -1
  161. package/dist/src/modules/history/semantic.d.ts +15 -3
  162. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  163. package/dist/src/modules/history/semantic.js +27 -5
  164. package/dist/src/modules/history/semantic.js.map +1 -1
  165. package/dist/src/modules/index.d.ts +1 -1
  166. package/dist/src/modules/index.d.ts.map +1 -1
  167. package/dist/src/modules/index.js.map +1 -1
  168. package/dist/src/modules/unknown-tool.d.ts +4 -1
  169. package/dist/src/modules/unknown-tool.d.ts.map +1 -1
  170. package/dist/src/modules/unknown-tool.js +4 -1
  171. package/dist/src/modules/unknown-tool.js.map +1 -1
  172. package/dist/src/modules/workspace/branch-intent.d.ts +3 -1
  173. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  174. package/dist/src/modules/workspace/branch-intent.js +3 -1
  175. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  176. package/dist/src/modules/workspace/edit-plan.d.ts +265 -0
  177. package/dist/src/modules/workspace/edit-plan.d.ts.map +1 -0
  178. package/dist/src/modules/workspace/edit-plan.js +794 -0
  179. package/dist/src/modules/workspace/edit-plan.js.map +1 -0
  180. package/dist/src/modules/workspace/edit-text.d.ts +117 -0
  181. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -0
  182. package/dist/src/modules/workspace/edit-text.js +584 -0
  183. package/dist/src/modules/workspace/edit-text.js.map +1 -0
  184. package/dist/src/modules/workspace/effects.d.ts +5 -4
  185. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/effects.js +78 -25
  187. package/dist/src/modules/workspace/effects.js.map +1 -1
  188. package/dist/src/modules/workspace/index.d.ts +484 -61
  189. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/index.js +2173 -645
  191. package/dist/src/modules/workspace/index.js.map +1 -1
  192. package/dist/src/modules/workspace/observe.d.ts +213 -38
  193. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/observe.js +416 -36
  195. package/dist/src/modules/workspace/observe.js.map +1 -1
  196. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/reconcile.js +15 -8
  198. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  199. package/dist/src/modules/workspace/text.d.ts +32 -0
  200. package/dist/src/modules/workspace/text.d.ts.map +1 -0
  201. package/dist/src/modules/workspace/text.js +67 -0
  202. package/dist/src/modules/workspace/text.js.map +1 -0
  203. package/dist/src/modules/workspace/types.d.ts +210 -6
  204. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  205. package/dist/src/modules/workspace/types.js.map +1 -1
  206. package/dist/src/modules/workspace/watcher.d.ts +3 -2
  207. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  208. package/dist/src/modules/workspace/watcher.js.map +1 -1
  209. package/dist/src/operator-log.d.ts +52 -24
  210. package/dist/src/operator-log.d.ts.map +1 -1
  211. package/dist/src/operator-log.js.map +1 -1
  212. package/dist/src/recovery/discord-awareness-outbox.d.ts +496 -72
  213. package/dist/src/recovery/discord-awareness-outbox.d.ts.map +1 -1
  214. package/dist/src/recovery/discord-awareness-outbox.js +1196 -289
  215. package/dist/src/recovery/discord-awareness-outbox.js.map +1 -1
  216. package/dist/src/recovery/offline-branch.d.ts +26 -1
  217. package/dist/src/recovery/offline-branch.d.ts.map +1 -1
  218. package/dist/src/recovery/offline-branch.js +164 -39
  219. package/dist/src/recovery/offline-branch.js.map +1 -1
  220. package/dist/src/recovery/recover-cli.js +23 -4
  221. package/dist/src/recovery/recover-cli.js.map +1 -1
  222. package/dist/src/send-tools.d.ts +11 -0
  223. package/dist/src/send-tools.d.ts.map +1 -0
  224. package/dist/src/send-tools.js +11 -0
  225. package/dist/src/send-tools.js.map +1 -0
  226. package/dist/src/tool-result-guard.d.ts +4 -3
  227. package/dist/src/tool-result-guard.d.ts.map +1 -1
  228. package/dist/src/tool-result-guard.js +5 -4
  229. package/dist/src/tool-result-guard.js.map +1 -1
  230. package/dist/src/tool-result-history.d.ts +1 -1
  231. package/dist/src/tool-result-history.js +1 -1
  232. package/dist/src/tune-out/coordinator.d.ts +21 -0
  233. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  234. package/dist/src/tune-out/coordinator.js +139 -37
  235. package/dist/src/tune-out/coordinator.js.map +1 -1
  236. package/dist/src/types/agent.d.ts +4 -0
  237. package/dist/src/types/agent.d.ts.map +1 -1
  238. package/dist/src/types/events.d.ts +62 -9
  239. package/dist/src/types/events.d.ts.map +1 -1
  240. package/dist/src/types/framework.d.ts +62 -58
  241. package/dist/src/types/framework.d.ts.map +1 -1
  242. package/dist/src/types/module.d.ts +40 -8
  243. package/dist/src/types/module.d.ts.map +1 -1
  244. package/dist/src/types/trace.d.ts +111 -10
  245. package/dist/src/types/trace.d.ts.map +1 -1
  246. package/dist/src/usage/types.d.ts +16 -0
  247. package/dist/src/usage/types.d.ts.map +1 -1
  248. package/dist/src/usage/usage-tracker.d.ts +14 -1
  249. package/dist/src/usage/usage-tracker.d.ts.map +1 -1
  250. package/dist/src/usage/usage-tracker.js +81 -41
  251. package/dist/src/usage/usage-tracker.js.map +1 -1
  252. package/package.json +6 -5
  253. package/src/agent.ts +93 -12
  254. package/src/api/server.ts +14 -12
  255. package/src/code-execution/engine.ts +595 -0
  256. package/src/code-execution/index.ts +5 -17
  257. package/src/code-execution/isolate/calls.ts +199 -0
  258. package/src/code-execution/isolate/child-entry.ts +5 -0
  259. package/src/code-execution/isolate/child-main.ts +132 -0
  260. package/src/code-execution/isolate/child.ts +327 -0
  261. package/src/code-execution/isolate/frames.ts +209 -0
  262. package/src/code-execution/isolate/output.ts +161 -0
  263. package/src/code-execution/isolate/prelude.ts +409 -0
  264. package/src/code-execution/isolate/protocol.ts +240 -0
  265. package/src/code-execution/isolate/render.ts +230 -0
  266. package/src/code-execution/isolate/runner.ts +911 -0
  267. package/src/code-execution/isolate/scripts.ts +479 -0
  268. package/src/code-execution/quickjs.wasm +0 -0
  269. package/src/code-execution/quickjs.wasm.LICENSES +149 -0
  270. package/src/code-execution/tool-definition.ts +236 -167
  271. package/src/deadline-timer.ts +1 -1
  272. package/src/framework.ts +4468 -2418
  273. package/src/gate/event-gate.ts +78 -11
  274. package/src/held-writes.ts +17 -3
  275. package/src/index.ts +30 -22
  276. package/src/mcpl/channel-registry.ts +1825 -368
  277. package/src/mcpl/conversation-router.ts +12 -7
  278. package/src/mcpl/convert-block.ts +9 -5
  279. package/src/mcpl/inbound-source.ts +131 -0
  280. package/src/mcpl/index.ts +4 -2
  281. package/src/mcpl/log-value.ts +9 -0
  282. package/src/mcpl/modern-connection.ts +162 -53
  283. package/src/mcpl/prose-grammar.ts +4 -5
  284. package/src/mcpl/protocol-family.ts +199 -21
  285. package/src/mcpl/push-coalescer.ts +306 -62
  286. package/src/mcpl/push-handler.ts +10 -2
  287. package/src/mcpl/reference-fetcher.ts +4 -4
  288. package/src/mcpl/server-connection.ts +180 -79
  289. package/src/mcpl/tags.ts +36 -0
  290. package/src/mcpl/tool-result-normalize.ts +2 -1
  291. package/src/mcpl/transport.ts +162 -33
  292. package/src/mcpl/types.ts +23 -6
  293. package/src/module-registry.ts +92 -13
  294. package/src/modules/discord/index.ts +8 -13
  295. package/src/modules/history/index.ts +25 -3
  296. package/src/modules/history/semantic.ts +29 -6
  297. package/src/modules/index.ts +1 -0
  298. package/src/modules/unknown-tool.ts +4 -1
  299. package/src/modules/workspace/branch-intent.ts +3 -1
  300. package/src/modules/workspace/edit-plan.ts +954 -0
  301. package/src/modules/workspace/edit-text.ts +618 -0
  302. package/src/modules/workspace/effects.ts +70 -19
  303. package/src/modules/workspace/index.ts +2247 -642
  304. package/src/modules/workspace/observe.ts +465 -56
  305. package/src/modules/workspace/reconcile.ts +14 -7
  306. package/src/modules/workspace/text.ts +68 -0
  307. package/src/modules/workspace/types.ts +211 -6
  308. package/src/modules/workspace/watcher.ts +3 -2
  309. package/src/operator-log.ts +47 -19
  310. package/src/recovery/discord-awareness-outbox.ts +1500 -356
  311. package/src/recovery/offline-branch.ts +189 -55
  312. package/src/recovery/recover-cli.ts +25 -4
  313. package/src/send-tools.ts +11 -0
  314. package/src/tool-result-guard.ts +6 -5
  315. package/src/tool-result-history.ts +1 -1
  316. package/src/tune-out/coordinator.ts +163 -42
  317. package/src/types/agent.ts +4 -0
  318. package/src/types/events.ts +62 -9
  319. package/src/types/framework.ts +62 -58
  320. package/src/types/module.ts +40 -8
  321. package/src/types/trace.ts +121 -15
  322. package/src/usage/types.ts +16 -0
  323. package/src/usage/usage-tracker.ts +86 -43
  324. package/dist/src/code-execution/py-runner.d.ts +0 -191
  325. package/dist/src/code-execution/py-runner.d.ts.map +0 -1
  326. package/dist/src/code-execution/py-runner.js +0 -655
  327. package/dist/src/code-execution/py-runner.js.map +0 -1
  328. package/dist/src/code-execution/runtime-py.d.ts +0 -50
  329. package/dist/src/code-execution/runtime-py.d.ts.map +0 -1
  330. package/dist/src/code-execution/runtime-py.js +0 -677
  331. package/dist/src/code-execution/runtime-py.js.map +0 -1
  332. package/dist/src/mcpl/prose-stream-router.d.ts +0 -88
  333. package/dist/src/mcpl/prose-stream-router.d.ts.map +0 -1
  334. package/dist/src/mcpl/prose-stream-router.js +0 -291
  335. package/dist/src/mcpl/prose-stream-router.js.map +0 -1
  336. package/src/code-execution/py-runner.ts +0 -791
  337. package/src/code-execution/runtime-py.ts +0 -677
  338. package/src/mcpl/prose-stream-router.ts +0 -268
@@ -32,8 +32,12 @@
32
32
  * a directory is checked by its canonical path and identity, not held open:
33
33
  * a parent replaced concurrently in the window between a check and its use
34
34
  * (or replaced and restored around it) can still redirect a listing or
35
- * lookup there. File contents are bound regardless: a file is read only
36
- * through a descriptor bound to its path first.
35
+ * lookup there. File contents are bound more tightly: a file is read only
36
+ * through a descriptor whose identity matches the file its path named, as a
37
+ * check before the open found it (hashAt) or as the path's realpath after
38
+ * the open finds it (openContainedFile), though a link put back after that
39
+ * realpath, leading to the opened file itself, still passes the check
40
+ * (sameLevels).
37
41
  */
38
42
  import { createHash } from 'node:crypto';
39
43
  import { constants as fsConstants } from 'node:fs';
@@ -154,7 +158,7 @@ export async function linkLeads(root, rootReal, link) {
154
158
  if (!contained(rootReal, leads))
155
159
  return { kind: 'outside' };
156
160
  const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
157
- return { kind: 'inside', target: relative(rootReal, leads).split(sep).join('/'), directory };
161
+ return { kind: 'inside', target: await diskSpelling(rootReal, relative(rootReal, leads).split(sep).join('/')), directory };
158
162
  }
159
163
  /**
160
164
  * The nested mount whose directory holds `target` (mount-relative), the
@@ -166,6 +170,113 @@ export function nestedHolder(target, nested) {
166
170
  .filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
167
171
  .sort((a, b) => b.dir.length - a.dir.length)[0];
168
172
  }
173
+ /**
174
+ * Whether a link's refusal names `holder`, the nested mount whose directory
175
+ * holds the link's target. The target's path is one of that mount's own, so
176
+ * its include decides: the refusal names a directory, or a file the include
177
+ * covers. A refusal's code follows the same test (linkReadError's
178
+ * `other_mount`), so the code says no more than the message.
179
+ */
180
+ export function namesHolder(leads, holder) {
181
+ return namesNestedPlace(leads.target.slice(holder.dir.length + 1), holder.include, leads.directory);
182
+ }
183
+ /**
184
+ * Whether a refusal names a place in a nested mount's directory, `rest`
185
+ * beneath that mount's root, as a place of that mount's: a directory, or a
186
+ * file its include covers. The place is one of that mount's own paths, so
187
+ * its include decides, and a refusal's code follows the same test, so the
188
+ * code says no more than the message. Anywhere else the place is no mount's:
189
+ * the outer mount passes over the nested directory, and the nested mount's
190
+ * include leaves it out. So a link the outer mount doesn't follow is refused
191
+ * as one it doesn't follow (namesHolder), and a path that reaches the place
192
+ * through one it follows is refused as one that leaves the mount, in the
193
+ * words each such refusal already has (nestedAtReal's callers).
194
+ */
195
+ export function namesNestedPlace(rest, include, directory) {
196
+ return directory || insideMount(rest, include);
197
+ }
198
+ /**
199
+ * Whether a path of a mount is one its passes decide and its listings may
200
+ * name: inside its include, and, in a nested mount's directory, a place
201
+ * that mount names (namesNestedPlace), its directory included. History the
202
+ * mount took in before another was nested there, which that mount's
203
+ * include leaves out, is no mount's, so no pass decides it, and no listing
204
+ * names it or a directory only it implies.
205
+ */
206
+ export function withinIncludes(view, path) {
207
+ if (!insideMount(path, view.include))
208
+ return false;
209
+ const holder = nestedHolder(path, view.nested ?? []);
210
+ return holder === undefined || path === holder.dir || namesNestedPlace(path.slice(holder.dir.length + 1), holder.include, false);
211
+ }
212
+ /**
213
+ * The path a mount's include judges for `given`, one of the mount's paths,
214
+ * which landed at `real`, its canonical path, inside the mount and in no
215
+ * nested mount's directory. Where realpath couldn't resolve it, as for a loop
216
+ * or a directory this process can't search, `real` is the path as
217
+ * path.resolve reads it, which this judges as named, so a caller refuses such
218
+ * a path first, as landingRefusal does (realBeneath's `unresolved`). The
219
+ * include is the operator's statement of which files' content the workspace
220
+ * holds, and a link is a path, not a grant of content, so where a followed
221
+ * path lands is judged as where it's named, as a nested mount's place is
222
+ * (namesNestedPlace). A path that meets no link lands where it's named, and
223
+ * is judged as named. realpath can spell such a path otherwise only as a disk
224
+ * that folds names does, with disk's stored names on macOS and Windows where
225
+ * a path keeps a workspace entry's spelling, so where the spellings differ
226
+ * only so, the path is judged as named where it reaches the landing itself,
227
+ * level by level (sameLevels). Otherwise the landing is judged, as disk
228
+ * spells it, since realpath keeps the spelling a link's target gives, which
229
+ * on a disk that folds names need not be disk's (diskSpelling).
230
+ */
231
+ export async function judgedAt(rootReal, real, given) {
232
+ const landed = relative(rootReal, real).split(sep).join('/');
233
+ if (landed === given)
234
+ return given;
235
+ if (foldedLoosely(landed) === foldedLoosely(given) && await sameLevels(rootReal, given, landed))
236
+ return given;
237
+ return diskSpelling(rootReal, landed);
238
+ }
239
+ /**
240
+ * Whether `given` reaches `landed`, two spellings of a path beneath
241
+ * `rootReal`, level by level: each directory on the way and the file itself
242
+ * are, as `lstat` finds them, one and the same on both sides, so the path as
243
+ * named passes through no link of its own that the landing doesn't. Beneath
244
+ * the last level both sides have, where neither has the next, a file a write
245
+ * would create there lands where it's named. A level only one side has, or
246
+ * one this can't ask, is no such path. It asks each level once, in turn: on a
247
+ * disk that folds names a level both spellings share is one entry, so a link
248
+ * disk puts there after the realpath meets both alike, and a name removed
249
+ * while this asks and put back after reads as a file a write would create.
250
+ * Where a read follows, its check of the opened file catches a link to
251
+ * another file, but not one back to the opened file itself; that race, like
252
+ * one where the spellings match, isn't closed here.
253
+ */
254
+ async function sameLevels(rootReal, given, landed) {
255
+ const named = given.split('/');
256
+ const landing = landed.split('/');
257
+ const level = (segments, i) => lstat(join(rootReal, ...segments.slice(0, i)), { bigint: true })
258
+ .then((st) => st, (err) => (errno(err) === 'ENOENT' || errno(err) === 'ENOTDIR' ? null : undefined));
259
+ for (let i = 1; i <= named.length; i++) {
260
+ const [a, b] = await Promise.all([level(named, i), level(landing, i)]);
261
+ if (a === undefined || b === undefined)
262
+ return false;
263
+ if (a === null && b === null)
264
+ return true;
265
+ if (a === null || b === null || !sameFile(a, b))
266
+ return false;
267
+ }
268
+ return true;
269
+ }
270
+ /**
271
+ * Whether a mount's own include covers the file one of its paths, `given`,
272
+ * lands on at `real` (judgedAt): a file it leaves out there is no file of
273
+ * this mount's, and a path that reaches it through a link is refused as one
274
+ * that leaves the mount, in the words each such refusal already has
275
+ * (nestedAtReal's callers). A mount without an include covers every file.
276
+ */
277
+ export async function coversLanding(view, rootReal, real, given) {
278
+ return view.include === undefined || insideMount(await judgedAt(rootReal, real, given), view.include);
279
+ }
169
280
  /**
170
281
  * Why a path is refused for what linkOnTheWay found, given where a link
171
282
  * leads (linkLeads). For a link, the link is the subject, with where it
@@ -178,9 +289,15 @@ export function nestedHolder(target, nested) {
178
289
  * path to use there in place of the link, and offers no setting, since
179
290
  * following the link would only reach into another mount's directory. For a
180
291
  * directory the walk couldn't ask, which one, and the answer it got. Paths
181
- * are spelled as the tools take them, from the mount's name.
292
+ * are spelled as the tools take them, from the mount's name. On a mount with
293
+ * an include, an inside target is named only where a tool could name it: a
294
+ * file the include covers, or a directory, which a listing names whatever
295
+ * lies beneath it. Anywhere else the refusal says only that the link isn't
296
+ * followed, so no tool names what lies outside the include. A link the
297
+ * include doesn't cover is outside the mount itself, so its refusal tells
298
+ * nothing of where it leads (shownLeads gives null for it).
182
299
  */
183
- export function linkWords(name, found, leads, nested = []) {
300
+ export function linkWords(name, found, leads, nested = [], include) {
184
301
  const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
185
302
  if (!('link' in found))
186
303
  return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
@@ -191,32 +308,52 @@ export function linkWords(name, found, leads, nested = []) {
191
308
  return `${link} leading outside the mount, which this mount does not follow`;
192
309
  const holder = nestedHolder(leads.target, nested);
193
310
  if (holder) {
311
+ if (!namesHolder(leads, holder))
312
+ return `${link}, which this mount does not follow`;
194
313
  const rest = leads.target.slice(holder.dir.length + 1);
195
314
  return `${link} to ${spell(leads.target)}, which this mount does not follow; it lies in mount "${holder.name}", ` +
196
315
  `nested in mount "${name}": use "${rest ? `${holder.name}/${rest}` : holder.name}" in place of ${spell(found.link)}`;
197
316
  }
198
- const setting = leads.directory ? "; the mount's host can set followSymlinks: true" : '';
317
+ if (!leads.directory && !insideMount(leads.target, include))
318
+ return `${link}, which this mount does not follow`;
319
+ // Following it serves only what the include covers where it leads (judgedAt).
320
+ const setting = !leads.directory ? '' : include === undefined ? "; the mount's host can set followSymlinks: true"
321
+ : "; the mount's host can set followSymlinks: true, for what the include covers where it leads";
199
322
  return `${link} to ${spell(leads.target)}, which this mount does not follow${setting}`;
200
323
  }
201
- /** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads). */
324
+ /**
325
+ * Where a link linkOnTheWay found leads, as its refusal may tell it
326
+ * (linkLeads), or null where the refusal tells nothing of it: where the walk
327
+ * found a directory it couldn't ask rather than a link, and where the mount's
328
+ * include doesn't cover the link, which is then outside the mount, so its
329
+ * refusal shows only that a link stands on the path. A refusal's code
330
+ * follows the same answer (linkReadError), so the code says no more than the
331
+ * message.
332
+ */
333
+ export async function shownLeads(root, rootReal, found, include) {
334
+ return 'link' in found && insideMount(found.link, include) ? linkLeads(root, rootReal, found.link) : null;
335
+ }
336
+ /** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
202
337
  export async function linkReason(view, rootReal, found) {
203
- return linkWords(view.name, found, 'link' in found ? await linkLeads(view.root, rootReal, found.link) : null, view.nested);
338
+ return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
204
339
  }
205
340
  /**
206
341
  * The nested mount whose root holds `real`, a canonical path, the innermost,
207
- * with the path to use there; undefined when none does. A path the lexical
208
- * checks pass (inOtherMount, the tools' holderOf) reaches another mount's
209
- * directory this way through a symlink the mount follows, so each check that
210
- * resolves a path canonically asks this too.
342
+ * with the path to use there, the path beneath its root (`rest`) and its
343
+ * include; undefined when none does. A path the lexical checks pass
344
+ * (inOtherMount, the tools' holderOf) reaches another mount's directory this
345
+ * way through a symlink the mount follows, so each check that resolves a
346
+ * path canonically asks this too, and names the mount only where
347
+ * namesNestedPlace says the place is that mount's.
211
348
  */
212
349
  export function nestedAtReal(real, nested = []) {
213
350
  const holder = nested
214
- .flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real }] : []))
351
+ .flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real, include: n.include }] : []))
215
352
  .sort((a, b) => b.real.length - a.real.length)[0];
216
353
  if (!holder)
217
354
  return undefined;
218
355
  const rest = relative(holder.real, real).split(sep).join('/');
219
- return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name };
356
+ return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, rest, include: holder.include };
220
357
  }
221
358
  /**
222
359
  * Why a mount-relative path that resolves into a nested mount's directory is
@@ -345,6 +482,20 @@ function entryName(paths, prefix, segment) {
345
482
  }
346
483
  return found;
347
484
  }
485
+ /**
486
+ * Whether a path's segments begin with a directory's: exactly, or, with
487
+ * `loosely`, with each segment compared as loosely as listedName compares
488
+ * names (foldedLoosely), as a disk that folds names may compare them. That
489
+ * errs toward the directory: on a disk that tells some of those spellings
490
+ * apart, such as case, or `ß` from `ss`, a distinct directory spelled one of
491
+ * them matches too, and resolveAbsolutePath, the one caller that compares
492
+ * loosely, gives null for a path beneath it.
493
+ */
494
+ export function beginsWithDir(segments, dir, loosely) {
495
+ if (segments.length < dir.length)
496
+ return false;
497
+ return dir.every((d, i) => segments[i] === d || (loosely && foldedLoosely(segments[i]) === foldedLoosely(d)));
498
+ }
348
499
  /**
349
500
  * The name `dir` lists for `segment`: `segment` itself, unless disk folds it
350
501
  * there and lists it otherwise. `dir` is listed only where a variant of
@@ -657,10 +808,14 @@ async function locateDir(view, rootReal, rel) {
657
808
  }
658
809
  else {
659
810
  // Through a link this mount follows, a directory can be a nested
660
- // mount's, which the mount neither reads nor writes.
811
+ // mount's, which the mount neither reads nor writes. A place there that
812
+ // mount's include leaves out is no mount's, and holds nothing beneath it,
813
+ // as at any file (namesNestedPlace).
661
814
  const holder = nestedAtReal(real, view.nested);
662
- if (holder)
663
- return { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) };
815
+ if (holder) {
816
+ const directory = holder.include !== undefined && await stat(real).then((info) => info.isDirectory(), () => false);
817
+ return namesNestedPlace(holder.rest, holder.include, directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
818
+ }
664
819
  }
665
820
  const checked = await checkedAt(view, rel, real);
666
821
  if (checked) {
@@ -895,10 +1050,11 @@ export async function filesystemTrustsCtime(root) {
895
1050
  // Paths and files
896
1051
  // ===========================================================================
897
1052
  /**
898
- * Where a path's parent directory canonically lies: inside the mount, gone
899
- * (absence is then proven separately), or somewhere a no-follow open would
900
- * still reach through — O_NOFOLLOW guards only the final component — which
901
- * on a mount that doesn't follow symlinks is any symlink on the way.
1053
+ * Where a path's parent directory canonically lies: inside the mount, with
1054
+ * that real path, gone (absence is then proven separately), or somewhere a
1055
+ * no-follow open would still reach through — O_NOFOLLOW guards only the
1056
+ * final component — which on a mount that doesn't follow symlinks is any
1057
+ * symlink on the way.
902
1058
  */
903
1059
  async function parentPlacement(view, rootReal, relativePath) {
904
1060
  try {
@@ -909,7 +1065,12 @@ async function parentPlacement(view, rootReal, relativePath) {
909
1065
  if (!contained(rootReal, parent))
910
1066
  return { reason: OUTSIDE_PARENT };
911
1067
  const holder = nestedAtReal(parent, view.nested);
912
- return holder ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'inside';
1068
+ if (!holder)
1069
+ return { inside: parent };
1070
+ // A place there that the nested mount's include leaves out is no mount's,
1071
+ // and holds nothing beneath it, as beneath any file (namesNestedPlace).
1072
+ const directory = holder.include !== undefined && await stat(parent).then((info) => info.isDirectory(), () => false);
1073
+ return namesNestedPlace(holder.rest, holder.include, directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
913
1074
  }
914
1075
  catch (err) {
915
1076
  const code = errno(err);
@@ -934,7 +1095,7 @@ export async function observePath(view, rootReal, relativePath) {
934
1095
  const placement = await parentPlacement(view, rootReal, relativePath);
935
1096
  if (placement === 'gone')
936
1097
  return absence(view, rootReal, relativePath);
937
- if (placement !== 'inside')
1098
+ if ('reason' in placement)
938
1099
  return { kind: 'unobserved', reason: placement.reason };
939
1100
  let info;
940
1101
  try {
@@ -966,11 +1127,20 @@ export async function observePath(view, rootReal, relativePath) {
966
1127
  if (!contained(rootReal, real))
967
1128
  return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
968
1129
  const holder = nestedAtReal(real, view.nested);
969
- if (holder)
970
- return { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder) };
1130
+ // A place a nested mount's include leaves out is no mount's, so the link
1131
+ // leaves this one (namesNestedPlace).
1132
+ if (holder) {
1133
+ return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : 'a symlink that leaves the mount' };
1134
+ }
971
1135
  if (!target.isFile()) {
972
1136
  return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file' };
973
1137
  }
1138
+ // A file this mount's own include leaves out where the link lands is no
1139
+ // file of this mount's (coversLanding). It's judged here, and not only
1140
+ // where the file is opened, since a pass takes a copy whose fingerprint
1141
+ // the stat vouches for without opening it.
1142
+ if (!(await coversLanding(view, rootReal, real, relativePath)))
1143
+ return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
974
1144
  info = target;
975
1145
  }
976
1146
  else if (info.isDirectory()) {
@@ -979,6 +1149,10 @@ export async function observePath(view, rootReal, relativePath) {
979
1149
  else if (!info.isFile()) {
980
1150
  return { kind: 'unobserved', reason: 'not a regular file' };
981
1151
  }
1152
+ else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
1153
+ // So is one reached through a link among its directories.
1154
+ return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1155
+ }
982
1156
  return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
983
1157
  }
984
1158
  /**
@@ -1032,12 +1206,17 @@ async function openContained(view, rootReal, relativePath) {
1032
1206
  return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
1033
1207
  if (!info.isFile())
1034
1208
  return { kind: 'unobserved', reason: info.isDirectory() ? 'a directory' : 'not a regular file' };
1035
- if (!contained(rootReal, real)) {
1036
- return { kind: 'unobserved', reason: view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT };
1037
- }
1209
+ const leaves = view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT;
1210
+ if (!contained(rootReal, real))
1211
+ return { kind: 'unobserved', reason: leaves };
1038
1212
  const holder = nestedAtReal(real, view.nested);
1213
+ // A place a nested mount's include leaves out is no mount's. Reached
1214
+ // through a link this mount doesn't follow, the link is what's refused.
1039
1215
  if (holder)
1040
- return { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder) };
1216
+ return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
1217
+ // So is a file this mount's own include leaves out (coversLanding).
1218
+ if (!(await coversLanding(view, rootReal, real, relativePath)))
1219
+ return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
1041
1220
  const named = await stat(real, { bigint: true }).catch(() => null);
1042
1221
  if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
1043
1222
  return { kind: 'unobserved', reason: 'the file changed while it was read' };
@@ -1167,6 +1346,48 @@ export async function readPath(view, rootReal, relativePath, limit) {
1167
1346
  await handle.close();
1168
1347
  }
1169
1348
  }
1349
+ /**
1350
+ * A mount-relative file's bytes, through the same located descriptor as
1351
+ * readPath, for a direct mount's tools, which keep nothing: at most `limit`
1352
+ * of them, and none hashed. A file over `limit` at its fstat has only its
1353
+ * head read, and one that grew past it since is read no further than it.
1354
+ * What the open refuses gets the reason a listing gives it, such as a link
1355
+ * to a directory, except a regular file it couldn't open, which keeps the
1356
+ * open's.
1357
+ */
1358
+ export async function readBytes(view, rootReal, relativePath, limit) {
1359
+ const opened = await openContained(view, rootReal, relativePath);
1360
+ if (!('handle' in opened)) {
1361
+ // A walk takes a link to a directory as an entry, where the mount follows
1362
+ // links, and the open may refuse it as a directory or, where it can't
1363
+ // be opened, by its errno: either way it's named as a listing names it
1364
+ // (observePath).
1365
+ if (opened.kind === 'unobserved') {
1366
+ const seen = await observePath(view, rootReal, relativePath);
1367
+ return seen.kind === 'file' ? opened : seen;
1368
+ }
1369
+ return opened;
1370
+ }
1371
+ const { handle, info } = opened;
1372
+ try {
1373
+ const size = Number(info.size);
1374
+ if (size > limit) {
1375
+ const head = Buffer.alloc(Math.min(HEAD_BYTES, size));
1376
+ const { bytesRead } = await handle.read(head, 0, head.length, 0);
1377
+ return { kind: 'bytes', size, bytes: null, head: head.subarray(0, bytesRead) };
1378
+ }
1379
+ const { bytes, more } = await readPrefix(handle, limit, size);
1380
+ if (more)
1381
+ return { kind: 'bytes', size: Math.max(Number((await handle.stat({ bigint: true })).size), bytes.length + 1), bytes: null, head: bytes.subarray(0, HEAD_BYTES) };
1382
+ return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
1383
+ }
1384
+ catch (err) {
1385
+ return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
1386
+ }
1387
+ finally {
1388
+ await handle.close();
1389
+ }
1390
+ }
1170
1391
  /**
1171
1392
  * The first bytes of a mount-relative file and its size, through the same
1172
1393
  * located descriptor as readPath — for listing a file nothing tracks, which
@@ -1271,6 +1492,83 @@ export function coveredByIgnore(relativePath, patterns) {
1271
1492
  }
1272
1493
  return false;
1273
1494
  }
1495
+ /** Why a walk passes over a file or link the mount's `include` doesn't cover, and a push leaves one alone. */
1496
+ export const OUTSIDE_INCLUDE = "outside the mount's include";
1497
+ /**
1498
+ * The segments of a path after its mount's name, as the workspace reads them
1499
+ * everywhere: split at `/`, and at `\` too where that's the platform's
1500
+ * separator, with no empty or `.` segment. A `..` segment stays, for the
1501
+ * parse to take back the segment before it or refuse as a climb.
1502
+ */
1503
+ export function pathSegments(path, platformSep = sep) {
1504
+ return (platformSep === '\\' ? path.replaceAll('\\', '/') : path).split('/').filter((s) => s !== '' && s !== '.');
1505
+ }
1506
+ /**
1507
+ * Whether a mount-relative path is spelled as the workspace's parsing spells
1508
+ * one: segments between single `/`, none empty, `.` or `..`, with no `\`
1509
+ * where that's the separator, or '' for the root. A walk and the watcher name
1510
+ * paths this way. An earlier version stored a path as it was written, so the
1511
+ * store can hold a key such as `a//b.txt` or `c/`, which no spelling names.
1512
+ */
1513
+ export function parsedSpelling(relativePath, platformSep = sep) {
1514
+ if (relativePath === '')
1515
+ return true;
1516
+ const segments = pathSegments(relativePath, platformSep);
1517
+ return !segments.includes('..') && segments.join('/') === relativePath;
1518
+ }
1519
+ /**
1520
+ * Whether a mount-relative path is inside its mount: spelled as parsing spells
1521
+ * one (parsedSpelling), and covered by the mount's `include` when it has one.
1522
+ * The mount's root, '', is inside. A store key no spelling names is outside,
1523
+ * as an entry the include doesn't cover is: nothing lists, counts, syncs or
1524
+ * writes it, and history keeps it as it was.
1525
+ */
1526
+ export function insideMount(relativePath, include) {
1527
+ if (!parsedSpelling(relativePath))
1528
+ return false;
1529
+ if (include === undefined || relativePath === '')
1530
+ return true;
1531
+ return coveredByInclude(relativePath, include);
1532
+ }
1533
+ /**
1534
+ * Whether the include's matcher can read `pattern`: a name or an `*.ext` glob,
1535
+ * which covers a file by its own name, or a `dir/**` glob, with no empty, `.`
1536
+ * or `..` segment, since those name no entry a path reaches. Where `\` is the
1537
+ * platform's separator, as on Windows, a pattern holding one can't name a
1538
+ * file either, since every path is read with `\` as a separator
1539
+ * (pathSegments). A pattern the matcher can't read would match nothing, and
1540
+ * take every file out of the mount unsaid, so the module refuses it.
1541
+ */
1542
+ export function includePatternReadable(pattern, platformSep = sep) {
1543
+ const segment = (s) => s.length > 0 && s !== '.' && s !== '..' && !(platformSep === '\\' && s.includes('\\'));
1544
+ const name = (s) => segment(s) && !s.includes('/') &&
1545
+ (!s.includes('*') || (s.startsWith('*.') && s.length > 2 && !s.slice(1).includes('*')));
1546
+ return typeof pattern === 'string' &&
1547
+ (pattern.endsWith('/**') ? pattern.length > 3 && pattern.slice(0, -3).split('/').every((s) => segment(s) && !s.includes('*')) : name(pattern));
1548
+ }
1549
+ /**
1550
+ * Whether a mount's include covers a mount-relative path. Its patterns take
1551
+ * `ignore`'s forms, read for files: a name or `*.ext` pattern covers a path by
1552
+ * its own last segment, at any depth, and `dir/**` covers what lies beneath
1553
+ * `dir`, and not a file named `dir`. Unlike `ignore`, a pattern that matches
1554
+ * a directory's name covers nothing beneath it: each file is covered by its
1555
+ * own name, so `gate.json` covers `x/gate.json` and not `x/gate.json/run.sh`,
1556
+ * and a subtree is named with `dir/**`. A directory is no file, so it needs
1557
+ * no covering: where one is a scope, refusesAsOutside (index.ts) decides.
1558
+ */
1559
+ function coveredByInclude(relativePath, include) {
1560
+ const name = relativePath.slice(relativePath.lastIndexOf('/') + 1);
1561
+ for (const pattern of include) {
1562
+ if (pattern.endsWith('/**')) {
1563
+ if (relativePath.startsWith(pattern.slice(0, -2)))
1564
+ return true;
1565
+ }
1566
+ else if (pattern === name || (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))) {
1567
+ return true;
1568
+ }
1569
+ }
1570
+ return false;
1571
+ }
1274
1572
  /**
1275
1573
  * The ignore patterns a walk that names `scope` reads beneath it: a pattern
1276
1574
  * covering the scope itself, or a directory above it, doesn't apply beneath
@@ -1297,9 +1595,17 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
1297
1595
  export const IGNORED = 'ignored by the mount';
1298
1596
  /** The walk's reason for an entry `EDITOR_FILE` names. */
1299
1597
  const EDITOR_FILE_REASON = "an editor's swap or backup file";
1598
+ /**
1599
+ * The walk's reason for a link it doesn't enter. On a mount with an include,
1600
+ * a link the include's names don't cover is passed over with it, by design,
1601
+ * since the walk takes no name the include leaves out as a file: on a mount
1602
+ * that follows links, one that leads to a directory, and on one that
1603
+ * doesn't, every one.
1604
+ */
1605
+ const LINK_NOT_WALKED = 'a symlink, not walked';
1300
1606
  /** Whether a reason the walk gave an entry is one it passes over by design. */
1301
1607
  export function passedOverByDesign(why) {
1302
- return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT;
1608
+ return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
1303
1609
  }
1304
1610
  /** Why a pass passes over a directory that another mount nested in this one holds, and what lies beneath it. */
1305
1611
  export const OTHER_MOUNT = "another mount's directory";
@@ -1423,7 +1729,7 @@ async function checkChild(view, rootReal, parent, rel) {
1423
1729
  */
1424
1730
  export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}) {
1425
1731
  const walk = {
1426
- files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), complete: new Set(), missing: new Set(),
1732
+ files: new Set(), seen: new Set(), othersTaken: new Set(), dirs: new Set(), others: new Map(), otherFiles: new Set(), complete: new Set(), missing: new Set(),
1427
1733
  incomplete: [],
1428
1734
  };
1429
1735
  // A walk that isn't named passes over a scope the mount passes over by
@@ -1516,6 +1822,9 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1516
1822
  for (const [path] of walk.others)
1517
1823
  if (beneath(path) && !walk.othersTaken.has(path))
1518
1824
  walk.others.delete(path);
1825
+ for (const path of walk.otherFiles)
1826
+ if (!walk.others.has(path))
1827
+ walk.otherFiles.delete(path);
1519
1828
  for (let i = walk.incomplete.length - 1; i >= 0; i--) {
1520
1829
  const region = walk.incomplete[i];
1521
1830
  if ((region.kind === 'symlink' || region.kind === 'ignored') && beneath(region.path))
@@ -1569,18 +1878,56 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1569
1878
  return null;
1570
1879
  }
1571
1880
  entries.sort((a, b) => compareNames(a.name, b.name));
1881
+ // A scope named through a link is listed from where the link leads, so
1882
+ // a file in it is judged where it lies as well as by its path, as
1883
+ // coversLanding judges where a path lands (judgedAt, once for the
1884
+ // listing).
1885
+ const judgedDir = view.include === undefined ? rel : await judgedAt(rootReal, dir.real, rel);
1572
1886
  const leaves = [];
1573
1887
  const subdirs = [];
1574
1888
  const others = [];
1889
+ const otherFiles = [];
1575
1890
  const regions = [];
1576
1891
  for (const entry of entries) {
1577
1892
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
1578
- const passedOver = passedOverBecause(childRel, entry.name, ignore, view.others);
1893
+ // Another mount's directory is passed over by its real path too, so a
1894
+ // walk that reaches one through a link to a directory above it lists
1895
+ // none of that mount's names (nestedAtReal).
1896
+ const passedOver = passedOverBecause(childRel, entry.name, ignore, view.others)
1897
+ ?? (entry.isDirectory() && nestedAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
1579
1898
  if (passedOver !== undefined) {
1580
1899
  others.push([childRel, passedOver]);
1581
1900
  regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
1582
1901
  continue;
1583
1902
  }
1903
+ // A file or link the mount's include doesn't cover is outside the
1904
+ // mount, a named scope's too. A directory is walked, since a name or an
1905
+ // extension glob in include can match beneath it. Anything but a link
1906
+ // (a regular file, a FIFO, a socket or a device) is noted as a file,
1907
+ // since nothing can lie beneath it.
1908
+ if (!entry.isDirectory() && (!insideMount(childRel, view.include) || (judgedDir !== rel && !insideMount(judgedDir === '' ? entry.name : `${judgedDir}/${entry.name}`, view.include)))) {
1909
+ // The walk enters no link. A link gets the reason a mount without an
1910
+ // include gives it, a symlink the walk doesn't enter, where that's
1911
+ // what matters about it. On a mount that follows links, that's a link
1912
+ // that leads to a directory: a name or an extension glob can cover
1913
+ // what lies beneath it, as beneath a directory, and what's read
1914
+ // through it is judged by its path and where it lands, so its own
1915
+ // name doesn't put what lies there outside. On one that doesn't, it's
1916
+ // every link, since nothing reads through one there, and such a mount
1917
+ // resolves none to say where it leads. The link's name is no covered
1918
+ // file's, so the walk passes it over as it does one, rather than
1919
+ // taking it.
1920
+ if (entry.isSymbolicLink() && (!view.followSymlinks || await stat(dir.at(entry.name)).then((info) => info.isDirectory(), () => false))) {
1921
+ others.push([childRel, LINK_NOT_WALKED]);
1922
+ regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'ignored' });
1923
+ continue;
1924
+ }
1925
+ others.push([childRel, OUTSIDE_INCLUDE]);
1926
+ if (!entry.isSymbolicLink())
1927
+ otherFiles.push(childRel);
1928
+ regions.push({ path: childRel, reason: OUTSIDE_INCLUDE, kind: 'ignored' });
1929
+ continue;
1930
+ }
1584
1931
  if (entry.isDirectory()) {
1585
1932
  subdirs.push(childRel);
1586
1933
  }
@@ -1592,7 +1939,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1592
1939
  leaves.push([childRel, false]);
1593
1940
  others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
1594
1941
  if (symlink)
1595
- regions.push({ path: childRel, reason: 'a symlink, not walked', kind: 'symlink' });
1942
+ regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
1596
1943
  }
1597
1944
  }
1598
1945
  // A listing of a directory that was moved or replaced meanwhile says
@@ -1608,6 +1955,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1608
1955
  walk.dirs.add(d);
1609
1956
  for (const [p, why] of others)
1610
1957
  walk.others.set(p, why);
1958
+ for (const p of otherFiles)
1959
+ walk.otherFiles.add(p);
1611
1960
  walk.incomplete.push(...regions);
1612
1961
  walk.complete.add(rel);
1613
1962
  const listing = { leaves, subdirs, ...dir.identity() };
@@ -1621,6 +1970,22 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1621
1970
  const at = subtreeAgainst(dir, after);
1622
1971
  return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
1623
1972
  };
1973
+ // Each nested root by which directory it is, read once, when the walk
1974
+ // first descends: a directory the walk reaches through a link spelled
1975
+ // otherwise than disk spells it, as a disk that folds names lets one, has
1976
+ // a real path the check by path doesn't know, and is still another
1977
+ // mount's directory.
1978
+ let nestedRoots;
1979
+ const isNestedRoot = async (dir) => {
1980
+ if (!view.nested?.length)
1981
+ return false;
1982
+ nestedRoots ??= (await Promise.all([...new Set(view.nested.flatMap((n) => (n.real === undefined ? [] : [n.real])))].map(async (real) => {
1983
+ const at = await stat(real, { bigint: true }).catch(() => null);
1984
+ return at === null ? [] : [{ dev: String(at.dev), ino: String(at.ino) }];
1985
+ }))).flat();
1986
+ const { dev, ino } = dir.identity();
1987
+ return nestedRoots.some((r) => r.dev === dev && r.ino === ino);
1988
+ };
1624
1989
  const visit = async (dir, stretch) => {
1625
1990
  visited.add(dir.rel);
1626
1991
  const listing = await list(dir);
@@ -1659,6 +2024,16 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1659
2024
  // list() drops it from one that isn't.
1660
2025
  walk.missing.delete(sub);
1661
2026
  if (child.kind === 'checked') {
2027
+ if (await isNestedRoot(child.dir)) {
2028
+ // Passed over as another mount's directory, as the listing passes
2029
+ // over one it knows by path.
2030
+ walk.dirs.delete(sub);
2031
+ if (!walk.others.has(sub)) {
2032
+ walk.others.set(sub, OTHER_MOUNT);
2033
+ walk.incomplete.push({ path: sub, reason: OTHER_MOUNT, kind: 'ignored' });
2034
+ }
2035
+ continue;
2036
+ }
1662
2037
  await visit(child.dir, stretch);
1663
2038
  continue;
1664
2039
  }
@@ -1714,9 +2089,12 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1714
2089
  }
1715
2090
  /**
1716
2091
  * Whether the walk proves nothing exists at `path`: some ancestor directory
1717
- * is missing, or a completely listed ancestor holds no entry that could lead
1718
- * to it. Ancestors outside the walked scope are passed through, so a walk of
1719
- * `a/b` decides paths beneath `a/b` and nothing else.
2092
+ * is missing, a completely listed ancestor holds no entry that could lead
2093
+ * to it, or an ancestor is a file the mount's include doesn't cover (anything
2094
+ * but a directory or a link), which the walk passed over and which holds
2095
+ * nothing beneath it.
2096
+ * Ancestors outside the walked scope are passed through, so a walk of `a/b`
2097
+ * decides paths beneath `a/b` and nothing else.
1720
2098
  */
1721
2099
  export function provenAbsent(walk, path) {
1722
2100
  const parts = path.split('/');
@@ -1726,6 +2104,8 @@ export function provenAbsent(walk, path) {
1726
2104
  return true;
1727
2105
  const child = dir ? `${dir}/${parts[i]}` : parts[i];
1728
2106
  if (walk.complete.has(dir)) {
2107
+ if (i < parts.length - 1 && walk.otherFiles.has(child))
2108
+ return true; // a file: nothing beneath it exists
1729
2109
  if (walk.others.has(child) || walk.seen.has(child))
1730
2110
  return false; // present, but unproven
1731
2111
  if (i === parts.length - 1)