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
@@ -17,11 +17,12 @@
17
17
  * @import { Unknown } from '../json/types.ts'
18
18
  * @import { Result } from '../../types/result/types.ts'
19
19
  * @import { DialectEntry } from '../types.ts'
20
- * @import { Revision, RevisionError } from './types.ts'
20
+ * @import { String as RttiString } from '../../types/rtti/types.ts'
21
+ * @import { LockField, LockFieldSchema, LockMap, LockSchema, Revision, RevisionError } from './types.ts'
21
22
  */
22
23
 
23
- import { array, number, option, record, string } from '../../types/rtti/module.f.mjs'
24
- import { validate as rttiValidate } from '../../types/rtti/validate/module.f.mjs'
24
+ import { array, number, option, string } from '../../types/rtti/module.f.mjs'
25
+ import { parse as rttiParse } from '../../types/rtti/parse/module.f.mjs'
25
26
  import { parse as parseJson } from '../json/module.f.mjs'
26
27
  import { cBase32ToVec } from '../../basen/cbase32/module.f.mjs'
27
28
  import { error, ok, okThen } from '../../types/result/module.f.mjs'
@@ -49,8 +50,64 @@ export const mediaType = /** @type {const} */ (`application/${dialect}+json`)
49
50
  */
50
51
  export const hash = string
51
52
 
52
- /** Structural schema for a Stage 1 flat lock map. */
53
- export const _lock = record(string)
53
+ /**
54
+ * rtti schema for a lock map: an open map whose every value is either a
55
+ * direct hash string or a nested lock map, to any depth.
56
+ *
57
+ * Self-referential through {@link lockValue}, which is a module-level
58
+ * constant rather than a union rebuilt inside the thunk: the rtti data form
59
+ * (`fjs/types/rtti/data`, which `toJsonSchema` routes through) closes
60
+ * reference cycles by *identity*, so a schema handing out a fresh union thunk
61
+ * on every call would present an infinite graph and never terminate.
62
+ *
63
+ * The named `@type` — rather than `@type {const}` — is what a self-referential
64
+ * schema needs twice over: a `const` cannot reference itself in its own
65
+ * initializer at all, and naming the recursive position is also what keeps
66
+ * declaration emit from inlining the structure and giving up at depth (see
67
+ * `fjs/AGENTS.md` §3.2 and `../json/rtti/module.f.mjs`).
68
+ *
69
+ * Like `hash`, this is `string` at the structural level; cbase32 decodability
70
+ * of every direct value, at every depth, is enforced by
71
+ * {@link checkReferences}.
72
+ *
73
+ * @type {LockSchema}
74
+ */
75
+ export const lock = () => ['record', lockValue]
76
+
77
+ /**
78
+ * One lock-map value: a direct hash, or a nested lock map. Written as the
79
+ * union tuple rather than as `or(string, lock)` so the thunk carries the name
80
+ * `lockValue`: this is where the reference cycle closes, and the data form
81
+ * names a rule after its defining function — an anonymous thunk would publish
82
+ * the recursion as `$defs: { '': … }` in every derived JSON Schema.
83
+ *
84
+ * @type {() => readonly['or', RttiString, LockSchema]}
85
+ */
86
+ const lockValue = () => ['or', string, lock]
87
+
88
+ /**
89
+ * rtti schema for a revision's `lock` **field**: the bindings inline as a lock
90
+ * map, or a hash naming a `vnd.fjs.lock` blob (`fjs/media/lock`) that holds
91
+ * one to share — see [Shared lock references](./README.md#shared-lock-references).
92
+ *
93
+ * Structurally identical to {@link lockValue}, and deliberately a separate
94
+ * name: the two positions mean different things. A string *inside* a map is a
95
+ * dependency's content hash; a string in this position is a lock blob's hash,
96
+ * i.e. where the whole map lives. Only the top level is widened, so a nested
97
+ * string keeps meaning exactly what it always did and no position is
98
+ * ambiguous.
99
+ *
100
+ * Widening the field rather than adding a `lockRef` sibling is what keeps this
101
+ * dialect: an older reader validates `lock` as a map and rejects a string
102
+ * outright, whereas an unknown sibling field would validate and be read as
103
+ * "no bindings were recorded" — the fail-open misread the versioning rule
104
+ * exists to prevent. It also makes "inline map *and* reference" unstatable, so
105
+ * the format defines no precedence between them, consistent with its refusal
106
+ * to define overlay or inheritance for nested maps.
107
+ *
108
+ * @type {LockFieldSchema}
109
+ */
110
+ export const lockField = () => ['or', hash, lock]
54
111
 
55
112
  /**
56
113
  * rtti schema for a `revision` BLOB. See the README for the full semantics of
@@ -64,7 +121,7 @@ export const revisionSchema = /** @type {const} */ ({
64
121
  snapshot: hash,
65
122
  generation: number,
66
123
  archived: option(true),
67
- lock: option(_lock),
124
+ lock: option(lockField),
68
125
  })
69
126
 
70
127
  /** Serializes a revision canonically, recursively sorting every object's property names.
@@ -73,19 +130,70 @@ export const revisionSchema = /** @type {const} */ ({
73
130
  export const encodeText = stringify(sort)
74
131
 
75
132
  /** Structural-only validator: checks the shape, not the hash / generation semantics. */
76
- const validateShape = rttiValidate(revisionSchema)
133
+ const validateShape = rttiParse(revisionSchema)
77
134
 
78
135
  /** True when `s` decodes as a cbase32 CAS hash (rejects `https://` and any other non-cbase32 string).
79
136
  * @type {(s: string) => boolean}
80
137
  */
81
138
  export const isHash = s => cBase32ToVec(s) !== null
82
139
 
140
+ /**
141
+ * The first reason a structurally valid lock map is not a valid one, or
142
+ * `null` when every direct value at every depth is a cbase32 hash
143
+ * ({@link isHash}). Nested maps are scopes, not references, so only the
144
+ * strings are checked; `scope` names the path walked to reach the offending
145
+ * value, so a failure deep in a nested map still says where it is.
146
+ *
147
+ * @type {(scope: readonly string[]) => (lock: LockMap) => string | null}
148
+ */
149
+ const lockError = scope => lock => {
150
+ for (const [subject, value] of definedEntries(lock)) {
151
+ const path = [...scope, subject]
152
+ const message = typeof value === 'string'
153
+ ? (isHash(value) ? null : `lock value for ${path.join('/')} is not a valid hash: ${value}`)
154
+ : lockError(path)(value)
155
+ if (message !== null) { return message }
156
+ }
157
+ return null
158
+ }
159
+
160
+ /**
161
+ * The first reason a structurally valid lock map is not a valid one, or `null`
162
+ * — {@link lockError} rooted at the empty scope, so a reported path is
163
+ * relative to the map itself.
164
+ *
165
+ * Exported because `fjs/media/lock` validates the very same map as a
166
+ * standalone blob: one recursive schema and one semantic check, so a map means
167
+ * the same thing inline and shared, and the two forms cannot drift.
168
+ *
169
+ * @type {(lock: LockMap) => string | null}
170
+ */
171
+ export const lockMapError = lockError([])
172
+
173
+ /**
174
+ * The first reason a structurally valid `lock` field is not a valid one, or
175
+ * `null`. A map is checked entry by entry ({@link lockMapError}); a string is
176
+ * a reference to a `vnd.fjs.lock` blob and is checked as a cbase32 hash and
177
+ * nothing more — this module is pure format with no store access, so whether
178
+ * the blob exists, and what its bindings mean once fetched, stay a resolver's
179
+ * business exactly as they do for `snapshot`.
180
+ *
181
+ * @type {(value: LockField) => string | null}
182
+ */
183
+ export const lockFieldError = value =>
184
+ typeof value === 'string'
185
+ ? (isHash(value) ? null : `lock reference is not a valid hash: ${value}`)
186
+ : lockMapError(value)
187
+
83
188
  /**
84
189
  * Checks the semantic refinements the structural schema can't express on an
85
190
  * already shape-valid revision: every `parents` entry and the `snapshot` must
86
- * decode as a cbase32 hash ({@link isHash}), and `generation` must be a
191
+ * decode as a cbase32 hash ({@link isHash}), the `lock` field must too
192
+ * every direct value at every depth of an inline map, or the shared-lock
193
+ * reference itself ({@link lockFieldError}) — and `generation` must be a
87
194
  * non-negative *safe* integer. `subject` is not checked — it is an identity
88
- * string, never a snapshot reference, so any string is valid.
195
+ * string, never a snapshot reference, so any string is valid, and the same
196
+ * goes for a lock map's keys, which are subjects.
89
197
  *
90
198
  * `generation` uses `Number.isSafeInteger`, not `Number.isInteger`: a value at
91
199
  * or above `2 ** 53` passes `isInteger` but is no longer uniquely
@@ -113,9 +221,8 @@ export const checkReferences = r => {
113
221
  if (!isHash(p)) { return error(`parent is not a valid hash: ${p}`) }
114
222
  }
115
223
  if (!isHash(r.snapshot)) { return error(`snapshot is not a valid hash: ${r.snapshot}`) }
116
- for (const [subject, snapshot] of definedEntries(r.lock ?? {})) {
117
- if (!isHash(snapshot)) { return error(`lock value for ${subject} is not a valid hash: ${snapshot}`) }
118
- }
224
+ const lockMessage = r.lock === undefined ? null : lockFieldError(r.lock)
225
+ if (lockMessage !== null) { return error(lockMessage) }
119
226
  if (!Number.isSafeInteger(r.generation) || r.generation < 0) {
120
227
  return error(`generation must be a non-negative safe integer: ${r.generation}`)
121
228
  }
@@ -28,6 +28,13 @@ export declare const proof: {
28
28
  malformedLockRejected: () => void;
29
29
  invalidHashLockRejected: () => void;
30
30
  aliasHashLockAccepted: () => void;
31
+ nestedLockAccepted: () => void;
32
+ deeplyNestedLockAccepted: () => void;
33
+ malformedNestedLockRejected: () => void;
34
+ invalidNestedHashLockRejected: () => void;
35
+ sharedLockReferenceAccepted: () => void;
36
+ invalidSharedLockReferenceRejected: () => void;
37
+ nestedValueIsStillNotAReference: () => void;
31
38
  wrongDialectRejected: () => void;
32
39
  missingSubjectRejected: () => void;
33
40
  extraFieldsAccepted: () => void;
@@ -40,6 +47,9 @@ export declare const proof: {
40
47
  };
41
48
  encodeText: {
42
49
  recursivelySortsObjectsLexicographically: () => void;
50
+ sortsNestedLockKeys: () => void;
51
+ equivalentNestedLockOrdersConverge: () => void;
52
+ serializesSharedLockReferenceAsAString: () => void;
43
53
  preservesArrayOrder: () => void;
44
54
  parsedEquivalentSourcesConverge: () => void;
45
55
  };
@@ -16,6 +16,11 @@ const alias = /** @type {const} */ ('I')
16
16
  /** @type {LockMap} */
17
17
  const _lockMapAllowsMissingSubjects = {}
18
18
 
19
+ // A lock map's values are `string | LockMap | undefined` at every depth, so
20
+ // the same value is a legal binding for a direct hash and for a nested scope.
21
+ /** @type {LockMap} */
22
+ const _lockMapNests = { direct: h1, scope: { direct: h2, deeper: { direct: h1 } } }
23
+
19
24
  // A shape-valid revision: every required field present (`snapshot` and
20
25
  // `generation` included), with `extra` overriding or adding fields per test.
21
26
  /** @type {(extra: JsonObject) => JsonObject} */
@@ -155,6 +160,62 @@ export const proof = {
155
160
  assertEq(t, 'ok')
156
161
  },
157
162
 
163
+ // A nested map scopes further bindings under a subject — the
164
+ // incompatible-diamond case from the README, where `B` and `C` each
165
+ // pick their own `D`.
166
+ nestedLockAccepted: () => {
167
+ const r = validate(revisionOf({ lock: { B: { B: h1, D: h1 }, C: { C: h2, D: h2 } } }))
168
+ assert(r[0] === 'ok', ['expected ok', r])
169
+ assert(encodeText(r[1]).includes(`"lock":{"B":{"B":"${h1}","D":"${h1}"},"C":{"C":"${h2}","D":"${h2}"}}`))
170
+ },
171
+
172
+ // Nesting has no depth limit, and a nested map may be sparse — it need
173
+ // not bind the subject it appears under, or anything at all.
174
+ deeplyNestedLockAccepted: () => {
175
+ const [t] = validate(revisionOf({ lock: { B: { C: { D: { E: h1 } } }, empty: {} } }))
176
+ assertEq(t, 'ok')
177
+ },
178
+
179
+ // A direct value must be a string or a map at every depth: a number
180
+ // nested two levels down fails structural (rtti) validation, exactly
181
+ // as it does at the root.
182
+ malformedNestedLockRejected: () => {
183
+ const [t] = validate(revisionOf({ lock: { B: { D: 1 } } }))
184
+ assertEq(t, 'error')
185
+ },
186
+
187
+ // Semantic reference checking recurses too: a non-hash string deep in
188
+ // a nested scope is rejected, and the message names the path to it.
189
+ invalidNestedHashLockRejected: () => {
190
+ const r = validate(revisionOf({ lock: { B: { D: 'https://example.com/x' } } }))
191
+ assert(r[0] === 'error', ['expected error', r])
192
+ assertEq(r[1], 'lock value for B/D is not a valid hash: https://example.com/x')
193
+ },
194
+
195
+ // In place of the map, `lock` may be the hash of a `vnd.fjs.lock`
196
+ // blob holding one — the only position where a string means "where
197
+ // the bindings are" rather than "this dependency's content".
198
+ sharedLockReferenceAccepted: () => {
199
+ const r = validate(revisionOf({ lock: h1 }))
200
+ assert(r[0] === 'ok', ['expected ok', r])
201
+ assertEq(r[1].lock, h1)
202
+ },
203
+
204
+ // A reference is validated as a cbase32 hash and nothing more — the
205
+ // same contract `snapshot` has — so a non-hash one is rejected.
206
+ invalidSharedLockReferenceRejected: () => {
207
+ const r = validate(revisionOf({ lock: 'https://example.com/lock' }))
208
+ assert(r[0] === 'error', ['expected error', r])
209
+ assertEq(r[1], 'lock reference is not a valid hash: https://example.com/lock')
210
+ },
211
+
212
+ // Only the top level is widened: a nested value is still a hash or a
213
+ // map, so nothing about an inline map's meaning changed.
214
+ nestedValueIsStillNotAReference: () => {
215
+ const [t] = validate(revisionOf({ lock: { B: { D: h1 } } }))
216
+ assertEq(t, 'ok')
217
+ },
218
+
158
219
  // Wrong dialect tag: structural validation rejects it outright.
159
220
  wrongDialectRejected: () => {
160
221
  const [t] = validate({ dialect: 'vnd.fjs.other', subject: h1, parents: [], snapshot: h2, generation: 0 })
@@ -213,6 +274,32 @@ export const proof = {
213
274
  `{"dialect":"${dialect}","generation":0,"lock":{"10":"${h1}","2":"${h2}"},"parents":[],"snapshot":"${h2}","subject":"${h1}"}`,
214
275
  )
215
276
  },
277
+ // The one `stringify(sort)` rule already reaches every depth, so a
278
+ // nested lock map needs no serialization special case: its keys sort
279
+ // as strings like every other object's ("10" before "2").
280
+ sortsNestedLockKeys: () => {
281
+ const revision = revisionOf({ lock: { scope: { '2': h2, '10': h1 }, '2': h2, '10': h1 } })
282
+ const decoded = validate(revision)
283
+ assert(decoded[0] === 'ok', ['expected ok', decoded])
284
+ assert(encodeText(decoded[1]).includes(
285
+ `"lock":{"10":"${h1}","2":"${h2}","scope":{"10":"${h1}","2":"${h2}"}}`))
286
+ },
287
+ // Two nested maps differing only in property order converge on one
288
+ // byte sequence, so they address the same CAS blob.
289
+ equivalentNestedLockOrdersConverge: () => {
290
+ const a = validate(revisionOf({ lock: { B: { B: h1, D: h2 }, C: { D: h1 } } }))
291
+ const b = validate(revisionOf({ lock: { C: { D: h1 }, B: { D: h2, B: h1 } } }))
292
+ assert(a[0] === 'ok', ['expected ok', a])
293
+ assert(b[0] === 'ok', ['expected ok', b])
294
+ assertEq(encodeText(a[1]), encodeText(b[1]))
295
+ },
296
+ // A shared-lock reference serializes as the plain string it is — no
297
+ // wrapper, no marker: the two forms are told apart by JSON type.
298
+ serializesSharedLockReferenceAsAString: () => {
299
+ const decoded = validate(revisionOf({ lock: h1 }))
300
+ assert(decoded[0] === 'ok', ['expected ok', decoded])
301
+ assert(encodeText(decoded[1]).includes(`"lock":"${h1}"`))
302
+ },
216
303
  preservesArrayOrder: () => {
217
304
  const decoded = validate(revisionOf({ parents: [h2, h1] }))
218
305
  assert(decoded[0] === 'ok', ['expected ok', decoded])
@@ -229,3 +316,4 @@ export const proof = {
229
316
  }
230
317
 
231
318
  void _lockMapAllowsMissingSubjects
319
+ void _lockMapNests
@@ -1,15 +1,44 @@
1
1
  /**
2
2
  * Type-level API for `fjs/media/revision/module.f.mjs`: `LockMap`,
3
- * `Revision`, and `RevisionError`, derived from the module's own rtti
4
- * schemas.
3
+ * `LockSchema`, `LockField`, `LockFieldSchema`, `Revision`, and
4
+ * `RevisionError`.
5
+ *
6
+ * `LockMap` is written by hand rather than derived, so that the recursion
7
+ * reads directly, and is then pinned against the module's rtti schema with
8
+ * `Assert<Equal<LockMap, Ts<typeof lock>>>` — the same arrangement the JSON
9
+ * data model uses in [`../json/types.ts`](../json/types.ts). `LockSchema` is
10
+ * the schema side of the same recursion: `lock` cannot infer its own type
11
+ * (a `const` may not reference itself in its own initializer), so it carries
12
+ * this named annotation instead.
5
13
  *
6
14
  * @module
7
15
  */
8
16
  import type { Ts } from '../../types/rtti/ts/types.ts';
17
+ import type { String as RttiString } from '../../types/rtti/types.ts';
9
18
  import type { ValidationError } from '../../types/rtti/common/types.ts';
10
- import type { _lock, revisionSchema } from './module.f.mjs';
11
- /** A flat set of subject-to-snapshot bindings supplied to dependency resolvers. */
12
- export type LockMap = Ts<typeof _lock>;
19
+ import type { revisionSchema } from './module.f.mjs';
20
+ /**
21
+ * A set of subject-to-snapshot bindings supplied to dependency resolvers.
22
+ * A direct string value selects immutable content; a nested map scopes
23
+ * further bindings under that subject (see the README).
24
+ *
25
+ * Spelled inline rather than as `StringMap<string | LockMap>`: a type alias
26
+ * may not reference itself through another alias's instantiation (TS2456).
27
+ */
28
+ export type LockMap = {
29
+ readonly [subject in string]?: string | LockMap;
30
+ };
31
+ /** The rtti schema type of `lock` — a record of `string | LockSchema`. */
32
+ export type LockSchema = () => readonly ['record', () => readonly ['or', RttiString, LockSchema]];
33
+ /**
34
+ * A revision's `lock` field: the bindings inline as a {@link LockMap}, or the
35
+ * cbase32 hash of a `vnd.fjs.lock` blob (`fjs/media/lock`) holding one to
36
+ * share. Only the top level admits a hash — a string *inside* a map is a
37
+ * dependency's content, unchanged.
38
+ */
39
+ export type LockField = string | LockMap;
40
+ /** The rtti schema type of `lockField` — a shared-lock reference or a lock map. */
41
+ export type LockFieldSchema = () => readonly ['or', RttiString, LockSchema];
13
42
  /** The TypeScript type derived from `revisionSchema` — the single source of truth. */
14
43
  export type Revision = Ts<typeof revisionSchema>;
15
44
  /** Either a structural validation error or a semantic (hash / generation) error message. */
@@ -37,24 +37,41 @@
37
37
  *
38
38
  * @import { Vec } from '../../types/bit_vec/types.ts'
39
39
  * @import { Nullable } from '../../types/nullable/types.ts'
40
- * @import { Effect, Operation } from '../../effects/types.ts'
40
+ * @import { Operation } from '../../effects/types.ts'
41
41
  * @import { List } from '../../effects/list/types.ts'
42
- * @import { IoResult } from '../../effects/node/types.ts'
42
+ * @import { IoChannel, IoResult } from '../../effects/node/types.ts'
43
+ * @import { Effect } from '../../effects/types.ts'
43
44
  * @import { DetectMeta, DetectState, _MagicState, _Signature, _Utf8Detect } from './types.ts'
44
45
  */
45
46
  import type { Vec } from '../../types/bit_vec/types.ts';
46
47
  import type { Nullable } from '../../types/nullable/types.ts';
47
- import type { Effect, Operation } from '../../effects/types.ts';
48
+ import type { Operation } from '../../effects/types.ts';
48
49
  import type { List } from '../../effects/list/types.ts';
49
- import type { IoResult } from '../../effects/node/types.ts';
50
+ import type { IoChannel } from '../../effects/node/types.ts';
51
+ import type { Effect } from '../../effects/types.ts';
50
52
  import type { DetectMeta, DetectState } from './types.ts';
51
53
  /**
52
- * Detects the MIME type of `bytes` from its leading magic-byte signature by
53
- * folding them through the same eliminator the streaming detector uses. Both
54
- * absorbing states end the fold: a `matched` signature is the answer, and a
55
- * `dead` eliminator can no longer produce one. Bytes still in `scan` at the end
56
- * of the `Vec` a prefix too short to complete any signature — leave the
57
- * eliminator unsettled, which reads as `null`.
54
+ * Detects the MIME type of `bytes` from its leading magic-byte signature, as
55
+ * the magic projection of the streaming detector below — one fold, read two
56
+ * ways, rather than a second copy of it.
57
+ *
58
+ * The two agree because `magicStep` returns a non-`scan` state unchanged, so
59
+ * both absorbing states are its fixed points. `detect`'s old loop stopped at
60
+ * the first one; `push` stops only once the *whole* verdict is settled and may
61
+ * keep folding for the UTF-8 factor after the magic state is `dead` — but a
62
+ * `dead` or `matched` magic cannot move again, so the final magic is the same
63
+ * either way. A signature still in `scan` at the end of the `Vec` — a prefix
64
+ * too short to complete any signature — reads as `null`.
65
+ *
66
+ * Cost follows `push`, which stops only once the whole verdict is settled, not
67
+ * once the magic state is. On a signature match or on non-text bytes that is
68
+ * still an early exit; on valid text it now reads the whole `Vec`, because the
69
+ * shared machine is still tracking the UTF-8 factor this function ignores
70
+ * (measured: 100 KB of ASCII, 1.2 ms before, 82 ms after). That is the same
71
+ * work {@link detectVec} already does on the same bytes, and `Vec` is capped at
72
+ * `maxLength`, so the cost is bounded rather than unbounded — but a caller
73
+ * scanning many large text blobs for signatures alone should know it is paying
74
+ * for the UTF-8 fold too.
58
75
  *
59
76
  * The bytes come from `u8List`, so a `Vec` whose length is not a whole number of
60
77
  * bytes has its trailing partial byte zero-padded — the same reading of a ragged
@@ -104,11 +121,12 @@ export declare const finish: (s: DetectState) => DetectMeta;
104
121
  export declare const detectVec: (bytes: Vec) => DetectMeta;
105
122
  /**
106
123
  * Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
107
- * deriving `cas_get` metadata without ever materializing the blob. A read `error`
108
- * item short-circuits into the `IoResult` error.
124
+ * deriving `cas_get` metadata without ever materializing the blob. A stream that
125
+ * fails carries its failure out as this one's, through `step` rather than
126
+ * through a case in the loop.
109
127
  *
110
128
  * @template {Operation} O
111
- * @param {List<O, IoResult<Vec>>} stream
112
- * @returns {Effect<O, IoResult<DetectMeta>>}
129
+ * @param {List<O, Vec, IoChannel>} stream
130
+ * @returns {Effect<O, DetectMeta, IoChannel>}
113
131
  */
114
- export declare const detectStream: <O extends Operation>(stream: List<O, IoResult<Vec>>) => Effect<O, IoResult<DetectMeta>>;
132
+ export declare const detectStream: <O extends Operation>(stream: List<O, Vec, IoChannel>) => Effect<O, DetectMeta, IoChannel>;
@@ -37,16 +37,16 @@
37
37
  *
38
38
  * @import { Vec } from '../../types/bit_vec/types.ts'
39
39
  * @import { Nullable } from '../../types/nullable/types.ts'
40
- * @import { Effect, Operation } from '../../effects/types.ts'
40
+ * @import { Operation } from '../../effects/types.ts'
41
41
  * @import { List } from '../../effects/list/types.ts'
42
- * @import { IoResult } from '../../effects/node/types.ts'
42
+ * @import { IoChannel, IoResult } from '../../effects/node/types.ts'
43
+ * @import { Effect } from '../../effects/types.ts'
43
44
  * @import { DetectMeta, DetectState, _MagicState, _Signature, _Utf8Detect } from './types.ts'
44
45
  */
45
46
 
46
47
  import { msb, length, u8List } from '../../types/bit_vec/module.f.mjs'
47
48
  import { iterable } from '../../types/list/module.f.mjs'
48
- import { pure, step } from '../../effects/module.f.mjs'
49
- import { ok, error } from '../../types/result/module.f.mjs'
49
+ import { pureOk, step as ioStep } from '../../effects/module.f.mjs'
50
50
  import { isValidCodePoint, isTextCodePoint } from '../../text/code_point/module.f.mjs'
51
51
  import { utf8ByteToCodePointOp } from '../../text/utf8/module.f.mjs'
52
52
 
@@ -100,12 +100,27 @@ const magicStep = (m, byte) => {
100
100
  const magicMime = m => m.tag === 'matched' ? m.mime : null
101
101
 
102
102
  /**
103
- * Detects the MIME type of `bytes` from its leading magic-byte signature by
104
- * folding them through the same eliminator the streaming detector uses. Both
105
- * absorbing states end the fold: a `matched` signature is the answer, and a
106
- * `dead` eliminator can no longer produce one. Bytes still in `scan` at the end
107
- * of the `Vec` a prefix too short to complete any signature — leave the
108
- * eliminator unsettled, which reads as `null`.
103
+ * Detects the MIME type of `bytes` from its leading magic-byte signature, as
104
+ * the magic projection of the streaming detector below — one fold, read two
105
+ * ways, rather than a second copy of it.
106
+ *
107
+ * The two agree because `magicStep` returns a non-`scan` state unchanged, so
108
+ * both absorbing states are its fixed points. `detect`'s old loop stopped at
109
+ * the first one; `push` stops only once the *whole* verdict is settled and may
110
+ * keep folding for the UTF-8 factor after the magic state is `dead` — but a
111
+ * `dead` or `matched` magic cannot move again, so the final magic is the same
112
+ * either way. A signature still in `scan` at the end of the `Vec` — a prefix
113
+ * too short to complete any signature — reads as `null`.
114
+ *
115
+ * Cost follows `push`, which stops only once the whole verdict is settled, not
116
+ * once the magic state is. On a signature match or on non-text bytes that is
117
+ * still an early exit; on valid text it now reads the whole `Vec`, because the
118
+ * shared machine is still tracking the UTF-8 factor this function ignores
119
+ * (measured: 100 KB of ASCII, 1.2 ms before, 82 ms after). That is the same
120
+ * work {@link detectVec} already does on the same bytes, and `Vec` is capped at
121
+ * `maxLength`, so the cost is bounded rather than unbounded — but a caller
122
+ * scanning many large text blobs for signatures alone should know it is paying
123
+ * for the UTF-8 fold too.
109
124
  *
110
125
  * The bytes come from `u8List`, so a `Vec` whose length is not a whole number of
111
126
  * bytes has its trailing partial byte zero-padded — the same reading of a ragged
@@ -117,15 +132,7 @@ const magicMime = m => m.tag === 'matched' ? m.mime : null
117
132
  *
118
133
  * @type {(bytes: Vec) => Nullable<string>}
119
134
  */
120
- export const detect = bytes => {
121
- /** @type {_MagicState} */
122
- let magic = magicInit
123
- for (const byte of iterable(u8List(msb)(bytes))) {
124
- magic = magicStep(magic, byte)
125
- if (magic.tag !== 'scan') { break }
126
- }
127
- return magicMime(magic)
128
- }
135
+ export const detect = bytes => magicMime(push(detectInit)(bytes).magic)
129
136
 
130
137
  // ── Streaming detector ────────────────────────────────────────────────────────────
131
138
  //
@@ -242,24 +249,23 @@ export const detectVec = bytes => finish(push(detectInit)(bytes))
242
249
 
243
250
  /**
244
251
  * Folds a CAS read stream through {@link push} and reads {@link finish} at EOF,
245
- * deriving `cas_get` metadata without ever materializing the blob. A read `error`
246
- * item short-circuits into the `IoResult` error.
252
+ * deriving `cas_get` metadata without ever materializing the blob. A stream that
253
+ * fails carries its failure out as this one's, through `step` rather than
254
+ * through a case in the loop.
247
255
  *
248
256
  * @template {Operation} O
249
- * @param {List<O, IoResult<Vec>>} stream
250
- * @returns {Effect<O, IoResult<DetectMeta>>}
257
+ * @param {List<O, Vec, IoChannel>} stream
258
+ * @returns {Effect<O, DetectMeta, IoChannel>}
251
259
  */
252
260
  export const detectStream = stream => {
253
- /** @type {(s: DetectState) => (l: List<O, IoResult<Vec>>) => Effect<O, IoResult<DetectMeta>>} */
261
+ /** @type {(s: DetectState) => (l: List<O, Vec, IoChannel>) => Effect<O, DetectMeta, IoChannel>} */
254
262
  const loop = s => l =>
255
- step(
263
+ ioStep(
256
264
  l,
257
265
  node => {
258
- if (node === undefined) { return pure(ok(finish(s))) }
266
+ if (node === undefined) { return pureOk(finish(s)) }
259
267
  const { first, tail } = node
260
- const [t, v] = first
261
- if (t === 'error') { return pure(error(v)) }
262
- return loop(push(s)(v))(tail)
268
+ return loop(push(s)(first))(tail)
263
269
  })
264
270
  return loop(detectInit)(stream)
265
271
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @import { Vec } from '../../types/bit_vec/types.ts'
3
3
  * @import { List } from '../../effects/list/types.ts'
4
- * @import { Result } from '../../types/result/types.ts'
4
+ * @import { IoChannel } from '../../effects/node/types.ts'
5
5
  * @import { DetectMeta } from './types.ts'
6
6
  */
7
7
  export declare const proof: {
@@ -16,6 +16,7 @@ export declare const proof: {
16
16
  spannedZip: () => void;
17
17
  webp: () => void;
18
18
  riffNotWebp: () => void;
19
+ settledMagicIsAFixedPoint: (() => void)[];
19
20
  textIsNull: () => void;
20
21
  shortIsNull: () => void;
21
22
  emptyIsNull: () => void;
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @import { Vec } from '../../types/bit_vec/types.ts'
3
3
  * @import { List } from '../../effects/list/types.ts'
4
- * @import { Result } from '../../types/result/types.ts'
4
+ * @import { IoChannel } from '../../effects/node/types.ts'
5
5
  * @import { DetectMeta } from './types.ts'
6
6
  */
7
7
 
@@ -9,7 +9,9 @@ import { assert, assertEq } from '../../asserts/module.f.mjs'
9
9
  import { msb, u8ListToVec, vec8, repeat, empty } from '../../types/bit_vec/module.f.mjs'
10
10
  import { runPure } from '../../effects/module.f.mjs'
11
11
  import { nonEmpty, empty as emptyList } from '../../effects/list/module.f.mjs'
12
- import { ok } from '../../types/result/module.f.mjs'
12
+ import { pureError } from '../../effects/module.f.mjs'
13
+ import { error, ok } from '../../types/result/module.f.mjs'
14
+ import { ioError } from '../../effects/node/module.f.mjs'
13
15
  import { detect, detectStream, detectVec } from './module.f.mjs'
14
16
 
15
17
  // Builds a big-endian `Vec` from a list of byte values — mirrors how the CAS
@@ -19,12 +21,12 @@ const bytes = (...b) => u8ListToVec(msb)(b)
19
21
 
20
22
  // ── Streaming detector helpers ──────────────────────────────────────────────────
21
23
 
22
- // Builds a CAS-style read stream from a sequence of ok(chunk) items.
23
- /** @type {(...chunks: readonly Vec[]) => List<never, Result<Vec, unknown>>} */
24
+ // Builds a CAS-style read stream from a sequence of chunks.
25
+ /** @type {(...chunks: readonly Vec[]) => List<never, Vec, IoChannel>} */
24
26
  const stream = (...chunks) =>
25
27
  chunks.reduceRight(
26
- (tail, c) => nonEmpty(ok(c), tail),
27
- /** @type {List<never, Result<Vec, unknown>>} */ (emptyList()))
28
+ (tail, c) => nonEmpty(c, tail),
29
+ /** @satisfies {List<never, Vec, IoChannel>} */ (emptyList()))
28
30
 
29
31
  // Runs the streaming detector over the given chunks and unwraps the metadata.
30
32
  /** @type {(...chunks: readonly Vec[]) => DetectMeta} */
@@ -84,6 +86,25 @@ export const proof = {
84
86
  null),
85
87
 
86
88
  // Plain ASCII text matches no signature.
89
+ // `detect` is the magic projection of `push`, and that only holds because
90
+ // `magicStep` leaves a non-`scan` state alone. These pin both absorbing
91
+ // states as fixed points from the outside, where `magicStep` is private.
92
+ settledMagicIsAFixedPoint: [
93
+ // `matched` survives whatever follows the signature — including bytes
94
+ // that would not extend it, and a long tail that would otherwise keep
95
+ // the fold running.
96
+ () => assertEq(detect(bytes(0xff, 0xd8, 0xff, 0x00, 0x01, 0x02)), 'image/jpeg'),
97
+ () => assertEq(detect(bytes(0xff, 0xd8, 0xff, ...Array(64).fill(0x41))), 'image/jpeg'),
98
+ // `dead` survives too. This is the case where `push` keeps folding
99
+ // after the magic state is settled — `isSettled` stays false while the
100
+ // UTF-8 factor is still valid text — so a `dead` magic is fed many more
101
+ // bytes here than `detect`'s old loop ever gave it.
102
+ () => assertEq(detect(bytes(0x41)), null),
103
+ () => assertEq(detect(bytes(0x41, ...Array(64).fill(0x42))), null),
104
+ // ...and the same input still reads as text through the full detector,
105
+ // confirming the fold really did continue past the dead magic.
106
+ () => assertEq(detectVec(bytes(0x41, ...Array(64).fill(0x42))).type, 'text'),
107
+ ],
87
108
  textIsNull: () =>
88
109
  assertEq(detect(bytes(0x68, 0x65, 0x6c, 0x6c, 0x6f)), null),
89
110
 
@@ -270,11 +291,10 @@ export const proof = {
270
291
  assertEq(m.length, 8n)
271
292
  },
272
293
 
273
- // A read `error` item short-circuits into the IoResult error.
294
+ // A stream that fails carries its failure out as the detector's own.
274
295
  readErrorSurfaces: () => {
275
- /** @type {List<never, Result<Vec, unknown>>} */
276
- const errStream =
277
- nonEmpty(/** @type {const} */ (['error', 'boom']), emptyList())
296
+ /** @type {List<never, Vec, IoChannel>} */
297
+ const errStream = pureError(ioError({ message: 'boom' }))
278
298
  const o = runPure(detectStream(errStream))
279
299
  assert(o.length === 1, 'effect is not pure')
280
300
  assert(o[0][0] === 'error')