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