@noy-db/hub 0.2.0-pre.17 → 0.2.0-pre.18

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 (284) hide show
  1. package/dist/aggregate/index.cjs.map +1 -1
  2. package/dist/aggregate/index.d.cts +3 -3
  3. package/dist/aggregate/index.d.ts +3 -3
  4. package/dist/aggregate/index.js +4 -4
  5. package/dist/attestation/index.cjs.map +1 -1
  6. package/dist/attestation/index.d.cts +4 -4
  7. package/dist/attestation/index.d.ts +4 -4
  8. package/dist/attestation/index.js +6 -6
  9. package/dist/blobs/index.cjs.map +1 -1
  10. package/dist/blobs/index.d.cts +5 -5
  11. package/dist/blobs/index.d.ts +5 -5
  12. package/dist/blobs/index.js +6 -6
  13. package/dist/bundle/index.cjs +994 -273
  14. package/dist/bundle/index.cjs.map +1 -1
  15. package/dist/bundle/index.d.cts +6 -6
  16. package/dist/bundle/index.d.ts +6 -6
  17. package/dist/bundle/index.js +10 -10
  18. package/dist/{chunk-SHX5QBCI.js → chunk-2U226RDC.js} +3 -3
  19. package/dist/{chunk-7H2GEJ3O.js → chunk-32XVU2LT.js} +3 -3
  20. package/dist/{chunk-XJV6OB4D.js → chunk-33DAO2XG.js} +2 -2
  21. package/dist/{chunk-U5QCMH3W.js → chunk-45643PAU.js} +4 -4
  22. package/dist/{chunk-BH3X5L6A.js → chunk-4UI5T3K7.js} +3 -3
  23. package/dist/{chunk-2FU2FTXD.js → chunk-5KKNBDCT.js} +2 -2
  24. package/dist/{chunk-HGVSHKZW.js → chunk-647TFNYL.js} +30 -7
  25. package/dist/chunk-647TFNYL.js.map +1 -0
  26. package/dist/{chunk-CD2AVTEM.js → chunk-6FHCU3QO.js} +5 -5
  27. package/dist/{chunk-NBBMMJ2H.js → chunk-6Q5XRLKG.js} +4 -4
  28. package/dist/{chunk-XPIHJ34I.js → chunk-6XEGHIBA.js} +4 -4
  29. package/dist/{chunk-5LIROIDM.js → chunk-6YEC7LLO.js} +2 -2
  30. package/dist/{chunk-C3HYQPV4.js → chunk-AB7JF2KF.js} +2 -2
  31. package/dist/{chunk-UMLVJTYV.js → chunk-ADB7GPM3.js} +7 -4
  32. package/dist/chunk-ADB7GPM3.js.map +1 -0
  33. package/dist/{chunk-KCEHMDZF.js → chunk-BUBJYIZ7.js} +3 -3
  34. package/dist/{chunk-I5IUYN7B.js → chunk-C2OYWD5S.js} +3 -3
  35. package/dist/{chunk-WV7WV6JO.js → chunk-CMISAJAE.js} +5 -5
  36. package/dist/{chunk-SNMJ7SB3.js → chunk-DKMPR76W.js} +5 -5
  37. package/dist/{chunk-XMVHEWF6.js → chunk-DR5I7Q6N.js} +4 -4
  38. package/dist/{chunk-D77ZQSQQ.js → chunk-F2IJ2HGD.js} +652 -152
  39. package/dist/chunk-F2IJ2HGD.js.map +1 -0
  40. package/dist/{chunk-BSZOCSDZ.js → chunk-FQRAYDS4.js} +4 -4
  41. package/dist/{chunk-ZEGSDPB7.js → chunk-HMFC6M2G.js} +19 -1
  42. package/dist/chunk-HMFC6M2G.js.map +1 -0
  43. package/dist/{chunk-M476FOQ7.js → chunk-HOO5I3VG.js} +2 -2
  44. package/dist/{chunk-F4G63NTZ.js → chunk-HWK75CYX.js} +2 -2
  45. package/dist/{chunk-FEJDVE3Z.js → chunk-HZOEBM67.js} +2 -2
  46. package/dist/{chunk-QHM6XEAH.js → chunk-IQ4GMEYZ.js} +6 -6
  47. package/dist/{chunk-5AXTH4QZ.js → chunk-K3NYRK7U.js} +2 -2
  48. package/dist/{chunk-ROPJVUG3.js → chunk-KOURQXIU.js} +5 -5
  49. package/dist/chunk-KOURQXIU.js.map +1 -0
  50. package/dist/{chunk-GP3SDSH2.js → chunk-KQ523X3A.js} +15 -2
  51. package/dist/chunk-KQ523X3A.js.map +1 -0
  52. package/dist/{chunk-3G3W65EQ.js → chunk-KTZ2MHQK.js} +2 -2
  53. package/dist/{chunk-SISBMAPO.js → chunk-LGPSCKWZ.js} +1 -1
  54. package/dist/chunk-LGPSCKWZ.js.map +1 -0
  55. package/dist/{chunk-E77UKJYL.js → chunk-LQ3GD5LL.js} +5 -5
  56. package/dist/{chunk-JDWE6JMX.js → chunk-M3H7VSRV.js} +2 -2
  57. package/dist/{chunk-DWEBTE2W.js → chunk-MGB67HKX.js} +4 -4
  58. package/dist/{chunk-AEIKD3PP.js → chunk-P57D4KBG.js} +3 -3
  59. package/dist/{chunk-YYVZYTWW.js → chunk-PGVEL5IZ.js} +3 -3
  60. package/dist/{chunk-UNTGHX5A.js → chunk-QJKZ5WUP.js} +2 -2
  61. package/dist/{chunk-BJSLBUJ7.js → chunk-QPJ7Z4L3.js} +2 -2
  62. package/dist/{chunk-NYSYPFXJ.js → chunk-RQFG2YSV.js} +3 -3
  63. package/dist/{chunk-J7RWBXFY.js → chunk-RZWQNMMP.js} +2 -2
  64. package/dist/{chunk-BL5GYANC.js → chunk-T4T5I5L6.js} +3 -3
  65. package/dist/{chunk-ZNGPEV5J.js → chunk-TFAN3NFD.js} +3 -3
  66. package/dist/{chunk-DYYYUW5D.js → chunk-TPOHMOGX.js} +2 -2
  67. package/dist/{chunk-XMHUK5PN.js → chunk-TTS3RWL5.js} +2 -2
  68. package/dist/{chunk-TIDXB5DF.js → chunk-VVDSDOVV.js} +4 -4
  69. package/dist/{chunk-WIAOUFFB.js → chunk-WZCG3EZ6.js} +2 -2
  70. package/dist/{chunk-QO6RGLLD.js → chunk-Y5XVB75E.js} +4 -4
  71. package/dist/chunk-YWYW2YNO.js +129 -0
  72. package/dist/chunk-YWYW2YNO.js.map +1 -0
  73. package/dist/{chunk-H2MRGONI.js → chunk-Z3BE5BRK.js} +2 -2
  74. package/dist/{chunk-ROVO6NPJ.js → chunk-Z3I2WNGF.js} +58 -3
  75. package/dist/chunk-Z3I2WNGF.js.map +1 -0
  76. package/dist/{state-vault-W2OEABNO.js → chunk-ZJ67TB4S.js} +24 -7
  77. package/dist/chunk-ZJ67TB4S.js.map +1 -0
  78. package/dist/consent/index.cjs.map +1 -1
  79. package/dist/consent/index.d.cts +5 -5
  80. package/dist/consent/index.d.ts +5 -5
  81. package/dist/consent/index.js +3 -3
  82. package/dist/{crypto-7BN2HDWG.js → crypto-FNK3XPCS.js} +3 -3
  83. package/dist/{delegation-MGH5SODX.js → delegation-FMXNUWE6.js} +5 -5
  84. package/dist/derivations/index.cjs +82 -2
  85. package/dist/derivations/index.cjs.map +1 -1
  86. package/dist/derivations/index.d.cts +6 -6
  87. package/dist/derivations/index.d.ts +6 -6
  88. package/dist/derivations/index.js +8 -6
  89. package/dist/{dev-unlock-iXbYFAWl.d.cts → dev-unlock-3_2b_vo6.d.cts} +1 -1
  90. package/dist/{dev-unlock-CI1ijTML.d.ts → dev-unlock-BMvwPr_E.d.ts} +1 -1
  91. package/dist/{errors-Dz64FA65.d.ts → errors-DUTlAt3Y.d.cts} +16 -1
  92. package/dist/{errors-Dz64FA65.d.cts → errors-DUTlAt3Y.d.ts} +16 -1
  93. package/dist/executor-IZ2NVXCY.js +11 -0
  94. package/dist/executor-THSEYEJG.js +8 -0
  95. package/dist/executor-WLFDUTOM.js +8 -0
  96. package/dist/{fanout-sidecar-FIJJ46YG.js → fanout-sidecar-JGHXAJO5.js} +2 -2
  97. package/dist/forget/index.js +4 -4
  98. package/dist/guards/index.cjs +80 -3
  99. package/dist/guards/index.cjs.map +1 -1
  100. package/dist/guards/index.d.cts +6 -6
  101. package/dist/guards/index.d.ts +6 -6
  102. package/dist/guards/index.js +8 -4
  103. package/dist/{hash-blk7Bkes.d.ts → hash-BThBJFO1.d.ts} +1 -1
  104. package/dist/{hash-tEcM5fnv.d.cts → hash-BnWnL9bQ.d.cts} +1 -1
  105. package/dist/history/index.cjs.map +1 -1
  106. package/dist/history/index.d.cts +6 -6
  107. package/dist/history/index.d.ts +6 -6
  108. package/dist/history/index.js +5 -5
  109. package/dist/i18n/index.cjs.map +1 -1
  110. package/dist/i18n/index.d.cts +5 -5
  111. package/dist/i18n/index.d.ts +5 -5
  112. package/dist/i18n/index.js +6 -6
  113. package/dist/{index-u-kWzSrL.d.cts → index-C6lgoUhK.d.cts} +40 -3
  114. package/dist/{index-DpU6KWof.d.ts → index-DP1JTWHZ.d.ts} +40 -3
  115. package/dist/index.cjs +1202 -322
  116. package/dist/index.cjs.map +1 -1
  117. package/dist/index.d.cts +14 -14
  118. package/dist/index.d.ts +14 -14
  119. package/dist/index.js +65 -47
  120. package/dist/index.js.map +1 -1
  121. package/dist/indexing/index.cjs.map +1 -1
  122. package/dist/indexing/index.js +4 -4
  123. package/dist/issue-R2MWQO6K.js +12 -0
  124. package/dist/{ledger-LFVLHE5H.js → ledger-GXC2YA3A.js} +5 -5
  125. package/dist/materialized-views/index.cjs.map +1 -1
  126. package/dist/materialized-views/index.d.cts +6 -6
  127. package/dist/materialized-views/index.d.ts +6 -6
  128. package/dist/materialized-views/index.js +7 -7
  129. package/dist/noydb-RJL6FQ4B.js +37 -0
  130. package/dist/overlay-views/index.cjs.map +1 -1
  131. package/dist/overlay-views/index.d.cts +6 -6
  132. package/dist/overlay-views/index.d.ts +6 -6
  133. package/dist/overlay-views/index.js +4 -4
  134. package/dist/periods/index.cjs.map +1 -1
  135. package/dist/periods/index.d.cts +5 -5
  136. package/dist/periods/index.d.ts +5 -5
  137. package/dist/periods/index.js +5 -5
  138. package/dist/{public-envelope-RXZNP3V6.js → public-envelope-HXOFHY4N.js} +4 -4
  139. package/dist/query/index.cjs +26 -3
  140. package/dist/query/index.cjs.map +1 -1
  141. package/dist/query/index.d.cts +3 -3
  142. package/dist/query/index.d.ts +3 -3
  143. package/dist/query/index.js +6 -6
  144. package/dist/read-only-facade-EX6WZZBP.js +7 -0
  145. package/dist/registry-3T2RZC5A.js +8 -0
  146. package/dist/registry-DMS7OKBM.js +8 -0
  147. package/dist/{registry-SECUWSGY.js → registry-WVXO6NH5.js} +3 -3
  148. package/dist/{revoke-B54H2S2W.js → revoke-7LCWE2AH.js} +6 -6
  149. package/dist/sealed-record/index.cjs.map +1 -1
  150. package/dist/sealed-record/index.d.cts +1 -1
  151. package/dist/sealed-record/index.d.ts +1 -1
  152. package/dist/sealed-record/index.js +2 -2
  153. package/dist/session/index.cjs.map +1 -1
  154. package/dist/session/index.d.cts +6 -6
  155. package/dist/session/index.d.ts +6 -6
  156. package/dist/session/index.js +3 -3
  157. package/dist/shadow/index.cjs.map +1 -1
  158. package/dist/shadow/index.d.cts +5 -5
  159. package/dist/shadow/index.d.ts +5 -5
  160. package/dist/shadow/index.js +2 -2
  161. package/dist/{signer-YSXZT574.js → signer-HAVDLGOK.js} +5 -5
  162. package/dist/snapshots/index.cjs.map +1 -1
  163. package/dist/snapshots/index.d.cts +5 -5
  164. package/dist/snapshots/index.d.ts +5 -5
  165. package/dist/snapshots/index.js +4 -4
  166. package/dist/{stale-TOA36SRK.js → stale-PGTEGJDI.js} +2 -2
  167. package/dist/state-vault-QKQKN3H3.js +14 -0
  168. package/dist/state-vault-QKQKN3H3.js.map +1 -0
  169. package/dist/store/index.cjs.map +1 -1
  170. package/dist/store/index.d.cts +5 -5
  171. package/dist/store/index.d.ts +5 -5
  172. package/dist/store/index.js +2 -2
  173. package/dist/{strategy-4M9jo172.d.ts → strategy-Diwh5lzS.d.ts} +1 -1
  174. package/dist/{strategy-CLC1j79g.d.cts → strategy-nuyN8K5N.d.cts} +1 -1
  175. package/dist/sync/index.cjs.map +1 -1
  176. package/dist/sync/index.d.cts +4 -4
  177. package/dist/sync/index.d.ts +4 -4
  178. package/dist/sync/index.js +4 -4
  179. package/dist/team/index.cjs.map +1 -1
  180. package/dist/team/index.d.cts +5 -5
  181. package/dist/team/index.d.ts +5 -5
  182. package/dist/team/index.js +8 -8
  183. package/dist/transition-guard--t3exQHF.d.cts +165 -0
  184. package/dist/transition-guard-BlI9Oy5K.d.ts +165 -0
  185. package/dist/tx/index.cjs.map +1 -1
  186. package/dist/tx/index.d.cts +5 -5
  187. package/dist/tx/index.d.ts +5 -5
  188. package/dist/tx/index.js +3 -3
  189. package/dist/{types-CrSpRDuG.d.cts → types-BpLPqyaO.d.cts} +487 -25
  190. package/dist/{types-CljIHm_J.d.ts → types-Diqc2caK.d.ts} +487 -25
  191. package/dist/{ulid-CWfL2Vfv.d.ts → ulid-B1zNV8r9.d.ts} +1 -1
  192. package/dist/{ulid-CrI7PPbA.d.cts → ulid-DNiRB4Mx.d.cts} +1 -1
  193. package/dist/util/index.cjs.map +1 -1
  194. package/dist/util/index.js +1 -1
  195. package/dist/{vault-group-DHAHFX2A.js → vault-group-DPZVFRI5.js} +182 -6
  196. package/dist/vault-group-DPZVFRI5.js.map +1 -0
  197. package/dist/{with-materialized-view-NzF71cG_.d.cts → with-materialized-view-BdH_A_r6.d.cts} +1 -1
  198. package/dist/{with-materialized-view-B892zYZV.d.ts → with-materialized-view-CzAgp_HJ.d.ts} +1 -1
  199. package/dist/{with-overlayed-view-CR6m7CHe.d.ts → with-overlayed-view-BJbqQnsR.d.ts} +1 -1
  200. package/dist/{with-overlayed-view-UI8qSGL4.d.cts → with-overlayed-view-C40rDPlu.d.cts} +1 -1
  201. package/dist/with-rollup-Bopu5UDZ.d.cts +47 -0
  202. package/dist/with-rollup-DrlGkxiE.d.ts +47 -0
  203. package/package.json +3 -3
  204. package/dist/chunk-D77ZQSQQ.js.map +0 -1
  205. package/dist/chunk-GP3SDSH2.js.map +0 -1
  206. package/dist/chunk-HGVSHKZW.js.map +0 -1
  207. package/dist/chunk-PDULVIBY.js +0 -63
  208. package/dist/chunk-PDULVIBY.js.map +0 -1
  209. package/dist/chunk-ROPJVUG3.js.map +0 -1
  210. package/dist/chunk-ROVO6NPJ.js.map +0 -1
  211. package/dist/chunk-SISBMAPO.js.map +0 -1
  212. package/dist/chunk-UMLVJTYV.js.map +0 -1
  213. package/dist/chunk-ZEGSDPB7.js.map +0 -1
  214. package/dist/executor-3W63Y44O.js +0 -11
  215. package/dist/executor-CFFWPWBJ.js +0 -8
  216. package/dist/executor-VDQQOR4F.js +0 -8
  217. package/dist/immutable-guard-B5M95nbq.d.ts +0 -82
  218. package/dist/immutable-guard-qN3zF8o1.d.cts +0 -82
  219. package/dist/issue-TTMGHQ2J.js +0 -12
  220. package/dist/noydb-36S6GQNC.js +0 -37
  221. package/dist/read-only-facade-ITU6L7BL.js +0 -7
  222. package/dist/registry-3YFLZ7WD.js +0 -8
  223. package/dist/registry-TGZISEWC.js +0 -8
  224. package/dist/state-vault-W2OEABNO.js.map +0 -1
  225. package/dist/vault-group-DHAHFX2A.js.map +0 -1
  226. package/dist/with-derivation-BZ2y4bzF.d.ts +0 -13
  227. package/dist/with-derivation-Bozs8DmD.d.cts +0 -13
  228. /package/dist/{chunk-SHX5QBCI.js.map → chunk-2U226RDC.js.map} +0 -0
  229. /package/dist/{chunk-7H2GEJ3O.js.map → chunk-32XVU2LT.js.map} +0 -0
  230. /package/dist/{chunk-XJV6OB4D.js.map → chunk-33DAO2XG.js.map} +0 -0
  231. /package/dist/{chunk-U5QCMH3W.js.map → chunk-45643PAU.js.map} +0 -0
  232. /package/dist/{chunk-BH3X5L6A.js.map → chunk-4UI5T3K7.js.map} +0 -0
  233. /package/dist/{chunk-2FU2FTXD.js.map → chunk-5KKNBDCT.js.map} +0 -0
  234. /package/dist/{chunk-CD2AVTEM.js.map → chunk-6FHCU3QO.js.map} +0 -0
  235. /package/dist/{chunk-NBBMMJ2H.js.map → chunk-6Q5XRLKG.js.map} +0 -0
  236. /package/dist/{chunk-XPIHJ34I.js.map → chunk-6XEGHIBA.js.map} +0 -0
  237. /package/dist/{chunk-5LIROIDM.js.map → chunk-6YEC7LLO.js.map} +0 -0
  238. /package/dist/{chunk-C3HYQPV4.js.map → chunk-AB7JF2KF.js.map} +0 -0
  239. /package/dist/{chunk-KCEHMDZF.js.map → chunk-BUBJYIZ7.js.map} +0 -0
  240. /package/dist/{chunk-I5IUYN7B.js.map → chunk-C2OYWD5S.js.map} +0 -0
  241. /package/dist/{chunk-WV7WV6JO.js.map → chunk-CMISAJAE.js.map} +0 -0
  242. /package/dist/{chunk-SNMJ7SB3.js.map → chunk-DKMPR76W.js.map} +0 -0
  243. /package/dist/{chunk-XMVHEWF6.js.map → chunk-DR5I7Q6N.js.map} +0 -0
  244. /package/dist/{chunk-BSZOCSDZ.js.map → chunk-FQRAYDS4.js.map} +0 -0
  245. /package/dist/{chunk-M476FOQ7.js.map → chunk-HOO5I3VG.js.map} +0 -0
  246. /package/dist/{chunk-F4G63NTZ.js.map → chunk-HWK75CYX.js.map} +0 -0
  247. /package/dist/{chunk-FEJDVE3Z.js.map → chunk-HZOEBM67.js.map} +0 -0
  248. /package/dist/{chunk-QHM6XEAH.js.map → chunk-IQ4GMEYZ.js.map} +0 -0
  249. /package/dist/{chunk-5AXTH4QZ.js.map → chunk-K3NYRK7U.js.map} +0 -0
  250. /package/dist/{chunk-3G3W65EQ.js.map → chunk-KTZ2MHQK.js.map} +0 -0
  251. /package/dist/{chunk-E77UKJYL.js.map → chunk-LQ3GD5LL.js.map} +0 -0
  252. /package/dist/{chunk-JDWE6JMX.js.map → chunk-M3H7VSRV.js.map} +0 -0
  253. /package/dist/{chunk-DWEBTE2W.js.map → chunk-MGB67HKX.js.map} +0 -0
  254. /package/dist/{chunk-AEIKD3PP.js.map → chunk-P57D4KBG.js.map} +0 -0
  255. /package/dist/{chunk-YYVZYTWW.js.map → chunk-PGVEL5IZ.js.map} +0 -0
  256. /package/dist/{chunk-UNTGHX5A.js.map → chunk-QJKZ5WUP.js.map} +0 -0
  257. /package/dist/{chunk-BJSLBUJ7.js.map → chunk-QPJ7Z4L3.js.map} +0 -0
  258. /package/dist/{chunk-NYSYPFXJ.js.map → chunk-RQFG2YSV.js.map} +0 -0
  259. /package/dist/{chunk-J7RWBXFY.js.map → chunk-RZWQNMMP.js.map} +0 -0
  260. /package/dist/{chunk-BL5GYANC.js.map → chunk-T4T5I5L6.js.map} +0 -0
  261. /package/dist/{chunk-ZNGPEV5J.js.map → chunk-TFAN3NFD.js.map} +0 -0
  262. /package/dist/{chunk-DYYYUW5D.js.map → chunk-TPOHMOGX.js.map} +0 -0
  263. /package/dist/{chunk-XMHUK5PN.js.map → chunk-TTS3RWL5.js.map} +0 -0
  264. /package/dist/{chunk-TIDXB5DF.js.map → chunk-VVDSDOVV.js.map} +0 -0
  265. /package/dist/{chunk-WIAOUFFB.js.map → chunk-WZCG3EZ6.js.map} +0 -0
  266. /package/dist/{chunk-QO6RGLLD.js.map → chunk-Y5XVB75E.js.map} +0 -0
  267. /package/dist/{chunk-H2MRGONI.js.map → chunk-Z3BE5BRK.js.map} +0 -0
  268. /package/dist/{crypto-7BN2HDWG.js.map → crypto-FNK3XPCS.js.map} +0 -0
  269. /package/dist/{delegation-MGH5SODX.js.map → delegation-FMXNUWE6.js.map} +0 -0
  270. /package/dist/{executor-3W63Y44O.js.map → executor-IZ2NVXCY.js.map} +0 -0
  271. /package/dist/{executor-CFFWPWBJ.js.map → executor-THSEYEJG.js.map} +0 -0
  272. /package/dist/{executor-VDQQOR4F.js.map → executor-WLFDUTOM.js.map} +0 -0
  273. /package/dist/{fanout-sidecar-FIJJ46YG.js.map → fanout-sidecar-JGHXAJO5.js.map} +0 -0
  274. /package/dist/{issue-TTMGHQ2J.js.map → issue-R2MWQO6K.js.map} +0 -0
  275. /package/dist/{ledger-LFVLHE5H.js.map → ledger-GXC2YA3A.js.map} +0 -0
  276. /package/dist/{noydb-36S6GQNC.js.map → noydb-RJL6FQ4B.js.map} +0 -0
  277. /package/dist/{public-envelope-RXZNP3V6.js.map → public-envelope-HXOFHY4N.js.map} +0 -0
  278. /package/dist/{read-only-facade-ITU6L7BL.js.map → read-only-facade-EX6WZZBP.js.map} +0 -0
  279. /package/dist/{registry-3YFLZ7WD.js.map → registry-3T2RZC5A.js.map} +0 -0
  280. /package/dist/{registry-SECUWSGY.js.map → registry-DMS7OKBM.js.map} +0 -0
  281. /package/dist/{registry-TGZISEWC.js.map → registry-WVXO6NH5.js.map} +0 -0
  282. /package/dist/{revoke-B54H2S2W.js.map → revoke-7LCWE2AH.js.map} +0 -0
  283. /package/dist/{signer-YSXZT574.js.map → signer-HAVDLGOK.js.map} +0 -0
  284. /package/dist/{stale-TOA36SRK.js.map → stale-PGTEGJDI.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -46,7 +46,7 @@ var init_types = __esm({
46
46
  });
47
47
 
48
48
  // src/errors.ts
49
- var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, PeriodClosedError, RecordLockedError, FieldFrozenError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, ScriptViolationError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, SnapshotNotFoundError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, SealedRecordExpiredError, SealedRecordMismatchError, RecordCekNotFoundError;
49
+ var NoydbError, DecryptionError, TamperedError, InvalidKeyError, KeyringCorruptError, NoAccessError, ReadOnlyError, ReadOnlyAtInstantError, ReadOnlyFrameError, PermissionDeniedError, ExportCapabilityError, KeyringExpiredError, ImportCapabilityError, StoreCapabilityError, PrivilegeEscalationError, ReservedVaultNameError, PeriodClosedError, RecordLockedError, FieldFrozenError, IllegalTransitionError, InvariantError, AmendmentForbiddenError, DirectoryDisabledError, TierNotGrantedError, ElevationExpiredError, AlreadyElevatedError, TierDemoteDeniedError, DelegationTargetMissingError, ConflictError, LedgerContentionError, SequenceContentionError, SequenceOfflineError, NumberingUncertaintyError, BundleVersionConflictError, NetworkError, NotFoundError, ValidationError, SchemaValidationError, SchemaUpdateError, NonAdditiveSchemaChangeError, SchemaLockedError, SchemaFenceError, MigrationRequiredError, QuiesceTimeoutError, GroupCardinalityError, IndexRequiredError, UniqueConstraintError, UnsupportedIndexOptionError, IndexWriteFailureError, BundleIntegrityError, BundleSealMismatchError, ReservedCollectionNameError, DictKeyMissingError, DictKeyInUseError, MissingTranslationError, LocaleNotSpecifiedError, ScriptViolationError, StaticDictReadonlyError, UnknownDictCodeError, TranslatorNotConfiguredError, BackupLedgerError, BackupCorruptedError, AttestationError, SessionExpiredError, SessionNotFoundError, SessionPolicyError, JoinTooLargeError, CrossJoinTooLargeError, CrossJoinSourceUnknownError, DanglingReferenceError, FilenameSanitizationError, PathEscapeError, DerivationCycleError, DerivationDepthError, DerivationOutputUnknownError, DerivationOutputShapeError, DerivationCapExceededError, MaterializedViewCycleError, MaterializedViewSourceUnknownError, MaterializedViewTooLargeError, MaterializedViewConfigError, OverlayBaseIsVirtualError, OverlayCollectionUnavailableError, OverlayNameCollisionError, OverlayIdMismatchError, SnapshotNotFoundError, UnknownShardError, ShardProvisioningError, CrossShardJoinError, VaultTemplateNotFoundError, ForgetStrategyNotConfiguredError, SealedRecordExpiredError, SealedRecordMismatchError, RecordCekNotFoundError;
50
50
  var init_errors = __esm({
51
51
  "src/errors.ts"() {
52
52
  "use strict";
@@ -245,6 +245,23 @@ var init_errors = __esm({
245
245
  this.fields = fields;
246
246
  }
247
247
  };
248
+ IllegalTransitionError = class extends NoydbError {
249
+ collection;
250
+ id;
251
+ from;
252
+ to;
253
+ constructor(collection, id, from, to) {
254
+ super(
255
+ "ILLEGAL_TRANSITION",
256
+ `Cannot transition ${collection}/${id} from "${from}" to "${to}" \u2014 not a declared arc. Use withTransactions({ amendment: true, reason }) with admin/owner role to override.`
257
+ );
258
+ this.name = "IllegalTransitionError";
259
+ this.collection = collection;
260
+ this.id = id;
261
+ this.from = from;
262
+ this.to = to;
263
+ }
264
+ };
248
265
  InvariantError = class extends NoydbError {
249
266
  constructor(message) {
250
267
  super("INVARIANT_VIOLATED", message);
@@ -496,14 +513,14 @@ var init_errors = __esm({
496
513
  recordId;
497
514
  fields;
498
515
  conflictingId;
499
- constructor(collection, recordId3, fields, conflictingId) {
516
+ constructor(collection, recordId4, fields, conflictingId) {
500
517
  super(
501
518
  "UNIQUE_CONSTRAINT",
502
- `Unique constraint on ${collection}.[${fields.join(", ")}] violated: record "${recordId3}" duplicates a value already held by "${conflictingId}".`
519
+ `Unique constraint on ${collection}.[${fields.join(", ")}] violated: record "${recordId4}" duplicates a value already held by "${conflictingId}".`
503
520
  );
504
521
  this.name = "UniqueConstraintError";
505
522
  this.collection = collection;
506
- this.recordId = recordId3;
523
+ this.recordId = recordId4;
507
524
  this.fields = fields;
508
525
  this.conflictingId = conflictingId;
509
526
  }
@@ -4637,14 +4654,17 @@ var init_read_only_facade = __esm({
4637
4654
  "use strict";
4638
4655
  ReadOnlyVaultFacade = class {
4639
4656
  _vault;
4640
- constructor(vault) {
4657
+ _layer;
4658
+ constructor(vault, layer = "read") {
4641
4659
  this._vault = vault;
4660
+ this._layer = layer;
4642
4661
  }
4643
4662
  collection(name) {
4644
4663
  const c = this._vault.collection(name);
4664
+ const layer = this._layer;
4645
4665
  return {
4646
- get: (id) => c.get(id),
4647
- list: () => c.list(),
4666
+ get: (id) => c.get(id, { _layer: layer }),
4667
+ list: () => c.list({ _layer: layer }),
4648
4668
  query: () => c.query()
4649
4669
  };
4650
4670
  }
@@ -4700,6 +4720,16 @@ var init_registry3 = __esm({
4700
4720
  if (fromExtra) fromExtra.push(reg);
4701
4721
  else this._bySource.set(extra, [reg]);
4702
4722
  }
4723
+ for (const t of spec.triggerBy ?? []) {
4724
+ const fromTrigger = this._bySource.get(t.collection);
4725
+ if (fromTrigger) fromTrigger.push(reg);
4726
+ else this._bySource.set(t.collection, [reg]);
4727
+ }
4728
+ if (spec.rollup) {
4729
+ const fromRollup = this._bySource.get(spec.rollup.from);
4730
+ if (fromRollup) fromRollup.push(reg);
4731
+ else this._bySource.set(spec.rollup.from, [reg]);
4732
+ }
4703
4733
  for (const key of outputKeys) {
4704
4734
  const output = spec.outputs[key];
4705
4735
  if (!output) continue;
@@ -4753,6 +4783,9 @@ var init_registry3 = __esm({
4753
4783
  for (const key of Object.keys(s.spec.outputs)) {
4754
4784
  const output = s.spec.outputs[key];
4755
4785
  if (!output) continue;
4786
+ if (output.shape === "record" && output.collection === s.spec.source && output.denorm !== void 0) {
4787
+ continue;
4788
+ }
4756
4789
  visit(output.collection);
4757
4790
  }
4758
4791
  }
@@ -4929,6 +4962,177 @@ var init_delegation = __esm({
4929
4962
  }
4930
4963
  });
4931
4964
 
4965
+ // src/federation/schema-manifest.ts
4966
+ function captureBlueprint(configure) {
4967
+ const recorded = [];
4968
+ const collectionStub = new Proxy(
4969
+ {},
4970
+ {
4971
+ get: () => () => collectionStub
4972
+ }
4973
+ );
4974
+ const proxy = new Proxy(
4975
+ {},
4976
+ {
4977
+ get: (_t, prop) => {
4978
+ if (prop === "collection") {
4979
+ return (name, opts) => {
4980
+ recorded.push({
4981
+ name,
4982
+ indexes: opts?.indexes ?? [],
4983
+ persistJsonSchema: !!opts?.persistJsonSchema
4984
+ });
4985
+ return collectionStub;
4986
+ };
4987
+ }
4988
+ return () => proxy;
4989
+ }
4990
+ }
4991
+ );
4992
+ configure(proxy);
4993
+ const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
4994
+ const indexes = {};
4995
+ const persistJsonSchema = [];
4996
+ for (const c of sorted) {
4997
+ indexes[c.name] = c.indexes;
4998
+ if (c.persistJsonSchema) persistJsonSchema.push(c.name);
4999
+ }
5000
+ return {
5001
+ // `persistJsonSchema` is already name-sorted: it is populated while
5002
+ // iterating `sorted` (collections in name order).
5003
+ collections: sorted.map((c) => c.name),
5004
+ indexes,
5005
+ persistJsonSchema
5006
+ };
5007
+ }
5008
+ function canonical(value) {
5009
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
5010
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5011
+ const obj = value;
5012
+ const keys = Object.keys(obj).sort();
5013
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
5014
+ }
5015
+ async function fingerprintBlueprint(bp) {
5016
+ return sha256Hex(new TextEncoder().encode(canonical(bp)));
5017
+ }
5018
+ var init_schema_manifest = __esm({
5019
+ "src/federation/schema-manifest.ts"() {
5020
+ "use strict";
5021
+ init_crypto();
5022
+ }
5023
+ });
5024
+
5025
+ // src/federation/state-vault.ts
5026
+ var state_vault_exports = {};
5027
+ __export(state_vault_exports, {
5028
+ STATE_VAULT_NAME: () => STATE_VAULT_NAME,
5029
+ StateManagementVault: () => StateManagementVault
5030
+ });
5031
+ var REGISTRY, MANIFEST, EVENTS, MIGRATION_STATUS, StateManagementVault;
5032
+ var init_state_vault = __esm({
5033
+ "src/federation/state-vault.ts"() {
5034
+ "use strict";
5035
+ init_schema_manifest();
5036
+ init_constants();
5037
+ init_ulid();
5038
+ init_constants();
5039
+ REGISTRY = "vaultRegistry";
5040
+ MANIFEST = "schemaManifest";
5041
+ EVENTS = "deploymentEvents";
5042
+ MIGRATION_STATUS = "migrationStatus";
5043
+ StateManagementVault = class _StateManagementVault {
5044
+ constructor(registry, schemaManifest, events, migrationStatus) {
5045
+ this.registry = registry;
5046
+ this.schemaManifest = schemaManifest;
5047
+ this.#events = events;
5048
+ this.#migrationStatus = migrationStatus;
5049
+ }
5050
+ registry;
5051
+ schemaManifest;
5052
+ /**
5053
+ * The append-only deployment-events log is kept truly private so the raw
5054
+ * mutable Collection is never surfaced — events may only be written via
5055
+ * `appendEvent` and read via `queryEvents`. (`registry` and
5056
+ * `schemaManifest` are deliberately public: consumers read and write them.)
5057
+ */
5058
+ #events;
5059
+ /** Per-shard fleet-migration progress (#271). Surfaced via typed methods only. */
5060
+ #migrationStatus;
5061
+ /** Idempotently open the reserved state vault and bind the control-plane collections. */
5062
+ static async open(db) {
5063
+ const vault = await db.openVault(STATE_VAULT_NAME);
5064
+ return new _StateManagementVault(
5065
+ vault.collection(REGISTRY),
5066
+ vault.collection(MANIFEST),
5067
+ vault.collection(EVENTS),
5068
+ vault.collection(MIGRATION_STATUS)
5069
+ );
5070
+ }
5071
+ /** Read one shard's migration status (or null). */
5072
+ async getMigrationStatus(vaultId) {
5073
+ return this.#migrationStatus.get(vaultId);
5074
+ }
5075
+ /** All migration-status rows (hydrates first). */
5076
+ async listMigrationStatus() {
5077
+ await this.#migrationStatus.list();
5078
+ return this.#migrationStatus.query().toArray();
5079
+ }
5080
+ /** Upsert one shard's migration status (keyed by vaultId). */
5081
+ async upsertMigrationStatus(row) {
5082
+ await this.#migrationStatus.put(row.vaultId, row);
5083
+ }
5084
+ /** Read-only query over the append-only deployment-events log. */
5085
+ queryEvents() {
5086
+ return this.#events.query();
5087
+ }
5088
+ /**
5089
+ * Append a deployment event with a fresh unique (ULID) id. This is the
5090
+ * only write path to the events log; no update/delete is exposed.
5091
+ * Callers should treat failures as non-fatal — this method does not
5092
+ * swallow errors, so wrap the call site in try/catch where appropriate.
5093
+ */
5094
+ async appendEvent(event) {
5095
+ const ts = event.ts ?? Date.now();
5096
+ const id = generateULID();
5097
+ await this.#events.put(id, { ...event, id, ts });
5098
+ }
5099
+ /**
5100
+ * Ensure a manifest row exists for `(templateName, template.version)`.
5101
+ * Safe to call repeatedly: the `fingerprint` is a deterministic hash of
5102
+ * the template's declared shape (stable across calls), though each call
5103
+ * refreshes `recordedAt`.
5104
+ */
5105
+ async recordManifest(templateName, template) {
5106
+ const bp = captureBlueprint(template.configure);
5107
+ const fingerprint = await fingerprintBlueprint(bp);
5108
+ await this.schemaManifest.put(`${templateName}:${template.version}`, {
5109
+ templateName,
5110
+ version: template.version,
5111
+ collections: bp.collections,
5112
+ indexes: bp.indexes,
5113
+ persistJsonSchema: bp.persistJsonSchema,
5114
+ fingerprint,
5115
+ recordedAt: Date.now()
5116
+ });
5117
+ return fingerprint;
5118
+ }
5119
+ /**
5120
+ * True when `template`'s current declared shape does not match the recorded
5121
+ * manifest for `(templateName, template.version)`. Because shards carry no
5122
+ * schema state independent of their template, this catches "a template's
5123
+ * shape changed without bumping `version`" — not independent per-shard drift.
5124
+ * A missing manifest is treated as drift (nothing to verify against).
5125
+ */
5126
+ async detectDrift(templateName, template) {
5127
+ const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
5128
+ if (!row) return true;
5129
+ const current = await fingerprintBlueprint(captureBlueprint(template.configure));
5130
+ return current !== row.fingerprint;
5131
+ }
5132
+ };
5133
+ }
5134
+ });
5135
+
4932
5136
  // src/federation/classify-skip.ts
4933
5137
  function classifyShardSkip(err) {
4934
5138
  return err instanceof NoAccessError ? "no-grant" : "error";
@@ -5213,6 +5417,7 @@ var SHARD_SEPARATOR, SAFE_PARTITION_KEY, VaultGroup, ShardedCollection, ShardedQ
5213
5417
  var init_vault_group = __esm({
5214
5418
  "src/federation/vault-group.ts"() {
5215
5419
  "use strict";
5420
+ init_state_vault();
5216
5421
  init_errors();
5217
5422
  init_constants();
5218
5423
  init_classify_skip();
@@ -5222,12 +5427,13 @@ var init_vault_group = __esm({
5222
5427
  SHARD_SEPARATOR = "--";
5223
5428
  SAFE_PARTITION_KEY = /^[A-Za-z0-9._-]+$/;
5224
5429
  VaultGroup = class {
5225
- constructor(db, name, registry, sharding, template) {
5430
+ constructor(db, name, registry, sharding, template, migrateOnOpen = false) {
5226
5431
  this.db = db;
5227
5432
  this.name = name;
5228
5433
  this.registry = registry;
5229
5434
  this.sharding = sharding;
5230
5435
  this.template = template;
5436
+ this.migrateOnOpen = migrateOnOpen;
5231
5437
  if (name.includes(SHARD_SEPARATOR)) {
5232
5438
  throw new ValidationError(
5233
5439
  `VaultGroup name "${name}" must not contain "--" (reserved shard vault-id separator).`
@@ -5239,6 +5445,7 @@ var init_vault_group = __esm({
5239
5445
  registry;
5240
5446
  sharding;
5241
5447
  template;
5448
+ migrateOnOpen;
5242
5449
  /** @internal — set when the group is managed (no explicit registry). */
5243
5450
  stateVault;
5244
5451
  /** @internal */
@@ -5272,8 +5479,22 @@ var init_vault_group = __esm({
5272
5479
  const rows = this.registry.query().toArray();
5273
5480
  return rows.filter((r) => r.group === this.name);
5274
5481
  }
5275
- /** Open an existing shard and apply the template. */
5482
+ /**
5483
+ * Open an existing shard and apply the template. When `migrateOnOpen` is set
5484
+ * (#271) and the shard's registry version is behind the template, its cutover
5485
+ * runs inline first — so a behind shard never surfaces a stale handle.
5486
+ */
5276
5487
  async openShard(partitionKey) {
5488
+ if (this.migrateOnOpen) {
5489
+ const row = await this.registry.get(this.registryId(partitionKey));
5490
+ if (row && row.schemaVersion < this.template.version) {
5491
+ await this.migrateShard(partitionKey);
5492
+ }
5493
+ }
5494
+ return this._openShardRaw(partitionKey);
5495
+ }
5496
+ /** @internal — open + configure with no migrate-on-open hook (used by the migration path itself to avoid recursion). */
5497
+ async _openShardRaw(partitionKey) {
5277
5498
  const vault = await this.db.openVault(this.shardVaultId(partitionKey), { create: false });
5278
5499
  this.template.configure(vault);
5279
5500
  return vault;
@@ -5351,6 +5572,161 @@ var init_vault_group = __esm({
5351
5572
  });
5352
5573
  return { eligible, skipped };
5353
5574
  }
5575
+ /** @internal — registered push-model cross-vault derivations (#271 Insight Vault). */
5576
+ crossVaultDerivations = [];
5577
+ /**
5578
+ * Register a push-model cross-vault derivation — the Insight Vault pattern
5579
+ * (#271, Layer 4). Drive it with {@link refreshInsights}.
5580
+ *
5581
+ * For each shard, `derive(records, ctx)` runs on that shard's `source`
5582
+ * records and its return value is written into the analytics
5583
+ * (`target.vault` / `target.collection`) vault, keyed by partition key —
5584
+ * one summary row per shard. The derivation runs in-process under THIS
5585
+ * group's `Noydb` (which already holds both the shard and Insight Vault
5586
+ * keyrings); the shard's decrypted records are reduced to a summary that is
5587
+ * re-encrypted under the Insight Vault's own DEK, so no shard ciphertext
5588
+ * crosses a DEK boundary.
5589
+ *
5590
+ * **Zero-knowledge note:** the Insight Vault backend sees aggregated
5591
+ * structure (totals, counts, timestamps) drawn from many shards — a weaker
5592
+ * ZK profile than the per-shard vaults. Opt-in; keep summaries to aggregate
5593
+ * scalars (no embeddings / no raw records).
5594
+ *
5595
+ * v1 is explicit-refresh (no write-path push); call `refreshInsights()`
5596
+ * after a batch of writes, or on a schedule.
5597
+ */
5598
+ withCrossVaultDerivation(spec) {
5599
+ this.crossVaultDerivations.push(spec);
5600
+ }
5601
+ /**
5602
+ * Run every registered {@link withCrossVaultDerivation}: read each eligible
5603
+ * shard's source records, derive a per-shard summary, and write it into the
5604
+ * Insight Vault keyed by partition key. Shards behind `minVersion`,
5605
+ * unprovisioned, or whose read errors are reported in `skippedVaults` and
5606
+ * are not written (a stale summary is never left behind for a failed shard).
5607
+ */
5608
+ async refreshInsights(options = {}) {
5609
+ if (this.crossVaultDerivations.length === 0) return { written: 0, skippedVaults: [] };
5610
+ const { eligible, skipped } = await this.resolveEligible(
5611
+ options.minVersion !== void 0 ? { minVersion: options.minVersion } : {}
5612
+ );
5613
+ let written = 0;
5614
+ for (const spec of this.crossVaultDerivations) {
5615
+ const results = await this.db.queryAcross(
5616
+ eligible.map((r) => r.vaultId),
5617
+ async (vault) => {
5618
+ this.template.configure(vault);
5619
+ return vault.collection(spec.source).list();
5620
+ },
5621
+ { create: false, ...options.concurrency !== void 0 ? { concurrency: options.concurrency } : {} }
5622
+ );
5623
+ const insight = await this.db.openVault(spec.target.vault);
5624
+ const out = insight.collection(spec.target.collection);
5625
+ for (let i = 0; i < eligible.length; i++) {
5626
+ const row = eligible[i];
5627
+ const res = results[i];
5628
+ if (!res || res.result === void 0) {
5629
+ skipped.push({ vaultId: row.vaultId, reason: "error", ...res?.error ? { error: res.error } : {} });
5630
+ continue;
5631
+ }
5632
+ const ctx = {
5633
+ vaultId: row.vaultId,
5634
+ partitionKey: row.partitionKey,
5635
+ schemaVersion: row.schemaVersion
5636
+ };
5637
+ const summary = spec.derive(res.result, ctx);
5638
+ await out.put(row.partitionKey, summary);
5639
+ written++;
5640
+ }
5641
+ }
5642
+ return { written, skippedVaults: skipped };
5643
+ }
5644
+ /** @internal — the control-plane vault for migration status; lazily opened. */
5645
+ async ensureStateVault() {
5646
+ if (!this.stateVault) this.stateVault = await StateManagementVault.open(this.db);
5647
+ return this.stateVault;
5648
+ }
5649
+ /**
5650
+ * Migrate ONE shard to the template's current version (#271 fleet runner,
5651
+ * per-shard step). Opens the shard (applying the template, which arms the
5652
+ * M12 cutover), drains schema-write detection, runs `vault.runSchemaCutover()`
5653
+ * (the per-vault drain-barrier-transform protocol), then advances the
5654
+ * registry row's `schemaVersion` and records `migration-status`. A shard
5655
+ * already at the template version is a no-op (`status: 'done'`, migrated 0).
5656
+ * Never throws on a cutover failure — it records `status: 'failed'` and
5657
+ * returns the row, so a fleet run continues past a bad shard.
5658
+ */
5659
+ async migrateShard(partitionKey) {
5660
+ const vaultId = this.shardVaultId(partitionKey);
5661
+ const row = await this.registry.get(this.registryId(partitionKey));
5662
+ if (!row) throw new UnknownShardError(partitionKey, this.name);
5663
+ const target = this.template.version;
5664
+ const sv = await this.ensureStateVault();
5665
+ const base = { vaultId, group: this.name, currentVersion: row.schemaVersion, targetVersion: target };
5666
+ if (row.schemaVersion >= target) {
5667
+ const done = { ...base, status: "done", migrated: 0, finishedAt: Date.now() };
5668
+ await sv.upsertMigrationStatus(done);
5669
+ return done;
5670
+ }
5671
+ await sv.upsertMigrationStatus({ ...base, status: "running", startedAt: Date.now() });
5672
+ try {
5673
+ await sv.appendEvent({ type: "migration-started", group: this.name, vaultId, version: target });
5674
+ } catch {
5675
+ }
5676
+ try {
5677
+ const vault = await this._openShardRaw(partitionKey);
5678
+ await vault._drainPendingSchemaWrites();
5679
+ const { migrated } = await vault.runSchemaCutover();
5680
+ await this.registry.put(this.registryId(partitionKey), { ...row, schemaVersion: target });
5681
+ const done = { ...base, currentVersion: target, status: "done", migrated, finishedAt: Date.now() };
5682
+ await sv.upsertMigrationStatus(done);
5683
+ try {
5684
+ await sv.appendEvent({ type: "migration-completed", group: this.name, vaultId, version: target });
5685
+ } catch {
5686
+ }
5687
+ return done;
5688
+ } catch (err) {
5689
+ const error = err instanceof Error ? err.message : String(err);
5690
+ const failed = { ...base, status: "failed", error, finishedAt: Date.now() };
5691
+ await sv.upsertMigrationStatus(failed);
5692
+ try {
5693
+ await sv.appendEvent({ type: "migration-failed", group: this.name, vaultId, version: target, detail: error });
5694
+ } catch {
5695
+ }
5696
+ return failed;
5697
+ }
5698
+ }
5699
+ /**
5700
+ * Active batch runner (#271): migrate every shard behind the template version
5701
+ * to it, in controlled batches. **Resumable + crash-safe** — shards already at
5702
+ * the target are skipped (the registry version is the source of truth), so a
5703
+ * re-run after a crash only picks up the unfinished + previously-failed shards.
5704
+ *
5705
+ * - `cohort` — restrict to these partition keys (the staged / canary rollout:
5706
+ * migrate a small cohort, verify the Insight Vault, then run the rest).
5707
+ * - `batchSize` — max shards migrated concurrently per batch (back-pressure).
5708
+ * Default 4. Batches run sequentially; shards within a batch run in parallel.
5709
+ */
5710
+ async migrateFleet(options = {}) {
5711
+ const target = this.template.version;
5712
+ const rows = await this.allRows();
5713
+ const cohort = options.cohort;
5714
+ const todo = rows.filter(
5715
+ (r) => r.schemaVersion < target && (cohort === void 0 || cohort.includes(r.partitionKey))
5716
+ );
5717
+ const batchSize = Math.max(1, options.batchSize ?? 4);
5718
+ const migrated = [];
5719
+ const failed = [];
5720
+ for (let i = 0; i < todo.length; i += batchSize) {
5721
+ const batch = todo.slice(i, i + batchSize);
5722
+ const settled = await Promise.all(batch.map((r) => this.migrateShard(r.partitionKey)));
5723
+ for (const res of settled) {
5724
+ if (res.status === "done") migrated.push(res.vaultId);
5725
+ else failed.push({ vaultId: res.vaultId, error: res.error ?? "unknown" });
5726
+ }
5727
+ }
5728
+ return { target, migrated, failed };
5729
+ }
5354
5730
  };
5355
5731
  ShardedCollection = class {
5356
5732
  constructor(group, collectionName) {
@@ -5558,159 +5934,6 @@ var init_vault_group = __esm({
5558
5934
  }
5559
5935
  });
5560
5936
 
5561
- // src/federation/schema-manifest.ts
5562
- function captureBlueprint(configure) {
5563
- const recorded = [];
5564
- const collectionStub = new Proxy(
5565
- {},
5566
- {
5567
- get: () => () => collectionStub
5568
- }
5569
- );
5570
- const proxy = new Proxy(
5571
- {},
5572
- {
5573
- get: (_t, prop) => {
5574
- if (prop === "collection") {
5575
- return (name, opts) => {
5576
- recorded.push({
5577
- name,
5578
- indexes: opts?.indexes ?? [],
5579
- persistJsonSchema: !!opts?.persistJsonSchema
5580
- });
5581
- return collectionStub;
5582
- };
5583
- }
5584
- return () => proxy;
5585
- }
5586
- }
5587
- );
5588
- configure(proxy);
5589
- const sorted = [...recorded].sort((a, b) => a.name.localeCompare(b.name));
5590
- const indexes = {};
5591
- const persistJsonSchema = [];
5592
- for (const c of sorted) {
5593
- indexes[c.name] = c.indexes;
5594
- if (c.persistJsonSchema) persistJsonSchema.push(c.name);
5595
- }
5596
- return {
5597
- // `persistJsonSchema` is already name-sorted: it is populated while
5598
- // iterating `sorted` (collections in name order).
5599
- collections: sorted.map((c) => c.name),
5600
- indexes,
5601
- persistJsonSchema
5602
- };
5603
- }
5604
- function canonical(value) {
5605
- if (value === null || typeof value !== "object") return JSON.stringify(value);
5606
- if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
5607
- const obj = value;
5608
- const keys = Object.keys(obj).sort();
5609
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonical(obj[k])}`).join(",")}}`;
5610
- }
5611
- async function fingerprintBlueprint(bp) {
5612
- return sha256Hex(new TextEncoder().encode(canonical(bp)));
5613
- }
5614
- var init_schema_manifest = __esm({
5615
- "src/federation/schema-manifest.ts"() {
5616
- "use strict";
5617
- init_crypto();
5618
- }
5619
- });
5620
-
5621
- // src/federation/state-vault.ts
5622
- var state_vault_exports = {};
5623
- __export(state_vault_exports, {
5624
- STATE_VAULT_NAME: () => STATE_VAULT_NAME,
5625
- StateManagementVault: () => StateManagementVault
5626
- });
5627
- var REGISTRY, MANIFEST, EVENTS, StateManagementVault;
5628
- var init_state_vault = __esm({
5629
- "src/federation/state-vault.ts"() {
5630
- "use strict";
5631
- init_schema_manifest();
5632
- init_constants();
5633
- init_ulid();
5634
- init_constants();
5635
- REGISTRY = "vaultRegistry";
5636
- MANIFEST = "schemaManifest";
5637
- EVENTS = "deploymentEvents";
5638
- StateManagementVault = class _StateManagementVault {
5639
- constructor(registry, schemaManifest, events) {
5640
- this.registry = registry;
5641
- this.schemaManifest = schemaManifest;
5642
- this.#events = events;
5643
- }
5644
- registry;
5645
- schemaManifest;
5646
- /**
5647
- * The append-only deployment-events log is kept truly private so the raw
5648
- * mutable Collection is never surfaced — events may only be written via
5649
- * `appendEvent` and read via `queryEvents`. (`registry` and
5650
- * `schemaManifest` are deliberately public: consumers read and write them.)
5651
- */
5652
- #events;
5653
- /** Idempotently open the reserved state vault and bind the three control-plane collections. */
5654
- static async open(db) {
5655
- const vault = await db.openVault(STATE_VAULT_NAME);
5656
- return new _StateManagementVault(
5657
- vault.collection(REGISTRY),
5658
- vault.collection(MANIFEST),
5659
- vault.collection(EVENTS)
5660
- );
5661
- }
5662
- /** Read-only query over the append-only deployment-events log. */
5663
- queryEvents() {
5664
- return this.#events.query();
5665
- }
5666
- /**
5667
- * Append a deployment event with a fresh unique (ULID) id. This is the
5668
- * only write path to the events log; no update/delete is exposed.
5669
- * Callers should treat failures as non-fatal — this method does not
5670
- * swallow errors, so wrap the call site in try/catch where appropriate.
5671
- */
5672
- async appendEvent(event) {
5673
- const ts = event.ts ?? Date.now();
5674
- const id = generateULID();
5675
- await this.#events.put(id, { ...event, id, ts });
5676
- }
5677
- /**
5678
- * Ensure a manifest row exists for `(templateName, template.version)`.
5679
- * Safe to call repeatedly: the `fingerprint` is a deterministic hash of
5680
- * the template's declared shape (stable across calls), though each call
5681
- * refreshes `recordedAt`.
5682
- */
5683
- async recordManifest(templateName, template) {
5684
- const bp = captureBlueprint(template.configure);
5685
- const fingerprint = await fingerprintBlueprint(bp);
5686
- await this.schemaManifest.put(`${templateName}:${template.version}`, {
5687
- templateName,
5688
- version: template.version,
5689
- collections: bp.collections,
5690
- indexes: bp.indexes,
5691
- persistJsonSchema: bp.persistJsonSchema,
5692
- fingerprint,
5693
- recordedAt: Date.now()
5694
- });
5695
- return fingerprint;
5696
- }
5697
- /**
5698
- * True when `template`'s current declared shape does not match the recorded
5699
- * manifest for `(templateName, template.version)`. Because shards carry no
5700
- * schema state independent of their template, this catches "a template's
5701
- * shape changed without bumping `version`" — not independent per-shard drift.
5702
- * A missing manifest is treated as drift (nothing to verify against).
5703
- */
5704
- async detectDrift(templateName, template) {
5705
- const row = await this.schemaManifest.get(`${templateName}:${template.version}`);
5706
- if (!row) return true;
5707
- const current = await fingerprintBlueprint(captureBlueprint(template.configure));
5708
- return current !== row.fingerprint;
5709
- }
5710
- };
5711
- }
5712
- });
5713
-
5714
5937
  // src/index.ts
5715
5938
  var src_exports = {};
5716
5939
  __export(src_exports, {
@@ -5773,6 +5996,7 @@ __export(src_exports, {
5773
5996
  GroupedQuery: () => GroupedQuery,
5774
5997
  GroupedQueryN: () => GroupedQueryN,
5775
5998
  INDEXED_STORE_POLICY: () => INDEXED_STORE_POLICY,
5999
+ IllegalTransitionError: () => IllegalTransitionError,
5776
6000
  ImportCapabilityError: () => ImportCapabilityError,
5777
6001
  IndexRequiredError: () => IndexRequiredError,
5778
6002
  IndexWriteFailureError: () => IndexWriteFailureError,
@@ -5785,6 +6009,8 @@ __export(src_exports, {
5785
6009
  LEDGER_DELTAS_COLLECTION: () => LEDGER_DELTAS_COLLECTION,
5786
6010
  LedgerContentionError: () => LedgerContentionError,
5787
6011
  LedgerStore: () => LedgerStore,
6012
+ LinkEndpointError: () => LinkEndpointError,
6013
+ LinkIntegrityError: () => LinkIntegrityError,
5788
6014
  LocaleNotSpecifiedError: () => LocaleNotSpecifiedError,
5789
6015
  Lru: () => Lru,
5790
6016
  MAGIC_LINK_CONTENT_INFO_PREFIX: () => MAGIC_LINK_CONTENT_INFO_PREFIX,
@@ -5916,6 +6142,7 @@ __export(src_exports, {
5916
6142
  canonicalJson: () => canonicalJson,
5917
6143
  checkGate: () => checkGate,
5918
6144
  clearDevUnlock: () => clearDevUnlock,
6145
+ compileSequenceFormat: () => compileSequenceFormat,
5919
6146
  computePatch: () => computePatch,
5920
6147
  coordinatedCutover: () => coordinatedCutover,
5921
6148
  count: () => count,
@@ -5978,11 +6205,13 @@ __export(src_exports, {
5978
6205
  isDictKeyDescriptor: () => isDictKeyDescriptor,
5979
6206
  isDiscriminant: () => isDiscriminant,
5980
6207
  isI18nTextDescriptor: () => isI18nTextDescriptor,
6208
+ isLinkCollectionName: () => isLinkCollectionName,
5981
6209
  isMagicLinkGrantExpired: () => isMagicLinkGrantExpired,
5982
6210
  isMoneyDescriptor: () => isMoneyDescriptor,
5983
6211
  isMoneyString: () => isMoneyString,
5984
6212
  isPreCompressed: () => isPreCompressed,
5985
6213
  isPublicEnvelope: () => isPublicEnvelope,
6214
+ isRefArray: () => isRefArray,
5986
6215
  isSessionAlive: () => isSessionAlive,
5987
6216
  isStaticDictDescriptor: () => isStaticDictDescriptor,
5988
6217
  isULID: () => isULID,
@@ -6036,6 +6265,7 @@ __export(src_exports, {
6036
6265
  recoverUser: () => recoverUser,
6037
6266
  reduceRecords: () => reduceRecords,
6038
6267
  ref: () => ref,
6268
+ refArray: () => refArray,
6039
6269
  removeAuthenticator: () => removeAuthenticator,
6040
6270
  resetBrotliSupportCache: () => resetBrotliSupportCache,
6041
6271
  resetJoinWarnings: () => resetJoinWarnings,
@@ -6063,6 +6293,7 @@ __export(src_exports, {
6063
6293
  sha256Hex: () => sha256Hex3,
6064
6294
  staticDict: () => staticDict,
6065
6295
  sum: () => sum,
6296
+ transitionGuard: () => transitionGuard,
6066
6297
  unwrapDeksFromBlob: () => unwrapDeksFromBlob,
6067
6298
  unwrapDeksFromPaperEntry: () => unwrapDeksFromPaperEntry,
6068
6299
  unwrapDeksFromShamirEntry: () => unwrapDeksFromShamirEntry,
@@ -6086,6 +6317,7 @@ __export(src_exports, {
6086
6317
  withMetrics: () => withMetrics,
6087
6318
  withOverlayedView: () => withOverlayedView,
6088
6319
  withRetry: () => withRetry,
6320
+ withRollup: () => withRollup,
6089
6321
  wrapBundleStore: () => wrapBundleStore,
6090
6322
  wrapStore: () => wrapStore,
6091
6323
  writeMagicLinkGrant: () => writeMagicLinkGrant,
@@ -8454,6 +8686,38 @@ init_crypto();
8454
8686
  init_errors();
8455
8687
  var SEQUENCE_COLLECTION = "_sequences";
8456
8688
  var MAX_NEXT_ATTEMPTS = 16;
8689
+ var SEQ_FORMAT_TOKEN = /\{([^{}]*)\}/g;
8690
+ var SEQ_PAD_TOKEN = /^seq:0(\d+)$/;
8691
+ var SEQ_PARTITION_TOKEN = /^partition\.(\d+)$/;
8692
+ function compileSequenceFormat(format, series, partition) {
8693
+ const parts = partition ?? [];
8694
+ for (const m of format.matchAll(SEQ_FORMAT_TOKEN)) {
8695
+ const token = m[1] ?? "";
8696
+ if (token === "seq") continue;
8697
+ if (SEQ_PAD_TOKEN.test(token)) continue;
8698
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
8699
+ if (partMatch) {
8700
+ const idx = Number(partMatch[1]);
8701
+ if (idx >= parts.length) {
8702
+ throw new ValidationError(
8703
+ `sequence("${series}"): format token "{${token}}" references partition index ${idx}, but only ${parts.length} partition component(s) were supplied.`
8704
+ );
8705
+ }
8706
+ continue;
8707
+ }
8708
+ throw new ValidationError(
8709
+ `sequence("${series}"): format contains unknown token "{${token}}". Accepted tokens: {seq}, {seq:0N}, {partition.i}.`
8710
+ );
8711
+ }
8712
+ return (serial) => format.replace(SEQ_FORMAT_TOKEN, (full, token) => {
8713
+ if (token === "seq") return String(serial);
8714
+ const padMatch = SEQ_PAD_TOKEN.exec(token);
8715
+ if (padMatch) return String(serial).padStart(Number(padMatch[1]), "0");
8716
+ const partMatch = SEQ_PARTITION_TOKEN.exec(token);
8717
+ if (partMatch) return String(parts[Number(partMatch[1])]);
8718
+ return full;
8719
+ });
8720
+ }
8457
8721
  function resolveSequenceKey(series, opts) {
8458
8722
  const partition = opts?.partition;
8459
8723
  if (!partition || partition.length === 0) return series;
@@ -9476,15 +9740,15 @@ function isEquivalent(a, b) {
9476
9740
 
9477
9741
  // src/history/history.ts
9478
9742
  var HISTORY_COLLECTION = "_history";
9479
- function matchesPrefix(id, collection, recordId3) {
9480
- if (recordId3) {
9481
- return id.startsWith(`${collection}:${recordId3}:`);
9743
+ function matchesPrefix(id, collection, recordId4) {
9744
+ if (recordId4) {
9745
+ return id.startsWith(`${collection}:${recordId4}:`);
9482
9746
  }
9483
9747
  return id.startsWith(`${collection}:`);
9484
9748
  }
9485
- async function getHistory(adapter, vault, collection, recordId3, options) {
9749
+ async function getHistory(adapter, vault, collection, recordId4, options) {
9486
9750
  const allIds = await adapter.list(vault, HISTORY_COLLECTION);
9487
- const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId3)).sort().reverse();
9751
+ const matchingIds = allIds.filter((id) => matchesPrefix(id, collection, recordId4)).sort().reverse();
9488
9752
  const entries = [];
9489
9753
  for (const id of matchingIds) {
9490
9754
  const envelope = await adapter.get(vault, HISTORY_COLLECTION, id);
@@ -9737,6 +10001,9 @@ init_ledger();
9737
10001
 
9738
10002
  // src/refs.ts
9739
10003
  init_errors();
10004
+ function isRefArray(desc) {
10005
+ return desc.isArray === true;
10006
+ }
9740
10007
  var RefIntegrityError = class extends NoydbError {
9741
10008
  collection;
9742
10009
  id;
@@ -9773,6 +10040,17 @@ function ref(target, mode = "strict") {
9773
10040
  }
9774
10041
  return { target, mode };
9775
10042
  }
10043
+ function refArray(target, mode = "strict") {
10044
+ if (target.includes("/")) {
10045
+ throw new RefScopeError(target);
10046
+ }
10047
+ if (!target || target.startsWith("_")) {
10048
+ throw new Error(
10049
+ `refArray(): target collection name must be non-empty and cannot start with '_' (reserved for internal collections). Got "${target}".`
10050
+ );
10051
+ }
10052
+ return { target, mode, isArray: true };
10053
+ }
9776
10054
  var RefRegistry = class {
9777
10055
  outbound = /* @__PURE__ */ new Map();
9778
10056
  inbound = /* @__PURE__ */ new Map();
@@ -9797,7 +10075,7 @@ var RefRegistry = class {
9797
10075
  for (const k of existingKeys) {
9798
10076
  const a = existing[k];
9799
10077
  const b = refs[k];
9800
- if (!a || !b || a.target !== b.target || a.mode !== b.mode) {
10078
+ if (!a || !b || a.target !== b.target || a.mode !== b.mode || a.isArray !== b.isArray) {
9801
10079
  throw new Error(
9802
10080
  `RefRegistry: conflicting ref declarations for collection "${collection}" field "${k}"`
9803
10081
  );
@@ -9808,31 +10086,169 @@ var RefRegistry = class {
9808
10086
  this.outbound.set(collection, { ...refs });
9809
10087
  for (const [field, desc] of Object.entries(refs)) {
9810
10088
  const list = this.inbound.get(desc.target) ?? [];
9811
- list.push({ collection, field, mode: desc.mode });
10089
+ list.push({ collection, field, mode: desc.mode, ...desc.isArray ? { isArray: true } : {} });
9812
10090
  this.inbound.set(desc.target, list);
9813
10091
  }
9814
10092
  }
9815
- /** Get the outbound refs declared by a collection (or `{}` if none). */
9816
- getOutbound(collection) {
9817
- return this.outbound.get(collection) ?? {};
10093
+ /** Get the outbound refs declared by a collection (or `{}` if none). */
10094
+ getOutbound(collection) {
10095
+ return this.outbound.get(collection) ?? {};
10096
+ }
10097
+ /** Get the inbound refs that target a given collection (or `[]`). */
10098
+ getInbound(target) {
10099
+ return this.inbound.get(target) ?? [];
10100
+ }
10101
+ /**
10102
+ * Iterate every (collection → refs) pair that has at least one
10103
+ * declared reference. Used by `checkIntegrity` to walk the full
10104
+ * universe of outbound refs without needing to track collection
10105
+ * names elsewhere.
10106
+ */
10107
+ entries() {
10108
+ return [...this.outbound.entries()];
10109
+ }
10110
+ /** Clear the registry. Test-only escape hatch; never called from production code. */
10111
+ clear() {
10112
+ this.outbound.clear();
10113
+ this.inbound.clear();
10114
+ }
10115
+ };
10116
+
10117
+ // src/links/link-set.ts
10118
+ init_types();
10119
+ init_crypto();
10120
+ init_errors();
10121
+ var LINK_COLLECTION_PREFIX = "_links_";
10122
+ function linkCollectionName(name) {
10123
+ return `${LINK_COLLECTION_PREFIX}${name}`;
10124
+ }
10125
+ function isLinkCollectionName(name) {
10126
+ return name.startsWith(LINK_COLLECTION_PREFIX);
10127
+ }
10128
+ function linkRowKey(aId, bId) {
10129
+ return `${encodeURIComponent(aId)}|${encodeURIComponent(bId)}`;
10130
+ }
10131
+ var LinkSet = class {
10132
+ constructor(adapter, vault, name, spec, encrypted, getDEK, actor, emitter, endpointExists) {
10133
+ this.adapter = adapter;
10134
+ this.vault = vault;
10135
+ this.name = name;
10136
+ this.spec = spec;
10137
+ this.encrypted = encrypted;
10138
+ this.getDEK = getDEK;
10139
+ this.actor = actor;
10140
+ this.emitter = emitter;
10141
+ this.endpointExists = endpointExists;
10142
+ this.collName = linkCollectionName(name);
10143
+ }
10144
+ adapter;
10145
+ vault;
10146
+ name;
10147
+ spec;
10148
+ encrypted;
10149
+ getDEK;
10150
+ actor;
10151
+ emitter;
10152
+ endpointExists;
10153
+ collName;
10154
+ dekPromise = null;
10155
+ dek() {
10156
+ if (!this.dekPromise) this.dekPromise = this.getDEK(this.collName);
10157
+ return this.dekPromise;
10158
+ }
10159
+ async encryptEntry(entry, version) {
10160
+ const json = JSON.stringify(entry);
10161
+ const base = { _noydb: NOYDB_FORMAT_VERSION, _v: version, _ts: (/* @__PURE__ */ new Date()).toISOString(), _by: this.actor };
10162
+ if (!this.encrypted) return { ...base, _iv: "", _data: json };
10163
+ const { iv, data } = await encrypt(json, await this.dek());
10164
+ return { ...base, _iv: iv, _data: data };
10165
+ }
10166
+ async decryptEntry(env) {
10167
+ const json = this.encrypted ? await decrypt(env._iv, env._data, await this.dek()) : env._data;
10168
+ return JSON.parse(json);
10169
+ }
10170
+ async connect(aId, bId, meta) {
10171
+ if (!await this.endpointExists(this.spec.a, aId)) {
10172
+ throw new LinkEndpointError(this.name, this.spec.a, aId);
10173
+ }
10174
+ if (!await this.endpointExists(this.spec.b, bId)) {
10175
+ throw new LinkEndpointError(this.name, this.spec.b, bId);
10176
+ }
10177
+ const key = linkRowKey(aId, bId);
10178
+ const entry = meta !== void 0 ? { a: aId, b: bId, meta } : { a: aId, b: bId };
10179
+ const existing = await this.adapter.get(this.vault, this.collName, key);
10180
+ const env = await this.encryptEntry(entry, (existing?._v ?? 0) + 1);
10181
+ await this.adapter.put(this.vault, this.collName, key, env, existing?._v);
10182
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "put" });
10183
+ }
10184
+ async disconnect(aId, bId) {
10185
+ const key = linkRowKey(aId, bId);
10186
+ const existing = await this.adapter.get(this.vault, this.collName, key);
10187
+ if (!existing) return;
10188
+ await this.adapter.delete(this.vault, this.collName, key);
10189
+ this.emitter.emit("change", { vault: this.vault, collection: this.collName, id: key, action: "delete" });
10190
+ }
10191
+ async has(aId, bId) {
10192
+ return await this.adapter.get(this.vault, this.collName, linkRowKey(aId, bId)) !== null;
10193
+ }
10194
+ async of(id) {
10195
+ const rows = await this.list();
10196
+ return rows.filter((r) => r.a === id || r.b === id);
10197
+ }
10198
+ async list() {
10199
+ const keys = await this.adapter.list(this.vault, this.collName);
10200
+ const out = [];
10201
+ for (const key of keys) {
10202
+ const env = await this.adapter.get(this.vault, this.collName, key);
10203
+ if (!env) continue;
10204
+ const e = await this.decryptEntry(env);
10205
+ out.push(e.meta !== void 0 ? { a: e.a, b: e.b, meta: e.meta } : { a: e.a, b: e.b });
10206
+ }
10207
+ return out;
10208
+ }
10209
+ // ── Vault-internal cascade helpers ──────────────────────────────────
10210
+ /** @internal — rows where the deleted endpoint id matches the relevant slot. */
10211
+ async _rowsTouchingEndpoint(collection, id) {
10212
+ const rows = await this.list();
10213
+ return rows.filter(
10214
+ (r) => this.spec.a === collection && r.a === id || this.spec.b === collection && r.b === id
10215
+ );
9818
10216
  }
9819
- /** Get the inbound refs that target a given collection (or `[]`). */
9820
- getInbound(target) {
9821
- return this.inbound.get(target) ?? [];
10217
+ /** @internal the storage collection name (for tx pre-image capture). */
10218
+ get _collectionName() {
10219
+ return this.collName;
9822
10220
  }
9823
- /**
9824
- * Iterate every (collection refs) pair that has at least one
9825
- * declared reference. Used by `checkIntegrity` to walk the full
9826
- * universe of outbound refs without needing to track collection
9827
- * names elsewhere.
9828
- */
9829
- entries() {
9830
- return [...this.outbound.entries()];
10221
+ };
10222
+ var LinkEndpointError = class extends NoydbError {
10223
+ link;
10224
+ endpoint;
10225
+ missingId;
10226
+ constructor(link, endpoint, missingId) {
10227
+ super(
10228
+ "LINK_ENDPOINT",
10229
+ `link("${link}").connect: endpoint "${endpoint}" has no record "${missingId}".`
10230
+ );
10231
+ this.name = "LinkEndpointError";
10232
+ this.link = link;
10233
+ this.endpoint = endpoint;
10234
+ this.missingId = missingId;
9831
10235
  }
9832
- /** Clear the registry. Test-only escape hatch; never called from production code. */
9833
- clear() {
9834
- this.outbound.clear();
9835
- this.inbound.clear();
10236
+ };
10237
+ var LinkIntegrityError = class extends NoydbError {
10238
+ link;
10239
+ endpoint;
10240
+ id;
10241
+ count;
10242
+ constructor(link, endpoint, id, count2) {
10243
+ super(
10244
+ "LINK_INTEGRITY",
10245
+ `Cannot delete "${endpoint}"/"${id}": ${count2} link(s) in "${link}" still reference it (onDelete: 'strict').`
10246
+ );
10247
+ this.name = "LinkIntegrityError";
10248
+ this.link = link;
10249
+ this.endpoint = endpoint;
10250
+ this.id = id;
10251
+ this.count = count2;
9836
10252
  }
9837
10253
  };
9838
10254
 
@@ -12840,6 +13256,21 @@ function formatCurrency(decimal, currency, scale, locale) {
12840
13256
  });
12841
13257
  return fmt.format(decimal);
12842
13258
  }
13259
+ function moneyScaledValue(stored, desc) {
13260
+ let raw;
13261
+ if (desc.mode === "fixed") {
13262
+ raw = stored;
13263
+ } else {
13264
+ if (!isMoneyValueObject(stored)) return null;
13265
+ raw = stored.amount;
13266
+ }
13267
+ if (typeof raw !== "string" && typeof raw !== "number") return null;
13268
+ try {
13269
+ return BigInt(String(raw));
13270
+ } catch {
13271
+ return null;
13272
+ }
13273
+ }
12843
13274
  function decodeValue(stored, desc) {
12844
13275
  let currency;
12845
13276
  let scaledIntString;
@@ -13923,7 +14354,7 @@ function executePlanWithSource(source, plan, joinContext) {
13923
14354
  result = remainingClauses.length === 0 ? [...candidates] : filterRecords(candidates, remainingClauses, fnViewDecoder(source));
13924
14355
  }
13925
14356
  if (plan.orderBy.length > 0) {
13926
- result = sortRecords(result, plan.orderBy);
14357
+ result = sortRecords(result, plan.orderBy, source.moneyFields);
13927
14358
  }
13928
14359
  if (plan.offset > 0) {
13929
14360
  result = result.slice(plan.offset);
@@ -14080,17 +14511,25 @@ function applyCrossJoin(leftRel, clause, rightSource) {
14080
14511
  }
14081
14512
  return expanded;
14082
14513
  }
14083
- function sortRecords(records, orderBy) {
14514
+ function sortRecords(records, orderBy, moneyFields) {
14084
14515
  return [...records].sort((a, b) => {
14085
14516
  for (const { field, direction } of orderBy) {
14086
14517
  const av = readField(a, field);
14087
14518
  const bv = readField(b, field);
14088
- const cmp = compareValues(av, bv);
14519
+ const desc = moneyFields?.[field];
14520
+ const cmp = desc ? compareMoney(av, bv, desc) : compareValues(av, bv);
14089
14521
  if (cmp !== 0) return direction === "asc" ? cmp : -cmp;
14090
14522
  }
14091
14523
  return 0;
14092
14524
  });
14093
14525
  }
14526
+ function compareMoney(a, b, desc) {
14527
+ const av = moneyScaledValue(a, desc);
14528
+ const bv = moneyScaledValue(b, desc);
14529
+ if (av === null) return bv === null ? 0 : 1;
14530
+ if (bv === null) return -1;
14531
+ return av < bv ? -1 : av > bv ? 1 : 0;
14532
+ }
14094
14533
  function readField(record, field) {
14095
14534
  if (record === null || record === void 0) return void 0;
14096
14535
  if (!field.includes(".")) {
@@ -14879,8 +15318,8 @@ function coerceRefKey2(value) {
14879
15318
 
14880
15319
  // src/indexing/persisted-indexes.ts
14881
15320
  var IDX_PREFIX = "_idx/";
14882
- function encodeIdxId(field, recordId3) {
14883
- return `${IDX_PREFIX}${field}/${recordId3}`;
15321
+ function encodeIdxId(field, recordId4) {
15322
+ return `${IDX_PREFIX}${field}/${recordId4}`;
14884
15323
  }
14885
15324
  function decodeIdxId(id) {
14886
15325
  if (!id.startsWith(IDX_PREFIX)) return null;
@@ -14888,9 +15327,9 @@ function decodeIdxId(id) {
14888
15327
  const firstSlash = rest.indexOf("/");
14889
15328
  if (firstSlash <= 0) return null;
14890
15329
  const field = rest.slice(0, firstSlash);
14891
- const recordId3 = rest.slice(firstSlash + 1);
14892
- if (recordId3.length === 0) return null;
14893
- return { field, recordId: recordId3 };
15330
+ const recordId4 = rest.slice(firstSlash + 1);
15331
+ if (recordId4.length === 0) return null;
15332
+ return { field, recordId: recordId4 };
14894
15333
  }
14895
15334
 
14896
15335
  // src/indexing/lazy-builder.ts
@@ -15780,6 +16219,15 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
15780
16219
  }
15781
16220
 
15782
16221
  // src/collection.ts
16222
+ function selfWriteFieldEqual(a, b) {
16223
+ if (a === b) return true;
16224
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") return false;
16225
+ try {
16226
+ return JSON.stringify(a) === JSON.stringify(b);
16227
+ } catch {
16228
+ return false;
16229
+ }
16230
+ }
15783
16231
  var fallbackWarned = /* @__PURE__ */ new Set();
15784
16232
  function warnOnceFallback(adapterName) {
15785
16233
  if (fallbackWarned.has(adapterName)) return;
@@ -16714,6 +17162,111 @@ var Collection = class {
16714
17162
  * output (carries `_derivedFrom`) — defensive guard against missed
16715
17163
  * cycle detection.
16716
17164
  */
17165
+ /**
17166
+ * @internal #376 — the RAW stored record (canonical-money form, i18n maps
17167
+ * intact), WITHOUT the locale resolution `get()` applies. Used as the
17168
+ * patch base for self-write reverse-denorm so writing back never clobbers
17169
+ * an i18n map or re-quantizes money incorrectly. Returns null for
17170
+ * missing / tombstoned records.
17171
+ */
17172
+ async _getStoredRecord(id) {
17173
+ let raw;
17174
+ if (this.lazy && this.lru) {
17175
+ const cached = this.lru.get(id);
17176
+ if (cached) raw = cached.record;
17177
+ else {
17178
+ const env = await this.adapter.get(this.vault, this.name, id);
17179
+ if (!env || isTombstone(env, this.encrypted)) return null;
17180
+ raw = await this.decryptRecord(env, { id });
17181
+ if (raw === null) return null;
17182
+ this.lru.set(id, { record: raw, version: env._v }, estimateRecordBytes(raw));
17183
+ }
17184
+ } else {
17185
+ await this.ensureHydrated();
17186
+ raw = this.cache.get(id)?.record ?? null;
17187
+ }
17188
+ if (raw === null) return null;
17189
+ return canonicalizeStoredMoney(raw, this.moneyFields);
17190
+ }
17191
+ /**
17192
+ * @internal #376 — ids of records whose top-level `field` equals `value`.
17193
+ * Uses the FK index when the field is indexed (O(matches)); otherwise a
17194
+ * linear scan (O(N) — fine for small child sets; index the FK to scale).
17195
+ */
17196
+ async _findMatchingIds(field, value) {
17197
+ const hit = this.getIndexes()?.lookupEqual(field, value);
17198
+ if (hit) return [...hit];
17199
+ const target = String(value);
17200
+ const matches = (rec) => {
17201
+ const fv = rec[field];
17202
+ return (typeof fv === "string" || typeof fv === "number") && String(fv) === target;
17203
+ };
17204
+ if (!this.lazy) {
17205
+ await this.ensureHydrated();
17206
+ const out2 = [];
17207
+ for (const [rid, e] of this.cache) {
17208
+ if (matches(e.record)) out2.push(rid);
17209
+ }
17210
+ return out2;
17211
+ }
17212
+ const ids = await this.adapter.list(this.vault, this.name);
17213
+ const out = [];
17214
+ for (const rid of ids) {
17215
+ const raw = await this._getStoredRecord(rid);
17216
+ if (raw !== null && matches(raw)) out.push(rid);
17217
+ }
17218
+ return out;
17219
+ }
17220
+ /**
17221
+ * @internal #376 slice 2 — recompute a rollup aggregate onto the parent.
17222
+ * Gathers every child of `parentId`, runs `compute`, and patches only the
17223
+ * rollup `field` onto the parent's raw stored record (value-equality
17224
+ * guarded). No-op when the parent record does not exist.
17225
+ */
17226
+ async recomputeRollup(spec, parentId) {
17227
+ if (this.derivationSource === void 0 || spec.rollup === void 0) return;
17228
+ const { from, key, field, compute } = spec.rollup;
17229
+ const into = spec.source;
17230
+ const intoColl = this.derivationSource.getCollection(into);
17231
+ const base = await intoColl._getStoredRecord(parentId);
17232
+ if (base === null) return;
17233
+ const fromColl = this.derivationSource.getCollection(from);
17234
+ const childIds = await fromColl._findMatchingIds(key, parentId);
17235
+ const children = [];
17236
+ for (const cid of childIds) {
17237
+ const c = await fromColl.get(cid);
17238
+ if (c !== null && c !== void 0) children.push(c);
17239
+ }
17240
+ const newValue = compute(children);
17241
+ if (selfWriteFieldEqual(base[field], newValue)) return;
17242
+ const patched = { ...base, [field]: newValue };
17243
+ const txCtx = this.derivationSource.getActiveTxContext();
17244
+ if (txCtx !== null) {
17245
+ const prior = await this.adapter.get(this.vault, into, parentId);
17246
+ txCtx._executed.push({
17247
+ op: { type: "put", vaultName: this.vault, collectionName: into, id: parentId },
17248
+ priorEnvelope: prior
17249
+ });
17250
+ }
17251
+ await intoColl.put(parentId, patched);
17252
+ }
17253
+ /**
17254
+ * @internal #376 slice 2 — fire any rollups for which THIS collection is the
17255
+ * child `from`, recomputing the affected parent after a child delete. Called
17256
+ * from the delete path with the just-removed record's key value. Other
17257
+ * derivation kinds do not react to deletes (unchanged).
17258
+ */
17259
+ async dispatchRollupsOnDelete(deleted) {
17260
+ if (this.derivationSource === void 0) return;
17261
+ const registry = this.derivationSource.registry();
17262
+ const rec = deleted;
17263
+ for (const { spec } of registry.strategiesForSource(this.name)) {
17264
+ if (!spec.rollup || spec.rollup.from !== this.name) continue;
17265
+ const kv = rec[spec.rollup.key];
17266
+ if (typeof kv !== "string" && typeof kv !== "number") continue;
17267
+ await this.recomputeRollup(spec, String(kv));
17268
+ }
17269
+ }
16717
17270
  async dispatchDerivations(id, record, version) {
16718
17271
  if (this.derivationSource === void 0) return;
16719
17272
  const incoming = canonicalizeStoredMoney(record, this.moneyFields);
@@ -16724,29 +17277,60 @@ var Collection = class {
16724
17277
  let DerivationExecutor2 = null;
16725
17278
  for (const { spec, strategyHash } of strategies) {
16726
17279
  const mode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
16727
- if (mode === "eager") {
16728
- if (DerivationExecutor2 === null) {
16729
- ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2)));
16730
- }
16731
- let sourceWithId;
16732
- let sourceVersion = version;
16733
- if (spec.source === this.name) {
16734
- sourceWithId = { ...incoming, id };
17280
+ if (spec.rollup) {
17281
+ if (mode !== "eager") continue;
17282
+ let parentId;
17283
+ if (this.name === spec.rollup.from) {
17284
+ const kv = incoming[spec.rollup.key];
17285
+ parentId = typeof kv === "string" || typeof kv === "number" ? String(kv) : null;
16735
17286
  } else {
16736
- const primary = await this.derivationSource.getCollection(spec.source).get(id);
16737
- if (primary === null || primary === void 0) continue;
16738
- sourceWithId = { ...primary, id };
16739
- sourceVersion = 0;
17287
+ parentId = id;
17288
+ }
17289
+ if (parentId !== null) await this.recomputeRollup(spec, parentId);
17290
+ continue;
17291
+ }
17292
+ const isSource = spec.source === this.name;
17293
+ const isSibling = !isSource && (spec.sources?.includes(this.name) ?? false);
17294
+ const trigger = !isSource && !isSibling ? spec.triggerBy?.find((t) => t.collection === this.name) : void 0;
17295
+ const runs = [];
17296
+ if (isSource) {
17297
+ runs.push({ input: { ...incoming, id }, base: incoming, runId: id, version });
17298
+ } else if (isSibling) {
17299
+ const p = await this.derivationSource.getCollection(spec.source).get(id);
17300
+ if (p !== null && p !== void 0) {
17301
+ const raw = await this.derivationSource.getCollection(spec.source)._getStoredRecord(id);
17302
+ runs.push({ input: { ...p, id }, base: raw ?? p, runId: id, version: 0 });
17303
+ }
17304
+ } else if (trigger) {
17305
+ const srcColl = this.derivationSource.getCollection(spec.source);
17306
+ const ids = await srcColl._findMatchingIds(trigger.on, id);
17307
+ if (trigger.maxFanout !== void 0 && ids.length > trigger.maxFanout) {
17308
+ throw new DerivationCapExceededError(`triggerBy ${this.name}\u2192${spec.source}`, ids.length, trigger.maxFanout);
16740
17309
  }
17310
+ for (const sid of ids) {
17311
+ const raw = await srcColl._getStoredRecord(sid);
17312
+ if (raw === null) continue;
17313
+ runs.push({ input: { ...raw, id: sid }, base: raw, runId: sid, version: 0 });
17314
+ }
17315
+ }
17316
+ if (runs.length === 0) continue;
17317
+ if (mode !== "eager") {
17318
+ for (const run of runs) await markStale(registry, spec, run.runId);
17319
+ continue;
17320
+ }
17321
+ if (DerivationExecutor2 === null) {
17322
+ ({ DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2)));
17323
+ }
17324
+ for (const run of runs) {
16741
17325
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
16742
- const result = await DerivationExecutor2.run(spec, sourceWithId, sourceVersion, strategyHash, ctx);
17326
+ const result = await DerivationExecutor2.run(spec, run.input, run.version, strategyHash, ctx);
16743
17327
  for (const key of Object.keys(spec.outputs)) {
16744
17328
  const out = result.outputs[key];
16745
17329
  if (!out) continue;
16746
17330
  if (out.kind === "failed") {
16747
17331
  const err = out.error;
16748
17332
  if (spec.strict) throw err;
16749
- console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${id}" failed:`, err);
17333
+ console.warn(`[derivation] output "${key}" for source "${spec.source}" id="${run.runId}" failed:`, err);
16750
17334
  continue;
16751
17335
  }
16752
17336
  const outSpec = spec.outputs[key];
@@ -16759,7 +17343,7 @@ var Collection = class {
16759
17343
  this.adapter,
16760
17344
  this.vault,
16761
17345
  spec.source,
16762
- id,
17346
+ run.runId,
16763
17347
  key
16764
17348
  );
16765
17349
  const prevKeys = new Set(prior?.keys ?? []);
@@ -16786,7 +17370,7 @@ var Collection = class {
16786
17370
  }
16787
17371
  await saveFanoutSidecar2(this.adapter, this.vault, {
16788
17372
  source: spec.source,
16789
- sourceId: id,
17373
+ sourceId: run.runId,
16790
17374
  outputKey: key,
16791
17375
  outputCollection: outSpec.collection,
16792
17376
  keys: newKeysList
@@ -16794,25 +17378,44 @@ var Collection = class {
16794
17378
  continue;
16795
17379
  }
16796
17380
  if (out.skipped === true) {
16797
- await outputCollection._internalDelete(id, txCtx);
17381
+ await outputCollection._internalDelete(run.runId, txCtx);
17382
+ continue;
17383
+ }
17384
+ if (outSpec.shape === "record" && outSpec.denorm !== void 0 && outSpec.collection === spec.source) {
17385
+ const value = out.value;
17386
+ const patched = { ...run.base };
17387
+ let changed = false;
17388
+ for (const f of outSpec.denorm) {
17389
+ if (!selfWriteFieldEqual(run.base[f], value[f])) {
17390
+ patched[f] = value[f];
17391
+ changed = true;
17392
+ }
17393
+ }
17394
+ if (!changed) continue;
17395
+ if (txCtx !== null) {
17396
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
17397
+ txCtx._executed.push({
17398
+ op: { type: "put", vaultName: this.vault, collectionName: outSpec.collection, id: run.runId },
17399
+ priorEnvelope: prior
17400
+ });
17401
+ }
17402
+ await outputCollection.put(run.runId, patched);
16798
17403
  continue;
16799
17404
  }
16800
17405
  if (txCtx !== null) {
16801
- const prior = await this.adapter.get(this.vault, outSpec.collection, id);
17406
+ const prior = await this.adapter.get(this.vault, outSpec.collection, run.runId);
16802
17407
  txCtx._executed.push({
16803
17408
  op: {
16804
17409
  type: "put",
16805
17410
  vaultName: this.vault,
16806
17411
  collectionName: outSpec.collection,
16807
- id
17412
+ id: run.runId
16808
17413
  },
16809
17414
  priorEnvelope: prior
16810
17415
  });
16811
17416
  }
16812
- await outputCollection.put(id, out.value);
17417
+ await outputCollection.put(run.runId, out.value);
16813
17418
  }
16814
- } else {
16815
- await markStale(registry, spec, id);
16816
17419
  }
16817
17420
  }
16818
17421
  }
@@ -17029,6 +17632,7 @@ var Collection = class {
17029
17632
  if (!internal) {
17030
17633
  await this.dispatchMaterializedViewsOnDelete(id);
17031
17634
  await this.dispatchArrayDerivationsOnDelete(id);
17635
+ if (existing) await this.dispatchRollupsOnDelete(existing.record);
17032
17636
  }
17033
17637
  }
17034
17638
  /**
@@ -17883,12 +18487,12 @@ var Collection = class {
17883
18487
  }
17884
18488
  }
17885
18489
  persisted.clear();
17886
- for (const recordId3 of canonicalIds) {
17887
- const envelope = await this.adapter.get(this.vault, this.name, recordId3);
18490
+ for (const recordId4 of canonicalIds) {
18491
+ const envelope = await this.adapter.get(this.vault, this.name, recordId4);
17888
18492
  if (!envelope) continue;
17889
18493
  const record = await this.decryptRecord(envelope, { skipValidation: true });
17890
18494
  if (record === null) continue;
17891
- await this.maintainPersistedIndexesOnPut(recordId3, record, null, envelope._v);
18495
+ await this.maintainPersistedIndexesOnPut(recordId4, record, null, envelope._v);
17892
18496
  }
17893
18497
  this.persistedIndexesLoaded = true;
17894
18498
  }
@@ -18083,14 +18687,15 @@ var Collection = class {
18083
18687
  (d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
18084
18688
  );
18085
18689
  if (!locale && !hasStaticDisplay) return result;
18690
+ const layer = localeOpts?._layer ?? "read";
18086
18691
  if (locale && hasI18n && this.i18nFields) {
18087
- result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback);
18692
+ result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
18088
18693
  }
18089
18694
  if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
18090
18695
  const withLabels = { ...result };
18091
18696
  const resolver = this.dictLabelResolver;
18092
18697
  for (const [field, desc] of Object.entries(this.dictKeyFields)) {
18093
- const policy = desc.onMissing ? resolvePolicy(desc.onMissing, "read") : "null";
18698
+ const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
18094
18699
  const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
18095
18700
  const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
18096
18701
  const resolveKey = async (key) => {
@@ -19124,8 +19729,8 @@ var DeferredNumberingStore = class {
19124
19729
  }
19125
19730
  await this.adapter.put(this.vault, collection, id, env, expectedVersion);
19126
19731
  }
19127
- pendingId(series, recordId3) {
19128
- return `${series}::${recordId3}`;
19732
+ pendingId(series, recordId4) {
19733
+ return `${series}::${recordId4}`;
19129
19734
  }
19130
19735
  /** Current last-assigned serial for a series (0 if none). */
19131
19736
  async peek(series) {
@@ -19139,16 +19744,16 @@ var DeferredNumberingStore = class {
19139
19744
  * at the next pass (the record's `field` is the durable source of truth —
19140
19745
  * `assigned` is an in-process convenience that a crash may drop).
19141
19746
  */
19142
- async enqueue(series, recordId3) {
19747
+ async enqueue(series, recordId4) {
19143
19748
  const cfg = this.configs.get(series);
19144
19749
  if (!cfg) throw new NumberingUncertaintyError(series);
19145
19750
  if (typeof this.adapter.getStoreTime !== "function") throw new NumberingUncertaintyError(series);
19146
19751
  const st = await this.adapter.getStoreTime();
19147
- const id = this.pendingId(series, recordId3);
19752
+ const id = this.pendingId(series, recordId4);
19148
19753
  const { env } = await this.readJson(NUMBERING_PENDING_COLLECTION, id);
19149
19754
  const entry = {
19150
19755
  series,
19151
- recordId: recordId3,
19756
+ recordId: recordId4,
19152
19757
  collection: cfg.collection,
19153
19758
  field: cfg.field,
19154
19759
  storeEarliest: st.earliest,
@@ -19505,13 +20110,13 @@ async function runCompaction(ctx, options = {}) {
19505
20110
  collectionsWithPolicy += 1;
19506
20111
  byCollection[collectionName] = { records: 0, evicted: 0 };
19507
20112
  const ids = await ctx.listRecords(collectionName);
19508
- for (const recordId3 of ids) {
20113
+ for (const recordId4 of ids) {
19509
20114
  if (evicted >= maxEvictions) break outer;
19510
- const record = await ctx.getRecord(collectionName, recordId3).catch(() => null);
20115
+ const record = await ctx.getRecord(collectionName, recordId4).catch(() => null);
19511
20116
  if (record === null) continue;
19512
20117
  records += 1;
19513
20118
  byCollection[collectionName].records += 1;
19514
- const slots = await ctx.listSlots(collectionName, recordId3).catch(() => []);
20119
+ const slots = await ctx.listSlots(collectionName, recordId4).catch(() => []);
19515
20120
  for (const slot of slots) {
19516
20121
  if (evicted >= maxEvictions) break outer;
19517
20122
  const policy = config[slot.name];
@@ -19523,11 +20128,11 @@ async function runCompaction(ctx, options = {}) {
19523
20128
  continue;
19524
20129
  }
19525
20130
  if (!dryRun) {
19526
- await ctx.deleteSlot(collectionName, recordId3, slot.name);
20131
+ await ctx.deleteSlot(collectionName, recordId4, slot.name);
19527
20132
  await writeAuditEntry(ctx, {
19528
- id: generateEvictionId(collectionName, recordId3, slot.name),
20133
+ id: generateEvictionId(collectionName, recordId4, slot.name),
19529
20134
  collection: collectionName,
19530
- recordId: recordId3,
20135
+ recordId: recordId4,
19531
20136
  slotName: slot.name,
19532
20137
  blobHash: slot.eTag,
19533
20138
  reason,
@@ -19594,11 +20199,11 @@ function evaluatePolicy(policy, record, slot, now) {
19594
20199
  if (predicateTriggered) return "predicate";
19595
20200
  return null;
19596
20201
  }
19597
- function generateEvictionId(collection, recordId3, slotName) {
20202
+ function generateEvictionId(collection, recordId4, slotName) {
19598
20203
  const rand = globalThis.crypto.getRandomValues(new Uint8Array(8));
19599
20204
  let suffix = "";
19600
20205
  for (const b of rand) suffix += b.toString(16).padStart(2, "0");
19601
- return `${collection}__${recordId3}__${slotName}__${suffix}`;
20206
+ return `${collection}__${recordId4}__${slotName}__${suffix}`;
19602
20207
  }
19603
20208
  async function writeAuditEntry(ctx, entry) {
19604
20209
  const json = JSON.stringify(entry);
@@ -19649,7 +20254,7 @@ async function deriveMagicLinkContentKey(serverSecret, token, vault) {
19649
20254
  ["encrypt", "decrypt"]
19650
20255
  );
19651
20256
  }
19652
- async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId3, opts) {
20257
+ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek, recordId4, opts) {
19653
20258
  const collectionName = opts.collection ?? null;
19654
20259
  const sourceKey = collectionName ? dekKey(collectionName, opts.tier) : `__any#${opts.tier}`;
19655
20260
  const sourceDek = grantor.deks.get(sourceKey);
@@ -19662,7 +20267,7 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
19662
20267
  const until = typeof opts.until === "string" ? opts.until : opts.until.toISOString();
19663
20268
  const createdAt = (/* @__PURE__ */ new Date()).toISOString();
19664
20269
  const payload = {
19665
- id: recordId3,
20270
+ id: recordId4,
19666
20271
  toUser: opts.toUser,
19667
20272
  fromUser: grantor.userId,
19668
20273
  tier: opts.tier,
@@ -19682,11 +20287,11 @@ async function writeMagicLinkGrant(store, vault, grantor, contentKey, grantKek,
19682
20287
  _data: data,
19683
20288
  _by: grantor.userId
19684
20289
  };
19685
- await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId3, envelope);
19686
- return { recordId: recordId3, payload };
20290
+ await store.put(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId4, envelope);
20291
+ return { recordId: recordId4, payload };
19687
20292
  }
19688
- async function readMagicLinkGrantRecord(store, vault, contentKey, recordId3) {
19689
- const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId3);
20293
+ async function readMagicLinkGrantRecord(store, vault, contentKey, recordId4) {
20294
+ const env = await store.get(vault, MAGIC_LINK_GRANTS_COLLECTION, recordId4);
19690
20295
  if (!env) return null;
19691
20296
  try {
19692
20297
  const json = await decrypt(env._iv, env._data, contentKey);
@@ -20342,13 +20947,17 @@ var Vault = class {
20342
20947
  */
20343
20948
  overlayedViewRegistry = null;
20344
20949
  /**
20345
- * Cached read-only facade handed to guard callbacks via `ctx.vault`,
20346
- * and to derivation callbacks via `derive(source, ctx)`. Allocated
20347
- * eagerly inside `_initGuards()` and/or `_initDerivations()` so read
20950
+ * Cached read-only facades handed to guard callbacks via `ctx.vault`
20951
+ * and to derivation callbacks via `derive(source, ctx)`. Split by
20952
+ * resolution layer (#285): the guard facade reads at `layer:'guard'`,
20953
+ * the derivation facade at `layer:'derivation'`, so i18nText / dictKey
20954
+ * fields resolve under that layer's `onMissing` policy. Allocated
20955
+ * eagerly inside `_initGuards()` / `_initDerivations()` so read
20348
20956
  * accessors stay synchronous (callers in `tx/transaction.ts` rely on
20349
- * that). Stays `null` for vaults with neither subsystem configured.
20957
+ * that). Each stays `null` for vaults without that subsystem.
20350
20958
  */
20351
- readOnlyFacade = null;
20959
+ guardFacade = null;
20960
+ derivationFacade = null;
20352
20961
  getDEK;
20353
20962
  /**
20354
20963
  * Per-principal user envelope API.
@@ -20510,6 +21119,10 @@ var Vault = class {
20510
21119
  i18nFieldRegistry = /* @__PURE__ */ new Map();
20511
21120
  /** Cache of DictionaryHandle instances, one per dictionary name. */
20512
21121
  dictionaryCache = /* @__PURE__ */ new Map();
21122
+ /** Registered link specs (#377-B), keyed by link name; set by `vault.link()`. */
21123
+ linkRegistry = /* @__PURE__ */ new Map();
21124
+ /** Cache of LinkSet handles, one per link name. */
21125
+ linkSetCache = /* @__PURE__ */ new Map();
20513
21126
  /** — subscribers for cross-tier access events. */
20514
21127
  crossTierSubs = /* @__PURE__ */ new Set();
20515
21128
  /** — currently-active elevation, or null. One per vault. */
@@ -20626,6 +21239,9 @@ var Vault = class {
20626
21239
  if (collectionName === SEQUENCE_COLLECTION) {
20627
21240
  throw new ReservedCollectionNameError(collectionName);
20628
21241
  }
21242
+ if (isLinkCollectionName(collectionName)) {
21243
+ throw new ReservedCollectionNameError(collectionName);
21244
+ }
20629
21245
  let coll = this.collectionCache.get(collectionName);
20630
21246
  if (coll && options?.moneyFields) {
20631
21247
  coll._applyMoneyFields(options.moneyFields);
@@ -20697,6 +21313,7 @@ var Vault = class {
20697
21313
  }));
20698
21314
  schemaUpdateGate = new SchemaUpdateGate(work);
20699
21315
  }
21316
+ const effectiveHistoryConfig = options?.historyConfig ?? this.historyConfig;
20700
21317
  const collOpts = {
20701
21318
  adapter: this.adapter,
20702
21319
  vault: this.name,
@@ -20712,7 +21329,7 @@ var Vault = class {
20712
21329
  schemaFence: this.schemaFence,
20713
21330
  getDEK: this.getDEK,
20714
21331
  onDirty: this.onDirty,
20715
- historyConfig: this.historyConfig,
21332
+ historyConfig: effectiveHistoryConfig,
20716
21333
  // thread the vault-wide blob strategy into every
20717
21334
  // collection. `undefined` is intentionally preserved so the
20718
21335
  // Collection constructor uses its NO_BLOBS default.
@@ -20723,7 +21340,11 @@ var Vault = class {
20723
21340
  historyStrategy: this.historyStrategy,
20724
21341
  i18nStrategy: this.i18nStrategy,
20725
21342
  syncStrategy: this.syncStrategy,
20726
- ledger: this.getLedgerOrNull() ?? void 0,
21343
+ // Per-collection ledger opt-out (#361): when this collection sets
21344
+ // `historyConfig.ledger: false`, withhold the ledger reference so all
21345
+ // four `if (this.ledger)` append sites in Collection no-op. The chain
21346
+ // stays valid — it simply never receives this collection's entries.
21347
+ ledger: effectiveHistoryConfig.ledger === false ? void 0 : this.getLedgerOrNull() ?? void 0,
20727
21348
  refEnforcer: this,
20728
21349
  joinResolver: this,
20729
21350
  defaultLocale: this.locale,
@@ -21084,6 +21705,68 @@ var Vault = class {
21084
21705
  }
21085
21706
  return handle;
21086
21707
  }
21708
+ /**
21709
+ * Declare a managed many-to-many link set (#377-B). Registers a
21710
+ * `_links_<name>` junction between two endpoint collections; access its
21711
+ * rows via `vault.links(name)`. Idempotent for an identical re-declaration;
21712
+ * a conflicting one throws. See {@link links}.
21713
+ *
21714
+ * ```ts
21715
+ * vault.link('saleLineLinks', { a: ref('saleLines'), b: ref('purchaseLines'), onDelete: 'cascade' })
21716
+ * ```
21717
+ *
21718
+ * `a` / `b` accept either a collection name or a `ref(target)` descriptor
21719
+ * (only its `target` is used — links manage their own integrity). `onDelete`
21720
+ * governs what happens to link rows when an endpoint record is deleted
21721
+ * (`'cascade'` default, `'strict'`, `'warn'`).
21722
+ */
21723
+ link(name, spec) {
21724
+ const a = typeof spec.a === "string" ? spec.a : spec.a.target;
21725
+ const b = typeof spec.b === "string" ? spec.b : spec.b.target;
21726
+ for (const [slot, target] of [["a", a], ["b", b]]) {
21727
+ if (!target || target.startsWith("_") || target.includes("/")) {
21728
+ throw new ValidationError(
21729
+ `vault.link("${name}"): endpoint "${slot}" must be a simple collection name, got "${target}".`
21730
+ );
21731
+ }
21732
+ }
21733
+ const resolved = { a, b, ...spec.onDelete ? { onDelete: spec.onDelete } : {} };
21734
+ const existing = this.linkRegistry.get(name);
21735
+ if (existing) {
21736
+ if (existing.a !== resolved.a || existing.b !== resolved.b || (existing.onDelete ?? "cascade") !== (resolved.onDelete ?? "cascade")) {
21737
+ throw new ValidationError(`vault.link("${name}"): conflicting re-declaration.`);
21738
+ }
21739
+ return;
21740
+ }
21741
+ this.linkRegistry.set(name, resolved);
21742
+ }
21743
+ /**
21744
+ * Access a declared link set (#377-B). Throws if `name` was not first
21745
+ * declared via {@link link}. Returns a cached {@link LinkSetHandle}:
21746
+ * `connect(a, b, meta?)`, `disconnect(a, b)`, `has(a, b)`, `of(id)`, `list()`.
21747
+ */
21748
+ links(name) {
21749
+ let handle = this.linkSetCache.get(name);
21750
+ if (!handle) {
21751
+ const spec = this.linkRegistry.get(name);
21752
+ if (!spec) {
21753
+ throw new ValidationError(`vault.links("${name}"): not declared. Call vault.link("${name}", { a, b }) first.`);
21754
+ }
21755
+ handle = new LinkSet(
21756
+ this.adapter,
21757
+ this.name,
21758
+ name,
21759
+ spec,
21760
+ this.encrypted,
21761
+ this.getDEK,
21762
+ this.keyring.userId,
21763
+ this.emitter,
21764
+ async (collection, id) => await this.collection(collection).get(id) !== null
21765
+ );
21766
+ this.linkSetCache.set(name, handle);
21767
+ }
21768
+ return handle;
21769
+ }
21087
21770
  /**
21088
21771
  * Build a `JoinableSource` for a dictKey field, for use in dict joins
21089
21772
  *. Returns a source whose snapshot contains `{ key, ...labels }`
@@ -21243,65 +21926,16 @@ var Vault = class {
21243
21926
  });
21244
21927
  }
21245
21928
  }
21246
- /**
21247
- * Bulk blob extraction primitive.
21248
- *
21249
- * Returns an async-iterable handle over every blob attached to
21250
- * records in the vault. Single capability check (`plaintext/blob`)
21251
- * at handle creation; single audit entry to `_export_audit` before
21252
- * the first yield. Per-blob decryption happens lazily as the
21253
- * consumer pulls tuples.
21254
- *
21255
- * ```ts
21256
- * const handle = vault.exportBlobs({
21257
- * collections: ['invoiceScans'],
21258
- * where: (rec) => (rec as { clientId?: string }).clientId === 'c-123',
21259
- * })
21260
- * for await (const { bytes, meta, recordRef } of handle) {
21261
- * await uploadToColdStorage(bytes, recordRef)
21262
- * }
21263
- * ```
21264
- *
21265
- * @see `@noy-db/hub/store/export-blobs` for the full option surface.
21266
- */
21267
- /**
21268
- * Evict blob slots per the per-collection `blobFields` retention
21269
- * policy.
21270
- *
21271
- * Iterates every collection declared with `{ blobFields: {...} }`.
21272
- * For each record, checks every configured slot against its
21273
- * policy — `retainDays` (age-based TTL) and/or `evictWhen(record)`
21274
- * (predicate) — and evicts matching slots. Every eviction writes
21275
- * one entry to `_blob_eviction_audit` (actor + eTag + reason +
21276
- * timestamp, no plaintext). Consumer-scheduled; noy-db never runs
21277
- * this on its own.
21278
- *
21279
- * ```ts
21280
- * await vault.compact() // run full pass
21281
- * await vault.compact({ dryRun: true }) // preview counts
21282
- * await vault.compact({ maxEvictions: 1000 }) // cap batch
21283
- * ```
21284
- */
21285
- /**
21286
- * Atomic, gap-free numbering. `vault.sequence('invoice-2026').next()`
21287
- * returns 1, 2, 3, … with no gaps or duplicates under concurrency, via
21288
- * an optimistic-CAS counter at `_sequences/<name>`. Each name is an
21289
- * independent sequence.
21290
- *
21291
- * **Online-only:** `next()` throws `SequenceOfflineError` unless the
21292
- * store advertises `capabilities.casAtomic` — gap-free numbering cannot
21293
- * be serialized by an offline / non-CAS writer.
21294
- *
21295
- * ```ts
21296
- * const n = await vault.sequence('invoice-2026').next() // 1, then 2, …
21297
- * const cur = await vault.sequence('invoice-2026').peek() // current value, no allocation
21298
- * ```
21299
- */
21300
21929
  sequence(series, opts) {
21301
21930
  if (series.includes("\0")) {
21302
21931
  throw new ValidationError(`sequence("${series}"): series name must not contain a null byte (\\x00).`);
21303
21932
  }
21304
21933
  if (this.numberingConfigs.has(series)) {
21934
+ if (opts?.format !== void 0) {
21935
+ throw new ValidationError(
21936
+ `sequence("${series}") is a deferred-numbering series; the format option applies to CAS sequences only.`
21937
+ );
21938
+ }
21305
21939
  const eng = this.deferred();
21306
21940
  return {
21307
21941
  next: async (nextOpts) => {
@@ -21325,7 +21959,17 @@ var Vault = class {
21325
21959
  actor: this.keyring.userId
21326
21960
  });
21327
21961
  }
21328
- return this.sequenceStore.handle(resolveSequenceKey(series, opts));
21962
+ const handle = this.sequenceStore.handle(resolveSequenceKey(series, opts));
21963
+ if (opts?.format === void 0) return handle;
21964
+ const render = compileSequenceFormat(opts.format, series, opts.partition);
21965
+ return {
21966
+ next: async (nextOpts) => {
21967
+ const serial = await handle.next(nextOpts);
21968
+ return { serial, formatted: render(serial) };
21969
+ },
21970
+ peek: () => handle.peek(),
21971
+ seedTo: (n) => handle.seedTo(n)
21972
+ };
21329
21973
  }
21330
21974
  /** @internal — lazily build the deferred-numbering engine with a cache-coherent stamp. */
21331
21975
  deferred() {
@@ -21340,11 +21984,11 @@ var Vault = class {
21340
21984
  // Stamp THROUGH the Collection layer so cache/indexes/MVs stay coherent —
21341
21985
  // `this.collection(name)` returns the shared cached instance, so a
21342
21986
  // subsequent user `collection.get(id)` sees the assigned serial.
21343
- stamp: async (collection, recordId3, field, serial) => {
21987
+ stamp: async (collection, recordId4, field, serial) => {
21344
21988
  const coll = this.collection(collection);
21345
- const rec = await coll.get(recordId3);
21989
+ const rec = await coll.get(recordId4);
21346
21990
  if (!rec) return false;
21347
- await coll.put(recordId3, { ...rec, [field]: serial });
21991
+ await coll.put(recordId4, { ...rec, [field]: serial });
21348
21992
  return true;
21349
21993
  }
21350
21994
  });
@@ -21552,6 +22196,43 @@ var Vault = class {
21552
22196
  if (descriptor.mode !== "strict") continue;
21553
22197
  const rawId = obj[field];
21554
22198
  if (rawId === null || rawId === void 0) continue;
22199
+ if (isRefArray(descriptor)) {
22200
+ if (!Array.isArray(rawId)) {
22201
+ throw new RefIntegrityError({
22202
+ collection: collectionName,
22203
+ id: obj["id"] ?? "<unknown>",
22204
+ field,
22205
+ refTo: descriptor.target,
22206
+ refId: null,
22207
+ message: `Array ref field "${collectionName}.${field}" must be an array, got ${typeof rawId}.`
22208
+ });
22209
+ }
22210
+ const arrTarget = this.collection(descriptor.target);
22211
+ for (const el of rawId) {
22212
+ if (typeof el !== "string" && typeof el !== "number") {
22213
+ throw new RefIntegrityError({
22214
+ collection: collectionName,
22215
+ id: obj["id"] ?? "<unknown>",
22216
+ field,
22217
+ refTo: descriptor.target,
22218
+ refId: null,
22219
+ message: `Array ref "${collectionName}.${field}" elements must be strings or numbers, got ${typeof el}.`
22220
+ });
22221
+ }
22222
+ const elId = String(el);
22223
+ if (!await arrTarget.get(elId)) {
22224
+ throw new RefIntegrityError({
22225
+ collection: collectionName,
22226
+ id: obj["id"] ?? "<unknown>",
22227
+ field,
22228
+ refTo: descriptor.target,
22229
+ refId: elId,
22230
+ message: `Strict array ref "${collectionName}.${field}" \u2192 "${descriptor.target}" cannot be satisfied: element id "${elId}" not found in "${descriptor.target}".`
22231
+ });
22232
+ }
22233
+ }
22234
+ continue;
22235
+ }
21555
22236
  if (typeof rawId !== "string" && typeof rawId !== "number") {
21556
22237
  throw new RefIntegrityError({
21557
22238
  collection: collectionName,
@@ -21601,6 +22282,11 @@ var Vault = class {
21601
22282
  const allRecords = await fromCollection.list();
21602
22283
  const matches = allRecords.filter((rec) => {
21603
22284
  const raw = rec[rule.field];
22285
+ if (rule.isArray) {
22286
+ return Array.isArray(raw) && raw.some(
22287
+ (el) => (typeof el === "string" || typeof el === "number") && String(el) === id
22288
+ );
22289
+ }
21604
22290
  if (typeof raw !== "string" && typeof raw !== "number") return false;
21605
22291
  return String(raw) === id;
21606
22292
  });
@@ -21639,10 +22325,45 @@ var Vault = class {
21639
22325
  }
21640
22326
  }
21641
22327
  }
22328
+ await this.enforceLinksOnDelete(collectionName, id);
21642
22329
  } finally {
21643
22330
  this.cascadeInProgress.delete(key);
21644
22331
  }
21645
22332
  }
22333
+ /**
22334
+ * @internal — apply link `onDelete` policy when an endpoint record is
22335
+ * deleted (#377-B). `'strict'` throws (blocks the delete), `'cascade'`
22336
+ * removes the touching link rows (tx-atomic when a transaction is active),
22337
+ * `'warn'` leaves orphans for `checkIntegrity()`.
22338
+ */
22339
+ async enforceLinksOnDelete(collectionName, id) {
22340
+ for (const [name, spec] of this.linkRegistry) {
22341
+ if (spec.a !== collectionName && spec.b !== collectionName) continue;
22342
+ const handle = this.links(name);
22343
+ const touching = await handle._rowsTouchingEndpoint(collectionName, id);
22344
+ if (touching.length === 0) continue;
22345
+ const mode = spec.onDelete ?? "cascade";
22346
+ if (mode === "warn") continue;
22347
+ if (mode === "strict") {
22348
+ throw new LinkIntegrityError(name, collectionName, id, touching.length);
22349
+ }
22350
+ const linkColl = handle._collectionName;
22351
+ const txCtx = this.noydb._activeTxContextOrNull;
22352
+ for (const row of touching) {
22353
+ const rowKey = linkRowKey(row.a, row.b);
22354
+ if (txCtx !== null) {
22355
+ const prior = await this.adapter.get(this.name, linkColl, rowKey);
22356
+ if (prior !== null) {
22357
+ txCtx._executed.push({
22358
+ op: { type: "delete", vaultName: this.name, collectionName: linkColl, id: rowKey },
22359
+ priorEnvelope: prior
22360
+ });
22361
+ }
22362
+ }
22363
+ await handle.disconnect(row.a, row.b);
22364
+ }
22365
+ }
22366
+ }
21646
22367
  // ─── Join resolver) ────────────────────
21647
22368
  /**
21648
22369
  * Look up the `RefDescriptor` the left collection declared for a
@@ -21703,6 +22424,23 @@ var Vault = class {
21703
22424
  for (const [field, descriptor] of Object.entries(refs)) {
21704
22425
  const rawId = record[field];
21705
22426
  if (rawId === null || rawId === void 0) continue;
22427
+ const target = this.collection(descriptor.target);
22428
+ if (isRefArray(descriptor)) {
22429
+ if (!Array.isArray(rawId)) {
22430
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: rawId, mode: descriptor.mode });
22431
+ continue;
22432
+ }
22433
+ for (const el of rawId) {
22434
+ if (typeof el !== "string" && typeof el !== "number") {
22435
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
22436
+ continue;
22437
+ }
22438
+ if (!await target.get(String(el))) {
22439
+ violations.push({ collection: collectionName, id: recId, field, refTo: descriptor.target, refId: el, mode: descriptor.mode });
22440
+ }
22441
+ }
22442
+ continue;
22443
+ }
21706
22444
  if (typeof rawId !== "string" && typeof rawId !== "number") {
21707
22445
  violations.push({
21708
22446
  collection: collectionName,
@@ -21715,7 +22453,6 @@ var Vault = class {
21715
22453
  continue;
21716
22454
  }
21717
22455
  const refId = String(rawId);
21718
- const target = this.collection(descriptor.target);
21719
22456
  const exists = await target.get(refId);
21720
22457
  if (!exists) {
21721
22458
  violations.push({
@@ -21730,6 +22467,19 @@ var Vault = class {
21730
22467
  }
21731
22468
  }
21732
22469
  }
22470
+ for (const [name, spec] of this.linkRegistry) {
22471
+ const linkColl = linkCollectionName(name);
22472
+ const rows = await this.links(name).list();
22473
+ for (const row of rows) {
22474
+ const rowKey = linkRowKey(row.a, row.b);
22475
+ if (await this.collection(spec.a).get(row.a) === null) {
22476
+ violations.push({ collection: linkColl, id: rowKey, field: "a", refTo: spec.a, refId: row.a, mode: spec.onDelete ?? "cascade" });
22477
+ }
22478
+ if (await this.collection(spec.b).get(row.b) === null) {
22479
+ violations.push({ collection: linkColl, id: rowKey, field: "b", refTo: spec.b, refId: row.b, mode: spec.onDelete ?? "cascade" });
22480
+ }
22481
+ }
22482
+ }
21733
22483
  return { violations };
21734
22484
  }
21735
22485
  /**
@@ -22005,7 +22755,7 @@ var Vault = class {
22005
22755
  const registry = new GuardRegistry2();
22006
22756
  for (const h of handles) registry.register(h.spec);
22007
22757
  this.guardRegistry = registry;
22008
- this.readOnlyFacade = new ReadOnlyVaultFacade2(this);
22758
+ this.guardFacade = new ReadOnlyVaultFacade2(this, "guard");
22009
22759
  }
22010
22760
  /**
22011
22761
  * @internal — The gate handler in Noydb.#registerGuardGate calls into
@@ -22036,8 +22786,8 @@ var Vault = class {
22036
22786
  }
22037
22787
  registry.validate();
22038
22788
  this.derivationRegistry = registry;
22039
- if (this.readOnlyFacade === null) {
22040
- this.readOnlyFacade = new ReadOnlyVaultFacade2(this);
22789
+ if (this.derivationFacade === null) {
22790
+ this.derivationFacade = new ReadOnlyVaultFacade2(this, "derivation");
22041
22791
  }
22042
22792
  }
22043
22793
  /**
@@ -22154,7 +22904,7 @@ var Vault = class {
22154
22904
  const { DerivationExecutor: DerivationExecutor2 } = await Promise.resolve().then(() => (init_executor2(), executor_exports2));
22155
22905
  const sourceColl = this.collection(sourceCollection);
22156
22906
  const records = await sourceColl.list();
22157
- const ctx = { vault: this.readOnlyFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this) };
22907
+ const ctx = { vault: this.derivationFacade ?? new (await Promise.resolve().then(() => (init_read_only_facade(), read_only_facade_exports))).ReadOnlyVaultFacade(this, "derivation") };
22158
22908
  let derived = 0;
22159
22909
  let failed = 0;
22160
22910
  for (const record of records) {
@@ -22219,17 +22969,18 @@ var Vault = class {
22219
22969
  * never see null).
22220
22970
  */
22221
22971
  _getReadOnlyFacade() {
22222
- return this.readOnlyFacade;
22972
+ return this.guardFacade;
22223
22973
  }
22224
22974
  /**
22225
- * Internal lazy-allocator for the read-only facade. Used as a
22226
- * defensive fallback; in practice `_initGuards()` eagerly
22227
- * instantiates this, so the lazy path is a no-op.
22975
+ * Internal lazy-allocator for the derivation read-only facade
22976
+ * (`layer:'derivation'`). Used as a defensive fallback; in practice
22977
+ * `_initDerivations()` eagerly instantiates this, so the lazy path is
22978
+ * a no-op.
22228
22979
  */
22229
22980
  _ensureReadOnlyFacade() {
22230
- if (this.readOnlyFacade !== null) return this.readOnlyFacade;
22981
+ if (this.derivationFacade !== null) return this.derivationFacade;
22231
22982
  throw new Error(
22232
- "Vault: guard hook fired before _initGuards() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
22983
+ "Vault: derivation hook fired before _initDerivations() completed. This typically means the vault was opened via the sync fallback path (Noydb.vault(name)) without first calling await db.openVault(name). See issue #132."
22233
22984
  );
22234
22985
  }
22235
22986
  /**
@@ -22350,7 +23101,7 @@ var Vault = class {
22350
23101
  *
22351
23102
  * @internal
22352
23103
  */
22353
- async _logConsent(op, collection, recordId3) {
23104
+ async _logConsent(op, collection, recordId4) {
22354
23105
  const ctx = this.consentContext;
22355
23106
  if (!ctx) return;
22356
23107
  await this.consentStrategy.write(
@@ -22363,7 +23114,7 @@ var Vault = class {
22363
23114
  consentHash: ctx.consentHash,
22364
23115
  op,
22365
23116
  collection,
22366
- recordId: recordId3
23117
+ recordId: recordId4
22367
23118
  },
22368
23119
  this.getDEK
22369
23120
  );
@@ -22546,14 +23297,14 @@ var Vault = class {
22546
23297
  * the HKDF derivation, record-id composition, and batch logic so the
22547
23298
  * grantor doesn't touch this method directly.
22548
23299
  */
22549
- async writeMagicLinkGrant(contentKey, grantKek, recordId3, opts) {
23300
+ async writeMagicLinkGrant(contentKey, grantKek, recordId4, opts) {
22550
23301
  return writeMagicLinkGrant(
22551
23302
  this.adapter,
22552
23303
  this.name,
22553
23304
  this.keyring,
22554
23305
  contentKey,
22555
23306
  grantKek,
22556
- recordId3,
23307
+ recordId4,
22557
23308
  opts
22558
23309
  );
22559
23310
  }
@@ -25117,7 +25868,7 @@ var Noydb = class {
25117
25868
  const { StateManagementVault: StateManagementVault2 } = await Promise.resolve().then(() => (init_state_vault(), state_vault_exports));
25118
25869
  const stateVault = opts.registry ? void 0 : await StateManagementVault2.open(this);
25119
25870
  const registry = opts.registry ?? stateVault.registry;
25120
- const group = new VaultGroup2(this, name, registry, opts.sharding, template);
25871
+ const group = new VaultGroup2(this, name, registry, opts.sharding, template, opts.migrateOnOpen ?? false);
25121
25872
  if (stateVault) {
25122
25873
  group._attachStateVault(stateVault);
25123
25874
  await stateVault.recordManifest(opts.sharding.vaultTemplate, template);
@@ -27461,6 +28212,60 @@ function immutableGuard(config) {
27461
28212
  return withGuard(spec);
27462
28213
  }
27463
28214
 
28215
+ // src/guards/transition-guard.ts
28216
+ init_errors();
28217
+ function recordId3(record) {
28218
+ const id = record?.id;
28219
+ return typeof id === "string" ? id : "";
28220
+ }
28221
+ function stateOf(record, field) {
28222
+ const v = record[field];
28223
+ return typeof v === "string" ? v : String(v);
28224
+ }
28225
+ function transitionGuard(config) {
28226
+ const { collection, field, transitions, initial, amendmentRoles, amendmentInvariant } = config;
28227
+ const allowIdempotent = config.allowIdempotent ?? true;
28228
+ if (!field) {
28229
+ throw new ValidationError("transitionGuard: `field` is required");
28230
+ }
28231
+ if (transitions === void 0 || typeof transitions !== "object") {
28232
+ throw new ValidationError("transitionGuard: `transitions` must be a state\u2192states map");
28233
+ }
28234
+ const spec = {
28235
+ collection,
28236
+ check: (incoming, ctx) => {
28237
+ const rec = incoming;
28238
+ const to = stateOf(rec, field);
28239
+ if (ctx.existing === null) {
28240
+ if (initial !== void 0 && !initial.includes(to)) {
28241
+ throw new IllegalTransitionError(collection, recordId3(rec), "(none)", to);
28242
+ }
28243
+ return;
28244
+ }
28245
+ const from = stateOf(ctx.existing, field);
28246
+ if (from === to) {
28247
+ if (allowIdempotent) return;
28248
+ throw new IllegalTransitionError(collection, recordId3(rec), from, to);
28249
+ }
28250
+ const allowed = transitions[from] ?? [];
28251
+ if (!allowed.includes(to)) {
28252
+ throw new IllegalTransitionError(collection, recordId3(rec), from, to);
28253
+ }
28254
+ },
28255
+ // The authorized override: inside an amendment transaction the check
28256
+ // is skipped and the change is ledgered. By default no extra invariant
28257
+ // — the amendment itself is the sanctioned exception. Callers may
28258
+ // supply `amendmentInvariant` to keep a constraint inviolable even
28259
+ // under amendment; a throw reverts the amendment as `InvariantError`.
28260
+ amendment: {
28261
+ roles: amendmentRoles ?? ["admin", "owner"],
28262
+ invariant: amendmentInvariant ?? (() => {
28263
+ })
28264
+ }
28265
+ };
28266
+ return withGuard(spec);
28267
+ }
28268
+
27464
28269
  // src/derivations/with-derivation.ts
27465
28270
  init_errors();
27466
28271
  function withDerivation(spec) {
@@ -27488,8 +28293,37 @@ function withDerivation(spec) {
27488
28293
  }
27489
28294
  }
27490
28295
  }
28296
+ if (spec.triggerBy !== void 0) {
28297
+ for (const t of spec.triggerBy) {
28298
+ if (typeof t?.collection !== "string" || t.collection.length === 0) {
28299
+ throw new ValidationError("withDerivation: each triggerBy entry needs a non-empty `collection`");
28300
+ }
28301
+ if (t.collection === spec.source) {
28302
+ throw new ValidationError(
28303
+ `withDerivation: triggerBy.collection must not equal the source "${spec.source}" (use sources[] for same-id triggers)`
28304
+ );
28305
+ }
28306
+ if (typeof t.on !== "string" || t.on.length === 0) {
28307
+ throw new ValidationError(
28308
+ `withDerivation: triggerBy on "${t.collection}" needs a non-empty \`on\` (the FK field on the source)`
28309
+ );
28310
+ }
28311
+ if (t.maxFanout !== void 0 && (!Number.isInteger(t.maxFanout) || t.maxFanout < 1)) {
28312
+ throw new ValidationError(
28313
+ `withDerivation: triggerBy maxFanout on "${t.collection}" must be a positive integer (got ${String(t.maxFanout)}).`
28314
+ );
28315
+ }
28316
+ }
28317
+ }
27491
28318
  const lifecycleMode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
27492
28319
  for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {
28320
+ if (outputSpec.shape === "record" && outputSpec.collection === spec.source) {
28321
+ if (!outputSpec.denorm || outputSpec.denorm.length === 0) {
28322
+ throw new ValidationError(
28323
+ `withDerivation: self-write output "${outputKey}" (collection === source "${spec.source}") must declare \`denorm: [...]\` naming the fields it maintains.`
28324
+ );
28325
+ }
28326
+ }
27493
28327
  if (outputSpec.shape === "array") {
27494
28328
  if (lifecycleMode !== "eager") {
27495
28329
  throw new ValidationError(
@@ -27517,6 +28351,43 @@ function withDerivation(spec) {
27517
28351
  };
27518
28352
  }
27519
28353
 
28354
+ // src/derivations/with-rollup.ts
28355
+ init_errors();
28356
+ function withRollup(config) {
28357
+ const { from, key, into, field, compute } = config;
28358
+ if (!from || from.length === 0) {
28359
+ throw new ValidationError("withRollup: `from` (child collection) is required");
28360
+ }
28361
+ if (!into || into.length === 0) {
28362
+ throw new ValidationError("withRollup: `into` (parent collection) is required");
28363
+ }
28364
+ if (from === into) {
28365
+ throw new ValidationError("withRollup: `from` and `into` must be different collections");
28366
+ }
28367
+ if (!key || key.length === 0) {
28368
+ throw new ValidationError("withRollup: `key` (FK field on the child) is required");
28369
+ }
28370
+ if (!field || field.length === 0) {
28371
+ throw new ValidationError("withRollup: `field` (target field on the parent) is required");
28372
+ }
28373
+ if (typeof compute !== "function") {
28374
+ throw new ValidationError("withRollup: `compute` must be a function");
28375
+ }
28376
+ const spec = {
28377
+ source: into,
28378
+ // the parent record is what carries the rolled-up field
28379
+ deterministic: true,
28380
+ rollup: { from, key, field, compute },
28381
+ // Synthetic self-write output for registry / cycle bookkeeping. Dispatch
28382
+ // patches `field` directly (value-equality guarded); the executor is not run.
28383
+ outputs: { value: { shape: "record", collection: into, denorm: [field] } },
28384
+ derive: () => ({ value: {} }),
28385
+ // never invoked for a rollup strategy
28386
+ lifecycle: "eager"
28387
+ };
28388
+ return { __noydb_strategy: "derivation", spec };
28389
+ }
28390
+
27520
28391
  // src/index.ts
27521
28392
  init_errors();
27522
28393
 
@@ -28578,6 +29449,7 @@ function shortJSON(value) {
28578
29449
  GroupedQuery,
28579
29450
  GroupedQueryN,
28580
29451
  INDEXED_STORE_POLICY,
29452
+ IllegalTransitionError,
28581
29453
  ImportCapabilityError,
28582
29454
  IndexRequiredError,
28583
29455
  IndexWriteFailureError,
@@ -28590,6 +29462,8 @@ function shortJSON(value) {
28590
29462
  LEDGER_DELTAS_COLLECTION,
28591
29463
  LedgerContentionError,
28592
29464
  LedgerStore,
29465
+ LinkEndpointError,
29466
+ LinkIntegrityError,
28593
29467
  LocaleNotSpecifiedError,
28594
29468
  Lru,
28595
29469
  MAGIC_LINK_CONTENT_INFO_PREFIX,
@@ -28721,6 +29595,7 @@ function shortJSON(value) {
28721
29595
  canonicalJson,
28722
29596
  checkGate,
28723
29597
  clearDevUnlock,
29598
+ compileSequenceFormat,
28724
29599
  computePatch,
28725
29600
  coordinatedCutover,
28726
29601
  count,
@@ -28783,11 +29658,13 @@ function shortJSON(value) {
28783
29658
  isDictKeyDescriptor,
28784
29659
  isDiscriminant,
28785
29660
  isI18nTextDescriptor,
29661
+ isLinkCollectionName,
28786
29662
  isMagicLinkGrantExpired,
28787
29663
  isMoneyDescriptor,
28788
29664
  isMoneyString,
28789
29665
  isPreCompressed,
28790
29666
  isPublicEnvelope,
29667
+ isRefArray,
28791
29668
  isSessionAlive,
28792
29669
  isStaticDictDescriptor,
28793
29670
  isULID,
@@ -28841,6 +29718,7 @@ function shortJSON(value) {
28841
29718
  recoverUser,
28842
29719
  reduceRecords,
28843
29720
  ref,
29721
+ refArray,
28844
29722
  removeAuthenticator,
28845
29723
  resetBrotliSupportCache,
28846
29724
  resetJoinWarnings,
@@ -28868,6 +29746,7 @@ function shortJSON(value) {
28868
29746
  sha256Hex,
28869
29747
  staticDict,
28870
29748
  sum,
29749
+ transitionGuard,
28871
29750
  unwrapDeksFromBlob,
28872
29751
  unwrapDeksFromPaperEntry,
28873
29752
  unwrapDeksFromShamirEntry,
@@ -28891,6 +29770,7 @@ function shortJSON(value) {
28891
29770
  withMetrics,
28892
29771
  withOverlayedView,
28893
29772
  withRetry,
29773
+ withRollup,
28894
29774
  wrapBundleStore,
28895
29775
  wrapStore,
28896
29776
  writeMagicLinkGrant,