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
@@ -3,19 +3,102 @@
3
3
  *
4
4
  * @module
5
5
  */
6
- export type Operation = readonly [string, (..._: readonly never[]) => unknown];
6
+ import type { Ok, Error, Result } from '../types/result/types.ts';
7
7
  /**
8
- * An `Effect<O, T>` is the raw value: a {@link Pure} thunk that yields `T`, or a
9
- * {@link Do} node describing a command to perform. It is plain data — compose
10
- * effects with the external `step`, which is eager wherever the head is
11
- * `Pure`.
8
+ * A command name paired with the signature a runner implements it at.
9
+ *
10
+ * **The return type is a {@link Result}, and that is a rule rather than a
11
+ * convention every operation happens to follow.** A runner may decline any
12
+ * command it was not given a handler for — `partialMatch` answers
13
+ * `error(notImplemented(command))` through the command's own output — so an
14
+ * operation whose return admitted no error would be a hole in that mechanism:
15
+ * there would be nowhere to put the refusal. Every *host* operation already
16
+ * returned `OpResult<…>` or `IoResult<…>` when this constraint was added, and
17
+ * so did four of the six declared inside proofs — through a bare `Result`
18
+ * rather than either alias, since those two are node conveniences. The two in
19
+ * `./proof.f.mjs` returned a bare `number`, and the commit that added the rule
20
+ * rewrote them.
21
+ *
22
+ * It is also the latch the whole representation now rests on. An operation
23
+ * *cannot* be declared infallible, so every {@link Effect} built from one has a
24
+ * `Result` to carry — which is what lets {@link Effect} name its error channel
25
+ * rather than leaving it inside an opaque payload.
12
26
  */
13
- export type Effect<O extends Operation, T> = Pure<T> | Do<O, T>;
27
+ export type Operation = readonly [string, (..._: readonly never[]) => Result<unknown, unknown>];
14
28
  /**
15
- * A pure effect: an *already-computed* `T` behind a thunk.
29
+ * The runner cannot dispatch this operation and has **not** started it.
30
+ *
31
+ * It is ordinary recoverable effect data rather than a fatal runner condition:
32
+ * the program receives control back and decides what an incompatible runner
33
+ * means for it — recover, choose a fallback operation, or treat it as fatal and
34
+ * panic itself (`throw`, e.g. via `unwrap`). Escalation belongs to the program,
35
+ * so the missing-handler path answers with `error(notImplemented)` rather than
36
+ * panicking on the program's behalf.
37
+ *
38
+ * **It identifies the operation by command name only.** An operation's payload
39
+ * may hold functions — `createServer`'s listener, `sandbox`'s thunk, `test`'s
40
+ * body — so carrying it would break the serializability the asserts in
41
+ * the asserts at the bottom of this file pin against the JSON data model.
42
+ *
43
+ * **It is not a security boundary.** A runner keeps its authority over
44
+ * execution through a separate, out-of-band mechanism: it may interrupt or
45
+ * terminate a program that is malicious, over budget, or violating host policy,
46
+ * and nothing in the error channel obliges it to hand control back. A
47
+ * capability the runner merely lacks is answered with this error; a refusal to
48
+ * continue is an interruption, never dressed up as `NotImplemented`.
49
+ *
50
+ * **It is why there is no infallible effect.** Every {@link Do} node is
51
+ * dispatched by a runner that may decline the command, so an effect that
52
+ * declared no error channel would be describing today's runner rather than the
53
+ * computation. That observation is what collapsed the representation into this
54
+ * one type — see {@link Effect}.
55
+ */
56
+ export type NotImplemented = readonly ['notImplemented', string];
57
+ /**
58
+ * An `Effect<O, T, E>` is the raw value: a {@link Pure} thunk yielding
59
+ * `Result<T, E>`, or a {@link Do} node describing a command to perform. It is
60
+ * plain data — compose effects with the combinators in
61
+ * [`./module.f.mjs`](./module.f.mjs), which are eager wherever the head
62
+ * is `Pure`.
63
+ *
64
+ * **The error channel is part of the representation, not a wrapper over it.**
65
+ * This used to be two types: a `RawEffect<O, T>` with an opaque payload, and an
66
+ * `Effect<O, T, E>` alias for `RawEffect<O, Result<T, E>>`. The division was
67
+ * described as *composition* against *representation*, and it was defended on
68
+ * the grounds that the representation is generic over its payload by
69
+ * definition — the runners, {@link match}, `runPure`, `do_`. That is true and
70
+ * it is not a reason for two names: {@link Operation} requires a `Result`
71
+ * return, so the payload a runner is generic over is *always* `Result<T, E>`
72
+ * already. Naming its two halves costs nothing and lets every signature in the
73
+ * system say which half it means.
16
74
  *
17
- * The thunk is a **discriminator, not a suspension**. `Effect` is a union with
18
- * no tag field, so telling its two cases apart needs a runtime test, and
75
+ * **`E` defaults to {@link NotImplemented}**, the one error every operation can
76
+ * answer with, so the common case is written `Effect<Sandbox, T>`. An
77
+ * operation's own failures extend the channel — `Effect<ReadFile, Vec,
78
+ * IoChannel>`, that alias being the node standard of
79
+ * `NotImplemented | IoError`.
80
+ *
81
+ * **`Effect<O, T, never>` is a claim, not an absence.** It says this code
82
+ * absorbs its own failures *here* — an MCP handler turning one into a JSON-RPC
83
+ * error response, a decoder turning one into `null`, a test body turning one
84
+ * into a panic. A reader who thinks the absorption is wrong has something to
85
+ * point at, which is what the opaque payload could not offer.
86
+ *
87
+ * The channel is where **short-circuiting** lives, not specifically where a
88
+ * runner's refusal lives. A parse failure, a domain verdict, or a non-zero exit
89
+ * code belongs in it for the same reason {@link NotImplemented} does: `step`
90
+ * should stop the chain and carry it out.
91
+ *
92
+ * It widens and does not narrow, in both channels and in `O` — the asserts in
93
+ * the asserts at the bottom of this file pin each direction, so an unhandled error
94
+ * type is a compile error rather than a value nobody looked at.
95
+ */
96
+ export type Effect<O extends Operation, T, E = NotImplemented> = Pure<T, E> | Do<O, T, E>;
97
+ /**
98
+ * A pure effect: an *already-computed* `Result<T, E>` behind a thunk.
99
+ *
100
+ * The thunk is a **discriminator, not a suspension**. {@link Effect} is a union
101
+ * with no tag field, so telling its two cases apart needs a runtime test, and
19
102
  * `typeof e === 'function'` is it — wrapping the value in a function is what
20
103
  * makes that test work. Deferral is not what the thunk is for. A `Pure` never
21
104
  * holds work that has yet to happen; everything that *does* something is a
@@ -26,19 +109,19 @@ export type Effect<O extends Operation, T> = Pure<T> | Do<O, T>;
26
109
  * - **The thunk must be pure and total.** Work hidden behind it is an effect
27
110
  * that no runner ever sees and no `OperationMap` can interpret or mock.
28
111
  * - **It may be called more than once.** Nothing memoizes it. The same effect
29
- * can be decoded repeatedly `Eff` re-forces the effect it wraps on each
30
- * `.step` — and under the first rule that costs nothing and changes nothing.
112
+ * can be decoded repeatedly, and under the first rule that costs nothing and
113
+ * changes nothing.
31
114
  *
32
115
  * A `lazy` constructor (`<T>(t: () => T): Effect<never, T> => t`) once existed
33
116
  * to advertise the thunk as a suspension. It was the identity function, and it
34
117
  * promised a deferral this representation does not keep; it has been removed.
35
118
  * Reintroducing it would reintroduce the contradiction, not fix one.
36
119
  */
37
- export type Pure<T> = () => T;
120
+ export type Pure<T, E> = () => Result<T, E>;
38
121
  export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (...args: infer P) => infer R] ? readonly [P, R] : never;
39
122
  /**
40
- * A `Do` node's continuation: given the command's output, produce the rest of
41
- * the effect.
123
+ * A {@link Do} node's continuation: given the command's output, produce the
124
+ * rest of the effect.
42
125
  *
43
126
  * The `out O` annotation asserts a covariance TypeScript cannot derive through
44
127
  * the conditional `Pr` type: the command's output sits in the *contravariant*
@@ -55,7 +138,7 @@ export type Pr<O extends Operation, K extends O[0]> = O extends readonly [K, (..
55
138
  * the continuation representation must re-check this argument before keeping the
56
139
  * annotation.
57
140
  */
58
- export type Cont<out O extends Operation, T> = (_: Pr<O, O[0]>[1]) => Effect<O, T>;
141
+ export type Cont<out O extends Operation, T, E> = (_: Pr<O, O[0]>[1]) => Effect<O, T, E>;
59
142
  /**
60
143
  * A `Do` node: the command to perform, its payload, and the continuation to
61
144
  * resume with the command's output. Its runtime value is exactly this record,
@@ -64,7 +147,7 @@ export type Cont<out O extends Operation, T> = (_: Pr<O, O[0]>[1]) => Effect<O,
64
147
  *
65
148
  * It must be an object rather than a tuple, and that is not a style choice:
66
149
  * only object / function / mapped-type aliases may carry a variance annotation
67
- * (`TS2637` forbids `out` on a tuple), and the raw `Effect` union must be
150
+ * (`TS2637` forbids `out` on a tuple), and the {@link Effect} union must be
68
151
  * covariant in `O` end to end. `command` and `payload` are indexed/conditional
69
152
  * types over `O` that TypeScript will not widen generically on their own —
70
153
  * annotating only {@link Cont} is not enough — so the whole node carries
@@ -78,27 +161,25 @@ export type Cont<out O extends Operation, T> = (_: Pr<O, O[0]>[1]) => Effect<O,
78
161
  * tuple's price without being a tuple. Named fields make the node
79
162
  * self-describing at every read and leave no layout to memorize.
80
163
  */
81
- export type Do<out O extends Operation, T> = {
164
+ export type Do<out O extends Operation, T, E> = {
82
165
  readonly command: O[0];
83
166
  readonly payload: Pr<O, O[0]>[0];
84
- readonly continuation: Cont<O, T>;
167
+ readonly continuation: Cont<O, T, E>;
85
168
  };
169
+ export type Param<O extends Operation> = F<O>[0];
170
+ export type Return<O extends Operation> = F<O>[1];
86
171
  /**
87
- * An effect whose result is a **history tuple**: the values a chain has bound so
88
- * far, newest first. `History<O, readonly[C, B, A]>` is three links deep, with
89
- * `A` bound earliest.
172
+ * The success half of a {@link Result} type, and {@link ErrOf} the error half.
90
173
  *
91
- * This is a transparent alias for `Effect`. It adds the tuple bound and
92
- * nothing else, so any tuple-valued effect satisfies it whether or not
93
- * `history` produced it it names the convention at the signatures that
94
- * rely on it rather than enforcing it.
95
- *
96
- * Heterogeneous by design: each element has its own type, so this is not a
97
- * `List` and nothing that folds or maps a list applies to it.
174
+ * These read an operation's *declared* return apart so that {@link Func} can
175
+ * state the effect `do_` builds from it. {@link Operation} guarantees the
176
+ * return is a `Result`, so neither is ever the `never` its fallback branch
177
+ * names; and both distribute over the union, which is what makes
178
+ * `Result<T, E>` decompose back into exactly `T` and `E`.
98
179
  */
99
- export type History<O extends Operation, H extends readonly unknown[]> = Effect<O, H>;
100
- export type Param<O extends Operation> = F<O>[0];
101
- export type Return<O extends Operation> = F<O>[1];
180
+ export type OkOf<R> = R extends Ok<infer T> ? T : never;
181
+ /** The error half of a {@link Result} type see {@link OkOf}. */
182
+ export type ErrOf<R> = R extends Error<infer E> ? E : never;
102
183
  /**
103
184
  * An operation map whose entries take a command's payload and return some
104
185
  * output `R`. Generalizes `ToAsyncOperationMap` (`R = Promise<…>`) and the
@@ -107,9 +188,56 @@ export type Return<O extends Operation> = F<O>[1];
107
188
  export type OperationMap<O extends Operation, R> = {
108
189
  readonly [K in O[0]]: (...payload: Pr<O, K>[0]) => R;
109
190
  };
110
- export type MatchResult<O extends Operation, T, R> = readonly ['done', T] | readonly ['cont', R, Do<O, T>['continuation']];
191
+ /**
192
+ * An {@link OperationMap} a runner may leave holes in: every handler it *does*
193
+ * provide has the same type, and any of them may be absent.
194
+ *
195
+ * Partiality is opt-in, and deliberately not the default. A total map means a
196
+ * runner that forgets a handler is a compile error, which is what should happen
197
+ * to the Node runner; this type is for a runner that is *meant* to lack
198
+ * operations — a virtual filesystem with no subprocesses, a mock that answers
199
+ * the three commands its proof issues. An absent handler here is an answer
200
+ * (`error(notImplemented)`), not an oversight.
201
+ */
202
+ export type PartialOperationMap<O extends Operation, R> = {
203
+ readonly [K in O[0]]?: (...payload: Pr<O, K>[0]) => R;
204
+ };
205
+ /**
206
+ * The runtime witness of `O`'s command set.
207
+ *
208
+ * A {@link PartialOperationMap} cannot say, at runtime, whether a command it
209
+ * has no handler for is one the operation set declares — types are erased, so
210
+ * an omitted `readFile` and a garbled `readFilee` reach the interpreter as the
211
+ * same missing lookup. They are not the same thing: the first is a capability
212
+ * this runner lacks and a program may recover from, the second is a `Do` node
213
+ * whose `command` was never the one its type claimed. Telling them apart needs
214
+ * `O`'s commands as data.
215
+ *
216
+ * Declare it as a record rather than an array — a `Record<O[0], null>` is
217
+ * checked for *completeness*, so a command added to `O` and forgotten here is a
218
+ * compile error, whereas an array literal only has its members checked and
219
+ * drifts silently.
220
+ */
221
+ export type CommandSet<O extends Operation> = Readonly<Record<O[0], null>>;
222
+ /** `O`'s commands, in the form {@link match} tests membership against. */
223
+ export type Commands<O extends Operation> = readonly O[0][];
224
+ /**
225
+ * What one decoding step of an effect yields: the finished `Result`, or the
226
+ * operation's output `R` paired with the continuation to resume with.
227
+ *
228
+ * The `done` payload is the whole `Result`, not its `ok` half. An interpreter
229
+ * is the one reader that must not short-circuit — a runner hands a failed
230
+ * command's `error` back *through* the continuation, so the two channels only
231
+ * separate above this layer.
232
+ */
233
+ export type MatchResult<O extends Operation, T, E, R> = readonly ['done', Result<T, E>] | readonly ['cont', R, Cont<O, T, E>];
111
234
  export type ToAsyncOperationMap<O extends Operation> = {
112
235
  readonly [K in O[0]]: (...payload: Pr<O, K>[0]) => Promise<Pr<O, K>[1]>;
113
236
  };
114
237
  export type F<O extends Operation> = Pr<O, O[0]>;
115
- export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, Return<O>>;
238
+ /**
239
+ * The effect-returning function `do_` builds for a single operation: its
240
+ * payload in, an {@link Effect} whose two channels are the halves of the
241
+ * operation's declared `Result` out.
242
+ */
243
+ export type Func<O extends Operation> = (..._: Param<O>) => Effect<O, OkOf<Return<O>>, ErrOf<Return<O>>>;
@@ -10,15 +10,17 @@
10
10
  *
11
11
  * @module
12
12
  *
13
- * @import { Effect, Operation } from '../effects/types.ts'
13
+ * @import { Operation } from '../effects/types.ts'
14
+ * @import { Effect, NotImplemented } from '../effects/types.ts'
14
15
  * @import { LoadModuleOperations, ModuleMap } from '../dev/types.ts'
15
16
  * @import { TestFn, TestEntry, TestSet, Path, Reporter, _TestState, _TestAndPath } from './types.ts'
16
- * @import { All, Await, Env, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write, WriteConsoles } from '../effects/node/types.ts'
17
+ * @import { All, Await, Env, IoChannel, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write, WriteConsoles } from '../effects/node/types.ts'
17
18
  */
18
- import type { Effect, Operation } from '../effects/types.ts';
19
+ import type { Operation } from '../effects/types.ts';
20
+ import type { Effect, NotImplemented } from '../effects/types.ts';
19
21
  import type { LoadModuleOperations, ModuleMap } from '../dev/types.ts';
20
22
  import type { TestEntry, TestSet, Path, Reporter, _TestAndPath } from './types.ts';
21
- import type { All, Await, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write } from '../effects/node/types.ts';
23
+ import type { All, Await, IoChannel, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write } from '../effects/node/types.ts';
22
24
  /**
23
25
  * Converts an arbitrary JS value into a `TestSet`.
24
26
  *
@@ -54,9 +56,9 @@ export declare const collectTests: (path: Path, throws: boolean, v: unknown) =>
54
56
  * This is the correct model for Node `--test`, Bun, and Deno, where tests must
55
57
  * be declared upfront and the framework drives execution.
56
58
  *
57
- * @type {(ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void>}
59
+ * @type {(ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void, NotImplemented>}
58
60
  */
59
- export declare const registerModule: (ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void>;
61
+ export declare const registerModule: (ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void, NotImplemented>;
60
62
  /**
61
63
  * Runs all test modules in `moduleMap` whose names pass `isTest`, accumulates
62
64
  * pass/fail/time via `reporter`, and returns an exit code (0 = all passed,
@@ -64,19 +66,24 @@ export declare const registerModule: (ctx: TestContext, k: string, v: unknown, s
64
66
  *
65
67
  * @template {Operation} O
66
68
  * @param {Reporter<O>} reporter
67
- * @returns {(moduleMap: ModuleMap) => Effect<O | All, number>}
69
+ * @returns {(moduleMap: ModuleMap) => Effect<O | All, number, IoChannel>}
68
70
  */
69
- export declare const runModuleMap: <O extends Operation>(reporter: Reporter<O>) => (moduleMap: ModuleMap) => Effect<O | All, number>;
71
+ export declare const runModuleMap: <O extends Operation>(reporter: Reporter<O>) => (moduleMap: ModuleMap) => Effect<O | All, number, IoChannel>;
70
72
  /**
71
73
  * Discovers all test modules via `loadModuleMap`, then runs them through
72
74
  * `runModuleMap`. The composed effect is a `NodeProgram` entry point for the
73
75
  * `fjs t` test runner.
74
76
  *
77
+ * The chain leaves the error channel here, because this is where a `Program` ends
78
+ * and a `Program`'s answer is an exit code rather than a `Result`. A run that
79
+ * could not report its own results is a failed run, so it exits `1` with the
80
+ * reason on `stderr` instead of unwinding as a panic.
81
+ *
75
82
  * @template {Operation} O
76
83
  * @param {Reporter<O>} reporter
77
- * @returns {Program<O | All | LoadModuleOperations>}
84
+ * @returns {Program<O | All | LoadModuleOperations | Write>}
78
85
  */
79
- export declare const testAll: <O extends Operation>(reporter: Reporter<O>) => Program<O | All | LoadModuleOperations>;
86
+ export declare const testAll: <O extends Operation>(reporter: Reporter<O>) => Program<O | All | LoadModuleOperations | Write>;
80
87
  /** Returns `true` if `s` is a non-negative decimal integer without a leading zero.
81
88
  *
82
89
  * @type {(s: string) => boolean}
@@ -125,9 +132,9 @@ export declare const ghEscape: (s: string) => string;
125
132
  * Default `Reporter.test` implementation: sandboxes `fn` once and inverts the
126
133
  * result when `throws` is `true` (caught error → pass, clean return → fail).
127
134
  *
128
- * @type {(file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>>}
135
+ * @type {(file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>, NotImplemented>}
129
136
  */
130
- export declare const defaultTest: (file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>>;
137
+ export declare const defaultTest: (file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>, NotImplemented>;
131
138
  /**
132
139
  * The terminal/GitHub reporter used by `fjs t`. Output goes through
133
140
  * `csiWrite`, so ANSI styles are stripped on non-TTY streams. When
@@ -10,15 +10,18 @@
10
10
  *
11
11
  * @module
12
12
  *
13
- * @import { Effect, Operation } from '../effects/types.ts'
13
+ * @import { Operation } from '../effects/types.ts'
14
+ * @import { Effect, NotImplemented } from '../effects/types.ts'
14
15
  * @import { LoadModuleOperations, ModuleMap } from '../dev/types.ts'
15
16
  * @import { TestFn, TestEntry, TestSet, Path, Reporter, _TestState, _TestAndPath } from './types.ts'
16
- * @import { All, Await, Env, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write, WriteConsoles } from '../effects/node/types.ts'
17
+ * @import { All, Await, Env, IoChannel, NodeProgram, NodeProgramOptions, Program, Sandbox, SandboxResult, Test, TestContext, Write, WriteConsoles } from '../effects/node/types.ts'
17
18
  */
18
19
 
19
20
  import { reset, fgGreen, fgRed, bold, csiWrite } from '../text/sgr/module.f.mjs'
20
- import { all, awaitIfPromise, sandbox, test } from '../effects/node/module.f.mjs'
21
- import { history, historyStep, mapStep, pure, step } from '../effects/module.f.mjs'
21
+ import { allOk, awaitIfPromise, errorExit, errorMessage, errorSummary, exitStep, sandbox, test } from '../effects/node/module.f.mjs'
22
+ import {
23
+ catchStep, history, historyStep, mapStep, pureError, pureOk, resultStep, step,
24
+ } from '../effects/module.f.mjs'
22
25
  import { loadModuleMap } from '../dev/module.f.mjs'
23
26
  import { invert } from '../types/result/module.f.mjs'
24
27
  import { definedEntries } from '../types/object/module.f.mjs'
@@ -104,10 +107,10 @@ export const collectTests = (path, throws, v) => {
104
107
  * This is the correct model for Node `--test`, Bun, and Deno, where tests must
105
108
  * be declared upfront and the framework drives execution.
106
109
  *
107
- * @type {(ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void>}
110
+ * @type {(ctx: TestContext, k: string, v: unknown, star: string) => Effect<Test | All | Await, void, NotImplemented>}
108
111
  */
109
112
  export const registerModule = (ctx, k, v, star) => {
110
- /** @type {(ctx: TestContext, entry: _TestAndPath) => Effect<Test | All | Await, void>} */
113
+ /** @type {(ctx: TestContext, entry: _TestAndPath) => Effect<Test | All | Await, void, NotImplemented>} */
111
114
  const registerOne = (ctx, [path, { fn, throws }]) => {
112
115
  // `star` (non-empty for Bun and for Node below the 26 baseline) signals
113
116
  // that all sub-tests run inline inside this single registration, so an
@@ -117,22 +120,38 @@ export const registerModule = (ctx, k, v, star) => {
117
120
  // extra suffix is needed.
118
121
  const base = fmtImport(k, path)
119
122
  const name = throws ? base : `${base}${star}`
120
- return test(ctx, name, throws, (/** @type {TestContext} */ t) =>
121
- step(awaitIfPromise(fn()), resolved => {
123
+ // The registered callback panics on failure, deliberately. `Test` hands
124
+ // it to an external framework (node `--test`, Bun, Deno) that takes a
125
+ // body which either returns or throws: there is no channel to answer a
126
+ // failure through, so propagating it here would only discard it one
127
+ // level up.
128
+ // A throw is what that framework *does* understand — it reports the
129
+ // test as failed, which is the outcome a caller wants anyway. The `test`
130
+ // operation's own result is propagated normally, just below.
131
+ //
132
+ // `catchStep` rather than `unwrapStep`, so the absorption is visible in
133
+ // the type: the body answers `Effect<…, void, never>`, and the `never`
134
+ // is *because* this handler panics. `errorSummary` still names what is
135
+ // being panicked on and pins it, so if the body's channel ever widens
136
+ // past the node one, the compiler asks here rather than turning a new
137
+ // recoverable failure into a crash.
138
+ /** @type {(t: TestContext) => Effect<Test | All | Await, void, never>} */
139
+ const body = t =>
140
+ catchStep(step(awaitIfPromise(fn()), resolved => {
122
141
  if (throws) {
123
- return pure(undefined)
142
+ return pureOk(undefined)
124
143
  }
125
144
  const sub = collectTests([...path, null], false, resolved)
126
145
  if (sub.length === 0) {
127
- return pure(undefined)
146
+ return pureOk(undefined)
128
147
  }
129
- return mapStep(all(...sub.map(e => registerOne(t, e))), () => undefined)
130
- })
131
- )
148
+ return mapStep(allOk(...sub.map(e => registerOne(t, e))), () => undefined)
149
+ }), e => { throw errorSummary(e) })
150
+ return test(ctx, name, throws, body)
132
151
  }
133
152
  const tests = collectTests([], false, v)
134
- if (tests.length === 0) { return pure(undefined) }
135
- return mapStep(all(...tests.map(e => registerOne(ctx, e))), () => undefined)
153
+ if (tests.length === 0) { return pureOk(undefined) }
154
+ return mapStep(allOk(...tests.map(e => registerOne(ctx, e))), () => undefined)
136
155
  }
137
156
 
138
157
  /** @type {(a: _TestState, b: _TestState) => _TestState} */
@@ -145,10 +164,10 @@ const zero = { time: 0, pass: 0, fail: 0 }
145
164
  /**
146
165
  * @template {Operation} O
147
166
  * @param {Reporter<O>} reporter
148
- * @returns {(k: string, v: unknown) => (ts: _TestState) => Effect<O | All, _TestState>}
167
+ * @returns {(k: string, v: unknown) => (ts: _TestState) => Effect<O | All, _TestState, IoChannel>}
149
168
  */
150
169
  const runModule = ({ result, test }) => (k, v) => ts => {
151
- /** @type {(entry: _TestAndPath) => Effect<O | All, _TestState>} */
170
+ /** @type {(entry: _TestAndPath) => Effect<O | All, _TestState, IoChannel>} */
152
171
  const one = ([testPath, set]) => {
153
172
  // The sandbox result is still needed after it has been reported, so the
154
173
  // reporting call is captured rather than nested inside its own step.
@@ -160,22 +179,22 @@ const runModule = ({ result, test }) => (k, v) => ts => {
160
179
  ([, sr]) => {
161
180
  const { result: [s, r], duration } = sr
162
181
  if (s !== 'ok') {
163
- return pure(addFail(duration)(zero))
182
+ return pureOk(addFail(duration)(zero))
164
183
  }
165
184
  if (set.throws) {
166
- return pure(addPass(duration)(zero))
185
+ return pureOk(addPass(duration)(zero))
167
186
  }
168
187
  // Walk return-value sub-tree; null marks the call boundary so
169
188
  // paths render as e.g. `outer().inner`. throws resets to false.
170
- return step(
189
+ return mapStep(
171
190
  walk([...testPath, null], false, r),
172
- sub => pure(mergeState(addPass(duration)(zero), sub)))
191
+ sub => mergeState(addPass(duration)(zero), sub))
173
192
  })
174
193
  }
175
- /** @type {(path: Path, throws: boolean, v: unknown) => Effect<O | All, _TestState>} */
194
+ /** @type {(path: Path, throws: boolean, v: unknown) => Effect<O | All, _TestState, IoChannel>} */
176
195
  const walk = (path, throws, v) => {
177
196
  const effects = collectTests(path, throws, v).map(one)
178
- return mapStep(all(...effects), states => states.reduce(mergeState, zero))
197
+ return mapStep(allOk(...effects), states => states.reduce(mergeState, zero))
179
198
  }
180
199
  return mapStep(walk([], false, v), delta => mergeState(ts, delta))
181
200
  }
@@ -192,13 +211,13 @@ const proofEntries = moduleMap =>
192
211
  *
193
212
  * @template {Operation} O
194
213
  * @param {Reporter<O>} reporter
195
- * @returns {(moduleMap: ModuleMap) => Effect<O | All, number>}
214
+ * @returns {(moduleMap: ModuleMap) => Effect<O | All, number, IoChannel>}
196
215
  */
197
216
  export const runModuleMap = reporter => moduleMap => {
198
217
  const { summary } = reporter
199
218
  const modules = proofEntries(moduleMap)
200
219
  const total = mapStep(
201
- all(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero))),
220
+ allOk(...modules.map(([k, v]) => runModule(reporter)(k, v)(zero))),
202
221
  m => m.reduce(mergeState, zero))
203
222
  // The totals are still needed after the summary has been printed, so they
204
223
  // are carried forward in a history rather than closed over by a nested
@@ -209,28 +228,57 @@ export const runModuleMap = reporter => moduleMap => {
209
228
  return mapStep(reported, ([, ts]) => ts.fail !== 0 ? 1 : 0)
210
229
  }
211
230
 
231
+ /**
232
+ * Ends a run with the exit code it computed, reporting a channel failure on
233
+ * `stderr` as exit `1`.
234
+ *
235
+ * Not `exitStep`, which answers `0` for every success: this chain's success
236
+ * value **is** the exit code, `1` when a test failed. The two policies differ
237
+ * only in what an `ok` means, and conflating them would report a failing suite
238
+ * as a passing run.
239
+ *
240
+ * A non-zero code therefore leaves through the *error* branch, which is what it
241
+ * means — a suite with failures is a failed program — and is why a caller
242
+ * cannot chain past it by accident.
243
+ *
244
+ * @type {<O extends Operation>(e: Effect<O, number, IoChannel>) => Effect<O | Write, 0, number>}
245
+ */
246
+ const exitCodeStep = e =>
247
+ resultStep(e, r => {
248
+ /** @type {Effect<Write, 0, number>} */
249
+ const code = r[0] === 'error'
250
+ ? errorExit(errorMessage(r[1]))
251
+ : r[1] === 0 ? pureOk(0) : pureError(r[1])
252
+ return code
253
+ })
254
+
212
255
  /**
213
256
  * Discovers all test modules via `loadModuleMap`, then runs them through
214
257
  * `runModuleMap`. The composed effect is a `NodeProgram` entry point for the
215
258
  * `fjs t` test runner.
216
259
  *
260
+ * The chain leaves the error channel here, because this is where a `Program` ends
261
+ * and a `Program`'s answer is an exit code rather than a `Result`. A run that
262
+ * could not report its own results is a failed run, so it exits `1` with the
263
+ * reason on `stderr` instead of unwinding as a panic.
264
+ *
217
265
  * @template {Operation} O
218
266
  * @param {Reporter<O>} reporter
219
- * @returns {Program<O | All | LoadModuleOperations>}
267
+ * @returns {Program<O | All | LoadModuleOperations | Write>}
220
268
  */
221
269
  export const testAll = reporter => options =>
222
- step(loadModuleMap(options.env), runModuleMap(reporter))
270
+ exitCodeStep(step(loadModuleMap(options.env), runModuleMap(reporter)))
223
271
 
224
272
  /**
225
273
  * Registers all modules in `moduleMap` that export a `proof` property with
226
274
  * `ctx`. Delegates to `registerModule` for each matching entry.
227
275
  *
228
- * @type {(ctx: TestContext, star: string) => (moduleMap: ModuleMap) => Effect<Test | All | Await, void>}
276
+ * @type {(ctx: TestContext, star: string) => (moduleMap: ModuleMap) => Effect<Test | All | Await, void, NotImplemented>}
229
277
  */
230
278
  const registerModuleMap = (ctx, star) => moduleMap => {
231
279
  const modules = proofEntries(moduleMap)
232
- if (modules.length === 0) { return pure(undefined) }
233
- return mapStep(all(...modules.map(([k, v]) => registerModule(ctx, k, v, star))), () => undefined)
280
+ if (modules.length === 0) { return pureOk(undefined) }
281
+ return mapStep(allOk(...modules.map(([k, v]) => registerModule(ctx, k, v, star))), () => undefined)
234
282
  }
235
283
 
236
284
  /** @type {(c: string) => boolean} */
@@ -316,7 +364,7 @@ export const ghEscape = s =>
316
364
  * Default `Reporter.test` implementation: sandboxes `fn` once and inverts the
317
365
  * result when `throws` is `true` (caught error → pass, clean return → fail).
318
366
  *
319
- * @type {(file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>>}
367
+ * @type {(file: string, path: Path, entry: TestEntry) => Effect<Sandbox, SandboxResult<unknown>, NotImplemented>}
320
368
  */
321
369
  export const defaultTest = (file, path, { fn, throws }) =>
322
370
  mapStep(sandbox(fn), r => throws ? { ...r, result: invert(r.result) } : r)
@@ -336,7 +384,12 @@ const fmtResultLine = (file, path, color, label, duration) =>
336
384
  */
337
385
  export const defaultReporter = options => {
338
386
  const write = csiWrite(options)
339
- /** @type {(w: WriteConsoles) => (s: string) => Effect<Write, void>} */
387
+ // A reporter that cannot emit its own output has no fallback to choose —
388
+ // there is nowhere left to report the failure — but it does not have to
389
+ // decide that here: the failure travels to the program's tail, which ends
390
+ // the run with the reason on `stderr` and exit `1`. That is the same
391
+ // outcome a panic produced, minus the stack trace.
392
+ /** @type {(w: WriteConsoles) => (s: string) => Effect<Write, void, NotImplemented>} */
340
393
  const line = w => {
341
394
  const x = write(w)
342
395
  return s => x(s + '\n')
@@ -351,6 +404,9 @@ export const defaultReporter = options => {
351
404
  ? csiLog(fmtResultLine(file, path, fgGreen, 'ok', duration) + (throws ? ' # EXPECTED TO THROW' : ''))
352
405
  : isGitHub
353
406
  ? csiError(`::error file=${file},line=1,title=${ghEscape(fmtImport(file, path))}::${ghEscape(String(v))}`)
407
+ // `step`, so the detail line is attempted only when the
408
+ // header line was written: two halves of one report, and
409
+ // half of it is worse than none.
354
410
  : step(
355
411
  csiError(fmtResultLine(file, path, fgRed, 'error', duration)),
356
412
  () => csiError(`${fgRed}${v}${reset}`)),
@@ -384,5 +440,9 @@ export const register = o => {
384
440
  const star = o.inlineTestContext ? ' ...' : ''
385
441
  const ctx = o.engine === 'bun' ? o.bunTestContext : o.testContext
386
442
  const registered = step(loadModuleMap(o.env), registerModuleMap(ctx, star))
387
- return mapStep(registered, () => 0)
443
+ // `exitStep`, not `mapStep(…, () => 0)`: registering is the whole job here,
444
+ // so "registered nothing, exit 0" is the answer this used to give and the
445
+ // one it must not. A success has no code of its own, which is exactly the
446
+ // shape `exitStep` is for.
447
+ return exitStep(registered)
388
448
  }
@@ -1,15 +1,17 @@
1
1
  /**
2
- * @import { Effect } from '../effects/types.ts'
3
- * @import { NodeProgramOptions, Sandbox, Write } from '../effects/node/types.ts'
2
+ * @import { RunInstance } from '../effects/mock/types.ts'
3
+ * @import { Effect, NotImplemented } from '../effects/types.ts'
4
+ * @import { NodeProgramOptions, OpResult, Sandbox, Write } from '../effects/node/types.ts'
4
5
  * @import { JsModule } from '../effects/node/virtual/types.ts'
5
6
  * @import { Reporter } from './types.ts'
6
- * @import { All, Await, Test, TestContext } from '../effects/node/types.ts'
7
+ * @import { All, Await, Import, Readdir, Test, TestContext } from '../effects/node/types.ts'
7
8
  * @import { Ts } from '../types/rtti/ts/types.ts'
8
9
  */
10
+ import type { RunInstance } from '../effects/mock/types.ts';
9
11
  import type { Effect } from '../effects/types.ts';
10
- import type { Sandbox, Write } from '../effects/node/types.ts';
12
+ import type { OpResult, Sandbox, Write } from '../effects/node/types.ts';
11
13
  import type { Reporter } from './types.ts';
12
- import type { All, Await, Test, TestContext } from '../effects/node/types.ts';
14
+ import type { All, Await, Import, Readdir, Test, TestContext } from '../effects/node/types.ts';
13
15
  import type { Ts } from '../types/rtti/ts/types.ts';
14
16
  /**
15
17
  * The mock reporter's stdout lines. A schema rather than a hand-written type:
@@ -39,13 +41,19 @@ export declare const defaultReporterOutput: () => void;
39
41
  export declare const defaultReporterOutputLargeDuration: () => void;
40
42
  export declare const defaultReporterFailOutput: () => void;
41
43
  export declare const githubReporterOutput: () => void;
44
+ export type _FailOps = All | Import | Readdir | Sandbox | Write;
45
+ /** @typedef {All | Import | Readdir | Sandbox | Write} _FailOps */
46
+ export declare const reporterWriteFailure: () => void;
42
47
  export type _RegisterMockState = readonly string[];
43
48
  export type _RegisterMockOps = Test | All | Await;
44
- export type _RegisterRunner = (s: _RegisterMockState) => <T>(e: Effect<_RegisterMockOps, T>) => readonly [_RegisterMockState, T];
45
- export type _RegisterTestOp = (runner: _RegisterRunner, ctx: TestContext, name: string, expectFailure: boolean, fn: (t: TestContext) => Effect<_RegisterMockOps, void>) => (s: _RegisterMockState) => readonly [_RegisterMockState, void];
49
+ export type _RegisterRunner = RunInstance<_RegisterMockOps, _RegisterMockState>;
50
+ export type _RegisterTestOp = (runner: _RegisterRunner, ctx: TestContext, name: string, expectFailure: boolean, fn: (t: TestContext) => Effect<_RegisterMockOps, void, never>) => (s: _RegisterMockState) => readonly [_RegisterMockState, OpResult<void>];
46
51
  export declare const registerSuffixes: () => void;
52
+ declare const registerBodyPanicsOnUndispatchableEffect: () => void;
47
53
  export declare const registerThrowsWithoutThrowing: () => void;
48
54
  export declare const registerEmptyProof: () => void;
55
+ export declare const registerEmptyModuleMap: () => void;
56
+ export declare const registerSelectsContextAndStar: () => void;
49
57
  export declare const helpers: {
50
58
  isInteger: () => void;
51
59
  isIdentifier: () => void;
@@ -61,6 +69,9 @@ export declare const helpers: {
61
69
  };
62
70
  declare const defaultReporterExpectedToThrow: () => void;
63
71
  export declare const proof: {
72
+ throw: {
73
+ registerBodyPanicsOnUndispatchableEffect: typeof registerBodyPanicsOnUndispatchableEffect;
74
+ };
64
75
  flat: typeof flat;
65
76
  nested: typeof nested;
66
77
  throwKey: typeof throwKey;
@@ -76,9 +87,12 @@ export declare const proof: {
76
87
  defaultReporterOutputLargeDuration: typeof defaultReporterOutputLargeDuration;
77
88
  defaultReporterFailOutput: typeof defaultReporterFailOutput;
78
89
  githubReporterOutput: typeof githubReporterOutput;
90
+ reporterWriteFailure: typeof reporterWriteFailure;
79
91
  registerSuffixes: typeof registerSuffixes;
80
92
  registerThrowsWithoutThrowing: typeof registerThrowsWithoutThrowing;
81
93
  registerEmptyProof: typeof registerEmptyProof;
94
+ registerEmptyModuleMap: typeof registerEmptyModuleMap;
95
+ registerSelectsContextAndStar: typeof registerSelectsContextAndStar;
82
96
  defaultReporterExpectedToThrow: typeof defaultReporterExpectedToThrow;
83
97
  helpers: {
84
98
  isInteger: () => void;