@noy-db/hub 0.7.0-pre.9 → 0.7.1-pre.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. package/CHANGELOG.md +1327 -0
  2. package/README.md +29 -11
  3. package/dist/api-WYW4YEQG.js +19 -0
  4. package/dist/as/index.js +14 -14
  5. package/dist/at/index.js +9 -10
  6. package/dist/attestation/index.js +18 -19
  7. package/dist/attestation/index.js.map +1 -1
  8. package/dist/{backup-QZQEP5RO.js → backup-XKCXVKV3.js} +23 -18
  9. package/dist/backup-XKCXVKV3.js.map +1 -0
  10. package/dist/blobs/index.js +13 -14
  11. package/dist/blobs/index.js.map +1 -1
  12. package/dist/broker/index.js +12 -13
  13. package/dist/broker/index.js.map +1 -1
  14. package/dist/by/index.js +2 -3
  15. package/dist/cargo/index.js +33 -34
  16. package/dist/{chunk-5UKIZIO3.js → chunk-2577DKXB.js} +2 -2
  17. package/dist/{chunk-SWFIXQF5.js → chunk-2ENE6445.js} +6 -6
  18. package/dist/{chunk-HLFLJCDX.js → chunk-2N5ML6HT.js} +7 -7
  19. package/dist/{chunk-X6YRP5EC.js → chunk-334WSVOW.js} +5 -5
  20. package/dist/{chunk-5EC7X5QA.js → chunk-3BVH633C.js} +5 -5
  21. package/dist/chunk-3BZZJJMQ.js +176 -0
  22. package/dist/chunk-3BZZJJMQ.js.map +1 -0
  23. package/dist/{chunk-7XT7KZTN.js → chunk-3PGS7TYR.js} +10 -10
  24. package/dist/{chunk-OHGCRKWA.js → chunk-3V4KWFX7.js} +18 -8
  25. package/dist/chunk-3V4KWFX7.js.map +1 -0
  26. package/dist/{chunk-AHIIBDNH.js → chunk-3VF3WQ4F.js} +7 -7
  27. package/dist/{chunk-GM4IM7TC.js → chunk-453F654R.js} +2 -2
  28. package/dist/{chunk-5W5CEHLA.js → chunk-4GFX6MPA.js} +6 -6
  29. package/dist/{chunk-JUBWUJY5.js → chunk-4GVM6QZA.js} +2 -2
  30. package/dist/{chunk-2DGBRE4K.js → chunk-4OZNXTO4.js} +2 -2
  31. package/dist/{chunk-DOPZWTMJ.js → chunk-4ZS56W5S.js} +8 -8
  32. package/dist/{chunk-P2RJAVMR.js → chunk-5VCRVKOZ.js} +4 -4
  33. package/dist/chunk-66TQOPFM.js +37 -0
  34. package/dist/chunk-66TQOPFM.js.map +1 -0
  35. package/dist/{chunk-FN26SFGH.js → chunk-6LKOJBV6.js} +2 -2
  36. package/dist/{chunk-P7KJME6C.js → chunk-6YXCPL4T.js} +2 -2
  37. package/dist/{chunk-IS5LFHV2.js → chunk-7APJ6CO6.js} +7 -7
  38. package/dist/{chunk-LAI7ENPW.js → chunk-7MG2LHO5.js} +3 -3
  39. package/dist/{chunk-N4CHCXDL.js → chunk-7MG7SJT7.js} +1 -1
  40. package/dist/{chunk-N4CHCXDL.js.map → chunk-7MG7SJT7.js.map} +1 -1
  41. package/dist/{chunk-SW4TOSJL.js → chunk-7O3Y4PPD.js} +1 -1
  42. package/dist/chunk-7O3Y4PPD.js.map +1 -0
  43. package/dist/{chunk-MU74BYOD.js → chunk-A2HKRLPV.js} +3 -3
  44. package/dist/{chunk-357ECVLE.js → chunk-A66I4JOA.js} +3 -3
  45. package/dist/{chunk-VT4DTCK5.js → chunk-ACAQ2DSG.js} +3 -3
  46. package/dist/chunk-ACAQ2DSG.js.map +1 -0
  47. package/dist/{chunk-TAJZ7GXO.js → chunk-AYX7AINO.js} +3 -3
  48. package/dist/{chunk-RPX2H2OL.js → chunk-BLN54W3L.js} +3 -3
  49. package/dist/{chunk-XGXVPCYW.js → chunk-BRLKZUBT.js} +3 -3
  50. package/dist/{chunk-CWFH36SI.js → chunk-BTDR2PXY.js} +2 -2
  51. package/dist/{chunk-65ZYRK7O.js → chunk-BUE2D7PX.js} +26 -3
  52. package/dist/chunk-BUE2D7PX.js.map +1 -0
  53. package/dist/{chunk-RCONOOVF.js → chunk-BVN6KXL2.js} +5 -5
  54. package/dist/chunk-BVN6KXL2.js.map +1 -0
  55. package/dist/{chunk-UTFY6W6X.js → chunk-BZ3534YM.js} +2 -2
  56. package/dist/{chunk-KZTPL7NL.js → chunk-CHQCZZWC.js} +2 -2
  57. package/dist/chunk-CUQPWYRX.js +1652 -0
  58. package/dist/chunk-CUQPWYRX.js.map +1 -0
  59. package/dist/{chunk-MQPHKJ74.js → chunk-DFMWUOU5.js} +12 -12
  60. package/dist/{chunk-3GIA3HJ2.js → chunk-DGOZ4UZD.js} +2 -2
  61. package/dist/{chunk-NMDMSHBF.js → chunk-EQEYOJBM.js} +3 -3
  62. package/dist/{chunk-OQDHIAR2.js → chunk-F3W6YMDC.js} +80 -8
  63. package/dist/chunk-F3W6YMDC.js.map +1 -0
  64. package/dist/{chunk-Q2ZN5DWZ.js → chunk-FAHMLERK.js} +2 -2
  65. package/dist/{chunk-PCIBCL26.js → chunk-FET5XCAT.js} +7 -5
  66. package/dist/{chunk-PCIBCL26.js.map → chunk-FET5XCAT.js.map} +1 -1
  67. package/dist/chunk-FFWI7UH7.js +53 -0
  68. package/dist/chunk-FFWI7UH7.js.map +1 -0
  69. package/dist/{chunk-GKOJR53Y.js → chunk-G73UVHCA.js} +5 -5
  70. package/dist/{chunk-H5GVPOB4.js → chunk-GPNTB4Y7.js} +5 -5
  71. package/dist/{chunk-DNMTN46H.js → chunk-GY4WYNNG.js} +2 -2
  72. package/dist/{chunk-HS3LEVT4.js → chunk-HIWCKBEY.js} +7 -7
  73. package/dist/{chunk-UL7TLJ54.js → chunk-HJUWN7JN.js} +6 -4
  74. package/dist/{chunk-UL7TLJ54.js.map → chunk-HJUWN7JN.js.map} +1 -1
  75. package/dist/{chunk-USKHHOIV.js → chunk-HPVZCW43.js} +30 -2
  76. package/dist/chunk-HPVZCW43.js.map +1 -0
  77. package/dist/{chunk-PV7XXAXK.js → chunk-JBDFMYKQ.js} +2 -2
  78. package/dist/{chunk-IZTU6RKH.js → chunk-JMGDHKQF.js} +2 -2
  79. package/dist/{chunk-P22XPNFW.js → chunk-JVIBCBC7.js} +2 -2
  80. package/dist/{chunk-OLUTCM23.js → chunk-K23VKTN4.js} +7 -7
  81. package/dist/{chunk-3SWP2FYA.js → chunk-KKEESNPE.js} +8 -8
  82. package/dist/{chunk-ZB2K3FV7.js → chunk-KTFRCC5C.js} +2 -2
  83. package/dist/{chunk-IAHDSFFA.js → chunk-LH5A3ZPG.js} +10 -10
  84. package/dist/{chunk-QOHGWFAW.js → chunk-LWBCUYZW.js} +2 -2
  85. package/dist/{chunk-5NKHCJU5.js → chunk-M4YPHYEH.js} +8 -8
  86. package/dist/{chunk-JFSUYGNE.js → chunk-MVMXZYXT.js} +2 -2
  87. package/dist/{chunk-FKQCC4C5.js → chunk-NQKIUNW5.js} +2 -2
  88. package/dist/{chunk-Q2OAAREU.js → chunk-OTQZ7ZWU.js} +6 -6
  89. package/dist/{chunk-MOQ6OGKS.js → chunk-P3MG6QCN.js} +4 -4
  90. package/dist/{chunk-75WNTBZL.js → chunk-PPTHY5QO.js} +3 -3
  91. package/dist/{chunk-TQGT5BZE.js → chunk-Q2MPFCQ7.js} +4 -4
  92. package/dist/{chunk-MTTPHBIE.js → chunk-Q2PU57HC.js} +2 -2
  93. package/dist/{chunk-FLRLZ4XZ.js → chunk-Q7HZX3IF.js} +2 -2
  94. package/dist/{chunk-KQIDBIZL.js → chunk-QIZPZMUI.js} +2 -2
  95. package/dist/{chunk-JRWLUWDM.js → chunk-QPQUVOZ2.js} +3 -3
  96. package/dist/{chunk-7YVSWPZB.js → chunk-QTQUH4DS.js} +7 -7
  97. package/dist/{chunk-2MQ2D5OZ.js → chunk-R42NSZI6.js} +5 -5
  98. package/dist/{chunk-FRIVOR5N.js → chunk-R7BX2NB3.js} +5 -172
  99. package/dist/chunk-R7BX2NB3.js.map +1 -0
  100. package/dist/{chunk-HOIWW2LV.js → chunk-RF6D3Y5E.js} +4 -4
  101. package/dist/{chunk-OEJ4R52I.js → chunk-RMS3NEA3.js} +2 -2
  102. package/dist/{chunk-FPN5D4JX.js → chunk-RSGLUIO6.js} +6 -6
  103. package/dist/chunk-RTBJ5IOO.js +87 -0
  104. package/dist/chunk-RTBJ5IOO.js.map +1 -0
  105. package/dist/chunk-RXLYKVTF.js +17 -0
  106. package/dist/chunk-RXLYKVTF.js.map +1 -0
  107. package/dist/{chunk-XKA6UH5V.js → chunk-SFAWCTEM.js} +2 -2
  108. package/dist/{chunk-I5LSKXT6.js → chunk-SGZAIJCV.js} +4 -4
  109. package/dist/{chunk-FYT3DXPN.js → chunk-SKGMN35F.js} +2 -2
  110. package/dist/chunk-SMH6SRX7.js +87 -0
  111. package/dist/chunk-SMH6SRX7.js.map +1 -0
  112. package/dist/{chunk-JILOSL4O.js → chunk-SPYWXKJN.js} +3 -3
  113. package/dist/{chunk-4RUFYGWG.js → chunk-SZJ3DLZA.js} +2 -2
  114. package/dist/chunk-TFMM5DBB.js +20 -0
  115. package/dist/chunk-TFMM5DBB.js.map +1 -0
  116. package/dist/{chunk-O3SNM66K.js → chunk-TFR7DSOV.js} +2 -2
  117. package/dist/{chunk-DIVY2CJZ.js → chunk-TRGTKX5E.js} +3 -3
  118. package/dist/{chunk-JY4U6CYV.js → chunk-TUFM4UZP.js} +2 -2
  119. package/dist/{chunk-LUVGSKXE.js → chunk-UDHWDSRL.js} +6 -6
  120. package/dist/{chunk-DZPMKY3J.js → chunk-UGTBXF5U.js} +3 -3
  121. package/dist/{chunk-K432FT7T.js → chunk-UHHPLO3I.js} +4 -4
  122. package/dist/{chunk-THNJ3IKU.js → chunk-ULUMAZ5S.js} +2 -2
  123. package/dist/{chunk-QJLOWGOU.js → chunk-UP4BECPY.js} +12 -82
  124. package/dist/chunk-UP4BECPY.js.map +1 -0
  125. package/dist/{chunk-YHIAWECL.js → chunk-UXVWEX5L.js} +2 -2
  126. package/dist/{chunk-NO272T7Q.js → chunk-V4PXLYWM.js} +2 -31
  127. package/dist/chunk-V4PXLYWM.js.map +1 -0
  128. package/dist/{chunk-OKCQNMO6.js → chunk-V7ZRRCRJ.js} +6 -6
  129. package/dist/{chunk-GHADG5QZ.js → chunk-VTFVLCUH.js} +2 -2
  130. package/dist/{chunk-U3RMWOXY.js → chunk-VXANTWBJ.js} +3 -3
  131. package/dist/{chunk-KHKKD62N.js → chunk-VYYM3MEN.js} +2 -16
  132. package/dist/chunk-VYYM3MEN.js.map +1 -0
  133. package/dist/{chunk-VTTDPGEK.js → chunk-WQBG4SZS.js} +26 -57
  134. package/dist/chunk-WQBG4SZS.js.map +1 -0
  135. package/dist/{chunk-MV2OEK3K.js → chunk-XKWGNJ26.js} +5 -5
  136. package/dist/{chunk-2E2LT37H.js → chunk-XLMEZIAJ.js} +2 -2
  137. package/dist/{chunk-RHTPGS56.js → chunk-YJRT5UQM.js} +3 -3
  138. package/dist/{chunk-FZWAGG5T.js → chunk-YJX7EOJD.js} +6 -6
  139. package/dist/{chunk-ZQGRZN5L.js → chunk-YYP7WSEB.js} +2 -2
  140. package/dist/{chunk-MENI33ZV.js → chunk-ZHKOPKE3.js} +3 -3
  141. package/dist/{chunk-4U52PMEL.js → chunk-ZN4FD4PE.js} +4 -4
  142. package/dist/{chunk-AW4KTQA4.js → chunk-ZORTV6NH.js} +2 -2
  143. package/dist/{chunk-6L5IMNJX.js → chunk-ZYIYHJ5R.js} +236 -1333
  144. package/dist/chunk-ZYIYHJ5R.js.map +1 -0
  145. package/dist/classified/index.js +5 -6
  146. package/dist/{classified-marker-R37XW5PC.js → classified-marker-BWOFVYAC.js} +3 -5
  147. package/dist/{classified-marker-R37XW5PC.js.map → classified-marker-BWOFVYAC.js.map} +1 -1
  148. package/dist/collection-facade-JSJYIKS4.js +45 -0
  149. package/dist/computed-VPOKQK4N.js +10 -0
  150. package/dist/consent/index.js +9 -10
  151. package/dist/consent/index.js.map +1 -1
  152. package/dist/cover/index.js +9 -10
  153. package/dist/crdt/index.js +0 -1
  154. package/dist/crdt/index.js.map +1 -1
  155. package/dist/custody/index.js +12 -13
  156. package/dist/{dead-filter-GRT6RAML.js → dead-filter-PKQDLOT4.js} +2 -3
  157. package/dist/{dead-filter-GRT6RAML.js.map → dead-filter-PKQDLOT4.js.map} +1 -1
  158. package/dist/debug/index.js +1 -2
  159. package/dist/debug/index.js.map +1 -1
  160. package/dist/{delegation-6QRAYSK6.js → delegation-DS5NRILW.js} +11 -12
  161. package/dist/{delegation-6QRAYSK6.js.map → delegation-DS5NRILW.js.map} +1 -1
  162. package/dist/derivations/index.js +30 -16
  163. package/dist/derive-E642AOUB.js +21 -0
  164. package/dist/directory/index.js +10 -11
  165. package/dist/{dispatch-QCF7KPIP.js → dispatch-IEPAVQUM.js} +5 -7
  166. package/dist/{dispatch-QCF7KPIP.js.map → dispatch-IEPAVQUM.js.map} +1 -1
  167. package/dist/dispatch-Q6MWLOJY.js +302 -0
  168. package/dist/dispatch-Q6MWLOJY.js.map +1 -0
  169. package/dist/{enclave-OXZY3NOP.js → enclave-55PAM4YO.js} +15 -10
  170. package/dist/executor-5NIMLSKZ.js +8 -0
  171. package/dist/executor-7UQ3BZ4F.js +29 -0
  172. package/dist/executor-AQVXV5GZ.js +8 -0
  173. package/dist/export-accessible-ORMKLA3E.js +22 -0
  174. package/dist/extract-partition-NKYO3WNG.js +37 -0
  175. package/dist/{fanout-sidecar-KGQCUJ7P.js → fanout-sidecar-S3QL53YX.js} +9 -10
  176. package/dist/{fanout-sidecar-KGQCUJ7P.js.map → fanout-sidecar-S3QL53YX.js.map} +1 -1
  177. package/dist/{fence-watcher-ZVSMLKQD.js → fence-watcher-U2PW6JU5.js} +1 -3
  178. package/dist/{fence-watcher-ZVSMLKQD.js.map → fence-watcher-U2PW6JU5.js.map} +1 -1
  179. package/dist/find-NOGJ25VX.js +10 -0
  180. package/dist/forget/index.js +9 -10
  181. package/dist/forget/index.js.map +1 -1
  182. package/dist/guards/index.js +4 -5
  183. package/dist/history/index.js +13 -14
  184. package/dist/history/index.js.map +1 -1
  185. package/dist/i18n/index.js +14 -15
  186. package/dist/i18n/index.js.map +1 -1
  187. package/dist/index.d.ts +21 -4
  188. package/dist/index.js +122 -115
  189. package/dist/index.js.map +1 -1
  190. package/dist/indexing/index.js +2 -3
  191. package/dist/indexing/index.js.map +1 -1
  192. package/dist/introspection/index.js +13 -13
  193. package/dist/issue-3DTQ7VJJ.js +18 -0
  194. package/dist/{json-schema-I22JCYCG.js → json-schema-V3M5MUCM.js} +1 -3
  195. package/dist/{json-schema-I22JCYCG.js.map → json-schema-V3M5MUCM.js.map} +1 -1
  196. package/dist/kernel/collection-config.d.ts +34 -0
  197. package/dist/kernel/collection.d.ts +11 -6
  198. package/dist/kernel/enclave/crypto.d.ts +16 -0
  199. package/dist/kernel/enclave/index.d.ts +1 -1
  200. package/dist/kernel/errors.d.ts +28 -0
  201. package/dist/kernel/match-pairs.d.ts +30 -0
  202. package/dist/kernel/memory-store.d.ts +35 -7
  203. package/dist/kernel/types.d.ts +39 -0
  204. package/dist/lazy/index.js +3 -3
  205. package/dist/{ledger-ZAFULLAF.js → ledger-EDXFFZQL.js} +11 -12
  206. package/dist/liberate-V35QUHJP.js +24 -0
  207. package/dist/link-set-2TTM3IZC.js +30 -0
  208. package/dist/{managed-secret-7OH7HKXV.js → managed-secret-NCWEMPUQ.js} +10 -11
  209. package/dist/materialized-views/index.js +21 -21
  210. package/dist/{migrate-cek-QHZ75E6E.js → migrate-cek-5ENH64AA.js} +2 -3
  211. package/dist/{migrate-cek-QHZ75E6E.js.map → migrate-cek-5ENH64AA.js.map} +1 -1
  212. package/dist/money/index.js +4 -5
  213. package/dist/noydb-62MX4QUG.js +90 -0
  214. package/dist/overlay-views/index.js +4 -5
  215. package/dist/periods/index.js +12 -13
  216. package/dist/pod/index.js +64 -60
  217. package/dist/{pod-handle-BLOI4IFY.js → pod-handle-ECVCDIXW.js} +10 -11
  218. package/dist/{pod-handle-BLOI4IFY.js.map → pod-handle-ECVCDIXW.js.map} +1 -1
  219. package/dist/policy/index.js +10 -11
  220. package/dist/port/to/index.d.ts +40 -0
  221. package/dist/portability/index.js +8 -9
  222. package/dist/portability/index.js.map +1 -1
  223. package/dist/{post-register-R33EKJQR.js → post-register-3IPCSG6Q.js} +6 -7
  224. package/dist/{post-register-R33EKJQR.js.map → post-register-3IPCSG6Q.js.map} +1 -1
  225. package/dist/{purge-scope-FGNNLWHD.js → purge-scope-4HEFPDOW.js} +1 -3
  226. package/dist/{purge-scope-FGNNLWHD.js.map → purge-scope-4HEFPDOW.js.map} +1 -1
  227. package/dist/query/index.js +3 -4
  228. package/dist/{read-only-facade-CHCKSEPD.js → read-only-facade-BIGO37U3.js} +1 -2
  229. package/dist/reduce/index.js +4 -5
  230. package/dist/reduce/index.js.map +1 -1
  231. package/dist/register-HHLXEQDN.js +31 -0
  232. package/dist/registry-23WF3TGH.js +8 -0
  233. package/dist/registry-4AYVUNXK.js +8 -0
  234. package/dist/registry-JS3YFQQM.js +18 -0
  235. package/dist/registry-XX2Z7B46.js +32 -0
  236. package/dist/request-withdrawal-CHPATE4G.js +30 -0
  237. package/dist/{reveal-PI6ADRQU.js → reveal-QZDHSGD4.js} +6 -7
  238. package/dist/{reveal-PI6ADRQU.js.map → reveal-QZDHSGD4.js.map} +1 -1
  239. package/dist/revoke-7M2VW4ON.js +23 -0
  240. package/dist/satellites/index.js +1 -2
  241. package/dist/schema-update/index.js +2 -3
  242. package/dist/sealed-record/index.js +12 -13
  243. package/dist/sealed-record/index.js.map +1 -1
  244. package/dist/search/index.js +4 -5
  245. package/dist/{seed-CG7JMJNY.js → seed-X5CC22CQ.js} +12 -13
  246. package/dist/{seed-CG7JMJNY.js.map → seed-X5CC22CQ.js.map} +1 -1
  247. package/dist/sequence/index.js +9 -10
  248. package/dist/session/index.js +9 -10
  249. package/dist/session/index.js.map +1 -1
  250. package/dist/shadow/index.js +2 -3
  251. package/dist/shadow/index.js.map +1 -1
  252. package/dist/share-link/index.js +0 -1
  253. package/dist/share-link/index.js.map +1 -1
  254. package/dist/signer-SJQJVSM6.js +24 -0
  255. package/dist/snapshots/index.js +10 -11
  256. package/dist/snapshots/index.js.map +1 -1
  257. package/dist/{stale-HZR7745Y.js → stale-BZS6H2RY.js} +10 -11
  258. package/dist/storage-RW4QBS23.js +22 -0
  259. package/dist/store/index.js +0 -1
  260. package/dist/{store-coordination-provider-MYIHSZXT.js → store-coordination-provider-4WZAIRHC.js} +10 -11
  261. package/dist/{store-coordination-provider-MYIHSZXT.js.map → store-coordination-provider-4WZAIRHC.js.map} +1 -1
  262. package/dist/sync/index.js +10 -11
  263. package/dist/sync/index.js.map +1 -1
  264. package/dist/team/index.js +15 -16
  265. package/dist/tiers/index.js +11 -12
  266. package/dist/to/index.js +2 -3
  267. package/dist/to/index.js.map +1 -1
  268. package/dist/transactions/index.js +3 -4
  269. package/dist/transactions/index.js.map +1 -1
  270. package/dist/{ulid-VQW6ERQN.js → ulid-4MKXG325.js} +1 -2
  271. package/dist/util/index.js +1 -2
  272. package/dist/util/index.js.map +1 -1
  273. package/dist/{vault-diff-FE5J35NJ.js → vault-diff-C6WUJFNP.js} +1 -2
  274. package/dist/vault-head/index.js +8 -9
  275. package/dist/vault-head/index.js.map +1 -1
  276. package/dist/{verify-R6CCBUA3.js → verify-6MDTIUEV.js} +5 -6
  277. package/dist/{verify-R6CCBUA3.js.map → verify-6MDTIUEV.js.map} +1 -1
  278. package/dist/walk-ZURCH6RZ.js +21 -0
  279. package/dist/with-formula/derivations/dispatch.d.ts +16 -1
  280. package/dist/with-formula/derivations/registry.d.ts +20 -0
  281. package/dist/with-formula/derivations/trigger-match.d.ts +104 -0
  282. package/dist/with-formula/derivations/types.d.ts +61 -0
  283. package/dist/with-party/team/keyring.d.ts +12 -5
  284. package/dist/with-party/team/wrapped-deks.d.ts +4 -4
  285. package/dist/with-pod/pod.d.ts +4 -4
  286. package/dist/with-shape/introspection/behaviors.d.ts +7 -0
  287. package/dist/with-shape/introspection/describe.d.ts +10 -1
  288. package/dist/with-shape/introspection/field-meta.d.ts +86 -0
  289. package/dist/withdraw-accessible-PXR4IXDX.js +27 -0
  290. package/package.json +8 -3
  291. package/dist/api-GJQF7OAJ.js +0 -20
  292. package/dist/backup-QZQEP5RO.js.map +0 -1
  293. package/dist/chunk-65ZYRK7O.js.map +0 -1
  294. package/dist/chunk-6L5IMNJX.js.map +0 -1
  295. package/dist/chunk-FRIVOR5N.js.map +0 -1
  296. package/dist/chunk-J6FXVDKC.js +0 -81
  297. package/dist/chunk-J6FXVDKC.js.map +0 -1
  298. package/dist/chunk-KHKKD62N.js.map +0 -1
  299. package/dist/chunk-NO272T7Q.js.map +0 -1
  300. package/dist/chunk-OHGCRKWA.js.map +0 -1
  301. package/dist/chunk-OQDHIAR2.js.map +0 -1
  302. package/dist/chunk-PZ5AY32C.js +0 -10
  303. package/dist/chunk-QJLOWGOU.js.map +0 -1
  304. package/dist/chunk-RCONOOVF.js.map +0 -1
  305. package/dist/chunk-SW4TOSJL.js.map +0 -1
  306. package/dist/chunk-USKHHOIV.js.map +0 -1
  307. package/dist/chunk-VT4DTCK5.js.map +0 -1
  308. package/dist/chunk-VTTDPGEK.js.map +0 -1
  309. package/dist/chunk-XJP66QEN.js +0 -344
  310. package/dist/chunk-XJP66QEN.js.map +0 -1
  311. package/dist/collection-facade-OPJ3EC4L.js +0 -45
  312. package/dist/computed-HW6IG5UH.js +0 -11
  313. package/dist/derive-AAIXSQIE.js +0 -22
  314. package/dist/dispatch-DR3XITEC.js +0 -194
  315. package/dist/dispatch-DR3XITEC.js.map +0 -1
  316. package/dist/executor-67NA4KPO.js +0 -29
  317. package/dist/executor-7JDP7UBK.js +0 -9
  318. package/dist/executor-CRH5OB4V.js +0 -9
  319. package/dist/export-accessible-K4GJAU7U.js +0 -23
  320. package/dist/extract-partition-YJFL5OKK.js +0 -38
  321. package/dist/find-SVZHMEQI.js +0 -11
  322. package/dist/issue-64N6OG7U.js +0 -19
  323. package/dist/liberate-XL6NSGOL.js +0 -25
  324. package/dist/link-set-LQI2IJX6.js +0 -31
  325. package/dist/noydb-SWUGPFBK.js +0 -86
  326. package/dist/register-AYBQNTM3.js +0 -32
  327. package/dist/registry-CQWWOBO4.js +0 -9
  328. package/dist/registry-JCZ4BNLV.js +0 -18
  329. package/dist/registry-JFFENZ44.js +0 -9
  330. package/dist/registry-ZA4HACYR.js +0 -19
  331. package/dist/request-withdrawal-7IKUIAEB.js +0 -31
  332. package/dist/revoke-PUJKRYJF.js +0 -24
  333. package/dist/signer-5TA7FUC3.js +0 -25
  334. package/dist/storage-J2QHNJCB.js +0 -23
  335. package/dist/walk-3PA4ZOKS.js +0 -21
  336. package/dist/withdraw-accessible-62VI4W7D.js +0 -28
  337. package/dist/withdraw-accessible-62VI4W7D.js.map +0 -1
  338. package/dist/zod-HAJM7LMS.js +0 -14763
  339. package/dist/zod-HAJM7LMS.js.map +0 -1
  340. /package/dist/{api-GJQF7OAJ.js.map → api-WYW4YEQG.js.map} +0 -0
  341. /package/dist/{chunk-5UKIZIO3.js.map → chunk-2577DKXB.js.map} +0 -0
  342. /package/dist/{chunk-SWFIXQF5.js.map → chunk-2ENE6445.js.map} +0 -0
  343. /package/dist/{chunk-HLFLJCDX.js.map → chunk-2N5ML6HT.js.map} +0 -0
  344. /package/dist/{chunk-X6YRP5EC.js.map → chunk-334WSVOW.js.map} +0 -0
  345. /package/dist/{chunk-5EC7X5QA.js.map → chunk-3BVH633C.js.map} +0 -0
  346. /package/dist/{chunk-7XT7KZTN.js.map → chunk-3PGS7TYR.js.map} +0 -0
  347. /package/dist/{chunk-AHIIBDNH.js.map → chunk-3VF3WQ4F.js.map} +0 -0
  348. /package/dist/{chunk-GM4IM7TC.js.map → chunk-453F654R.js.map} +0 -0
  349. /package/dist/{chunk-5W5CEHLA.js.map → chunk-4GFX6MPA.js.map} +0 -0
  350. /package/dist/{chunk-JUBWUJY5.js.map → chunk-4GVM6QZA.js.map} +0 -0
  351. /package/dist/{chunk-2DGBRE4K.js.map → chunk-4OZNXTO4.js.map} +0 -0
  352. /package/dist/{chunk-DOPZWTMJ.js.map → chunk-4ZS56W5S.js.map} +0 -0
  353. /package/dist/{chunk-P2RJAVMR.js.map → chunk-5VCRVKOZ.js.map} +0 -0
  354. /package/dist/{chunk-FN26SFGH.js.map → chunk-6LKOJBV6.js.map} +0 -0
  355. /package/dist/{chunk-P7KJME6C.js.map → chunk-6YXCPL4T.js.map} +0 -0
  356. /package/dist/{chunk-IS5LFHV2.js.map → chunk-7APJ6CO6.js.map} +0 -0
  357. /package/dist/{chunk-LAI7ENPW.js.map → chunk-7MG2LHO5.js.map} +0 -0
  358. /package/dist/{chunk-MU74BYOD.js.map → chunk-A2HKRLPV.js.map} +0 -0
  359. /package/dist/{chunk-357ECVLE.js.map → chunk-A66I4JOA.js.map} +0 -0
  360. /package/dist/{chunk-TAJZ7GXO.js.map → chunk-AYX7AINO.js.map} +0 -0
  361. /package/dist/{chunk-RPX2H2OL.js.map → chunk-BLN54W3L.js.map} +0 -0
  362. /package/dist/{chunk-XGXVPCYW.js.map → chunk-BRLKZUBT.js.map} +0 -0
  363. /package/dist/{chunk-CWFH36SI.js.map → chunk-BTDR2PXY.js.map} +0 -0
  364. /package/dist/{chunk-UTFY6W6X.js.map → chunk-BZ3534YM.js.map} +0 -0
  365. /package/dist/{chunk-KZTPL7NL.js.map → chunk-CHQCZZWC.js.map} +0 -0
  366. /package/dist/{chunk-MQPHKJ74.js.map → chunk-DFMWUOU5.js.map} +0 -0
  367. /package/dist/{chunk-3GIA3HJ2.js.map → chunk-DGOZ4UZD.js.map} +0 -0
  368. /package/dist/{chunk-NMDMSHBF.js.map → chunk-EQEYOJBM.js.map} +0 -0
  369. /package/dist/{chunk-Q2ZN5DWZ.js.map → chunk-FAHMLERK.js.map} +0 -0
  370. /package/dist/{chunk-GKOJR53Y.js.map → chunk-G73UVHCA.js.map} +0 -0
  371. /package/dist/{chunk-H5GVPOB4.js.map → chunk-GPNTB4Y7.js.map} +0 -0
  372. /package/dist/{chunk-DNMTN46H.js.map → chunk-GY4WYNNG.js.map} +0 -0
  373. /package/dist/{chunk-HS3LEVT4.js.map → chunk-HIWCKBEY.js.map} +0 -0
  374. /package/dist/{chunk-PV7XXAXK.js.map → chunk-JBDFMYKQ.js.map} +0 -0
  375. /package/dist/{chunk-IZTU6RKH.js.map → chunk-JMGDHKQF.js.map} +0 -0
  376. /package/dist/{chunk-P22XPNFW.js.map → chunk-JVIBCBC7.js.map} +0 -0
  377. /package/dist/{chunk-OLUTCM23.js.map → chunk-K23VKTN4.js.map} +0 -0
  378. /package/dist/{chunk-3SWP2FYA.js.map → chunk-KKEESNPE.js.map} +0 -0
  379. /package/dist/{chunk-ZB2K3FV7.js.map → chunk-KTFRCC5C.js.map} +0 -0
  380. /package/dist/{chunk-IAHDSFFA.js.map → chunk-LH5A3ZPG.js.map} +0 -0
  381. /package/dist/{chunk-QOHGWFAW.js.map → chunk-LWBCUYZW.js.map} +0 -0
  382. /package/dist/{chunk-5NKHCJU5.js.map → chunk-M4YPHYEH.js.map} +0 -0
  383. /package/dist/{chunk-JFSUYGNE.js.map → chunk-MVMXZYXT.js.map} +0 -0
  384. /package/dist/{chunk-FKQCC4C5.js.map → chunk-NQKIUNW5.js.map} +0 -0
  385. /package/dist/{chunk-Q2OAAREU.js.map → chunk-OTQZ7ZWU.js.map} +0 -0
  386. /package/dist/{chunk-MOQ6OGKS.js.map → chunk-P3MG6QCN.js.map} +0 -0
  387. /package/dist/{chunk-75WNTBZL.js.map → chunk-PPTHY5QO.js.map} +0 -0
  388. /package/dist/{chunk-TQGT5BZE.js.map → chunk-Q2MPFCQ7.js.map} +0 -0
  389. /package/dist/{chunk-MTTPHBIE.js.map → chunk-Q2PU57HC.js.map} +0 -0
  390. /package/dist/{chunk-FLRLZ4XZ.js.map → chunk-Q7HZX3IF.js.map} +0 -0
  391. /package/dist/{chunk-KQIDBIZL.js.map → chunk-QIZPZMUI.js.map} +0 -0
  392. /package/dist/{chunk-JRWLUWDM.js.map → chunk-QPQUVOZ2.js.map} +0 -0
  393. /package/dist/{chunk-7YVSWPZB.js.map → chunk-QTQUH4DS.js.map} +0 -0
  394. /package/dist/{chunk-2MQ2D5OZ.js.map → chunk-R42NSZI6.js.map} +0 -0
  395. /package/dist/{chunk-HOIWW2LV.js.map → chunk-RF6D3Y5E.js.map} +0 -0
  396. /package/dist/{chunk-OEJ4R52I.js.map → chunk-RMS3NEA3.js.map} +0 -0
  397. /package/dist/{chunk-FPN5D4JX.js.map → chunk-RSGLUIO6.js.map} +0 -0
  398. /package/dist/{chunk-XKA6UH5V.js.map → chunk-SFAWCTEM.js.map} +0 -0
  399. /package/dist/{chunk-I5LSKXT6.js.map → chunk-SGZAIJCV.js.map} +0 -0
  400. /package/dist/{chunk-FYT3DXPN.js.map → chunk-SKGMN35F.js.map} +0 -0
  401. /package/dist/{chunk-JILOSL4O.js.map → chunk-SPYWXKJN.js.map} +0 -0
  402. /package/dist/{chunk-4RUFYGWG.js.map → chunk-SZJ3DLZA.js.map} +0 -0
  403. /package/dist/{chunk-O3SNM66K.js.map → chunk-TFR7DSOV.js.map} +0 -0
  404. /package/dist/{chunk-DIVY2CJZ.js.map → chunk-TRGTKX5E.js.map} +0 -0
  405. /package/dist/{chunk-JY4U6CYV.js.map → chunk-TUFM4UZP.js.map} +0 -0
  406. /package/dist/{chunk-LUVGSKXE.js.map → chunk-UDHWDSRL.js.map} +0 -0
  407. /package/dist/{chunk-DZPMKY3J.js.map → chunk-UGTBXF5U.js.map} +0 -0
  408. /package/dist/{chunk-K432FT7T.js.map → chunk-UHHPLO3I.js.map} +0 -0
  409. /package/dist/{chunk-THNJ3IKU.js.map → chunk-ULUMAZ5S.js.map} +0 -0
  410. /package/dist/{chunk-YHIAWECL.js.map → chunk-UXVWEX5L.js.map} +0 -0
  411. /package/dist/{chunk-OKCQNMO6.js.map → chunk-V7ZRRCRJ.js.map} +0 -0
  412. /package/dist/{chunk-GHADG5QZ.js.map → chunk-VTFVLCUH.js.map} +0 -0
  413. /package/dist/{chunk-U3RMWOXY.js.map → chunk-VXANTWBJ.js.map} +0 -0
  414. /package/dist/{chunk-MV2OEK3K.js.map → chunk-XKWGNJ26.js.map} +0 -0
  415. /package/dist/{chunk-2E2LT37H.js.map → chunk-XLMEZIAJ.js.map} +0 -0
  416. /package/dist/{chunk-RHTPGS56.js.map → chunk-YJRT5UQM.js.map} +0 -0
  417. /package/dist/{chunk-FZWAGG5T.js.map → chunk-YJX7EOJD.js.map} +0 -0
  418. /package/dist/{chunk-ZQGRZN5L.js.map → chunk-YYP7WSEB.js.map} +0 -0
  419. /package/dist/{chunk-MENI33ZV.js.map → chunk-ZHKOPKE3.js.map} +0 -0
  420. /package/dist/{chunk-4U52PMEL.js.map → chunk-ZN4FD4PE.js.map} +0 -0
  421. /package/dist/{chunk-AW4KTQA4.js.map → chunk-ZORTV6NH.js.map} +0 -0
  422. /package/dist/{chunk-PZ5AY32C.js.map → collection-facade-JSJYIKS4.js.map} +0 -0
  423. /package/dist/{collection-facade-OPJ3EC4L.js.map → computed-VPOKQK4N.js.map} +0 -0
  424. /package/dist/{computed-HW6IG5UH.js.map → derive-E642AOUB.js.map} +0 -0
  425. /package/dist/{derive-AAIXSQIE.js.map → enclave-55PAM4YO.js.map} +0 -0
  426. /package/dist/{enclave-OXZY3NOP.js.map → executor-5NIMLSKZ.js.map} +0 -0
  427. /package/dist/{executor-67NA4KPO.js.map → executor-7UQ3BZ4F.js.map} +0 -0
  428. /package/dist/{executor-7JDP7UBK.js.map → executor-AQVXV5GZ.js.map} +0 -0
  429. /package/dist/{executor-CRH5OB4V.js.map → export-accessible-ORMKLA3E.js.map} +0 -0
  430. /package/dist/{export-accessible-K4GJAU7U.js.map → extract-partition-NKYO3WNG.js.map} +0 -0
  431. /package/dist/{extract-partition-YJFL5OKK.js.map → find-NOGJ25VX.js.map} +0 -0
  432. /package/dist/{find-SVZHMEQI.js.map → issue-3DTQ7VJJ.js.map} +0 -0
  433. /package/dist/{issue-64N6OG7U.js.map → ledger-EDXFFZQL.js.map} +0 -0
  434. /package/dist/{ledger-ZAFULLAF.js.map → liberate-V35QUHJP.js.map} +0 -0
  435. /package/dist/{liberate-XL6NSGOL.js.map → link-set-2TTM3IZC.js.map} +0 -0
  436. /package/dist/{link-set-LQI2IJX6.js.map → managed-secret-NCWEMPUQ.js.map} +0 -0
  437. /package/dist/{managed-secret-7OH7HKXV.js.map → noydb-62MX4QUG.js.map} +0 -0
  438. /package/dist/{noydb-SWUGPFBK.js.map → read-only-facade-BIGO37U3.js.map} +0 -0
  439. /package/dist/{read-only-facade-CHCKSEPD.js.map → register-HHLXEQDN.js.map} +0 -0
  440. /package/dist/{register-AYBQNTM3.js.map → registry-23WF3TGH.js.map} +0 -0
  441. /package/dist/{registry-CQWWOBO4.js.map → registry-4AYVUNXK.js.map} +0 -0
  442. /package/dist/{registry-JCZ4BNLV.js.map → registry-JS3YFQQM.js.map} +0 -0
  443. /package/dist/{registry-JFFENZ44.js.map → registry-XX2Z7B46.js.map} +0 -0
  444. /package/dist/{registry-ZA4HACYR.js.map → request-withdrawal-CHPATE4G.js.map} +0 -0
  445. /package/dist/{request-withdrawal-7IKUIAEB.js.map → revoke-7M2VW4ON.js.map} +0 -0
  446. /package/dist/{revoke-PUJKRYJF.js.map → signer-SJQJVSM6.js.map} +0 -0
  447. /package/dist/{signer-5TA7FUC3.js.map → stale-BZS6H2RY.js.map} +0 -0
  448. /package/dist/{stale-HZR7745Y.js.map → storage-RW4QBS23.js.map} +0 -0
  449. /package/dist/{storage-J2QHNJCB.js.map → ulid-4MKXG325.js.map} +0 -0
  450. /package/dist/{ulid-VQW6ERQN.js.map → vault-diff-C6WUJFNP.js.map} +0 -0
  451. /package/dist/{vault-diff-FE5J35NJ.js.map → walk-ZURCH6RZ.js.map} +0 -0
  452. /package/dist/{walk-3PA4ZOKS.js.map → withdraw-accessible-PXR4IXDX.js.map} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1332 @@
1
1
  # Changelog — hub
2
2
 
3
+ ## 0.7.1-pre.0
4
+
5
+ ### Patch Changes
6
+
7
+ - **`@noy-db/shamir` (new):** Shamir Secret Sharing over GF(2^8) and the share codecs, extracted from `@noy-db/on-shamir` as a zero-dependency primitive with no hub contract. `@noy-db/on-shamir` now depends on this package and re-exports its surface; import from here when composing threshold sharing into something that is not a noy-db unlock method. Error messages are prefixed `shamir:`.
8
+
9
+ **`@noy-db/hub`:** Hub's recovery tests no longer depend on `@noy-db/on-shamir`; they exercise the real k-of-n math through `@noy-db/shamir`. `packages/on-shamir` has left this repository for `vLannaAi/noy-db-on` — `@noy-db/on-shamir@0.7.0` is the last version published from here; later versions come from noy-db-on on its own line. No runtime change: `NoydbShamir` on `@noy-db/hub/on` is unchanged and is now the only declaration of that interface in the family. (Second half of #211.)
10
+
11
+ ## 0.7.0
12
+
13
+ ### Minor Changes
14
+
15
+ - `withDerivation`'s `triggerBy` accepts a multi-field `match` form (#1249):
16
+ `{ collection, match: [{ from, to }] }` fans a write out to every source
17
+ record where ALL pairs satisfy `String(source[to]) === String(written[from])`.
18
+ `from: 'id'` reads the written record's id, making the existing `on` form the
19
+ single-pair special case (it is unchanged and stays supported). This makes
20
+ shared-key ("reverse") relationships and composite keys like
21
+ `(clientId, cycle)` expressible without denormalising a synthetic key.
22
+
23
+ Also, for BOTH forms:
24
+
25
+ - a LOCAL UPDATE that changes any matched field fans out on old-match ∪
26
+ new-match, so records addressed by the previous value no longer go
27
+ silently stale; a sync-applied wave write or a tiers restore, which don't
28
+ thread the prior record, fan out on the new tuple only;
29
+ - a parent DELETE now fans out using the tombstoned record's values —
30
+ previously deletes fired no triggers at all, leaving matched sources
31
+ stale. The fan-out runs after the delete commits, so cap/strict errors
32
+ can surface from `delete()` (same as the existing rollup-on-delete
33
+ precedent);
34
+ - match fields are validated against the collections' enumerable field sets at
35
+ `vault.collection()` (the #1253 pattern): a provable typo throws instead of
36
+ silently matching nothing forever; TS-generic collections stay unguarded by
37
+ design.
38
+
39
+ `maxFanout` caps the unioned matched set per written event.
40
+
41
+ - Export `NOYDB_ENVELOPE_GENERATION` — a monotonic generation of the envelope _sealing_ format, distinct from `NOYDB_FORMAT_VERSION` (#1207).
42
+
43
+ `NOYDB_FORMAT_VERSION` records what is written; the generation records what a reader must **compute** to open an envelope. The two move independently: 0.6.0-pre.18 bound record identity into the AEAD (#1041) without changing a stored byte, so nothing published could express that envelopes sealed before it are unopenable after it. The generation closes that gap: 1 = no AAD (hub ≤ 0.6.0-pre.17; absence of the export also means 1), 2 = identity + `_v` bound via `noydb-aad/2` (hub ≥ 0.6.0-pre.18).
44
+
45
+ Exported from the root barrel and re-exported from the `/cargo` and `/to` seams (additive), so an orchestrator can stamp it into a manifest and a store host can report "sealed under generation N, this build reads generation M" instead of a bare `TamperedError` from code that is correct. **Diagnostic only**: a reader must never branch on a generation read from an untrusted source (ADR 0003) — classification only, refusal unchanged, the same contract as `TamperedError.reason`. A test pins the generation to the AAD bytes actually emitted, so a sealing-format change cannot ship without a generation decision in the same diff.
46
+
47
+ - **The port vocabulary, and the seams that make it navigable.**
48
+
49
+ A developer should be able to learn the available ports, then pick a package —
50
+ internal or family — that binds one. This line makes the subpath and the type
51
+ name say which port they belong to.
52
+
53
+ **BREAKING — the `Provider` suffix is retired.** A type a satellite implements
54
+ is now `Noydb<Stem>`, matching `NoydbStore`, which was already the pattern.
55
+ `Provider` marked some port instances and not others, so it distinguished
56
+ nothing. Every removed name is in the shipped codemod map
57
+ (`@noy-db/hub/codemods/0.7.0-pre.json`), with `safeGlobalReplace` per row —
58
+ bare-noun rows are flagged unsafe because they collide with ordinary prose.
59
+
60
+ **New published seams: `/at`, `/by`, `/on`, `/as`.** Each ships with a
61
+ conformance kit — `test-sealer-conformance`, `test-mesh-conformance`,
62
+ `test-ceremony-conformance`, `test-format-conformance` — and every one of them
63
+ found a real defect in a binding it was written against.
64
+
65
+ **`/as` is now a port, not a family convention.** Hub owns `export` and
66
+ `import`; a format supplies `encode`/`decode` and declares its own `id`. That
67
+ consolidates a six-copy `ImportPolicy` and lets a format ship outside this repo.
68
+
69
+ **`ExportFormat` is an open union.** A third-party format id can be granted in
70
+ an `exportCapability`, not merely checked — previously the only way to authorise
71
+ one was the `'*'` wildcard, which grants every format at once.
72
+
73
+ **`FenceState` names one type again.** `/by` and `/cargo` carried a duplicate
74
+ object under the same name as the root barrel's string union; they now re-export
75
+ `FenceDoc`.
76
+
77
+ Two decisions are recorded in `docs/adr/`: **0004** (the `as-*` inversion) and
78
+ **0005** (there is no `/ui` port — a UI is a driving adapter, and egress rather
79
+ than rendering is what `assertCanExport` gates).
80
+
81
+ - New published type on `@noy-db/hub/to`: `NoydbRelayStore` — the store contract
82
+ minus the two members a relay profile omits by construction (#1237).
83
+
84
+ `saveAll` is whole-vault replace, a rollback superweapon pointed at a relay's own
85
+ hosts. `listVaults` is an existence leak. A relay handler typed against
86
+ `NoydbRelayStore` **cannot compile a call to either**, so the exclusions are
87
+ enforced by the compiler rather than by handing a handler an object that carries
88
+ `saveAll` and trusting a runtime `Set` not to call it.
89
+
90
+ Purely additive and purely type-level. The `NoydbStore` runtime contract is
91
+ unchanged — still the same 6 methods — and a full store satisfies the relay type
92
+ structurally, so relaying an ordinary store needs no changes to it. That
93
+ boundary was a precondition rather than a convenience: the format-conformance
94
+ kit's store-observation design (#1211) names a change to the `NoydbStore`
95
+ contract as the single thing that would invalidate it, so a narrowing had to
96
+ stay in the type layer or stop.
97
+
98
+ It ships on `/to` rather than inside a relay package because a second consumer
99
+ already needs to name the shape without depending on a relay server it does not
100
+ run.
101
+
102
+ - Add an authenticated, comparable roster epoch (#1097).
103
+
104
+ A roster tag authenticates the roster's **contents** and makes no claim about being **current**. That gap is reachable without forging anything: there is no role-change API, so narrowing a user's standing means calling `grant` again with a lower role, and that write overwrites in place. The previous, broader file was legitimately minted by the vault — so a store that kept a copy can re-serve it. The KEK unwraps, the canary checks out, the tag verifies, and the replayed file **restores the higher role**, usably.
105
+
106
+ The rotation half of #1097 already shipped and converts live access back into stale access. It cannot touch the role, because role gates capabilities rather than keys.
107
+
108
+ ## What is new
109
+
110
+ - `KeyringFile.roster_epoch?: number`, bumped on every roster write and bound into `rosterCanonical`, so a store can neither edit nor strip it.
111
+ - `assertRosterEpochCurrent(found, expected, userId)` — refuses a keyring older than a floor the caller obtained **out of band**.
112
+ - Two `KeyringTamperedReason` codes: `roster-epoch-rewound` and `roster-epoch-absent`.
113
+
114
+ ## Why an out-of-band anchor
115
+
116
+ An epoch stored beside the roster is not an anchor — a store rewinding the roster rewinds the epoch with it. It becomes one only when compared against a value that reached the reader by a channel the store does not carry. **Hub owns the epoch and the comparison; who carries the expectation is deliberately the consumer's problem**, because hub cannot know which channel a deployment trusts.
117
+
118
+ Considered and rejected, recorded so they are not re-derived: **time** (defeats long rewinds, useless against fresh ones — and a narrowing replay is entirely the fresh case) and **the vault head** (circular: head buckets are read with a keyring-issued DEK, and a rewound roster renders as the benign `no-expectations` verdict).
119
+
120
+ ## Not a format break
121
+
122
+ Binding the epoch **conditionally** is what makes this additive. `stable()` drops `undefined`, so a keyring written before the epoch existed canonicalises byte-identically and its existing tag still verifies. Binding it as `?? null` — the shape every other optional field uses — would have failed every pre-existing tag and rendered every existing vault unopenable.
123
+
124
+ ## Opt-in, and absence is never zero
125
+
126
+ With no expected epoch this changes nothing for a caller. When one is supplied, a file carrying **no** epoch is refused as `roster-epoch-absent` rather than treated as epoch 0 — treating absence as zero would accept every pre-epoch file, which is exactly the replay the mechanism exists to refuse. `found > expected` is accepted: the anchor is a floor, not an equality, since a roster may legitimately have moved on since an invite was minted.
127
+
128
+ All thirteen roster-mint sites stamp the epoch before the tag is minted, and `writeKeyringFile` now refuses to write a keyring without one — an output-domain guard, so a future mint site cannot ship the field empty.
129
+
130
+ - `triggerBy` match pairs accept ONE declared hop through an intermediate
131
+ collection (#1277).
132
+
133
+ ```ts
134
+ match: [
135
+ {
136
+ from: "clientId",
137
+ to: "entityId",
138
+ via: { collection: "clients", take: "id", on: "entityId" },
139
+ },
140
+ { from: "cycle", to: "cycle" },
141
+ ];
142
+ ```
143
+
144
+ A source matches when the intermediate — the record in `via.collection` whose
145
+ `via.take` equals `written[from]` — carries a `via.on` equal to `source[to]`.
146
+ This makes a relationship expressible where the two collections share no field
147
+ at all: bills carry `entityId`, disbursements carry `clientId`, and the client
148
+ record is what relates them.
149
+
150
+ The alternative was a denormalised key on one side. That is a second copy of
151
+ something the vault can already resolve, and a partial backfill goes quiet on
152
+ exactly the oldest, least-audited rows — the silent staleness `match` exists to
153
+ remove, handed back one layer down.
154
+
155
+ **A write to the INTERMEDIATE collection fires the trigger too**, fanning out on
156
+ its old ∪ new value. This is the whole correctness argument: re-pointing an
157
+ intermediate writes to neither the trigger nor the source collection, so nothing
158
+ else in the system can notice, and every source it used to address would be
159
+ stranded silently. The cheaper design — resolving the hop only on trigger writes
160
+ — passes every test anyone would think to write and fails only on that edit.
161
+
162
+ Hop resolution costs ONE lookup per written record (`take: 'id'` is a direct
163
+ get), never one per candidate row. A dangling hop matches nothing rather than
164
+ throwing. `maxFanout` caps the hop fan-out as it does the direct form, and the
165
+ intermediate's edge enters the cycle-detection graph.
166
+
167
+ ### Patch Changes
168
+
169
+ - **The `at-*` options types now follow their factories.**
170
+
171
+ `0.7.0-pre.0` renamed every `at-*` factory to `at<Pkg>()` and left four of the
172
+ five options types carrying the retired `SealingProvider` vocabulary — so it
173
+ shipped `atAwsKms()` taking an `AwsKmsSealingProviderOptions`. `at-env` had
174
+ already renamed both halves, which is what makes this a gap rather than a
175
+ decision.
176
+
177
+ | before | after |
178
+ | ------------------------------------- | ------------------------ |
179
+ | `AwsKmsSealingProviderOptions` | `AtAwsKmsOptions` |
180
+ | `GcpKmsSealingProviderOptions` | `AtGcpKmsOptions` |
181
+ | `AzureKeyVaultSealingProviderOptions` | `AtAzureKeyvaultOptions` |
182
+ | `MacosKeychainSealingProviderOptions` | `AtMacosKeychainOptions` |
183
+
184
+ Four rows added to `@noy-db/hub/codemods/0.7.0-pre.json`. `AwsKmsRecipientSealerOptions`
185
+ is deliberately unchanged — it belongs to a different factory that was not renamed.
186
+
187
+ Also corrects a doc comment in `vault.ts` that named two capability gates,
188
+ `canExportPlaintext` and `canExportBundle`, **neither of which has ever
189
+ existed**. The gate is `assertCanExport('plaintext', fmt)` /
190
+ `assertCanExport('bundle')`. That comment ships as JSDoc in the `.d.ts` and had
191
+ propagated into the docs site.
192
+
193
+ - The bundle-size gate now needs a growth to exceed BOTH its percentage tolerance
194
+ and an absolute byte allowance before failing (#1268). No runtime change — this
195
+ is the CI gate only.
196
+
197
+ A percentage on a small baseline measures the wrong thing. The `floor` scenario
198
+ is ~500 gzipped bytes, so 5% is ~25 bytes — narrower than a single
199
+ registration-time guard. The gate had fired twice on necessary validation
200
+ (#1249, #1266) and never once on the thing it exists to catch.
201
+
202
+ The two are separated by orders of magnitude: a subsystem leaking into a bundle
203
+ is kilobytes (measured elsewhere in the family: forcing an SDK inline moved a
204
+ package from 14,069 to 1,081,539 bytes), while a guard is tens of bytes. A
205
+ 192-byte allowance sits far above the latter and far below the former.
206
+
207
+ Verified in both directions rather than assumed: a simulated leak (+406 bytes)
208
+ still FAILS, and a 26-byte guard at +5.2% now PASSES. The numbers still ratchet
209
+ and nothing was re-baselined.
210
+
211
+ - Correct a factual claim in the `0.7.0-pre.5` changelog entry: the cached read path returned `undefined`, not `null`.
212
+
213
+ That entry described the #1220 failure as _"the cached path returned `null`, indistinguishable from no such record."_ The value is wrong. Measured on published `0.7.0-pre.4` with `to-memory@0.7.0-pre.4`, correct arity throughout:
214
+
215
+ | read | value |
216
+ | --------------------------------- | ------------------------------------------- |
217
+ | genuinely absent id | `null` |
218
+ | empty-sealed record, cached path | **`undefined`** |
219
+ | empty-sealed record, hydrate path | `SyntaxError: Unexpected end of JSON input` |
220
+
221
+ **The argument the entry made is unaffected — it gets stronger.** `undefined` is off `get()`'s declared `T | null` contract, so a caller writing `if (!rec)` or `?? fallback` folds it into _"no such record"_ exactly as it would fold `null`, **and** a caller writing `rec === null` fails to catch it as well. The collapse of distinguishable states is wider than the original sentence claimed, not narrower.
222
+
223
+ `0.7.0-pre.5`'s entry is left standing as the record of what that release said — its tarball is immutable, so amending it would only make later tarballs disagree with the shipped one. This is the correction published alongside, the same move `0.7.0-pre.4` made for `0.7.0-pre.3`.
224
+
225
+ Scope: the incorrect sentence appeared only in `CHANGELOG.md` on the published surface — it was not in any shipped `.d.ts`. No code, type, or behaviour change. The guards added in `0.7.0-pre.5` are unaffected, and note they prevent such a record being **created**, not **read**: a record sealed by an earlier build still throws on the hydrate path today.
226
+
227
+ - **The conformance kit covers both entry-point shapes and both gates (#1209).**
228
+
229
+ `0.7.0-pre.0`'s `/as` inversion silently blinded `@noy-db/test-format-conformance`:
230
+ it denied by proxying the vault, which the inverted method-on-vault shape
231
+ (`vault.export(asCsv())`) bypasses — `this` inside `Vault.export` is the real,
232
+ unproxied object. The four inverted formats' fixtures had been deleted rather
233
+ than migrated, so coverage dropped from nine formats to five with nothing
234
+ turning red.
235
+
236
+ The kit now **patches the instance** instead: own-property assignment shadows
237
+ the prototype method at call time, intercepting the argument shape, the
238
+ inverted shape, and hub's internal delegation. Denials are matched on the
239
+ kit's own error class rather than "it threw", every entry point gets an
240
+ ungated-success guard, and the **import gate (`assertCanImport`) is covered
241
+ for the first time** — a format shipping a `decode` with no declared import
242
+ entries gets a loud `SKIPPED` line.
243
+
244
+ All four fixtures are restored, and a new architecture rule
245
+ (`as-conformance-fixture`) makes a silent fixture deletion impossible.
246
+
247
+ - Correct the scoping of `NOYDB_ENVELOPE_GENERATION`'s documentation, and record that adopting it is not free (#1207 follow-up).
248
+
249
+ `0.7.0-pre.3` documented generation 1 as "no AAD (absence of the export also means 1)". That parenthetical is wrong as written, in the published `.d.ts` and in that release's changelog entry, and it is wrong in the direction that produces a false stamp rather than a missing one. "Absence of the **export**" is a statement about a hub _build_, and hub `0.6.0-pre.18` … `0.7.0-pre.2` seal at generation 2 while exporting no constant — verified against the published `0.7.0-pre.2` tarball. A writer using absence as a fallback would stamp a gen-2 artefact as gen 1.
250
+
251
+ The corrected form, now stated outright rather than left to inference: absence of a **stamp on an artefact** classifies that artefact as _unknown — possibly older than the constant_, never as generation 1; and a build's own absence of the export says nothing about the generation it seals at. A writer that cannot determine its generation omits the stamp.
252
+
253
+ Also documented: **adoption is not free.** "Diagnostic only" describes how the value may be used, not what it costs to import. It is a _value_ export, so a named import moves a consumer's real hub floor to `0.7.0-pre.3` regardless of the range its `peerDependencies` declares — measured as `TS2305` at a declared `^0.7.0-pre.0` floor while build, typecheck, lint and the full suite were green at the exact dev pin. Both correct postures are named: a defensive read at an unchanged floor, or a floor narrowing where the consumer publishes nothing and so gates no downstream range.
254
+
255
+ Documentation only — no value, signature, or export changed. The `0.7.0-pre.3` changelog entry is left standing as the record of what that release said; this entry is the correction published alongside it.
256
+
257
+ - **Fix: a schema-fence transition no longer erases `schemaHash`.**
258
+
259
+ `StoreMesh.setFence` wrote the caller's document whole, and callers legitimately
260
+ construct a partial one — so every drain, migrate, complete and abort dropped
261
+ the field #946 added. "Which schema is generation N" was answerable from
262
+ `schemaFenceState()` only until the first cutover. Nothing reported it: the
263
+ fence still loaded, still validated, and still gated writes.
264
+
265
+ - `collection.describe()` now validates `fieldMeta` keys on the **sync** path when
266
+ the configured validator exposes its field names (#1253).
267
+
268
+ Previously the guard ran only on the async `describe(opts)` path, so a typo'd
269
+ `fieldMeta` key on the sync path produced a **phantom field carrying its declared
270
+ `sensitivity`** while the real field went undescribed — an inventory wrong in both
271
+ directions, silently, on the surface `sensitivity` exists to serve.
272
+
273
+ The sync path was thought unable to know the schema's fields because deriving them
274
+ is async. That is true of field **types** and false of field **keys**: a Zod object
275
+ exposes `.shape` directly, on v3 and v4, with no JSON-Schema derivation and no
276
+ `zod-to-json-schema` peer. The latter matters — on Zod 3 that peer is required for
277
+ the async path, so the sync path is the only one some consumers can reach.
278
+
279
+ Deliberately still silent in two cases, both of which would otherwise reject correct
280
+ code: a validator whose fields hub cannot read, and **no validator at all** — a
281
+ collection typed by a TypeScript generic alone has fields that are real and present
282
+ in the data but appear in no runtime config, and `fieldMeta` legitimately names them.
283
+
284
+ - Two dependency-declaration defects, both invisible to every gate because both
285
+ were satisfied by something that never promised anything.
286
+
287
+ **`happy-dom` was used by 22 packages and declared by 4.** The `as-*`, `on-*`
288
+ and `by-*` families named it in their vitest `environment:` and never declared
289
+ it; it resolved only because `in-react`, `in-pinia`, `in-nuxt` and
290
+ `to-browser-idb` happened to declare it — at **three different majors**
291
+ (`^15.11.7`, `^17.4.4`, `^18.0.0`). So those suites ran against whichever
292
+ version won hoisting, pinned by nothing, and a devDependency bump in an
293
+ unrelated package could have silently changed the DOM implementation under
294
+ them. Every user now declares it, all at one range.
295
+
296
+ ⚠️ It is named by no `import` anywhere — only by a vitest config string, or (in
297
+ hub's case) by an `@vitest-environment` DOCBLOCK PRAGMA, which is invisible to
298
+ an import scan and a config grep alike. Found by extracting a family and
299
+ watching it fail to stand alone.
300
+
301
+ ⛔ **Declaring it does NOT make the mistake self-detecting, and a sweep alone
302
+ would have implied otherwise.** pnpm's virtual store still satisfies an
303
+ undeclared package from a sibling that declares it — deleting a declaration and
304
+ reinstalling leaves the suite green, including the test that needs the
305
+ environment. So correct manifests today prevent nothing tomorrow. A static
306
+ check (`pnpm check:test-env-deps`, wired into CI) is the part that enforces it;
307
+ the manifests are merely honest. (`require.resolve` reports the opposite —
308
+ Node's algorithm is not pnpm's store, so the probe has to read manifests rather
309
+ than resolve.)
310
+
311
+ **Sibling `peerDependencies` published as EXACT versions**, completing the fix
312
+ started in #1228. That issue converted the `hub` peer to `workspace:^` (which
313
+ publishes as a caret) and left sibling peers at `workspace:*` (which publishes
314
+ as an exact pin), so two satellites from different cuts were mutually
315
+ uninstallable by name. Install-proven with a discriminating control:
316
+
317
+ ```
318
+ as-xlsx@pre.17 + as-zip@pre.16 -> exit 1 the defect
319
+ as-xlsx@pre.17 + as-zip@pre.17 -> exit 0 control: it is the SKEW, not the pair
320
+ as-csv@pre.17 + in-vue@pre.16 -> exit 0 caret peers DO cross cuts
321
+ ```
322
+
323
+ Nine sibling edges across seven packages move `workspace:*` → `workspace:^`.
324
+ That changes the FORM without changing the KIND: they stay peers, so a consumer
325
+ still controls instance identity, and no per-pair judgement about whether
326
+ deduplication matters was required.
327
+
328
+ Already-published versions stay broken — an exact peer is frozen in a published
329
+ manifest. What this buys is that the next cut is not broken too.
330
+
331
+ - Document that `KeyringTamperedError` carries its reason at `err.details.reason`, not `err.reason`.
332
+
333
+ `TamperedError` has a **direct top-level** `reason`; `KeyringTamperedError` nests it under `details` alongside `userId` and the format transition. The classes genuinely differ, and the resemblance is a trap: a caller who copies the `TamperedError` shape reads `undefined`, falls through to the else-branch, and **renders a format transition as an attack** — the exact collapse these reason codes exist to prevent.
334
+
335
+ The class had no JSDoc saying where to read it. It now states the access shape, names the contrast, and carries a worked `catch` block, so the answer is in the shipped `.d.ts` where a caller meets it.
336
+
337
+ Documentation only — no type, value, or behaviour change.
338
+
339
+ Found by noy-db-docs, which had documented `err.reason` for this class **two lines below its own sentence warning that the resemblance to `TamperedError.reason` is "a trap worth naming"**. It identified the trap and fell into it one level earlier than it was looking.
340
+
341
+ Hub's own prose was swept and is clean: every `.reason` reference in the changelog is to `TamperedError.reason`, which is correct for that class.
342
+
343
+ - A FAILED non-strict lazy re-derive no longer clears the stale flag (#1258).
344
+
345
+ `resolveStaleOnRead` consumes the pending flag before reading the source (a
346
+ recursion guard for self-write outputs). A STRICT failure throws and the catch
347
+ restores it; a NON-STRICT failure warned and continued, leaving the flag
348
+ consumed — so the record was served as fresh and never retried, permanently,
349
+ because nothing would mark it stale again.
350
+
351
+ The decision this needed was what `strict: false` means. It means "a failed
352
+ derivation must not break the read", NOT "report this output as current". The
353
+ record is still served, the warning still fires, and the flag now survives so
354
+ the next read retries — strict-mode behaviour minus the throw.
355
+
356
+ Deliberate cost, stated so it is not later mistaken for a bug: a
357
+ permanently-failing non-strict derivation now re-runs on every read of that id
358
+ rather than once. That is louder and more expensive than silently serving a
359
+ stale value forever, and it is the trade made everywhere else here — a degraded
360
+ state must not render as a healthy one.
361
+
362
+ - `HistoryConfig.ledger` now documents what it costs (#1248).
363
+
364
+ **The hash chain is the entire cost of history; per-record snapshots are free.**
365
+ Measured on the primitive write path (no guards / MVs / derivations, sequential
366
+ `put()`, in-memory store, N = 3000): snapshots-only is 1.00×, ledger-only is
367
+ 2.35×, both is 2.41×.
368
+
369
+ The cause is structural rather than an inefficiency: an entry's `prevHash`
370
+ depends on the current head, so appends are inherently serialized — one head
371
+ read, one delta encryption and one CAS-put per record op. That serialization is
372
+ the tamper-evidence property.
373
+
374
+ ⚠️ The microsecond figures come from an in-memory store and **understate the
375
+ felt cost**: on browser IndexedDB the ledger adds a whole extra encrypted store
376
+ write to a path whose base op is already milliseconds. The ~2.4× ratio is the
377
+ portable number; the microseconds are a floor.
378
+
379
+ A single human-paced write pays a difference nobody perceives. What pays visibly
380
+ is a one-click batch — an "approve all", a CSV import, a period prefill — and a
381
+ write that fans out through derivations and MVs pays once per resulting stored
382
+ write. `HistoryConfig` is per-collection so the chain can be confined to the
383
+ collections where tamper-evidence carries weight.
384
+
385
+ - CORRECTION to `0.7.0-pre.14`'s description of #1269 — the fix is unchanged; what
386
+ it covers was described wrongly, twice.
387
+
388
+ The guard reads **declarative `spec.groupBy` and nothing else**. So:
389
+
390
+ - **caught** — a spec declaring `groupBy: [...]` as a top-level field, whether
391
+ `unionSources` or query-form;
392
+ - **silent** — `.groupBy(...)` chained INSIDE a `query: (db) => …` callback,
393
+ which is a runtime call on the query builder and never appears in the spec;
394
+ - **irrelevant** — `spec.sources`. The guard never reads it, so declaring it
395
+ cannot bring a shape into coverage.
396
+
397
+ The published `pre.14` entry said the uncovered case was "a single-query MV that
398
+ constructs its own source before the dependency is recorded". That framing is
399
+ wrong in a way that misleads: it points at dependency ORDERING and invites a
400
+ reader to fix coverage by declaring `sources`, which does nothing. Ordering
401
+ decides which OTHER guard fires first — a late-attach reconcile can loudly
402
+ refuse a virtual field on an already-constructed collection — not whether this
403
+ one fires.
404
+
405
+ Measured by a consumer on the published `pre.14` and re-derived from the code
406
+ here. No behaviour change: a `.groupBy()` chained in a callback was silent
407
+ before this correction and is silent after it. Closing that residue needs the
408
+ built query plan inspected, or a compute-time refusal, and is not attempted
409
+ here.
410
+
411
+ - A materialized view whose `groupBy` names a VIRTUAL computed field is now
412
+ refused at collection registration (#1269).
413
+
414
+ A virtual field is evaluated on read and never stored, so the MV pipeline read
415
+ the stored row, found nothing, and bucketed every row under an `undefined` key —
416
+ a well-formed aggregate carrying a wrong NUMBER rather than an error, on the one
417
+ path where nobody re-checks the arithmetic. `query().where()` already refuses the
418
+ same field with `FieldNotQueryableError`, so the two halves of what reads as one
419
+ query layer disagreed.
420
+
421
+ Same principle as the executor's existing object-valued-group-key refusal
422
+ ("refuse, don't bucket wrong") and the `triggerBy` virtual-target refusal
423
+ (#1266), applied at the earliest point that can see both the MV and the
424
+ collection's field modes.
425
+
426
+ **Known residue, stated rather than left to be discovered:** a single-query MV of
427
+ the shape `query: (db) => db.collection(name)…` constructs its own source from
428
+ inside `MaterializedViewRegistry.register()`, before the dependency is recorded,
429
+ so that shape is not caught by this guard. `unionSources`, an aggregate with
430
+ explicit `sources`, and any source built by an earlier `vault.collection()` call
431
+ are caught. The same ordering is already documented for the neighbouring
432
+ tiers+crdt guard.
433
+
434
+ - Satellite hub peers now publish as a caret **range** instead of an exact version (#1228).
435
+
436
+ Every `@noy-db/*` satellite declared `peerDependencies['@noy-db/hub'] = "workspace:*"`, which **publishes as an exact version**:
437
+
438
+ ```
439
+ workspace:* -> "@noy-db/hub": "0.7.0-pre.8" exact
440
+ workspace:^ -> "@noy-db/hub": "^0.7.0-pre.8" a range
441
+ ```
442
+
443
+ Exact peers make the satellite set co-installable **only when every member came from the same cut**. Measured against published packages:
444
+
445
+ ```
446
+ npm i @noy-db/as-csv@0.7.0-pre.6 @noy-db/in-vue@0.7.0-pre.5 -> exit 1, ERESOLVE
447
+ npm i @noy-db/as-csv@0.7.0-pre.6 @noy-db/in-vue@0.7.0-pre.6 -> exit 0
448
+ ```
449
+
450
+ No hub version satisfies two different exact peers, so any lag — a satellite not republished in a given release, or a consumer upgrading one package — made a pair uninstallable by name. Caret ranges are jointly satisfiable (`hub@pre.8` satisfies both `^pre.7` and `^pre.8`) and still stop at the next minor, so a satellite never claims to work against a hub line it has not seen.
451
+
452
+ ## The reason for the old form did not hold
453
+
454
+ The architecture rule mandated `workspace:*` because `workspace:^` "trips the changeset-cli pre-1.0 dep-propagation heuristic and forces unintended major bumps on every dependent". `release.mjs`'s own header contradicted that all along: the heuristic fires **"even with loose `workspace:*` constraints"**. The peer form never prevented the bumps — the normalizer does.
455
+
456
+ Verified rather than reasoned: a full `release:version` was run with all 50 satellites on `workspace:^`. The line advanced normally, the normalizer corrected the heuristic as usual, and every package landed on one version.
457
+
458
+ **Nothing breaks for existing consumers.** A range is strictly more permissive than the exact version it replaces, so anything that resolved before still resolves. The `peer-deps` architecture rule now requires `workspace:^` and is mutation-checked.
459
+
460
+ - Two guards now refuse where the information to refuse already exists (design
461
+ pass: _where a check refuses vs where it quietly answers_).
462
+
463
+ **`fieldMeta` typo keys are refused at `vault.collection()`, not only at
464
+ `describe()`.** A collection nobody describes was never checked at all — and
465
+ `fieldMeta` is what carries `sensitivity`, so the unchecked case was the one
466
+ with a data-classification inventory hanging off it.
467
+
468
+ ⚠️ **BREAKING for code that catches around `describe()`.** For a validator whose
469
+ fields read synchronously, `FieldMetaUnknownFieldError` now arrives from
470
+ `vault.collection()` instead of from `describe()`. This is not only "the throw
471
+ comes earlier" — **a `try` that wraps only the `describe()` call no longer
472
+ catches it at all**, so the error escapes the handler written for it. A pilot
473
+ consumer's guard suite had exactly this shape: it constructed the collection
474
+ outside the `try`, and on upgrade those tests would have started ERRORING rather
475
+ than failing with a clear message, reading as "the new release broke our tests"
476
+ instead of "the guard moved". If you assert on this error, catch around the
477
+ `vault.collection()` call — or around both, and report which surface rejected,
478
+ which turns the relocation into a changed label rather than a break.
479
+
480
+ The describe-time check is KEPT, not moved, because there are three tiers of
481
+ knowability rather than two: a name declared in the config itself needs no
482
+ schema; a validator whose fields read synchronously is checkable at
483
+ registration; fields that exist only after async derivation are checkable only
484
+ at `describe()`. Hoisting alone would move the check earlier for some
485
+ collections and remove it for others. Unchanged: no readable validator means no
486
+ check, because a TS-generic collection names real fields that appear in no
487
+ runtime config.
488
+
489
+ **`withMaterializedView` distinguishes an absent `rowKey` from a wrong-typed
490
+ one.** Passing a field NAME — the shape every neighbouring option takes —
491
+ reported `rowKey is required`, which names absence. It now says it must be a
492
+ function, names the type it got, and shows the fix. Two states that warrant
493
+ different responses no longer render identically.
494
+
495
+ - A rollup child that RE-PARENTS now recomputes the old parent as well as the new
496
+ one (#1257).
497
+
498
+ The dispatcher read the rollup key from the incoming record only, so moving a
499
+ child from parent A to parent B recomputed B and left A's aggregate holding a
500
+ number that was correct before the move. That is the dangerous shape: a stale
501
+ total reads as data, not as an error, so nothing prompts anyone to re-check it.
502
+
503
+ Same old-value class as the `triggerBy` update fan-out fixed in #1249 — and it
504
+ is a small change rather than a new mechanism only because that fix added
505
+ prior-record capture to the write path, which this reuses.
506
+
507
+ Consistent with the rest of the derivation surface: a write that does not thread
508
+ a prior record (a sync-applied wave write, a tiers restore) degrades to
509
+ new-parent-only, the previous behaviour, rather than guessing at the old key.
510
+
511
+ - Export `assertRosterEpochCurrent` and `nextRosterEpoch` — they shipped in `0.7.0-pre.9` reachable from nothing (#1097 follow-up).
512
+
513
+ `0.7.0-pre.9` added the roster epoch and **listed `assertRosterEpochCurrent` in its release notes under "what is new"**. The function was present in `dist/**/*.d.ts` and exported from **no entry point** — all 53 were enumerated and imported; none had it.
514
+
515
+ That is the whole mechanism's caller half. Hub stamps and binds the epoch, but the expectation arrives by a second channel hub does not carry, so **only application code can perform the comparison**. A consumer following the release notes could not import the function they were told to call.
516
+
517
+ Both are now exported from the root barrel, beside `assertRosterAuthenticated`.
518
+
519
+ ## The class, and why it is worse than #1224
520
+
521
+ #1224 was a predicate reachable from the **wrong** seam — the root barrel rather than `/to`, which is all a store binds. This one was reachable from **none**. Both were found the same way: someone tried to follow the documentation.
522
+
523
+ Guarded by an invariant rather than by adding two names: a test asserts that **every** exported member of `roster-epoch.ts` reaches a published barrel, and that the query finds something in the first place. A helper added to that module later and exported from nothing fails the test rather than shipping unreachable.
524
+
525
+ Additive — the root barrel's golden baseline moves by two entries; nothing is removed or renamed.
526
+
527
+ - Say what is actually true about runtime dependencies (#1227).
528
+
529
+ Four places claimed zero runtime dependencies. Measured across all 57 packages, **51 have none** — but six do, and four of those are third-party:
530
+
531
+ | package | runtime dependency |
532
+ | ----------------------------------- | ---------------------------------------------------------- |
533
+ | `@noy-db/as-xml`, `@noy-db/as-xlsx` | `fast-xml-parser` |
534
+ | `@noy-db/in-devtools-tui` | `ink`, `react` |
535
+ | `create-noy-db` | its scaffolder toolchain |
536
+ | `@noy-db/hub` | `@noy-db/attestation` — a sibling on the same version line |
537
+ | `@noy-db/in-nuxt` | `@noy-db/in-devtools` — likewise |
538
+
539
+ Corrected:
540
+
541
+ - **`@noy-db/hub`** — "Zero runtime dependencies" → _no third-party runtime dependencies to install_, naming `@noy-db/attestation` as the one runtime dependency. Also states plainly that `zod` is **vendored into the bundle** for the optional persisted-schema converter, loaded lazily, never executing unless you use that path — and that it will not appear in your lockfile. That last clause is the one an SBOM or audit process needs, and no manifest field carries it.
542
+ - **`@noy-db/as-xlsx`** — "Zero runtime dependencies" was **flatly false**; it depends on `fast-xml-parser`. The intent was "no heavyweight spreadsheet library", which is true and is now what it says.
543
+
544
+ Two other claims were checked and left alone because they are correct: `@noy-db/as-zip` and `@noy-db/on-totp` genuinely have no dependencies.
545
+
546
+ Documentation only. No code, dependency, or behaviour change — the manifests already said this; only the prose disagreed with them.
547
+
548
+ - `schemaFieldKeys` now unwraps `z.preprocess()` on both Zod majors, so the two
549
+ field-typo guards (#1253's `fieldMeta`, #1249's `triggerBy` match) see through
550
+ it (#1262, reported by the pilot).
551
+
552
+ The `0.7.0-pre.12` fix followed refinement effects only and put `preprocess` on
553
+ the carve-out side with `transform`. That was wrong: `z.preprocess(fn, inner)`
554
+ rewrites the INPUT and then parses with `inner`, so the parsed record's keys ARE
555
+ `inner`'s keys — measured, `preprocess({a,b})` parses to `['a','b']` while
556
+ `transform` parses to `['c']`.
557
+
558
+ Scope, measured rather than assumed: this left the **sync** `describe()` path
559
+ unguarded for wrapped schemas. The async path was never affected —
560
+ `derivePersistedSchema` sees through `preprocess` on its own, so
561
+ `buildDescription` gets a populated field map and never reaches the
562
+ `schemaFieldKeys` fallback. The sync path is public, is what tooling reaches
563
+ for first, and is the only path some Zod 3 consumers can reach at all (the
564
+ async path needs the `zod-to-json-schema` peer), so the gap was real — but it
565
+ was one path, not both.
566
+
567
+ Replaced the effect-kind allowlist with one rule that covers every wrapper on
568
+ both majors: **follow the output side.** These keys describe the parsed record,
569
+ so the only question a wrapper raises is whether it changes the parsed shape.
570
+ Zod 3 `ZodEffects` follows `_def.schema` for `refinement` and `preprocess`;
571
+ Zod 4 wraps both `z.preprocess()` and `.transform()` as a `ZodPipe` and
572
+ following `_def.out` resolves them with no effect-kind test at all — preprocess
573
+ reaches the object, `.transform()` reaches a shapeless `ZodTransform` and stays
574
+ silent on its own. **This also fixes Zod 4 `z.preprocess()`, which the narrower
575
+ one-line widening would have missed.**
576
+
577
+ `.transform()` remains deliberately silent on both majors: it replaces the
578
+ output, so the inner keys would describe a record that is never stored. The two
579
+ are one string apart and mean opposite things, so tests pin them against each
580
+ other on both majors.
581
+
582
+ - `schemaFieldKeys` now unwraps Zod 3 `ZodEffects` created by object-level
583
+ `.refine()`/`.superRefine()` (reported by the pilot): the wrapper hides
584
+ `.shape`, so both field-typo guards — #1253's `fieldMeta` validation and
585
+ #1249's `triggerBy` match validation — were silent for exactly the schemas
586
+ most worth guarding. Unwrapping follows refinement effects only: a
587
+ `.transform()`/`.preprocess()` changes the output shape, so those stay
588
+ silent by design. Zod 4 keeps `.shape` through `.refine()` and is unaffected.
589
+ - Refuse to seal a record against a coerced address or over an empty plaintext (#1220).
590
+
591
+ Two boundary values were accepted silently and produced envelopes that are **valid and undecodable** — sealed correctly, addressed or filled with nothing. Both are now `TypeError`s at the seal, stated as output-domain invariants so they hold for every caller rather than for the two calls that surfaced them:
592
+
593
+ - **`buildRecordAad` refuses a non-string `collection` or `id`.** `String({})` is `"[object Object]"` and `String(undefined)` is `"undefined"`; both make perfectly good AAD, so a record sealed against one is stored at an address nothing queries. Sibling of the `version` assertion already in that function, and there for the same reason: it can only fire for a caller TypeScript never saw.
594
+ - **`RecordCodec.encryptJsonString` refuses a non-string plaintext.** `JSON.stringify(undefined)` is `undefined`, so an undefined record sealed over _nothing_ — `_data` a bare GCM tag over zero ciphertext.
595
+
596
+ Why this was worth a guard rather than a documentation note: the two read paths disagreed about the same bytes and neither named the cause. The cached path returned `null`, indistinguishable from _no such record_; the hydrate path threw `SyntaxError` out of `decryptRecord`, indistinguishable from _your store returned corrupt bytes_. A `to-*` author or daemon operator meeting that would reasonably suspect their store, indefinitely, over a caller mistake made days earlier.
597
+
598
+ No format, encryption, or integrity change: envelopes that seal continue to seal identically, and `NOYDB_ENVELOPE_GENERATION` is unaffected. This only refuses inputs that previously produced unreadable records.
599
+
600
+ - Fix the getting-started example in hub's module JSDoc, which ships inside
601
+ `dist/index.d.ts`. It omitted the required `user` option and passed `secret` to
602
+ `openVault()`, where it is not accepted — `secret` is a `createNoydb()` option.
603
+ Compiled verbatim, the published snippet produced `TS2741` and `TS2353`.
604
+
605
+ `packages/hub/README.md` carried the same class of defect: it taught
606
+ `userId: 'alice'` (the option is `user`) and imported `memory` from
607
+ `@noy-db/to-memory` (renamed to `toMemory`). Both ship in the tarball.
608
+
609
+ Guarded going forward by `pnpm check:prose-examples`, which typechecks every
610
+ fenced `ts` block in shipped prose against the built `dist`. `check:prose-api`
611
+ verifies that a documented method _exists_; these defects all named methods that
612
+ do exist and passed arguments that do not, which only a compiler can see.
613
+
614
+ - `check:test-env-deps` now catches OVER-declaration too, and one live instance is
615
+ removed.
616
+
617
+ The guard shipped with the previous fix caught only the missing direction — a
618
+ package running tests in an environment it does not declare. The mirror case
619
+ turns out to be the mechanism the whole class rests on: **a package that
620
+ declares an environment it never runs in is what silently satisfies the packages
621
+ that use it and do not declare it.** The over-declaration is what makes the
622
+ under-declaration invisible.
623
+
624
+ `to-browser-idb` carried exactly that — `happy-dom ^18.0.0` while running
625
+ `environment: 'node'` with a fake-indexeddb polyfill. It was plausibly the
626
+ declarer supplying 18.x to the nine `as-*` suites that named the environment and
627
+ declared nothing. Removed; every genuine user now declares its own.
628
+
629
+ Added after the same mistake was made twice in one day: a substring scan gave
630
+ `by-peer` a declaration for a mention in a COMMENT, and this predated all of it.
631
+
632
+ Mutation-checked in both directions — a package that genuinely uses the
633
+ environment keeps its declaration without firing (no false positive on
634
+ legitimate declarers), and restoring the phantom fails the check.
635
+
636
+ - Export `isConflictError` from `@noy-db/hub/to` (#1224).
637
+
638
+ The predicate was reachable only from the root barrel, while `/to` exported the `ConflictError` **class**. That is the wrong way round for the one seam that needs it: `isConflictError` exists precisely because a store may bind a different copy of `@noy-db/hub/to` than its caller, making `instanceof` against that class silently miss (#935) — CAS retry loops rethrow instead of retrying, and the sync engine misfiles the conflict with no resolution run.
639
+
640
+ A store binds `/to` and nothing else, so store authors were told by the predicate's own documentation to use something they could not import, and the obvious fallback — `instanceof ConflictError` off `/to` — is exactly the defect the predicate prevents.
641
+
642
+ Additive: an existing function on an existing subpath. Nothing is removed or renamed, and the root barrel export is unchanged.
643
+
644
+ Guarded by an invariant rather than an enumeration: a test parses `kernel/errors.ts` for any `is*` predicate whose contract mentions the store seam and asserts each is reachable from `/to`, so a future sibling cannot repeat this. `/to`'s golden surface baseline moves by one entry.
645
+
646
+ - A DELETE through a `triggerBy` hop now fans out — it silently did not (#1294).
647
+
648
+ Reported by a consumer adopting the hop from `0.7.0-pre.16`: puts through a
649
+ `via` hop re-fired the derivation, deletes did not, while deletes through a
650
+ plain pair did. The delete path built its tuple with the UNHOPPED builder, so a
651
+ mapped pair compared the written record's `from` value against `source[to]` —
652
+ the wrong side of the relationship — and matched nothing. No error, no fan-out.
653
+
654
+ **Two gaps, not one.** Deleting the INTERMEDIATE record was also unhandled, and
655
+ strands every source it addressed for the same reason re-pointing one does:
656
+ nothing is written to the trigger or source collection, so no other path can
657
+ notice. The write path already covered that; the delete path did not, which made
658
+ the hop's correctness argument hold for puts only.
659
+
660
+ **The shipped typings said deletes fan out "for BOTH forms".** That read as
661
+ covering hops and did not — a behaviour gap and a prose-vs-artefact mismatch in
662
+ one. The sentence now enumerates `on`, plain `match`, `via` hops, and deletes of
663
+ the intermediate itself.
664
+
665
+ **The sync tuple builder is deleted rather than kept**, and that is the durable
666
+ half: two builders that had to agree is exactly how this drifted, because the
667
+ delete path called the wrong one. `resolveTuple` with no `via` does what the old
668
+ one did, its unit tests moved across unchanged, and one path cannot disagree
669
+ with itself.
670
+
671
+ - `triggerBy` match targets are now checked against where the field actually
672
+ LIVES, not just whether it is declared (#1266, reported by the pilot).
673
+
674
+ A derivation matcher reads STORED records. A `mode: 'virtual'` computed field is
675
+ evaluated on the read path and never persisted — but it appears in `computed:`
676
+ (and in `via(computed(...))`) exactly like a materialized one, so registration
677
+ accepted it and the fan-out then matched nothing, forever. That is the precise
678
+ failure the #1249 match guard exists to prevent, reached THROUGH the guard
679
+ rather than around it, and it is the first thing a consumer reaches for when a
680
+ match target is not already stored.
681
+
682
+ Refused at registration rather than supported: matching a virtual field means
683
+ running user code for every candidate row, turning an indexed narrow into a full
684
+ scan. `mode: 'materialized'` is stored, already works, and is what the error
685
+ names. Both sides are refused — `to` reads the source record and `from` reads
686
+ the written record, and both are the stored shape. (The report covered `to`;
687
+ `from` had the identical defect.)
688
+
689
+ The virtual check runs even when the schema's field list is unreadable, unlike
690
+ the typo guard beside it. The typo guard needs a field list to compare against
691
+ and stays silent without one; this one does not — "declared, but never stored"
692
+ is provable from the declaration alone.
693
+
694
+ Second defect fixed in the same change: `viaFields` was missing from the guard's
695
+ key set entirely, so a `via()`-declared MATERIALIZED field — a perfectly valid
696
+ match target — was rejected as an undeclared typo. A guard that refuses valid
697
+ configurations is how people learn to stop trusting it, so both directions ship
698
+ together, each with a test that fails without its half of the fix.
699
+
700
+ - `zod` is now an OPTIONAL peer dependency instead of being vendored into the
701
+ bundle (#1227).
702
+
703
+ Hub shipped `zod@4.4.3` inside `dist/` — 548 KB, at a build-frozen version,
704
+ declared in no manifest field. `npm ls zod` in a consumer tree showed nothing
705
+ while that copy was reachable, so SBOM and audit tooling missed it and a zod
706
+ advisory could not be remediated by a consumer bumping zod.
707
+
708
+ **This was never a deliberate vendoring.** The loader's own comment said "this
709
+ is a dynamic import so it does not add a static zod dependency to hub" — but
710
+ tsup externalises DECLARED dependencies and bundles everything else, static or
711
+ dynamic alike, and zod was a devDependency only. The comment described an
712
+ intent the build silently defeated; it now records the actual mechanism.
713
+
714
+ **Declaring it is also the correctness fix, not only a size one.** Zod's
715
+ `toJSONSchema` reads a schema's internals, and the schema is built by the
716
+ CONSUMER's zod. A vendored copy meant hub inspected one zod's objects with a
717
+ different zod's reader, with version skew that nothing detected. Now there is
718
+ one zod — theirs.
719
+
720
+ Nothing to install: the peer is optional, exactly like `zod-to-json-schema`,
721
+ and the converter is still loaded lazily. A caller with a Zod v4 schema
722
+ necessarily already has zod; a caller without one never reaches the path.
723
+
724
+ Measured, both sides rebuilt: tarball 3.3 MB → 3.1 MB packed, 12.0 MB → 10.6 MB
725
+ unpacked. The README claim is updated in the same change to say what is now
726
+ true.
727
+
728
+ - CORRECTION to `0.7.0-pre.14`: the optional `zod` peer now reads
729
+ `^3.0.0 || ^4.0.0`, not `^4.0.0`.
730
+
731
+ `pre.14` un-vendored zod and declared it as an optional peer — correctly — but
732
+ declared a range NARROWER THAN WHAT HUB SUPPORTS. Under npm that made
733
+ `@noy-db/hub@0.7.0-pre.14` + `zod@3` uninstallable by name.
734
+
735
+ Measured, with controls in both directions:
736
+
737
+ ```
738
+ npm i @noy-db/hub@0.7.0-pre.14 zod@3.25.76 -> exit 1, ERESOLVE
739
+ npm i @noy-db/hub@0.7.0-pre.14 zod@4.4.3 -> exit 0 (control: major, not range shape)
740
+ npm i @noy-db/hub@0.7.0-pre.13 zod@3.25.76 -> exit 0 (control: the DECLARATION changed,
741
+ not the support)
742
+ ```
743
+
744
+ Zod 3 has always worked and still does — through the `zod-to-json-schema`
745
+ optional peer, with the v4-native `toJSONSchema` loader falling back when it is
746
+ absent. The two releases immediately before this one were substantially Zod 3
747
+ hardening (the `ZodEffects` unwrap, `z.preprocess` on both majors), so the
748
+ investment and the declaration pointed in opposite directions.
749
+
750
+ **Why no gate caught it:** while zod was vendored, no resolver ever saw a range,
751
+ so none was ever exercised. Every in-repo check stays green under either
752
+ declaration — a peer FORM deciding installability, which is the same class as
753
+ this family's exact-peer incident. There is now a test asserting the range
754
+ admits a real version of each major, and refusing a range that ends in a
755
+ dangling `||`.
756
+
757
+ pnpm only warns, so a pnpm workspace saw nothing; npm refuses.
758
+
759
+ - @noy-db/attestation@0.7.0
760
+
761
+ ## 0.7.0-pre.18
762
+
763
+ ### Patch Changes
764
+
765
+ - Two dependency-declaration defects, both invisible to every gate because both
766
+ were satisfied by something that never promised anything.
767
+
768
+ **`happy-dom` was used by 22 packages and declared by 4.** The `as-*`, `on-*`
769
+ and `by-*` families named it in their vitest `environment:` and never declared
770
+ it; it resolved only because `in-react`, `in-pinia`, `in-nuxt` and
771
+ `to-browser-idb` happened to declare it — at **three different majors**
772
+ (`^15.11.7`, `^17.4.4`, `^18.0.0`). So those suites ran against whichever
773
+ version won hoisting, pinned by nothing, and a devDependency bump in an
774
+ unrelated package could have silently changed the DOM implementation under
775
+ them. Every user now declares it, all at one range.
776
+
777
+ ⚠️ It is named by no `import` anywhere — only by a vitest config string, or (in
778
+ hub's case) by an `@vitest-environment` DOCBLOCK PRAGMA, which is invisible to
779
+ an import scan and a config grep alike. Found by extracting a family and
780
+ watching it fail to stand alone.
781
+
782
+ ⛔ **Declaring it does NOT make the mistake self-detecting, and a sweep alone
783
+ would have implied otherwise.** pnpm's virtual store still satisfies an
784
+ undeclared package from a sibling that declares it — deleting a declaration and
785
+ reinstalling leaves the suite green, including the test that needs the
786
+ environment. So correct manifests today prevent nothing tomorrow. A static
787
+ check (`pnpm check:test-env-deps`, wired into CI) is the part that enforces it;
788
+ the manifests are merely honest. (`require.resolve` reports the opposite —
789
+ Node's algorithm is not pnpm's store, so the probe has to read manifests rather
790
+ than resolve.)
791
+
792
+ **Sibling `peerDependencies` published as EXACT versions**, completing the fix
793
+ started in #1228. That issue converted the `hub` peer to `workspace:^` (which
794
+ publishes as a caret) and left sibling peers at `workspace:*` (which publishes
795
+ as an exact pin), so two satellites from different cuts were mutually
796
+ uninstallable by name. Install-proven with a discriminating control:
797
+
798
+ ```
799
+ as-xlsx@pre.17 + as-zip@pre.16 -> exit 1 the defect
800
+ as-xlsx@pre.17 + as-zip@pre.17 -> exit 0 control: it is the SKEW, not the pair
801
+ as-csv@pre.17 + in-vue@pre.16 -> exit 0 caret peers DO cross cuts
802
+ ```
803
+
804
+ Nine sibling edges across seven packages move `workspace:*` → `workspace:^`.
805
+ That changes the FORM without changing the KIND: they stay peers, so a consumer
806
+ still controls instance identity, and no per-pair judgement about whether
807
+ deduplication matters was required.
808
+
809
+ Already-published versions stay broken — an exact peer is frozen in a published
810
+ manifest. What this buys is that the next cut is not broken too.
811
+
812
+ - `check:test-env-deps` now catches OVER-declaration too, and one live instance is
813
+ removed.
814
+
815
+ The guard shipped with the previous fix caught only the missing direction — a
816
+ package running tests in an environment it does not declare. The mirror case
817
+ turns out to be the mechanism the whole class rests on: **a package that
818
+ declares an environment it never runs in is what silently satisfies the packages
819
+ that use it and do not declare it.** The over-declaration is what makes the
820
+ under-declaration invisible.
821
+
822
+ `to-browser-idb` carried exactly that — `happy-dom ^18.0.0` while running
823
+ `environment: 'node'` with a fake-indexeddb polyfill. It was plausibly the
824
+ declarer supplying 18.x to the nine `as-*` suites that named the environment and
825
+ declared nothing. Removed; every genuine user now declares its own.
826
+
827
+ Added after the same mistake was made twice in one day: a substring scan gave
828
+ `by-peer` a declaration for a mention in a COMMENT, and this predated all of it.
829
+
830
+ Mutation-checked in both directions — a package that genuinely uses the
831
+ environment keeps its declaration without firing (no false positive on
832
+ legitimate declarers), and restoring the phantom fails the check.
833
+
834
+ ## 0.7.0-pre.17
835
+
836
+ ### Patch Changes
837
+
838
+ - A DELETE through a `triggerBy` hop now fans out — it silently did not (#1294).
839
+
840
+ Reported by a consumer adopting the hop from `0.7.0-pre.16`: puts through a
841
+ `via` hop re-fired the derivation, deletes did not, while deletes through a
842
+ plain pair did. The delete path built its tuple with the UNHOPPED builder, so a
843
+ mapped pair compared the written record's `from` value against `source[to]` —
844
+ the wrong side of the relationship — and matched nothing. No error, no fan-out.
845
+
846
+ **Two gaps, not one.** Deleting the INTERMEDIATE record was also unhandled, and
847
+ strands every source it addressed for the same reason re-pointing one does:
848
+ nothing is written to the trigger or source collection, so no other path can
849
+ notice. The write path already covered that; the delete path did not, which made
850
+ the hop's correctness argument hold for puts only.
851
+
852
+ **The shipped typings said deletes fan out "for BOTH forms".** That read as
853
+ covering hops and did not — a behaviour gap and a prose-vs-artefact mismatch in
854
+ one. The sentence now enumerates `on`, plain `match`, `via` hops, and deletes of
855
+ the intermediate itself.
856
+
857
+ **The sync tuple builder is deleted rather than kept**, and that is the durable
858
+ half: two builders that had to agree is exactly how this drifted, because the
859
+ delete path called the wrong one. `resolveTuple` with no `via` does what the old
860
+ one did, its unit tests moved across unchanged, and one path cannot disagree
861
+ with itself.
862
+
863
+ ## 0.7.0-pre.16
864
+
865
+ ### Minor Changes
866
+
867
+ - New published type on `@noy-db/hub/to`: `NoydbRelayStore` — the store contract
868
+ minus the two members a relay profile omits by construction (#1237).
869
+
870
+ `saveAll` is whole-vault replace, a rollback superweapon pointed at a relay's own
871
+ hosts. `listVaults` is an existence leak. A relay handler typed against
872
+ `NoydbRelayStore` **cannot compile a call to either**, so the exclusions are
873
+ enforced by the compiler rather than by handing a handler an object that carries
874
+ `saveAll` and trusting a runtime `Set` not to call it.
875
+
876
+ Purely additive and purely type-level. The `NoydbStore` runtime contract is
877
+ unchanged — still the same 6 methods — and a full store satisfies the relay type
878
+ structurally, so relaying an ordinary store needs no changes to it. That
879
+ boundary was a precondition rather than a convenience: the format-conformance
880
+ kit's store-observation design (#1211) names a change to the `NoydbStore`
881
+ contract as the single thing that would invalidate it, so a narrowing had to
882
+ stay in the type layer or stop.
883
+
884
+ It ships on `/to` rather than inside a relay package because a second consumer
885
+ already needs to name the shape without depending on a relay server it does not
886
+ run.
887
+
888
+ - `triggerBy` match pairs accept ONE declared hop through an intermediate
889
+ collection (#1277).
890
+
891
+ ```ts
892
+ match: [
893
+ {
894
+ from: "clientId",
895
+ to: "entityId",
896
+ via: { collection: "clients", take: "id", on: "entityId" },
897
+ },
898
+ { from: "cycle", to: "cycle" },
899
+ ];
900
+ ```
901
+
902
+ A source matches when the intermediate — the record in `via.collection` whose
903
+ `via.take` equals `written[from]` — carries a `via.on` equal to `source[to]`.
904
+ This makes a relationship expressible where the two collections share no field
905
+ at all: bills carry `entityId`, disbursements carry `clientId`, and the client
906
+ record is what relates them.
907
+
908
+ The alternative was a denormalised key on one side. That is a second copy of
909
+ something the vault can already resolve, and a partial backfill goes quiet on
910
+ exactly the oldest, least-audited rows — the silent staleness `match` exists to
911
+ remove, handed back one layer down.
912
+
913
+ **A write to the INTERMEDIATE collection fires the trigger too**, fanning out on
914
+ its old ∪ new value. This is the whole correctness argument: re-pointing an
915
+ intermediate writes to neither the trigger nor the source collection, so nothing
916
+ else in the system can notice, and every source it used to address would be
917
+ stranded silently. The cheaper design — resolving the hop only on trigger writes
918
+ — passes every test anyone would think to write and fails only on that edit.
919
+
920
+ Hop resolution costs ONE lookup per written record (`take: 'id'` is a direct
921
+ get), never one per candidate row. A dangling hop matches nothing rather than
922
+ throwing. `maxFanout` caps the hop fan-out as it does the direct form, and the
923
+ intermediate's edge enters the cycle-detection graph.
924
+
925
+ ### Patch Changes
926
+
927
+ - `HistoryConfig.ledger` now documents what it costs (#1248).
928
+
929
+ **The hash chain is the entire cost of history; per-record snapshots are free.**
930
+ Measured on the primitive write path (no guards / MVs / derivations, sequential
931
+ `put()`, in-memory store, N = 3000): snapshots-only is 1.00×, ledger-only is
932
+ 2.35×, both is 2.41×.
933
+
934
+ The cause is structural rather than an inefficiency: an entry's `prevHash`
935
+ depends on the current head, so appends are inherently serialized — one head
936
+ read, one delta encryption and one CAS-put per record op. That serialization is
937
+ the tamper-evidence property.
938
+
939
+ ⚠️ The microsecond figures come from an in-memory store and **understate the
940
+ felt cost**: on browser IndexedDB the ledger adds a whole extra encrypted store
941
+ write to a path whose base op is already milliseconds. The ~2.4× ratio is the
942
+ portable number; the microseconds are a floor.
943
+
944
+ A single human-paced write pays a difference nobody perceives. What pays visibly
945
+ is a one-click batch — an "approve all", a CSV import, a period prefill — and a
946
+ write that fans out through derivations and MVs pays once per resulting stored
947
+ write. `HistoryConfig` is per-collection so the chain can be confined to the
948
+ collections where tamper-evidence carries weight.
949
+
950
+ ## 0.7.0-pre.15
951
+
952
+ ### Patch Changes
953
+
954
+ - CORRECTION to `0.7.0-pre.14`'s description of #1269 — the fix is unchanged; what
955
+ it covers was described wrongly, twice.
956
+
957
+ The guard reads **declarative `spec.groupBy` and nothing else**. So:
958
+
959
+ - **caught** — a spec declaring `groupBy: [...]` as a top-level field, whether
960
+ `unionSources` or query-form;
961
+ - **silent** — `.groupBy(...)` chained INSIDE a `query: (db) => …` callback,
962
+ which is a runtime call on the query builder and never appears in the spec;
963
+ - **irrelevant** — `spec.sources`. The guard never reads it, so declaring it
964
+ cannot bring a shape into coverage.
965
+
966
+ The published `pre.14` entry said the uncovered case was "a single-query MV that
967
+ constructs its own source before the dependency is recorded". That framing is
968
+ wrong in a way that misleads: it points at dependency ORDERING and invites a
969
+ reader to fix coverage by declaring `sources`, which does nothing. Ordering
970
+ decides which OTHER guard fires first — a late-attach reconcile can loudly
971
+ refuse a virtual field on an already-constructed collection — not whether this
972
+ one fires.
973
+
974
+ Measured by a consumer on the published `pre.14` and re-derived from the code
975
+ here. No behaviour change: a `.groupBy()` chained in a callback was silent
976
+ before this correction and is silent after it. Closing that residue needs the
977
+ built query plan inspected, or a compute-time refusal, and is not attempted
978
+ here.
979
+
980
+ - CORRECTION to `0.7.0-pre.14`: the optional `zod` peer now reads
981
+ `^3.0.0 || ^4.0.0`, not `^4.0.0`.
982
+
983
+ `pre.14` un-vendored zod and declared it as an optional peer — correctly — but
984
+ declared a range NARROWER THAN WHAT HUB SUPPORTS. Under npm that made
985
+ `@noy-db/hub@0.7.0-pre.14` + `zod@3` uninstallable by name.
986
+
987
+ Measured, with controls in both directions:
988
+
989
+ ```
990
+ npm i @noy-db/hub@0.7.0-pre.14 zod@3.25.76 -> exit 1, ERESOLVE
991
+ npm i @noy-db/hub@0.7.0-pre.14 zod@4.4.3 -> exit 0 (control: major, not range shape)
992
+ npm i @noy-db/hub@0.7.0-pre.13 zod@3.25.76 -> exit 0 (control: the DECLARATION changed,
993
+ not the support)
994
+ ```
995
+
996
+ Zod 3 has always worked and still does — through the `zod-to-json-schema`
997
+ optional peer, with the v4-native `toJSONSchema` loader falling back when it is
998
+ absent. The two releases immediately before this one were substantially Zod 3
999
+ hardening (the `ZodEffects` unwrap, `z.preprocess` on both majors), so the
1000
+ investment and the declaration pointed in opposite directions.
1001
+
1002
+ **Why no gate caught it:** while zod was vendored, no resolver ever saw a range,
1003
+ so none was ever exercised. Every in-repo check stays green under either
1004
+ declaration — a peer FORM deciding installability, which is the same class as
1005
+ this family's exact-peer incident. There is now a test asserting the range
1006
+ admits a real version of each major, and refusing a range that ends in a
1007
+ dangling `||`.
1008
+
1009
+ pnpm only warns, so a pnpm workspace saw nothing; npm refuses.
1010
+
1011
+ ## 0.7.0-pre.14
1012
+
1013
+ ### Patch Changes
1014
+
1015
+ - The bundle-size gate now needs a growth to exceed BOTH its percentage tolerance
1016
+ and an absolute byte allowance before failing (#1268). No runtime change — this
1017
+ is the CI gate only.
1018
+
1019
+ A percentage on a small baseline measures the wrong thing. The `floor` scenario
1020
+ is ~500 gzipped bytes, so 5% is ~25 bytes — narrower than a single
1021
+ registration-time guard. The gate had fired twice on necessary validation
1022
+ (#1249, #1266) and never once on the thing it exists to catch.
1023
+
1024
+ The two are separated by orders of magnitude: a subsystem leaking into a bundle
1025
+ is kilobytes (measured elsewhere in the family: forcing an SDK inline moved a
1026
+ package from 14,069 to 1,081,539 bytes), while a guard is tens of bytes. A
1027
+ 192-byte allowance sits far above the latter and far below the former.
1028
+
1029
+ Verified in both directions rather than assumed: a simulated leak (+406 bytes)
1030
+ still FAILS, and a 26-byte guard at +5.2% now PASSES. The numbers still ratchet
1031
+ and nothing was re-baselined.
1032
+
1033
+ - A materialized view whose `groupBy` names a VIRTUAL computed field is now
1034
+ refused at collection registration (#1269).
1035
+
1036
+ A virtual field is evaluated on read and never stored, so the MV pipeline read
1037
+ the stored row, found nothing, and bucketed every row under an `undefined` key —
1038
+ a well-formed aggregate carrying a wrong NUMBER rather than an error, on the one
1039
+ path where nobody re-checks the arithmetic. `query().where()` already refuses the
1040
+ same field with `FieldNotQueryableError`, so the two halves of what reads as one
1041
+ query layer disagreed.
1042
+
1043
+ Same principle as the executor's existing object-valued-group-key refusal
1044
+ ("refuse, don't bucket wrong") and the `triggerBy` virtual-target refusal
1045
+ (#1266), applied at the earliest point that can see both the MV and the
1046
+ collection's field modes.
1047
+
1048
+ **Known residue, stated rather than left to be discovered:** a single-query MV of
1049
+ the shape `query: (db) => db.collection(name)…` constructs its own source from
1050
+ inside `MaterializedViewRegistry.register()`, before the dependency is recorded,
1051
+ so that shape is not caught by this guard. `unionSources`, an aggregate with
1052
+ explicit `sources`, and any source built by an earlier `vault.collection()` call
1053
+ are caught. The same ordering is already documented for the neighbouring
1054
+ tiers+crdt guard.
1055
+
1056
+ - Two guards now refuse where the information to refuse already exists (design
1057
+ pass: _where a check refuses vs where it quietly answers_).
1058
+
1059
+ **`fieldMeta` typo keys are refused at `vault.collection()`, not only at
1060
+ `describe()`.** A collection nobody describes was never checked at all — and
1061
+ `fieldMeta` is what carries `sensitivity`, so the unchecked case was the one
1062
+ with a data-classification inventory hanging off it.
1063
+
1064
+ ⚠️ **BREAKING for code that catches around `describe()`.** For a validator whose
1065
+ fields read synchronously, `FieldMetaUnknownFieldError` now arrives from
1066
+ `vault.collection()` instead of from `describe()`. This is not only "the throw
1067
+ comes earlier" — **a `try` that wraps only the `describe()` call no longer
1068
+ catches it at all**, so the error escapes the handler written for it. A pilot
1069
+ consumer's guard suite had exactly this shape: it constructed the collection
1070
+ outside the `try`, and on upgrade those tests would have started ERRORING rather
1071
+ than failing with a clear message, reading as "the new release broke our tests"
1072
+ instead of "the guard moved". If you assert on this error, catch around the
1073
+ `vault.collection()` call — or around both, and report which surface rejected,
1074
+ which turns the relocation into a changed label rather than a break.
1075
+
1076
+ The describe-time check is KEPT, not moved, because there are three tiers of
1077
+ knowability rather than two: a name declared in the config itself needs no
1078
+ schema; a validator whose fields read synchronously is checkable at
1079
+ registration; fields that exist only after async derivation are checkable only
1080
+ at `describe()`. Hoisting alone would move the check earlier for some
1081
+ collections and remove it for others. Unchanged: no readable validator means no
1082
+ check, because a TS-generic collection names real fields that appear in no
1083
+ runtime config.
1084
+
1085
+ **`withMaterializedView` distinguishes an absent `rowKey` from a wrong-typed
1086
+ one.** Passing a field NAME — the shape every neighbouring option takes —
1087
+ reported `rowKey is required`, which names absence. It now says it must be a
1088
+ function, names the type it got, and shows the fix. Two states that warrant
1089
+ different responses no longer render identically.
1090
+
1091
+ - `zod` is now an OPTIONAL peer dependency instead of being vendored into the
1092
+ bundle (#1227).
1093
+
1094
+ Hub shipped `zod@4.4.3` inside `dist/` — 548 KB, at a build-frozen version,
1095
+ declared in no manifest field. `npm ls zod` in a consumer tree showed nothing
1096
+ while that copy was reachable, so SBOM and audit tooling missed it and a zod
1097
+ advisory could not be remediated by a consumer bumping zod.
1098
+
1099
+ **This was never a deliberate vendoring.** The loader's own comment said "this
1100
+ is a dynamic import so it does not add a static zod dependency to hub" — but
1101
+ tsup externalises DECLARED dependencies and bundles everything else, static or
1102
+ dynamic alike, and zod was a devDependency only. The comment described an
1103
+ intent the build silently defeated; it now records the actual mechanism.
1104
+
1105
+ **Declaring it is also the correctness fix, not only a size one.** Zod's
1106
+ `toJSONSchema` reads a schema's internals, and the schema is built by the
1107
+ CONSUMER's zod. A vendored copy meant hub inspected one zod's objects with a
1108
+ different zod's reader, with version skew that nothing detected. Now there is
1109
+ one zod — theirs.
1110
+
1111
+ Nothing to install: the peer is optional, exactly like `zod-to-json-schema`,
1112
+ and the converter is still loaded lazily. A caller with a Zod v4 schema
1113
+ necessarily already has zod; a caller without one never reaches the path.
1114
+
1115
+ Measured, both sides rebuilt: tarball 3.3 MB → 3.1 MB packed, 12.0 MB → 10.6 MB
1116
+ unpacked. The README claim is updated in the same change to say what is now
1117
+ true.
1118
+
1119
+ ## 0.7.0-pre.13
1120
+
1121
+ ### Patch Changes
1122
+
1123
+ - A FAILED non-strict lazy re-derive no longer clears the stale flag (#1258).
1124
+
1125
+ `resolveStaleOnRead` consumes the pending flag before reading the source (a
1126
+ recursion guard for self-write outputs). A STRICT failure throws and the catch
1127
+ restores it; a NON-STRICT failure warned and continued, leaving the flag
1128
+ consumed — so the record was served as fresh and never retried, permanently,
1129
+ because nothing would mark it stale again.
1130
+
1131
+ The decision this needed was what `strict: false` means. It means "a failed
1132
+ derivation must not break the read", NOT "report this output as current". The
1133
+ record is still served, the warning still fires, and the flag now survives so
1134
+ the next read retries — strict-mode behaviour minus the throw.
1135
+
1136
+ Deliberate cost, stated so it is not later mistaken for a bug: a
1137
+ permanently-failing non-strict derivation now re-runs on every read of that id
1138
+ rather than once. That is louder and more expensive than silently serving a
1139
+ stale value forever, and it is the trade made everywhere else here — a degraded
1140
+ state must not render as a healthy one.
1141
+
1142
+ - A rollup child that RE-PARENTS now recomputes the old parent as well as the new
1143
+ one (#1257).
1144
+
1145
+ The dispatcher read the rollup key from the incoming record only, so moving a
1146
+ child from parent A to parent B recomputed B and left A's aggregate holding a
1147
+ number that was correct before the move. That is the dangerous shape: a stale
1148
+ total reads as data, not as an error, so nothing prompts anyone to re-check it.
1149
+
1150
+ Same old-value class as the `triggerBy` update fan-out fixed in #1249 — and it
1151
+ is a small change rather than a new mechanism only because that fix added
1152
+ prior-record capture to the write path, which this reuses.
1153
+
1154
+ Consistent with the rest of the derivation surface: a write that does not thread
1155
+ a prior record (a sync-applied wave write, a tiers restore) degrades to
1156
+ new-parent-only, the previous behaviour, rather than guessing at the old key.
1157
+
1158
+ - `schemaFieldKeys` now unwraps `z.preprocess()` on both Zod majors, so the two
1159
+ field-typo guards (#1253's `fieldMeta`, #1249's `triggerBy` match) see through
1160
+ it (#1262, reported by the pilot).
1161
+
1162
+ The `0.7.0-pre.12` fix followed refinement effects only and put `preprocess` on
1163
+ the carve-out side with `transform`. That was wrong: `z.preprocess(fn, inner)`
1164
+ rewrites the INPUT and then parses with `inner`, so the parsed record's keys ARE
1165
+ `inner`'s keys — measured, `preprocess({a,b})` parses to `['a','b']` while
1166
+ `transform` parses to `['c']`.
1167
+
1168
+ Scope, measured rather than assumed: this left the **sync** `describe()` path
1169
+ unguarded for wrapped schemas. The async path was never affected —
1170
+ `derivePersistedSchema` sees through `preprocess` on its own, so
1171
+ `buildDescription` gets a populated field map and never reaches the
1172
+ `schemaFieldKeys` fallback. The sync path is public, is what tooling reaches
1173
+ for first, and is the only path some Zod 3 consumers can reach at all (the
1174
+ async path needs the `zod-to-json-schema` peer), so the gap was real — but it
1175
+ was one path, not both.
1176
+
1177
+ Replaced the effect-kind allowlist with one rule that covers every wrapper on
1178
+ both majors: **follow the output side.** These keys describe the parsed record,
1179
+ so the only question a wrapper raises is whether it changes the parsed shape.
1180
+ Zod 3 `ZodEffects` follows `_def.schema` for `refinement` and `preprocess`;
1181
+ Zod 4 wraps both `z.preprocess()` and `.transform()` as a `ZodPipe` and
1182
+ following `_def.out` resolves them with no effect-kind test at all — preprocess
1183
+ reaches the object, `.transform()` reaches a shapeless `ZodTransform` and stays
1184
+ silent on its own. **This also fixes Zod 4 `z.preprocess()`, which the narrower
1185
+ one-line widening would have missed.**
1186
+
1187
+ `.transform()` remains deliberately silent on both majors: it replaces the
1188
+ output, so the inner keys would describe a record that is never stored. The two
1189
+ are one string apart and mean opposite things, so tests pin them against each
1190
+ other on both majors.
1191
+
1192
+ - `triggerBy` match targets are now checked against where the field actually
1193
+ LIVES, not just whether it is declared (#1266, reported by the pilot).
1194
+
1195
+ A derivation matcher reads STORED records. A `mode: 'virtual'` computed field is
1196
+ evaluated on the read path and never persisted — but it appears in `computed:`
1197
+ (and in `via(computed(...))`) exactly like a materialized one, so registration
1198
+ accepted it and the fan-out then matched nothing, forever. That is the precise
1199
+ failure the #1249 match guard exists to prevent, reached THROUGH the guard
1200
+ rather than around it, and it is the first thing a consumer reaches for when a
1201
+ match target is not already stored.
1202
+
1203
+ Refused at registration rather than supported: matching a virtual field means
1204
+ running user code for every candidate row, turning an indexed narrow into a full
1205
+ scan. `mode: 'materialized'` is stored, already works, and is what the error
1206
+ names. Both sides are refused — `to` reads the source record and `from` reads
1207
+ the written record, and both are the stored shape. (The report covered `to`;
1208
+ `from` had the identical defect.)
1209
+
1210
+ The virtual check runs even when the schema's field list is unreadable, unlike
1211
+ the typo guard beside it. The typo guard needs a field list to compare against
1212
+ and stays silent without one; this one does not — "declared, but never stored"
1213
+ is provable from the declaration alone.
1214
+
1215
+ Second defect fixed in the same change: `viaFields` was missing from the guard's
1216
+ key set entirely, so a `via()`-declared MATERIALIZED field — a perfectly valid
1217
+ match target — was rejected as an undeclared typo. A guard that refuses valid
1218
+ configurations is how people learn to stop trusting it, so both directions ship
1219
+ together, each with a test that fails without its half of the fix.
1220
+
1221
+ ## 0.7.0-pre.12
1222
+
1223
+ ### Minor Changes
1224
+
1225
+ - `withDerivation`'s `triggerBy` accepts a multi-field `match` form (#1249):
1226
+ `{ collection, match: [{ from, to }] }` fans a write out to every source
1227
+ record where ALL pairs satisfy `String(source[to]) === String(written[from])`.
1228
+ `from: 'id'` reads the written record's id, making the existing `on` form the
1229
+ single-pair special case (it is unchanged and stays supported). This makes
1230
+ shared-key ("reverse") relationships and composite keys like
1231
+ `(clientId, cycle)` expressible without denormalising a synthetic key.
1232
+
1233
+ Also, for BOTH forms:
1234
+
1235
+ - a LOCAL UPDATE that changes any matched field fans out on old-match ∪
1236
+ new-match, so records addressed by the previous value no longer go
1237
+ silently stale; a sync-applied wave write or a tiers restore, which don't
1238
+ thread the prior record, fan out on the new tuple only;
1239
+ - a parent DELETE now fans out using the tombstoned record's values —
1240
+ previously deletes fired no triggers at all, leaving matched sources
1241
+ stale. The fan-out runs after the delete commits, so cap/strict errors
1242
+ can surface from `delete()` (same as the existing rollup-on-delete
1243
+ precedent);
1244
+ - match fields are validated against the collections' enumerable field sets at
1245
+ `vault.collection()` (the #1253 pattern): a provable typo throws instead of
1246
+ silently matching nothing forever; TS-generic collections stay unguarded by
1247
+ design.
1248
+
1249
+ `maxFanout` caps the unioned matched set per written event.
1250
+
1251
+ ### Patch Changes
1252
+
1253
+ - `schemaFieldKeys` now unwraps Zod 3 `ZodEffects` created by object-level
1254
+ `.refine()`/`.superRefine()` (reported by the pilot): the wrapper hides
1255
+ `.shape`, so both field-typo guards — #1253's `fieldMeta` validation and
1256
+ #1249's `triggerBy` match validation — were silent for exactly the schemas
1257
+ most worth guarding. Unwrapping follows refinement effects only: a
1258
+ `.transform()`/`.preprocess()` changes the output shape, so those stay
1259
+ silent by design. Zod 4 keeps `.shape` through `.refine()` and is unaffected.
1260
+
1261
+ ## 0.7.0-pre.11
1262
+
1263
+ ### Patch Changes
1264
+
1265
+ - `collection.describe()` now validates `fieldMeta` keys on the **sync** path when
1266
+ the configured validator exposes its field names (#1253).
1267
+
1268
+ Previously the guard ran only on the async `describe(opts)` path, so a typo'd
1269
+ `fieldMeta` key on the sync path produced a **phantom field carrying its declared
1270
+ `sensitivity`** while the real field went undescribed — an inventory wrong in both
1271
+ directions, silently, on the surface `sensitivity` exists to serve.
1272
+
1273
+ The sync path was thought unable to know the schema's fields because deriving them
1274
+ is async. That is true of field **types** and false of field **keys**: a Zod object
1275
+ exposes `.shape` directly, on v3 and v4, with no JSON-Schema derivation and no
1276
+ `zod-to-json-schema` peer. The latter matters — on Zod 3 that peer is required for
1277
+ the async path, so the sync path is the only one some consumers can reach.
1278
+
1279
+ Deliberately still silent in two cases, both of which would otherwise reject correct
1280
+ code: a validator whose fields hub cannot read, and **no validator at all** — a
1281
+ collection typed by a TypeScript generic alone has fields that are real and present
1282
+ in the data but appear in no runtime config, and `fieldMeta` legitimately names them.
1283
+
1284
+ - Document that `KeyringTamperedError` carries its reason at `err.details.reason`, not `err.reason`.
1285
+
1286
+ `TamperedError` has a **direct top-level** `reason`; `KeyringTamperedError` nests it under `details` alongside `userId` and the format transition. The classes genuinely differ, and the resemblance is a trap: a caller who copies the `TamperedError` shape reads `undefined`, falls through to the else-branch, and **renders a format transition as an attack** — the exact collapse these reason codes exist to prevent.
1287
+
1288
+ The class had no JSDoc saying where to read it. It now states the access shape, names the contrast, and carries a worked `catch` block, so the answer is in the shipped `.d.ts` where a caller meets it.
1289
+
1290
+ Documentation only — no type, value, or behaviour change.
1291
+
1292
+ Found by noy-db-docs, which had documented `err.reason` for this class **two lines below its own sentence warning that the resemblance to `TamperedError.reason` is "a trap worth naming"**. It identified the trap and fell into it one level earlier than it was looking.
1293
+
1294
+ Hub's own prose was swept and is clean: every `.reason` reference in the changelog is to `TamperedError.reason`, which is correct for that class.
1295
+
1296
+ - Fix the getting-started example in hub's module JSDoc, which ships inside
1297
+ `dist/index.d.ts`. It omitted the required `user` option and passed `secret` to
1298
+ `openVault()`, where it is not accepted — `secret` is a `createNoydb()` option.
1299
+ Compiled verbatim, the published snippet produced `TS2741` and `TS2353`.
1300
+
1301
+ `packages/hub/README.md` carried the same class of defect: it taught
1302
+ `userId: 'alice'` (the option is `user`) and imported `memory` from
1303
+ `@noy-db/to-memory` (renamed to `toMemory`). Both ship in the tarball.
1304
+
1305
+ Guarded going forward by `pnpm check:prose-examples`, which typechecks every
1306
+ fenced `ts` block in shipped prose against the built `dist`. `check:prose-api`
1307
+ verifies that a documented method _exists_; these defects all named methods that
1308
+ do exist and passed arguments that do not, which only a compiler can see.
1309
+
1310
+ ## 0.7.0-pre.10
1311
+
1312
+ ### Patch Changes
1313
+
1314
+ - Export `assertRosterEpochCurrent` and `nextRosterEpoch` — they shipped in `0.7.0-pre.9` reachable from nothing (#1097 follow-up).
1315
+
1316
+ `0.7.0-pre.9` added the roster epoch and **listed `assertRosterEpochCurrent` in its release notes under "what is new"**. The function was present in `dist/**/*.d.ts` and exported from **no entry point** — all 53 were enumerated and imported; none had it.
1317
+
1318
+ That is the whole mechanism's caller half. Hub stamps and binds the epoch, but the expectation arrives by a second channel hub does not carry, so **only application code can perform the comparison**. A consumer following the release notes could not import the function they were told to call.
1319
+
1320
+ Both are now exported from the root barrel, beside `assertRosterAuthenticated`.
1321
+
1322
+ ## The class, and why it is worse than #1224
1323
+
1324
+ #1224 was a predicate reachable from the **wrong** seam — the root barrel rather than `/to`, which is all a store binds. This one was reachable from **none**. Both were found the same way: someone tried to follow the documentation.
1325
+
1326
+ Guarded by an invariant rather than by adding two names: a test asserts that **every** exported member of `roster-epoch.ts` reaches a published barrel, and that the query finds something in the first place. A helper added to that module later and exported from nothing fails the test rather than shipping unreachable.
1327
+
1328
+ Additive — the root barrel's golden baseline moves by two entries; nothing is removed or renamed.
1329
+
3
1330
  ## 0.7.0-pre.9
4
1331
 
5
1332
  ### Minor Changes