@animalabs/agent-framework 0.26.2 → 0.28.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 (371) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent.d.ts +291 -23
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +596 -61
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +5 -4
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +34 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +36 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/cut-off-round.d.ts +24 -0
  18. package/dist/src/cut-off-round.d.ts.map +1 -0
  19. package/dist/src/cut-off-round.js +87 -0
  20. package/dist/src/cut-off-round.js.map +1 -0
  21. package/dist/src/framework.d.ts +1038 -193
  22. package/dist/src/framework.d.ts.map +1 -1
  23. package/dist/src/framework.js +5967 -1514
  24. package/dist/src/framework.js.map +1 -1
  25. package/dist/src/gate/event-gate.d.ts +405 -46
  26. package/dist/src/gate/event-gate.d.ts.map +1 -1
  27. package/dist/src/gate/event-gate.js +2646 -399
  28. package/dist/src/gate/event-gate.js.map +1 -1
  29. package/dist/src/gate/index.d.ts +3 -2
  30. package/dist/src/gate/index.d.ts.map +1 -1
  31. package/dist/src/gate/index.js +2 -1
  32. package/dist/src/gate/index.js.map +1 -1
  33. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  34. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  35. package/dist/src/gate/recipe-reconcile.js +325 -0
  36. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  37. package/dist/src/gate/reply-judge.d.ts +116 -0
  38. package/dist/src/gate/reply-judge.d.ts.map +1 -0
  39. package/dist/src/gate/reply-judge.js +467 -0
  40. package/dist/src/gate/reply-judge.js.map +1 -0
  41. package/dist/src/gate/types.d.ts +229 -8
  42. package/dist/src/gate/types.d.ts.map +1 -1
  43. package/dist/src/heartbeat/heartbeat.d.ts +184 -0
  44. package/dist/src/heartbeat/heartbeat.d.ts.map +1 -0
  45. package/dist/src/heartbeat/heartbeat.js +752 -0
  46. package/dist/src/heartbeat/heartbeat.js.map +1 -0
  47. package/dist/src/heartbeat/reminder-time.d.ts +5 -0
  48. package/dist/src/heartbeat/reminder-time.d.ts.map +1 -0
  49. package/dist/src/heartbeat/reminder-time.js +107 -0
  50. package/dist/src/heartbeat/reminder-time.js.map +1 -0
  51. package/dist/src/index.d.ts +10 -4
  52. package/dist/src/index.d.ts.map +1 -1
  53. package/dist/src/index.js +7 -2
  54. package/dist/src/index.js.map +1 -1
  55. package/dist/src/mcpl/capability-mask.d.ts.map +1 -1
  56. package/dist/src/mcpl/capability-mask.js +3 -2
  57. package/dist/src/mcpl/capability-mask.js.map +1 -1
  58. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  59. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  60. package/dist/src/mcpl/channel-registry.js +1154 -435
  61. package/dist/src/mcpl/channel-registry.js.map +1 -1
  62. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  63. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  64. package/dist/src/mcpl/conversation-router.js +29 -15
  65. package/dist/src/mcpl/conversation-router.js.map +1 -1
  66. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  67. package/dist/src/mcpl/convert-block.js +14 -2
  68. package/dist/src/mcpl/convert-block.js.map +1 -1
  69. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  70. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  71. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  72. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  73. package/dist/src/mcpl/inbound-source.d.ts +26 -10
  74. package/dist/src/mcpl/inbound-source.d.ts.map +1 -1
  75. package/dist/src/mcpl/inbound-source.js +40 -69
  76. package/dist/src/mcpl/inbound-source.js.map +1 -1
  77. package/dist/src/mcpl/index.d.ts +5 -1
  78. package/dist/src/mcpl/index.d.ts.map +1 -1
  79. package/dist/src/mcpl/index.js +4 -0
  80. package/dist/src/mcpl/index.js.map +1 -1
  81. package/dist/src/mcpl/inference-router.d.ts +14 -2
  82. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  83. package/dist/src/mcpl/inference-router.js +73 -28
  84. package/dist/src/mcpl/inference-router.js.map +1 -1
  85. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  86. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  87. package/dist/src/mcpl/prose-grammar.js +6 -0
  88. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  89. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  90. package/dist/src/mcpl/protocol-family.js +3 -0
  91. package/dist/src/mcpl/protocol-family.js.map +1 -1
  92. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  93. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  94. package/dist/src/mcpl/push-coalescer.js +33 -11
  95. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  96. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  97. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  98. package/dist/src/mcpl/reference-fetcher.js +177 -145
  99. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  100. package/dist/src/mcpl/references.d.ts +380 -46
  101. package/dist/src/mcpl/references.d.ts.map +1 -1
  102. package/dist/src/mcpl/references.js +736 -77
  103. package/dist/src/mcpl/references.js.map +1 -1
  104. package/dist/src/mcpl/server-connection.d.ts +93 -4
  105. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  106. package/dist/src/mcpl/server-connection.js +176 -11
  107. package/dist/src/mcpl/server-connection.js.map +1 -1
  108. package/dist/src/mcpl/tags.d.ts +9 -2
  109. package/dist/src/mcpl/tags.d.ts.map +1 -1
  110. package/dist/src/mcpl/tags.js +12 -2
  111. package/dist/src/mcpl/tags.js.map +1 -1
  112. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  113. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  114. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  115. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  116. package/dist/src/mcpl/tool-presence.d.ts +311 -0
  117. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  118. package/dist/src/mcpl/tool-presence.js +697 -0
  119. package/dist/src/mcpl/tool-presence.js.map +1 -0
  120. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  121. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  122. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  123. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  124. package/dist/src/mcpl/tool-signature.d.ts +46 -0
  125. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  126. package/dist/src/mcpl/tool-signature.js +321 -0
  127. package/dist/src/mcpl/tool-signature.js.map +1 -0
  128. package/dist/src/mcpl/toolset-host.d.ts +251 -0
  129. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  130. package/dist/src/mcpl/toolset-host.js +720 -0
  131. package/dist/src/mcpl/toolset-host.js.map +1 -0
  132. package/dist/src/mcpl/toolsets-module.d.ts +83 -0
  133. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  134. package/dist/src/mcpl/toolsets-module.js +523 -0
  135. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  136. package/dist/src/mcpl/transport.d.ts +2 -2
  137. package/dist/src/mcpl/transport.d.ts.map +1 -1
  138. package/dist/src/mcpl/transport.js +3 -2
  139. package/dist/src/mcpl/transport.js.map +1 -1
  140. package/dist/src/mcpl/types.d.ts +24 -2
  141. package/dist/src/mcpl/types.d.ts.map +1 -1
  142. package/dist/src/mcpl/types.js.map +1 -1
  143. package/dist/src/mcpl/visible-content.d.ts +7 -9
  144. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  145. package/dist/src/mcpl/visible-content.js +21 -9
  146. package/dist/src/mcpl/visible-content.js.map +1 -1
  147. package/dist/src/module-registry.d.ts +27 -9
  148. package/dist/src/module-registry.d.ts.map +1 -1
  149. package/dist/src/module-registry.js +77 -17
  150. package/dist/src/module-registry.js.map +1 -1
  151. package/dist/src/modules/discord/index.js +1 -1
  152. package/dist/src/modules/discord/index.js.map +1 -1
  153. package/dist/src/modules/history/index.d.ts +2 -1
  154. package/dist/src/modules/history/index.d.ts.map +1 -1
  155. package/dist/src/modules/history/index.js +73 -28
  156. package/dist/src/modules/history/index.js.map +1 -1
  157. package/dist/src/modules/history/semantic.d.ts +108 -6
  158. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  159. package/dist/src/modules/history/semantic.js +273 -57
  160. package/dist/src/modules/history/semantic.js.map +1 -1
  161. package/dist/src/modules/workspace/answers.d.ts +22 -0
  162. package/dist/src/modules/workspace/answers.d.ts.map +1 -0
  163. package/dist/src/modules/workspace/answers.js +123 -0
  164. package/dist/src/modules/workspace/answers.js.map +1 -0
  165. package/dist/src/modules/workspace/branch-intent.d.ts +5 -3
  166. package/dist/src/modules/workspace/branch-intent.d.ts.map +1 -1
  167. package/dist/src/modules/workspace/branch-intent.js +6 -2
  168. package/dist/src/modules/workspace/branch-intent.js.map +1 -1
  169. package/dist/src/modules/workspace/direct-write.d.ts +184 -0
  170. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  171. package/dist/src/modules/workspace/direct-write.js +417 -0
  172. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  173. package/dist/src/modules/workspace/disk-agreement.d.ts +33 -11
  174. package/dist/src/modules/workspace/disk-agreement.d.ts.map +1 -1
  175. package/dist/src/modules/workspace/disk-agreement.js +48 -17
  176. package/dist/src/modules/workspace/disk-agreement.js.map +1 -1
  177. package/dist/src/modules/workspace/edit-text.d.ts +27 -12
  178. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  179. package/dist/src/modules/workspace/edit-text.js +61 -29
  180. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  181. package/dist/src/modules/workspace/effects.d.ts +10 -0
  182. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  183. package/dist/src/modules/workspace/effects.js +21 -14
  184. package/dist/src/modules/workspace/effects.js.map +1 -1
  185. package/dist/src/modules/workspace/index.d.ts +308 -78
  186. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  187. package/dist/src/modules/workspace/index.js +2103 -720
  188. package/dist/src/modules/workspace/index.js.map +1 -1
  189. package/dist/src/modules/workspace/observe.d.ts +75 -63
  190. package/dist/src/modules/workspace/observe.d.ts.map +1 -1
  191. package/dist/src/modules/workspace/observe.js +366 -180
  192. package/dist/src/modules/workspace/observe.js.map +1 -1
  193. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  194. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  195. package/dist/src/modules/workspace/read-page.js +219 -0
  196. package/dist/src/modules/workspace/read-page.js.map +1 -0
  197. package/dist/src/modules/workspace/reconcile.d.ts.map +1 -1
  198. package/dist/src/modules/workspace/reconcile.js +26 -16
  199. package/dist/src/modules/workspace/reconcile.js.map +1 -1
  200. package/dist/src/modules/workspace/result-text.d.ts +68 -0
  201. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  202. package/dist/src/modules/workspace/result-text.js +94 -0
  203. package/dist/src/modules/workspace/result-text.js.map +1 -0
  204. package/dist/src/modules/workspace/sync.d.ts +9 -0
  205. package/dist/src/modules/workspace/sync.d.ts.map +1 -1
  206. package/dist/src/modules/workspace/sync.js +14 -0
  207. package/dist/src/modules/workspace/sync.js.map +1 -1
  208. package/dist/src/modules/workspace/text.d.ts +29 -12
  209. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  210. package/dist/src/modules/workspace/text.js +35 -33
  211. package/dist/src/modules/workspace/text.js.map +1 -1
  212. package/dist/src/modules/workspace/types.d.ts +137 -60
  213. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  214. package/dist/src/modules/workspace/types.js.map +1 -1
  215. package/dist/src/modules/workspace/watcher.d.ts.map +1 -1
  216. package/dist/src/modules/workspace/watcher.js +8 -3
  217. package/dist/src/modules/workspace/watcher.js.map +1 -1
  218. package/dist/src/prose-drafts.d.ts +4 -3
  219. package/dist/src/prose-drafts.d.ts.map +1 -1
  220. package/dist/src/prose-drafts.js +3 -2
  221. package/dist/src/prose-drafts.js.map +1 -1
  222. package/dist/src/reasoning-loss-notice.d.ts +23 -0
  223. package/dist/src/reasoning-loss-notice.d.ts.map +1 -0
  224. package/dist/src/reasoning-loss-notice.js +50 -0
  225. package/dist/src/reasoning-loss-notice.js.map +1 -0
  226. package/dist/src/reasoning-loss.d.ts +24 -0
  227. package/dist/src/reasoning-loss.d.ts.map +1 -0
  228. package/dist/src/reasoning-loss.js +102 -0
  229. package/dist/src/reasoning-loss.js.map +1 -0
  230. package/dist/src/send-tools.d.ts +3 -2
  231. package/dist/src/send-tools.d.ts.map +1 -1
  232. package/dist/src/send-tools.js +3 -2
  233. package/dist/src/send-tools.js.map +1 -1
  234. package/dist/src/sent-rows.d.ts +59 -0
  235. package/dist/src/sent-rows.d.ts.map +1 -0
  236. package/dist/src/sent-rows.js +145 -0
  237. package/dist/src/sent-rows.js.map +1 -0
  238. package/dist/src/silent-heartbeat.d.ts +17 -10
  239. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  240. package/dist/src/silent-heartbeat.js.map +1 -1
  241. package/dist/src/stable-hash.d.ts +3 -0
  242. package/dist/src/stable-hash.d.ts.map +1 -0
  243. package/dist/src/stable-hash.js +15 -0
  244. package/dist/src/stable-hash.js.map +1 -0
  245. package/dist/src/stored-injections.d.ts +99 -0
  246. package/dist/src/stored-injections.d.ts.map +1 -0
  247. package/dist/src/stored-injections.js +106 -0
  248. package/dist/src/stored-injections.js.map +1 -0
  249. package/dist/src/text-as-stored.d.ts +4 -3
  250. package/dist/src/text-as-stored.d.ts.map +1 -1
  251. package/dist/src/text-as-stored.js +4 -3
  252. package/dist/src/text-as-stored.js.map +1 -1
  253. package/dist/src/thinking-drops.d.ts +74 -0
  254. package/dist/src/thinking-drops.d.ts.map +1 -0
  255. package/dist/src/thinking-drops.js +96 -0
  256. package/dist/src/thinking-drops.js.map +1 -0
  257. package/dist/src/tool-args.d.ts +12 -0
  258. package/dist/src/tool-args.d.ts.map +1 -1
  259. package/dist/src/tool-args.js +35 -1
  260. package/dist/src/tool-args.js.map +1 -1
  261. package/dist/src/tool-image-ledger.d.ts +9 -8
  262. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  263. package/dist/src/tool-image-ledger.js +9 -8
  264. package/dist/src/tool-image-ledger.js.map +1 -1
  265. package/dist/src/tool-presentation.d.ts.map +1 -1
  266. package/dist/src/tool-presentation.js +3 -1
  267. package/dist/src/tool-presentation.js.map +1 -1
  268. package/dist/src/tool-result-guard.d.ts +12 -6
  269. package/dist/src/tool-result-guard.d.ts.map +1 -1
  270. package/dist/src/tool-result-guard.js +44 -17
  271. package/dist/src/tool-result-guard.js.map +1 -1
  272. package/dist/src/tool-result-history.d.ts +176 -24
  273. package/dist/src/tool-result-history.d.ts.map +1 -1
  274. package/dist/src/tool-result-history.js +411 -91
  275. package/dist/src/tool-result-history.js.map +1 -1
  276. package/dist/src/tune-out/coordinator.d.ts +17 -9
  277. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  278. package/dist/src/tune-out/coordinator.js +84 -37
  279. package/dist/src/tune-out/coordinator.js.map +1 -1
  280. package/dist/src/tune-out/tools.d.ts +4 -1
  281. package/dist/src/tune-out/tools.d.ts.map +1 -1
  282. package/dist/src/types/agent.d.ts +217 -7
  283. package/dist/src/types/agent.d.ts.map +1 -1
  284. package/dist/src/types/events.d.ts +33 -9
  285. package/dist/src/types/events.d.ts.map +1 -1
  286. package/dist/src/types/framework.d.ts +39 -3
  287. package/dist/src/types/framework.d.ts.map +1 -1
  288. package/dist/src/types/module.d.ts +95 -9
  289. package/dist/src/types/module.d.ts.map +1 -1
  290. package/dist/src/types/trace.d.ts +91 -3
  291. package/dist/src/types/trace.d.ts.map +1 -1
  292. package/package.json +5 -4
  293. package/src/agent.ts +661 -67
  294. package/src/api/server.ts +5 -4
  295. package/src/auto-continue.ts +43 -0
  296. package/src/code-execution/isolate/scripts.ts +11 -3
  297. package/src/cut-off-round.ts +98 -0
  298. package/src/framework.ts +6168 -1540
  299. package/src/gate/event-gate.ts +2895 -405
  300. package/src/gate/index.ts +10 -1
  301. package/src/gate/recipe-reconcile.ts +355 -0
  302. package/src/gate/reply-judge.ts +512 -0
  303. package/src/gate/types.ts +234 -9
  304. package/src/heartbeat/heartbeat.ts +804 -0
  305. package/src/heartbeat/reminder-time.ts +107 -0
  306. package/src/index.ts +11 -4
  307. package/src/mcpl/capability-mask.ts +3 -2
  308. package/src/mcpl/channel-registry.ts +1222 -468
  309. package/src/mcpl/conversation-router.ts +34 -16
  310. package/src/mcpl/convert-block.ts +14 -1
  311. package/src/mcpl/hook-orchestrator.ts +33 -6
  312. package/src/mcpl/inbound-source.ts +42 -69
  313. package/src/mcpl/index.ts +6 -1
  314. package/src/mcpl/inference-router.ts +79 -34
  315. package/src/mcpl/prose-grammar.ts +7 -0
  316. package/src/mcpl/protocol-family.ts +2 -0
  317. package/src/mcpl/push-coalescer.ts +51 -13
  318. package/src/mcpl/reference-fetcher.ts +201 -162
  319. package/src/mcpl/references.ts +851 -77
  320. package/src/mcpl/server-connection.ts +189 -11
  321. package/src/mcpl/tags.ts +13 -2
  322. package/src/mcpl/tool-lifecycle.ts +21 -0
  323. package/src/mcpl/tool-presence.ts +716 -0
  324. package/src/mcpl/tool-result-normalize.ts +18 -6
  325. package/src/mcpl/tool-signature.ts +316 -0
  326. package/src/mcpl/toolset-host.ts +727 -0
  327. package/src/mcpl/toolsets-module.ts +545 -0
  328. package/src/mcpl/transport.ts +3 -2
  329. package/src/mcpl/types.ts +25 -2
  330. package/src/mcpl/visible-content.ts +22 -9
  331. package/src/module-registry.ts +85 -26
  332. package/src/modules/discord/index.ts +1 -1
  333. package/src/modules/history/index.ts +70 -30
  334. package/src/modules/history/semantic.ts +308 -54
  335. package/src/modules/workspace/answers.ts +133 -0
  336. package/src/modules/workspace/branch-intent.ts +10 -5
  337. package/src/modules/workspace/direct-write.ts +460 -0
  338. package/src/modules/workspace/disk-agreement.ts +52 -21
  339. package/src/modules/workspace/edit-text.ts +84 -35
  340. package/src/modules/workspace/effects.ts +22 -13
  341. package/src/modules/workspace/index.ts +2142 -723
  342. package/src/modules/workspace/observe.ts +381 -177
  343. package/src/modules/workspace/read-page.ts +228 -0
  344. package/src/modules/workspace/reconcile.ts +27 -15
  345. package/src/modules/workspace/result-text.ts +121 -0
  346. package/src/modules/workspace/sync.ts +16 -0
  347. package/src/modules/workspace/text.ts +58 -32
  348. package/src/modules/workspace/types.ts +137 -60
  349. package/src/modules/workspace/watcher.ts +8 -3
  350. package/src/prose-drafts.ts +4 -3
  351. package/src/reasoning-loss-notice.ts +62 -0
  352. package/src/reasoning-loss.ts +120 -0
  353. package/src/send-tools.ts +3 -2
  354. package/src/sent-rows.ts +162 -0
  355. package/src/silent-heartbeat.ts +17 -10
  356. package/src/stable-hash.ts +14 -0
  357. package/src/stored-injections.ts +131 -0
  358. package/src/text-as-stored.ts +4 -3
  359. package/src/thinking-drops.ts +135 -0
  360. package/src/tool-args.ts +34 -1
  361. package/src/tool-image-ledger.ts +9 -8
  362. package/src/tool-presentation.ts +3 -1
  363. package/src/tool-result-guard.ts +45 -17
  364. package/src/tool-result-history.ts +471 -90
  365. package/src/tune-out/coordinator.ts +80 -37
  366. package/src/tune-out/tools.ts +4 -1
  367. package/src/types/agent.ts +220 -7
  368. package/src/types/events.ts +33 -9
  369. package/src/types/framework.ts +36 -3
  370. package/src/types/module.ts +91 -9
  371. package/src/types/trace.ts +94 -3
@@ -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 { 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,10 +62,14 @@ 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';
72
+ export const CHANGED = 'it changed while it was observed';
68
73
  const CHANGED_WALK = 'the directory changed while the walk ran';
69
74
  const BENEATH_SYMLINK = 'beneath a symlink that does not lead to a directory inside the mount';
70
75
  export function errno(err) {
@@ -166,19 +171,19 @@ export async function linkLeads(root, rootReal, link) {
166
171
  * does.
167
172
  */
168
173
  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];
174
+ return nestedHolders(target, nested)[0];
172
175
  }
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);
176
+ function nestedHolders(target, nested) {
177
+ const segments = target.split('/');
178
+ return mayHold(nested.filter((n) => beneathNested(segments, n)), (n) => n.dir.split('/').length, (n) => !n.guessed && surelyBeneath(segments, n.dir.split('/')));
179
+ }
180
+ function mayHold(holders, depth, sure) {
181
+ const sorted = [...holders].sort((a, b) => depth(b) - depth(a) || Number(sure(b)) - Number(sure(a)));
182
+ const surely = sorted.findIndex(sure);
183
+ return surely === -1 ? sorted : sorted.slice(0, surely + 1);
184
+ }
185
+ export function namesHolder(leads, nested) {
186
+ return nestedHolders(leads.target, nested).every((holder) => namesNestedPlace(restBeneath(leads.target, holder), holder.include, leads.directory));
182
187
  }
183
188
  /**
184
189
  * Whether a refusal names a place in a nested mount's directory, `rest`
@@ -195,19 +200,13 @@ export function namesHolder(leads, holder) {
195
200
  export function namesNestedPlace(rest, include, directory) {
196
201
  return directory || insideMount(rest, include);
197
202
  }
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
203
  export function withinIncludes(view, path) {
207
204
  if (!insideMount(path, view.include))
208
205
  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);
206
+ return nestedHolders(path, view.nested ?? []).every((holder) => {
207
+ const rest = restBeneath(path, holder);
208
+ return rest === '' || namesNestedPlace(rest, holder.include, false);
209
+ });
211
210
  }
212
211
  /**
213
212
  * The path a mount's include judges for `given`, one of the mount's paths,
@@ -300,7 +299,7 @@ export async function coversLanding(view, rootReal, real, given) {
300
299
  export function linkWords(name, found, leads, nested = [], include) {
301
300
  const spell = (rel) => `"${rel === '' ? name : `${name}/${rel}`}"`;
302
301
  if (!('link' in found))
303
- return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${found.code})`;
302
+ return `${UNSURE_PREFIX}${name}/${found.unsure}" is a symlink (${systemWords(errnoCode({ code: found.code }), found.code)})`;
304
303
  const link = `${spell(found.link)} is a symlink`;
305
304
  if (leads === null || leads.kind === 'nowhere')
306
305
  return `${link}, which this mount does not follow`;
@@ -308,11 +307,12 @@ export function linkWords(name, found, leads, nested = [], include) {
308
307
  return `${link} leading outside the mount, which this mount does not follow`;
309
308
  const holder = nestedHolder(leads.target, nested);
310
309
  if (holder) {
311
- if (!namesHolder(leads, holder))
310
+ if (!namesHolder(leads, nested))
312
311
  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)}`;
312
+ const rest = restBeneath(leads.target, holder);
313
+ const guess = nestedGuess(name, leads.target.split('/'), holder);
314
+ return `${link} to ${spell(leads.target)}, which this mount does not follow; it ${guess === undefined ? 'lies' : 'may lie'} in mount ` +
315
+ `"${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
316
  }
317
317
  if (!leads.directory && !insideMount(leads.target, include))
318
318
  return `${link}, which this mount does not follow`;
@@ -337,23 +337,81 @@ export async function shownLeads(root, rootReal, found, include) {
337
337
  export async function linkReason(view, rootReal, found) {
338
338
  return linkWords(view.name, found, await shownLeads(view.root, rootReal, found, view.include), view.nested, view.include);
339
339
  }
340
+ export function realSegments(path) {
341
+ return path.split(sep).filter((segment, i) => i === 0 || segment !== '');
342
+ }
343
+ export function beneathRoot(path, root) {
344
+ const segments = realSegments(path);
345
+ const top = realSegments(root);
346
+ return segments.length > top.length && beginsWith(segments, top) ? segments.slice(top.length).join('/') : undefined;
347
+ }
348
+ export function sameRoot(path, root) {
349
+ const segments = realSegments(path);
350
+ const top = realSegments(root);
351
+ return segments.length === top.length && beginsWith(segments, top);
352
+ }
353
+ export function surelyAtRoot(path, root) {
354
+ return surelyBeneath(realSegments(path), realSegments(root));
355
+ }
356
+ export function nestedRootAtReal(real, nested = []) {
357
+ return nestedRootsAtReal(real, nested)[0];
358
+ }
359
+ function nestedRootsAtReal(real, nested) {
360
+ // realpath keeps the spelling it's given where disk folds it on Linux, as
361
+ // from a link's target, so a real root is compared as loosely as a path.
362
+ const segments = realSegments(real);
363
+ const holders = nested.flatMap((n) => {
364
+ if (n.real === undefined)
365
+ return [];
366
+ const root = realSegments(n.real);
367
+ return beginsWith(segments, root) ? [{ mount: n, depth: root.length, sure: surelyBeneath(segments, root) }] : [];
368
+ });
369
+ return mayHold(holders, (h) => h.depth, (h) => h.sure);
370
+ }
340
371
  /**
341
372
  * 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
373
+ * with the path to use there and the path beneath its root (`rest`);
374
+ * undefined when none does. A path the lexical checks pass
344
375
  * (inOtherMount, the tools' holderOf) reaches another mount's directory this
345
376
  * 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.
377
+ * path canonically asks this too.
348
378
  */
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];
379
+ export async function nestedAtReal(real, nested = [], at) {
380
+ const holders = nestedRootsAtReal(real, nested);
381
+ const holder = holders[0];
353
382
  if (!holder)
354
383
  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 };
384
+ const segments = realSegments(real);
385
+ // realpath keeps a link target's spelling beneath the root too, so the
386
+ // rest is read as disk spells it, for each mount's include to judge.
387
+ const rests = await Promise.all(holders.map(async (h) => {
388
+ const given = segments.slice(h.depth).join('/');
389
+ return given === '' ? '' : await diskSpelling(h.mount.real, given);
390
+ }));
391
+ const rest = rests[0];
392
+ const { name } = holder.mount;
393
+ const root = realSegments(holder.mount.real);
394
+ const found = {
395
+ name,
396
+ use: rest ? `${name}/${rest}` : name,
397
+ rest,
398
+ judgedByInclude: holders.some((h) => h.mount.include !== undefined),
399
+ names: (directory) => holders.every((h, i) => namesNestedPlace(rests[i], h.mount.include, directory)),
400
+ };
401
+ if (surelyBeneath(segments, root))
402
+ return found;
403
+ if (at === undefined)
404
+ return { ...found, guess: null };
405
+ const top = realSegments(at.rootReal);
406
+ if (segments.length <= top.length || root.length <= top.length || !top.every((t, i) => segments[i] === t))
407
+ return { ...found, guess: null };
408
+ const within = segments.slice(top.length);
409
+ const below = root.slice(top.length);
410
+ const guess = guessFor(at.name, within, below);
411
+ const leads = guess === undefined ? [at.name, ...within].join('/') : [guess.given, ...within.slice(below.length)].join('/');
412
+ if (!surelyBeneath(root.slice(0, top.length), top))
413
+ return { ...found, guess: { ...placedGuess(at.name, within.slice(0, below.length), guess), leads } };
414
+ return { ...found, guess: guess === undefined ? null : { ...guess, leads } };
357
415
  }
358
416
  /**
359
417
  * Why a mount-relative path that resolves into a nested mount's directory is
@@ -361,7 +419,8 @@ export function nestedAtReal(real, nested = []) {
361
419
  * place. Spelled as the tools take paths, from the mount's name.
362
420
  */
363
421
  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`;
422
+ const verb = holder.guess === undefined ? 'leads' : 'may lead';
423
+ return `"${rel === '' ? name : `${name}/${rel}`}" ${verb} into mount "${holder.name}", nested in mount "${name}": ${nestedStep(holder, { after: ' in its place' })}`;
365
424
  }
366
425
  // ===========================================================================
367
426
  // Disk's spelling of a path
@@ -371,9 +430,8 @@ const sameFile = (a, b) => a.dev === b.dev && a.ino === b.ino;
371
430
  const otherCase = (c) => (c === c.toLowerCase() ? c.toUpperCase() : c.toLowerCase());
372
431
  /**
373
432
  * 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.
433
+ * letter whose own other case is the letter again. `ß`, whose upper case is
434
+ * `SS`, and dotless `ı`, whose upper case `I` lowers to `i`, stay as they are.
377
435
  */
378
436
  const flippedCase = (name) => [...name].map((c) => {
379
437
  const other = otherCase(c);
@@ -388,6 +446,15 @@ const folded = (name) => name.normalize('NFC').toLowerCase();
388
446
  * the same-file check keeps that from naming another file.
389
447
  */
390
448
  export const foldedLoosely = (name) => name.normalize('NFC').toLowerCase().toUpperCase().toLowerCase();
449
+ /**
450
+ * Linux's casefold, which ext4 and f2fs set per directory, drops
451
+ * default-ignorable code points (on every kernel but 6.12 to 6.12.4) and
452
+ * compares names decomposed, in full case folding, and HFS+ passes over some
453
+ * of those code points too. Case mapped on the decomposed name joins marks
454
+ * that come in another order around a combining ypogegrammeni (U+0345),
455
+ * whose upper case is a letter.
456
+ */
457
+ const foldedFully = (name) => name.replace(/\p{Default_Ignorable_Code_Point}/gu, '').normalize('NFD').toLowerCase().toUpperCase().toLowerCase().normalize('NFD');
391
458
  /**
392
459
  * Whether `dir` folds case, told without writing: by looking up flipped a
393
460
  * name it lists whose flipped spelling it doesn't list too (a name with no
@@ -482,19 +549,73 @@ function entryName(paths, prefix, segment) {
482
549
  }
483
550
  return found;
484
551
  }
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)));
552
+ function sameLevel(given, name) {
553
+ return given === name || (given !== '' && foldedFully(given) === foldedFully(name));
554
+ }
555
+ function beginsWith(segments, dir) {
556
+ return segments.length >= dir.length && dir.every((d, i) => sameLevel(segments[i], d));
557
+ }
558
+ export function beneathNested(segments, nested) {
559
+ return beginsWith(segments, nested.dir.split('/'));
560
+ }
561
+ export function restBeneath(path, nested) {
562
+ // By segments, since a name that matches can be another length, as `SS`
563
+ // is beside `ß`.
564
+ return path.split('/').slice(nested.dir.split('/').length).join('/');
565
+ }
566
+ function surelyBeneath(segments, dir) {
567
+ return segments.length >= dir.length && dir.every((d, i) => segments[i] === d);
568
+ }
569
+ function shownByCodePoints(name) {
570
+ return [...name].map((c) => {
571
+ const code = c.codePointAt(0);
572
+ if (c === '\\')
573
+ return '\\\\';
574
+ return code < 0x80 ? c : code > 0xffff ? `\\u{${code.toString(16)}}` : `\\u${code.toString(16).padStart(4, '0')}`;
575
+ }).join('');
576
+ }
577
+ function guessFor(mount, given, dir) {
578
+ if (surelyBeneath(given, dir))
579
+ return undefined;
580
+ const levels = given.slice(0, dir.length);
581
+ const plain = (a, b) => a === b || !/[^\x00-\x7f]/.test(a + b);
582
+ const shown = (name, other) => (plain(name, other) ? name : shownByCodePoints(name));
583
+ return {
584
+ given: [mount, ...levels.map((g, i) => shown(g, dir[i]))].join('/'),
585
+ dir: [mount, ...dir.map((d, i) => shown(d, levels[i]))].join('/'),
586
+ };
587
+ }
588
+ export function nestedGuess(mount, segments, nested) {
589
+ const dir = nested.dir.split('/');
590
+ const guess = guessFor(mount, segments, dir);
591
+ return nested.guessed ? placedGuess(mount, segments.slice(0, dir.length), guess) : guess;
592
+ }
593
+ function placedGuess(mount, levels, guess) {
594
+ return guess === undefined ? { placed: [mount, ...levels].join('/') } : { placed: guess.given, dir: guess.dir };
595
+ }
596
+ export function nestedStep(holder, opts = {}) {
597
+ const { guess } = holder;
598
+ const after = opts.after ?? '';
599
+ if (guess === undefined)
600
+ return `use "${holder.use}"${after}`;
601
+ if (guess === null) {
602
+ return `this workspace can't tell whether where it leads is that mount's directory. ` +
603
+ `If you meant that mount's directory, use "${holder.use}"${after}; if not, ask whoever runs this workspace`;
604
+ }
605
+ const leads = guess.leads === undefined ? '' : `it leads to "${guess.leads}", and `;
606
+ if ('placed' in guess) {
607
+ const check = 'ask whoever runs this workspace to check where the two mounts are configured';
608
+ const outside = guess.dir === undefined ? `"${guess.placed}"` : `"${guess.placed}" and "${guess.dir}"`;
609
+ return `${leads}this workspace can't tell whether "${guess.placed}" is mount "${holder.name}"'s directory, since the two mounts' roots are configured in ` +
610
+ `spellings it can't tell apart. If you meant that mount's directory, use "${holder.use}"${after}. ` +
611
+ (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}`);
612
+ }
613
+ const remedy = `rename one of "${guess.given}" and "${guess.dir}"`;
614
+ const own = opts.named
615
+ ? `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}`
616
+ : `If not, ask whoever runs this workspace to ${remedy}`;
617
+ return `${leads}this workspace can't tell "${guess.given}" from "${guess.dir}", ` +
618
+ `mount "${holder.name}"'s directory. If you meant that mount's directory, use "${holder.use}"${after}. ${own}`;
498
619
  }
499
620
  /**
500
621
  * The name `dir` lists for `segment`: `segment` itself, unless disk folds it
@@ -707,20 +828,26 @@ export class CheckedDir {
707
828
  lexical;
708
829
  dev;
709
830
  ino;
831
+ born;
710
832
  constructor(
711
833
  /** Mount-relative path ('' for the root). */
712
834
  rel,
713
835
  /** Its canonical location when checked. */
714
- real, lexical, dev, ino) {
836
+ real, lexical, dev, ino, born) {
715
837
  this.rel = rel;
716
838
  this.real = real;
717
839
  this.lexical = lexical;
718
840
  this.dev = dev;
719
841
  this.ino = ino;
842
+ this.born = born;
720
843
  }
721
844
  /** Which directory it is. */
722
845
  identity() {
723
- return { dev: String(this.dev), ino: String(this.ino) };
846
+ return {
847
+ dev: String(this.dev),
848
+ ino: String(this.ino),
849
+ ...(this.born > 0n ? { born: String(this.born) } : {}),
850
+ };
724
851
  }
725
852
  /** A path that reaches `name` in this directory. */
726
853
  at(name) {
@@ -764,12 +891,26 @@ export class CheckedDir {
764
891
  }
765
892
  }
766
893
  }
894
+ /**
895
+ * What is at a mount root's path now, followed as the root is: for a
896
+ * directory, undefined; for anything else, ENOTDIR; and where the path
897
+ * can't be followed to anything, the errno its stat failed with, ENOENT
898
+ * when nothing is there.
899
+ */
900
+ export async function rootAbsence(root) {
901
+ try {
902
+ return (await stat(root)).isDirectory() ? undefined : 'ENOTDIR';
903
+ }
904
+ catch (err) {
905
+ return errno(err);
906
+ }
907
+ }
767
908
  /** Check the directory at a canonical location, if one is there. */
768
909
  async function checkedAt(view, rel, real) {
769
910
  const info = await lstat(real, { bigint: true }).catch(() => null);
770
911
  if (!info?.isDirectory())
771
912
  return null;
772
- return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino);
913
+ return new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs);
773
914
  }
774
915
  /**
775
916
  * Where a mount directory is now: checked; missing — nothing there, or not a
@@ -784,13 +925,13 @@ async function locateDir(view, rootReal, rel) {
784
925
  catch (err) {
785
926
  const code = errno(err);
786
927
  if (code === 'ENOENT' || code === 'ENOTDIR')
787
- return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE } : { kind: 'missing' };
928
+ return rel === '' ? { kind: 'unobserved', reason: ROOT_UNAVAILABLE, errno: code } : { kind: 'missing' };
788
929
  // A link on the way that can't be resolved, such as a loop, is still a
789
930
  // link this mount doesn't follow.
790
931
  const found = rel === '' ? null : await linkOnTheWay(view, rel);
791
932
  if (found !== null)
792
933
  return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
793
- return { kind: 'unobserved', reason: `cannot resolve the directory (${code ?? String(err)})` };
934
+ return { kind: 'unobserved', reason: `cannot resolve the directory (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
794
935
  }
795
936
  // A mount that doesn't follow symlinks goes through none on the way to a
796
937
  // directory, inside the mount or out of it.
@@ -811,10 +952,10 @@ async function locateDir(view, rootReal, rel) {
811
952
  // mount's, which the mount neither reads nor writes. A place there that
812
953
  // mount's include leaves out is no mount's, and holds nothing beneath it,
813
954
  // as at any file (namesNestedPlace).
814
- const holder = nestedAtReal(real, view.nested);
955
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
815
956
  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' };
957
+ const directory = holder.judgedByInclude && await stat(real).then((info) => info.isDirectory(), () => false);
958
+ return holder.names(directory) ? { kind: 'unobserved', reason: leadsInto(view.name, rel, holder) } : { kind: 'missing' };
818
959
  }
819
960
  }
820
961
  const checked = await checkedAt(view, rel, real);
@@ -842,7 +983,7 @@ export async function checkRoot(view, rootReal) {
842
983
  const found = await locateDir(view, rootReal, '');
843
984
  if (found.kind === 'checked')
844
985
  return { dir: found.dir };
845
- return { reason: found.kind === 'unobserved' ? found.reason : ROOT_UNAVAILABLE };
986
+ return found.kind === 'unobserved' ? { reason: found.reason, ...(found.errno !== undefined ? { errno: found.errno } : {}) } : { reason: ROOT_UNAVAILABLE };
846
987
  }
847
988
  /**
848
989
  * Check the directory at a mount-relative path. With `create`, missing
@@ -879,7 +1020,7 @@ async function createDir(view, rootReal, rel) {
879
1020
  const childRel = parts.slice(0, i + 1).join('/');
880
1021
  // Nothing is made in a nested mount's directory, its root included where
881
1022
  // it isn't made yet, by a path that reaches it through a link.
882
- const holder = nestedAtReal(base.at(parts[i]), view.nested);
1023
+ const holder = await nestedAtReal(base.at(parts[i]), view.nested, { name: view.name, rootReal });
883
1024
  if (holder)
884
1025
  return { kind: 'unobserved', reason: leadsInto(view.name, childRel, holder) };
885
1026
  try {
@@ -888,7 +1029,7 @@ async function createDir(view, rootReal, rel) {
888
1029
  catch (err) {
889
1030
  const code = errno(err);
890
1031
  if (code !== 'EEXIST')
891
- return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${code ?? String(err)})` };
1032
+ return { kind: 'unobserved', reason: `cannot create the directory ${childRel} (${systemWords(code, err)})`, ...(code !== undefined ? { errno: code } : {}) };
892
1033
  }
893
1034
  // Made, or there already (made meanwhile, or not a directory): it must
894
1035
  // be a real directory now, never a symlink or file in its place.
@@ -899,7 +1040,7 @@ async function createDir(view, rootReal, rel) {
899
1040
  const there = view.followSymlinks ? null : await lstat(base.at(parts[i])).catch(() => null);
900
1041
  if (there?.isSymbolicLink())
901
1042
  return { kind: 'unobserved', reason: await linkReason(view, rootReal, { link: childRel }) };
902
- return { kind: 'unobserved', reason: 'a parent path is not a directory' };
1043
+ return { kind: 'unobserved', reason: NOT_A_DIRECTORY };
903
1044
  }
904
1045
  base = checked;
905
1046
  }
@@ -947,14 +1088,14 @@ export async function confirmAbsent(view, rootReal, rel, target = 'entry') {
947
1088
  if (code === 'ENOTDIR')
948
1089
  return CHANGED;
949
1090
  if (code !== 'ENOENT')
950
- return `cannot stat (${code ?? String(err)})`;
1091
+ return `cannot stat (${systemWords(code, err)})`;
951
1092
  // Absent from a directory that, like each above it, is still the one its path names.
952
1093
  return proven();
953
1094
  }
954
1095
  if (info.isDirectory()) {
955
1096
  if (last)
956
1097
  return CHANGED; // present after all
957
- cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino);
1098
+ cur = new CheckedDir(childRel, cur.at(name), lexicalOf(view, childRel), info.dev, info.ino, info.birthtimeNs);
958
1099
  walked.push(cur);
959
1100
  continue;
960
1101
  }
@@ -1064,13 +1205,13 @@ async function parentPlacement(view, rootReal, relativePath) {
1064
1205
  return { reason: await linkReason(view, rootReal, found) };
1065
1206
  if (!contained(rootReal, parent))
1066
1207
  return { reason: OUTSIDE_PARENT };
1067
- const holder = nestedAtReal(parent, view.nested);
1208
+ const holder = await nestedAtReal(parent, view.nested, { name: view.name, rootReal });
1068
1209
  if (!holder)
1069
1210
  return { inside: parent };
1070
1211
  // A place there that the nested mount's include leaves out is no mount's,
1071
1212
  // 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';
1213
+ const directory = holder.judgedByInclude && await stat(parent).then((info) => info.isDirectory(), () => false);
1214
+ return holder.names(directory) ? { reason: leadsInto(view.name, parentOf(relativePath), holder) } : 'gone';
1074
1215
  }
1075
1216
  catch (err) {
1076
1217
  const code = errno(err);
@@ -1081,7 +1222,7 @@ async function parentPlacement(view, rootReal, relativePath) {
1081
1222
  const found = await linkOnTheWay(view, relativePath, 'parents');
1082
1223
  if (found !== null)
1083
1224
  return { reason: await linkReason(view, rootReal, found) };
1084
- return { reason: `cannot resolve the parent directory (${code ?? String(err)})` };
1225
+ return { reason: `cannot resolve the parent directory (${systemWords(code, err)})` };
1085
1226
  }
1086
1227
  }
1087
1228
  /**
@@ -1105,7 +1246,7 @@ export async function observePath(view, rootReal, relativePath) {
1105
1246
  const code = errno(err);
1106
1247
  if (code === 'ENOENT' || code === 'ENOTDIR')
1107
1248
  return absence(view, rootReal, relativePath);
1108
- return { kind: 'unobserved', reason: `cannot stat (${code ?? String(err)})` };
1249
+ return { kind: 'unobserved', reason: `cannot stat (${systemWords(code, err)})` };
1109
1250
  }
1110
1251
  if (info.isSymbolicLink()) {
1111
1252
  if (!view.followSymlinks)
@@ -1115,43 +1256,43 @@ export async function observePath(view, rootReal, relativePath) {
1115
1256
  target = await stat(lexical, { bigint: true });
1116
1257
  }
1117
1258
  catch (err) {
1118
- return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${errno(err) ?? String(err)})` };
1259
+ return { kind: 'unobserved', reason: `a symlink whose target cannot be read (${systemWords(errno(err), err)})` };
1119
1260
  }
1120
1261
  let real;
1121
1262
  try {
1122
1263
  real = await realpath(lexical);
1123
1264
  }
1124
1265
  catch (err) {
1125
- return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${errno(err) ?? String(err)})` };
1266
+ return { kind: 'unobserved', reason: `a symlink that cannot be resolved (${systemWords(errno(err), err)})` };
1126
1267
  }
1127
1268
  if (!contained(rootReal, real))
1128
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1129
- const holder = nestedAtReal(real, view.nested);
1269
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1270
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
1130
1271
  // A place a nested mount's include leaves out is no mount's, so the link
1131
1272
  // leaves this one (namesNestedPlace).
1132
1273
  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' };
1274
+ return { kind: 'unobserved', reason: holder.names(target.isDirectory()) ? leadsInto(view.name, relativePath, holder) : LEAVES_MOUNT };
1134
1275
  }
1135
1276
  if (!target.isFile()) {
1136
- return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : 'not a regular file' };
1277
+ return { kind: 'unobserved', reason: target.isDirectory() ? 'a symlink to a directory, not walked' : NOT_REGULAR };
1137
1278
  }
1138
1279
  // A file this mount's own include leaves out where the link lands is no
1139
1280
  // file of this mount's (coversLanding). It's judged here, and not only
1140
1281
  // where the file is opened, since a pass takes a copy whose fingerprint
1141
1282
  // the stat vouches for without opening it.
1142
1283
  if (!(await coversLanding(view, rootReal, real, relativePath)))
1143
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1284
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1144
1285
  info = target;
1145
1286
  }
1146
1287
  else if (info.isDirectory()) {
1147
- return { kind: 'unobserved', reason: 'a directory' };
1288
+ return { kind: 'unobserved', reason: DIRECTORY };
1148
1289
  }
1149
1290
  else if (!info.isFile()) {
1150
- return { kind: 'unobserved', reason: 'not a regular file' };
1291
+ return { kind: 'unobserved', reason: NOT_REGULAR };
1151
1292
  }
1152
1293
  else if (!(await coversLanding(view, rootReal, join(placement.inside, basename(lexical)), relativePath))) {
1153
1294
  // So is one reached through a link among its directories.
1154
- return { kind: 'unobserved', reason: 'a symlink that leaves the mount' };
1295
+ return { kind: 'unobserved', reason: LEAVES_MOUNT };
1155
1296
  }
1156
1297
  return { kind: 'file', size: Number(info.size), mtimeMs: Number(info.mtimeMs), mode: Number(info.mode & 511n), fp: statFingerprint(info) };
1157
1298
  }
@@ -1187,8 +1328,8 @@ async function openContained(view, rootReal, relativePath) {
1187
1328
  return { kind: 'unobserved', reason: found === null ? NOT_FOLLOWED : await linkReason(view, rootReal, found) };
1188
1329
  }
1189
1330
  if (code === 'EISDIR')
1190
- return { kind: 'unobserved', reason: 'a directory' };
1191
- return { kind: 'unobserved', reason: `cannot open (${code ?? String(err)})` };
1331
+ return { kind: 'unobserved', reason: DIRECTORY };
1332
+ return { kind: 'unobserved', reason: `cannot open (${systemWords(code, err)})` };
1192
1333
  }
1193
1334
  let keep = false;
1194
1335
  try {
@@ -1205,15 +1346,15 @@ async function openContained(view, rootReal, relativePath) {
1205
1346
  if (found !== null)
1206
1347
  return { kind: 'unobserved', reason: await linkReason(view, rootReal, found) };
1207
1348
  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;
1349
+ return { kind: 'unobserved', reason: info.isDirectory() ? DIRECTORY : NOT_REGULAR };
1350
+ const leaves = view.followSymlinks ? LEAVES_MOUNT : OUTSIDE_PARENT;
1210
1351
  if (!contained(rootReal, real))
1211
1352
  return { kind: 'unobserved', reason: leaves };
1212
- const holder = nestedAtReal(real, view.nested);
1353
+ const holder = await nestedAtReal(real, view.nested, { name: view.name, rootReal });
1213
1354
  // A place a nested mount's include leaves out is no mount's. Reached
1214
1355
  // through a link this mount doesn't follow, the link is what's refused.
1215
1356
  if (holder)
1216
- return { kind: 'unobserved', reason: namesNestedPlace(holder.rest, holder.include, false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
1357
+ return { kind: 'unobserved', reason: holder.names(false) ? leadsInto(view.name, relativePath, holder) : view.followSymlinks ? leaves : NOT_FOLLOWED };
1217
1358
  // So is a file this mount's own include leaves out (coversLanding).
1218
1359
  if (!(await coversLanding(view, rootReal, real, relativePath)))
1219
1360
  return { kind: 'unobserved', reason: view.followSymlinks ? leaves : NOT_FOLLOWED };
@@ -1239,7 +1380,7 @@ async function openContained(view, rootReal, relativePath) {
1239
1380
  const code = errno(err);
1240
1381
  if (code === 'ENOENT')
1241
1382
  return absence(view, rootReal, relativePath);
1242
- return { kind: 'unobserved', reason: `cannot read (${code ?? String(err)})` };
1383
+ return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
1243
1384
  }
1244
1385
  finally {
1245
1386
  if (!keep)
@@ -1340,7 +1481,7 @@ export async function readPath(view, rootReal, relativePath, limit) {
1340
1481
  const code = errno(err);
1341
1482
  if (code === 'ENOENT')
1342
1483
  return absence(view, rootReal, relativePath);
1343
- return { kind: 'unobserved', reason: `cannot read (${code ?? String(err)})` };
1484
+ return { kind: 'unobserved', reason: `cannot read (${systemWords(code, err)})` };
1344
1485
  }
1345
1486
  finally {
1346
1487
  await handle.close();
@@ -1382,7 +1523,7 @@ export async function readBytes(view, rootReal, relativePath, limit) {
1382
1523
  return { kind: 'bytes', size: bytes.byteLength, bytes, head: bytes.subarray(0, HEAD_BYTES) };
1383
1524
  }
1384
1525
  catch (err) {
1385
- return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
1526
+ return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
1386
1527
  }
1387
1528
  finally {
1388
1529
  await handle.close();
@@ -1404,7 +1545,7 @@ export async function readHead(view, rootReal, relativePath) {
1404
1545
  return { kind: 'head', size: Number(info.size), mtimeMs: Number(info.mtimeMs), head: head.subarray(0, bytesRead) };
1405
1546
  }
1406
1547
  catch (err) {
1407
- return { kind: 'unobserved', reason: `cannot read (${errno(err) ?? String(err)})` };
1548
+ return { kind: 'unobserved', reason: `cannot read (${systemWords(errno(err), err)})` };
1408
1549
  }
1409
1550
  finally {
1410
1551
  await handle.close();
@@ -1480,14 +1621,14 @@ function subtreeAgainst(dir, at) {
1480
1621
  return 'holds';
1481
1622
  }
1482
1623
  /**
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.
1624
+ * Whether a mount's ignore list covers a mount-relative path: the path itself,
1625
+ * a directory or not, or any directory above it, as the walk's own test reads
1626
+ * each entry by its path and kind.
1486
1627
  */
1487
- export function coveredByIgnore(relativePath, patterns) {
1628
+ export function coveredByIgnore(relativePath, patterns, directory) {
1488
1629
  const segments = relativePath.split('/').filter(Boolean);
1489
1630
  for (let i = 0; i < segments.length; i++) {
1490
- if (isIgnored(segments.slice(0, i + 1).join('/'), segments[i], patterns))
1631
+ if (isIgnored(segments.slice(0, i + 1).join('/'), patterns, i < segments.length - 1 || directory))
1491
1632
  return true;
1492
1633
  }
1493
1634
  return false;
@@ -1530,44 +1671,90 @@ export function insideMount(relativePath, include) {
1530
1671
  return true;
1531
1672
  return coveredByInclude(relativePath, include);
1532
1673
  }
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));
1674
+ const readings = new Map();
1675
+ function readPattern(pattern) {
1676
+ let reading = readings.get(pattern);
1677
+ if (reading === undefined) {
1678
+ reading = parsePattern(pattern);
1679
+ readings.set(pattern, reading);
1680
+ }
1681
+ return reading;
1682
+ }
1683
+ function parsePattern(pattern) {
1684
+ if (pattern.startsWith('!'))
1685
+ return 'a leading "!" makes an exception in a .gitignore, and this list has none, so leave it out';
1686
+ if (pattern.startsWith('#'))
1687
+ return 'a leading "#" marks a comment in a .gitignore, so leave comments out of the list';
1688
+ if (pattern.includes('\\'))
1689
+ 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';
1690
+ if (pattern.endsWith(' '))
1691
+ return 'a .gitignore drops trailing spaces, so remove them';
1692
+ const anyDepth = pattern.startsWith('**/');
1693
+ let body = anyDepth ? pattern.slice(3) : pattern;
1694
+ const beneath = body.endsWith('/**');
1695
+ const directory = beneath || body.endsWith('/');
1696
+ body = body.slice(0, beneath ? -3 : directory ? -1 : undefined);
1697
+ const anchored = (beneath && !anyDepth) || body.includes('/');
1698
+ if (body.startsWith('/') && !anyDepth)
1699
+ body = body.slice(1);
1700
+ const segments = body.split('/');
1701
+ if (segments.some((s) => s === '' || s === '.' || s === '..'))
1702
+ return 'it has an empty, "." or ".." segment, which names no entry';
1703
+ if (body.includes('**') || (anyDepth && anchored))
1704
+ return '"**" is read only as a leading "**/" before one name, or a trailing "/**"';
1705
+ const glob = (s) => /[*?]/.test(s)
1706
+ ? new RegExp(`^${s.replace(/[.+^${}()|[\]*?]/g, (c) => (c === '*' ? '.*' : c === '?' ? '.' : `\\${c}`))}$`, 'su')
1707
+ : s;
1708
+ return { segments: segments.map(glob), anchored, directory };
1709
+ }
1710
+ function nameMatches(segment, name) {
1711
+ return typeof segment === 'string' ? segment === name : segment.test(name);
1712
+ }
1713
+ function matchesEntry(pattern, segments, directory) {
1714
+ if (pattern.directory && !directory)
1715
+ return false;
1716
+ if (!pattern.anchored)
1717
+ return nameMatches(pattern.segments[0], segments[segments.length - 1]);
1718
+ return segments.length === pattern.segments.length && pattern.segments.every((s, i) => nameMatches(s, segments[i]));
1719
+ }
1720
+ export function patternRefusal(pattern, list) {
1721
+ if (typeof pattern !== 'string')
1722
+ return "it isn't a string";
1723
+ const reading = readPattern(pattern);
1724
+ if (typeof reading === 'string')
1725
+ return reading;
1726
+ if (list === 'ignore' && pattern.includes('[')) {
1727
+ return '"[" starts a character class in a .gitignore, which this list doesn\'t read, so write each name out, with "?" for a "[" in it';
1728
+ }
1729
+ return undefined;
1730
+ }
1731
+ export function directoryAt(path, followSymlinks) {
1732
+ if (!followSymlinks)
1733
+ return lstat(path).then((s) => s.isDirectory(), () => undefined);
1734
+ return stat(path).then((s) => s.isDirectory(), () => lstat(path).then(() => false, () => undefined));
1548
1735
  }
1549
1736
  /**
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.
1737
+ * Whether a mount's include covers a mount-relative path, read as a file: a
1738
+ * pattern that matches only directories covers what lies beneath a directory
1739
+ * it matches, and any other covers a file by its own name or path, and
1740
+ * nothing beneath a directory it matches. So `gate.json` covers `x/gate.json`
1741
+ * and not `x/gate.json/run.sh`, and a subtree is named with `dir/` or
1742
+ * `dir/**`. A directory is no file, so it needs no covering: where one is a
1743
+ * scope, refusesAsOutside (index.ts) decides.
1558
1744
  */
1559
1745
  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)))
1746
+ const segments = relativePath.split('/');
1747
+ return include.some((p) => {
1748
+ const pattern = readPattern(p);
1749
+ if (typeof pattern === 'string')
1750
+ return false;
1751
+ if (!pattern.directory)
1752
+ return matchesEntry(pattern, segments, false);
1753
+ for (let k = 1; k < segments.length; k++)
1754
+ if (matchesEntry(pattern, segments.slice(0, k), true))
1564
1755
  return true;
1565
- }
1566
- else if (pattern === name || (pattern.startsWith('*.') && name.endsWith(pattern.slice(1)))) {
1567
- return true;
1568
- }
1569
- }
1570
- return false;
1756
+ return false;
1757
+ });
1571
1758
  }
1572
1759
  /**
1573
1760
  * The ignore patterns a walk that names `scope` reads beneath it: a pattern
@@ -1575,7 +1762,7 @@ function coveredByInclude(relativePath, include) {
1575
1762
  * it (#276); one covering only something inside it does.
1576
1763
  */
1577
1764
  export function ignoreBeneath(scope, patterns) {
1578
- return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern]));
1765
+ return patterns.filter((pattern) => !coveredByIgnore(scope, [pattern], true));
1579
1766
  }
1580
1767
  /**
1581
1768
  * Names editors give the files they keep beside the one being edited: vim's
@@ -1618,18 +1805,18 @@ export const OTHER_MOUNT = "another mount's directory";
1618
1805
  * Every nested-mount check (a pass's, a walk's, a push's, grep's) asks this.
1619
1806
  */
1620
1807
  export function inOtherMount(relativePath, others) {
1621
- const lexical = posix.normalize(relativePath).replace(/\/+$/, '');
1622
- return others.some((dir) => lexical === dir || lexical.startsWith(dir + '/'));
1808
+ const segments = posix.normalize(relativePath).replace(/\/+$/, '').split('/');
1809
+ return others.some((dir) => beneathNested(segments, { dir }));
1623
1810
  }
1624
1811
  /**
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.
1812
+ * Why a walk passes over an entry by design, by its mount-relative path, its
1813
+ * name and whether it's a directory: another mount's directory, the ignore
1814
+ * list, or an editor's file name. Undefined when none covers it.
1628
1815
  */
1629
- function passedOverBecause(relativePath, name, patterns, others) {
1816
+ function passedOverBecause(relativePath, name, patterns, others, directory) {
1630
1817
  if (inOtherMount(relativePath, others))
1631
1818
  return OTHER_MOUNT;
1632
- if (isIgnored(relativePath, name, patterns))
1819
+ if (isIgnored(relativePath, patterns, directory))
1633
1820
  return IGNORED;
1634
1821
  if (EDITOR_FILE.test(name))
1635
1822
  return EDITOR_FILE_REASON;
@@ -1637,17 +1824,17 @@ function passedOverBecause(relativePath, name, patterns, others) {
1637
1824
  }
1638
1825
  /**
1639
1826
  * 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.
1827
+ * path, a directory or not: the path itself or a directory above it is passed
1828
+ * over by design, as the walk reads each entry by its path, name and kind. A
1829
+ * path in another mount's directory is that mount's, whatever else covers a
1830
+ * directory above it. Undefined when nothing covers it.
1644
1831
  */
1645
- export function passedOverAt(relativePath, patterns, others) {
1832
+ export function passedOverAt(relativePath, patterns, others, directory) {
1646
1833
  if (inOtherMount(relativePath, others))
1647
1834
  return OTHER_MOUNT;
1648
1835
  const segments = relativePath.split('/').filter(Boolean);
1649
1836
  for (let i = 0; i < segments.length; i++) {
1650
- const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
1837
+ const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, i < segments.length - 1 || directory);
1651
1838
  if (why !== undefined)
1652
1839
  return why;
1653
1840
  }
@@ -1663,28 +1850,19 @@ export function passedOverBetween(scope, relativePath, patterns, others) {
1663
1850
  const segments = relativePath.split('/').filter(Boolean);
1664
1851
  const from = scope === '' ? 0 : scope.split('/').filter(Boolean).length;
1665
1852
  for (let i = from; i < segments.length - 1; i++) {
1666
- const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others);
1853
+ const why = passedOverBecause(segments.slice(0, i + 1).join('/'), segments[i], patterns, others, true);
1667
1854
  if (why !== undefined)
1668
1855
  return why;
1669
1856
  }
1670
1857
  return undefined;
1671
1858
  }
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;
1859
+ /** Whether an ignore pattern matches the entry at a mount-relative path, a directory or not, itself. */
1860
+ function isIgnored(relativePath, patterns, directory) {
1861
+ const segments = relativePath.split('/');
1862
+ return patterns.some((p) => {
1863
+ const pattern = readPattern(p);
1864
+ return typeof pattern !== 'string' && matchesEntry(pattern, segments, directory);
1865
+ });
1688
1866
  }
1689
1867
  /**
1690
1868
  * A subdirectory a checked directory's listing showed, checked in turn: still a
@@ -1699,13 +1877,13 @@ async function checkChild(view, rootReal, parent, rel) {
1699
1877
  }
1700
1878
  catch (err) {
1701
1879
  if (errno(err) !== 'ENOENT')
1702
- return { kind: 'unobserved', reason: `cannot stat (${errno(err) ?? String(err)})` };
1880
+ return { kind: 'unobserved', reason: `cannot stat (${systemWords(errno(err), err)})` };
1703
1881
  const proof = await confirmAbsent(view, rootReal, rel, 'beneath');
1704
1882
  return typeof proof === 'string' ? { kind: 'unobserved', reason: proof } : { kind: 'absent', by: proof.by };
1705
1883
  }
1706
1884
  if (!info.isDirectory())
1707
1885
  return { kind: 'unobserved', reason: CHANGED_WALK };
1708
- return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino) };
1886
+ return { kind: 'checked', dir: new CheckedDir(rel, real, lexicalOf(view, rel), info.dev, info.ino, info.birthtimeNs) };
1709
1887
  }
1710
1888
  /**
1711
1889
  * List a directory scope ('' for the mount root), descending when recursive,
@@ -1734,12 +1912,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1734
1912
  };
1735
1913
  // A walk that isn't named passes over a scope the mount passes over by
1736
1914
  // 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.
1915
+ // over its entry. So a listing, glob or grep there never starts tracking
1916
+ // what the mount leaves out, and a stored copy beneath it is unobserved, as
1917
+ // beneath any region passed over by design. Its region says how to take
1918
+ // what's there in: a sync that names it, whose scope is walked (#276),
1919
+ // below.
1743
1920
  // Another mount's directory is passed over by every walk, named or not,
1744
1921
  // and no sync of this mount takes it in, so its region gives no advice.
1745
1922
  if (scope !== '' && inOtherMount(scope, view.others)) {
@@ -1748,7 +1925,11 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1748
1925
  return walk;
1749
1926
  }
1750
1927
  if (!opts.named && scope !== '') {
1751
- const why = passedOverAt(scope, view.ignore, view.others);
1928
+ let why = passedOverAt(scope, view.ignore, view.others, true);
1929
+ if (why !== undefined && passedOverAt(scope, view.ignore, view.others, false) === undefined) {
1930
+ if ((await directoryAt(join(rootReal, scope), view.followSymlinks)) === false)
1931
+ why = undefined;
1932
+ }
1752
1933
  if (why !== undefined) {
1753
1934
  walk.others.set(scope, why);
1754
1935
  walk.incomplete.push({ path: scope, reason: `${why}: a sync that names this path takes in what's there, editors' files aside`, kind: 'ignored' });
@@ -1892,9 +2073,14 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1892
2073
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
1893
2074
  // Another mount's directory is passed over by its real path too, so a
1894
2075
  // 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);
2076
+ // none of that mount's names (nestedRootAtReal).
2077
+ let passedOver = passedOverBecause(childRel, entry.name, ignore, view.others, entry.isDirectory())
2078
+ ?? (!entry.isSymbolicLink() && nestedRootAtReal(dir.at(entry.name), view.nested) !== undefined ? OTHER_MOUNT : undefined);
2079
+ if (passedOver === undefined && view.followSymlinks && entry.isSymbolicLink()) {
2080
+ const asDirectory = passedOverBecause(childRel, entry.name, ignore, view.others, true);
2081
+ if (asDirectory !== undefined && (await directoryAt(dir.at(entry.name), true)) === true)
2082
+ passedOver = asDirectory;
2083
+ }
1898
2084
  if (passedOver !== undefined) {
1899
2085
  others.push([childRel, passedOver]);
1900
2086
  regions.push({ path: childRel, reason: passedOver, kind: 'ignored' });
@@ -1937,7 +2123,7 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1937
2123
  else {
1938
2124
  const symlink = entry.isSymbolicLink();
1939
2125
  leaves.push([childRel, false]);
1940
- others.push([childRel, symlink ? 'a symlink' : 'not a regular file']);
2126
+ others.push([childRel, symlink ? 'a symlink' : NOT_REGULAR]);
1941
2127
  if (symlink)
1942
2128
  regions.push({ path: childRel, reason: LINK_NOT_WALKED, kind: 'symlink' });
1943
2129
  }
@@ -1971,10 +2157,10 @@ export async function walkScope(view, rootReal, scope, recursive, cap, opts = {}
1971
2157
  return at === 'holds' || at === (stretch === 'after' ? 'after' : 'before');
1972
2158
  };
1973
2159
  // 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.
2160
+ // first descends: a directory the walk reaches by a path that no
2161
+ // comparison of names places in the nested mount's directory, as through a
2162
+ // bind mount, or on a disk whose folding goes past the comparison's, is
2163
+ // still another mount's directory.
1978
2164
  let nestedRoots;
1979
2165
  const isNestedRoot = async (dir) => {
1980
2166
  if (!view.nested?.length)