@animalabs/agent-framework 0.27.0 → 0.29.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 (344) hide show
  1. package/README.md +145 -41
  2. package/dist/src/agent.d.ts +29 -2
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +73 -12
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/mcp-server-core.d.ts.map +1 -1
  7. package/dist/src/api/mcp-server-core.js +3 -12
  8. package/dist/src/api/mcp-server-core.js.map +1 -1
  9. package/dist/src/api/server.d.ts.map +1 -1
  10. package/dist/src/api/server.js +11 -5
  11. package/dist/src/api/server.js.map +1 -1
  12. package/dist/src/auto-continue.d.ts +5 -2
  13. package/dist/src/auto-continue.d.ts.map +1 -1
  14. package/dist/src/auto-continue.js +9 -5
  15. package/dist/src/auto-continue.js.map +1 -1
  16. package/dist/src/code-execution/isolate/runner.d.ts +7 -5
  17. package/dist/src/code-execution/isolate/runner.d.ts.map +1 -1
  18. package/dist/src/code-execution/isolate/runner.js +7 -5
  19. package/dist/src/code-execution/isolate/runner.js.map +1 -1
  20. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  21. package/dist/src/code-execution/isolate/scripts.js +3 -2
  22. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  23. package/dist/src/cut-off-round.d.ts +24 -0
  24. package/dist/src/cut-off-round.d.ts.map +1 -0
  25. package/dist/src/cut-off-round.js +87 -0
  26. package/dist/src/cut-off-round.js.map +1 -0
  27. package/dist/src/framework.d.ts +468 -125
  28. package/dist/src/framework.d.ts.map +1 -1
  29. package/dist/src/framework.js +3651 -1111
  30. package/dist/src/framework.js.map +1 -1
  31. package/dist/src/gate/event-gate.d.ts +133 -13
  32. package/dist/src/gate/event-gate.d.ts.map +1 -1
  33. package/dist/src/gate/event-gate.js +654 -59
  34. package/dist/src/gate/event-gate.js.map +1 -1
  35. package/dist/src/gate/index.d.ts +3 -2
  36. package/dist/src/gate/index.d.ts.map +1 -1
  37. package/dist/src/gate/index.js +2 -1
  38. package/dist/src/gate/index.js.map +1 -1
  39. package/dist/src/gate/reply-judge.d.ts +116 -0
  40. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  41. package/dist/src/gate/reply-judge.js +467 -0
  42. package/dist/src/gate/reply-judge.js.map +1 -0
  43. package/dist/src/gate/types.d.ts +173 -2
  44. package/dist/src/gate/types.d.ts.map +1 -1
  45. package/dist/src/heartbeat/heartbeat.d.ts +202 -0
  46. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  47. package/dist/src/heartbeat/heartbeat.js +778 -0
  48. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  50. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  51. package/dist/src/heartbeat/reminder-time.js +107 -0
  52. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  53. package/dist/src/held-writes.d.ts +3 -3
  54. package/dist/src/held-writes.js +3 -3
  55. package/dist/src/host-presentation.d.ts +170 -0
  56. package/dist/src/host-presentation.d.ts.map +1 -0
  57. package/dist/src/host-presentation.js +561 -0
  58. package/dist/src/host-presentation.js.map +1 -0
  59. package/dist/src/index.d.ts +5 -1
  60. package/dist/src/index.d.ts.map +1 -1
  61. package/dist/src/index.js +3 -1
  62. package/dist/src/index.js.map +1 -1
  63. package/dist/src/line-reader.d.ts +4 -0
  64. package/dist/src/line-reader.d.ts.map +1 -0
  65. package/dist/src/line-reader.js +59 -0
  66. package/dist/src/line-reader.js.map +1 -0
  67. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  68. package/dist/src/mcpl/capability-mask.js +3 -2
  69. package/dist/src/mcpl/capability-mask.js.map +1 -1
  70. package/dist/src/mcpl/channel-registry.d.ts +34 -6
  71. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  72. package/dist/src/mcpl/channel-registry.js +133 -54
  73. package/dist/src/mcpl/channel-registry.js.map +1 -1
  74. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  75. package/dist/src/mcpl/conversation-router.js +2 -1
  76. package/dist/src/mcpl/conversation-router.js.map +1 -1
  77. package/dist/src/mcpl/convert-block.d.ts +1 -0
  78. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  79. package/dist/src/mcpl/convert-block.js +16 -4
  80. package/dist/src/mcpl/convert-block.js.map +1 -1
  81. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  82. package/dist/src/mcpl/hook-orchestrator.js +4 -3
  83. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  84. package/dist/src/mcpl/inbound-source.d.ts +29 -11
  85. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  86. package/dist/src/mcpl/inbound-source.js +43 -70
  87. package/dist/src/mcpl/inbound-source.js.map +1 -1
  88. package/dist/src/mcpl/modern-connection.d.ts.map +1 -1
  89. package/dist/src/mcpl/modern-connection.js +11 -7
  90. package/dist/src/mcpl/modern-connection.js.map +1 -1
  91. package/dist/src/mcpl/protocol-family.d.ts +3 -2
  92. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  93. package/dist/src/mcpl/protocol-family.js +25 -3
  94. package/dist/src/mcpl/protocol-family.js.map +1 -1
  95. package/dist/src/mcpl/push-handler.js +2 -2
  96. package/dist/src/mcpl/push-handler.js.map +1 -1
  97. package/dist/src/mcpl/references.d.ts +12 -40
  98. package/dist/src/mcpl/references.d.ts.map +1 -1
  99. package/dist/src/mcpl/references.js +29 -23
  100. package/dist/src/mcpl/references.js.map +1 -1
  101. package/dist/src/mcpl/server-connection.d.ts +18 -8
  102. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  103. package/dist/src/mcpl/server-connection.js +70 -30
  104. package/dist/src/mcpl/server-connection.js.map +1 -1
  105. package/dist/src/mcpl/skip-reply-wake.d.ts +25 -0
  106. package/dist/src/mcpl/skip-reply-wake.d.ts.map +1 -1
  107. package/dist/src/mcpl/skip-reply-wake.js +25 -0
  108. package/dist/src/mcpl/skip-reply-wake.js.map +1 -1
  109. package/dist/src/mcpl/tags.d.ts +9 -2
  110. package/dist/src/mcpl/tags.d.ts.map +1 -1
  111. package/dist/src/mcpl/tags.js +12 -2
  112. package/dist/src/mcpl/tags.js.map +1 -1
  113. package/dist/src/mcpl/tool-presence.d.ts +66 -14
  114. package/dist/src/mcpl/tool-presence.d.ts.map +1 -1
  115. package/dist/src/mcpl/tool-presence.js +150 -80
  116. package/dist/src/mcpl/tool-presence.js.map +1 -1
  117. package/dist/src/mcpl/tool-signature.d.ts +10 -3
  118. package/dist/src/mcpl/tool-signature.d.ts.map +1 -1
  119. package/dist/src/mcpl/tool-signature.js +60 -30
  120. package/dist/src/mcpl/tool-signature.js.map +1 -1
  121. package/dist/src/mcpl/toolset-host.d.ts +88 -55
  122. package/dist/src/mcpl/toolset-host.d.ts.map +1 -1
  123. package/dist/src/mcpl/toolset-host.js +220 -111
  124. package/dist/src/mcpl/toolset-host.js.map +1 -1
  125. package/dist/src/mcpl/toolsets-module.d.ts +27 -35
  126. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -1
  127. package/dist/src/mcpl/toolsets-module.js +313 -91
  128. package/dist/src/mcpl/toolsets-module.js.map +1 -1
  129. package/dist/src/mcpl/transport.d.ts +1 -1
  130. package/dist/src/mcpl/transport.d.ts.map +1 -1
  131. package/dist/src/mcpl/transport.js +16 -16
  132. package/dist/src/mcpl/transport.js.map +1 -1
  133. package/dist/src/mcpl/types.d.ts +31 -13
  134. package/dist/src/mcpl/types.d.ts.map +1 -1
  135. package/dist/src/mcpl/types.js.map +1 -1
  136. package/dist/src/module-registry.d.ts +10 -8
  137. package/dist/src/module-registry.d.ts.map +1 -1
  138. package/dist/src/module-registry.js +15 -9
  139. package/dist/src/module-registry.js.map +1 -1
  140. package/dist/src/modules/api/index.d.ts.map +1 -1
  141. package/dist/src/modules/api/index.js +4 -2
  142. package/dist/src/modules/api/index.js.map +1 -1
  143. package/dist/src/modules/discord/index.d.ts.map +1 -1
  144. package/dist/src/modules/discord/index.js +33 -25
  145. package/dist/src/modules/discord/index.js.map +1 -1
  146. package/dist/src/modules/history/index.d.ts.map +1 -1
  147. package/dist/src/modules/history/index.js +30 -17
  148. package/dist/src/modules/history/index.js.map +1 -1
  149. package/dist/src/modules/sender-name.d.ts +7 -0
  150. package/dist/src/modules/sender-name.d.ts.map +1 -0
  151. package/dist/src/modules/sender-name.js +10 -0
  152. package/dist/src/modules/sender-name.js.map +1 -0
  153. package/dist/src/modules/workspace/answers.d.ts +26 -0
  154. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  155. package/dist/src/modules/workspace/answers.js +138 -0
  156. package/dist/src/modules/workspace/answers.js.map +1 -0
  157. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  158. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  159. package/dist/src/modules/workspace/branch-intent.js +6 -2
  160. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  161. package/dist/src/modules/workspace/direct-write.d.ts +3 -0
  162. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -1
  163. package/dist/src/modules/workspace/direct-write.js +59 -24
  164. package/dist/src/modules/workspace/direct-write.js.map +1 -1
  165. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  166. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  168. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  169. package/dist/src/modules/workspace/edit-text.d.ts +2 -1
  170. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  171. package/dist/src/modules/workspace/edit-text.js +4 -2
  172. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  173. package/dist/src/modules/workspace/effects.d.ts +4 -2
  174. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/effects.js +67 -50
  176. package/dist/src/modules/workspace/effects.js.map +1 -1
  177. package/dist/src/modules/workspace/index.d.ts +63 -51
  178. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/index.js +980 -547
  180. package/dist/src/modules/workspace/index.js.map +1 -1
  181. package/dist/src/modules/workspace/observe.d.ts +125 -82
  182. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/observe.js +465 -241
  184. package/dist/src/modules/workspace/observe.js.map +1 -1
  185. package/dist/src/modules/workspace/read-page.d.ts.map +1 -1
  186. package/dist/src/modules/workspace/read-page.js +7 -1
  187. package/dist/src/modules/workspace/read-page.js.map +1 -1
  188. package/dist/src/modules/workspace/reconcile.d.ts +7 -1
  189. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  190. package/dist/src/modules/workspace/reconcile.js +156 -50
  191. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  192. package/dist/src/modules/workspace/result-text.d.ts +1 -0
  193. package/dist/src/modules/workspace/result-text.d.ts.map +1 -1
  194. package/dist/src/modules/workspace/result-text.js.map +1 -1
  195. package/dist/src/modules/workspace/sync.d.ts +9 -0
  196. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  197. package/dist/src/modules/workspace/sync.js +14 -0
  198. package/dist/src/modules/workspace/sync.js.map +1 -1
  199. package/dist/src/modules/workspace/types.d.ts +96 -43
  200. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  201. package/dist/src/modules/workspace/types.js.map +1 -1
  202. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  203. package/dist/src/modules/workspace/watcher.js +8 -3
  204. package/dist/src/modules/workspace/watcher.js.map +1 -1
  205. package/dist/src/notice-tags.d.ts +79 -0
  206. package/dist/src/notice-tags.d.ts.map +1 -0
  207. package/dist/src/notice-tags.js +83 -0
  208. package/dist/src/notice-tags.js.map +1 -0
  209. package/dist/src/prose-drafts.d.ts +4 -3
  210. package/dist/src/prose-drafts.d.ts.map +1 -1
  211. package/dist/src/prose-drafts.js +3 -2
  212. package/dist/src/prose-drafts.js.map +1 -1
  213. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  214. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  215. package/dist/src/reasoning-loss-notice.js +52 -0
  216. package/dist/src/reasoning-loss-notice.js.map +1 -0
  217. package/dist/src/reasoning-loss.d.ts +24 -0
  218. package/dist/src/reasoning-loss.d.ts.map +1 -0
  219. package/dist/src/reasoning-loss.js +102 -0
  220. package/dist/src/reasoning-loss.js.map +1 -0
  221. package/dist/src/send-tools.d.ts +3 -2
  222. package/dist/src/send-tools.d.ts.map +1 -1
  223. package/dist/src/send-tools.js +3 -2
  224. package/dist/src/send-tools.js.map +1 -1
  225. package/dist/src/sent-rows.js +1 -1
  226. package/dist/src/silent-heartbeat.d.ts +1 -53
  227. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  228. package/dist/src/silent-heartbeat.js +56 -3
  229. package/dist/src/silent-heartbeat.js.map +1 -1
  230. package/dist/src/supplied-text.d.ts +35 -0
  231. package/dist/src/supplied-text.d.ts.map +1 -0
  232. package/dist/src/supplied-text.js +141 -0
  233. package/dist/src/supplied-text.js.map +1 -0
  234. package/dist/src/thinking-drops.d.ts +4 -11
  235. package/dist/src/thinking-drops.d.ts.map +1 -1
  236. package/dist/src/thinking-drops.js +7 -14
  237. package/dist/src/thinking-drops.js.map +1 -1
  238. package/dist/src/tool-args.d.ts +12 -0
  239. package/dist/src/tool-args.d.ts.map +1 -1
  240. package/dist/src/tool-args.js +39 -4
  241. package/dist/src/tool-args.js.map +1 -1
  242. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  243. package/dist/src/tool-image-ledger.js +2 -1
  244. package/dist/src/tool-image-ledger.js.map +1 -1
  245. package/dist/src/tool-presentation.d.ts.map +1 -1
  246. package/dist/src/tool-presentation.js +5 -4
  247. package/dist/src/tool-presentation.js.map +1 -1
  248. package/dist/src/tool-result-guard.d.ts +8 -4
  249. package/dist/src/tool-result-guard.d.ts.map +1 -1
  250. package/dist/src/tool-result-guard.js +23 -11
  251. package/dist/src/tool-result-guard.js.map +1 -1
  252. package/dist/src/tune-out/coordinator.d.ts +3 -2
  253. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  254. package/dist/src/tune-out/coordinator.js +24 -26
  255. package/dist/src/tune-out/coordinator.js.map +1 -1
  256. package/dist/src/tune-out/tools.d.ts +4 -1
  257. package/dist/src/tune-out/tools.d.ts.map +1 -1
  258. package/dist/src/types/agent.d.ts +63 -18
  259. package/dist/src/types/agent.d.ts.map +1 -1
  260. package/dist/src/types/events.d.ts +18 -8
  261. package/dist/src/types/events.d.ts.map +1 -1
  262. package/dist/src/types/framework.d.ts +58 -0
  263. package/dist/src/types/framework.d.ts.map +1 -1
  264. package/dist/src/types/module.d.ts +27 -13
  265. package/dist/src/types/module.d.ts.map +1 -1
  266. package/dist/src/types/trace.d.ts +34 -1
  267. package/dist/src/types/trace.d.ts.map +1 -1
  268. package/package.json +3 -3
  269. package/src/agent.ts +76 -12
  270. package/src/api/mcp-server-core.ts +3 -14
  271. package/src/api/server.ts +11 -5
  272. package/src/auto-continue.ts +12 -5
  273. package/src/code-execution/isolate/runner.ts +7 -5
  274. package/src/code-execution/isolate/scripts.ts +3 -2
  275. package/src/cut-off-round.ts +98 -0
  276. package/src/framework.ts +3772 -1138
  277. package/src/gate/event-gate.ts +705 -60
  278. package/src/gate/index.ts +9 -1
  279. package/src/gate/reply-judge.ts +512 -0
  280. package/src/gate/types.ts +176 -3
  281. package/src/heartbeat/heartbeat.ts +843 -0
  282. package/src/heartbeat/reminder-time.ts +107 -0
  283. package/src/held-writes.ts +3 -3
  284. package/src/host-presentation.ts +605 -0
  285. package/src/index.ts +5 -1
  286. package/src/line-reader.ts +58 -0
  287. package/src/mcpl/capability-mask.ts +3 -2
  288. package/src/mcpl/channel-registry.ts +139 -53
  289. package/src/mcpl/conversation-router.ts +2 -1
  290. package/src/mcpl/convert-block.ts +17 -4
  291. package/src/mcpl/hook-orchestrator.ts +4 -3
  292. package/src/mcpl/inbound-source.ts +45 -70
  293. package/src/mcpl/modern-connection.ts +11 -7
  294. package/src/mcpl/protocol-family.ts +24 -3
  295. package/src/mcpl/push-handler.ts +2 -2
  296. package/src/mcpl/references.ts +31 -23
  297. package/src/mcpl/server-connection.ts +76 -34
  298. package/src/mcpl/skip-reply-wake.ts +35 -0
  299. package/src/mcpl/tags.ts +13 -2
  300. package/src/mcpl/tool-presence.ts +149 -82
  301. package/src/mcpl/tool-signature.ts +64 -32
  302. package/src/mcpl/toolset-host.ts +212 -110
  303. package/src/mcpl/toolsets-module.ts +277 -91
  304. package/src/mcpl/transport.ts +18 -14
  305. package/src/mcpl/types.ts +32 -13
  306. package/src/module-registry.ts +14 -9
  307. package/src/modules/api/index.ts +4 -2
  308. package/src/modules/discord/index.ts +37 -25
  309. package/src/modules/history/index.ts +30 -18
  310. package/src/modules/sender-name.ts +9 -0
  311. package/src/modules/workspace/answers.ts +151 -0
  312. package/src/modules/workspace/branch-intent.ts +10 -5
  313. package/src/modules/workspace/direct-write.ts +59 -23
  314. package/src/modules/workspace/disk-agreement.ts +52 -21
  315. package/src/modules/workspace/edit-text.ts +7 -5
  316. package/src/modules/workspace/effects.ts +69 -50
  317. package/src/modules/workspace/index.ts +966 -556
  318. package/src/modules/workspace/observe.ts +497 -242
  319. package/src/modules/workspace/read-page.ts +7 -1
  320. package/src/modules/workspace/reconcile.ts +184 -49
  321. package/src/modules/workspace/result-text.ts +2 -0
  322. package/src/modules/workspace/sync.ts +16 -0
  323. package/src/modules/workspace/types.ts +96 -43
  324. package/src/modules/workspace/watcher.ts +8 -3
  325. package/src/notice-tags.ts +84 -0
  326. package/src/prose-drafts.ts +4 -3
  327. package/src/reasoning-loss-notice.ts +64 -0
  328. package/src/reasoning-loss.ts +120 -0
  329. package/src/send-tools.ts +3 -2
  330. package/src/sent-rows.ts +1 -1
  331. package/src/silent-heartbeat.ts +4 -3
  332. package/src/supplied-text.ts +154 -0
  333. package/src/thinking-drops.ts +10 -16
  334. package/src/tool-args.ts +38 -4
  335. package/src/tool-image-ledger.ts +2 -1
  336. package/src/tool-presentation.ts +5 -4
  337. package/src/tool-result-guard.ts +26 -11
  338. package/src/tune-out/coordinator.ts +25 -27
  339. package/src/tune-out/tools.ts +4 -1
  340. package/src/types/agent.ts +63 -18
  341. package/src/types/events.ts +18 -8
  342. package/src/types/framework.ts +59 -0
  343. package/src/types/module.ts +28 -13
  344. package/src/types/trace.ts +36 -1
@@ -44,6 +44,7 @@ import { constants as fsConstants } from 'node:fs';
44
44
  import { lstat, mkdir, open, readdir, readlink, realpath, stat, statfs } from 'node:fs/promises';
45
45
  import { basename, dirname, isAbsolute, join, posix, relative, resolve, sep } from 'node:path';
46
46
  import { sameRootIdentity } from './disk-agreement.js';
47
+ import { OUTSIDE_STEP, UNFOLLOWED_STEP, errnoClause, errnoCode, systemWords } from './answers.js';
47
48
  /**
48
49
  * Opened with every open of a workspace path that may already be there, a
49
50
  * read, a write in place or a directory's sync, where the platform has it:
@@ -61,15 +62,26 @@ export const HEAD_BYTES = 8192;
61
62
  export const RACY_WINDOW_MS = 2000;
62
63
  export const OUTSIDE_PARENT = 'a parent directory resolves outside the mount';
63
64
  export const NOT_FOLLOWED = 'a symlink, which this mount does not follow';
65
+ export const DIRECTORY = 'a directory';
66
+ export const NOT_REGULAR = 'not a regular file';
67
+ export const LEAVES_MOUNT = 'a symlink that leaves the mount';
68
+ export const NOT_A_DIRECTORY = 'a parent path is not a directory';
64
69
  export const ROOT_UNAVAILABLE = 'the mount root is unavailable';
65
70
  export const ROOT_REPLACED = 'the mount root is not the directory disk last agreed with (an unmounted drive, or a replaced directory): ' +
66
71
  'reconnect it; if it was replaced on purpose, sync the mount with acceptRoot to take it as it is now';
67
- const CHANGED = 'it changed while it was observed';
68
- const CHANGED_WALK = 'the directory changed while the walk ran';
69
- const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
72
+ export const CHANGED = 'it changed while it was observed';
73
+ export const CHANGED_WALK = 'the directory changed while the walk ran';
74
+ export const FILE_CHANGED = 'the file changed while it was read';
75
+ export const RESOLVES_OUTSIDE = 'the directory resolves outside the mount';
76
+ export const LINK_TO_DIRECTORY = 'a symlink to a directory, not walked';
77
+ export const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
70
78
  export function errno(err) {
71
79
  return err?.code;
72
80
  }
81
+ function failedOn(what, err) {
82
+ const code = errno(err);
83
+ return { reason: `${what} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
84
+ }
73
85
  export function contained(rootReal, candidate) {
74
86
  return candidate === rootReal || candidate.startsWith(rootReal.endsWith(sep) ? rootReal : rootReal + sep);
75
87
  }
@@ -152,9 +164,14 @@ export async function linkRefusal(view, rootReal, rel, real, through = 'path') {
152
164
  return linkOnTheWay(view, rel, through);
153
165
  }
154
166
  export async function linkLeads(root, rootReal, link) {
155
- const leads = await realpath(join(root, link)).catch(() => null);
156
- if (leads === null)
157
- return { kind: 'nowhere' };
167
+ let leads;
168
+ try {
169
+ leads = await realpath(join(root, link));
170
+ }
171
+ catch (err) {
172
+ const code = errno(err);
173
+ return code === 'ENOENT' || code === 'ENOTDIR' || code === 'ELOOP' ? { kind: 'nowhere' } : null;
174
+ }
158
175
  if (!contained(rootReal, leads))
159
176
  return { kind: 'outside' };
160
177
  const directory = await stat(leads).then((info) => info.isDirectory(), () => false);
@@ -166,19 +183,19 @@ export async function linkLeads(root, rootReal, link) {
166
183
  * does.
167
184
  */
168
185
  export function nestedHolder(target, nested) {
169
- return nested
170
- .filter((n) => target === n.dir || target.startsWith(`${n.dir}/`))
171
- .sort((a, b) => b.dir.length - a.dir.length)[0];
186
+ return nestedHolders(target, nested)[0];
172
187
  }
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);
188
+ function nestedHolders(target, nested) {
189
+ const segments = target.split('/');
190
+ return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
191
+ }
192
+ function mayHold(holders, depth, sure) {
193
+ const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
194
+ const surely = sorted.findIndex(sure);
195
+ return surely === -1 ? sorted : sorted.slice(0, surely + 1);
196
+ }
197
+ export function namesHolder(leads, nested) {
198
+ return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
182
199
  }
183
200
  /**
184
201
  * Whether a refusal names a place in a nested mount's directory, `rest`
@@ -195,19 +212,13 @@ export function namesHolder(leads, holder) {
195
212
  export function namesNestedPlace(rest, include, directory) {
196
213
  return directory || insideMount(rest, include);
197
214
  }
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
215
  export function withinIncludes(view, path) {
207
216
  if (!insideMount(path, view.include))
208
217
  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);
218
+ return nestedHolders(path, view.nested ?? []).every((holder) => {
219
+ const rest = restBeneath(path, holder);
220
+ return rest === '' || namesNestedPlace(rest, holder.include, false);
221
+ });
211
222
  }
212
223
  /**
213
224
  * The path a mount's include judges for `given`, one of the mount's paths,
@@ -279,16 +290,23 @@ export async function coversLanding(view, rootReal, real, given) {
279
290
  }
280
291
  /**
281
292
  * Why a path is refused for what linkOnTheWay found, given where a link
282
- * leads (linkLeads). For a link, the link is the subject, with where it
283
- * leads: for a link inside the mount, its target, and for a link to a
284
- * directory there, the setting the mount's host can change (the link stands
285
- * among the path's directories, so following a link to a file can't make the
286
- * path beneath it exist); for one leading outside the mount, that it does;
287
- * for one that can't be resolved, nothing more. A target in a nested mount's
288
- * directory (`nested`) is that mount's: the refusal names the mount and the
289
- * path to use there in place of the link, and offers no setting, since
290
- * following the link would only reach into another mount's directory. For a
291
- * directory the walk couldn't ask, which one, and the answer it got. Paths
293
+ * leads (linkLeads), and the way on. For a link, the link is the subject,
294
+ * with where it leads: for a link inside the mount, its target, as the path
295
+ * to use in its place, and for a link to a directory there, the setting
296
+ * whoever runs the host can change too; for a link to something that isn't
297
+ * a directory, that nothing lies beneath it (the link stands among the
298
+ * path's directories, so following it can't make the path beneath it
299
+ * exist); for one leading outside the mount, that it does, and that
300
+ * whoever runs the host can mount what it leads to, or a directory that
301
+ * holds it; for one leading nowhere, that whoever runs the host can remove it or make
302
+ * it lead to what it stood for; for one whose target the refusal doesn't
303
+ * tell, what whoever runs the host can do wherever it leads
304
+ * (UNFOLLOWED_STEP).
305
+ * A target in a nested mount's directory (`nested`) is that mount's: the
306
+ * refusal names the mount and the path to use there in place of the link,
307
+ * and offers no setting, since following the link would only reach into
308
+ * another mount's directory. For a directory the walk couldn't ask, which
309
+ * one, the answer it got, and the errno's way on (errnoClause). Paths
292
310
  * are spelled as the tools take them, from the mount's name. On a mount with
293
311
  * an include, an inside target is named only where a tool could name it: a
294
312
  * file the include covers, or a directory, which a listing names whatever
@@ -299,27 +317,37 @@ export async function coversLanding(view, rootReal, real, given) {
299
317
  */
300
318
  export function linkWords(name, found, leads, nested = [], include) {
301
319
  const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
302
- if (!('link' in found))
303
- return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
320
+ if (!('link' in found)) {
321
+ const code = errnoCode({ code: found.code });
322
+ return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(code, found.code)}); ${errnoClause(code, 'calling again', 'calling again then gets past it')}`;
323
+ }
304
324
  const link = `${spell(found.link)} is a symlink`;
305
- if (leads === null || leads.kind === 'nowhere')
306
- return `${link}, which this mount does not follow`;
307
- if (leads.kind === 'outside')
308
- return `${link} leading outside the mount, which this mount does not follow`;
325
+ const unknown = `${link}, which this mount does not follow; ${UNFOLLOWED_STEP}`;
326
+ if (leads === null)
327
+ return unknown;
328
+ if (leads.kind === 'nowhere') {
329
+ return `${link} that leads nowhere, which this mount does not follow; whoever runs this host can remove it or make it lead to what it stood for`;
330
+ }
331
+ if (leads.kind === 'outside') {
332
+ return `${link} leading outside the mount, which this mount does not follow; followed or not, ${OUTSIDE_STEP}`;
333
+ }
309
334
  const holder = nestedHolder(leads.target, nested);
310
335
  if (holder) {
311
- if (!namesHolder(leads, holder))
312
- return `${link}, which this mount does not follow`;
313
- const rest = leads.target.slice(holder.dir.length + 1);
314
- return `${link} to ${spell(leads.target)}, which this mount does not follow; it lies in mount "${holder.name}", ` +
315
- `nested in mount "${name}": use "${rest ? `${holder.name}/${rest}` : holder.name}" in place of ${spell(found.link)}`;
336
+ if (!namesHolder(leads, nested))
337
+ return unknown;
338
+ const rest = restBeneath(leads.target, holder);
339
+ const guess = nestedGuess(name, leads.target.split('/'), holder);
340
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
341
+ `"${holder.name}", nested in mount "${name}": ${nestedStep({ name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, guess }, { after: ` in place of ${spell(found.link)}` })}`;
316
342
  }
317
343
  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";
322
- return `${link} to ${spell(leads.target)}, which this mount does not follow${setting}`;
344
+ return unknown;
345
+ if (!leads.directory)
346
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; that isn't a directory, so nothing lies beneath it`;
347
+ // Either way serves only what the include covers where it leads (judgedAt).
348
+ const covered = include === undefined ? '' : '; either way, the mount reaches only what its include covers there';
349
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; use ${spell(leads.target)} in place of ${spell(found.link)}, ` +
350
+ `or whoever runs this host can set followSymlinks: true on mount "${name}"${covered}`;
323
351
  }
324
352
  /**
325
353
  * Where a link linkOnTheWay found leads, as its refusal may tell it
@@ -335,25 +363,83 @@ export async function shownLeads(root, rootReal, found, include) {
335
363
  }
336
364
  /** Why a path is refused for what linkOnTheWay found (linkWords, with where the link leads, as shownLeads tells it). */
337
365
  export async function linkReason(view, rootReal, found) {
338
- return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
366
+ return { reason: linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include), ownStep: true };
367
+ }
368
+ export function realSegments(path) {
369
+ return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
370
+ }
371
+ export function beneathRoot(path, root) {
372
+ const segments = realSegments(path);
373
+ const top = realSegments(root);
374
+ return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
375
+ }
376
+ export function sameRoot(path, root) {
377
+ const segments = realSegments(path);
378
+ const top = realSegments(root);
379
+ return segments.length === top.length && beginsWith(segments, top);
380
+ }
381
+ export function surelyAtRoot(path, root) {
382
+ return surelyBeneath(realSegments(path), realSegments(root));
383
+ }
384
+ export function nestedRootAtReal(real, nested = []) {
385
+ return nestedRootsAtReal(real, nested)[0];
386
+ }
387
+ function nestedRootsAtReal(real, nested) {
388
+ // realpath keeps the spelling it's given where disk folds it on Linux, as
389
+ // from a link's target, so a real root is compared as loosely as a path.
390
+ const segments = realSegments(real);
391
+ const holders = nested.flatMap((n) => {
392
+ if (n.real === undefined)
393
+ return [];
394
+ const root = realSegments(n.real);
395
+ return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
396
+ });
397
+ return mayHold(holders, (h) => h.depth, (h) => h.sure);
339
398
  }
340
399
  /**
341
400
  * The nested mount whose root holds `real`, a canonical path, the innermost,
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
401
+ * with the path to use there and the path beneath its root (`rest`);
402
+ * undefined when none does. A path the lexical checks pass
344
403
  * (inOtherMount, the tools' holderOf) reaches another mount's directory this
345
404
  * 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.
405
+ * path canonically asks this too.
348
406
  */
349
- export function nestedAtReal(real, nested = []) {
350
- const holder = nested
351
- .flatMap((n) => (n.real !== undefined && contained(n.real, real) ? [{ name: n.name, real: n.real, include: n.include }] : []))
352
- .sort((a, b) => b.real.length - a.real.length)[0];
407
+ export async function nestedAtReal(real, nested = [], at) {
408
+ const holders = nestedRootsAtReal(real, nested);
409
+ const holder = holders[0];
353
410
  if (!holder)
354
411
  return undefined;
355
- const rest = relative(holder.real, real).split(sep).join('/');
356
- return { name: holder.name, use: rest ? `${holder.name}/${rest}` : holder.name, rest, include: holder.include };
412
+ const segments = realSegments(real);
413
+ // realpath keeps a link target's spelling beneath the root too, so the
414
+ // rest is read as disk spells it, for each mount's include to judge.
415
+ const rests = await Promise.all(holders.map(async (h) => {
416
+ const given = segments.slice(h.depth).join('/');
417
+ return given === '' ? '' : await diskSpelling(h.mount.real, given);
418
+ }));
419
+ const rest = rests[0];
420
+ const { name } = holder.mount;
421
+ const root = realSegments(holder.mount.real);
422
+ const found = {
423
+ name,
424
+ use: rest ? `${name}/${rest}` : name,
425
+ rest,
426
+ judgedByInclude: holders.some((h) => h.mount.include !== undefined),
427
+ names: (directory) => holders.every((h, i) => namesNestedPlace(rests[i], h.mount.include, directory)),
428
+ };
429
+ if (surelyBeneath(segments, root))
430
+ return found;
431
+ if (at === undefined)
432
+ return { ...found, guess: null };
433
+ const top = realSegments(at.rootReal);
434
+ if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t))
435
+ return { ...found, guess: null };
436
+ const within = segments.slice(top.length);
437
+ const below = root.slice(top.length);
438
+ const guess = guessFor(at.name, within, below);
439
+ const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
440
+ if (!surelyBeneath(root.slice(0, top.length), top))
441
+ return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
442
+ return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
357
443
  }
358
444
  /**
359
445
  * Why a mount-relative path that resolves into a nested mount's directory is
@@ -361,7 +447,8 @@ export function nestedAtReal(real, nested = []) {
361
447
  * place. Spelled as the tools take paths, from the mount's name.
362
448
  */
363
449
  export function leadsInto(name, rel, holder) {
364
- return `"${rel === '' ? name : `${name}/${rel}`}" leads into mount "${holder.name}", nested in mount "${name}": use "${holder.use}" in its place`;
450
+ const verb = holder.guess === undefined ? 'leads' : 'may lead';
451
+ return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
365
452
  }
366
453
  // ===========================================================================
367
454
  // Disk's spelling of a path
@@ -371,9 +458,8 @@ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
371
458
  const otherCase = (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase());
372
459
  /**
373
460
  * The name with each letter's case flipped, where its other case is one
374
- * letter whose own other case is the letter again, so the flip changes only
375
- * pairs that any folding disk folds together. `ß`, whose upper case is `SS`,
376
- * and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
461
+ * letter whose own other case is the letter again. `ß`, whose upper case is
462
+ * `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
377
463
  */
378
464
  const flippedCase = (name) => [...name].map((c) => {
379
465
  const other = otherCase(c);
@@ -388,6 +474,15 @@ const folded = (name) => name.normalize('NFC').toLowerCase();
388
474
  * the same-file check keeps that from naming another file.
389
475
  */
390
476
  export const foldedLoosely = (name) => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
477
+ /**
478
+ * Linux's casefold, which ext4 and f2fs set per directory, drops
479
+ * default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
480
+ * compares names decomposed, in full case folding, and HFS+ passes over some
481
+ * of those code points too. Case mapped on the decomposed name joins marks
482
+ * that come in another order around a combining ypogegrammeni (U+0345),
483
+ * whose upper case is a letter.
484
+ */
485
+ const foldedFully = (name) => name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
391
486
  /**
392
487
  * Whether `dir` folds case, told without writing: by looking up flipped a
393
488
  * name it lists whose flipped spelling it doesn't list too (a name with no
@@ -482,19 +577,73 @@ function entryName(paths, prefix, segment) {
482
577
  }
483
578
  return found;
484
579
  }
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)));
580
+ function sameLevel(given, name) {
581
+ return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
582
+ }
583
+ function beginsWith(segments, dir) {
584
+ return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i], d));
585
+ }
586
+ export function beneathNested(segments, nested) {
587
+ return beginsWith(segments, nested.dir.split('/'));
588
+ }
589
+ export function restBeneath(path, nested) {
590
+ // By segments, since a name that matches can be another length, as `SS`
591
+ // is beside `ß`.
592
+ return path.split('/').slice(nested.dir.split('/').length).join('/');
593
+ }
594
+ function surelyBeneath(segments, dir) {
595
+ return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
596
+ }
597
+ function shownByCodePoints(name) {
598
+ return [...name].map((c) => {
599
+ const code = c.codePointAt(0);
600
+ if (c === '\\')
601
+ return '\\\\';
602
+ return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
603
+ }).join('');
604
+ }
605
+ function guessFor(mount, given, dir) {
606
+ if (surelyBeneath(given, dir))
607
+ return undefined;
608
+ const levels = given.slice(0, dir.length);
609
+ const plain = (a, b) => a === b || !/[^\x00-\x7f]/.test(a + b);
610
+ const shown = (name, other) => (plain(name, other) ? name : shownByCodePoints(name));
611
+ return {
612
+ given: [mount, ...levels.map((g, i) => shown(g, dir[i]))].join('/'),
613
+ dir: [mount, ...dir.map((d, i) => shown(d, levels[i]))].join('/'),
614
+ };
615
+ }
616
+ export function nestedGuess(mount, segments, nested) {
617
+ const dir = nested.dir.split('/');
618
+ const guess = guessFor(mount, segments, dir);
619
+ return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
620
+ }
621
+ function placedGuess(mount, levels, guess) {
622
+ return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
623
+ }
624
+ export function nestedStep(holder, opts = {}) {
625
+ const { guess } = holder;
626
+ const after = opts.after ?? '';
627
+ if (guess === undefined)
628
+ return `use "${holder.use}"${after}`;
629
+ if (guess === null) {
630
+ return `this workspace can't tell whether where it leads is that mount's directory. ` +
631
+ `If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
632
+ }
633
+ const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
634
+ if ('placed' in guess) {
635
+ const check = 'ask whoever runs this workspace to check where the two mounts are configured';
636
+ const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
637
+ return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
638
+ `spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
639
+ (opts.named ? `If you meant one of this mount's own, for a new file use a path outside ${outside}, and for one already there ${check}` : `If not, ${check}`);
640
+ }
641
+ const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
642
+ const own = opts.named
643
+ ? `If you meant one of this mount's own, for a new file use a path outside "${guess.given}" and "${guess.dir}", and for one already there ask whoever runs this workspace to ${remedy}`
644
+ : `If not, ask whoever runs this workspace to ${remedy}`;
645
+ return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
646
+ `mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
498
647
  }
499
648
  /**
500
649
  * The name `dir` lists for `segment`: `segment` itself, unless disk folds it
@@ -707,20 +856,26 @@ export class CheckedDir {
707
856
  lexical;
708
857
  dev;
709
858
  ino;
859
+ born;
710
860
  constructor(
711
861
  /** Mount-relative path ('' for the root). */
712
862
  rel,
713
863
  /** Its canonical location when checked. */
714
- real, lexical, dev, ino) {
864
+ real, lexical, dev, ino, born) {
715
865
  this.rel = rel;
716
866
  this.real = real;
717
867
  this.lexical = lexical;
718
868
  this.dev = dev;
719
869
  this.ino = ino;
870
+ this.born = born;
720
871
  }
721
872
  /** Which directory it is. */
722
873
  identity() {
723
- return { dev: String(this.dev), ino: String(this.ino) };
874
+ return {
875
+ dev: String(this.dev),
876
+ ino: String(this.ino),
877
+ ...(this.born > 0n ? { born: String(this.born) } : {}),
878
+ };
724
879
  }
725
880
  /** A path that reaches `name` in this directory. */
726
881
  at(name) {
@@ -764,12 +919,26 @@ export class CheckedDir {
764
919
  }
765
920
  }
766
921
  }
922
+ /**
923
+ * What is at a mount root's path now, followed as the root is: for a
924
+ * directory, undefined; for anything else, ENOTDIR; and where the path
925
+ * can't be followed to anything, the errno its stat failed with, ENOENT
926
+ * when nothing is there.
927
+ */
928
+ export async function rootAbsence(root) {
929
+ try {
930
+ return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
931
+ }
932
+ catch (err) {
933
+ return errno(err);
934
+ }
935
+ }
767
936
  /** Check the directory at a canonical location, if one is there. */
768
937
  async function checkedAt(view, rel, real) {
769
938
  const info = await lstat(real, { bigint: true }).catch(() => null);
770
939
  if (!info?.isDirectory())
771
940
  return null;
772
- return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
941
+ return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
773
942
  }
774
943
  /**
775
944
  * Where a mount directory is now: checked; missing — nothing there, or not a
@@ -784,19 +953,19 @@ async function locateDir(view, rootReal, rel) {
784
953
  catch (err) {
785
954
  const code = errno(err);
786
955
  if (code === 'ENOENT' || code === 'ENOTDIR')
787
- return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
956
+ return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
788
957
  // A link on the way that can't be resolved, such as a loop, is still a
789
958
  // link this mount doesn't follow.
790
959
  const found = rel === '' ? null : await linkOnTheWay(view, rel);
791
960
  if (found !== null)
792
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
793
- return { kind: 'unobserved', reason: `cannot resolve the directory (${code ?? String(err)})` };
961
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
962
+ return { kind: 'unobserved', ...failedOn('cannot resolve the directory', err) };
794
963
  }
795
964
  // A mount that doesn't follow symlinks goes through none on the way to a
796
965
  // directory, inside the mount or out of it.
797
966
  const found = await linkRefusal(view, rootReal, rel, real);
798
967
  if (found !== null)
799
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
968
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
800
969
  if (rel === '') {
801
970
  // The root is whatever the configured path names, and it must still be
802
971
  // the root the pass was given.
@@ -811,10 +980,10 @@ async function locateDir(view, rootReal, rel) {
811
980
  // mount's, which the mount neither reads nor writes. A place there that
812
981
  // mount's include leaves out is no mount's, and holds nothing beneath it,
813
982
  // as at any file (namesNestedPlace).
814
- const holder = nestedAtReal(real, view.nested);
983
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
815
984
  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' };
985
+ const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
986
+ return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder), ownStep: true } : { kind: 'missing' };
818
987
  }
819
988
  }
820
989
  const checked = await checkedAt(view, rel, real);
@@ -842,7 +1011,10 @@ export async function checkRoot(view, rootReal) {
842
1011
  const found = await locateDir(view, rootReal, '');
843
1012
  if (found.kind === 'checked')
844
1013
  return { dir: found.dir };
845
- return { reason: found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE };
1014
+ const reason = found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE;
1015
+ // What is at the root's path now says whether the directory is gone from it.
1016
+ const code = found.kind === 'unobserved' && found.errno !== undefined ? found.errno : reason === ROOT_UNAVAILABLE ? await rootAbsence(view.root) : undefined;
1017
+ return { reason, ...(code !== undefined ? { errno: code } : {}) };
846
1018
  }
847
1019
  /**
848
1020
  * Check the directory at a mount-relative path. With `create`, missing
@@ -858,7 +1030,7 @@ export async function checkDir(view, rootReal, rel, opts = {}) {
858
1030
  if (opts.create)
859
1031
  return createDir(view, rootReal, rel);
860
1032
  const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
861
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
1033
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
862
1034
  }
863
1035
  async function createDir(view, rootReal, rel) {
864
1036
  const parts = rel.split('/');
@@ -879,16 +1051,16 @@ async function createDir(view, rootReal, rel) {
879
1051
  const childRel = parts.slice(0, i + 1).join('/');
880
1052
  // Nothing is made in a nested mount's directory, its root included where
881
1053
  // it isn't made yet, by a path that reaches it through a link.
882
- const holder = nestedAtReal(base.at(parts[i]), view.nested);
1054
+ const holder = await nestedAtReal(base.at(parts[i]), view.nested, { name: view.name, rootReal });
883
1055
  if (holder)
884
- return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
1056
+ return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder), ownStep: true };
885
1057
  try {
886
1058
  await mkdir(base.at(parts[i]));
887
1059
  }
888
1060
  catch (err) {
889
1061
  const code = errno(err);
890
1062
  if (code !== 'EEXIST')
891
- return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${code ?? String(err)})` };
1063
+ return { kind: 'unobserved', ...failedOn(`cannot create the directory ${childRel}`, err) };
892
1064
  }
893
1065
  // Made, or there already (made meanwhile, or not a directory): it must
894
1066
  // be a real directory now, never a symlink or file in its place.
@@ -898,8 +1070,8 @@ async function createDir(view, rootReal, rel) {
898
1070
  // doesn't follow symlinks, is named as one.
899
1071
  const there = view.followSymlinks ? null : await lstat(base.at(parts[i])).catch(() => null);
900
1072
  if (there?.isSymbolicLink())
901
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, { link: childRel }) };
902
- return { kind: 'unobserved', reason: 'a parent path is not a directory' };
1073
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, { link: childRel })) };
1074
+ return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
903
1075
  }
904
1076
  base = checked;
905
1077
  }
@@ -923,14 +1095,14 @@ async function createDir(view, rootReal, rel) {
923
1095
  export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
924
1096
  const root = await locateDir(view, rootReal, '');
925
1097
  if (root.kind !== 'checked')
926
- return ROOT_UNAVAILABLE;
1098
+ return { reason: ROOT_UNAVAILABLE };
927
1099
  let cur = root.dir;
928
1100
  /** Every directory the walk went through, the root first. */
929
1101
  const walked = [cur];
930
1102
  const proven = async () => {
931
1103
  for (const dir of walked)
932
1104
  if (!(await dir.stillHere()))
933
- return CHANGED;
1105
+ return { reason: CHANGED };
934
1106
  return { by: cur.rel };
935
1107
  };
936
1108
  const parts = rel.split('/');
@@ -945,48 +1117,51 @@ export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
945
1117
  catch (err) {
946
1118
  const code = errno(err);
947
1119
  if (code === 'ENOTDIR')
948
- return CHANGED;
1120
+ return { reason: CHANGED };
949
1121
  if (code !== 'ENOENT')
950
- return `cannot stat (${code ?? String(err)})`;
1122
+ return failedOn('cannot stat', err);
951
1123
  // Absent from a directory that, like each above it, is still the one its path names.
952
1124
  return proven();
953
1125
  }
954
1126
  if (info.isDirectory()) {
955
1127
  if (last)
956
- return CHANGED; // present after all
957
- cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
1128
+ return { reason: CHANGED }; // present after all
1129
+ cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
958
1130
  walked.push(cur);
959
1131
  continue;
960
1132
  }
961
1133
  if (info.isSymbolicLink()) {
962
1134
  if (last && target === 'entry')
963
- return CHANGED; // present: the link itself
1135
+ return { reason: CHANGED }; // present: the link itself
964
1136
  // A mount that doesn't follow symlinks goes through none on the way.
965
1137
  if (!view.followSymlinks)
966
1138
  return linkReason(view, rootReal, { link: childRel });
967
1139
  const located = await locateDir(view, rootReal, childRel);
1140
+ if (located.kind === 'unobserved' && located.outside !== true) {
1141
+ return { reason: located.reason, ...(located.errno !== undefined ? { errno: located.errno } : {}), ...(located.ownStep === true ? { ownStep: true } : {}) };
1142
+ }
968
1143
  if (located.kind !== 'checked')
969
- return BENEATH_SYMLINK;
1144
+ return { reason: BENEATH_SYMLINK };
970
1145
  if (last)
971
- return CHANGED; // a directory there after all
1146
+ return { reason: CHANGED }; // a directory there after all
972
1147
  cur = located.dir;
973
1148
  walked.push(cur);
974
1149
  continue;
975
1150
  }
976
1151
  if (last && target === 'entry')
977
- return CHANGED; // present after all
1152
+ return { reason: CHANGED }; // present after all
978
1153
  // A non-directory: nothing can exist beneath it, while it is still this
979
1154
  // same one, in the directory still at its path.
980
1155
  const again = await lstat(cur.at(name), { bigint: true }).catch(() => null);
981
1156
  if (again === null || again.isDirectory() || again.isSymbolicLink() || again.dev !== info.dev || again.ino !== info.ino)
982
- return CHANGED;
1157
+ return { reason: CHANGED };
983
1158
  return proven();
984
1159
  }
985
- return CHANGED;
1160
+ return { reason: CHANGED };
986
1161
  }
987
1162
  async function absence(view, rootReal, rel) {
988
1163
  const proof = await confirmAbsent(view, rootReal, rel);
989
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent' };
1164
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent' };
990
1165
  }
991
1166
  // ===========================================================================
992
1167
  // Fingerprints
@@ -1061,16 +1236,16 @@ async function parentPlacement(view, rootReal, relativePath) {
1061
1236
  const parent = await realpath(dirname(join(view.root, relativePath)));
1062
1237
  const found = await linkRefusal(view, rootReal, relativePath, parent, 'parents');
1063
1238
  if (found !== null)
1064
- return { reason: await linkReason(view, rootReal, found) };
1239
+ return linkReason(view, rootReal, found);
1065
1240
  if (!contained(rootReal, parent))
1066
1241
  return { reason: OUTSIDE_PARENT };
1067
- const holder = nestedAtReal(parent, view.nested);
1242
+ const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
1068
1243
  if (!holder)
1069
1244
  return { inside: parent };
1070
1245
  // A place there that the nested mount's include leaves out is no mount's,
1071
1246
  // 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';
1247
+ const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
1248
+ return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder), ownStep: true } : 'gone';
1074
1249
  }
1075
1250
  catch (err) {
1076
1251
  const code = errno(err);
@@ -1080,8 +1255,8 @@ async function parentPlacement(view, rootReal, relativePath) {
1080
1255
  // link this mount doesn't follow.
1081
1256
  const found = await linkOnTheWay(view, relativePath, 'parents');
1082
1257
  if (found !== null)
1083
- return { reason: await linkReason(view, rootReal, found) };
1084
- return { reason: `cannot resolve the parent directory (${code ?? String(err)})` };
1258
+ return linkReason(view, rootReal, found);
1259
+ return failedOn('cannot resolve the parent directory', err);
1085
1260
  }
1086
1261
  }
1087
1262
  /**
@@ -1096,7 +1271,7 @@ export async function observePath(view, rootReal, relativePath) {
1096
1271
  if (placement === 'gone')
1097
1272
  return absence(view, rootReal, relativePath);
1098
1273
  if ('reason' in placement)
1099
- return { kind: 'unobserved', reason: placement.reason };
1274
+ return { kind: 'unobserved', ...placement };
1100
1275
  let info;
1101
1276
  try {
1102
1277
  info = await lstat(lexical, { bigint: true });
@@ -1105,7 +1280,7 @@ export async function observePath(view, rootReal, relativePath) {
1105
1280
  const code = errno(err);
1106
1281
  if (code === 'ENOENT' || code === 'ENOTDIR')
1107
1282
  return absence(view, rootReal, relativePath);
1108
- return { kind: 'unobserved', reason: `cannot stat (${code ?? String(err)})` };
1283
+ return { kind: 'unobserved', ...failedOn('cannot stat', err) };
1109
1284
  }
1110
1285
  if (info.isSymbolicLink()) {
1111
1286
  if (!view.followSymlinks)
@@ -1115,43 +1290,43 @@ export async function observePath(view, rootReal, relativePath) {
1115
1290
  target = await stat(lexical, { bigint: true });
1116
1291
  }
1117
1292
  catch (err) {
1118
- return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${errno(err) ?? String(err)})` };
1293
+ return { kind: 'unobserved', ...failedOn('a symlink whose target cannot be read', err) };
1119
1294
  }
1120
1295
  let real;
1121
1296
  try {
1122
1297
  real = await realpath(lexical);
1123
1298
  }
1124
1299
  catch (err) {
1125
- return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${errno(err) ?? String(err)})` };
1300
+ return { kind: 'unobserved', ...failedOn('a symlink that cannot be resolved', err) };
1126
1301
  }
1127
1302
  if (!contained(rootReal, real))
1128
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1129
- const holder = nestedAtReal(real, view.nested);
1303
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1304
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
1130
1305
  // A place a nested mount's include leaves out is no mount's, so the link
1131
1306
  // leaves this one (namesNestedPlace).
1132
1307
  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' };
1308
+ return holder.names(target.isDirectory()) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: LEAVES_MOUNT };
1134
1309
  }
1135
1310
  if (!target.isFile()) {
1136
- return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file' };
1311
+ return { kind: 'unobserved', reason: target.isDirectory() ? LINK_TO_DIRECTORY : NOT_REGULAR };
1137
1312
  }
1138
1313
  // A file this mount's own include leaves out where the link lands is no
1139
1314
  // file of this mount's (coversLanding). It's judged here, and not only
1140
1315
  // where the file is opened, since a pass takes a copy whose fingerprint
1141
1316
  // the stat vouches for without opening it.
1142
1317
  if (!(await coversLanding(view, rootReal, real, relativePath)))
1143
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1318
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1144
1319
  info = target;
1145
1320
  }
1146
1321
  else if (info.isDirectory()) {
1147
- return { kind: 'unobserved', reason: 'a directory' };
1322
+ return { kind: 'unobserved', reason: DIRECTORY };
1148
1323
  }
1149
1324
  else if (!info.isFile()) {
1150
- return { kind: 'unobserved', reason: 'not a regular file' };
1325
+ return { kind: 'unobserved', reason: NOT_REGULAR };
1151
1326
  }
1152
1327
  else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
1153
1328
  // So is one reached through a link among its directories.
1154
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1329
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1155
1330
  }
1156
1331
  return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
1157
1332
  }
@@ -1184,18 +1359,18 @@ async function openContained(view, rootReal, relativePath) {
1184
1359
  if (code === 'ELOOP' && !view.followSymlinks) {
1185
1360
  // The link at the path, or a looping one on its way.
1186
1361
  const found = await linkOnTheWay(view, relativePath, 'parents');
1187
- return { kind: 'unobserved', reason: found === null ? NOT_FOLLOWED : await linkReason(view, rootReal, found) };
1362
+ return found === null ? { kind: 'unobserved', reason: NOT_FOLLOWED } : { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
1188
1363
  }
1189
1364
  if (code === 'EISDIR')
1190
- return { kind: 'unobserved', reason: 'a directory' };
1191
- return { kind: 'unobserved', reason: `cannot open (${code ?? String(err)})` };
1365
+ return { kind: 'unobserved', reason: DIRECTORY };
1366
+ return { kind: 'unobserved', ...failedOn('cannot open', err) };
1192
1367
  }
1193
1368
  let keep = false;
1194
1369
  try {
1195
1370
  const info = await handle.stat({ bigint: true });
1196
1371
  const real = await realpath(lexical).catch(() => null);
1197
1372
  if (real === null)
1198
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1373
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1199
1374
  // O_NOFOLLOW guards only the final component: a mount that doesn't
1200
1375
  // follow symlinks goes through none among the directories on the way
1201
1376
  // either. Asked of what the path resolved to after the open, which the
@@ -1203,23 +1378,23 @@ async function openContained(view, rootReal, relativePath) {
1203
1378
  // descriptor holds, so a link gets its own answer whatever it leads to.
1204
1379
  const found = await linkRefusal(view, rootReal, relativePath, real);
1205
1380
  if (found !== null)
1206
- return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
1381
+ return { kind: 'unobserved', ...(await linkReason(view, rootReal, found)) };
1207
1382
  if (!info.isFile())
1208
- return { kind: 'unobserved', reason: info.isDirectory() ? 'a directory' : 'not a regular file' };
1209
- const leaves = view.followSymlinks ? 'a symlink that leaves the mount' : OUTSIDE_PARENT;
1383
+ return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
1384
+ const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
1210
1385
  if (!contained(rootReal, real))
1211
1386
  return { kind: 'unobserved', reason: leaves };
1212
- const holder = nestedAtReal(real, view.nested);
1387
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
1213
1388
  // A place a nested mount's include leaves out is no mount's. Reached
1214
1389
  // through a link this mount doesn't follow, the link is what's refused.
1215
1390
  if (holder)
1216
- return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
1391
+ return holder.names(false) ? { kind: 'unobserved', reason: leadsInto(view.name, relativePath, holder), ownStep: true } : { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
1217
1392
  // So is a file this mount's own include leaves out (coversLanding).
1218
1393
  if (!(await coversLanding(view, rootReal, real, relativePath)))
1219
1394
  return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
1220
1395
  const named = await stat(real, { bigint: true }).catch(() => null);
1221
1396
  if (named === null || named.dev !== info.dev || named.ino !== info.ino) {
1222
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1397
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1223
1398
  }
1224
1399
  if (!view.followSymlinks) {
1225
1400
  // Where no symlink is followed, the path itself must be the file the
@@ -1229,7 +1404,7 @@ async function openContained(view, rootReal, relativePath) {
1229
1404
  if (entry?.isSymbolicLink())
1230
1405
  return { kind: 'unobserved', reason: NOT_FOLLOWED };
1231
1406
  if (entry === null || !entry.isFile() || entry.dev !== info.dev || entry.ino !== info.ino) {
1232
- return { kind: 'unobserved', reason: 'the file changed while it was read' };
1407
+ return { kind: 'unobserved', reason: FILE_CHANGED };
1233
1408
  }
1234
1409
  }
1235
1410
  keep = true;
@@ -1239,7 +1414,7 @@ async function openContained(view, rootReal, relativePath) {
1239
1414
  const code = errno(err);
1240
1415
  if (code === 'ENOENT')
1241
1416
  return absence(view, rootReal, relativePath);
1242
- return { kind: 'unobserved', reason: `cannot read (${code ?? String(err)})` };
1417
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1243
1418
  }
1244
1419
  finally {
1245
1420
  if (!keep)
@@ -1340,7 +1515,7 @@ export async function readPath(view, rootReal, relativePath, limit) {
1340
1515
  const code = errno(err);
1341
1516
  if (code === 'ENOENT')
1342
1517
  return absence(view, rootReal, relativePath);
1343
- return { kind: 'unobserved', reason: `cannot read (${code ?? String(err)})` };
1518
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1344
1519
  }
1345
1520
  finally {
1346
1521
  await handle.close();
@@ -1382,7 +1557,7 @@ export async function readBytes(view, rootReal, relativePath, limit) {
1382
1557
  return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
1383
1558
  }
1384
1559
  catch (err) {
1385
- return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
1560
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1386
1561
  }
1387
1562
  finally {
1388
1563
  await handle.close();
@@ -1404,7 +1579,7 @@ export async function readHead(view, rootReal, relativePath) {
1404
1579
  return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
1405
1580
  }
1406
1581
  catch (err) {
1407
- return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
1582
+ return { kind: 'unobserved', ...failedOn('cannot read', err) };
1408
1583
  }
1409
1584
  finally {
1410
1585
  await handle.close();
@@ -1480,14 +1655,14 @@ function subtreeAgainst(dir, at) {
1480
1655
  return 'holds';
1481
1656
  }
1482
1657
  /**
1483
- * Whether a mount's ignore list covers a mount-relative path: the path itself
1484
- * or any directory above it, as the walk's own test reads each entry by its
1485
- * path and name.
1658
+ * Whether a mount's ignore list covers a mount-relative path: the path itself,
1659
+ * a directory or not, or any directory above it, as the walk's own test reads
1660
+ * each entry by its path and kind.
1486
1661
  */
1487
- export function coveredByIgnore(relativePath, patterns) {
1662
+ export function coveredByIgnore(relativePath, patterns, directory) {
1488
1663
  const segments = relativePath.split('/').filter(Boolean);
1489
1664
  for (let i = 0; i < segments.length; i++) {
1490
- if (isIgnored(segments.slice(0, i + 1).join('/'), segments[i], patterns))
1665
+ if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory))
1491
1666
  return true;
1492
1667
  }
1493
1668
  return false;
@@ -1530,44 +1705,90 @@ export function insideMount(relativePath, include) {
1530
1705
  return true;
1531
1706
  return coveredByInclude(relativePath, include);
1532
1707
  }
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));
1708
+ const readings = new Map();
1709
+ function readPattern(pattern) {
1710
+ let reading = readings.get(pattern);
1711
+ if (reading === undefined) {
1712
+ reading = parsePattern(pattern);
1713
+ readings.set(pattern, reading);
1714
+ }
1715
+ return reading;
1716
+ }
1717
+ function parsePattern(pattern) {
1718
+ if (pattern.startsWith('!'))
1719
+ return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
1720
+ if (pattern.startsWith('#'))
1721
+ return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
1722
+ if (pattern.includes('\\'))
1723
+ return '"\\" is an escape in a .gitignore and a separator on Windows, so no pattern here holds one, and "?" stands for one in a name';
1724
+ if (pattern.endsWith(' '))
1725
+ return 'a .gitignore drops trailing spaces, so remove them';
1726
+ const anyDepth = pattern.startsWith('**/');
1727
+ let body = anyDepth ? pattern.slice(3) : pattern;
1728
+ const beneath = body.endsWith('/**');
1729
+ const directory = beneath || body.endsWith('/');
1730
+ body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
1731
+ const anchored = (beneath && !anyDepth) || body.includes('/');
1732
+ if (body.startsWith('/') && !anyDepth)
1733
+ body = body.slice(1);
1734
+ const segments = body.split('/');
1735
+ if (segments.some((s) => s === '' || s === '.' || s === '..'))
1736
+ return 'it has an empty, "." or ".." segment, which names no entry';
1737
+ if (body.includes('**') || (anyDepth && anchored))
1738
+ return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
1739
+ const glob = (s) => /[*?]/.test(s)
1740
+ ? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
1741
+ : s;
1742
+ return { segments: segments.map(glob), anchored, directory };
1743
+ }
1744
+ function nameMatches(segment, name) {
1745
+ return typeof segment === 'string' ? segment === name : segment.test(name);
1746
+ }
1747
+ function matchesEntry(pattern, segments, directory) {
1748
+ if (pattern.directory && !directory)
1749
+ return false;
1750
+ if (!pattern.anchored)
1751
+ return nameMatches(pattern.segments[0], segments[segments.length - 1]);
1752
+ return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]));
1753
+ }
1754
+ export function patternRefusal(pattern, list) {
1755
+ if (typeof pattern !== 'string')
1756
+ return "it isn't a string";
1757
+ const reading = readPattern(pattern);
1758
+ if (typeof reading === 'string')
1759
+ return reading;
1760
+ if (list === 'ignore' && pattern.includes('[')) {
1761
+ return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
1762
+ }
1763
+ return undefined;
1764
+ }
1765
+ export function directoryAt(path, followSymlinks) {
1766
+ if (!followSymlinks)
1767
+ return lstat(path).then((s) => s.isDirectory(), () => undefined);
1768
+ return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
1548
1769
  }
1549
1770
  /**
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.
1771
+ * Whether a mount's include covers a mount-relative path, read as a file: a
1772
+ * pattern that matches only directories covers what lies beneath a directory
1773
+ * it matches, and any other covers a file by its own name or path, and
1774
+ * nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
1775
+ * and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
1776
+ * `dir/**`. A directory is no file, so it needs no covering: where one is a
1777
+ * scope, refusesAsOutside (index.ts) decides.
1558
1778
  */
1559
1779
  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)))
1780
+ const segments = relativePath.split('/');
1781
+ return include.some((p) => {
1782
+ const pattern = readPattern(p);
1783
+ if (typeof pattern === 'string')
1784
+ return false;
1785
+ if (!pattern.directory)
1786
+ return matchesEntry(pattern, segments, false);
1787
+ for (let k = 1; k < segments.length; k++)
1788
+ if (matchesEntry(pattern, segments.slice(0, k), true))
1564
1789
  return true;
1565
- }
1566
- else if (pattern === name || (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))) {
1567
- return true;
1568
- }
1569
- }
1570
- return false;
1790
+ return false;
1791
+ });
1571
1792
  }
1572
1793
  /**
1573
1794
  * The ignore patterns a walk that names `scope` reads beneath it: a pattern
@@ -1575,7 +1796,7 @@ function coveredByInclude(relativePath, include) {
1575
1796
  * it (#276); one covering only something inside it does.
1576
1797
  */
1577
1798
  export function ignoreBeneath(scope, patterns) {
1578
- return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
1799
+ return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
1579
1800
  }
1580
1801
  /**
1581
1802
  * Names editors give the files they keep beside the one being edited: vim's
@@ -1594,7 +1815,7 @@ const EDITOR_FILE = /^\..+\.sw[px]$|~$|\.subl.*\.tmp$/;
1594
1815
  /** The walk's reason for an entry the mount's ignore list covers. */
1595
1816
  export const IGNORED = 'ignored by the mount';
1596
1817
  /** The walk's reason for an entry `EDITOR_FILE` names. */
1597
- const EDITOR_FILE_REASON = "an editor's swap or backup file";
1818
+ export const EDITOR_FILE_REASON = "an editor's swap or backup file";
1598
1819
  /**
1599
1820
  * The walk's reason for a link it doesn't enter. On a mount with an include,
1600
1821
  * a link the include's names don't cover is passed over with it, by design,
@@ -1602,7 +1823,7 @@ const EDITOR_FILE_REASON = "an editor's swap or backup file";
1602
1823
  * that follows links, one that leads to a directory, and on one that
1603
1824
  * doesn't, every one.
1604
1825
  */
1605
- const LINK_NOT_WALKED = 'a symlink, not walked';
1826
+ export const LINK_NOT_WALKED = 'a symlink, not walked';
1606
1827
  /** Whether a reason the walk gave an entry is one it passes over by design. */
1607
1828
  export function passedOverByDesign(why) {
1608
1829
  return why === IGNORED || why === EDITOR_FILE_REASON || why === OTHER_MOUNT || why === OUTSIDE_INCLUDE || why === LINK_NOT_WALKED;
@@ -1618,18 +1839,18 @@ export const OTHER_MOUNT = "another mount's directory";
1618
1839
  * Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
1619
1840
  */
1620
1841
  export function inOtherMount(relativePath, others) {
1621
- const lexical = posix.normalize(relativePath).replace(/\/+$/, '');
1622
- return others.some((dir) => lexical === dir || lexical.startsWith(dir + '/'));
1842
+ const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
1843
+ return others.some((dir) => beneathNested(segments, { dir }));
1623
1844
  }
1624
1845
  /**
1625
- * Why a walk passes over an entry by design, by its mount-relative path and
1626
- * its name: another mount's directory, the ignore list, or an editor's file
1627
- * name. Undefined when none covers it.
1846
+ * Why a walk passes over an entry by design, by its mount-relative path, its
1847
+ * name and whether it's a directory: another mount's directory, the ignore
1848
+ * list, or an editor's file name. Undefined when none covers it.
1628
1849
  */
1629
- function passedOverBecause(relativePath, name, patterns, others) {
1850
+ function passedOverBecause(relativePath, name, patterns, others, directory) {
1630
1851
  if (inOtherMount(relativePath, others))
1631
1852
  return OTHER_MOUNT;
1632
- if (isIgnored(relativePath, name, patterns))
1853
+ if (isIgnored(relativePath, patterns, directory))
1633
1854
  return IGNORED;
1634
1855
  if (EDITOR_FILE.test(name))
1635
1856
  return EDITOR_FILE_REASON;
@@ -1637,17 +1858,17 @@ function passedOverBecause(relativePath, name, patterns, others) {
1637
1858
  }
1638
1859
  /**
1639
1860
  * Why a sync without a path, and the watcher, pass over a mount-relative
1640
- * path: the path itself or a directory above it is passed over by design, as
1641
- * the walk reads each entry by its path and name. A path in another mount's
1642
- * directory is that mount's, whatever else covers a directory above it.
1643
- * Undefined when nothing covers it.
1861
+ * path, a directory or not: the path itself or a directory above it is passed
1862
+ * over by design, as the walk reads each entry by its path, name and kind. A
1863
+ * path in another mount's directory is that mount's, whatever else covers a
1864
+ * directory above it. Undefined when nothing covers it.
1644
1865
  */
1645
- export function passedOverAt(relativePath, patterns, others) {
1866
+ export function passedOverAt(relativePath, patterns, others, directory) {
1646
1867
  if (inOtherMount(relativePath, others))
1647
1868
  return OTHER_MOUNT;
1648
1869
  const segments = relativePath.split('/').filter(Boolean);
1649
1870
  for (let i = 0; i < segments.length; i++) {
1650
- const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
1871
+ const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, i < segments.length - 1 || directory);
1651
1872
  if (why !== undefined)
1652
1873
  return why;
1653
1874
  }
@@ -1663,28 +1884,19 @@ export function passedOverBetween(scope, relativePath, patterns, others) {
1663
1884
  const segments = relativePath.split('/').filter(Boolean);
1664
1885
  const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
1665
1886
  for (let i = from; i < segments.length - 1; i++) {
1666
- const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
1887
+ const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, true);
1667
1888
  if (why !== undefined)
1668
1889
  return why;
1669
1890
  }
1670
1891
  return undefined;
1671
1892
  }
1672
- /** Simple ignore patterns, as the watcher and the old walk read them. */
1673
- export function isIgnored(relativePath, name, patterns) {
1674
- for (const pattern of patterns) {
1675
- if (pattern === name)
1676
- return true;
1677
- if (pattern.endsWith('/**')) {
1678
- const prefix = pattern.slice(0, -3);
1679
- if (relativePath === prefix || relativePath.startsWith(prefix + '/'))
1680
- return true;
1681
- }
1682
- if (!pattern.includes('/') && relativePath === pattern)
1683
- return true;
1684
- if (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))
1685
- return true;
1686
- }
1687
- return false;
1893
+ /** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
1894
+ function isIgnored(relativePath, patterns, directory) {
1895
+ const segments = relativePath.split('/');
1896
+ return patterns.some((p) => {
1897
+ const pattern = readPattern(p);
1898
+ return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
1899
+ });
1688
1900
  }
1689
1901
  /**
1690
1902
  * A subdirectory a checked directory's listing showed, checked in turn: still a
@@ -1699,13 +1911,13 @@ async function checkChild(view, rootReal, parent, rel) {
1699
1911
  }
1700
1912
  catch (err) {
1701
1913
  if (errno(err) !== 'ENOENT')
1702
- return { kind: 'unobserved', reason: `cannot stat (${errno(err) ?? String(err)})` };
1914
+ return { kind: 'unobserved', ...failedOn('cannot stat', err) };
1703
1915
  const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
1704
- return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
1916
+ return 'reason' in proof ? { kind: 'unobserved', ...proof } : { kind: 'absent', by: proof.by };
1705
1917
  }
1706
1918
  if (!info.isDirectory())
1707
1919
  return { kind: 'unobserved', reason: CHANGED_WALK };
1708
- return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
1920
+ return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
1709
1921
  }
1710
1922
  /**
1711
1923
  * List a directory scope ('' for the mount root), descending when recursive,
@@ -1734,12 +1946,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1734
1946
  };
1735
1947
  // A walk that isn't named passes over a scope the mount passes over by
1736
1948
  // design, at it or a directory above it, as its parent's listing would pass
1737
- // over its entry: a name rule (a bare-name ignore entry, an editor's name)
1738
- // doesn't match what lies beneath, as a path rule (`cache/**`) does. So a
1739
- // listing, glob or grep there never starts tracking what the mount leaves
1740
- // out, and a stored copy beneath it is unobserved, as beneath any region
1741
- // passed over by design. Its region says how to take what's there in: a
1742
- // sync that names it, whose scope is walked (#276), below.
1949
+ // over its entry. So a listing, glob or grep there never starts tracking
1950
+ // what the mount leaves out, and a stored copy beneath it is unobserved, as
1951
+ // beneath any region passed over by design. Its region says how to take
1952
+ // what's there in: a sync that names it, whose scope is walked (#276),
1953
+ // below.
1743
1954
  // Another mount's directory is passed over by every walk, named or not,
1744
1955
  // and no sync of this mount takes it in, so its region gives no advice.
1745
1956
  if (scope !== '' && inOtherMount(scope, view.others)) {
@@ -1748,7 +1959,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1748
1959
  return walk;
1749
1960
  }
1750
1961
  if (!opts.named && scope !== '') {
1751
- const why = passedOverAt(scope, view.ignore, view.others);
1962
+ let why = passedOverAt(scope, view.ignore, view.others, true);
1963
+ if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
1964
+ if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false)
1965
+ why = undefined;
1966
+ }
1752
1967
  if (why !== undefined) {
1753
1968
  walk.others.set(scope, why);
1754
1969
  walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
@@ -1761,7 +1976,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1761
1976
  return walk;
1762
1977
  }
1763
1978
  if (top.kind === 'unobserved') {
1764
- walk.incomplete.push({ path: scope, reason: top.outside ? 'the directory resolves outside the mount' : top.reason, kind: 'error' });
1979
+ walk.incomplete.push({ path: scope, reason: top.outside ? RESOLVES_OUTSIDE : top.reason, kind: 'error', ...(top.errno !== undefined ? { errno: top.errno } : {}) });
1765
1980
  return walk;
1766
1981
  }
1767
1982
  // A scope a path sync names is walked even where the ignore list covers
@@ -1786,11 +2001,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1786
2001
  const visited = new Set();
1787
2002
  /** Paths already reported as errors, so a second visit doesn't report them again. */
1788
2003
  const failed = new Set();
1789
- const fail = (path, reason) => {
2004
+ const fail = (path, reason, code) => {
1790
2005
  if (failed.has(path))
1791
2006
  return;
1792
2007
  failed.add(path);
1793
- walk.incomplete.push({ path, reason, kind: 'error' });
2008
+ walk.incomplete.push({ path, reason, kind: 'error', ...(code !== undefined ? { errno: code } : {}) });
1794
2009
  };
1795
2010
  let left = cap;
1796
2011
  let lastTaken;
@@ -1866,6 +2081,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1866
2081
  const proof = rel === '' ? ROOT_UNAVAILABLE : await confirmAbsent(view, rootReal, rel, 'beneath');
1867
2082
  if (typeof proof === 'string')
1868
2083
  fail(rel, proof);
2084
+ else if ('reason' in proof)
2085
+ fail(rel, proof.reason, proof.errno);
1869
2086
  else {
1870
2087
  walk.missing.add(rel);
1871
2088
  // Nothing was listed, so a later check that finds it again, on the
@@ -1874,7 +2091,8 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1874
2091
  }
1875
2092
  return null;
1876
2093
  }
1877
- fail(rel, `cannot list (${code ?? String(err)})`);
2094
+ const listed = failedOn('cannot list', err);
2095
+ fail(rel, listed.reason, listed.errno);
1878
2096
  return null;
1879
2097
  }
1880
2098
  entries.sort((a, b) => compareNames(a.name, b.name));
@@ -1892,9 +2110,14 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1892
2110
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
1893
2111
  // Another mount's directory is passed over by its real path too, so a
1894
2112
  // 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);
2113
+ // none of that mount's names (nestedRootAtReal).
2114
+ let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
2115
+ ?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
2116
+ if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
2117
+ const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
2118
+ if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true)
2119
+ passedOver = asDirectory;
2120
+ }
1898
2121
  if (passedOver !== undefined) {
1899
2122
  others.push([childRel, passedOver]);
1900
2123
  regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
@@ -1937,7 +2160,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1937
2160
  else {
1938
2161
  const symlink = entry.isSymbolicLink();
1939
2162
  leaves.push([childRel, false]);
1940
- others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
2163
+ others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
1941
2164
  if (symlink)
1942
2165
  regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
1943
2166
  }
@@ -1971,10 +2194,10 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1971
2194
  return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
1972
2195
  };
1973
2196
  // 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.
2197
+ // first descends: a directory the walk reaches by a path that no
2198
+ // comparison of names places in the nested mount's directory, as through a
2199
+ // bind mount, or on a disk whose folding goes past the comparison's, is
2200
+ // still another mount's directory.
1978
2201
  let nestedRoots;
1979
2202
  const isNestedRoot = async (dir) => {
1980
2203
  if (!view.nested?.length)
@@ -2043,7 +2266,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2043
2266
  if (child.kind === 'absent')
2044
2267
  walk.missing.add(sub);
2045
2268
  else
2046
- fail(sub, child.reason);
2269
+ fail(sub, child.reason, child.errno);
2047
2270
  }
2048
2271
  };
2049
2272
  if (after === undefined) {
@@ -2058,6 +2281,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2058
2281
  // directories a listing showed that weren't listed. A walk that doesn't
2059
2282
  // descend leaves its subdirectories by design.
2060
2283
  const capped = `the walk cap (${cap}) was reached`;
2284
+ const raise = "whoever runs this host can raise the mount's walkCap";
2061
2285
  const holdingUntaken = new Set();
2062
2286
  for (const f of walk.seen)
2063
2287
  holdingUntaken.add(parentOf(f));
@@ -2065,7 +2289,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2065
2289
  if (!passedOverByDesign(why) && !walk.othersTaken.has(p))
2066
2290
  holdingUntaken.add(parentOf(p));
2067
2291
  for (const d of holdingUntaken) {
2068
- walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken`, kind: 'cap' });
2292
+ walk.incomplete.push({ path: d, reason: `${capped} before everything here was taken; naming a path beneath it walks only that path, and ${raise}`, kind: 'cap' });
2069
2293
  }
2070
2294
  let unlisted = 0;
2071
2295
  if (recursive) {
@@ -2073,7 +2297,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
2073
2297
  if (visited.has(d) || walk.missing.has(d) || failed.has(d))
2074
2298
  continue;
2075
2299
  unlisted++;
2076
- walk.incomplete.push({ path: d, reason: `not visited: ${capped}`, kind: 'cap' });
2300
+ walk.incomplete.push({ path: d, reason: `not visited: ${capped}; naming this path walks only it, and ${raise}`, kind: 'cap' });
2077
2301
  }
2078
2302
  }
2079
2303
  // The cap stopped the walk only if it left something: a walk whose last