functionalscript 0.45.0 → 0.46.1

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 (296) hide show
  1. package/README.md +5 -3
  2. package/fjs/asn.1/module.f.mjs +8 -1
  3. package/fjs/asn.1/proof.f.d.mts +10 -0
  4. package/fjs/asn.1/proof.f.mjs +16 -0
  5. package/fjs/basen/base128/module.f.mjs +22 -5
  6. package/fjs/bnf/data/module.f.d.mts +32 -6
  7. package/fjs/bnf/data/module.f.mjs +112 -7
  8. package/fjs/bnf/data/proof.f.d.mts +2 -0
  9. package/fjs/bnf/data/proof.f.mjs +74 -3
  10. package/fjs/bnf/data/types.d.ts +20 -2
  11. package/fjs/bnf/descent/module.f.d.mts +28 -14
  12. package/fjs/bnf/descent/module.f.mjs +122 -77
  13. package/fjs/bnf/descent/proof.f.d.mts +2 -0
  14. package/fjs/bnf/descent/proof.f.mjs +117 -31
  15. package/fjs/bnf/descent/types.d.ts +12 -14
  16. package/fjs/bnf/ll1/module.f.d.mts +64 -22
  17. package/fjs/bnf/ll1/module.f.mjs +214 -154
  18. package/fjs/bnf/ll1/proof.f.d.mts +15 -2
  19. package/fjs/bnf/ll1/proof.f.mjs +323 -149
  20. package/fjs/bnf/ll1/types.d.ts +16 -24
  21. package/fjs/bnf/matcher/module.f.d.mts +66 -0
  22. package/fjs/bnf/matcher/module.f.mjs +81 -0
  23. package/fjs/bnf/matcher/proof.f.d.mts +10 -0
  24. package/fjs/bnf/matcher/proof.f.mjs +79 -0
  25. package/fjs/bnf/matcher/types.d.ts +54 -0
  26. package/fjs/bnf/testlib.f.d.mts +31 -0
  27. package/fjs/bnf/testlib.f.mjs +80 -0
  28. package/fjs/cas/cli/module.f.d.mts +1 -1
  29. package/fjs/cas/cli/module.f.mjs +14 -20
  30. package/fjs/cas/cli/proof.f.d.mts +1 -3
  31. package/fjs/cas/cli/proof.f.mjs +44 -33
  32. package/fjs/cas/evo/module.f.d.mts +64 -18
  33. package/fjs/cas/evo/module.f.mjs +148 -70
  34. package/fjs/cas/evo/proof.f.d.mts +10 -1
  35. package/fjs/cas/evo/proof.f.mjs +305 -223
  36. package/fjs/cas/evo/types.d.ts +45 -9
  37. package/fjs/cas/module.f.d.mts +18 -32
  38. package/fjs/cas/module.f.mjs +129 -128
  39. package/fjs/cas/proof.f.d.mts +11 -8
  40. package/fjs/cas/proof.f.mjs +259 -147
  41. package/fjs/cas/types.d.ts +24 -10
  42. package/fjs/ci/config/module.f.d.mts +2 -2
  43. package/fjs/ci/config/module.f.mjs +2 -2
  44. package/fjs/ci/module.f.d.mts +5 -5
  45. package/fjs/ci/module.f.mjs +8 -7
  46. package/fjs/ci/nix/module.f.d.mts +7 -5
  47. package/fjs/ci/nix/module.f.mjs +13 -12
  48. package/fjs/ci/nix/proof.f.mjs +2 -2
  49. package/fjs/ci/proof.f.mjs +9 -5
  50. package/fjs/cli/module.f.d.mts +4 -6
  51. package/fjs/cli/module.f.mjs +4 -8
  52. package/fjs/cli/proof.f.mjs +17 -16
  53. package/fjs/cli/types.d.ts +2 -3
  54. package/fjs/common/monoid/types.d.ts +1 -1
  55. package/fjs/crypto/hmac/module.f.mjs +2 -2
  56. package/fjs/crypto/sha2/module.f.mjs +3 -1
  57. package/fjs/crypto/sha2/proof.f.d.mts +1 -0
  58. package/fjs/crypto/sha2/proof.f.mjs +24 -0
  59. package/fjs/crypto/sha2/types.d.ts +11 -0
  60. package/fjs/crypto/sign/module.f.mjs +2 -2
  61. package/fjs/dev/module.f.d.mts +13 -4
  62. package/fjs/dev/module.f.mjs +56 -27
  63. package/fjs/dev/update/module.f.d.mts +9 -4
  64. package/fjs/dev/update/module.f.mjs +14 -10
  65. package/fjs/dev/update/proof.f.d.mts +1 -3
  66. package/fjs/dev/update/proof.f.mjs +10 -5
  67. package/fjs/djs/module.f.d.mts +13 -5
  68. package/fjs/djs/module.f.mjs +31 -16
  69. package/fjs/djs/parser/module.f.d.mts +13 -3
  70. package/fjs/djs/parser/module.f.mjs +117 -16
  71. package/fjs/djs/parser/proof.f.d.mts +5 -1
  72. package/fjs/djs/parser/proof.f.mjs +274 -12
  73. package/fjs/djs/parser/types.d.ts +7 -1
  74. package/fjs/djs/proof.f.d.mts +18 -2
  75. package/fjs/djs/proof.f.mjs +187 -12
  76. package/fjs/djs/serializer/module.f.d.mts +25 -8
  77. package/fjs/djs/serializer/module.f.mjs +61 -15
  78. package/fjs/djs/serializer/proof.f.d.mts +5 -0
  79. package/fjs/djs/serializer/proof.f.mjs +23 -1
  80. package/fjs/djs/tokenizer/module.f.d.mts +17 -5
  81. package/fjs/djs/tokenizer/module.f.mjs +99 -60
  82. package/fjs/djs/tokenizer/proof.f.d.mts +1 -1
  83. package/fjs/djs/tokenizer/proof.f.mjs +74 -67
  84. package/fjs/djs/transpiler/module.f.d.mts +12 -7
  85. package/fjs/djs/transpiler/module.f.mjs +82 -56
  86. package/fjs/djs/transpiler/types.d.ts +7 -3
  87. package/fjs/djs/types.d.ts +7 -1
  88. package/fjs/effects/list/module.f.d.mts +14 -11
  89. package/fjs/effects/list/module.f.mjs +13 -11
  90. package/fjs/effects/list/types.d.ts +27 -7
  91. package/fjs/effects/memory/module.f.d.mts +2 -1
  92. package/fjs/effects/memory/module.f.mjs +3 -4
  93. package/fjs/effects/memory/proof.f.mjs +13 -7
  94. package/fjs/effects/memory/types.d.ts +4 -3
  95. package/fjs/effects/mock/module.f.d.mts +20 -4
  96. package/fjs/effects/mock/module.f.mjs +37 -5
  97. package/fjs/effects/mock/types.d.ts +10 -1
  98. package/fjs/effects/module.d.mts +4 -2
  99. package/fjs/effects/module.f.d.mts +466 -280
  100. package/fjs/effects/module.f.mjs +537 -299
  101. package/fjs/effects/module.mjs +2 -1
  102. package/fjs/effects/node/memory/module.d.mts +4 -2
  103. package/fjs/effects/node/memory/module.mjs +6 -3
  104. package/fjs/effects/node/memory/proof.mjs +10 -4
  105. package/fjs/effects/node/module.d.mts +4 -3
  106. package/fjs/effects/node/module.f.d.mts +168 -33
  107. package/fjs/effects/node/module.f.mjs +256 -52
  108. package/fjs/effects/node/module.mjs +57 -34
  109. package/fjs/effects/node/proof.f.d.mts +28 -2
  110. package/fjs/effects/node/proof.f.mjs +161 -42
  111. package/fjs/effects/node/types.d.ts +106 -18
  112. package/fjs/effects/node/virtual/module.f.d.mts +18 -4
  113. package/fjs/effects/node/virtual/module.f.mjs +110 -68
  114. package/fjs/effects/node/virtual/proof.f.d.mts +28 -2
  115. package/fjs/effects/node/virtual/proof.f.mjs +190 -9
  116. package/fjs/effects/proof.f.d.mts +69 -37
  117. package/fjs/effects/proof.f.mjs +410 -130
  118. package/fjs/effects/types.d.ts +161 -33
  119. package/fjs/emergent_testing/module.f.d.mts +19 -12
  120. package/fjs/emergent_testing/module.f.mjs +93 -33
  121. package/fjs/emergent_testing/proof.f.d.mts +21 -7
  122. package/fjs/emergent_testing/proof.f.mjs +166 -32
  123. package/fjs/emergent_testing/types.d.ts +22 -4
  124. package/fjs/fsm/module.f.d.mts +14 -4
  125. package/fjs/fsm/module.f.mjs +54 -37
  126. package/fjs/fsm/proof.f.d.mts +2 -0
  127. package/fjs/fsm/proof.f.mjs +83 -114
  128. package/fjs/js/keywords/module.f.d.mts +52 -0
  129. package/fjs/js/keywords/module.f.mjs +72 -0
  130. package/fjs/js/keywords/proof.f.d.mts +3 -0
  131. package/fjs/js/keywords/proof.f.mjs +13 -0
  132. package/fjs/js/tokenizer/module.f.d.mts +26 -6
  133. package/fjs/js/tokenizer/module.f.mjs +145 -159
  134. package/fjs/js/tokenizer/proof.f.d.mts +1 -0
  135. package/fjs/js/tokenizer/proof.f.mjs +54 -24
  136. package/fjs/js/tokenizer/types.d.ts +33 -24
  137. package/fjs/mcp/cas/module.f.d.mts +1 -6
  138. package/fjs/mcp/cas/module.f.mjs +55 -51
  139. package/fjs/mcp/cas/proof.f.d.mts +15 -0
  140. package/fjs/mcp/cas/proof.f.mjs +174 -0
  141. package/fjs/mcp/evo/module.f.d.mts +19 -10
  142. package/fjs/mcp/evo/module.f.mjs +48 -27
  143. package/fjs/mcp/evo/proof.f.d.mts +6 -1
  144. package/fjs/mcp/evo/proof.f.mjs +115 -31
  145. package/fjs/mcp/module.f.d.mts +4 -4
  146. package/fjs/mcp/module.f.mjs +6 -6
  147. package/fjs/mcp/proof.f.d.mts +5 -3
  148. package/fjs/mcp/proof.f.mjs +112 -46
  149. package/fjs/media/html/module.f.mjs +1 -1
  150. package/fjs/media/json/extended/module.f.d.mts +82 -0
  151. package/fjs/media/json/extended/module.f.mjs +153 -0
  152. package/fjs/media/json/extended/proof.f.d.mts +42 -0
  153. package/fjs/media/json/extended/proof.f.mjs +127 -0
  154. package/fjs/media/json/extended/types.d.ts +23 -0
  155. package/fjs/media/json/module.f.d.mts +8 -2
  156. package/fjs/media/json/module.f.mjs +43 -41
  157. package/fjs/media/json/number/module.f.d.mts +59 -0
  158. package/fjs/media/json/number/module.f.mjs +136 -0
  159. package/fjs/media/json/number/proof.f.d.mts +24 -0
  160. package/fjs/media/json/number/proof.f.mjs +86 -0
  161. package/fjs/media/json/number/types.d.ts +28 -0
  162. package/fjs/media/json/parser/module.f.d.mts +25 -13
  163. package/fjs/media/json/parser/module.f.mjs +114 -70
  164. package/fjs/media/json/parser/proof.f.d.mts +5 -0
  165. package/fjs/media/json/parser/proof.f.mjs +31 -1
  166. package/fjs/media/json/parser/types.d.ts +33 -14
  167. package/fjs/media/json/rtti/module.f.d.mts +1 -1
  168. package/fjs/media/json/rtti/module.f.mjs +1 -1
  169. package/fjs/media/json/rtti/proof.f.mjs +9 -9
  170. package/fjs/media/json/schema/module.f.mjs +3 -13
  171. package/fjs/media/json/schema/proof.f.d.mts +0 -1
  172. package/fjs/media/json/schema/proof.f.mjs +1 -2
  173. package/fjs/media/json/serializer/module.f.d.mts +32 -1
  174. package/fjs/media/json/serializer/module.f.mjs +64 -2
  175. package/fjs/media/json/tokenizer/module.f.mjs +7 -3
  176. package/fjs/media/json/tokenizer/proof.f.d.mts +6 -0
  177. package/fjs/media/json/tokenizer/proof.f.mjs +62 -21
  178. package/fjs/media/json/types.d.ts +36 -10
  179. package/fjs/media/lock/module.f.d.mts +100 -0
  180. package/fjs/media/lock/module.f.mjs +125 -0
  181. package/fjs/media/lock/proof.f.d.mts +33 -0
  182. package/fjs/media/lock/proof.f.mjs +196 -0
  183. package/fjs/media/lock/types.d.ts +15 -0
  184. package/fjs/media/module.f.d.mts +6 -5
  185. package/fjs/media/module.f.mjs +8 -7
  186. package/fjs/media/note/module.f.d.mts +121 -0
  187. package/fjs/media/note/module.f.mjs +131 -0
  188. package/fjs/media/note/proof.f.d.mts +29 -0
  189. package/fjs/media/note/proof.f.mjs +150 -0
  190. package/fjs/media/note/types.d.ts +10 -0
  191. package/fjs/media/proof.f.d.mts +4 -1
  192. package/fjs/media/proof.f.mjs +40 -21
  193. package/fjs/media/revision/module.f.d.mts +78 -7
  194. package/fjs/media/revision/module.f.mjs +119 -12
  195. package/fjs/media/revision/proof.f.d.mts +10 -0
  196. package/fjs/media/revision/proof.f.mjs +88 -0
  197. package/fjs/media/revision/types.d.ts +34 -5
  198. package/fjs/media/type/module.f.d.mts +33 -15
  199. package/fjs/media/type/module.f.mjs +35 -29
  200. package/fjs/media/type/proof.f.d.mts +2 -1
  201. package/fjs/media/type/proof.f.mjs +30 -10
  202. package/fjs/module.f.mjs +29 -8
  203. package/fjs/nanvm/proof.f.mjs +3 -3
  204. package/fjs/nanvm/rust/module.f.mjs +1 -1
  205. package/fjs/nanvm/update/module.f.d.mts +4 -4
  206. package/fjs/nanvm/update/module.f.mjs +8 -9
  207. package/fjs/nanvm/update/proof.f.mjs +4 -3
  208. package/fjs/proof.f.d.mts +3 -3
  209. package/fjs/proof.f.mjs +36 -10
  210. package/fjs/protocol/json_rpc/module.f.d.mts +2 -2
  211. package/fjs/protocol/json_rpc/module.f.mjs +3 -3
  212. package/fjs/protocol/json_rpc/proof.f.mjs +4 -4
  213. package/fjs/protocol/mcp/module.f.d.mts +10 -13
  214. package/fjs/protocol/mcp/module.f.mjs +74 -58
  215. package/fjs/protocol/mcp/proof.f.d.mts +13 -4
  216. package/fjs/protocol/mcp/proof.f.mjs +193 -61
  217. package/fjs/protocol/mcp/stdio/module.f.d.mts +15 -7
  218. package/fjs/protocol/mcp/stdio/module.f.mjs +38 -25
  219. package/fjs/protocol/mcp/stdio/proof.f.d.mts +4 -2
  220. package/fjs/protocol/mcp/stdio/proof.f.mjs +45 -12
  221. package/fjs/protocol/mcp/stdio/types.d.ts +1 -1
  222. package/fjs/protocol/mcp/types.d.ts +17 -6
  223. package/fjs/sul/id/module.f.d.mts +0 -1
  224. package/fjs/sul/id/module.f.mjs +2 -3
  225. package/fjs/sul/level/hash/module.f.mjs +2 -1
  226. package/fjs/sul/level/hash/proof.f.mjs +2 -2
  227. package/fjs/sul/module.f.mjs +18 -13
  228. package/fjs/text/code_point/module.f.d.mts +8 -0
  229. package/fjs/text/code_point/module.f.mjs +8 -1
  230. package/fjs/text/code_point/proof.f.d.mts +1 -0
  231. package/fjs/text/code_point/proof.f.mjs +11 -0
  232. package/fjs/text/sgr/module.f.d.mts +4 -6
  233. package/fjs/text/sgr/module.f.mjs +6 -7
  234. package/fjs/text/utf16/module.f.mjs +7 -2
  235. package/fjs/text/utf16/proof.f.mjs +3 -3
  236. package/fjs/text/utf8/module.f.mjs +4 -2
  237. package/fjs/types/array/module.f.mjs +14 -2
  238. package/fjs/types/bit_vec/module.f.d.mts +0 -2
  239. package/fjs/types/bit_vec/module.f.mjs +46 -32
  240. package/fjs/types/bit_vec/proof.f.mjs +2 -2
  241. package/fjs/types/btree/remove/module.f.mjs +1 -1
  242. package/fjs/types/btree/set/module.f.mjs +9 -11
  243. package/fjs/types/btree/set/proof.f.mjs +12 -0
  244. package/fjs/types/byte_set/module.f.d.mts +11 -4
  245. package/fjs/types/byte_set/module.f.mjs +14 -6
  246. package/fjs/types/byte_set/proof.f.mjs +7 -7
  247. package/fjs/types/function/compare/module.f.mjs +10 -3
  248. package/fjs/types/list/module.f.d.mts +1 -1
  249. package/fjs/types/list/module.f.mjs +1 -1
  250. package/fjs/types/nullable/module.f.d.mts +18 -4
  251. package/fjs/types/nullable/module.f.mjs +21 -4
  252. package/fjs/types/nullable/proof.f.d.mts +4 -0
  253. package/fjs/types/nullable/proof.f.mjs +15 -0
  254. package/fjs/types/object/module.f.d.mts +12 -2
  255. package/fjs/types/object/module.f.mjs +11 -1
  256. package/fjs/types/patricia_trie/module.f.mjs +26 -11
  257. package/fjs/types/result/module.f.d.mts +3 -3
  258. package/fjs/types/result/module.f.mjs +3 -3
  259. package/fjs/types/rtti/common/module.f.d.mts +25 -28
  260. package/fjs/types/rtti/common/module.f.mjs +36 -32
  261. package/fjs/types/rtti/common/proof.f.mjs +5 -5
  262. package/fjs/types/rtti/data/module.f.d.mts +14 -0
  263. package/fjs/types/rtti/data/module.f.mjs +25 -2
  264. package/fjs/types/rtti/data/proof.f.d.mts +1 -0
  265. package/fjs/types/rtti/data/proof.f.mjs +25 -2
  266. package/fjs/types/rtti/parse/module.f.d.mts +24 -14
  267. package/fjs/types/rtti/parse/module.f.mjs +37 -28
  268. package/fjs/types/rtti/parse/proof.f.d.mts +3 -2
  269. package/fjs/types/rtti/parse/proof.f.mjs +33 -14
  270. package/fjs/types/rtti/proof.f.mjs +3 -1
  271. package/fjs/types/rtti/ts/module.f.mjs +13 -17
  272. package/fjs/types/rtti/ts/types.d.ts +14 -1
  273. package/fjs/types/rtti/validate/module.f.d.mts +81 -19
  274. package/fjs/types/rtti/validate/module.f.mjs +107 -61
  275. package/fjs/types/rtti/validate/proof.f.d.mts +13 -10
  276. package/fjs/types/rtti/validate/proof.f.mjs +186 -197
  277. package/fjs/types/sorted_set/module.f.d.mts +18 -0
  278. package/fjs/types/sorted_set/module.f.mjs +22 -0
  279. package/fjs/types/sorted_set/proof.f.d.mts +1 -0
  280. package/fjs/types/sorted_set/proof.f.mjs +16 -1
  281. package/fjs/types/uint8array/module.f.d.mts +1 -1
  282. package/fjs/types/uint8array/module.f.mjs +1 -1
  283. package/fjs/website/module.f.d.mts +3 -3
  284. package/fjs/website/module.f.mjs +4 -7
  285. package/fjs/website/proof.f.mjs +2 -1
  286. package/package.json +2 -2
  287. package/fjs/dev/package_json/module.f.d.mts +0 -39
  288. package/fjs/dev/package_json/module.f.mjs +0 -40
  289. package/fjs/dev/package_json/proof.f.d.mts +0 -6
  290. package/fjs/dev/package_json/proof.f.mjs +0 -32
  291. package/fjs/effects/eff/module.f.d.mts +0 -20
  292. package/fjs/effects/eff/module.f.mjs +0 -70
  293. package/fjs/effects/eff/proof.f.d.mts +0 -15
  294. package/fjs/effects/eff/proof.f.mjs +0 -69
  295. package/fjs/effects/eff/types.d.ts +0 -71
  296. package/fjs/types/rtti/validate/types.d.ts +0 -6
@@ -11,9 +11,10 @@
11
11
  * @module
12
12
  *
13
13
  * @import { Vec } from '../../types/bit_vec/types.ts'
14
- * @import { Effect, Func, Operation } from '../types.ts'
14
+ * @import { Result } from '../../types/result/types.ts'
15
+ * @import { Commands, CommandSet, Effect, Func, NotImplemented, Operation } from '../types.ts'
15
16
  * @import { List } from '../list/types.ts'
16
- * @import { All, Access, Await, Console, CreateExclusive, CreateServer, Dirent, Engine, Env, Exec, ExecResult, Fetch, FileStat, Forever, Fs, Headers, Http, IncomingMessage, Import, IoResult, Listen, MakeDirectoryOptions, Mkdir, Module, Now, NodeOp, NodeProgramOptions, RandomInt, Read, ReadBytes, ReadConsoles, ReadFile, Readdir, ReaddirOptions, RequestListener, Rename, Rm, Sandbox, SandboxResult, Server, ServerResponse, Stat, Test, TestContext, TestFn, Write, WriteBytes, WriteConsoles, WriteFile, _UtfList, _WriteLoop, } from './types.ts'
17
+ * @import { All, Access, Await, Console, CreateExclusive, CreateServer, Dirent, Engine, Env, Exec, ExecResult, Fetch, FileStat, Forever, Fs, Headers, Http, IncomingMessage, Import, IoChannel, IoError, IoErrorInfo, Listen, MakeDirectoryOptions, Mkdir, Module, Now, NodeOp, NodeProgramOptions, RandomInt, Read, ReadBytes, ReadConsoles, ReadFile, Readdir, ReaddirOptions, RequestListener, Rename, Rm, Sandbox, SandboxResult, Server, ServerResponse, Stat, Test, TestContext, TestFn, Write, WriteBytes, WriteConsoles, WriteFile, _UtfList, _WriteLoop } from './types.ts'
17
18
  */
18
19
 
19
20
  import { utf8, utf8ToString } from '../../text/module.f.mjs'
@@ -21,37 +22,153 @@ import { toCodePointList } from '../../text/utf8/module.f.mjs'
21
22
  import { codePointListToString } from '../../text/utf16/module.f.mjs'
22
23
  import { reverse } from '../../types/list/module.f.mjs'
23
24
  import { length } from '../../types/bit_vec/module.f.mjs'
24
- import { ok, error as resultError, mapOk } from '../../types/result/module.f.mjs'
25
- import { do_, mapStep, okStep, pure, step } from '../module.f.mjs'
25
+ import { error as resultError, ok as resultOk, unwrap } from '../../types/result/module.f.mjs'
26
+ import { do_, pure } from '../module.f.mjs'
27
+ import {
28
+ mapStep as ioMapStep, pureError, pureOk, resultMapStep, resultStep, step as ioStep,
29
+ } from '../module.f.mjs'
30
+
31
+ /**
32
+ * Builds a normalized host error. The constructor exists so the shape is
33
+ * written once: every runner reports its failures through it, and a consumer
34
+ * matching on `'ioError'` knows what the payload holds.
35
+ *
36
+ * @type {(info: IoErrorInfo) => IoError}
37
+ */
38
+ export const ioError = info => ['ioError', info]
39
+
40
+ /**
41
+ * Normalizes a **thrown** value into an {@link IoError}: the OS error code when
42
+ * the host attached a string one, and a message that is the `Error`'s own or
43
+ * the value's string form.
44
+ *
45
+ * This is the boundary where an impure runner's `catch` becomes ordinary effect
46
+ * data. Nothing past it sees the thrown object, which is the point — a stack, a
47
+ * `cause`, and arbitrary own properties do not survive a wire hop, and a
48
+ * program that branched on them would be reading the host's implementation
49
+ * rather than the operation's contract.
50
+ *
51
+ * @type {(e: unknown) => IoError}
52
+ */
53
+ export const toIoError = e => {
54
+ const message = e instanceof Error ? e.message : String(e)
55
+ if (typeof e !== 'object' || e === null || !('code' in e) || typeof e.code !== 'string') {
56
+ return ioError({ message })
57
+ }
58
+ return ioError({ code: e.code, message })
59
+ }
26
60
 
27
61
  /**
28
62
  * True if `e` is a "file or directory does not exist" (`ENOENT`) error.
29
63
  *
30
- * Node's filesystem rejections are `Error`s carrying `code: 'ENOENT'`; the
31
- * virtual interpreter mirrors that shape for absent paths. Lets callers swallow
32
- * only the missing-path case (e.g. a fresh store) while propagating genuine
33
- * failures (permissions, corruption) rather than masking them.
64
+ * Node's filesystem rejections are `Error`s carrying `code: 'ENOENT'`, which
65
+ * {@link toIoError} keeps; the virtual interpreter reports the same code for
66
+ * absent paths. Lets callers swallow only the missing-path case (e.g. a fresh
67
+ * store) while propagating genuine failures (permissions, corruption) rather
68
+ * than masking them.
34
69
  *
35
- * @type {(e: unknown) => boolean}
70
+ * A {@link NotImplemented} is never "not found": a runner that cannot perform
71
+ * the operation has not looked for the path at all, so the two must not
72
+ * collapse into one benign branch — which is exactly what a bare `unknown`
73
+ * error channel used to allow.
74
+ *
75
+ * @type {(e: IoChannel) => boolean}
36
76
  */
37
- export const isNotFound = e =>
38
- typeof e === 'object' && e !== null && (/** @type {{ readonly code?: unknown }} */ (e)).code === 'ENOENT'
77
+ export const isNotFound = ([tag, payload]) =>
78
+ tag === 'ioError' && payload.code === 'ENOENT'
79
+
80
+ /**
81
+ * `NodeOp`'s commands as data, so a runner that implements only part of them
82
+ * can still tell an operation it lacks from a `Do` node whose `command` was
83
+ * never a `NodeOp` at all — see `CommandSet` in `../types.ts` for why the
84
+ * distinction needs the set at runtime.
85
+ *
86
+ * Declared as a record because `CommandSet<NodeOp>` is checked for
87
+ * *completeness*: adding a command to `NodeOp` and forgetting it here is a
88
+ * compile error, where an array literal would only have its members checked and
89
+ * would drift silently.
90
+ *
91
+ * @type {CommandSet<NodeOp>}
92
+ */
93
+ const nodeCommandSet = {
94
+ access: null, all: null, await: null, createExclusive: null,
95
+ createServer: null, exec: null, fetch: null, forever: null,
96
+ import: null, listen: null, memCreate: null, memRead: null,
97
+ memWrite: null, mkdir: null, now: null, randomInt: null,
98
+ read: null, readBytes: null, readFile: null, readdir: null,
99
+ rename: null, rm: null, sandbox: null, stat: null,
100
+ test: null, write: null, writeBytes: null, writeFile: null,
101
+ }
102
+
103
+ /**
104
+ * The commands of {@link nodeCommandSet}, in the form a partial runner tests
105
+ * membership against. The cast is the one `Object.keys` always needs: it
106
+ * answers `string[]` for a record whose keys the type system knows exactly.
107
+ *
108
+ * @type {Commands<NodeOp>}
109
+ */
110
+ export const nodeCommands = /** @type {Commands<NodeOp>} */ (Object.keys(nodeCommandSet))
39
111
 
40
112
  // all
41
113
 
42
114
  /**
43
115
  * To run the operation `O` should be known by the runner/engine.
44
- * This is the reason why we merge `O` with `All` in the resulted `Effect`.
116
+ * This is the reason why we merge `O` with `All` in the resulting effect.
45
117
  */
46
118
  export const all =
47
- /** @type {<O extends Operation, T>(...a: readonly Effect<O, T>[]) => Effect<O | All, readonly T[]>} */
48
- (do_('all'))
119
+ // `Func` cannot express a variadic generic operation, so the declared type
120
+ // is written out here and `do_`'s is set aside.
121
+ /** @type {<O extends Operation, T, E>(...a: readonly Effect<O, T, E>[]) => Effect<O | All, readonly Result<T, E>[], NotImplemented>} */
122
+ (/** @type {unknown} */ (do_('all')))
123
+
124
+ /**
125
+ * Collapses a list of results into a result of the list, keeping the **first**
126
+ * error in list order and discarding the later ones.
127
+ *
128
+ * Keeping one is what makes this a `Result` rather than a report: the callers
129
+ * that need it are chains, and a chain has one error channel. A site that wants
130
+ * every failure wants a different return type and should not reach for this.
131
+ *
132
+ * @type {<T, E>(list: readonly Result<T, E>[]) => Result<readonly T[], E>}
133
+ */
134
+ const okList = list => {
135
+ for (const r of list) {
136
+ if (r[0] === 'error') { return r }
137
+ }
138
+ return resultOk(list.map(unwrap))
139
+ }
140
+
141
+ /**
142
+ * {@link all} in the `ok` channel: collects the values when every effect
143
+ * succeeded, and answers with the first failure otherwise.
144
+ *
145
+ * `all` alone cannot serve a fallible chain. Its envelope is the runner's
146
+ * (`OpResult`, saying whether the *operation* could be dispatched), so handing
147
+ * it `Effect`s nests one `Result` inside another and the caller receives
148
+ * `readonly Result<T, E>[]`. That has to be collapsed before the chain can
149
+ * `step` again, and a continuation that forgets to is the value-discarding
150
+ * hazard this migration exists to remove — one level in, where it is harder to
151
+ * see.
152
+ *
153
+ * **Every effect still runs.** The short-circuit is in the *result*, not in the
154
+ * execution: `all` performs them concurrently and this reads the answers once
155
+ * they are all in, so a failure does not cancel its siblings the way it stops
156
+ * the sequential `forEachStep` in `./module.f.mjs`. The error channel
157
+ * unions the runner's
158
+ * `NotImplemented` with the effects' own `E` for the same reason every other
159
+ * step does — either can be what went wrong.
160
+ *
161
+ * @type {<O extends Operation, T, E>(...a: readonly Effect<O, T, E>[]) => Effect<O | All, readonly T[], NotImplemented | E>}
162
+ */
163
+ export const allOk = (...a) =>
164
+ ioStep(all(...a), rs => pure(okList(rs)))
49
165
 
50
166
  /**
51
167
  * @template {Operation} O0
52
168
  * @template T0
53
- * @param {Effect<O0, T0>} a
54
- * @returns {<O1 extends Operation, T1>(b: Effect<O1, T1>) => Effect<O0 | O1 | All, readonly[T0, T1]>}
169
+ * @template E0
170
+ * @param {Effect<O0, T0, E0>} a
171
+ * @returns {<O1 extends Operation, T1, E1>(b: Effect<O1, T1, E1>) => Effect<O0 | O1 | All, readonly[Result<T0, E0>, Result<T1, E1>], NotImplemented>}
55
172
  */
56
173
  export const both = a => b =>
57
174
  /** @type {any} */ (all)(a, b)
@@ -74,14 +191,14 @@ export const readFile = do_('readFile')
74
191
  /**
75
192
  * Reads a file as UTF-8 text.
76
193
  *
77
- * Preserves the `IoResult` instead of unwrapping so callers can pattern-match
78
- * on errors (e.g. convert them into domain-specific errors) or `unwrap` at the
79
- * call site.
194
+ * Preserves the error channel instead of unwrapping so callers can
195
+ * pattern-match on it (e.g. convert a failure into a domain-specific error) or
196
+ * `unwrap` at the call site.
80
197
  *
81
- * @type {(path: string) => Effect<ReadFile, IoResult<string>>}
198
+ * @type {(path: string) => Effect<ReadFile, string, IoChannel>}
82
199
  */
83
200
  export const readUtf8File = path =>
84
- mapStep(readFile(path), mapOk(utf8ToString))
201
+ ioMapStep(readFile(path), utf8ToString)
85
202
 
86
203
  // readdir
87
204
 
@@ -96,7 +213,7 @@ export const writeFile = do_('writeFile')
96
213
  /**
97
214
  * Writes a string to `path` as UTF-8 bytes.
98
215
  *
99
- * @type {(path: string, content: string) => Effect<WriteFile, IoResult<void>>}
216
+ * @type {(path: string, content: string) => Effect<WriteFile, void, IoChannel>}
100
217
  */
101
218
  export const writeUtf8File = (path, content) =>
102
219
  writeFile(path, utf8(content))
@@ -145,21 +262,18 @@ export const writeBytes = do_('writeBytes')
145
262
  const writeLoop = path => {
146
263
  /** @type {_WriteLoop} */
147
264
  const f = (offset, e) =>
148
- step(e, r => {
149
- if (r === undefined) {
150
- return pure(ok(undefined))
151
- }
152
- const { first: [t, v], tail } = r
153
- if (t === 'error') {
154
- return pure(resultError(v))
265
+ ioStep(e, node => {
266
+ if (node === undefined) {
267
+ return pureOk(undefined)
155
268
  }
269
+ const { first: v, tail } = node
156
270
  const lenV = length(v)
157
271
  if ((lenV & 0b111n) !== 0n) {
158
- return pure(resultError('invalid buffer size'))
272
+ return pureError(ioError({ message: 'invalid buffer size' }))
159
273
  }
160
- return step(
274
+ return ioStep(
161
275
  writeBytes(path, offset, v),
162
- okStep(() => f(offset + Number(lenV >> 3n), tail)))
276
+ () => f(offset + Number(lenV >> 3n), tail))
163
277
  })
164
278
  return f
165
279
  }
@@ -167,13 +281,13 @@ const writeLoop = path => {
167
281
  /**
168
282
  * @template {Operation} O
169
283
  * @param {string} path
170
- * @param {List<O, IoResult<Vec>>} e
171
- * @returns {Effect<O | WriteBytes | CreateExclusive, IoResult<void>>}
284
+ * @param {List<O, Vec, IoChannel>} e
285
+ * @returns {Effect<O | WriteBytes | CreateExclusive, void, IoChannel>}
172
286
  */
173
287
  export const writeFromStream = (path, e) =>
174
- step(
288
+ ioStep(
175
289
  createExclusive(path),
176
- okStep(() => /** @type {Effect<O | WriteBytes, IoResult<void>>} */ (writeLoop(path)(0, e))))
290
+ () => writeLoop(path)(0, e))
177
291
 
178
292
  // stat
179
293
 
@@ -204,27 +318,31 @@ export const import_ = do_('import')
204
318
  // write
205
319
 
206
320
  /** Emits a `Write` effect to the given named stream. */
207
- export const write = /** @type {Func<Write>} */ (do_('write'))
321
+ /** @type {Func<Write>} */
322
+ export const write = do_('write')
208
323
 
209
324
  /**
210
325
  * Encodes `s + '\n'` as UTF-8 and emits a `Write` effect to `stream`.
211
326
  * Shared implementation for `log` and `error`.
212
327
  *
213
- * @type {(stream: WriteConsoles) => (s: string) => Effect<Write, void>}
328
+ * @type {(stream: WriteConsoles) => Console}
214
329
  */
215
330
  const writeString = stream => s =>
216
331
  write(stream, utf8(s + '\n'))
217
332
 
218
333
  /** Writes a line to `stdout`. Replaces the retired `Log` effect. */
219
- export const log = /** @type {Console} */ (writeString('stdout'))
334
+ /** @type {Console} */
335
+ export const log = writeString('stdout')
220
336
 
221
337
  /** Writes a line to `stderr`. Replaces the retired `Error` effect. */
222
- export const error = /** @type {Console} */ (writeString('stderr'))
338
+ /** @type {Console} */
339
+ export const error = writeString('stderr')
223
340
 
224
341
  // read
225
342
 
226
343
  /** Emits a `Read` effect, yielding the next input byte or `null` at EOF. */
227
- export const read = /** @type {Func<Read>} */ (do_('read'))
344
+ /** @type {Func<Read>} */
345
+ export const read = do_('read')
228
346
 
229
347
  /** @type {(bytes: _UtfList) => string} */
230
348
  const utf8ListToString = bytes =>
@@ -247,17 +365,20 @@ const lf = 0x0a
247
365
  * reversed and decoded once at the terminator, so a large line costs O(n)
248
366
  * rather than the O(n²) of copying a growing array on every byte.
249
367
  *
250
- * @type {(stream: ReadConsoles) => Effect<Read, string | null>}
368
+ * A failed `read` a runner without the operation — propagates: the line is
369
+ * not silently truncated into a `null` that a caller would read as EOF.
370
+ *
371
+ * @type {(stream: ReadConsoles) => Effect<Read, string | null, NotImplemented>}
251
372
  */
252
373
  export const readLine = stream => {
253
- /** @type {(acc: _UtfList) => Effect<Read, string | null>} */
374
+ /** @type {(acc: _UtfList) => Effect<Read, string | null, NotImplemented>} */
254
375
  const loop = acc =>
255
- step(
376
+ ioStep(
256
377
  read(stream),
257
378
  b => b === null
258
- ? pure(acc === null ? null : utf8ListToString(reverse(acc)))
379
+ ? pureOk(acc === null ? null : utf8ListToString(reverse(acc)))
259
380
  : b === lf
260
- ? pure(utf8ListToString(reverse(acc)))
381
+ ? pureOk(utf8ListToString(reverse(acc)))
261
382
  : loop({ first: b, tail: acc })
262
383
  )
263
384
  return loop(null)
@@ -295,9 +416,9 @@ export const sandbox = do_('sandbox')
295
416
  /** @type {Func<Await>} */
296
417
  const awaitPromise = do_('await')
297
418
 
298
- /** @type {(p: unknown) => Effect<Await, unknown>} */
419
+ /** @type {(p: unknown) => Effect<Await, unknown, NotImplemented>} */
299
420
  export const awaitIfPromise = p =>
300
- mapStep(awaitPromise(p), ([x]) => x)
421
+ ioMapStep(awaitPromise(p), ([x]) => x)
301
422
 
302
423
  // Test registration
303
424
 
@@ -307,14 +428,97 @@ export const test = do_('test')
307
428
  // Node
308
429
 
309
430
  /**
310
- * Writes an error line to `stderr` and yields exit code `1`. The canonical
431
+ * Writes an error line to `stderr` and fails with exit code `1`. The canonical
311
432
  * "fail with a message" program for a `NodeProgram`. For non-`1` exit codes,
312
- * compose `mapStep(error(s), () => n)` directly.
433
+ * compose `resultMapStep(error(s), () => resultError(n))` directly.
434
+ *
435
+ * **It never succeeds, and the type says so.** `E` is `number` and `T` is
436
+ * `never`, so `step`'s continuation takes a `never` and can never run. That is
437
+ * a continuation nobody reaches, not a compile error: writing one still type-
438
+ * checks, because a function accepting `never` accepts anything. What the type
439
+ * buys is that no *value* can be invented for the success branch, so nothing
440
+ * downstream can proceed as if this had succeeded.
441
+ *
442
+ * **The write's own outcome is deliberately discarded**, which is why this is
443
+ * `resultMapStep` rather than `mapStep`. The program is already failing and
444
+ * the exit code is `1` whether or not `stderr` accepted the bytes; propagating
445
+ * here would hand every caller a "failed to report a failure" branch with no
446
+ * better answer available to it than the one taken here.
313
447
  *
314
- * @type {(s: string) => Effect<Write, number>}
448
+ * @type {(s: string) => Effect<Write, never, number>}
315
449
  */
316
450
  export const errorExit = s =>
317
- mapStep(error(s), () => 1)
451
+ resultMapStep(error(s), () => resultError(1))
452
+
453
+ /**
454
+ * The exit code a {@link Program} answered, from whichever branch it came.
455
+ *
456
+ * `Result<0, number>` puts a number at `[1]` on both sides — `ok(0)` for
457
+ * success, `error(n)` for failure — so reading the code never asks which
458
+ * branch produced it, while a caller that cares *whether* it failed still asks
459
+ * `[0]`. That is why the success type is the literal `0` rather than `void`.
460
+ *
461
+ * A non-zero code belongs in the `error` branch and `0` in the `ok` branch; the
462
+ * type cannot say so, since there is no "non-zero number", and nothing depends
463
+ * on it — this reads `[1]` either way.
464
+ *
465
+ * @type {(r: Result<0, number>) => number}
466
+ */
467
+ export const exitCode = ([, code]) => code
468
+
469
+ /**
470
+ * Renders a channel error as a human line: an {@link IoError}'s own message, or
471
+ * the command name a runner could not dispatch.
472
+ *
473
+ * @type {(e: IoChannel) => string}
474
+ */
475
+ export const errorMessage = ([tag, payload]) =>
476
+ tag === 'notImplemented' ? `operation not implemented: ${payload}` : payload.message
477
+
478
+ /**
479
+ * Renders a channel error for a **remote** caller: the command name for a
480
+ * {@link NotImplemented}, the OS error code for an `IoError`, and nothing else.
481
+ *
482
+ * {@link errorMessage} is for the operator of the program, who is entitled to
483
+ * the host's own words — including the path that failed. A protocol client is
484
+ * not, and the difference is not stylistic: `payload.message` is where the
485
+ * host puts the absolute path it could not read, so answering an MCP tool call
486
+ * with it publishes the server's filesystem layout to whoever is on the other
487
+ * end. The code (`ENOENT`, `EACCES`) says *what* went wrong without saying
488
+ * *where*, which is the part a client can act on anyway.
489
+ *
490
+ * A host that attached no code leaves nothing safe to forward, so the answer is
491
+ * the bare kind. That is deliberate: guessing which part of a free-text message
492
+ * is path-free is exactly the mistake this exists to prevent.
493
+ *
494
+ * @type {(e: IoChannel) => string}
495
+ */
496
+ export const errorSummary = ([tag, payload]) =>
497
+ tag === 'notImplemented'
498
+ ? `operation not implemented: ${payload}`
499
+ : payload.code === undefined ? 'io error' : `io error: ${payload.code}`
500
+
501
+ /**
502
+ * Ends a program with an exit code that reflects `e`: `ok` yields `0`, and a
503
+ * failure is reported on `stderr` and yields `1` ({@link errorExit}).
504
+ *
505
+ * This is the exit-code policy a `NodeProgram` needs at the end of its chain,
506
+ * and the reason a program does not have to invent one per command. It is the
507
+ * counterpart of {@link isNotFound} at the other end of the channel: where that
508
+ * one asks which failure this is, this one stops asking and reports.
509
+ *
510
+ * @type {<O extends Operation, T>(e: Effect<O, T, IoChannel>) => Effect<O | Write, 0, number>}
511
+ */
512
+ export const exitStep = e =>
513
+ resultStep(e, r => {
514
+ // Bound rather than returned inline: the two branches are
515
+ // `Effect<Write, never, number>` and `Effect<never, 0, never>`, both
516
+ // assignable to this, but `step` infers its continuation's type from
517
+ // the union and picks neither.
518
+ /** @type {Effect<Write, 0, number>} */
519
+ const code = r[0] === 'error' ? errorExit(errorMessage(r[1])) : pureOk(0)
520
+ return code
521
+ })
318
522
 
319
523
  /** @type {(version: string) => readonly number[]} */
320
524
  const versionParts = version =>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Node.js effect runner: interprets `Effect<NodeOp, T>` directly against the
2
+ * Node.js effect runner: interprets `Effect<NodeOp, T, E>` directly against the
3
3
  * Node globals and built-in modules (`fs`, `http`, `child_process`, `process`,
4
4
  * `fetch`, …).
5
5
  *
@@ -13,7 +13,7 @@
13
13
  * @module
14
14
  *
15
15
  * @import { Effect } from '../types.ts'
16
- * @import { Server as EffectServer, Headers, Module, NodeOp, RequestListener as Erl, NodeProgram, NodeProgramOptions, WriteConsoles, TestContext, TestFn, } from './types.ts'
16
+ * @import { IoResult, Server as EffectServer, Headers, Module, NodeOp, RequestListener as Erl, NodeProgram, NodeProgramOptions, WriteConsoles, TestContext, TestFn, } from './types.ts'
17
17
  * @import { Result } from '../../types/result/types.ts'
18
18
  * @import { StringMap } from '../../types/object/types.ts'
19
19
  */
@@ -30,9 +30,9 @@ import * as testContext from 'node:test'
30
30
  import { concat, normalize, toPosix } from '../../path/module.f.mjs'
31
31
  import { asyncRun } from '../module.mjs'
32
32
  import { memoryOperationMap } from './memory/module.mjs'
33
- import { usesInlineTestContext } from './module.f.mjs'
33
+ import { exitCode, toIoError, usesInlineTestContext } from './module.f.mjs'
34
34
  import { asBase, asNominal } from '../../types/nominal/module.f.mjs'
35
- import { error, ok } from '../../types/result/module.f.mjs'
35
+ import { error, ok, unwrap } from '../../types/result/module.f.mjs'
36
36
  import { asyncTryCatch } from '../../types/result/module.mjs'
37
37
  import { fromVec, listToVec, toVec } from '../../types/uint8array/module.f.mjs'
38
38
  import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs'
@@ -67,7 +67,23 @@ import { maxLengthBytes } from '../../types/bit_vec/module.f.mjs'
67
67
  */
68
68
  const createServer = http.createServer
69
69
 
70
- /** @typedef {<T>(effect: Effect<NodeOp, T>) => Promise<T>} _EffectToPromise */
70
+ /** @typedef {<T, E>(effect: Effect<NodeOp, T, E>) => Promise<Result<T, E>>} _EffectToPromise */
71
+
72
+ /**
73
+ * Performs host IO, reporting a thrown failure as an {@link IoResult} error.
74
+ *
75
+ * Every filesystem, network, and subprocess handler below goes through it, so
76
+ * the `catch` that turns an exception into effect data — and the normalization
77
+ * that keeps the channel serializable — happens in exactly one place.
78
+ *
79
+ * @template T
80
+ * @param {() => Promise<T>} f
81
+ * @returns {Promise<IoResult<T>>}
82
+ */
83
+ const io = async f => {
84
+ const r = await asyncTryCatch(f)
85
+ return r[0] === 'ok' ? r : error(toIoError(r[1]))
86
+ }
71
87
 
72
88
  /**
73
89
  * @template T
@@ -115,7 +131,7 @@ const sandbox = async f => {
115
131
  p = await p
116
132
  after = performance.now()
117
133
  }
118
- result = ok(/** @type {T} */ (p))
134
+ result = ok(p)
119
135
  } catch (e) {
120
136
  after = performance.now()
121
137
  result = error(e)
@@ -190,7 +206,7 @@ const waitReadableOrEnd = stdin =>
190
206
  const readStdinByte = async () => {
191
207
  const stdin = process.stdin
192
208
  while (true) {
193
- const chunk = /** @type {Uint8Array | null} */ (stdin.read(1))
209
+ const chunk = stdin.read(1)
194
210
  if (chunk !== null) {
195
211
  return chunk[0]
196
212
  }
@@ -210,16 +226,16 @@ const { randomInt } = crypto
210
226
  /** @type {_EffectToPromise} */
211
227
  const runNodeEffect = asyncRun({
212
228
  ...memoryOperationMap(),
213
- all: async (...effects) => await Promise.all(effects.map(runNodeEffect)),
214
- fetch: async url => asyncTryCatch(async () => {
229
+ all: async (...effects) => ok(await Promise.all(effects.map(runNodeEffect))),
230
+ fetch: url => io(async () => {
215
231
  const response = await fetch(url)
216
232
  if (!response.ok) {
217
233
  throw new Error(`Fetch error: ${response.status} ${response.statusText}`)
218
234
  }
219
235
  return toVec(new Uint8Array(await response.arrayBuffer()))
220
236
  }),
221
- mkdir: (path, options) => asyncTryCatch(async () => { await mkdir(path, options) }),
222
- readFile: path => asyncTryCatch(async () => {
237
+ mkdir: (path, options) => io(async () => { await mkdir(path, options) }),
238
+ readFile: path => io(async () => {
223
239
  const fileStats = await stat(path)
224
240
  // if the file is too big, toVec should fail anyway but in this case we don't want to load the file.
225
241
  if (fileStats.size > maxFileSizeBytes) {
@@ -227,7 +243,7 @@ const runNodeEffect = asyncRun({
227
243
  }
228
244
  return toVec(await readFile(path))
229
245
  }),
230
- readdir: (path, r) => asyncTryCatch(async () =>
246
+ readdir: (path, r) => io(async () =>
231
247
  (await readdir(path, { ...r, withFileTypes: true }))
232
248
  .map(v => ({
233
249
  name: v.name,
@@ -235,10 +251,10 @@ const runNodeEffect = asyncRun({
235
251
  isFile: v.isFile()
236
252
  }))
237
253
  ),
238
- writeFile: (path, data) => asyncTryCatch(() => writeFile(path, fromVec(data))),
239
- rm: path => asyncTryCatch(() => rm(path)),
240
- rename: (src, dst) => asyncTryCatch(() => rename(src, dst)),
241
- readBytes: (path, offset, size) => asyncTryCatch(async () => {
254
+ writeFile: (path, data) => io(() => writeFile(path, fromVec(data))),
255
+ rm: path => io(() => rm(path)),
256
+ rename: (src, dst) => io(() => rename(src, dst)),
257
+ readBytes: (path, offset, size) => io(async () => {
242
258
  if (offset < 0) {
243
259
  throw new Error(`Offset ${offset} is negative`)
244
260
  }
@@ -254,13 +270,13 @@ const runNodeEffect = asyncRun({
254
270
  await fh.close()
255
271
  }
256
272
  }),
257
- randomInt: async () => randomInt(randomMax),
258
- access: path => asyncTryCatch(() => access(path)),
259
- createExclusive: path => asyncTryCatch(async () => {
273
+ randomInt: async () => ok(randomInt(randomMax)),
274
+ access: path => io(() => access(path)),
275
+ createExclusive: path => io(async () => {
260
276
  const fh = await open(path, 'wx')
261
277
  await fh.close()
262
278
  }),
263
- writeBytes: (path, offset, data) => asyncTryCatch(async () => {
279
+ writeBytes: (path, offset, data) => io(async () => {
264
280
  const fh = await open(path, 'r+')
265
281
  try {
266
282
  const buffer = fromVec(data)
@@ -275,11 +291,11 @@ const runNodeEffect = asyncRun({
275
291
  await fh.close()
276
292
  }
277
293
  }),
278
- stat: path => asyncTryCatch(async () => ({ size: (await stat(path)).size })),
279
- import: path => asyncTryCatch(() => asyncImport(path)),
294
+ stat: path => io(async () => ({ size: (await stat(path)).size })),
295
+ import: path => io(() => asyncImport(path)),
280
296
  exec: (command, stdin) => new Promise(resolve => {
281
297
  const child = exec(command, (e, stdout, stderr) =>
282
- resolve(e !== null ? /** @type {const} */ (['error', e]) : ok({ stdout, stderr }))
298
+ resolve(e !== null ? error(toIoError(e)) : ok({ stdout, stderr }))
283
299
  )
284
300
  child.stdin?.end(stdin)
285
301
  }),
@@ -289,30 +305,37 @@ const runNodeEffect = asyncRun({
289
305
  const nodeRl = async (req, res) => {
290
306
  const reqBody = await collect(req)
291
307
  const { method, url, headers } = req
292
- const { status, headers: outHeaders, body: outBody } = await runNodeEffect(erl({
308
+ // `RequestListener` answers `Effect<…, ServerResponse, never>`
309
+ // the response frame is where a listener puts its failures — so
310
+ // this unwrap is total.
311
+ const { status, headers: outHeaders, body: outBody } = unwrap(await runNodeEffect(erl({
293
312
  method,
294
313
  url,
295
314
  headers,
296
315
  body: listToVec(reqBody)
297
- }))
316
+ })))
298
317
  res
299
318
  .writeHead(status, outHeaders)
300
319
  .end(fromVec(outBody))
301
320
  }
302
- return /** @satisfies {EffectServer} */ (asNominal(createServer(nodeRl)))
321
+ return ok(/** @satisfies {EffectServer} */ (asNominal(createServer(nodeRl))))
303
322
  },
304
323
  listen: async (server, port) => {
305
324
  const s = /** @type {_Server} */ (asBase(server))
306
325
  s.listen(port)
326
+ return ok(undefined)
307
327
  },
308
328
  forever: () => new Promise(() => {}),
309
- now: async () => now(),
310
- sandbox,
311
- await: awaitPromise,
312
- write: (stream, data) => writeAll(streams[stream], fromVec(data)),
313
- read: readStdinByte,
329
+ now: async () => ok(now()),
330
+ sandbox: async f => ok(await sandbox(f)),
331
+ await: async p => ok(await awaitPromise(p)),
332
+ write: async (stream, data) => ok(await writeAll(streams[stream], fromVec(data))),
333
+ read: async () => ok(await readStdinByte()),
314
334
  test: async (ctx, name, expectFailure, test) =>
315
- ctx.test(name, { expectFailure }, async t => runNodeEffect(test(t))),
335
+ // The body's answer is `ok(undefined)` a `Test` callback absorbs its
336
+ // own failures by panicking, which is the only signal these frameworks
337
+ // read — so it is awaited and discarded rather than returned.
338
+ ok(await ctx.test(name, { expectFailure }, async t => { await runNodeEffect(test(t)) })),
316
339
  })
317
340
 
318
341
  /** @type {TestFn} */
@@ -365,8 +388,8 @@ const options = {
365
388
  * the program's code, use {@link run} instead.
366
389
  * @type {(p: NodeProgram) => Promise<number>}
367
390
  */
368
- export const runEffect = program =>
369
- runNodeEffect(program(options))
391
+ export const runEffect = async program =>
392
+ exitCode(await runNodeEffect(program(options)))
370
393
 
371
394
  /**
372
395
  * CLI entry point: runs a `NodeProgram` via {@link runEffect}, then calls