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
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * @import { Cas } from '../types.ts'
3
+ * @import { EvoChannel } from './types.ts'
4
+ * @import { Result } from '../../types/result/types.ts'
5
+ * @import { Effect } from '../../effects/types.ts'
6
+ * @import { IoChannel, NodeOp } from '../../effects/node/types.ts'
7
+ * @import { State } from '../../effects/node/virtual/types.ts'
3
8
  * @import { Vec } from '../../types/bit_vec/types.ts'
4
9
  * @import { Ok } from '../../types/result/types.ts'
5
- * @import { IoResult } from '../../effects/node/types.ts'
6
10
  * @import { List } from '../../effects/list/types.ts'
7
11
  * @import { RevisionData } from './types.ts'
8
12
  */
@@ -13,13 +17,15 @@ import {
13
17
  assertStructurallySame,
14
18
  } from '../../asserts/module.f.mjs'
15
19
  import { pure } from '../../effects/module.f.mjs'
20
+ import { pureError } from '../../effects/module.f.mjs'
21
+ import { ioError } from '../../effects/node/module.f.mjs'
16
22
  import { fileCas } from '../module.f.mjs'
17
23
  import { sha256 } from '../../crypto/sha2/module.f.mjs'
18
24
  import { emptyState, virtual } from '../../effects/node/virtual/module.f.mjs'
19
25
  import { vec, vec8 } from '../../types/bit_vec/module.f.mjs'
20
26
  import { cBase32ToVec, vecToCBase32 } from '../../basen/cbase32/module.f.mjs'
21
27
  import { unwrap } from '../../types/nullable/module.f.mjs'
22
- import { ok, error } from '../../types/result/module.f.mjs'
28
+ import { ok, error, unwrap as unwrapResult } from '../../types/result/module.f.mjs'
23
29
  import { nonEmpty, empty as elEmpty } from '../../effects/list/module.f.mjs'
24
30
  import { tryUtf8 } from '../../text/module.f.mjs'
25
31
  import { dialect as revisionDialect } from '../../media/revision/module.f.mjs'
@@ -34,18 +40,18 @@ const home = '.'
34
40
  /** @type {Cas<never>} */
35
41
  const writeFailingCas = {
36
42
  read: () => elEmpty(),
37
- write: () => pure(error('boom')),
38
- list: () => pure([]),
43
+ write: () => pure(error(ioError({ message: 'boom' }))),
44
+ list: () => pure(ok([])),
39
45
  }
40
46
 
41
- // A `Cas<never>` whose `read` yields an error item that is *not* a missing
42
- // shard — what a permission error, a mid-stream I/O failure, or a blob too
43
- // large for `collectRead` to buffer looks like to a caller.
47
+ // A `Cas<never>` whose `read` fails the stream with something that is *not* a
48
+ // missing shard — what a permission error, a mid-stream I/O failure, or a blob
49
+ // too large for `collectRead` to buffer looks like to a caller.
44
50
  /** @type {Cas<never>} */
45
51
  const readFailingCas = {
46
- read: () => nonEmpty(/** @type {IoResult<Vec>} */ (error('boom')), elEmpty()),
47
- write: () => pure(error('write not supported')),
48
- list: () => pure([]),
52
+ read: () => pureError(ioError({ message: 'boom' })),
53
+ write: () => pure(error(ioError({ message: 'write not supported' }))),
54
+ list: () => pure(ok([])),
49
55
  }
50
56
 
51
57
  // A `Cas<never>` backed by a fixed set of (hash, content) entries, returned
@@ -57,45 +63,80 @@ const fixedCas = entries => ({
57
63
  read: hash => {
58
64
  const found = entries.find(([h]) => vecToCBase32(h) === vecToCBase32(hash))
59
65
  return found === undefined
60
- ? nonEmpty(/** @type {IoResult<Vec>} */ (error('not found')), elEmpty())
61
- : nonEmpty(/** @type {IoResult<Vec>} */ (ok(found[1])), elEmpty())
66
+ ? pureError(ioError({ message: 'not found' }))
67
+ : nonEmpty(found[1], elEmpty())
62
68
  },
63
- write: () => pure(error('write not supported')),
64
- list: () => pure(entries.map(([h]) => h)),
69
+ write: () => pure(error(ioError({ message: 'write not supported' }))),
70
+ list: () => pure(ok(entries.map(([h]) => h))),
65
71
  })
66
72
 
73
+ /**
74
+ * Runs `e` against the virtual runner and unwraps the effect channel.
75
+ *
76
+ * These proofs drive a runner that implements every operation they use, so a
77
+ * channel failure here is a broken fixture rather than a case under test, and
78
+ * panicking on one is the wanted outcome.
79
+ *
80
+ * The cases that *are* about failure therefore do not come through here: a
81
+ * rejected revision now travels in the same channel as an undispatchable
82
+ * operation, so unwrapping would panic on the thing under test. Those use the
83
+ * raw `virtual` and `assertEvoError`.
84
+ *
85
+ * @type {(state: State) => <T, E>(e: Effect<NodeOp, T, E>) => readonly [State, T]}
86
+ */
87
+ const virtualOk = state => e => {
88
+ const [s, r] = virtual(state)(e)
89
+ return [s, unwrapResult(r)]
90
+ }
91
+
92
+ /**
93
+ * Asserts that `r` failed with an {@link EvoError} whose message contains
94
+ * `expected`.
95
+ *
96
+ * The tag is checked as well as the text, so a `notImplemented` from a runner
97
+ * that quietly lost a handler cannot pass for a rejected revision. Both now
98
+ * arrive through the same channel, which is the point — telling them apart is
99
+ * what the tag is for.
100
+ *
101
+ * @type {(r: Result<unknown, EvoChannel>, expected: string) => void}
102
+ */
103
+ const assertEvoError = (r, expected) =>
104
+ assert(
105
+ r[0] === 'error' && r[1][0] === 'evoError' && r[1][1].includes(expected),
106
+ ['expected an evo error containing', expected, r])
107
+
67
108
  export const proof = {
68
109
  buildCacheEmptyStoreYieldsEmptyCache: () => {
69
110
  const c = fileCas(sha256)(home)
70
- const [, cache] = virtual(emptyState)(buildCache(c))
111
+ const [, cache] = virtualOk(emptyState)(buildCache(c))
71
112
  assertStructurallySame(cache, emptyCache)
72
113
  },
73
114
  buildCacheSkipsNonRevisionBlob: () => {
74
115
  const c = fileCas(sha256)(home)
75
116
  const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON
76
- const [state1] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
77
- const [, cache] = virtual(state1)(buildCache(c))
117
+ const [state1] = virtual(emptyState)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
118
+ const [, cache] = virtualOk(state1)(buildCache(c))
78
119
  assertEq(Object.keys(cache.bySubject).length, 0)
79
120
  },
80
121
  decodeRevisionBlobMissingHashIsNull: () => {
81
122
  const c = fileCas(sha256)(home)
82
- const [, revision] = virtual(emptyState)(decodeRevisionBlob(c)(vec8(0x99n)))
123
+ const [, revision] = virtualOk(emptyState)(decodeRevisionBlob(c)(vec8(0x99n)))
83
124
  assertEq(revision, null)
84
125
  },
85
126
  decodeRevisionBlobNonUtf8IsNull: () => {
86
127
  const c = fileCas(sha256)(home)
87
128
  const oddVec = vec(5n)(0b10101n) // not a whole number of bytes
88
- const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(oddVec), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
129
+ const [state1, w] = virtual(emptyState)(c.write(nonEmpty(oddVec, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
89
130
  assert(w[0] === 'ok', ['expected write ok', w])
90
- const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1]))
131
+ const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
91
132
  assertEq(revision, null)
92
133
  },
93
134
  decodeRevisionBlobInvalidJsonIsNull: () => {
94
135
  const c = fileCas(sha256)(home)
95
136
  const content = vec8(0x7bn) // '{' alone: valid UTF-8, not parseable JSON
96
- const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(content), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
137
+ const [state1, w] = virtual(emptyState)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
97
138
  assert(w[0] === 'ok', ['expected write ok', w])
98
- const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1]))
139
+ const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
99
140
  assertEq(revision, null)
100
141
  },
101
142
  decodeRevisionBlobValidRevisionRoundTrips: () => {
@@ -104,9 +145,9 @@ export const proof = {
104
145
  const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
105
146
  const bytes = tryUtf8(text)
106
147
  assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
107
- const [state1, w] = virtual(emptyState)(c.write(nonEmpty(ok(bytes), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
148
+ const [state1, w] = virtual(emptyState)(c.write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
108
149
  assert(w[0] === 'ok', ['expected write ok', w])
109
- const [, revision] = virtual(state1)(decodeRevisionBlob(c)(w[1]))
150
+ const [, revision] = virtualOk(state1)(decodeRevisionBlob(c)(w[1]))
110
151
  assert(revision !== null, 'expected a decoded revision')
111
152
  assertEq(revision?.subject, subjectHash)
112
153
  },
@@ -120,9 +161,9 @@ export const proof = {
120
161
  const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
121
162
  const bytes = tryUtf8(text)
122
163
  assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
123
- const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(ok(bytes), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
164
+ const [state1, w] = virtual(emptyState)(fileCas(sha256)(home).write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
124
165
  assert(w[0] === 'ok', ['expected write ok', w])
125
- const [, cache] = virtual(state1)(buildCache(c))
166
+ const [, cache] = virtualOk(state1)(buildCache(c))
126
167
  assertEq(cache.bySubject[subjectHash]?.hashes.length, 1)
127
168
  assertEq(cache.bySubject[subjectHash]?.hashes[0], vecToCBase32(w[1]))
128
169
  },
@@ -144,8 +185,8 @@ export const proof = {
144
185
  const childBytes = tryUtf8(childText)
145
186
  assert(rootBytes !== null && childBytes !== null, 'expected sample revisions to encode as UTF-8')
146
187
  const cas = fixedCas([[childHash, childBytes], [rootHash, rootBytes]])
147
- const [state0, cacheKey] = virtual(emptyState)(initEvo(cas))
148
- const [, heads] = virtual(state0)(evo(cas)(cacheKey).head('doc'))
188
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
189
+ const [, heads] = virtualOk(state0)(evo(cas)(cacheKey).head('doc'))
149
190
  assertEq(heads.length, 1)
150
191
  assertEq(heads[0], childCBase32)
151
192
  },
@@ -168,8 +209,8 @@ export const proof = {
168
209
  const childBytes = tryUtf8(childText)
169
210
  assert(rootBytes !== null && childBytes !== null, 'expected sample revisions to encode as UTF-8')
170
211
  const cas = fixedCas([[rootHash, rootBytes], [childHash, childBytes]])
171
- const [state0, cacheKey] = virtual(emptyState)(initEvo(cas))
172
- const [, heads] = virtual(state0)(evo(cas)(cacheKey).head('doc'))
212
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
213
+ const [, heads] = virtualOk(state0)(evo(cas)(cacheKey).head('doc'))
173
214
  assertEq(heads.length, 1)
174
215
  assertEq(heads[0], vecToCBase32(childHash))
175
216
  },
@@ -179,38 +220,35 @@ export const proof = {
179
220
  // set), and a keep (an unrelated existing head surviving the fold).
180
221
  addRevisionBuildsHeadsAcrossChainAndFork: () => {
181
222
  const c = fileCas(sha256)(home)
182
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
223
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
183
224
  const e = evo(c)(cacheKey)
184
225
  const snapshotHash = vecToCBase32(vec8(0x22n))
185
226
 
186
227
  /** @type {RevisionData} */
187
228
  const rev0 = { parents: [], subject: 'doc', snapshot: snapshotHash }
188
- const [state1, rev0Result] = virtual(state0)(e.add(rev0))
189
- assert(rev0Result[0] === 'ok', ['expected rev0 ok', rev0Result])
190
- const rev0Hash = rev0Result[1]
229
+ const [state1, rev0Result] = virtualOk(state0)(e.add(rev0))
230
+ const rev0Hash = rev0Result
191
231
 
192
232
  // rev1 inherits rev0's snapshot; rev2 states a different one — so the
193
233
  // two concurrent children are genuinely distinct blobs (both children
194
234
  // of rev0, same generation), not one deduplicated revision.
195
235
  /** @type {RevisionData} */
196
236
  const rev1 = { parents: [rev0Hash], subject: 'doc' }
197
- const [state2, rev1Result] = virtual(state1)(e.add(rev1))
198
- assert(rev1Result[0] === 'ok', ['expected rev1 ok', rev1Result])
199
- const rev1Hash = rev1Result[1]
237
+ const [state2, rev1Result] = virtualOk(state1)(e.add(rev1))
238
+ const rev1Hash = rev1Result
200
239
 
201
240
  /** @type {RevisionData} */
202
241
  const rev2 = { parents: [rev0Hash], subject: 'doc', snapshot: vecToCBase32(vec8(0x23n)) }
203
- const [state3, rev2Result] = virtual(state2)(e.add(rev2))
204
- assert(rev2Result[0] === 'ok', ['expected rev2 ok', rev2Result])
205
- const rev2Hash = rev2Result[1]
242
+ const [state3, rev2Result] = virtualOk(state2)(e.add(rev2))
243
+ const rev2Hash = rev2Result
206
244
 
207
- const [state4, heads] = virtual(state3)(e.head('doc'))
245
+ const [state4, heads] = virtualOk(state3)(e.head('doc'))
208
246
  assertEq(heads.length, 2)
209
247
  assert(heads.includes(rev1Hash), ['expected rev1 to remain a head', heads])
210
248
  assert(heads.includes(rev2Hash), ['expected rev2 to be a head', heads])
211
249
  assert(!heads.includes(rev0Hash), ['expected rev0 to no longer be a head', heads])
212
250
 
213
- const [, subjects] = virtual(state4)(e.list())
251
+ const [, subjects] = virtualOk(state4)(e.list())
214
252
  assertEq(subjects.length, 1)
215
253
  assertEq(subjects[0], 'doc')
216
254
  },
@@ -220,16 +258,14 @@ export const proof = {
220
258
  // and neither subject appears in the other's result.
221
259
  listPartitionsSubjectsByHeadArchivedFlag: () => {
222
260
  const c = fileCas(sha256)(home)
223
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
261
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
224
262
  const e = evo(c)(cacheKey)
225
- const [state1, live] = virtual(state0)(e.add({ parents: [], subject: 'live', snapshot: vecToCBase32(vec8(0x40n)) }))
226
- assert(live[0] === 'ok', ['expected the active add to succeed', live])
227
- const [state2, gone] = virtual(state1)(e.add({ parents: [], subject: 'gone', snapshot: vecToCBase32(vec8(0x41n)), archived: true }))
228
- assert(gone[0] === 'ok', ['expected the archived add to succeed', gone])
229
- const [state3, active] = virtual(state2)(e.list())
263
+ const [state1, live] = virtualOk(state0)(e.add({ parents: [], subject: 'live', snapshot: vecToCBase32(vec8(0x40n)) }))
264
+ const [state2, gone] = virtualOk(state1)(e.add({ parents: [], subject: 'gone', snapshot: vecToCBase32(vec8(0x41n)), archived: true }))
265
+ const [state3, active] = virtualOk(state2)(e.list())
230
266
  assertEq(active.length, 1)
231
267
  assertEq(active[0], 'live')
232
- const [, archived] = virtual(state3)(e.list(true))
268
+ const [, archived] = virtualOk(state3)(e.list(true))
233
269
  assertEq(archived.length, 1)
234
270
  assertEq(archived[0], 'gone')
235
271
  },
@@ -238,22 +274,19 @@ export const proof = {
238
274
  // — so the subject stays out of the archived-only result.
239
275
  listTreatsDisagreeingHeadsAsActive: () => {
240
276
  const c = fileCas(sha256)(home)
241
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
277
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
242
278
  const e = evo(c)(cacheKey)
243
- const [state1, root] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)) }))
244
- assert(root[0] === 'ok', ['expected root ok', root])
279
+ const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)) }))
245
280
  // The two children differ only in `archived`, which is enough to make
246
281
  // them distinct blobs, hence two concurrent heads of one root.
247
- const [state2, kept] = virtual(state1)(e.add({ parents: [root[1]], subject: 'doc' }))
248
- assert(kept[0] === 'ok', ['expected the unarchived child ok', kept])
249
- const [state3, dropped] = virtual(state2)(e.add({ parents: [root[1]], subject: 'doc', archived: true }))
250
- assert(dropped[0] === 'ok', ['expected the archived child ok', dropped])
251
- const [state4, heads] = virtual(state3)(e.head('doc'))
282
+ const [state2] = virtualOk(state1)(e.add({ parents: [root], subject: 'doc' }))
283
+ const [state3] = virtualOk(state2)(e.add({ parents: [root], subject: 'doc', archived: true }))
284
+ const [state4, heads] = virtualOk(state3)(e.head('doc'))
252
285
  assertEq(heads.length, 2)
253
- const [state5, active] = virtual(state4)(e.list())
286
+ const [state5, active] = virtualOk(state4)(e.list())
254
287
  assertEq(active.length, 1)
255
288
  assertEq(active[0], 'doc')
256
- const [, archived] = virtual(state5)(e.list(true))
289
+ const [, archived] = virtualOk(state5)(e.list(true))
257
290
  assertEq(archived.length, 0)
258
291
  },
259
292
  // `archived` is a property of a revision, not of a subject: a subject
@@ -261,16 +294,14 @@ export const proof = {
261
294
  // demotes that revision out of the head set.
262
295
  listIgnoresArchivedRevisionsThatAreNoLongerHeads: () => {
263
296
  const c = fileCas(sha256)(home)
264
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
297
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
265
298
  const e = evo(c)(cacheKey)
266
- const [state1, root] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x45n)), archived: true }))
267
- assert(root[0] === 'ok', ['expected the archived root ok', root])
268
- const [state2, revived] = virtual(state1)(e.add({ parents: [root[1]], subject: 'doc' }))
269
- assert(revived[0] === 'ok', ['expected the unarchived child ok', revived])
270
- const [state3, active] = virtual(state2)(e.list())
299
+ const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x45n)), archived: true }))
300
+ const [state2] = virtualOk(state1)(e.add({ parents: [root], subject: 'doc' }))
301
+ const [state3, active] = virtualOk(state2)(e.list())
271
302
  assertEq(active.length, 1)
272
303
  assertEq(active[0], 'doc')
273
- const [, archived] = virtual(state3)(e.list(true))
304
+ const [, archived] = virtualOk(state3)(e.list(true))
274
305
  assertEq(archived.length, 0)
275
306
  },
276
307
  // A subject can end up with no current head at all, and a status is a
@@ -286,29 +317,27 @@ export const proof = {
286
317
  const bytes = tryUtf8(text)
287
318
  assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
288
319
  const cas = fixedCas([[selfHash, bytes]])
289
- const [state0, cacheKey] = virtual(emptyState)(initEvo(cas))
320
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(cas))
290
321
  const e = evo(cas)(cacheKey)
291
- const [state1, heads] = virtual(state0)(e.head('doc'))
322
+ const [state1, heads] = virtualOk(state0)(e.head('doc'))
292
323
  assertEq(heads.length, 0)
293
- const [state2, active] = virtual(state1)(e.list())
324
+ const [state2, active] = virtualOk(state1)(e.list())
294
325
  assertEq(active.length, 0)
295
- const [, archived] = virtual(state2)(e.list(true))
326
+ const [, archived] = virtualOk(state2)(e.list(true))
296
327
  assertEq(archived.length, 0)
297
328
  },
298
329
  // Adding the exact same revision twice yields the same (deduplicated)
299
330
  // content hash and must not duplicate the head entry.
300
331
  addRevisionIdempotentOnDuplicateContent: () => {
301
332
  const c = fileCas(sha256)(home)
302
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
333
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
303
334
  const e = evo(c)(cacheKey)
304
335
  /** @type {RevisionData} */
305
336
  const input = { parents: [], subject: 'x', snapshot: vecToCBase32(vec8(0x33n)) }
306
- const [state1, r1] = virtual(state0)(e.add(input))
307
- assert(r1[0] === 'ok', ['expected first add ok', r1])
308
- const [state2, r2] = virtual(state1)(e.add(input))
309
- assert(r2[0] === 'ok', ['expected second add ok', r2])
310
- assertEq(r1[1], r2[1])
311
- const [, heads] = virtual(state2)(e.head('x'))
337
+ const [state1, r1] = virtualOk(state0)(e.add(input))
338
+ const [state2, r2] = virtualOk(state1)(e.add(input))
339
+ assertEq(r1, r2)
340
+ const [, heads] = virtualOk(state2)(e.head('x'))
312
341
  assertEq(heads.length, 1)
313
342
  },
314
343
  // Two `add` calls describing the same logical revision but spelling the
@@ -318,44 +347,39 @@ export const proof = {
318
347
  // `canonicalHash` exists to prevent.
319
348
  addRevisionCanonicalizesParentSpellingBeforeSerializing: () => {
320
349
  const c = fileCas(sha256)(home)
321
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
350
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
322
351
  const e = evo(c)(cacheKey)
323
352
  /** @type {RevisionData} */
324
353
  const root = { parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x2en)) }
325
- const [state1, rootResult] = virtual(state0)(e.add(root))
326
- assert(rootResult[0] === 'ok', ['expected root ok', rootResult])
327
- const rootHash = rootResult[1]
354
+ const [state1, rootResult] = virtualOk(state0)(e.add(root))
355
+ const rootHash = rootResult
328
356
  assert(rootHash !== rootHash.toUpperCase(), 'expected the sample hash to contain letters')
329
357
  /** @type {RevisionData} */
330
358
  const child1 = { parents: [rootHash], subject: 'doc' }
331
- const [state2, child1Result] = virtual(state1)(e.add(child1))
332
- assert(child1Result[0] === 'ok', ['expected child1 ok', child1Result])
359
+ const [state2, child1Result] = virtualOk(state1)(e.add(child1))
333
360
  /** @type {RevisionData} */
334
361
  const child2 = { parents: [rootHash.toUpperCase()], subject: 'doc' }
335
- const [state3, child2Result] = virtual(state2)(e.add(child2))
336
- assert(child2Result[0] === 'ok', ['expected child2 ok', child2Result])
337
- assertEq(child1Result[1], child2Result[1])
338
- const [, heads] = virtual(state3)(e.head('doc'))
362
+ const [state3, child2Result] = virtualOk(state2)(e.add(child2))
363
+ assertEq(child1Result, child2Result)
364
+ const [, heads] = virtualOk(state3)(e.head('doc'))
339
365
  assertEq(heads.length, 1)
340
- assertEq(heads[0], child1Result[1])
366
+ assertEq(heads[0], child1Result)
341
367
  },
342
368
  // `subject` omitted with exactly one parent is inherited from that
343
369
  // parent's own `subject`.
344
370
  addRevisionResolvesSubjectFromSingleParent: () => {
345
371
  const c = fileCas(sha256)(home)
346
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
372
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
347
373
  const e = evo(c)(cacheKey)
348
374
  /** @type {RevisionData} */
349
375
  const root = { parents: [], subject: 'inherit-me', snapshot: vecToCBase32(vec8(0x44n)) }
350
- const [state1, rootResult] = virtual(state0)(e.add(root))
351
- assert(rootResult[0] === 'ok', ['expected root ok', rootResult])
352
- const rootHash = rootResult[1]
376
+ const [state1, rootResult] = virtualOk(state0)(e.add(root))
377
+ const rootHash = rootResult
353
378
  /** @type {RevisionData} */
354
379
  const child = { parents: [rootHash] }
355
- const [state2, childResult] = virtual(state1)(e.add(child))
356
- assert(childResult[0] === 'ok', ['expected child ok', childResult])
357
- const childHash = childResult[1]
358
- const [, heads] = virtual(state2)(e.head('inherit-me'))
380
+ const [state2, childResult] = virtualOk(state1)(e.add(child))
381
+ const childHash = childResult
382
+ const [, heads] = virtualOk(state2)(e.head('inherit-me'))
359
383
  assertEq(heads.length, 1)
360
384
  assertEq(heads[0], childHash)
361
385
  },
@@ -365,21 +389,19 @@ export const proof = {
365
389
  // inherits the parent's stored snapshot and gets `generation` 1.
366
390
  addComputesGenerationAndResolvesSnapshot: () => {
367
391
  const c = fileCas(sha256)(home)
368
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
392
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
369
393
  const e = evo(c)(cacheKey)
370
394
  const subjectHash = vecToCBase32(vec8(0x71n))
371
- const [state1, rootResult] = virtual(state0)(e.add({ parents: [], subject: subjectHash }))
372
- assert(rootResult[0] === 'ok', ['expected root ok', rootResult])
373
- const rootHashVec = unwrap(cBase32ToVec(rootResult[1]))
374
- const [state2, root] = virtual(state1)(decodeRevisionBlob(c)(rootHashVec))
395
+ const [state1, rootResult] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash }))
396
+ const rootHashVec = unwrap(cBase32ToVec(rootResult))
397
+ const [state2, root] = virtualOk(state1)(decodeRevisionBlob(c)(rootHashVec))
375
398
  assert(root !== null, 'expected the stored root to decode')
376
399
  assertEq(root?.generation, 0)
377
400
  assertEq(root?.snapshot, subjectHash)
378
401
 
379
- const [state3, childResult] = virtual(state2)(e.add({ parents: [rootResult[1]], subject: subjectHash }))
380
- assert(childResult[0] === 'ok', ['expected child ok', childResult])
381
- const childHashVec = unwrap(cBase32ToVec(childResult[1]))
382
- const [, child] = virtual(state3)(decodeRevisionBlob(c)(childHashVec))
402
+ const [state3, childResult] = virtualOk(state2)(e.add({ parents: [rootResult], subject: subjectHash }))
403
+ const childHashVec = unwrap(cBase32ToVec(childResult))
404
+ const [, child] = virtualOk(state3)(decodeRevisionBlob(c)(childHashVec))
383
405
  assert(child !== null, 'expected the stored child to decode')
384
406
  assertEq(child?.generation, 1)
385
407
  assertEq(child?.snapshot, subjectHash)
@@ -388,24 +410,19 @@ export const proof = {
388
410
  // and one at generation 1 yield generation 3.
389
411
  addComputesMergeGenerationFromMaxOfParents: () => {
390
412
  const c = fileCas(sha256)(home)
391
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
413
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
392
414
  const e = evo(c)(cacheKey)
393
415
  const snap = vecToCBase32(vec8(0x72n))
394
416
  // Mainline chain: root(gen0) → a(gen1) → b(gen2).
395
- const [state1, root] = virtual(state0)(e.add({ parents: [], subject: 'm', snapshot: snap }))
396
- assert(root[0] === 'ok', ['expected root ok', root])
397
- const [state2, a] = virtual(state1)(e.add({ parents: [root[1]], subject: 'm', snapshot: snap }))
398
- assert(a[0] === 'ok', ['expected a ok', a])
399
- const [state3, b] = virtual(state2)(e.add({ parents: [a[1]], subject: 'm', snapshot: snap }))
400
- assert(b[0] === 'ok', ['expected b ok', b])
417
+ const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: 'm', snapshot: snap }))
418
+ const [state2, a] = virtualOk(state1)(e.add({ parents: [root], subject: 'm', snapshot: snap }))
419
+ const [state3, b] = virtualOk(state2)(e.add({ parents: [a], subject: 'm', snapshot: snap }))
401
420
  // Side branch off root: c(gen1).
402
- const [state4, cRev] = virtual(state3)(e.add({ parents: [root[1]], subject: 'm', snapshot: snap }))
403
- assert(cRev[0] === 'ok', ['expected c ok', cRev])
421
+ const [state4, cRev] = virtualOk(state3)(e.add({ parents: [root], subject: 'm', snapshot: snap }))
404
422
  // Merge of b(gen2) and c(gen1) → gen3.
405
- const [state5, merge] = virtual(state4)(e.add({ parents: [b[1], cRev[1]], subject: 'm', snapshot: snap }))
406
- assert(merge[0] === 'ok', ['expected merge ok', merge])
407
- const mergeHashVec = unwrap(cBase32ToVec(merge[1]))
408
- const [, mergeRev] = virtual(state5)(decodeRevisionBlob(c)(mergeHashVec))
423
+ const [state5, merge] = virtualOk(state4)(e.add({ parents: [b, cRev], subject: 'm', snapshot: snap }))
424
+ const mergeHashVec = unwrap(cBase32ToVec(merge))
425
+ const [, mergeRev] = virtualOk(state5)(decodeRevisionBlob(c)(mergeHashVec))
409
426
  assert(mergeRev !== null, 'expected the stored merge to decode')
410
427
  assertEq(mergeRev?.generation, 3)
411
428
  },
@@ -415,25 +432,22 @@ export const proof = {
415
432
  // byte-identical to the same `add` without it.
416
433
  addIgnoresSuppliedGeneration: () => {
417
434
  const c = fileCas(sha256)(home)
418
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
435
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
419
436
  const e = evo(c)(cacheKey)
420
437
  const subjectHash = vecToCBase32(vec8(0x73n))
421
- const [state1, claimed] = virtual(state0)(e.add({ parents: [], subject: subjectHash, generation: 42 }))
422
- assert(claimed[0] === 'ok', ['expected add ok', claimed])
423
- const [state2, plain] = virtual(state1)(e.add({ parents: [], subject: subjectHash }))
424
- assert(plain[0] === 'ok', ['expected add ok', plain])
425
- assertEq(claimed[1], plain[1])
426
- const [, revision] = virtual(state2)(e.revision(claimed[1]))
438
+ const [state1, claimed] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash, generation: 42 }))
439
+ const [state2, plain] = virtualOk(state1)(e.add({ parents: [], subject: subjectHash }))
440
+ assertEq(claimed, plain)
441
+ const [, revision] = virtual(state2)(e.revision(claimed))
427
442
  assert(revision[0] === 'ok', ['expected revision ok', revision])
428
443
  assertEq(revision[1].generation, 0)
429
444
  },
430
445
  addRevisionSubjectRequiredForZeroParents: () => {
431
446
  const c = fileCas(sha256)(home)
432
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
447
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
433
448
  const e = evo(c)(cacheKey)
434
449
  const [, result] = virtual(state0)(e.add({ parents: [] }))
435
- assertEq(result[0], 'error')
436
- assert(result[0] === 'error' && result[1].includes('subject is required'), ['unexpected message', result])
450
+ assertEvoError(result, 'subject is required')
437
451
  },
438
452
  // Zero parents and no input `snapshot`: `resolveSnapshot` falls back to
439
453
  // `subject` as the reference, which must itself be a hash. A subject with
@@ -442,40 +456,36 @@ export const proof = {
442
456
  // an explicit `snapshot`.
443
457
  addRevisionNonHashSubjectWithoutSnapshotIsError: () => {
444
458
  const c = fileCas(sha256)(home)
445
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
459
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
446
460
  const e = evo(c)(cacheKey)
447
461
  const [, result] = virtual(state0)(e.add({ parents: [], subject: 'not-a-hash!' }))
448
- assertEq(result[0], 'error')
449
- assert(result[0] === 'error' && result[1].includes('subject must be a valid hash'), ['unexpected message', result])
462
+ assertEvoError(result, 'subject must be a valid hash')
450
463
  },
451
464
  addRevisionInvalidParentHashIsError: () => {
452
465
  const c = fileCas(sha256)(home)
453
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
466
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
454
467
  const e = evo(c)(cacheKey)
455
468
  const [, result] = virtual(state0)(e.add({ parents: ['not a valid cbase32!'] }))
456
- assertEq(result[0], 'error')
457
- assert(result[0] === 'error' && result[1].includes('invalid parent hash'), ['unexpected message', result])
469
+ assertEvoError(result, 'invalid parent hash')
458
470
  },
459
471
  addRevisionParentNotARevisionIsError: () => {
460
472
  const c = fileCas(sha256)(home)
461
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
473
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
462
474
  const e = evo(c)(cacheKey)
463
475
  const missingParent = vecToCBase32(vec8(0x55n))
464
476
  const [, result] = virtual(state0)(e.add({ parents: [missingParent] }))
465
- assertEq(result[0], 'error')
466
- assert(result[0] === 'error' && result[1].includes('parent is not a revision blob'), ['unexpected message', result])
477
+ assertEvoError(result, 'parent is not a revision blob')
467
478
  },
468
479
  // An explicit `subject` must not bypass parent validation: a missing
469
480
  // parent is still an error even though `subject` alone would otherwise
470
481
  // resolve without looking at any parent.
471
482
  addRevisionValidatesParentsEvenWithExplicitSubject: () => {
472
483
  const c = fileCas(sha256)(home)
473
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
484
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
474
485
  const e = evo(c)(cacheKey)
475
486
  const missingParent = vecToCBase32(vec8(0x66n))
476
487
  const [, result] = virtual(state0)(e.add({ parents: [missingParent], subject: 'doc' }))
477
- assertEq(result[0], 'error')
478
- assert(result[0] === 'error' && result[1].includes('parent is not a revision blob'), ['unexpected message', result])
488
+ assertEvoError(result, 'parent is not a revision blob')
479
489
  },
480
490
  // A revision models one step in the evolution of a single subject: a
481
491
  // parent that actually belongs to a *different* subject must be
@@ -485,27 +495,24 @@ export const proof = {
485
495
  // never remove the parent from its real subject's head set.
486
496
  addRevisionRejectsCrossSubjectParent: () => {
487
497
  const c = fileCas(sha256)(home)
488
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
498
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
489
499
  const e = evo(c)(cacheKey)
490
500
  /** @type {RevisionData} */
491
501
  const rootA = { parents: [], subject: 'A', snapshot: vecToCBase32(vec8(0x67n)) }
492
- const [state1, rootAResult] = virtual(state0)(e.add(rootA))
493
- assert(rootAResult[0] === 'ok', ['expected rootA ok', rootAResult])
494
- const [, result] = virtual(state1)(e.add({ parents: [rootAResult[1]], subject: 'B' }))
495
- assertEq(result[0], 'error')
496
- assert(result[0] === 'error' && result[1].includes('different subject'), ['unexpected message', result])
502
+ const [state1, rootAResult] = virtualOk(state0)(e.add(rootA))
503
+ const [, result] = virtual(state1)(e.add({ parents: [rootAResult], subject: 'B' }))
504
+ assertEvoError(result, 'different subject')
497
505
  },
498
506
  // With multiple parents, a failure on an earlier one short-circuits the
499
507
  // fold — the second (here, a well-formed but nonexistent) parent is
500
508
  // never even looked up.
501
509
  addRevisionShortCircuitsOnFirstInvalidParentAmongMultiple: () => {
502
510
  const c = fileCas(sha256)(home)
503
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
511
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
504
512
  const e = evo(c)(cacheKey)
505
513
  const missingParent = vecToCBase32(vec8(0x99n))
506
514
  const [, result] = virtual(state0)(e.add({ parents: ['not a valid cbase32!', missingParent], subject: 'doc' }))
507
- assertEq(result[0], 'error')
508
- assert(result[0] === 'error' && result[1].includes('invalid parent hash'), ['unexpected message', result])
515
+ assertEvoError(result, 'invalid parent hash')
509
516
  },
510
517
  // Two *valid* parents without an explicit `snapshot` cannot resolve one
511
518
  // (no single parent snapshot to inherit) — a distinct failure from a
@@ -513,37 +520,35 @@ export const proof = {
513
520
  // boundary now that the format requires an explicit `snapshot`.
514
521
  addRevisionInvalidReferencesIsError: () => {
515
522
  const c = fileCas(sha256)(home)
516
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
523
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
517
524
  const e = evo(c)(cacheKey)
518
525
  /** @type {RevisionData} */
519
526
  const rootA = { parents: [], subject: 'merge', snapshot: vecToCBase32(vec8(0x77n)) }
520
- const [state1, rootAResult] = virtual(state0)(e.add(rootA))
521
- assert(rootAResult[0] === 'ok', ['expected rootA ok', rootAResult])
527
+ const [state1, rootAResult] = virtualOk(state0)(e.add(rootA))
522
528
  /** @type {RevisionData} */
523
529
  const rootB = { parents: [], subject: 'merge', snapshot: vecToCBase32(vec8(0x88n)) }
524
- const [state2, rootBResult] = virtual(state1)(e.add(rootB))
525
- assert(rootBResult[0] === 'ok', ['expected rootB ok', rootBResult])
526
- const [, result] = virtual(state2)(e.add({ parents: [rootAResult[1], rootBResult[1]], subject: 'merge' }))
527
- assertEq(result[0], 'error')
528
- assert(result[0] === 'error' && !result[1].includes('parent is not a revision blob'), ['unexpected message', result])
530
+ const [state2, rootBResult] = virtualOk(state1)(e.add(rootB))
531
+ const [, result] = virtual(state2)(e.add({ parents: [rootAResult, rootBResult], subject: 'merge' }))
532
+ assert(
533
+ result[0] === 'error' && result[1][0] === 'evoError'
534
+ && !result[1][1].includes('parent is not a revision blob'),
535
+ ['unexpected message', result])
529
536
  },
530
537
  addRevisionTooLargeToEncodeIsError: () => {
531
538
  const c = fileCas(sha256)(home)
532
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
539
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
533
540
  const e = evo(c)(cacheKey)
534
541
  const hugeSubject = 'x'.repeat(200_000)
535
542
  /** @type {RevisionData} */
536
543
  const input = { parents: [], subject: hugeSubject, snapshot: vecToCBase32(vec8(0x88n)) }
537
544
  const [, result] = virtual(state0)(e.add(input))
538
- assertEq(result[0], 'error')
539
- assert(result[0] === 'error' && result[1] === 'revision too large to encode', ['unexpected message', result])
545
+ assertEvoError(result, 'revision too large to encode')
540
546
  },
541
547
  addRevisionWriteFailureIsError: () => {
542
- const [, cacheKey] = virtual(emptyState)(initEvo(writeFailingCas))
548
+ const [, cacheKey] = virtualOk(emptyState)(initEvo(writeFailingCas))
543
549
  const e = evo(writeFailingCas)(cacheKey)
544
550
  const [, result] = virtual(emptyState)(e.add({ parents: [], subject: vecToCBase32(vec8(0x99n)) }))
545
- assertEq(result[0], 'error')
546
- assert(result[0] === 'error' && result[1] === 'failed to write revision to CAS', ['unexpected message', result])
551
+ assertEvoError(result, 'failed to write revision to CAS')
547
552
  },
548
553
  // Every way `revision` can fail is its own message, not one `null`: an
549
554
  // undecodable hash, a hash the store has nothing under, a read that failed
@@ -552,41 +557,37 @@ export const proof = {
552
557
  // stores of arbitrary content); this read keeps them apart.
553
558
  revisionInvalidHashIsError: () => {
554
559
  const c = fileCas(sha256)(home)
555
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
560
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
556
561
  const e = evo(c)(cacheKey)
557
562
  const [, result] = virtual(state0)(e.revision('not a valid cbase32!'))
558
- assertEq(result[0], 'error')
559
- assert(result[0] === 'error' && result[1].includes('invalid hash'), ['unexpected message', result])
563
+ assertEvoError(result, 'invalid hash')
560
564
  },
561
565
  revisionMissingHashIsError: () => {
562
566
  const c = fileCas(sha256)(home)
563
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
567
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
564
568
  const e = evo(c)(cacheKey)
565
569
  const [, result] = virtual(state0)(e.revision(vecToCBase32(vec8(0x9an))))
566
- assertEq(result[0], 'error')
567
- assert(result[0] === 'error' && result[1].includes('revision not found'), ['unexpected message', result])
570
+ assertEvoError(result, 'revision not found')
568
571
  },
569
572
  // Regression: a read failure that is not a missing shard — a permission or
570
573
  // mid-stream I/O error, or a blob too large to buffer into one `Vec` —
571
574
  // must not be reported as "not found". The blob may well be there; saying
572
575
  // it is absent would be a false answer, not merely a vague one.
573
576
  revisionReadFailureIsNotReportedAsMissing: () => {
574
- const [state0, cacheKey] = virtual(emptyState)(initEvo(readFailingCas))
577
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(readFailingCas))
575
578
  const e = evo(readFailingCas)(cacheKey)
576
579
  const [, result] = virtual(state0)(e.revision(vecToCBase32(vec8(0x9bn))))
577
- assertEq(result[0], 'error')
578
- assert(result[0] === 'error' && result[1].includes('failed to read revision'), ['unexpected message', result])
580
+ assertEvoError(result, 'failed to read revision')
579
581
  },
580
582
  revisionNonRevisionBlobIsError: () => {
581
583
  const c = fileCas(sha256)(home)
582
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
584
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
583
585
  const e = evo(c)(cacheKey)
584
586
  const content = vec8(0x41n) // 'A' — valid UTF-8, not revision JSON
585
- const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(content), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
587
+ const [state1, w] = virtual(state0)(c.write(nonEmpty(content, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
586
588
  assert(w[0] === 'ok', ['expected write ok', w])
587
589
  const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1])))
588
- assertEq(result[0], 'error')
589
- assert(result[0] === 'error' && result[1].includes('not a revision blob'), ['unexpected message', result])
590
+ assertEvoError(result, 'not a revision blob')
590
591
  },
591
592
  // A blob written by somebody else can spell its references in any accepted
592
593
  // cbase32 alias (case, `i`/`l`/`o`). What `revision` returns is
@@ -594,7 +595,7 @@ export const proof = {
594
595
  // directly instead of knowing about cbase32 aliasing.
595
596
  revisionCanonicalizesReferenceSpellings: () => {
596
597
  const c = fileCas(sha256)(home)
597
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
598
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
598
599
  const e = evo(c)(cacheKey)
599
600
  const parentCanonical = vecToCBase32(vec8(0xffn))
600
601
  const snapshotCanonical = vecToCBase32(vec8(0xfen))
@@ -606,7 +607,7 @@ export const proof = {
606
607
  const text = `{"dialect":"${revisionDialect}","subject":"doc","parents":["${parentAlias}"],"snapshot":"${snapshotAlias}","generation":1}`
607
608
  const bytes = tryUtf8(text)
608
609
  assert(bytes !== null, 'expected the sample revision text to encode as UTF-8')
609
- const [state1, w] = virtual(state0)(c.write(nonEmpty(ok(bytes), /** @type {List<never, Ok<Vec>>} */ (elEmpty()))))
610
+ const [state1, w] = virtual(state0)(c.write(nonEmpty(bytes, /** @satisfies {List<never, Vec, IoChannel>} */ (elEmpty()))))
610
611
  assert(w[0] === 'ok', ['expected write ok', w])
611
612
  const [, result] = virtual(state1)(e.revision(vecToCBase32(w[1])))
612
613
  assert(result[0] === 'ok', ['expected revision ok', result])
@@ -621,78 +622,160 @@ export const proof = {
621
622
  // can be added again unchanged — same content, same hash.
622
623
  revisionRoundTripsThroughAdd: () => {
623
624
  const c = fileCas(sha256)(home)
624
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
625
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
625
626
  const e = evo(c)(cacheKey)
626
627
  const subjectHash = vecToCBase32(vec8(0x74n))
627
- const [state1, root] = virtual(state0)(e.add({ parents: [], subject: subjectHash }))
628
- assert(root[0] === 'ok', ['expected root ok', root])
629
- const [state2, child] = virtual(state1)(e.add({ parents: [root[1]], archived: true }))
630
- assert(child[0] === 'ok', ['expected child ok', child])
631
- const [state3, result] = virtual(state2)(e.revision(child[1]))
628
+ const [state1, root] = virtualOk(state0)(e.add({ parents: [], subject: subjectHash }))
629
+ const [state2, child] = virtualOk(state1)(e.add({ parents: [root], archived: true }))
630
+ const [state3, result] = virtual(state2)(e.revision(child))
632
631
  assert(result[0] === 'ok', ['expected revision ok', result])
633
632
  assertEq(result[1].subject, subjectHash)
634
633
  assertEq(result[1].snapshot, subjectHash)
635
634
  assertEq(result[1].generation, 1)
636
635
  assertEq(result[1].archived, true)
637
636
  assertEq(result[1].parents.length, 1)
638
- assertEq(result[1].parents[0], root[1])
639
- const [, readded] = virtual(state3)(e.add(result[1]))
640
- assert(readded[0] === 'ok', ['expected re-add ok', readded])
641
- assertEq(readded[1], child[1])
637
+ assertEq(result[1].parents[0], root)
638
+ const [, readded] = virtualOk(state3)(e.add(result[1]))
639
+ assertEq(readded, child)
642
640
  },
643
641
  revisionLockRoundTripsAndCanonicalizes: () => {
644
642
  const c = fileCas(sha256)(home)
645
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
643
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
646
644
  const e = evo(c)(cacheKey)
647
645
  const canonical = vecToCBase32(vec8(0xffn))
648
646
  const alias = canonical.toUpperCase()
649
- const [state1, added] = virtual(state0)(e.add({
647
+ const [state1, added] = virtualOk(state0)(e.add({
650
648
  parents: [], subject: 'doc', snapshot: canonical,
651
649
  lock: { dependency: alias },
652
650
  }))
653
- assert(added[0] === 'ok', ['expected add ok', added])
654
- const [, result] = virtual(state1)(e.revision(added[1]))
651
+ const [, result] = virtual(state1)(e.revision(added))
652
+ assert(result[0] === 'ok', ['expected revision ok', result])
653
+ const lock = result[1].lock
654
+ assert(typeof lock === 'object', ['expected an inline lock map', lock])
655
+ assertEq(lock.dependency, canonical)
656
+ },
657
+ // A `lock` naming a `vnd.fjs.lock` blob is stored and read back as the
658
+ // reference it is: validated as a hash and re-spelled canonically, like
659
+ // `snapshot`, and never followed — resolving one is a resolver's job.
660
+ revisionSharedLockReferenceRoundTripsAndCanonicalizes: () => {
661
+ const c = fileCas(sha256)(home)
662
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
663
+ const e = evo(c)(cacheKey)
664
+ const canonical = vecToCBase32(vec8(0xffn))
665
+ const alias = canonical.toUpperCase()
666
+ const [state1, added] = virtualOk(state0)(e.add({
667
+ parents: [], subject: 'doc', snapshot: canonical, lock: alias,
668
+ }))
669
+ const [state2, result] = virtual(state1)(e.revision(added))
670
+ assert(result[0] === 'ok', ['expected revision ok', result])
671
+ assertEq(result[1].lock, canonical)
672
+ const [, readded] = virtualOk(state2)(e.add(result[1]))
673
+ assertEq(readded, added)
674
+ },
675
+ // A shared-lock reference is checked as a hash on the way in, so a
676
+ // non-cBase32 one fails the write instead of being stored unresolvable.
677
+ revisionInvalidSharedLockReferenceIsRejected: () => {
678
+ const c = fileCas(sha256)(home)
679
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
680
+ const e = evo(c)(cacheKey)
681
+ const [, result] = virtual(state0)(e.add({
682
+ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0xffn)),
683
+ lock: 'https://example.com/lock',
684
+ }))
685
+ assertEvoError(result, 'lock reference is not a valid hash')
686
+ },
687
+ // A nested lock map round-trips with its scope structure intact, and
688
+ // canonicalization recurses into it: an alias spelling bound inside a
689
+ // scope comes back canonical, like a direct one.
690
+ revisionNestedLockRoundTripsAndCanonicalizes: () => {
691
+ const c = fileCas(sha256)(home)
692
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
693
+ const e = evo(c)(cacheKey)
694
+ const canonical = vecToCBase32(vec8(0xffn))
695
+ const alias = canonical.toUpperCase()
696
+ const [state1, added] = virtualOk(state0)(e.add({
697
+ parents: [], subject: 'doc', snapshot: canonical,
698
+ lock: { B: { D: alias }, C: canonical },
699
+ }))
700
+ const [state2, result] = virtual(state1)(e.revision(added))
655
701
  assert(result[0] === 'ok', ['expected revision ok', result])
656
- assertEq(result[1].lock?.dependency, canonical)
702
+ const lock = result[1].lock
703
+ assert(typeof lock === 'object', ['expected an inline lock map', lock])
704
+ const scope = lock.B
705
+ assert(typeof scope === 'object', ['expected a nested scope', scope])
706
+ assertEq(scope.D, canonical)
707
+ // The read value is still valid `add` input, and re-adding it reuses
708
+ // the same address — the round trip loses nothing.
709
+ const [, readded] = virtualOk(state2)(e.add(result[1]))
710
+ assertEq(readded, added)
711
+ },
712
+ // Semantic checking recurses: a non-hash string inside a nested scope
713
+ // fails the write, like one at the root.
714
+ revisionInvalidNestedLockValueIsRejected: () => {
715
+ const c = fileCas(sha256)(home)
716
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
717
+ const e = evo(c)(cacheKey)
718
+ const [, result] = virtual(state0)(e.add({
719
+ parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
720
+ lock: { B: { dependency: 'not a hash!' } },
721
+ }))
722
+ assertEvoError(result, 'B/dependency')
657
723
  },
658
724
  revisionAbsentAndEmptyLocksRemainDistinct: () => {
659
725
  const c = fileCas(sha256)(home)
660
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
726
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
661
727
  const e = evo(c)(cacheKey)
662
728
  const snapshot = vecToCBase32(vec8(0x42n))
663
- const [state1, absent] = virtual(state0)(e.add({ parents: [], subject: 'absent', snapshot }))
664
- assert(absent[0] === 'ok', ['expected add ok', absent])
665
- const [state2, empty] = virtual(state1)(e.add({ parents: [], subject: 'empty', snapshot, lock: {} }))
666
- assert(empty[0] === 'ok', ['expected add ok', empty])
667
- const [state3, absentRead] = virtual(state2)(e.revision(absent[1]))
729
+ const [state1, absent] = virtualOk(state0)(e.add({ parents: [], subject: 'absent', snapshot }))
730
+ const [state2, empty] = virtualOk(state1)(e.add({ parents: [], subject: 'empty', snapshot, lock: {} }))
731
+ const [state3, absentRead] = virtual(state2)(e.revision(absent))
668
732
  assert(absentRead[0] === 'ok', ['expected revision ok', absentRead])
669
- const [, emptyRead] = virtual(state3)(e.revision(empty[1]))
733
+ const [, emptyRead] = virtual(state3)(e.revision(empty))
670
734
  assert(emptyRead[0] === 'ok', ['expected revision ok', emptyRead])
671
735
  assertEq(absentRead[1].lock, undefined)
672
736
  assertEq(Object.keys(emptyRead[1].lock ?? {}).length, 0)
673
737
  },
674
738
  revisionInvalidLockValueIsRejected: () => {
675
739
  const c = fileCas(sha256)(home)
676
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
740
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
677
741
  const e = evo(c)(cacheKey)
678
742
  const [, result] = virtual(state0)(e.add({
679
743
  parents: [], subject: 'doc', snapshot: vecToCBase32(vec8(0x42n)),
680
744
  lock: { dependency: 'not a hash!' },
681
745
  }))
682
- assertEq(result[0], 'error')
746
+ assertEvoError(result, 'dependency')
683
747
  },
684
748
  equivalentLockOrdersReuseOneCasAddress: () => {
685
749
  const c = fileCas(sha256)(home)
686
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
750
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
751
+ const e = evo(c)(cacheKey)
752
+ const a = vecToCBase32(vec8(0x1an))
753
+ const b = vecToCBase32(vec8(0x2bn))
754
+ const snapshot = vecToCBase32(vec8(0x3cn))
755
+ const [state1, first] = virtualOk(state0)(e.add({ parents: [], subject: 'doc', snapshot, lock: { '2': b, '10': a } }))
756
+ const [, second] = virtualOk(state1)(e.add({ lock: { '10': a, '2': b }, snapshot, subject: 'doc', parents: [] }))
757
+ assertEq(second, first)
758
+ },
759
+ // Canonical serialization reaches into nested scopes, so two adds
760
+ // differing only in the property order of a nested map — at the root and
761
+ // inside the scope alike — deduplicate to one blob rather than leaving
762
+ // two heads.
763
+ equivalentNestedLockOrdersReuseOneCasAddress: () => {
764
+ const c = fileCas(sha256)(home)
765
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
687
766
  const e = evo(c)(cacheKey)
688
767
  const a = vecToCBase32(vec8(0x1an))
689
768
  const b = vecToCBase32(vec8(0x2bn))
690
769
  const snapshot = vecToCBase32(vec8(0x3cn))
691
- const [state1, first] = virtual(state0)(e.add({ parents: [], subject: 'doc', snapshot, lock: { '2': b, '10': a } }))
692
- assert(first[0] === 'ok', ['expected add ok', first])
693
- const [, second] = virtual(state1)(e.add({ lock: { '10': a, '2': b }, snapshot, subject: 'doc', parents: [] }))
694
- assert(second[0] === 'ok', ['expected add ok', second])
695
- assertEq(second[1], first[1])
770
+ const [state1, first] = virtualOk(state0)(e.add({
771
+ parents: [], subject: 'doc', snapshot,
772
+ lock: { B: { '2': b, '10': a }, A: a },
773
+ }))
774
+ const [, second] = virtualOk(state1)(e.add({
775
+ parents: [], subject: 'doc', snapshot,
776
+ lock: { A: a, B: { '10': a, '2': b } },
777
+ }))
778
+ assertEq(second, first)
696
779
  },
697
780
  // A raw CAS write (e.g. `cas_add`) of valid revision content is folded
698
781
  // into the cache exactly as `addRevision` would, without going through
@@ -700,29 +783,29 @@ export const proof = {
700
783
  // same store consistent (see `fjs/mcp`).
701
784
  syncRevisionFoldsValidRevisionIntoCache: () => {
702
785
  const c = fileCas(sha256)(home)
703
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
786
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
704
787
  const subjectHash = vecToCBase32(vec8(0x13n))
705
788
  const text = `{"dialect":"${revisionDialect}","subject":"${subjectHash}","parents":[],"snapshot":"${subjectHash}","generation":0}`
706
789
  const bytes = tryUtf8(text)
707
790
  assert(bytes !== null, 'expected sample revision to encode as UTF-8')
708
791
  const hashVec = vec8(0x14n)
709
- const [state1] = virtual(state0)(syncRevision(cacheKey)(hashVec)(bytes))
710
- const [, heads] = virtual(state1)(evo(c)(cacheKey).head(subjectHash))
792
+ const [state1] = virtualOk(state0)(syncRevision(cacheKey)(hashVec)(bytes))
793
+ const [, heads] = virtualOk(state1)(evo(c)(cacheKey).head(subjectHash))
711
794
  assertEq(heads.length, 1)
712
795
  assertEq(heads[0], vecToCBase32(hashVec))
713
796
  },
714
797
  syncRevisionIgnoresNonRevisionContent: () => {
715
798
  const c = fileCas(sha256)(home)
716
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
717
- const [state1] = virtual(state0)(syncRevision(cacheKey)(vec8(0x15n))(vec8(0x41n)))
718
- const [, subjects] = virtual(state1)(evo(c)(cacheKey).list())
799
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
800
+ const [state1] = virtualOk(state0)(syncRevision(cacheKey)(vec8(0x15n))(vec8(0x41n)))
801
+ const [, subjects] = virtualOk(state1)(evo(c)(cacheKey).list())
719
802
  assertEq(subjects.length, 0)
720
803
  },
721
804
  evoHeadUnknownSubjectIsEmpty: () => {
722
805
  const c = fileCas(sha256)(home)
723
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
806
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
724
807
  const e = evo(c)(cacheKey)
725
- const [, heads] = virtual(state0)(e.head('nope'))
808
+ const [, heads] = virtualOk(state0)(e.head('nope'))
726
809
  assertEq(heads.length, 0)
727
810
  },
728
811
  // Regression: subjects are arbitrary caller-supplied strings, so one can
@@ -732,21 +815,20 @@ export const proof = {
732
815
  // `.hashes` — the cache must use an own-property lookup instead.
733
816
  evoHeadUnknownPrototypeNamedSubjectIsEmpty: () => {
734
817
  const c = fileCas(sha256)(home)
735
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
818
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
736
819
  const e = evo(c)(cacheKey)
737
- const [, heads] = virtual(state0)(e.head('constructor'))
820
+ const [, heads] = virtualOk(state0)(e.head('constructor'))
738
821
  assertEq(heads.length, 0)
739
822
  },
740
823
  evoAddAndHeadHandlePrototypeNamedSubject: () => {
741
824
  const c = fileCas(sha256)(home)
742
- const [state0, cacheKey] = virtual(emptyState)(initEvo(c))
825
+ const [state0, cacheKey] = virtualOk(emptyState)(initEvo(c))
743
826
  const e = evo(c)(cacheKey)
744
827
  /** @type {RevisionData} */
745
828
  const input = { parents: [], subject: 'toString', snapshot: vecToCBase32(vec8(0x16n)) }
746
- const [state1, result] = virtual(state0)(e.add(input))
747
- assert(result[0] === 'ok', ['expected add ok', result])
748
- const [, heads] = virtual(state1)(e.head('toString'))
829
+ const [state1, result] = virtualOk(state0)(e.add(input))
830
+ const [, heads] = virtualOk(state1)(e.head('toString'))
749
831
  assertEq(heads.length, 1)
750
- assertEq(heads[0], result[1])
832
+ assertEq(heads[0], result)
751
833
  },
752
834
  }