@noy-db/hub 0.3.0-pre.7 → 0.3.0-pre.9

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 (401) hide show
  1. package/dist/adapter/index.d.ts +2 -3
  2. package/dist/adapter/index.js +1 -1
  3. package/dist/aggregate/index.d.ts +3 -4
  4. package/dist/aggregate/index.js +9 -8
  5. package/dist/aggregate/index.js.map +1 -1
  6. package/dist/api-HV4PDUWF.js +20 -0
  7. package/dist/as/index.d.ts +4 -5
  8. package/dist/as/index.js +11 -11
  9. package/dist/at/index.d.ts +2 -3
  10. package/dist/attestation/index.d.ts +3 -4
  11. package/dist/attestation/index.js +18 -18
  12. package/dist/{backup-7PX6NDKT.js → backup-ODXO7OGA.js} +12 -12
  13. package/dist/blobs/index.d.ts +4 -5
  14. package/dist/blobs/index.js +11 -11
  15. package/dist/blobs/index.js.map +1 -1
  16. package/dist/bundle/index.d.ts +5 -6
  17. package/dist/bundle/index.js +20 -20
  18. package/dist/{bundle-BN_VSq5L.d.ts → bundle-DlSxo31R.d.ts} +1 -1
  19. package/dist/by/index.js +2 -2
  20. package/dist/cargo/index.d.ts +4 -5
  21. package/dist/cargo/index.js +23 -22
  22. package/dist/{chunk-QYNBPDGO.js → chunk-2644KEGW.js} +18 -10
  23. package/dist/chunk-2644KEGW.js.map +1 -0
  24. package/dist/{chunk-XW5LZYV4.js → chunk-2HVUKRNE.js} +2 -2
  25. package/dist/{chunk-VFQOE76I.js → chunk-2KOSJTMU.js} +2 -2
  26. package/dist/{chunk-7AERMJP6.js → chunk-2UQ4RMHV.js} +4 -4
  27. package/dist/{chunk-5GQE5IDB.js → chunk-2XE6LIXT.js} +13 -11
  28. package/dist/chunk-2XE6LIXT.js.map +1 -0
  29. package/dist/{chunk-VIWOGJ2O.js → chunk-34RRRIC3.js} +2 -2
  30. package/dist/{chunk-BASCPHNM.js → chunk-3C5CKXFI.js} +289 -48
  31. package/dist/chunk-3C5CKXFI.js.map +1 -0
  32. package/dist/{chunk-LFGJG4XZ.js → chunk-3I4KYWNP.js} +2 -2
  33. package/dist/{chunk-RUWEHFRB.js → chunk-42KVY3OC.js} +4 -4
  34. package/dist/{chunk-5K7KZMXG.js → chunk-4C4WBS4V.js} +2048 -948
  35. package/dist/chunk-4C4WBS4V.js.map +1 -0
  36. package/dist/{chunk-J54DC5IC.js → chunk-4G3IT7J7.js} +7 -7
  37. package/dist/{chunk-WJEMUO32.js → chunk-4RQ62VSR.js} +2 -2
  38. package/dist/{chunk-QAVFI23O.js → chunk-4ZSGSRBI.js} +8 -8
  39. package/dist/{chunk-C4NOLA5X.js → chunk-5GAX2QOH.js} +2 -2
  40. package/dist/{chunk-IEKK2XI4.js → chunk-5SAZMBW7.js} +7 -7
  41. package/dist/{chunk-II6DLDZM.js → chunk-5TZZUL5X.js} +2 -2
  42. package/dist/{chunk-GMZFHDSG.js → chunk-6CU3FN74.js} +8 -8
  43. package/dist/chunk-6G2SCQYS.js +29 -0
  44. package/dist/chunk-6G2SCQYS.js.map +1 -0
  45. package/dist/{chunk-UWZ7O4NR.js → chunk-6MWUANRF.js} +2 -2
  46. package/dist/{chunk-U7LV2DUN.js → chunk-6ULCYCMU.js} +2 -2
  47. package/dist/{chunk-6Z435B6N.js → chunk-7HAPHVRJ.js} +131 -92
  48. package/dist/chunk-7HAPHVRJ.js.map +1 -0
  49. package/dist/{chunk-VFG5YXWM.js → chunk-7IZQTCTR.js} +3 -3
  50. package/dist/chunk-7ZANJ6JZ.js +265 -0
  51. package/dist/chunk-7ZANJ6JZ.js.map +1 -0
  52. package/dist/{chunk-VMRXI3IF.js → chunk-A2APWS32.js} +6 -6
  53. package/dist/chunk-A2APWS32.js.map +1 -0
  54. package/dist/{chunk-U5IJHUHZ.js → chunk-A4MAD6PO.js} +5 -5
  55. package/dist/chunk-ANDBBA2P.js +36 -0
  56. package/dist/chunk-ANDBBA2P.js.map +1 -0
  57. package/dist/{chunk-5HZULLHE.js → chunk-AUR7MRHA.js} +2 -2
  58. package/dist/{chunk-YSJG4L4A.js → chunk-AYGZMQP5.js} +3 -3
  59. package/dist/chunk-AYGZMQP5.js.map +1 -0
  60. package/dist/{chunk-H7WQMGXH.js → chunk-CJEMA2Z6.js} +2 -2
  61. package/dist/{chunk-WRLPV6SI.js → chunk-CTSFKDGA.js} +2 -2
  62. package/dist/{chunk-OJ64XMPJ.js → chunk-CU62BBRB.js} +32 -1
  63. package/dist/chunk-CU62BBRB.js.map +1 -0
  64. package/dist/{chunk-OFYY5Q4R.js → chunk-CWCCYH34.js} +2 -2
  65. package/dist/{chunk-WGH3YV6Y.js → chunk-DEYAO3N6.js} +10 -10
  66. package/dist/{chunk-7L6EH6S7.js → chunk-DFIBOSOQ.js} +8 -8
  67. package/dist/{chunk-3CHNKEFI.js → chunk-E5AHO65E.js} +7 -7
  68. package/dist/{chunk-AG2PLGQ2.js → chunk-FC3XK5JT.js} +8 -8
  69. package/dist/{chunk-CP3GUIAN.js → chunk-FGOGS7CE.js} +10 -12
  70. package/dist/chunk-FGOGS7CE.js.map +1 -0
  71. package/dist/{chunk-DJ4TNF4N.js → chunk-GX2BYNCT.js} +7 -7
  72. package/dist/{chunk-YTCCC6NG.js → chunk-GZ2MOPVX.js} +34 -63
  73. package/dist/chunk-GZ2MOPVX.js.map +1 -0
  74. package/dist/chunk-H57P2SJB.js +126 -0
  75. package/dist/chunk-H57P2SJB.js.map +1 -0
  76. package/dist/{chunk-55LSDABW.js → chunk-HBTT7YQX.js} +2 -2
  77. package/dist/{chunk-PYIUDUGS.js → chunk-HOCEGVGQ.js} +2 -2
  78. package/dist/{chunk-5JQMX2EH.js → chunk-ICV32FPK.js} +2 -2
  79. package/dist/{chunk-WFBZRXS5.js → chunk-IIP5WFVT.js} +138 -92
  80. package/dist/chunk-IIP5WFVT.js.map +1 -0
  81. package/dist/{chunk-YSU2E4GZ.js → chunk-IUKGIB2Q.js} +19 -6
  82. package/dist/chunk-IUKGIB2Q.js.map +1 -0
  83. package/dist/{chunk-OWGAXPRE.js → chunk-IWTYFS3D.js} +2 -2
  84. package/dist/{chunk-FCOZ7DEC.js → chunk-JC5R7P44.js} +3 -3
  85. package/dist/{chunk-K6WHZBCO.js → chunk-KBBWY46Y.js} +2 -2
  86. package/dist/{chunk-RNYDHIQL.js → chunk-KBF5GSGX.js} +3 -3
  87. package/dist/{chunk-6P5AJ2ZS.js → chunk-KCG3BHFG.js} +2 -23
  88. package/dist/chunk-KCG3BHFG.js.map +1 -0
  89. package/dist/{chunk-T4NQMPBN.js → chunk-KNXN47MO.js} +3 -3
  90. package/dist/{chunk-VK6OC22R.js → chunk-KWLD2D75.js} +4 -3
  91. package/dist/chunk-KWLD2D75.js.map +1 -0
  92. package/dist/{chunk-7F2P2NHX.js → chunk-KXKLKJW4.js} +6 -6
  93. package/dist/{chunk-PAJCRV7O.js → chunk-KYC4LIEU.js} +2 -2
  94. package/dist/{chunk-AZPGB4OD.js → chunk-LEOXZY36.js} +8 -8
  95. package/dist/{chunk-2L7PPF2C.js → chunk-M5I354EE.js} +9 -9
  96. package/dist/{chunk-26FSVNBL.js → chunk-MK2BSS44.js} +2 -2
  97. package/dist/{chunk-G5CFMITA.js → chunk-MSVY7ULZ.js} +3 -3
  98. package/dist/{chunk-PE25C4QL.js → chunk-NNWK3PSY.js} +9 -9
  99. package/dist/{chunk-JT4A6OAJ.js → chunk-NSZ6J5DF.js} +7 -7
  100. package/dist/{chunk-RHB7BOEO.js → chunk-NUEFXLZU.js} +7 -7
  101. package/dist/{chunk-I6Q7YECW.js → chunk-NYVQCZ54.js} +5 -5
  102. package/dist/chunk-NYVQCZ54.js.map +1 -0
  103. package/dist/{chunk-DKGZIX6S.js → chunk-OQAODTYR.js} +2 -2
  104. package/dist/{chunk-HV3VNAYI.js → chunk-OV4CMJGR.js} +5 -5
  105. package/dist/{chunk-V76PHWLE.js → chunk-PEDZFFKW.js} +2 -2
  106. package/dist/chunk-PF3FQCCF.js +164 -0
  107. package/dist/chunk-PF3FQCCF.js.map +1 -0
  108. package/dist/{chunk-CND5WQLO.js → chunk-Q2KVKTC2.js} +5 -5
  109. package/dist/{chunk-RUIMKQTA.js → chunk-QJR3W3MF.js} +1 -1
  110. package/dist/chunk-QJR3W3MF.js.map +1 -0
  111. package/dist/{chunk-HBVEGUON.js → chunk-QTXQ6RNZ.js} +3 -3
  112. package/dist/{chunk-J5YDMJHG.js → chunk-RJ5522XX.js} +12 -12
  113. package/dist/{chunk-H3MPSYCH.js → chunk-RMUXPHIH.js} +56 -7
  114. package/dist/chunk-RMUXPHIH.js.map +1 -0
  115. package/dist/{chunk-CHK5VVMI.js → chunk-S6AQXDN4.js} +2 -2
  116. package/dist/{chunk-E3LURVGQ.js → chunk-SSFC3IYA.js} +9 -9
  117. package/dist/chunk-SSFC3IYA.js.map +1 -0
  118. package/dist/{chunk-MOQ5M46U.js → chunk-SZCHEDML.js} +2 -2
  119. package/dist/{chunk-UB3XHGFG.js → chunk-TDPEJINP.js} +14 -7
  120. package/dist/chunk-TDPEJINP.js.map +1 -0
  121. package/dist/{chunk-HCK2DFEK.js → chunk-UNJU345Q.js} +1 -1
  122. package/dist/chunk-UNJU345Q.js.map +1 -0
  123. package/dist/chunk-V7RNIND6.js +265 -0
  124. package/dist/chunk-V7RNIND6.js.map +1 -0
  125. package/dist/{chunk-VUHXLL32.js → chunk-VJ3NQK4K.js} +5 -5
  126. package/dist/{chunk-62TTFKU4.js → chunk-WNA6T3MA.js} +8 -8
  127. package/dist/{chunk-7CZ6YXMO.js → chunk-WVCQ75OK.js} +2 -2
  128. package/dist/{chunk-NO53W6O5.js → chunk-X66MXMR5.js} +2 -2
  129. package/dist/{chunk-JIW3Q5JN.js → chunk-XHD3L3JO.js} +199 -35
  130. package/dist/chunk-XHD3L3JO.js.map +1 -0
  131. package/dist/{chunk-6PALJTC2.js → chunk-XJK2WIYN.js} +3 -3
  132. package/dist/{chunk-5UOPIPZC.js → chunk-XQ7UYPVN.js} +3 -3
  133. package/dist/{chunk-HU55NPHZ.js → chunk-Y2OCKFZ3.js} +2 -2
  134. package/dist/{chunk-UJJJPKJO.js → chunk-YBFOBRTO.js} +3 -3
  135. package/dist/{chunk-I6JJ6VJS.js → chunk-YLN33ADH.js} +2 -2
  136. package/dist/{chunk-HGQ6UNHE.js → chunk-Z5YFRMG5.js} +2 -2
  137. package/dist/{chunk-EU7HDMT3.js → chunk-ZWQFOXBR.js} +2 -2
  138. package/dist/classified/index.d.ts +4 -5
  139. package/dist/classified/index.js +5 -3
  140. package/dist/{config-drift-LRKCC3VZ.js → classified-marker-IO5EBW7Q.js} +4 -4
  141. package/dist/classified-marker-IO5EBW7Q.js.map +1 -0
  142. package/dist/collection-facade-3RAXG2OU.js +38 -0
  143. package/dist/{computed-6FOFXQPE.js → computed-R62HGCO4.js} +3 -3
  144. package/dist/consent/index.d.ts +3 -4
  145. package/dist/consent/index.js +9 -9
  146. package/dist/crdt/index.d.ts +3 -4
  147. package/dist/{dead-filter-NZORV45S.js → dead-filter-VNA4M3GP.js} +2 -2
  148. package/dist/delegation-ZKII5UBI.js +25 -0
  149. package/dist/derivations/index.d.ts +4 -5
  150. package/dist/derivations/index.js +12 -11
  151. package/dist/derive-5D2C3WG4.js +21 -0
  152. package/dist/describe/index.d.ts +4 -1
  153. package/dist/{dev-unlock-CsOqX7O2.d.ts → dev-unlock-DDTafVO6.d.ts} +1 -1
  154. package/dist/{enclave-7X5BUAMS.js → enclave-EUBEJGPG.js} +11 -9
  155. package/dist/executor-IL7SRNUZ.js +23 -0
  156. package/dist/executor-OUA2Y6YC.js +9 -0
  157. package/dist/executor-WAEI6PDV.js +9 -0
  158. package/dist/export-accessible-O55RKAGC.js +23 -0
  159. package/dist/extract-partition-VBGCBA6Q.js +36 -0
  160. package/dist/{fanout-sidecar-BDKLP5GG.js → fanout-sidecar-CI5BCMQM.js} +11 -11
  161. package/dist/find-FISGUWSK.js +11 -0
  162. package/dist/forget/index.d.ts +2 -2
  163. package/dist/forget/index.js +9 -9
  164. package/dist/guards/index.d.ts +4 -5
  165. package/dist/guards/index.js +4 -4
  166. package/dist/{hash-BPzm02Za.d.ts → hash-DBkZfIBW.d.ts} +1 -1
  167. package/dist/history/index.d.ts +4 -5
  168. package/dist/history/index.js +11 -11
  169. package/dist/i18n/index.d.ts +24 -5
  170. package/dist/i18n/index.js +45 -34
  171. package/dist/i18n/index.js.map +1 -1
  172. package/dist/in/index.d.ts +2 -3
  173. package/dist/{index-CLucFLeN.d.ts → index-CyclmyDr.d.ts} +2 -3
  174. package/dist/{index-BJ3L-XcV.d.ts → index-DzhNI2vE.d.ts} +6378 -5022
  175. package/dist/index.d.ts +64 -23
  176. package/dist/index.js +411 -132
  177. package/dist/index.js.map +1 -1
  178. package/dist/indexing/index.d.ts +3 -4
  179. package/dist/indexing/index.js +4 -4
  180. package/dist/issue-EQAWIYFQ.js +19 -0
  181. package/dist/kernel/index.d.ts +3 -4
  182. package/dist/kernel/index.js +13 -12
  183. package/dist/lazy/index.d.ts +3 -4
  184. package/dist/{ledger-EMDHRJJN.js → ledger-AJA2SR3C.js} +11 -11
  185. package/dist/liberate-SEGR4JLY.js +24 -0
  186. package/dist/{link-set-62Y36TOG.js → link-set-YBAC2DQT.js} +11 -11
  187. package/dist/materialized-views/index.d.ts +14 -5
  188. package/dist/materialized-views/index.js +17 -15
  189. package/dist/{mime-magic-BlE9fzn2.d.ts → mime-magic-CbV1quyp.d.ts} +1 -1
  190. package/dist/noydb-AMEOU456.js +65 -0
  191. package/dist/on/index.d.ts +2 -3
  192. package/dist/on/index.js +9 -9
  193. package/dist/overlay-views/index.d.ts +4 -5
  194. package/dist/overlay-views/index.js +4 -4
  195. package/dist/periods/index.d.ts +3 -4
  196. package/dist/periods/index.js +20 -12
  197. package/dist/pod/index.d.ts +3 -4
  198. package/dist/pod/index.js +11 -11
  199. package/dist/{policy-SXPLTQS4.js → policy-SDGRWVIL.js} +5 -5
  200. package/dist/portability/index.d.ts +3 -4
  201. package/dist/portability/index.js +8 -8
  202. package/dist/{post-register-DCJ2SRVG.js → post-register-MROIUA5O.js} +6 -6
  203. package/dist/{public-envelope-CBRIJORQ.js → public-envelope-J5PLQOW2.js} +4 -4
  204. package/dist/query/index.d.ts +2 -3
  205. package/dist/query/index.js +9 -6
  206. package/dist/{read-only-facade-5ADRJVTM.js → read-only-facade-UCYNRE56.js} +2 -2
  207. package/dist/register-OVJKW7BS.js +23 -0
  208. package/dist/registry-5RJSK5AJ.js +9 -0
  209. package/dist/registry-DQRNTWRL.js +18 -0
  210. package/dist/registry-LTGLERS7.js +19 -0
  211. package/dist/request-withdrawal-27O77D2V.js +31 -0
  212. package/dist/{reveal-R5N7JKA5.js → reveal-LT3I2K4P.js} +6 -6
  213. package/dist/revoke-4N45G7D2.js +24 -0
  214. package/dist/satellites/index.d.ts +2 -3
  215. package/dist/satellites/index.js +1 -1
  216. package/dist/sealed-record/index.d.ts +3 -4
  217. package/dist/sealed-record/index.js +12 -12
  218. package/dist/session/index.d.ts +4 -5
  219. package/dist/session/index.js +9 -9
  220. package/dist/shadow/index.d.ts +3 -4
  221. package/dist/shadow/index.js +2 -2
  222. package/dist/signer-XJCOV7JD.js +25 -0
  223. package/dist/snapshots/index.d.ts +3 -4
  224. package/dist/snapshots/index.js +10 -10
  225. package/dist/{stale-DP66HFT3.js → stale-IYYXEH3U.js} +2 -2
  226. package/dist/storage-Y44CAPE6.js +23 -0
  227. package/dist/{store-coordination-provider-F5N5ASDF.js → store-coordination-provider-WKGAE2WI.js} +3 -3
  228. package/dist/{subject-index-O75wKshW.d.ts → subject-index-CyOgGMJL.d.ts} +18 -0
  229. package/dist/sync/index.d.ts +2 -3
  230. package/dist/sync/index.js +9 -9
  231. package/dist/team/index.d.ts +3 -4
  232. package/dist/team/index.js +15 -15
  233. package/dist/tiers/index.d.ts +2 -3
  234. package/dist/tiers/index.js +10 -10
  235. package/dist/to/index.d.ts +2 -3
  236. package/dist/to/index.js +1 -1
  237. package/dist/{transition-guard-BzsAQEd-.d.ts → transition-guard-BOUkzQU4.d.ts} +1 -1
  238. package/dist/tx/index.d.ts +3 -4
  239. package/dist/tx/index.js +3 -3
  240. package/dist/ui/index.d.ts +4 -1
  241. package/dist/util/index.js +1 -1
  242. package/dist/{validators-DGmYFP51.d.ts → validators-B3SalxlJ.d.ts} +1 -1
  243. package/dist/{vault-diff-DmBBatkx.d.ts → vault-diff-Dt9NkLzw.d.ts} +1 -1
  244. package/dist/{verify-CAYGXYNO.js → verify-GH6CWVSW.js} +5 -5
  245. package/dist/{walk-3SB25HF2.js → walk-76UCFXYV.js} +11 -11
  246. package/dist/with/index.d.ts +3 -4
  247. package/dist/with/index.js +11 -11
  248. package/dist/{with-materialized-view-DPEVoGoB.d.ts → with-materialized-view-BP33v1Z6.d.ts} +1 -1
  249. package/dist/{with-overlayed-view-DGpKFSv1.d.ts → with-overlayed-view-DntFpOKN.d.ts} +1 -1
  250. package/dist/{with-rollup-B2RwnTq0.d.ts → with-rollup-ClCHO6-4.d.ts} +1 -1
  251. package/dist/withdraw-accessible-6J2UH3IN.js +28 -0
  252. package/package.json +3 -3
  253. package/dist/api-Z66JMGOU.js +0 -20
  254. package/dist/chunk-47C4CI7R.js +0 -108
  255. package/dist/chunk-47C4CI7R.js.map +0 -1
  256. package/dist/chunk-5GQE5IDB.js.map +0 -1
  257. package/dist/chunk-5K7KZMXG.js.map +0 -1
  258. package/dist/chunk-6P5AJ2ZS.js.map +0 -1
  259. package/dist/chunk-6Z435B6N.js.map +0 -1
  260. package/dist/chunk-7KTL4RHZ.js +0 -120
  261. package/dist/chunk-7KTL4RHZ.js.map +0 -1
  262. package/dist/chunk-BASCPHNM.js.map +0 -1
  263. package/dist/chunk-CP3GUIAN.js.map +0 -1
  264. package/dist/chunk-E3LURVGQ.js.map +0 -1
  265. package/dist/chunk-H3MPSYCH.js.map +0 -1
  266. package/dist/chunk-HCK2DFEK.js.map +0 -1
  267. package/dist/chunk-I6Q7YECW.js.map +0 -1
  268. package/dist/chunk-JIW3Q5JN.js.map +0 -1
  269. package/dist/chunk-OJ64XMPJ.js.map +0 -1
  270. package/dist/chunk-QYNBPDGO.js.map +0 -1
  271. package/dist/chunk-RUIMKQTA.js.map +0 -1
  272. package/dist/chunk-UB3XHGFG.js.map +0 -1
  273. package/dist/chunk-VK6OC22R.js.map +0 -1
  274. package/dist/chunk-VMRXI3IF.js.map +0 -1
  275. package/dist/chunk-WFBZRXS5.js.map +0 -1
  276. package/dist/chunk-YSJG4L4A.js.map +0 -1
  277. package/dist/chunk-YSU2E4GZ.js.map +0 -1
  278. package/dist/chunk-YTCCC6NG.js.map +0 -1
  279. package/dist/collection-facade-JP6N7USE.js +0 -39
  280. package/dist/config-drift-LRKCC3VZ.js.map +0 -1
  281. package/dist/delegation-FDOXXHNG.js +0 -25
  282. package/dist/derive-JR2C5JFG.js +0 -21
  283. package/dist/describe-C6iHNlqJ.d.ts +0 -162
  284. package/dist/executor-6LHOV7MN.js +0 -21
  285. package/dist/executor-EUPYHCLU.js +0 -9
  286. package/dist/executor-KZ4B3PRT.js +0 -9
  287. package/dist/export-accessible-7XLSZBGJ.js +0 -23
  288. package/dist/extract-partition-RM6EROS5.js +0 -36
  289. package/dist/find-OH4BEULF.js +0 -11
  290. package/dist/issue-FJE2IHUI.js +0 -19
  291. package/dist/liberate-HFIPHJGR.js +0 -24
  292. package/dist/noydb-4ZASGII5.js +0 -62
  293. package/dist/register-5NRE7HBZ.js +0 -23
  294. package/dist/registry-5QLETK3Q.js +0 -17
  295. package/dist/registry-DYTNLOJC.js +0 -19
  296. package/dist/registry-HSNQ4NBP.js +0 -9
  297. package/dist/request-withdrawal-5APB7RKN.js +0 -31
  298. package/dist/revoke-VELFCK7S.js +0 -24
  299. package/dist/signer-IVU3B6DP.js +0 -25
  300. package/dist/storage-RH5VBLLL.js +0 -23
  301. package/dist/withdraw-accessible-LN4AOJDD.js +0 -28
  302. /package/dist/{api-Z66JMGOU.js.map → api-HV4PDUWF.js.map} +0 -0
  303. /package/dist/{backup-7PX6NDKT.js.map → backup-ODXO7OGA.js.map} +0 -0
  304. /package/dist/{chunk-XW5LZYV4.js.map → chunk-2HVUKRNE.js.map} +0 -0
  305. /package/dist/{chunk-VFQOE76I.js.map → chunk-2KOSJTMU.js.map} +0 -0
  306. /package/dist/{chunk-7AERMJP6.js.map → chunk-2UQ4RMHV.js.map} +0 -0
  307. /package/dist/{chunk-VIWOGJ2O.js.map → chunk-34RRRIC3.js.map} +0 -0
  308. /package/dist/{chunk-LFGJG4XZ.js.map → chunk-3I4KYWNP.js.map} +0 -0
  309. /package/dist/{chunk-RUWEHFRB.js.map → chunk-42KVY3OC.js.map} +0 -0
  310. /package/dist/{chunk-J54DC5IC.js.map → chunk-4G3IT7J7.js.map} +0 -0
  311. /package/dist/{chunk-WJEMUO32.js.map → chunk-4RQ62VSR.js.map} +0 -0
  312. /package/dist/{chunk-QAVFI23O.js.map → chunk-4ZSGSRBI.js.map} +0 -0
  313. /package/dist/{chunk-C4NOLA5X.js.map → chunk-5GAX2QOH.js.map} +0 -0
  314. /package/dist/{chunk-IEKK2XI4.js.map → chunk-5SAZMBW7.js.map} +0 -0
  315. /package/dist/{chunk-II6DLDZM.js.map → chunk-5TZZUL5X.js.map} +0 -0
  316. /package/dist/{chunk-GMZFHDSG.js.map → chunk-6CU3FN74.js.map} +0 -0
  317. /package/dist/{chunk-UWZ7O4NR.js.map → chunk-6MWUANRF.js.map} +0 -0
  318. /package/dist/{chunk-U7LV2DUN.js.map → chunk-6ULCYCMU.js.map} +0 -0
  319. /package/dist/{chunk-VFG5YXWM.js.map → chunk-7IZQTCTR.js.map} +0 -0
  320. /package/dist/{chunk-U5IJHUHZ.js.map → chunk-A4MAD6PO.js.map} +0 -0
  321. /package/dist/{chunk-5HZULLHE.js.map → chunk-AUR7MRHA.js.map} +0 -0
  322. /package/dist/{chunk-H7WQMGXH.js.map → chunk-CJEMA2Z6.js.map} +0 -0
  323. /package/dist/{chunk-WRLPV6SI.js.map → chunk-CTSFKDGA.js.map} +0 -0
  324. /package/dist/{chunk-OFYY5Q4R.js.map → chunk-CWCCYH34.js.map} +0 -0
  325. /package/dist/{chunk-WGH3YV6Y.js.map → chunk-DEYAO3N6.js.map} +0 -0
  326. /package/dist/{chunk-7L6EH6S7.js.map → chunk-DFIBOSOQ.js.map} +0 -0
  327. /package/dist/{chunk-3CHNKEFI.js.map → chunk-E5AHO65E.js.map} +0 -0
  328. /package/dist/{chunk-AG2PLGQ2.js.map → chunk-FC3XK5JT.js.map} +0 -0
  329. /package/dist/{chunk-DJ4TNF4N.js.map → chunk-GX2BYNCT.js.map} +0 -0
  330. /package/dist/{chunk-55LSDABW.js.map → chunk-HBTT7YQX.js.map} +0 -0
  331. /package/dist/{chunk-PYIUDUGS.js.map → chunk-HOCEGVGQ.js.map} +0 -0
  332. /package/dist/{chunk-5JQMX2EH.js.map → chunk-ICV32FPK.js.map} +0 -0
  333. /package/dist/{chunk-OWGAXPRE.js.map → chunk-IWTYFS3D.js.map} +0 -0
  334. /package/dist/{chunk-FCOZ7DEC.js.map → chunk-JC5R7P44.js.map} +0 -0
  335. /package/dist/{chunk-K6WHZBCO.js.map → chunk-KBBWY46Y.js.map} +0 -0
  336. /package/dist/{chunk-RNYDHIQL.js.map → chunk-KBF5GSGX.js.map} +0 -0
  337. /package/dist/{chunk-T4NQMPBN.js.map → chunk-KNXN47MO.js.map} +0 -0
  338. /package/dist/{chunk-7F2P2NHX.js.map → chunk-KXKLKJW4.js.map} +0 -0
  339. /package/dist/{chunk-PAJCRV7O.js.map → chunk-KYC4LIEU.js.map} +0 -0
  340. /package/dist/{chunk-AZPGB4OD.js.map → chunk-LEOXZY36.js.map} +0 -0
  341. /package/dist/{chunk-2L7PPF2C.js.map → chunk-M5I354EE.js.map} +0 -0
  342. /package/dist/{chunk-26FSVNBL.js.map → chunk-MK2BSS44.js.map} +0 -0
  343. /package/dist/{chunk-G5CFMITA.js.map → chunk-MSVY7ULZ.js.map} +0 -0
  344. /package/dist/{chunk-PE25C4QL.js.map → chunk-NNWK3PSY.js.map} +0 -0
  345. /package/dist/{chunk-JT4A6OAJ.js.map → chunk-NSZ6J5DF.js.map} +0 -0
  346. /package/dist/{chunk-RHB7BOEO.js.map → chunk-NUEFXLZU.js.map} +0 -0
  347. /package/dist/{chunk-DKGZIX6S.js.map → chunk-OQAODTYR.js.map} +0 -0
  348. /package/dist/{chunk-HV3VNAYI.js.map → chunk-OV4CMJGR.js.map} +0 -0
  349. /package/dist/{chunk-V76PHWLE.js.map → chunk-PEDZFFKW.js.map} +0 -0
  350. /package/dist/{chunk-CND5WQLO.js.map → chunk-Q2KVKTC2.js.map} +0 -0
  351. /package/dist/{chunk-HBVEGUON.js.map → chunk-QTXQ6RNZ.js.map} +0 -0
  352. /package/dist/{chunk-J5YDMJHG.js.map → chunk-RJ5522XX.js.map} +0 -0
  353. /package/dist/{chunk-CHK5VVMI.js.map → chunk-S6AQXDN4.js.map} +0 -0
  354. /package/dist/{chunk-MOQ5M46U.js.map → chunk-SZCHEDML.js.map} +0 -0
  355. /package/dist/{chunk-VUHXLL32.js.map → chunk-VJ3NQK4K.js.map} +0 -0
  356. /package/dist/{chunk-62TTFKU4.js.map → chunk-WNA6T3MA.js.map} +0 -0
  357. /package/dist/{chunk-7CZ6YXMO.js.map → chunk-WVCQ75OK.js.map} +0 -0
  358. /package/dist/{chunk-NO53W6O5.js.map → chunk-X66MXMR5.js.map} +0 -0
  359. /package/dist/{chunk-6PALJTC2.js.map → chunk-XJK2WIYN.js.map} +0 -0
  360. /package/dist/{chunk-5UOPIPZC.js.map → chunk-XQ7UYPVN.js.map} +0 -0
  361. /package/dist/{chunk-HU55NPHZ.js.map → chunk-Y2OCKFZ3.js.map} +0 -0
  362. /package/dist/{chunk-UJJJPKJO.js.map → chunk-YBFOBRTO.js.map} +0 -0
  363. /package/dist/{chunk-I6JJ6VJS.js.map → chunk-YLN33ADH.js.map} +0 -0
  364. /package/dist/{chunk-HGQ6UNHE.js.map → chunk-Z5YFRMG5.js.map} +0 -0
  365. /package/dist/{chunk-EU7HDMT3.js.map → chunk-ZWQFOXBR.js.map} +0 -0
  366. /package/dist/{collection-facade-JP6N7USE.js.map → collection-facade-3RAXG2OU.js.map} +0 -0
  367. /package/dist/{computed-6FOFXQPE.js.map → computed-R62HGCO4.js.map} +0 -0
  368. /package/dist/{dead-filter-NZORV45S.js.map → dead-filter-VNA4M3GP.js.map} +0 -0
  369. /package/dist/{delegation-FDOXXHNG.js.map → delegation-ZKII5UBI.js.map} +0 -0
  370. /package/dist/{derive-JR2C5JFG.js.map → derive-5D2C3WG4.js.map} +0 -0
  371. /package/dist/{enclave-7X5BUAMS.js.map → enclave-EUBEJGPG.js.map} +0 -0
  372. /package/dist/{executor-6LHOV7MN.js.map → executor-IL7SRNUZ.js.map} +0 -0
  373. /package/dist/{executor-EUPYHCLU.js.map → executor-OUA2Y6YC.js.map} +0 -0
  374. /package/dist/{executor-KZ4B3PRT.js.map → executor-WAEI6PDV.js.map} +0 -0
  375. /package/dist/{export-accessible-7XLSZBGJ.js.map → export-accessible-O55RKAGC.js.map} +0 -0
  376. /package/dist/{extract-partition-RM6EROS5.js.map → extract-partition-VBGCBA6Q.js.map} +0 -0
  377. /package/dist/{fanout-sidecar-BDKLP5GG.js.map → fanout-sidecar-CI5BCMQM.js.map} +0 -0
  378. /package/dist/{find-OH4BEULF.js.map → find-FISGUWSK.js.map} +0 -0
  379. /package/dist/{issue-FJE2IHUI.js.map → issue-EQAWIYFQ.js.map} +0 -0
  380. /package/dist/{ledger-EMDHRJJN.js.map → ledger-AJA2SR3C.js.map} +0 -0
  381. /package/dist/{liberate-HFIPHJGR.js.map → liberate-SEGR4JLY.js.map} +0 -0
  382. /package/dist/{link-set-62Y36TOG.js.map → link-set-YBAC2DQT.js.map} +0 -0
  383. /package/dist/{noydb-4ZASGII5.js.map → noydb-AMEOU456.js.map} +0 -0
  384. /package/dist/{policy-SXPLTQS4.js.map → policy-SDGRWVIL.js.map} +0 -0
  385. /package/dist/{post-register-DCJ2SRVG.js.map → post-register-MROIUA5O.js.map} +0 -0
  386. /package/dist/{public-envelope-CBRIJORQ.js.map → public-envelope-J5PLQOW2.js.map} +0 -0
  387. /package/dist/{read-only-facade-5ADRJVTM.js.map → read-only-facade-UCYNRE56.js.map} +0 -0
  388. /package/dist/{register-5NRE7HBZ.js.map → register-OVJKW7BS.js.map} +0 -0
  389. /package/dist/{registry-5QLETK3Q.js.map → registry-5RJSK5AJ.js.map} +0 -0
  390. /package/dist/{registry-DYTNLOJC.js.map → registry-DQRNTWRL.js.map} +0 -0
  391. /package/dist/{registry-HSNQ4NBP.js.map → registry-LTGLERS7.js.map} +0 -0
  392. /package/dist/{request-withdrawal-5APB7RKN.js.map → request-withdrawal-27O77D2V.js.map} +0 -0
  393. /package/dist/{reveal-R5N7JKA5.js.map → reveal-LT3I2K4P.js.map} +0 -0
  394. /package/dist/{revoke-VELFCK7S.js.map → revoke-4N45G7D2.js.map} +0 -0
  395. /package/dist/{signer-IVU3B6DP.js.map → signer-XJCOV7JD.js.map} +0 -0
  396. /package/dist/{stale-DP66HFT3.js.map → stale-IYYXEH3U.js.map} +0 -0
  397. /package/dist/{storage-RH5VBLLL.js.map → storage-Y44CAPE6.js.map} +0 -0
  398. /package/dist/{store-coordination-provider-F5N5ASDF.js.map → store-coordination-provider-WKGAE2WI.js.map} +0 -0
  399. /package/dist/{verify-CAYGXYNO.js.map → verify-GH6CWVSW.js.map} +0 -0
  400. /package/dist/{walk-3SB25HF2.js.map → walk-76UCFXYV.js.map} +0 -0
  401. /package/dist/{withdraw-accessible-LN4AOJDD.js.map → withdraw-accessible-6J2UH3IN.js.map} +0 -0
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  loadVaultPolicy
3
- } from "./chunk-NO53W6O5.js";
3
+ } from "./chunk-X66MXMR5.js";
4
4
  import {
5
5
  jsonSchemaToFields
6
6
  } from "./chunk-TQ3REHVF.js";
@@ -9,31 +9,31 @@ import {
9
9
  SEQUENCE_COLLECTION,
10
10
  compileSequenceFormat,
11
11
  resolveSequenceKey
12
- } from "./chunk-IEKK2XI4.js";
12
+ } from "./chunk-5SAZMBW7.js";
13
13
  import {
14
14
  LinkIntegrityError,
15
15
  isLinkCollectionName,
16
16
  linkCollectionName,
17
17
  linkRowKey
18
- } from "./chunk-V76PHWLE.js";
18
+ } from "./chunk-PEDZFFKW.js";
19
19
  import {
20
20
  hashFields,
21
21
  validateSatelliteDeclaration
22
- } from "./chunk-5JQMX2EH.js";
22
+ } from "./chunk-ICV32FPK.js";
23
23
  import {
24
24
  loadFence
25
- } from "./chunk-WRLPV6SI.js";
25
+ } from "./chunk-CTSFKDGA.js";
26
26
  import {
27
27
  InvertedIndex,
28
28
  MemoryIndexStore,
29
29
  NO_SEARCH,
30
30
  VectorSet,
31
31
  buildPersistedIndexCallbacks
32
- } from "./chunk-CP3GUIAN.js";
32
+ } from "./chunk-FGOGS7CE.js";
33
33
  import {
34
34
  derivePersistedSchema,
35
35
  isZod4Schema
36
- } from "./chunk-55LSDABW.js";
36
+ } from "./chunk-HBTT7YQX.js";
37
37
  import {
38
38
  isBaseLive,
39
39
  liveBaseIdSet
@@ -42,7 +42,7 @@ import {
42
42
  CustodyApi,
43
43
  NO_CARGO,
44
44
  NO_CUSTODY
45
- } from "./chunk-RUWEHFRB.js";
45
+ } from "./chunk-42KVY3OC.js";
46
46
  import {
47
47
  ServiceBus,
48
48
  WriteHookRegistry,
@@ -50,45 +50,59 @@ import {
50
50
  assertCanImport,
51
51
  canExport,
52
52
  canImport
53
- } from "./chunk-HBVEGUON.js";
53
+ } from "./chunk-QTXQ6RNZ.js";
54
54
  import {
55
55
  NO_ATTESTATION
56
- } from "./chunk-LFGJG4XZ.js";
56
+ } from "./chunk-3I4KYWNP.js";
57
57
  import {
58
58
  NO_CLASSIFIED,
59
- enforceClassifiedWrite,
59
+ guardClassifiedCompat,
60
60
  resolveClassifiedFields
61
- } from "./chunk-47C4CI7R.js";
61
+ } from "./chunk-V7RNIND6.js";
62
62
  import {
63
63
  NO_TIERS,
64
64
  classifySealedShred
65
- } from "./chunk-E3LURVGQ.js";
65
+ } from "./chunk-SSFC3IYA.js";
66
66
  import {
67
67
  NO_PORTABILITY
68
- } from "./chunk-II6DLDZM.js";
68
+ } from "./chunk-5TZZUL5X.js";
69
69
  import {
70
70
  runDrainBarrier
71
- } from "./chunk-EU7HDMT3.js";
71
+ } from "./chunk-ZWQFOXBR.js";
72
72
  import {
73
- PERIODS_COLLECTION
74
- } from "./chunk-YSU2E4GZ.js";
73
+ PERIODS_COLLECTION,
74
+ PERIOD_ARCHIVES_COLLECTION,
75
+ PERIOD_FREEZES_COLLECTION,
76
+ PERIOD_TARGET_PURGES_COLLECTION,
77
+ periodExclusiveUpperBound
78
+ } from "./chunk-IUKGIB2Q.js";
75
79
  import {
76
80
  TxContext,
77
81
  revertExecuted
78
- } from "./chunk-FCOZ7DEC.js";
82
+ } from "./chunk-JC5R7P44.js";
83
+ import {
84
+ DEFAULT_POSTURE,
85
+ ViaGraph
86
+ } from "./chunk-7ZANJ6JZ.js";
87
+ import {
88
+ forgetDerivedFanout,
89
+ ledgerAuditHook,
90
+ putDerivedOutput,
91
+ runGraphDispatchWave
92
+ } from "./chunk-H57P2SJB.js";
79
93
  import {
80
94
  OverlayedCollection
81
- } from "./chunk-MOQ5M46U.js";
95
+ } from "./chunk-SZCHEDML.js";
82
96
  import {
83
97
  EXPORT_AUDIT_COLLECTION,
84
98
  createExportBlobsHandle,
85
99
  runCompaction
86
- } from "./chunk-I6Q7YECW.js";
100
+ } from "./chunk-NYVQCZ54.js";
87
101
  import {
88
102
  LazyQuery,
89
103
  decodeIdxId,
90
104
  encodeIdxId
91
- } from "./chunk-6PALJTC2.js";
105
+ } from "./chunk-XJK2WIYN.js";
92
106
  import {
93
107
  IMPLICIT_LAZY,
94
108
  Lru,
@@ -97,11 +111,10 @@ import {
97
111
  import {
98
112
  resolveManagedSecret,
99
113
  saveSealedPassphrase
100
- } from "./chunk-H7WQMGXH.js";
114
+ } from "./chunk-CJEMA2Z6.js";
101
115
  import {
102
- isDictCollectionName,
103
- isStaticDictDescriptor
104
- } from "./chunk-WFBZRXS5.js";
116
+ dictCollectionName
117
+ } from "./chunk-IIP5WFVT.js";
105
118
  import {
106
119
  NO_TEAM,
107
120
  enrollAuthenticator,
@@ -112,7 +125,7 @@ import {
112
125
  rotatePassphrase,
113
126
  updateAuthenticator,
114
127
  writeMagicLinkGrant
115
- } from "./chunk-WGH3YV6Y.js";
128
+ } from "./chunk-DEYAO3N6.js";
116
129
  import {
117
130
  hasRecoveryEnrolled,
118
131
  hasStrongRecoveryEnrolled,
@@ -122,7 +135,7 @@ import {
122
135
  mintShamirRecoveryEntry,
123
136
  savePaperRecoveryEntries,
124
137
  saveShamirRecoveryEntries
125
- } from "./chunk-VUHXLL32.js";
138
+ } from "./chunk-VJ3NQK4K.js";
126
139
  import {
127
140
  INDEXED_STORE_POLICY
128
141
  } from "./chunk-VQNJ7UZL.js";
@@ -138,17 +151,17 @@ import {
138
151
  listUsers,
139
152
  loadKeyring,
140
153
  updateKeyringIdentity
141
- } from "./chunk-GMZFHDSG.js";
154
+ } from "./chunk-6CU3FN74.js";
142
155
  import {
143
156
  persistDirectoryConfig,
144
157
  readDirectoryConfig
145
- } from "./chunk-3CHNKEFI.js";
158
+ } from "./chunk-E5AHO65E.js";
146
159
  import {
147
160
  generateULID
148
161
  } from "./chunk-ZJADGNYF.js";
149
162
  import {
150
163
  sha256Hex
151
- } from "./chunk-G5CFMITA.js";
164
+ } from "./chunk-MSVY7ULZ.js";
152
165
  import {
153
166
  NO_FORGET,
154
167
  addSubjectRef,
@@ -157,50 +170,59 @@ import {
157
170
  readDottedPath,
158
171
  rebuildSubjectIndex,
159
172
  removeSubjectRef
160
- } from "./chunk-VMRXI3IF.js";
173
+ } from "./chunk-A2APWS32.js";
161
174
  import {
162
175
  NO_SEALED_RECORD
163
- } from "./chunk-XW5LZYV4.js";
176
+ } from "./chunk-2HVUKRNE.js";
164
177
  import {
165
178
  RecordCodec,
166
179
  SEALED_CEK_NS,
167
180
  findByDet,
168
181
  hasPerRecordKey,
182
+ makeReservedEnvelopes,
169
183
  openEnvelopeJson,
170
184
  queryByDet,
171
185
  resolveStableCek,
172
186
  validateSchemaInput
173
- } from "./chunk-JIW3Q5JN.js";
187
+ } from "./chunk-XHD3L3JO.js";
174
188
  import {
175
189
  buildDeleteMarker,
176
190
  buildTombstone,
177
191
  isDeleteMarker,
178
192
  isTombstone
179
- } from "./chunk-HGQ6UNHE.js";
180
- import {
181
- NOYDB_FORMAT_VERSION
182
- } from "./chunk-HCK2DFEK.js";
193
+ } from "./chunk-Z5YFRMG5.js";
183
194
  import {
184
195
  encrypt
185
- } from "./chunk-5HZULLHE.js";
196
+ } from "./chunk-AUR7MRHA.js";
197
+ import {
198
+ NOYDB_FORMAT_VERSION,
199
+ SealedHandle
200
+ } from "./chunk-UNJU345Q.js";
186
201
  import {
187
202
  NO_AGGREGATE,
188
203
  Query,
189
204
  ScanBuilder
190
- } from "./chunk-6Z435B6N.js";
205
+ } from "./chunk-7HAPHVRJ.js";
191
206
  import {
192
207
  canonicalGroupKey
193
- } from "./chunk-5GQE5IDB.js";
208
+ } from "./chunk-2XE6LIXT.js";
194
209
  import {
195
- moneyRuntime,
196
210
  readPath
197
- } from "./chunk-6P5AJ2ZS.js";
211
+ } from "./chunk-KCG3BHFG.js";
198
212
  import {
213
+ NO_I18N,
199
214
  getAtPath,
200
- resolvePolicy,
201
- setAtPathInPlace,
202
- stripI18nFilled
203
- } from "./chunk-BASCPHNM.js";
215
+ isDictCollectionName,
216
+ isDictKeyDescriptor,
217
+ isI18nTextDescriptor,
218
+ isStaticDictDescriptor,
219
+ presentI18nForJoin
220
+ } from "./chunk-3C5CKXFI.js";
221
+ import {
222
+ installViaBinder,
223
+ isViaInstalled,
224
+ viaBinder
225
+ } from "./chunk-6G2SCQYS.js";
204
226
  import {
205
227
  AlreadyElevatedError,
206
228
  AttestationError,
@@ -210,12 +232,13 @@ import {
210
232
  ConflictError,
211
233
  DebugPlaintextError,
212
234
  DerivationCapExceededError,
235
+ DictKeyInUseError,
213
236
  ElevationExpiredError,
237
+ FieldNotQueryableError,
214
238
  ForgetStrategyNotConfiguredError,
215
239
  IndexWriteFailureError,
216
240
  InvalidKeyError,
217
241
  KeyringCorruptError,
218
- LocaleNotSpecifiedError,
219
242
  ManagedRecoveryNotEnrolledError,
220
243
  MigrationRequiredError,
221
244
  NoAccessError,
@@ -232,85 +255,281 @@ import {
232
255
  StaticDictReadonlyError,
233
256
  StoreCapabilityError,
234
257
  TierNotGrantedError,
235
- TranslatorNotConfiguredError,
236
258
  USER_ENVELOPE_COLLECTION,
237
259
  UniqueConstraintError,
238
260
  UnknownDictCodeError,
239
261
  UnsupportedIndexOptionError,
240
262
  ValidationError
241
- } from "./chunk-OJ64XMPJ.js";
263
+ } from "./chunk-CU62BBRB.js";
242
264
 
243
265
  // src/port/by/default-provider.ts
244
266
  async function createDefaultCoordinationProvider(store) {
245
- const { StoreCoordinationProvider } = await import("./store-coordination-provider-F5N5ASDF.js");
267
+ const { StoreCoordinationProvider } = await import("./store-coordination-provider-WKGAE2WI.js");
246
268
  return new StoreCoordinationProvider(store);
247
269
  }
248
270
 
249
- // src/with-shape/classified/guards.ts
250
- function guardClassifiedCompat(collection, byField, ctx) {
251
- const digestOnly = Object.entries(byField).filter(([, s]) => s.storage === "digest-only");
252
- const protectedForms = Object.values(byField).filter((s) => s.storage === "digest-only" || s.storage === "recoverable");
253
- if (protectedForms.length > 0 && (ctx.crdt || ctx.hasConflictPolicy)) {
254
- throw new ClassifiedConfigError(
255
- collection,
256
- "digest-only/recoverable classified fields cannot combine with a crdt mode or a conflictPolicy resolver \u2014 merge paths bypass write enforcement (R2)"
257
- );
258
- }
259
- for (const [f, spec] of Object.entries(byField)) {
260
- if (spec.equatable === true && spec.storage !== "digest-only") {
261
- throw new ClassifiedConfigError(
262
- collection,
263
- `field "${f}" declares equatable but storage is '${spec.storage}' \u2014 equatable is a digest-only knob; recoverable equality is _det's job (R7)`
264
- );
271
+ // src/kernel/via-pipeline.ts
272
+ var ViaPipeline = class _ViaPipeline {
273
+ constructor(bindings, taint) {
274
+ this.bindings = bindings;
275
+ this.taint = taint;
276
+ }
277
+ bindings;
278
+ taint;
279
+ /** undefined when there is nothing to enforce — the zero-via fast path is
280
+ * `this.via === undefined` (#553: keeps an all-plain collection sync). */
281
+ static build(bindings, taint) {
282
+ if (bindings.length === 0 && (!taint || taint.postures.size === 0)) return void 0;
283
+ return new _ViaPipeline(bindings, taint);
284
+ }
285
+ /** Refuse a write before crypto runs: awaits each binding's `enforceWrite` in order first throw wins. */
286
+ async enforceWrite(record, ctx) {
287
+ for (const b of this.bindings) if (b.enforceWrite) await b.enforceWrite(record, ctx);
288
+ }
289
+ ingest(record) {
290
+ let r = record;
291
+ for (const b of this.bindings) if (b.ingest) r = b.ingest(r);
292
+ return r;
293
+ }
294
+ canonicalizeStored(record) {
295
+ let r = record;
296
+ for (const b of this.bindings) if (b.canonicalizeStored) r = b.canonicalizeStored(r);
297
+ return r;
298
+ }
299
+ async encodeWrite(record, ctx) {
300
+ let r = record;
301
+ for (const b of this.bindings) if (b.encodeWrite) r = await b.encodeWrite(r, ctx);
302
+ return r;
303
+ }
304
+ /**
305
+ * Final write-pipeline stage: folds each binding's `encodeAtRest` in
306
+ * order, threading the record and accumulating the sealed-slot map. A
307
+ * field belongs to exactly one via feature — two bindings sealing the
308
+ * same field is a brand-keyed collision and throws.
309
+ */
310
+ async encodeAtRest(record, crypto) {
311
+ let r = record;
312
+ let sealed;
313
+ const sealedBy = /* @__PURE__ */ new Map();
314
+ for (const b of this.bindings) {
315
+ if (!b.encodeAtRest) continue;
316
+ const result = await b.encodeAtRest(r, crypto);
317
+ r = result.record;
318
+ if (!result.sealed) continue;
319
+ for (const [field, ref2] of Object.entries(result.sealed)) {
320
+ const priorBrand = sealedBy.get(field);
321
+ if (priorBrand !== void 0) {
322
+ throw new ValidationError(
323
+ `via encodeAtRest: field "${field}" sealed by binding "${priorBrand}" is also sealed by binding "${b.brand}"`
324
+ );
325
+ }
326
+ sealedBy.set(field, b.brand);
327
+ sealed ??= {};
328
+ sealed[field] = ref2;
329
+ }
265
330
  }
331
+ return sealed ? { record: r, sealed } : { record: r };
266
332
  }
267
- const hasEquatable = Object.values(byField).some((s) => s.equatable === true);
268
- if (hasEquatable && ctx.acknowledgeEquatableRisk !== true) {
269
- throw new ClassifiedConfigError(
270
- collection,
271
- `classified equatable fields require \`acknowledgeEquatableRisk: true\` \u2014 equal values produce equal store-visible index tags (a partition leak a DEK holder can offline-dictionary at the PBKDF2 floor); the door is the real control for low-entropy fields (R8)`
272
- );
333
+ /** First read-pipeline stage: folds each binding's `decodeAtRest` in order. */
334
+ async decodeAtRest(record, sealed, crypto, opts) {
335
+ let r = record;
336
+ for (const b of this.bindings) if (b.decodeAtRest) r = await b.decodeAtRest(r, sealed, crypto, opts);
337
+ return r;
273
338
  }
274
- if (digestOnly.length === 0) return;
275
- if (!ctx.storeCiphertext) {
276
- throw new ClassifiedConfigError(
277
- collection,
278
- `storage:'digest-only' requires an encrypted collection \u2014 with encrypt: false the write path skips the digest slot entirely and the secret would persist as raw plaintext`
279
- );
339
+ async present(record, ctx) {
340
+ let r = record;
341
+ for (const b of this.bindings) if (b.present) r = await b.present(r, ctx);
342
+ return r;
343
+ }
344
+ buildClause(field, op, value) {
345
+ for (const b of this.bindings) {
346
+ const payload = b.buildClause?.(field, op, value);
347
+ if (payload !== void 0) return { brand: b.brand, payload };
348
+ }
349
+ return void 0;
350
+ }
351
+ evaluateClause(clause, actual, op) {
352
+ const b = this.bindings.find((x) => x.brand === clause.brand);
353
+ return b?.evaluateClause ? b.evaluateClause(actual, op, clause.payload) : false;
354
+ }
355
+ decodeResults(record) {
356
+ let r = record;
357
+ for (const b of this.bindings) if (b.decodeResults) r = b.decodeResults(r);
358
+ return r;
280
359
  }
281
- if (!ctx.perRecordKeys) {
282
- throw new ClassifiedConfigError(
283
- collection,
284
- `storage:'digest-only' requires perRecordKeys: true \u2014 vdig keys derive from the per-record CEK so forget() shreds them (R1)`
285
- );
360
+ /** undefined = no binding covers the field → caller falls back to generic compare. */
361
+ compareForOrder(field, a, b) {
362
+ for (const bind of this.bindings) {
363
+ const c = bind.compareForOrder?.(field, a, b);
364
+ if (c !== void 0) return c;
365
+ }
366
+ return void 0;
367
+ }
368
+ /** undefined = no binding resolves an order-sort label for `key` at `field`/`locale` (#650 Task 7 —
369
+ * the `orderBy(..., {by:'label'})` per-call-locale channel `compareForOrder` above can't serve). */
370
+ resolveOrderLabel(field, key, locale) {
371
+ for (const bind of this.bindings) {
372
+ const label = bind.resolveOrderLabel?.(field, key, locale);
373
+ if (label !== void 0) return label;
374
+ }
375
+ return void 0;
376
+ }
377
+ /**
378
+ * Which posture governs `field`, if any binding covers it — `undefined`
379
+ * means no binding declares `field` (the generic, non-via query path
380
+ * applies). #629 Task 8's posture consumer: `.where()`/`.orderBy()`
381
+ * consult this before building/evaluating a clause, refusing fields whose
382
+ * posture is `queryable: 'none'` (e.g. blob); every other posture
383
+ * ('det-exact'/'ordered'/'full') is left to the existing per-binding
384
+ * buildClause/evaluateClause/compareForOrder machinery, unchanged.
385
+ *
386
+ * #638 Task 3: the graph-computed taint overlay is consulted FIRST — a
387
+ * derived field's assigned (most-restrictive-of-sources) posture wins over
388
+ * whatever a binding would otherwise report for that field name (no
389
+ * binding covers a computed/derived field today, so this never actually
390
+ * shadows one — see `via-taint-binding.ts`'s `taintBinding.covers`, which
391
+ * only claims the sealed subset for `encodeAtRest`/`decodeAtRest`, not for
392
+ * this lookup).
393
+ */
394
+ postureFor(field) {
395
+ const t = this.taint?.postures.get(field);
396
+ if (t) return t;
397
+ for (const b of this.bindings) {
398
+ if (b.covers?.(field)) return b.posture;
399
+ }
400
+ return void 0;
401
+ }
402
+ /**
403
+ * Refuse any field-based reducer over a `queryable: 'none'` field (blob) —
404
+ * metadata-only: walks each reducer's `.field` and checks posture, no
405
+ * rewriting. Reducers with no `.field` (e.g. `count()`) are skipped.
406
+ *
407
+ * Shared by `wrapReducers` below (which ALSO applies each binding's
408
+ * rewrite, e.g. money's exact-BigInt reducer swap) and by
409
+ * `ScanBuilder.aggregate()` (#629 Task 8 review fix wave 1), which must
410
+ * call THIS, not `wrapReducers` — wiring full `wrapReducers` into
411
+ * `ScanBuilder.aggregate()` would newly activate money/i18n reducer
412
+ * wrapping on a path that has never run it, a parity-breaking behavior
413
+ * change for existing brands.
414
+ */
415
+ refuseUnqueryableReducers(spec) {
416
+ for (const reducer of Object.values(spec)) {
417
+ const field = reducer?.field;
418
+ if (field !== void 0 && this.postureFor(field)?.queryable === "none") {
419
+ throw new FieldNotQueryableError(field);
420
+ }
421
+ }
422
+ }
423
+ /**
424
+ * Rewrite an aggregate spec (money exact reducers), then refuse any
425
+ * field-based reducer over a `queryable: 'none'` field (blob) — the same
426
+ * posture gate `.where()`/`.orderBy()` apply, extended to `.aggregate()`
427
+ * (both the bare-spec and builder forms funnel through here).
428
+ */
429
+ wrapReducers(spec) {
430
+ this.refuseUnqueryableReducers(spec);
431
+ let s = spec;
432
+ for (const b of this.bindings) if (b.wrapReducers) s = b.wrapReducers(s);
433
+ return s;
434
+ }
435
+ /**
436
+ * Deliberate export-layer redaction (#629 Task 9's posture consumer):
437
+ * for every OWN field the decoded record carries, if the covering
438
+ * binding's posture declares `exportable: false` (classified today;
439
+ * money/i18n/blob are all `true`), replace the value with
440
+ * {@link EXPORT_REDACTION_MARKER} — the same `'[sealed]'` string
441
+ * `SealedHandle.toJSON()` (`kernel/types.ts`) already emits, so this is
442
+ * byte-parity with today's accidental redaction, now independent of the
443
+ * field's runtime shape. `SealedHandle.toJSON()` stays untouched as
444
+ * defense-in-depth: a record read via `collection.get()` and
445
+ * JSON.stringify'd outside `exportStream()` still gets the accident's
446
+ * redaction on its own. Never mutates the input; returns a new object
447
+ * only when a field was actually redacted.
448
+ */
449
+ redactForExport(record) {
450
+ let out;
451
+ for (const field of Object.keys(record)) {
452
+ if (this.postureFor(field)?.exportable === false) {
453
+ out ??= { ...record };
454
+ out[field] = EXPORT_REDACTION_MARKER;
455
+ }
456
+ }
457
+ return out ?? record;
458
+ }
459
+ /**
460
+ * Shared fold over a subset of `this.bindings`: runs each binding's
461
+ * `erase`, concatenating shredded/retainedShared counts and residue.
462
+ * Consults `posture.forgettable` (#629 Task 10) — a binding declaring
463
+ * `forgettable: false` is skipped even if it defines `erase` (none does
464
+ * today; a future non-forgettable binding is supported without a brand check).
465
+ */
466
+ async foldErase(ctx, bindings) {
467
+ let shredded = 0;
468
+ let retainedShared = 0;
469
+ const residue = [];
470
+ for (const b of bindings) {
471
+ if (!b.erase || b.posture.forgettable === false) continue;
472
+ const report = await b.erase(ctx);
473
+ shredded += report.shredded;
474
+ retainedShared += report.retainedShared ?? 0;
475
+ residue.push(...report.residue);
476
+ }
477
+ return retainedShared > 0 ? { shredded, residue, retainedShared } : { shredded, residue };
478
+ }
479
+ /** `forget()`'s per-ref erasure door: folds every binding's `erase`. */
480
+ async erase(ctx) {
481
+ return this.foldErase(ctx, this.bindings);
482
+ }
483
+ /**
484
+ * `forget()`'s SEALED-posture-only erase fold (#629 Task 10, `Collection._onViaErase`'s
485
+ * one caller) — classified today; metadata-filtered on
486
+ * `posture.encryptedAtRest`, not brand-checked. `undefined` when no
487
+ * sealed-posture binding is compiled in — `vault.ts` then falls back to
488
+ * its own bare-`sensitive` classification, which no via binding covers.
489
+ */
490
+ async eraseSealed(ctx) {
491
+ const sealed = this.bindings.filter((b) => b.posture.encryptedAtRest === "sealed");
492
+ return sealed.length === 0 ? void 0 : this.foldErase(ctx, sealed);
286
493
  }
287
- for (const [f, spec] of digestOnly) {
288
- const n = spec.notLastN ?? 0;
289
- if (!Number.isInteger(n) || n < 0 || n > 8) {
290
- throw new ClassifiedConfigError(
291
- collection,
292
- `digest-only field "${f}" declares notLastN ${n} \u2014 must be an integer 0..8 (write cost is n \xD7 600K PBKDF2; ring blast radius is documented)`
293
- );
294
- }
295
- if (ctx.bareSensitiveFields.has(f)) {
296
- throw new ClassifiedConfigError(
297
- collection,
298
- `field "${f}" is digest-only AND listed in sensitive[] \u2014 storage forms are mutually exclusive per field (R5)`
299
- );
300
- }
301
- if (ctx.deterministicFields?.has(f)) {
302
- throw new ClassifiedConfigError(
303
- collection,
304
- `digest-only field "${f}" cannot be deterministic \u2014 equality-correlatable ciphertext defeats per-write salts (R3)`
305
- );
306
- }
307
- if (ctx.indexedFields.has(f) || ctx.textIndexFields.has(f) || ctx.vectorSourceFields.has(f) || ctx.subjectKeyField === f) {
308
- throw new ClassifiedConfigError(
309
- collection,
310
- `digest-only field "${f}" cannot be indexed, text-indexed, embedded, or a forget-subject key (R4)`
311
- );
494
+ /** True iff any binding implements decodeResults (query fast-path gating). */
495
+ get hasResultDecode() {
496
+ return this.bindings.some((b) => b.decodeResults !== void 0);
497
+ }
498
+ /**
499
+ * True iff any binding declares an at-rest hook (`encodeAtRest`/
500
+ * `decodeAtRest`) — the codec boundary's async-stack gate (#629 Task 3
501
+ * consults this to choose between the hook path and today's inline
502
+ * sealed-slot path; a stack with only sync hooks, e.g. money-only, stays
503
+ * `false`).
504
+ */
505
+ get hasAtRestHooks() {
506
+ return this.bindings.some((b) => b.encodeAtRest !== void 0 || b.decodeAtRest !== void 0);
507
+ }
508
+ /**
509
+ * Fold every binding's `describeFragment()` into one `brand -> fragment`
510
+ * map (#650 Task 7 — the first-ever consumer; `describeFragment` was
511
+ * declared at `via.ts:136` since #623 with zero callers until this task).
512
+ * `undefined` when no compiled binding implements it. Consumed by
513
+ * `Collection.describe()`/`describeAsync()`, threaded to `buildDescription`
514
+ * as `BuildDescriptionInput.viaFragments`.
515
+ */
516
+ describeFragments() {
517
+ let out;
518
+ for (const b of this.bindings) {
519
+ const f = b.describeFragment?.();
520
+ if (f !== void 0) {
521
+ out ??= {};
522
+ out[b.brand] = f;
523
+ }
312
524
  }
525
+ return out;
313
526
  }
527
+ };
528
+ var EXPORT_REDACTION_MARKER = "[sealed]";
529
+ function exportRedact(coll, record) {
530
+ if (record === null || typeof record !== "object") return record;
531
+ const via2 = coll.via;
532
+ return via2 ? via2.redactForExport(record) : record;
314
533
  }
315
534
 
316
535
  // src/with-lookup/indexing/collection-facade.ts
@@ -398,7 +617,7 @@ async function rebuildIndexes(ctx) {
398
617
  for (const recordId of canonicalIds) {
399
618
  const envelope = await ctx.adapter.get(ctx.vault, ctx.name, recordId);
400
619
  if (!envelope) continue;
401
- const record = await ctx.codec.decryptRecord(envelope, { skipValidation: true });
620
+ const record = await ctx.codec.decryptRecord(envelope, { skipValidation: true, id: recordId });
402
621
  if (record === null) continue;
403
622
  await maintainPersistedIndexesOnPut(ctx, recordId, record, null, envelope._v);
404
623
  }
@@ -451,7 +670,7 @@ async function reconcileIndex(ctx, field, opts = {}) {
451
670
  if (id.startsWith("_")) continue;
452
671
  const env = await ctx.adapter.get(ctx.vault, ctx.name, id);
453
672
  if (!env) continue;
454
- const record = await ctx.codec.decryptRecord(env, { skipValidation: true });
673
+ const record = await ctx.codec.decryptRecord(env, { skipValidation: true, id });
455
674
  if (record === null) continue;
456
675
  const live = readPersistedValue(record, field);
457
676
  const stored = sidecar.get(id);
@@ -875,13 +1094,14 @@ function deriveWidget(opts) {
875
1094
  case "percent":
876
1095
  return "number";
877
1096
  }
878
- if (opts.dict !== void 0) return "select";
1097
+ if (opts.dict !== void 0 || opts.lookup !== void 0) return "select";
879
1098
  if (opts.type === "boolean") return "checkbox";
880
1099
  if (opts.type === "number") return "number";
881
1100
  return "text";
882
1101
  }
883
1102
  function buildDescription(input) {
884
- const { collection, fieldMeta, moneyFields, dictKeyFields, computed, refs, zodFields, dictLabels, meta, i18nFields, classified } = input;
1103
+ const { collection, fieldMeta, moneyFields, dictKeyFields, lookupFields, computed, refs, zodFields, dictLabels, meta, i18nFields, classified, taint } = input;
1104
+ const lookupFragments = input.viaFragments?.["lookup"]?.lookupFields;
885
1105
  if (zodFields !== void 0 && Object.keys(zodFields).length > 0 && fieldMeta !== void 0) {
886
1106
  const knownFields = /* @__PURE__ */ new Set([
887
1107
  ...Object.keys(moneyFields ?? {}),
@@ -889,13 +1109,15 @@ function buildDescription(input) {
889
1109
  ...Object.keys(refs),
890
1110
  ...Object.keys(computed ?? {}),
891
1111
  ...Object.keys(zodFields),
892
- ...Object.keys(i18nFields ?? {})
1112
+ ...Object.keys(i18nFields ?? {}),
1113
+ ...Object.keys(lookupFields ?? {})
893
1114
  ]);
894
1115
  validateFieldMetaKeys(collection, fieldMeta, knownFields);
895
1116
  }
896
1117
  const allKeys = /* @__PURE__ */ new Set([
897
1118
  ...Object.keys(moneyFields ?? {}),
898
1119
  ...Object.keys(dictKeyFields ?? {}),
1120
+ ...Object.keys(lookupFields ?? {}),
899
1121
  ...Object.keys(refs),
900
1122
  ...Object.keys(computed ?? {}),
901
1123
  ...Object.keys(fieldMeta ?? {}),
@@ -908,15 +1130,18 @@ function buildDescription(input) {
908
1130
  const zod = zodFields?.[key];
909
1131
  const money = moneyFields?.[key];
910
1132
  const dict = dictKeyFields?.[key];
1133
+ const lookupDesc = lookupFields?.[key];
911
1134
  const refDesc = refs[key];
912
1135
  const isComputed = computed !== void 0 && key in computed;
913
1136
  const i18nDesc = i18nFields?.[key];
914
1137
  const cls = classified?.[key];
1138
+ const taintPosture = taint?.postures.get(key);
915
1139
  let type = zod?.type ?? "unknown";
916
1140
  const inferred = {};
917
1141
  let moneyBlock;
918
1142
  let dictBlock;
919
1143
  let refBlock;
1144
+ let lookupBlock;
920
1145
  if (money) {
921
1146
  type = "number";
922
1147
  inferred.semanticType = "currency";
@@ -963,10 +1188,49 @@ function buildDescription(input) {
963
1188
  dictBlock = { name: dict.name, static: false };
964
1189
  }
965
1190
  }
1191
+ } else if (lookupDesc) {
1192
+ type = "enum";
1193
+ const lookupLabelMap = dictLabels?.[lookupDesc.dimension];
1194
+ const lookupLabelMapHasEntries = lookupLabelMap !== void 0 && Object.keys(lookupLabelMap).length > 0;
1195
+ if (lookupDesc.table !== void 0) {
1196
+ const displayLocale = lookupDesc.displayLocale;
1197
+ const table = lookupDesc.table;
1198
+ const values = (lookupDesc.keys ?? Object.keys(table)).map((k) => {
1199
+ const localeMap = table[k];
1200
+ const label = displayLocale !== void 0 ? localeMap?.[displayLocale] ?? void 0 : void 0;
1201
+ return label !== void 0 ? { value: k, label } : { value: k };
1202
+ });
1203
+ dictBlock = { name: lookupDesc.dimension, static: true, values };
1204
+ } else if (lookupLabelMapHasEntries) {
1205
+ const values = Object.entries(lookupLabelMap).map(([value, label]) => ({ value, label }));
1206
+ dictBlock = { name: lookupDesc.dimension, static: false, values };
1207
+ } else if (lookupDesc.keys !== void 0) {
1208
+ const values = lookupDesc.keys.map((k) => {
1209
+ const label = lookupDesc.labels?.[k];
1210
+ return label !== void 0 ? { value: k, label } : { value: k };
1211
+ });
1212
+ dictBlock = { name: lookupDesc.dimension, static: false, values };
1213
+ } else {
1214
+ dictBlock = { name: lookupDesc.dimension, static: false };
1215
+ }
966
1216
  } else if (i18nDesc) {
967
1217
  type = "string";
968
1218
  } else if (isComputed) {
969
1219
  }
1220
+ const lookupFragmentEntry = lookupFragments?.[key];
1221
+ if (lookupFragmentEntry !== void 0) {
1222
+ lookupBlock = {
1223
+ ...lookupFragmentEntry.dimension !== void 0 ? { dimension: lookupFragmentEntry.dimension } : {},
1224
+ backing: lookupFragmentEntry.backing,
1225
+ vocabulary: lookupFragmentEntry.vocabulary,
1226
+ key: lookupFragmentEntry.key,
1227
+ onDelete: lookupFragmentEntry.onDelete,
1228
+ ...lookupFragmentEntry.altKeys !== void 0 ? { altKeys: lookupFragmentEntry.altKeys } : {},
1229
+ ...lookupFragmentEntry.present !== void 0 ? { present: lookupFragmentEntry.present } : {},
1230
+ ...lookupFragmentEntry.sortBy !== void 0 ? { sortBy: lookupFragmentEntry.sortBy } : {},
1231
+ ...lookupFragmentEntry.keys !== void 0 ? { keys: lookupFragmentEntry.keys } : {}
1232
+ };
1233
+ }
970
1234
  if (cls !== void 0) inferred.sensitivity = cls.sensitivity;
971
1235
  const channelEntry = fieldMeta?.[key];
972
1236
  const zodMeta = zod?.meta;
@@ -988,6 +1252,7 @@ function buildDescription(input) {
988
1252
  type,
989
1253
  ...resolved.semanticType !== void 0 ? { semanticType: resolved.semanticType } : {},
990
1254
  ...dictBlock !== void 0 ? { dict: dictBlock } : {},
1255
+ ...lookupBlock !== void 0 ? { lookup: lookupBlock } : {},
991
1256
  ...resolved.widget !== void 0 ? { resolvedWidget: resolved.widget } : {}
992
1257
  });
993
1258
  const editable = !isComputed && key !== "id";
@@ -1009,6 +1274,7 @@ function buildDescription(input) {
1009
1274
  ...refBlock !== void 0 ? { ref: refBlock } : {},
1010
1275
  ...moneyBlock !== void 0 ? { money: moneyBlock } : {},
1011
1276
  ...dictBlock !== void 0 ? { dict: dictBlock } : {},
1277
+ ...lookupBlock !== void 0 ? { lookup: lookupBlock } : {},
1012
1278
  ...isComputed ? { computed: true } : {},
1013
1279
  ...i18nBlock !== void 0 ? { i18n: i18nBlock } : {},
1014
1280
  widget,
@@ -1020,7 +1286,8 @@ function buildDescription(input) {
1020
1286
  list: cls.list.kind === "omit" ? "omit" : cls.list.kind === "mask" ? { mask: cls.list.pattern } : { rider: cls.list.rider },
1021
1287
  ...cls.equatable === true ? { equatable: true } : {}
1022
1288
  }
1023
- } : {}
1289
+ } : {},
1290
+ ...taintPosture !== void 0 ? { taint: { posture: taintPosture, forcedBy: taint?.provenance?.get(key) ?? [] } } : {}
1024
1291
  };
1025
1292
  fields.push(field);
1026
1293
  }
@@ -1071,7 +1338,7 @@ async function resolveStaleOnRead(accessor, outputCollection, id) {
1071
1338
  }
1072
1339
  const sourceWithId = { ...source, id };
1073
1340
  if (DerivationExecutor === null) {
1074
- ({ DerivationExecutor } = await import("./executor-EUPYHCLU.js"));
1341
+ ({ DerivationExecutor } = await import("./executor-WAEI6PDV.js"));
1075
1342
  }
1076
1343
  const ctx = { vault: accessor.getReadOnlyFacade() };
1077
1344
  const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
@@ -1168,51 +1435,64 @@ var NO_HISTORY = {
1168
1435
  }
1169
1436
  };
1170
1437
 
1171
- // src/with-shape/i18n/strategy.ts
1172
- function notEnabled2(op) {
1173
- return new Error(
1174
- `${op} requires the i18n strategy. Import \`{ withI18n }\` from "@noy-db/hub/i18n" and pass it to \`createNoydb({ i18nStrategy: withI18n() })\`.`
1175
- );
1176
- }
1177
- var NO_I18N = {
1178
- applyI18nLocale(record) {
1179
- return record;
1180
- },
1181
- validateI18nTextValue() {
1182
- throw notEnabled2("i18nText field validation");
1183
- },
1184
- enforceScript(value) {
1185
- return { value, warnings: [] };
1186
- },
1187
- computeExemptFills() {
1188
- return /* @__PURE__ */ new Map();
1189
- },
1190
- densify() {
1191
- },
1192
- buildDictionaryHandle() {
1193
- throw notEnabled2("vault.dictionary()");
1194
- }
1195
- };
1196
-
1197
1438
  // src/with-party/team/sync-strategy.ts
1198
- function notEnabled3(op) {
1439
+ function notEnabled2(op) {
1199
1440
  return new Error(
1200
1441
  `${op} requires the sync strategy. Import \`{ withSync }\` from "@noy-db/hub/sync" and pass it to \`createNoydb({ syncStrategy: withSync() })\`.`
1201
1442
  );
1202
1443
  }
1203
1444
  var NO_SYNC = {
1204
1445
  buildSyncEngine() {
1205
- throw notEnabled3("SyncEngine");
1446
+ throw notEnabled2("SyncEngine");
1206
1447
  },
1207
1448
  buildSyncTransaction() {
1208
- throw notEnabled3("SyncTransaction");
1449
+ throw notEnabled2("SyncTransaction");
1209
1450
  },
1210
1451
  buildPresence() {
1211
- throw notEnabled3("collection.presence()");
1452
+ throw notEnabled2("collection.presence()");
1212
1453
  }
1213
1454
  };
1214
1455
 
1215
- // src/with-shape/blobs/strategy.ts
1456
+ // src/shape/via-blob/binding.ts
1457
+ async function eraseBlobs(ctx, cfg) {
1458
+ if (cfg.purgeBlobsForRecord) return cfg.purgeBlobsForRecord(ctx.id);
1459
+ return { shredded: 0, residue: [] };
1460
+ }
1461
+ function buildBlobDescribeFragment(fields) {
1462
+ return {
1463
+ blobFields: Object.fromEntries(
1464
+ Object.entries(fields).map(([field, p]) => [field, {
1465
+ ...p.retainDays !== void 0 ? { retainDays: p.retainDays } : {},
1466
+ ...p.evictWhen !== void 0 ? { evictWhen: true } : {},
1467
+ ...p.legalHold !== void 0 ? { legalHold: true } : {},
1468
+ ...p.retainUntil !== void 0 ? { retainUntil: true } : {},
1469
+ ...p.external === true ? { external: true } : {},
1470
+ ...p.public === true ? { public: true } : {},
1471
+ ...p.backlink !== void 0 ? { backlink: p.backlink } : {}
1472
+ }])
1473
+ )
1474
+ };
1475
+ }
1476
+ function blobBinding(cfg) {
1477
+ return {
1478
+ brand: "blob",
1479
+ // encryptedAtRest: 'envelope' — blob chunks are AEAD-encrypted envelopes
1480
+ // in `_blob_chunks` (service-side), not sealed record fields; queryable:
1481
+ // 'none' — nothing indexes blob content; exportable — vault.exportBlobs()
1482
+ // is a first-class door; forgettable — shredAllForRecord participates in
1483
+ // vault.forget().
1484
+ posture: { encryptedAtRest: "envelope", queryable: "none", exportable: true, forgettable: true },
1485
+ covers: (field) => field in cfg.fields,
1486
+ erase: (ctx) => eraseBlobs(ctx, cfg),
1487
+ describeFragment: () => buildBlobDescribeFragment(cfg.fields)
1488
+ };
1489
+ }
1490
+ function linkBlobVia() {
1491
+ installViaBinder("blob", (c) => blobBinding(c));
1492
+ }
1493
+
1494
+ // src/port/with/blob-strategy.ts
1495
+ linkBlobVia();
1216
1496
  var NO_BLOBS = {
1217
1497
  openSlot() {
1218
1498
  throw new Error(
@@ -1221,23 +1501,502 @@ var NO_BLOBS = {
1221
1501
  }
1222
1502
  };
1223
1503
 
1504
+ // src/shape/via-lookup/registry.ts
1505
+ function enforceStaticDictOnPut(staticFields, record) {
1506
+ if (!staticFields || Object.keys(staticFields).length === 0) return;
1507
+ if (!record || typeof record !== "object") return;
1508
+ const obj = record;
1509
+ for (const [field, desc] of Object.entries(staticFields)) {
1510
+ if (desc.validateCodes === false) continue;
1511
+ const known = new Set(desc.keys);
1512
+ const values = getAtPath(obj, field);
1513
+ for (const value of values) {
1514
+ if (value === void 0 || value === null) continue;
1515
+ const codes = Array.isArray(value) ? value : [value];
1516
+ for (const code of codes) {
1517
+ if (typeof code !== "string") continue;
1518
+ if (!known.has(code)) {
1519
+ throw new UnknownDictCodeError(desc.name, field, code);
1520
+ }
1521
+ }
1522
+ }
1523
+ }
1524
+ }
1525
+ function resolveDictSource(leftCollection, field, staticDescriptorByField, dictKeyFieldRegistry, getDictionaryHandle) {
1526
+ const staticFields = staticDescriptorByField.get(leftCollection);
1527
+ if (staticFields && field in staticFields) {
1528
+ const desc = staticFields[field];
1529
+ const rows = Object.entries(desc.table).map(
1530
+ ([key, labels]) => ({ key, labels, ...labels })
1531
+ );
1532
+ const source = {
1533
+ snapshot() {
1534
+ return rows;
1535
+ },
1536
+ lookupById(id) {
1537
+ return rows.find((e) => e["key"] === id);
1538
+ }
1539
+ };
1540
+ if (desc.displayLocale !== void 0) {
1541
+ ;
1542
+ source.displayLocale = desc.displayLocale;
1543
+ }
1544
+ return source;
1545
+ }
1546
+ const dictFields = dictKeyFieldRegistry.get(leftCollection);
1547
+ if (!dictFields || !(field in dictFields)) return null;
1548
+ const dictName = dictFields[field];
1549
+ if (!dictName) return null;
1550
+ const handle = getDictionaryHandle(dictName);
1551
+ return {
1552
+ snapshot() {
1553
+ return handle.snapshotEntries();
1554
+ },
1555
+ lookupById(id) {
1556
+ const entries = handle.snapshotEntries();
1557
+ return entries.find((e) => e["key"] === id);
1558
+ }
1559
+ };
1560
+ }
1561
+ async function updateReferencingRecords(registry, getCollection, name, oldKey, newKey) {
1562
+ for (const [collectionName, dictFields] of registry) {
1563
+ const fields = Object.entries(dictFields).filter(([, dn]) => dn === name).map(([field]) => field);
1564
+ if (fields.length === 0) continue;
1565
+ const coll = getCollection(collectionName);
1566
+ const records = await coll.list();
1567
+ for (const record of records) {
1568
+ let changed = false;
1569
+ const updated = { ...record };
1570
+ for (const field of fields) {
1571
+ if (updated[field] === oldKey) {
1572
+ updated[field] = newKey;
1573
+ changed = true;
1574
+ }
1575
+ }
1576
+ if (changed) {
1577
+ const id = record["id"];
1578
+ if (id !== void 0) {
1579
+ await coll.put(id, updated);
1580
+ }
1581
+ }
1582
+ }
1583
+ }
1584
+ }
1585
+ function resolveLabelFromMap(labels, locale, fallback) {
1586
+ if (labels[locale] !== void 0) return labels[locale];
1587
+ const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
1588
+ for (const fb of chain) {
1589
+ if (fb === "any") {
1590
+ const any = Object.values(labels)[0];
1591
+ if (any !== void 0) return any;
1592
+ } else if (labels[fb] !== void 0) {
1593
+ return labels[fb];
1594
+ }
1595
+ }
1596
+ return void 0;
1597
+ }
1598
+ function lookupToStaticDictCompat(desc) {
1599
+ if (desc.backing !== "static" || desc.table === void 0) return void 0;
1600
+ return {
1601
+ _noydbStaticDict: true,
1602
+ _viaBrand: "i18n",
1603
+ name: desc.dimension,
1604
+ table: desc.table,
1605
+ keys: desc.keys ?? Object.keys(desc.table),
1606
+ ...desc.displayLocale !== void 0 ? { displayLocale: desc.displayLocale } : {},
1607
+ ...desc.onMissing !== void 0 ? { onMissing: desc.onMissing } : {},
1608
+ ...desc.substitute !== void 0 ? { substitute: desc.substitute } : {},
1609
+ validateCodes: desc.vocabulary === "closed"
1610
+ };
1611
+ }
1612
+ function collectLookupDictCompat(lookupFields) {
1613
+ const dictFieldMap = {};
1614
+ const staticEntries = [];
1615
+ for (const [field, desc] of Object.entries(lookupFields ?? {})) {
1616
+ if (desc.backing === "reserved") {
1617
+ dictFieldMap[field] = desc.dimension;
1618
+ } else {
1619
+ const compat = lookupToStaticDictCompat(desc);
1620
+ if (compat) staticEntries.push([field, compat]);
1621
+ }
1622
+ }
1623
+ return { dictFieldMap, staticEntries };
1624
+ }
1625
+ function materializeBackingTable(descriptor, rows) {
1626
+ const keys = new Set(rows.keys());
1627
+ const owner = /* @__PURE__ */ new Map();
1628
+ for (const key of keys) owner.set(key, key);
1629
+ const altIndex = /* @__PURE__ */ new Map();
1630
+ const altFields = descriptor.altKeys ?? [];
1631
+ for (const [canonicalKey, row] of rows) {
1632
+ for (const altField of altFields) {
1633
+ const value = row[altField];
1634
+ if (typeof value !== "string" || value === "") continue;
1635
+ const existingOwner = owner.get(value);
1636
+ if (existingOwner !== void 0 && existingOwner !== canonicalKey) {
1637
+ throw new ValidationError(
1638
+ `lookup "${descriptor.dimension}": altKey field "${altField}" value "${value}" is claimed by both "${existingOwner}" and "${canonicalKey}" \u2014 key/altKey values must be unique across the dimension.`
1639
+ );
1640
+ }
1641
+ owner.set(value, canonicalKey);
1642
+ altIndex.set(value, canonicalKey);
1643
+ }
1644
+ }
1645
+ return { keys, altIndex };
1646
+ }
1647
+ function checkLookupMembership(descriptor, key, getDictionary, getCollection) {
1648
+ if (descriptor.backing === "static") {
1649
+ const known = descriptor.keys ?? (descriptor.table ? Object.keys(descriptor.table) : []);
1650
+ return known.includes(key);
1651
+ }
1652
+ if (descriptor.backing === "reserved") {
1653
+ if ((descriptor.keys ?? []).includes(key)) return true;
1654
+ return getDictionary(descriptor.dimension).snapshotEntries().some((e) => e["key"] === key);
1655
+ }
1656
+ return buildLookupAltIndex(descriptor, getDictionary, getCollection).keys.has(key);
1657
+ }
1658
+ function buildLookupAltIndex(descriptor, getDictionary, getCollection) {
1659
+ if (descriptor.backing === "static") {
1660
+ return materializeBackingTable(descriptor, new Map(Object.entries(descriptor.table ?? {})));
1661
+ }
1662
+ if (descriptor.backing === "reserved") {
1663
+ const entries = getDictionary(descriptor.dimension).snapshotEntries();
1664
+ return materializeBackingTable(descriptor, new Map(entries.map((e) => [String(e["key"]), e])));
1665
+ }
1666
+ const rows = getCollection(descriptor.dimension).querySourceForJoin().snapshot();
1667
+ return materializeBackingTable(
1668
+ descriptor,
1669
+ new Map(
1670
+ rows.map((r) => {
1671
+ const row = r;
1672
+ return [String(row[descriptor.key]), row];
1673
+ })
1674
+ )
1675
+ );
1676
+ }
1677
+ function collectLookupRefEdges(collectionName, lookupFields) {
1678
+ const edges = [];
1679
+ for (const [field, desc] of Object.entries(lookupFields ?? {})) {
1680
+ if (desc.backing === "static") continue;
1681
+ const backing = desc.backing === "reserved" ? dictCollectionName(desc.dimension) : desc.dimension;
1682
+ edges.push({
1683
+ referencing: { collection: collectionName, field },
1684
+ sources: [
1685
+ { collection: backing, field: "*" },
1686
+ ...desc.present?.label !== void 0 ? [{ collection: backing, field: desc.present.label }] : []
1687
+ ],
1688
+ onDelete: desc.onDelete,
1689
+ keyField: desc.key
1690
+ });
1691
+ }
1692
+ return edges;
1693
+ }
1694
+ function registerLookupRefEdges(graph, collectionName, lookupFields) {
1695
+ for (const e of collectLookupRefEdges(collectionName, lookupFields)) {
1696
+ graph.registerDerived(e.referencing, e.sources, "ref", "record", e.onDelete, e.keyField);
1697
+ }
1698
+ }
1699
+ function buildLookupSnapshotRows(descriptor, isReservedDimension, getDictionary, getCollection) {
1700
+ const dimension = descriptor.dimension;
1701
+ if (isReservedDimension(dimension)) {
1702
+ const rows = /* @__PURE__ */ new Map();
1703
+ for (const entry of getDictionary(dimension).snapshotEntries()) {
1704
+ const key = entry["key"];
1705
+ if (typeof key === "string") rows.set(key, entry);
1706
+ }
1707
+ return rows;
1708
+ }
1709
+ if (descriptor.backing === "collection") {
1710
+ const rawRows = getCollection(dimension).querySourceForJoin().snapshot();
1711
+ const rows = /* @__PURE__ */ new Map();
1712
+ for (const r of rawRows) {
1713
+ const row = r;
1714
+ rows.set(String(row[descriptor.key]), row);
1715
+ }
1716
+ return rows;
1717
+ }
1718
+ return void 0;
1719
+ }
1720
+
1721
+ // src/shape/via-lookup/snapshot.ts
1722
+ function readRowField(row, field, by, locale, fallback) {
1723
+ if (!row || field === void 0) return void 0;
1724
+ const raw = row[field];
1725
+ if (by === void 0) return typeof raw === "string" ? raw : void 0;
1726
+ if (!raw || typeof raw !== "object") return void 0;
1727
+ const map = raw;
1728
+ const val = map[locale];
1729
+ if (typeof val === "string") return val;
1730
+ if (typeof fallback === "string" && typeof map[fallback] === "string") return map[fallback];
1731
+ return void 0;
1732
+ }
1733
+ function buildLookupSnapshot(dimension, rows, descriptor) {
1734
+ void dimension;
1735
+ return {
1736
+ row: (key) => rows.get(key),
1737
+ label: (key, locale, fallback) => readRowField(rows.get(key), descriptor.present?.label, descriptor.present?.by, locale, fallback),
1738
+ compareKeys: (a, b, locale) => {
1739
+ const sortField = descriptor.sortBy ?? descriptor.present?.label;
1740
+ const av = readRowField(rows.get(a), sortField, descriptor.present?.by, locale) ?? a;
1741
+ const bv = readRowField(rows.get(b), sortField, descriptor.present?.by, locale) ?? b;
1742
+ return av < bv ? -1 : av > bv ? 1 : 0;
1743
+ }
1744
+ };
1745
+ }
1746
+ function presentLookupForJoin(record, locale, lookupFields, getSnapshotRows) {
1747
+ let result = record;
1748
+ for (const [field, desc] of Object.entries(lookupFields)) {
1749
+ if (desc.present === void 0) continue;
1750
+ const raw = record[field];
1751
+ if (typeof raw !== "string") continue;
1752
+ const rows = desc.backing === "static" ? desc.table ? new Map(Object.entries(desc.table)) : void 0 : getSnapshotRows(desc);
1753
+ if (!rows) continue;
1754
+ const label = buildLookupSnapshot(desc.dimension, rows, desc).label(raw, locale);
1755
+ if (label === void 0) continue;
1756
+ if (result === record) result = { ...record };
1757
+ result[`${field}Label`] = label;
1758
+ }
1759
+ return result;
1760
+ }
1761
+ function buildPresentForJoin(i18nFields, lookupFields, getSnapshotRows) {
1762
+ const hasI18n = i18nFields !== void 0 && Object.keys(i18nFields).length > 0;
1763
+ const hasLookup = lookupFields !== void 0 && Object.keys(lookupFields).length > 0;
1764
+ if (!hasI18n && !hasLookup) return void 0;
1765
+ const resolveRows = getSnapshotRows ?? (() => void 0);
1766
+ return (record, locale) => {
1767
+ if (record === null || typeof record !== "object") return record;
1768
+ let result = record;
1769
+ if (hasI18n) result = presentI18nForJoin(result, i18nFields, locale);
1770
+ if (hasLookup) result = presentLookupForJoin(result, locale, lookupFields, resolveRows);
1771
+ return result;
1772
+ };
1773
+ }
1774
+
1775
+ // src/port/with/lookup-strategy.ts
1776
+ function isLookupDescriptor(x) {
1777
+ return typeof x === "object" && x !== null && x._viaBrand === "lookup";
1778
+ }
1779
+
1780
+ // src/kernel/via-compose.ts
1781
+ function via(...descriptors) {
1782
+ if (descriptors.length === 0) {
1783
+ throw new ValidationError("via(): at least one descriptor is required, e.g. via(money({ currency: 'EUR' }))");
1784
+ }
1785
+ return { _noydbVia: true, descriptors };
1786
+ }
1787
+ function isViaFieldSpec(x) {
1788
+ return typeof x === "object" && x !== null && x._noydbVia === true;
1789
+ }
1790
+ function mergeViaFields(sources) {
1791
+ for (const field of Object.keys(sources.dictKeyFields ?? {})) {
1792
+ if (sources.lookupFields && field in sources.lookupFields) {
1793
+ throw new ValidationError(
1794
+ `mergeViaFields(): field "${field}" is declared in both \`dictKeyFields\` and \`lookupFields\` \u2014 declare it in one place only.`
1795
+ );
1796
+ }
1797
+ }
1798
+ if (!sources.viaFields || Object.keys(sources.viaFields).length === 0) {
1799
+ return {
1800
+ moneyFields: sources.moneyFields,
1801
+ i18nFields: sources.i18nFields,
1802
+ dictKeyFields: sources.dictKeyFields,
1803
+ computedFields: void 0,
1804
+ lookupFields: sources.lookupFields
1805
+ };
1806
+ }
1807
+ const sugarFieldNames = /* @__PURE__ */ new Set([
1808
+ ...Object.keys(sources.moneyFields ?? {}),
1809
+ ...Object.keys(sources.i18nFields ?? {}),
1810
+ ...Object.keys(sources.dictKeyFields ?? {}),
1811
+ ...Object.keys(sources.lookupFields ?? {})
1812
+ ]);
1813
+ const viaMoney = {};
1814
+ const viaI18nText = {};
1815
+ const viaDictKey = {};
1816
+ const viaComputed = {};
1817
+ const viaLookup = {};
1818
+ for (const [field, spec] of Object.entries(sources.viaFields)) {
1819
+ if (sugarFieldNames.has(field)) {
1820
+ throw new ValidationError(
1821
+ `via(): field "${field}" is declared via both a sugar key and \`viaFields\` \u2014 declare it in one place only.`
1822
+ );
1823
+ }
1824
+ for (const descriptor of spec.descriptors) {
1825
+ if (descriptor._viaBrand === "money") {
1826
+ viaMoney[field] = descriptor;
1827
+ } else if (descriptor._viaBrand === "i18n") {
1828
+ if (isI18nTextDescriptor(descriptor)) viaI18nText[field] = descriptor;
1829
+ else if (isDictKeyDescriptor(descriptor) || isStaticDictDescriptor(descriptor)) viaDictKey[field] = descriptor;
1830
+ } else if (descriptor._viaBrand === "computed") {
1831
+ viaComputed[field] = descriptor;
1832
+ } else if (descriptor._viaBrand === "lookup" && isLookupDescriptor(descriptor)) {
1833
+ viaLookup[field] = descriptor;
1834
+ } else {
1835
+ throw new ValidationError(`via(): field "${field}" has a descriptor with unrecognized _viaBrand "${descriptor._viaBrand}" \u2014 via() only supports money/i18n/computed/lookup descriptors today.`);
1836
+ }
1837
+ }
1838
+ }
1839
+ return {
1840
+ moneyFields: Object.keys(viaMoney).length > 0 ? { ...sources.moneyFields ?? {}, ...viaMoney } : sources.moneyFields,
1841
+ i18nFields: Object.keys(viaI18nText).length > 0 ? { ...sources.i18nFields ?? {}, ...viaI18nText } : sources.i18nFields,
1842
+ dictKeyFields: Object.keys(viaDictKey).length > 0 ? { ...sources.dictKeyFields ?? {}, ...viaDictKey } : sources.dictKeyFields,
1843
+ computedFields: Object.keys(viaComputed).length > 0 ? viaComputed : void 0,
1844
+ lookupFields: Object.keys(viaLookup).length > 0 ? { ...sources.lookupFields ?? {}, ...viaLookup } : sources.lookupFields
1845
+ };
1846
+ }
1847
+
1224
1848
  // src/kernel/collection-config.ts
1849
+ function computedEntryParts(entry) {
1850
+ if (typeof entry === "function") return { fn: entry, mode: "materialized" };
1851
+ return { fn: entry.fn, ...entry.deps !== void 0 ? { deps: entry.deps } : {}, mode: entry.mode ?? "materialized" };
1852
+ }
1853
+ function unifyComputedFields(opts, viaComputedFields) {
1854
+ return { ...opts.computed ?? {}, ...viaComputedFields ?? {} };
1855
+ }
1856
+ function compileViaBindings(opts, classifiedGuardCtx, eraseCfgOut) {
1857
+ const { moneyFields, i18nFields, dictKeyFields, computedFields, lookupFields } = mergeViaFields(opts);
1858
+ const bindings = [];
1859
+ if (moneyFields) bindings.push(viaBinder("money")(moneyFields));
1860
+ if (i18nFields || dictKeyFields) {
1861
+ const densify = i18nFields ? Object.fromEntries(
1862
+ Object.entries(i18nFields).filter(([, d]) => d.options.densifyOnWrite === true)
1863
+ ) : {};
1864
+ const i18nDensifyFields = Object.keys(densify).length > 0 ? densify : void 0;
1865
+ bindings.push(viaBinder("i18n")({
1866
+ ...i18nFields !== void 0 ? { i18nFields } : {},
1867
+ ...dictKeyFields !== void 0 ? { dictKeyFields } : {},
1868
+ ...i18nDensifyFields !== void 0 ? { i18nDensifyFields } : {},
1869
+ strategy: opts.i18nStrategy ?? NO_I18N,
1870
+ ...opts.defaultLocale !== void 0 ? { defaultLocale: opts.defaultLocale } : {},
1871
+ ...opts.autoTranslateHook !== void 0 ? { autoTranslateHook: opts.autoTranslateHook } : {},
1872
+ ...opts.dictLabelResolver !== void 0 ? { dictLabelResolver: opts.dictLabelResolver } : {},
1873
+ ...opts.i18nPutValidator !== void 0 ? { i18nPutValidator: opts.i18nPutValidator } : {},
1874
+ collectionName: opts.name
1875
+ }));
1876
+ }
1877
+ if (lookupFields !== void 0) {
1878
+ bindings.push(viaBinder("lookup")({
1879
+ lookupFields,
1880
+ ...opts.lookupLabelResolver !== void 0 ? { lookupLabelResolver: opts.lookupLabelResolver } : {},
1881
+ ...opts.getLookupBacking !== void 0 ? { getLookupBacking: opts.getLookupBacking } : {},
1882
+ ...opts.membership !== void 0 ? { membership: opts.membership } : {},
1883
+ ...opts.getAltIndex !== void 0 ? { getAltIndex: opts.getAltIndex } : {},
1884
+ ...opts.snapshotFor !== void 0 ? { snapshotFor: opts.snapshotFor } : {},
1885
+ collectionName: opts.name
1886
+ }));
1887
+ }
1888
+ if (opts.classifiedFields !== void 0) {
1889
+ const classifiedCfg = {
1890
+ entries: opts.classifiedFields,
1891
+ collectionName: opts.name,
1892
+ guardCtx: classifiedGuardCtx
1893
+ };
1894
+ if (eraseCfgOut) eraseCfgOut.classified = classifiedCfg;
1895
+ bindings.push(viaBinder("classified")(classifiedCfg));
1896
+ }
1897
+ if (opts.blobFields !== void 0) {
1898
+ bindings.push(viaBinder("blob")({
1899
+ fields: opts.blobFields,
1900
+ collectionName: opts.name
1901
+ }));
1902
+ }
1903
+ const virtualFields = /* @__PURE__ */ new Map();
1904
+ for (const [field, entry] of Object.entries(unifyComputedFields(opts, computedFields))) {
1905
+ const parts = computedEntryParts(entry);
1906
+ if (parts.mode === "virtual") {
1907
+ virtualFields.set(field, { _viaBrand: "computed", fn: parts.fn, mode: "virtual", ...parts.deps !== void 0 ? { deps: parts.deps } : {} });
1908
+ }
1909
+ }
1910
+ if (virtualFields.size > 0) {
1911
+ bindings.push(viaBinder("computed")({ virtualFields }));
1912
+ }
1913
+ return bindings;
1914
+ }
1915
+ function collectKnownFieldNames(parts) {
1916
+ return /* @__PURE__ */ new Set([
1917
+ ...Object.keys(parts.moneyFields ?? {}),
1918
+ ...Object.keys(parts.i18nFields ?? {}),
1919
+ ...Object.keys(parts.dictKeyFields ?? {}),
1920
+ ...Object.keys(parts.classifiedFields ?? {}),
1921
+ ...Object.keys(parts.computed ?? {}),
1922
+ ...Object.keys(parts.lookupFields ?? {})
1923
+ ]);
1924
+ }
1925
+ var EMPTY_KNOWN_FIELDS = /* @__PURE__ */ new Set();
1926
+ function resolveComputedEdges(collectionName, computed, hasClassifiedFields, knownFields = EMPTY_KNOWN_FIELDS) {
1927
+ if (!computed) return [];
1928
+ const edges = [];
1929
+ for (const [field, entry] of Object.entries(computed)) {
1930
+ const parts = computedEntryParts(entry);
1931
+ const deps = parts.deps;
1932
+ if (deps === void 0) {
1933
+ if (hasClassifiedFields) {
1934
+ throw new ValidationError(
1935
+ `Collection "${collectionName}": computed field "${field}" has no declared \`deps\` and the collection declares classified fields \u2014 an opaque computed function could silently copy a classified field's plaintext into an ordinary, unredacted field. Declare \`deps\` naming the source fields it reads, e.g. computed: { ${field}: { fn, deps: [...] } } or via(computed(fn, { deps: [...] })).`
1936
+ );
1937
+ }
1938
+ continue;
1939
+ }
1940
+ if (deps.length === 0) {
1941
+ throw new ValidationError(`Collection "${collectionName}": computed field "${field}"'s \`deps\` must be non-empty.`);
1942
+ }
1943
+ for (const dep of deps) {
1944
+ if (typeof dep !== "string" || dep.length === 0) {
1945
+ throw new ValidationError(`Collection "${collectionName}": computed field "${field}"'s \`deps\` entries must be non-empty strings.`);
1946
+ }
1947
+ if (hasClassifiedFields && !knownFields.has(dep)) {
1948
+ throw new ValidationError(
1949
+ `Collection "${collectionName}": computed field "${field}"'s \`deps\` entry "${dep}" does not name a declared field, and the collection declares classified fields \u2014 an opaque computed function could silently copy a classified field's plaintext into an ordinary, unredacted field via a mistyped/unknown dep name. Declare \`deps\` naming only known fields (money/i18n/dictKey/classified/computed) \u2014 check "${dep}" for a typo.`
1950
+ );
1951
+ }
1952
+ }
1953
+ edges.push({
1954
+ target: { collection: collectionName, field },
1955
+ sources: deps.map((d) => ({ collection: collectionName, field: d })),
1956
+ grain: parts.mode === "virtual" ? "virtual" : "record"
1957
+ });
1958
+ }
1959
+ return edges;
1960
+ }
1961
+ function resolveViaBindingDepsEdges(collectionName, bindings, knownFields) {
1962
+ const edges = [];
1963
+ for (const binding of bindings) {
1964
+ if (!binding.deps || binding.deps.length === 0) continue;
1965
+ for (const dep of binding.deps) {
1966
+ if (!knownFields.has(dep)) {
1967
+ throw new ValidationError(
1968
+ `Collection "${collectionName}": via binding "${binding.brand}" deps references undeclared field "${dep}".`
1969
+ );
1970
+ }
1971
+ }
1972
+ const sources = binding.deps.map((d) => ({ collection: collectionName, field: d }));
1973
+ for (const field of knownFields) {
1974
+ if (binding.covers?.(field)) edges.push({ target: { collection: collectionName, field }, sources });
1975
+ }
1976
+ }
1977
+ return edges;
1978
+ }
1225
1979
  function resolveCollectionConfig(opts) {
1226
1980
  if (opts.embeddings && opts.crdt) {
1227
1981
  throw new Error(
1228
1982
  `Collection "${opts.name}": embeddings are not supported on CRDT collections (L2). Use a non-CRDT collection for semantic search.`
1229
1983
  );
1230
1984
  }
1231
- if (opts.moneyFields) moneyRuntime().validateMoneyFieldPaths(opts.moneyFields);
1985
+ const effectiveViaFields = mergeViaFields(opts);
1232
1986
  const resolvedClassified = opts.classifiedFields !== void 0 ? resolveClassifiedFields(opts.name, opts.classifiedFields) : void 0;
1233
- let mergedComputed = opts.computed;
1987
+ const allComputed = unifyComputedFields(opts, effectiveViaFields.computedFields);
1988
+ const materializedComputed = {};
1989
+ for (const [field, entry] of Object.entries(allComputed)) {
1990
+ if (computedEntryParts(entry).mode !== "virtual") materializedComputed[field] = entry;
1991
+ }
1992
+ let mergedComputed = Object.keys(materializedComputed).length > 0 ? materializedComputed : void 0;
1234
1993
  if (resolvedClassified !== void 0) {
1235
- for (const key of Object.keys(opts.computed ?? {})) {
1994
+ for (const key of Object.keys(allComputed)) {
1236
1995
  if (resolvedClassified.riderComputed[key] !== void 0) {
1237
1996
  throw new ClassifiedConfigError(opts.name, `computed field "${key}" collides with a rider companion`);
1238
1997
  }
1239
1998
  }
1240
- mergedComputed = { ...resolvedClassified.riderComputed, ...opts.computed ?? {} };
1999
+ mergedComputed = { ...resolvedClassified.riderComputed, ...materializedComputed };
1241
2000
  }
1242
2001
  let deterministicFields;
1243
2002
  if (opts.deterministicFields && opts.deterministicFields.length > 0) {
@@ -1250,10 +2009,6 @@ function resolveCollectionConfig(opts) {
1250
2009
  } else {
1251
2010
  deterministicFields = null;
1252
2011
  }
1253
- const densifyFields = opts.i18nFields ? Object.fromEntries(
1254
- Object.entries(opts.i18nFields).filter(([, d]) => d.options.densifyOnWrite === true)
1255
- ) : {};
1256
- const i18nDensifyFields = Object.keys(densifyFields).length > 0 ? densifyFields : void 0;
1257
2012
  const guardIndexedFields = /* @__PURE__ */ new Set();
1258
2013
  for (const ix of opts.indexes ?? []) {
1259
2014
  if (typeof ix === "string") guardIndexedFields.add(ix);
@@ -1289,6 +2044,18 @@ function resolveCollectionConfig(opts) {
1289
2044
  const vdigFields = vdigEntries.length > 0 ? new Map(vdigEntries) : null;
1290
2045
  const perRecordCek = opts.perRecordKeys === true;
1291
2046
  const cekCache = perRecordCek ? new Lru({ maxRecords: 4096 }) : null;
2047
+ const viaEraseCfgOut = {};
2048
+ const via2 = ViaPipeline.build(compileViaBindings(opts, classifiedGuardCtx, viaEraseCfgOut));
2049
+ const knownFields = collectKnownFieldNames({
2050
+ moneyFields: effectiveViaFields.moneyFields,
2051
+ i18nFields: effectiveViaFields.i18nFields,
2052
+ dictKeyFields: effectiveViaFields.dictKeyFields,
2053
+ classifiedFields: resolvedClassified?.byField,
2054
+ computed: allComputed,
2055
+ lookupFields: effectiveViaFields.lookupFields
2056
+ });
2057
+ const computedEdges = resolveComputedEdges(opts.name, allComputed, resolvedClassified !== void 0, knownFields);
2058
+ const viaDepsEdges = resolveViaBindingDepsEdges(opts.name, via2?.bindings ?? [], knownFields);
1292
2059
  return {
1293
2060
  adapter: opts.adapter,
1294
2061
  vault: opts.vault,
@@ -1320,20 +2087,34 @@ function resolveCollectionConfig(opts) {
1320
2087
  ledger: opts.ledger,
1321
2088
  refEnforcer: opts.refEnforcer,
1322
2089
  joinResolver: opts.joinResolver,
1323
- i18nFields: opts.i18nFields,
2090
+ i18nFields: effectiveViaFields.i18nFields,
1324
2091
  textIndexes: opts.textIndexes,
1325
- i18nDensifyFields,
1326
2092
  embeddings: opts.embeddings,
1327
2093
  vectorSet: opts.embeddings ? new VectorSet() : void 0,
1328
- dictKeyFields: opts.dictKeyFields,
2094
+ dictKeyFields: effectiveViaFields.dictKeyFields,
2095
+ lookupFields: effectiveViaFields.lookupFields,
2096
+ // #650 Task 6 — the sync join-dressing hook (#626 retirement, spec §5);
2097
+ // `undefined` when this collection declares neither i18nText nor lookup
2098
+ // fields, matching today's `i18nFields`-absent JoinableSource shape.
2099
+ presentForJoin: buildPresentForJoin(effectiveViaFields.i18nFields, effectiveViaFields.lookupFields, opts.snapshotFor),
1329
2100
  fieldMeta: opts.fieldMeta,
1330
2101
  meta: opts.meta,
1331
2102
  _refs: opts.declaredRefs ?? {},
1332
- moneyFields: opts.moneyFields,
2103
+ via: via2,
2104
+ classifiedEraseCfg: viaEraseCfgOut.classified,
2105
+ moneyFields: effectiveViaFields.moneyFields,
1333
2106
  classified: resolvedClassified,
1334
2107
  classifiedGuardCtx,
1335
2108
  classifiedStrategy: opts.classifiedStrategy ?? NO_CLASSIFIED,
1336
2109
  computed: mergedComputed,
2110
+ computedEdges,
2111
+ viaDepsEdges,
2112
+ // #638 Task 7 — every declared computed field name (both modes, both surfaces); the
2113
+ // reconcile path's cross-call depsless-computed leak-guard memory
2114
+ // (`via-graph-wiring.ts#registerCollectionGraphSources`) reads this instead of
2115
+ // `Object.keys(opts.computed ?? {})` alone, so a via(computed(...))-declared field
2116
+ // participates in that guard identically to a sugar-declared one.
2117
+ computedFieldNames: Object.keys(allComputed),
1337
2118
  dictLabelResolver: opts.dictLabelResolver,
1338
2119
  getDictionary: opts.getDictionary,
1339
2120
  i18nPutValidator: opts.i18nPutValidator,
@@ -1344,6 +2125,7 @@ function resolveCollectionConfig(opts) {
1344
2125
  onAccess: opts.onAccess,
1345
2126
  derivationSource: opts.derivationSource,
1346
2127
  materializedViewSource: opts.materializedViewSource,
2128
+ graphDispatch: opts.graphDispatch,
1347
2129
  tiers: opts.tiers && opts.tiers.length > 0 ? new Set(opts.tiers) : null,
1348
2130
  tiersStrategy: opts.tiersStrategy ?? NO_TIERS,
1349
2131
  searchStrategy: opts.searchStrategy ?? NO_SEARCH,
@@ -1361,7 +2143,7 @@ function resolveCollectionConfig(opts) {
1361
2143
  // src/with-formula/computed/lazy.ts
1362
2144
  var fn = null;
1363
2145
  async function loadEvalComputedFields() {
1364
- fn ??= (await import("./computed-6FOFXQPE.js")).evalComputedFields;
2146
+ fn ??= (await import("./computed-R62HGCO4.js")).evalComputedFields;
1365
2147
  return fn;
1366
2148
  }
1367
2149
 
@@ -1420,7 +2202,6 @@ var Collection = class {
1420
2202
  tiersStrategy;
1421
2203
  searchStrategy;
1422
2204
  historyStrategy;
1423
- i18nStrategy;
1424
2205
  syncStrategy;
1425
2206
  // In-memory cache of decrypted records (eager mode only). Lazy mode
1426
2207
  // uses `lru` instead. Both fields exist so a single Collection instance
@@ -1522,70 +2303,38 @@ var Collection = class {
1522
2303
  * throw `LocaleNotSpecifiedError` unless a per-call locale is passed.
1523
2304
  */
1524
2305
  defaultLocale;
1525
- /**
1526
- * Map of field name → `I18nTextDescriptor` for fields declared with
1527
- * `i18nText()`. Used by:
1528
- * - `put()` via `i18nPutValidator` to enforce required translations
1529
- * - `get()`/`list()` to apply locale resolution after decryption
1530
- *
1531
- * Declared via the `i18nFields` collection option.
1532
- */
2306
+ /** Field name → `I18nTextDescriptor` for `i18nText()` fields (`i18nFields` option); write/read
2307
+ * runs through the compiled `via` i18n binding this remains for `describe()`
2308
+ * and the search-index build path. */
1533
2309
  i18nFields;
1534
- /**
1535
- * The configured string fields exposed to `retrieve()`. `undefined`
1536
- * for ordinary collections, so the search path costs nothing when unused.
1537
- */
2310
+ /** The configured string fields exposed to `retrieve()`; `undefined` for ordinary collections (zero-cost). */
1538
2311
  textIndexes;
1539
- /**
1540
- * The session-scoped lexical index store. `undefined` (so the dirty
1541
- * poke + retrieve are zero-cost) unless `textIndexes` is non-empty.
1542
- */
2312
+ /** Session-scoped lexical index store; `undefined` (zero-cost) unless `textIndexes` is non-empty. */
1543
2313
  searchIndexStore;
1544
- /**
1545
- * The densify-enabled subset of {@link i18nFields} (fields whose
1546
- * descriptor opts in via `densifyOnWrite: true`). `undefined` when none opt
1547
- * in, so the write path skips all densify work for ordinary collections.
1548
- */
1549
- i18nDensifyFields;
1550
- /**
1551
- * Embedding config for write-time vector derivation. `undefined`
1552
- * for ordinary collections (zero cost). When set, `put()` encodes the
1553
- * source field(s) and stores an encrypted `_vec` sidecar.
1554
- */
2314
+ /** Embedding config for write-time vector derivation; `undefined` (zero-cost) for ordinary
2315
+ * collections. When set, `put()` encodes the source field(s) and stores an encrypted `_vec` sidecar. */
1555
2316
  embeddings;
1556
- /**
1557
- * In-memory vector set, populated lazily from `_vec` sidecars.
1558
- * `undefined` when no embedding config is declared.
1559
- */
2317
+ /** In-memory vector set, populated lazily from `_vec` sidecars; `undefined` when no embedding config is declared. */
1560
2318
  vectorSet;
1561
- /**
1562
- * Map of field name `DictKeyDescriptor` for fields declared with
1563
- * `dictKey()`. Used by `get()`/`list()` to add `<field>Label` virtual
1564
- * fields when a locale is requested.
1565
- */
2319
+ /** Field name → `DictKeyDescriptor` for `dictKey()` fields; used by `get()`/`list()` to add
2320
+ * `<field>Label` virtual fields when a locale is requested. */
1566
2321
  dictKeyFields;
1567
- /**
1568
- * Consumer-neutral per-field descriptors declared via the `fieldMeta`
1569
- * collection option. Read by `getFieldMeta()`; merged by `collection.describe()`.
1570
- */
2322
+ /** Field name → `LookupDescriptor` for native `lookup()`/`enumOf()`/`dict()` fields (#650 Task 2) — describe()-only in this task. */
2323
+ lookupFields;
2324
+ /** Sync join-dressing hook (#650 Task 6, #626 retirement) `querySourceForJoin()`'s `presentForJoin`. */
2325
+ presentForJoin;
2326
+ /** Consumer-neutral per-field descriptors declared via `fieldMeta`; read by `getFieldMeta()`, merged by `describe()`. */
1571
2327
  fieldMeta;
1572
- /**
1573
- * Collection-level descriptive metadata declared via the `meta` collection
1574
- * option. Read by `getMeta()`; surfaced in `collection.describe()`.
1575
- */
2328
+ /** Collection-level descriptive metadata declared via `meta`; read by `getMeta()`, surfaced in `describe()`. */
1576
2329
  meta;
1577
- /**
1578
- * Outbound ref declarations for this collection (snapshot from vault
1579
- * refRegistry at construction time). Used by `describe()` (sync, config-only).
1580
- */
2330
+ /** Outbound ref declarations (snapshot from vault refRegistry at construction time); used by `describe()`. */
1581
2331
  _refs;
1582
- /**
1583
- * Money field descriptors keyed by field path. Declared via the
1584
- * `moneyFields` collection option: `put()` quantizes to a scaled-int
1585
- * string, `get()`/`list()` decode back. Mutable so {@link _applyMoneyFields}
1586
- * can attach descriptors to a collection MV-analysis pre-created.
1587
- */
2332
+ /** Money field descriptors keyed by field path, typed as the opaque {@link ViaDescriptor} marker
2333
+ * (the kernel never inspects the concrete shape); `put()` quantizes to a scaled-int string,
2334
+ * `get()`/`list()` decode back. Mutable so {@link _applyMoneyFields} can attach. */
1588
2335
  moneyFields;
2336
+ via;
2337
+ // compiled Via pipeline (money, i18n); rebuilt by {@link _applyMoneyFields}
1589
2338
  /**
1590
2339
  * Computed scalar fields, evaluated first on every `put()`. Mutable for
1591
2340
  * the same MV-pre-creation reconcile as {@link moneyFields}.
@@ -1620,27 +2369,12 @@ var Collection = class {
1620
2369
  * `createNoydb()` option (opt in with `withClassified()`).
1621
2370
  */
1622
2371
  classifiedStrategy;
1623
- /**
1624
- * Async callback provided by the Vault that resolves a dict key
1625
- * to its label for a given locale. Used by the locale-read path for
1626
- * dictKey fields.
1627
- *
1628
- * Signature: `(dictName, key, locale, fallback?) => Promise<string | undefined>`
1629
- */
1630
- dictLabelResolver;
1631
2372
  /**
1632
2373
  * Async callback provided by the Vault to open a dynamic
1633
2374
  * dictionary handle (for label-map pre-computation in the search index).
1634
2375
  * Only used in `resolveDictLabelMaps()`; static dicts bypass this entirely.
1635
2376
  */
1636
2377
  getDictionary;
1637
- /**
1638
- * Synchronous callback provided by the Vault that validates
1639
- * i18nText fields on `put()`. Throws `MissingTranslationError` when
1640
- * a required translation is absent. Called after schema validation,
1641
- * before encryption.
1642
- */
1643
- i18nPutValidator;
1644
2378
  /**
1645
2379
  * declared deterministic fields. `null` when the feature
1646
2380
  * is inactive for this collection; a frozen `Set` otherwise.
@@ -1695,14 +2429,6 @@ var Collection = class {
1695
2429
  tiers;
1696
2430
  tierMode;
1697
2431
  onCrossTierAccess;
1698
- /**
1699
- * Async translator callback provided by Noydb via Vault for
1700
- * `i18nText` fields with `autoTranslate: true`. Called
1701
- * before i18n validation so translated values are present when the
1702
- * validator runs. `undefined` when no `plaintextTranslator` was
1703
- * configured on `createNoydb()`.
1704
- */
1705
- autoTranslateHook;
1706
2432
  /**
1707
2433
  * Optional reference to the vault-level hash-chained audit
1708
2434
  * log. When present, every successful `put()` and `delete()` appends
@@ -1740,6 +2466,8 @@ var Collection = class {
1740
2466
  * commits + after `dispatchDerivations` has run.
1741
2467
  */
1742
2468
  materializedViewSource;
2469
+ /** #638 Task 4 — `Vault._collectGraphTouch`; a no-op absent an open sync/cutover/restore batch. */
2470
+ graphDispatch;
1743
2471
  /**
1744
2472
  * Optional back-reference to the owning compartment's ref
1745
2473
  * enforcer. When present, `Collection.put` calls
@@ -1802,7 +2530,6 @@ var Collection = class {
1802
2530
  this.tiersStrategy = cfg.tiersStrategy;
1803
2531
  this.searchStrategy = cfg.searchStrategy;
1804
2532
  this.historyStrategy = cfg.historyStrategy;
1805
- this.i18nStrategy = cfg.i18nStrategy;
1806
2533
  this.syncStrategy = cfg.syncStrategy;
1807
2534
  this.reconcileOnOpen = cfg.reconcileOnOpen;
1808
2535
  this.getDEK = cfg.getDEK;
@@ -1816,29 +2543,29 @@ var Collection = class {
1816
2543
  this.i18nFields = cfg.i18nFields;
1817
2544
  this.textIndexes = cfg.textIndexes;
1818
2545
  this.searchIndexStore = opts.textIndexes && opts.textIndexes.length > 0 ? opts.textIndexPersist ? new PersistedIndexStore(buildPersistedIndexCallbacks(() => this.searchContext())) : new MemoryIndexStore() : void 0;
1819
- this.i18nDensifyFields = cfg.i18nDensifyFields;
1820
2546
  this.embeddings = cfg.embeddings;
1821
2547
  this.vectorSet = cfg.vectorSet;
1822
2548
  this.dictKeyFields = cfg.dictKeyFields;
2549
+ this.lookupFields = cfg.lookupFields;
2550
+ this.presentForJoin = cfg.presentForJoin;
1823
2551
  this.fieldMeta = cfg.fieldMeta;
1824
2552
  this.meta = cfg.meta;
1825
2553
  this._refs = cfg._refs;
1826
2554
  this.moneyFields = cfg.moneyFields;
2555
+ this.via = cfg.via;
1827
2556
  this.classified = cfg.classified;
1828
2557
  this.classifiedGuardCtx = cfg.classifiedGuardCtx;
1829
2558
  this.vdigFields = cfg.vdigFields;
1830
2559
  this.classifiedStrategy = cfg.classifiedStrategy;
1831
2560
  this.computed = cfg.computed;
1832
- this.dictLabelResolver = cfg.dictLabelResolver;
1833
2561
  this.getDictionary = cfg.getDictionary;
1834
- this.i18nPutValidator = cfg.i18nPutValidator;
1835
- this.autoTranslateHook = cfg.autoTranslateHook;
1836
2562
  this.defaultLocale = cfg.defaultLocale;
1837
2563
  this.crdtMode = cfg.crdtMode;
1838
2564
  this.syncAdapter = cfg.syncAdapter;
1839
2565
  this.onAccess = cfg.onAccess;
1840
2566
  this.derivationSource = cfg.derivationSource;
1841
2567
  this.materializedViewSource = cfg.materializedViewSource;
2568
+ this.graphDispatch = cfg.graphDispatch;
1842
2569
  this.tiers = cfg.tiers;
1843
2570
  this.tierMode = cfg.tierMode;
1844
2571
  this.onCrossTierAccess = cfg.onCrossTierAccess;
@@ -1878,10 +2605,12 @@ var Collection = class {
1878
2605
  crdtMode: this.crdtMode,
1879
2606
  crdtStrategy: this.crdtStrategy,
1880
2607
  schema: this.schema,
1881
- getDEK: () => this.getDEK(this.name),
2608
+ getDEK: (collection) => this.getDEK(collection ?? this.name),
1882
2609
  cekCache: this.cekCache,
1883
- classifiedMarkerDigestOnly: () => this._classifiedMarkerDigestOnly()
2610
+ classifiedMarkerDigestOnly: () => this._classifiedMarkerDigestOnly(),
2611
+ via: this.via
1884
2612
  });
2613
+ if (cfg.classifiedEraseCfg) cfg.classifiedEraseCfg.classifySealedShred = (live) => this.codec.classifySealedShred(live);
1885
2614
  if (opts.crdt && opts.onRegisterConflictResolver) {
1886
2615
  const crdtMode = opts.crdt;
1887
2616
  const crdtResolver = async (id, local, remote) => {
@@ -1945,7 +2674,7 @@ var Collection = class {
1945
2674
  const merged = mergeFn(localRecord, remoteRecord);
1946
2675
  const mergedVersion = Math.max(local._v, remote._v) + 1;
1947
2676
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
1948
- return this.codec.encryptRecord(merged, mergedVersion, cek);
2677
+ return this.codec.encryptRecord(merged, mergedVersion, cek, void 0, void 0, void 0, id);
1949
2678
  };
1950
2679
  }
1951
2680
  opts.onRegisterConflictResolver(collectionName, resolver);
@@ -2035,35 +2764,41 @@ var Collection = class {
2035
2764
  fieldMeta: this.fieldMeta,
2036
2765
  moneyFields: this.moneyFields,
2037
2766
  dictKeyFields: this.dictKeyFields,
2767
+ ...this.lookupFields !== void 0 ? { lookupFields: this.lookupFields } : {},
2038
2768
  computed: this.computed,
2039
2769
  refs: this._refs,
2040
2770
  zodFields: void 0,
2041
2771
  ...this.meta !== void 0 ? { meta: this.meta } : {},
2042
2772
  ...this.i18nFields !== void 0 ? { i18nFields: this.i18nFields } : {},
2043
- ...this.classified !== void 0 ? { classified: this.classified.byField } : {}
2773
+ ...this.classified !== void 0 ? { classified: this.classified.byField } : {},
2774
+ ...this.via?.taint !== void 0 ? { taint: this.via.taint } : {},
2775
+ ...this.via ? { viaFragments: this.via.describeFragments() } : {}
2776
+ // #650 Task 7
2044
2777
  });
2045
2778
  }
2046
2779
  /**
2047
- * Async describe implementation.
2048
- * Derives validator-exact types via deriveZodFields (lazy, no static zod import),
2049
- * optionally resolves dynamic-dict labels from vault.dictionary(name).list(),
2050
- * then delegates to buildDescription which also runs fieldMeta key-validation.
2780
+ * Async describe implementation. Derives validator-exact types via deriveZodFields
2781
+ * (lazy, no static zod import), optionally resolves dynamic-dict labels from vault.dictionary(name).list(),
2782
+ * then delegates to buildDescription (which also runs fieldMeta key-validation).
2051
2783
  */
2052
2784
  async describeAsync(opts) {
2053
2785
  const zodFields = this.schema !== void 0 ? await deriveZodFields(this.schema) : void 0;
2054
2786
  let dictLabels;
2055
- if (opts.resolveDictLabels === true && this.dictKeyFields !== void 0) {
2056
- dictLabels = {};
2057
- for (const [, desc] of Object.entries(this.dictKeyFields)) {
2058
- if (!isStaticDictDescriptor(desc) && this.getDictionary !== void 0) {
2059
- const handle = await this.getDictionary(desc.name);
2787
+ if (opts.resolveDictLabels === true && this.getDictionary !== void 0) {
2788
+ const names = /* @__PURE__ */ new Set();
2789
+ for (const desc of Object.values(this.dictKeyFields ?? {})) if (!isStaticDictDescriptor(desc)) names.add(desc.name);
2790
+ for (const desc of Object.values(this.lookupFields ?? {})) if (desc.backing === "reserved") names.add(desc.dimension);
2791
+ if (names.size > 0) {
2792
+ dictLabels = {};
2793
+ for (const name of names) {
2794
+ const handle = await this.getDictionary(name);
2060
2795
  const entries = await handle.list();
2061
2796
  const valueToLabel = {};
2062
2797
  for (const entry of entries) {
2063
2798
  const label = Object.values(entry.labels)[0];
2064
2799
  if (label !== void 0) valueToLabel[entry.key] = label;
2065
2800
  }
2066
- dictLabels[desc.name] = valueToLabel;
2801
+ dictLabels[name] = valueToLabel;
2067
2802
  }
2068
2803
  }
2069
2804
  }
@@ -2072,13 +2807,17 @@ var Collection = class {
2072
2807
  fieldMeta: this.fieldMeta,
2073
2808
  moneyFields: this.moneyFields,
2074
2809
  dictKeyFields: this.dictKeyFields,
2810
+ ...this.lookupFields !== void 0 ? { lookupFields: this.lookupFields } : {},
2075
2811
  computed: this.computed,
2076
2812
  refs: this._refs,
2077
2813
  zodFields,
2078
2814
  ...dictLabels !== void 0 ? { dictLabels } : {},
2079
2815
  ...this.meta !== void 0 ? { meta: this.meta } : {},
2080
2816
  ...this.i18nFields !== void 0 ? { i18nFields: this.i18nFields } : {},
2081
- ...this.classified !== void 0 ? { classified: this.classified.byField } : {}
2817
+ ...this.classified !== void 0 ? { classified: this.classified.byField } : {},
2818
+ ...this.via?.taint !== void 0 ? { taint: this.via.taint } : {},
2819
+ ...this.via ? { viaFragments: this.via.describeFragments() } : {}
2820
+ // #650 Task 7
2082
2821
  });
2083
2822
  }
2084
2823
  /** JSON Schema for this collection with describe() metadata as x- extensions. */
@@ -2199,7 +2938,7 @@ var Collection = class {
2199
2938
  }
2200
2939
  await this.onAccess?.("find", "*");
2201
2940
  if (hits.length === 0) return [];
2202
- const { verifyDigestField } = await import("./verify-CAYGXYNO.js");
2941
+ const { verifyDigestField } = await import("./verify-GH6CWVSW.js");
2203
2942
  const policy = {
2204
2943
  normalize: spec.verifyNormalize ?? "password",
2205
2944
  notLastN: spec.notLastN ?? 0,
@@ -2286,10 +3025,17 @@ var Collection = class {
2286
3025
  * analysis auto-creates a source collection (without options) during
2287
3026
  * `openVault`, before the user's `collection(name, { moneyFields })`
2288
3027
  * declaration; this reconciles that ordering. First-wins. Not public.
3028
+ *
3029
+ * PREPENDS money rather than appending: {@link compileViaBindings} always
3030
+ * compiles money before i18n (money-first pipeline order — see its
3031
+ * docstring), and by the time this reconcile runs an i18n binding may
3032
+ * already be sitting in `this.via.bindings` (declared at construction).
3033
+ * Appending here would yield `[i18n, money]` on this path vs `[money,
3034
+ * i18n]` from compile — prepending keeps both paths money-first.
2289
3035
  */
2290
3036
  _applyMoneyFields(moneyFields) {
2291
3037
  if (this.moneyFields !== void 0) return;
2292
- moneyRuntime().validateMoneyFieldPaths(moneyFields);
3038
+ this.via = ViaPipeline.build([viaBinder("money")(moneyFields), ...this.via?.bindings ?? []]);
2293
3039
  this.moneyFields = moneyFields;
2294
3040
  }
2295
3041
  /** @internal — attach computed fields post-construction. See {@link _applyMoneyFields}. */
@@ -2349,6 +3095,13 @@ var Collection = class {
2349
3095
  }
2350
3096
  this.classified = resolved;
2351
3097
  this.computed = { ...resolved.riderComputed, ...this.computed ?? {} };
3098
+ this.via = ViaPipeline.build([...this.via?.bindings ?? [], viaBinder("classified")({
3099
+ entries: classifiedFields,
3100
+ collectionName: this.name,
3101
+ guardCtx: this.classifiedGuardCtx,
3102
+ classifySealedShred: (live) => this.codec.classifySealedShred(live)
3103
+ // #629 T10
3104
+ })]);
2352
3105
  }
2353
3106
  /** @internal — used only in tests; do not read in production code. */
2354
3107
  get _ramCiphertext() {
@@ -2375,7 +3128,7 @@ var Collection = class {
2375
3128
  }
2376
3129
  }
2377
3130
  if (this.materializedViewSource !== void 0) {
2378
- const { resolveStaleMVOnRead } = await import("./stale-DP66HFT3.js");
3131
+ const { resolveStaleMVOnRead } = await import("./stale-IYYXEH3U.js");
2379
3132
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
2380
3133
  }
2381
3134
  let record;
@@ -2531,7 +3284,7 @@ var Collection = class {
2531
3284
  if (cached) return cached.record;
2532
3285
  const env = await this.adapter.get(this.vault, this.name, id);
2533
3286
  if (!env) return void 0;
2534
- const rec = await this.codec.decryptRecord(env);
3287
+ const rec = await this.codec.decryptRecord(env, { id });
2535
3288
  return rec === null ? void 0 : rec;
2536
3289
  }
2537
3290
  await this.ensureHydrated();
@@ -2579,7 +3332,7 @@ var Collection = class {
2579
3332
  if (cached2) return { record: cached2.record, version: cached2.version };
2580
3333
  const env = await this.adapter.get(this.vault, this.name, id);
2581
3334
  if (!env) return { record: null, version: 0 };
2582
- return { record: await this.codec.decryptRecord(env, { skipValidation: true }) ?? null, version: env._v };
3335
+ return { record: await this.codec.decryptRecord(env, { skipValidation: true, id }) ?? null, version: env._v };
2583
3336
  }
2584
3337
  await this.ensureHydrated();
2585
3338
  const cached = this.cache.get(id);
@@ -2613,7 +3366,7 @@ var Collection = class {
2613
3366
  const env = await this.adapter.get(this.vault, this.name, id);
2614
3367
  if (!env) return { env: null, record: null, elided: false };
2615
3368
  try {
2616
- return { env, record: await this.codec.decryptRecord(env, { skipValidation: true }), elided: false };
3369
+ return { env, record: await this.codec.decryptRecord(env, { skipValidation: true, id }), elided: false };
2617
3370
  } catch {
2618
3371
  return { env, record: null, elided: false };
2619
3372
  }
@@ -2638,7 +3391,7 @@ var Collection = class {
2638
3391
  if (!hasWritePermission(this.keyring, this.name)) {
2639
3392
  throw new ReadOnlyError();
2640
3393
  }
2641
- if (this.moneyFields) record = moneyRuntime().canonicalizeIncomingMoney(record, this.moneyFields);
3394
+ if (this.via) record = this.via.ingest(record);
2642
3395
  if (this.subsystemBus?.hasGateHandlers("beforePut")) {
2643
3396
  const { env: existingEnv, record: existingRecord } = await this.resolveGatePrior("beforePut", id);
2644
3397
  const gateEvent = {
@@ -2647,7 +3400,7 @@ var Collection = class {
2647
3400
  collection: this.name,
2648
3401
  docId: id,
2649
3402
  incoming: record,
2650
- existing: this.moneyFields ? moneyRuntime().canonicalizeStoredMoney(existingRecord, this.moneyFields) : existingRecord,
3403
+ existing: this.via ? this.via.canonicalizeStored(existingRecord) : existingRecord,
2651
3404
  existingVersion: existingEnv?._v ?? 0,
2652
3405
  existingTs: existingEnv?._ts,
2653
3406
  userId: this.keyring.userId,
@@ -2656,99 +3409,21 @@ var Collection = class {
2656
3409
  };
2657
3410
  await this.subsystemBus.dispatchGate("beforePut", gateEvent);
2658
3411
  }
2659
- if (this.classified !== void 0) {
2660
- enforceClassifiedWrite(record, this.classified.byField, this.name);
2661
- }
3412
+ const viaWriteCtx = {
3413
+ id,
3414
+ vault: this.vault,
3415
+ prior: async () => await this.resolveDensifyPrior(id) ?? null,
3416
+ emit: (e, p) => this.emitter.emit(e, p)
3417
+ };
3418
+ if (this.via) await this.via.enforceWrite(record, viaWriteCtx);
2662
3419
  if (this.computed !== void 0) {
2663
3420
  record = (await loadEvalComputedFields())(record, this.computed, id);
2664
3421
  }
2665
3422
  if (this.schema !== void 0) {
2666
3423
  record = await validateSchemaInput(this.schema, record, `put(${id})`);
2667
3424
  }
2668
- if (this.moneyFields) {
2669
- record = moneyRuntime().quantizeMoneyFields(record, this.moneyFields);
2670
- }
2671
- if (this.i18nFields) {
2672
- const obj = record;
2673
- for (const [field, descriptor] of Object.entries(this.i18nFields)) {
2674
- if (!descriptor.options.autoTranslate) continue;
2675
- const leafValues = getAtPath(obj, field);
2676
- if (leafValues.length !== 1) continue;
2677
- const value = leafValues[0];
2678
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
2679
- const map = value;
2680
- const { languages, required } = descriptor.options;
2681
- const missing = languages.filter(
2682
- (lang) => !(lang in map) || map[lang] === ""
2683
- );
2684
- if (missing.length === 0) continue;
2685
- const sourceLocale = languages.find((l) => l in map && map[l] !== "");
2686
- if (!sourceLocale) continue;
2687
- if (!this.autoTranslateHook) {
2688
- throw new TranslatorNotConfiguredError(field, this.name);
2689
- }
2690
- const toTranslate = required === "any" ? [] : required === "all" ? missing : missing.filter((l) => required.includes(l));
2691
- const translated = { ...map };
2692
- for (const targetLocale of toTranslate) {
2693
- translated[targetLocale] = await this.autoTranslateHook(
2694
- map[sourceLocale],
2695
- sourceLocale,
2696
- targetLocale,
2697
- field,
2698
- this.name
2699
- );
2700
- }
2701
- setAtPathInPlace(obj, field, translated);
2702
- }
2703
- }
2704
- let densifyPrior;
2705
- let exemptFills;
2706
- if (this.i18nDensifyFields) {
2707
- densifyPrior = await this.resolveDensifyPrior(id);
2708
- exemptFills = this.i18nStrategy.computeExemptFills(
2709
- densifyPrior,
2710
- record,
2711
- this.i18nDensifyFields
2712
- );
2713
- }
2714
- if (this.i18nFields) {
2715
- const obj = record;
2716
- for (const [field, descriptor] of Object.entries(this.i18nFields)) {
2717
- if (!descriptor.options.script) continue;
2718
- for (const leaf of getAtPath(obj, field)) {
2719
- if (!leaf || typeof leaf !== "object" || Array.isArray(leaf)) continue;
2720
- const leafMap = leaf;
2721
- const { value: cleaned, warnings } = this.i18nStrategy.enforceScript(
2722
- leafMap,
2723
- field,
2724
- descriptor,
2725
- exemptFills?.get(field)
2726
- );
2727
- if (cleaned !== leafMap) Object.assign(leafMap, cleaned);
2728
- const mode = descriptor.options.onScriptViolation;
2729
- if (mode === "warn" || mode === "filter") {
2730
- for (const w of warnings) {
2731
- this.emitter.emit("i18n:script-violation", {
2732
- vault: this.vault,
2733
- collection: this.name,
2734
- id,
2735
- mode,
2736
- warning: w
2737
- });
2738
- }
2739
- }
2740
- }
2741
- }
2742
- }
2743
- if (this.i18nPutValidator !== void 0) {
2744
- this.i18nPutValidator(record);
2745
- }
2746
- if (this.i18nDensifyFields) {
2747
- this.i18nStrategy.densify(
2748
- record,
2749
- densifyPrior,
2750
- this.i18nDensifyFields
2751
- );
3425
+ if (this.via) {
3426
+ record = await this.via.encodeWrite(record, viaWriteCtx);
2752
3427
  }
2753
3428
  if (this.refEnforcer !== void 0) {
2754
3429
  await this.refEnforcer.enforceRefsOnPut(this.name, record);
@@ -2795,7 +3470,7 @@ var Collection = class {
2795
3470
  const existingResolved = existingResolvedRecord !== null ? { record: existingResolvedRecord, version: existingVersion } : void 0;
2796
3471
  if (existingResolved && this.historyConfig.enabled !== false) {
2797
3472
  const vdigCtx2 = this.vdigFields !== null ? { id, prev: existingEnvelope } : void 0;
2798
- const histEnvelope = await this.codec.encryptRecord(existingResolved.record, existingResolved.version, cek2, void 0, void 0, vdigCtx2);
3473
+ const histEnvelope = await this.codec.encryptRecord(existingResolved.record, existingResolved.version, cek2, void 0, void 0, vdigCtx2, id);
2799
3474
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, histEnvelope);
2800
3475
  this.emitter.emit("history:save", { vault: this.vault, collection: this.name, id, version: existingResolved.version });
2801
3476
  if (this.historyConfig.maxVersions) {
@@ -2827,12 +3502,7 @@ var Collection = class {
2827
3502
  this.cache.set(id, { record: resolvedRecord, version: version2 });
2828
3503
  this.indexes?.upsert(id, resolvedRecord, existingResolved ? existingResolved.record : null);
2829
3504
  }
2830
- await this.onDirty?.(this.name, id, "put", version2);
2831
- this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "put" });
2832
- this.searchIndexStore?.markDirty();
2833
- await this.onAccess?.("put", id);
2834
- await this.dispatchDerivations(id, record, version2);
2835
- await this.dispatchMaterializedViews(id, record);
3505
+ await this._onRecordMutated(id, "put", "local-write", { record, version: version2 });
2836
3506
  return;
2837
3507
  }
2838
3508
  let existing;
@@ -2842,7 +3512,7 @@ var Collection = class {
2842
3512
  if (!existing) {
2843
3513
  priorRaw = await this.adapter.get(this.vault, this.name, id);
2844
3514
  if (priorRaw) {
2845
- const previousRecord = await this.codec.decryptRecord(priorRaw);
3515
+ const previousRecord = await this.codec.decryptRecord(priorRaw, { id });
2846
3516
  if (previousRecord !== null) {
2847
3517
  existing = { record: previousRecord, version: priorRaw._v };
2848
3518
  }
@@ -2861,7 +3531,7 @@ var Collection = class {
2861
3531
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
2862
3532
  const vdigCtx = this.vdigFields !== null ? { id, prev: await this.adapter.get(this.vault, this.name, id) } : void 0;
2863
3533
  if (existing && this.historyConfig.enabled !== false) {
2864
- const historyEnvelope = await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, vdigCtx);
3534
+ const historyEnvelope = await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, vdigCtx, id);
2865
3535
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
2866
3536
  this.emitter.emit("history:save", {
2867
3537
  vault: this.vault,
@@ -2875,7 +3545,7 @@ var Collection = class {
2875
3545
  });
2876
3546
  }
2877
3547
  }
2878
- const envelope = await this.codec.encryptRecord(record, version, cek, options?.source, options?.sourceTs, vdigCtx);
3548
+ const envelope = await this.codec.encryptRecord(record, version, cek, options?.source, options?.sourceTs, vdigCtx, id);
2879
3549
  await this.adapter.put(this.vault, this.name, id, envelope);
2880
3550
  await this._ensureClassifiedMarker();
2881
3551
  if (this.embeddings) {
@@ -2904,17 +3574,7 @@ var Collection = class {
2904
3574
  this.indexes?.upsert(id, record, existing ? existing.record : null);
2905
3575
  this.uniqueConstraints?.upsert(id, record, existing?.record);
2906
3576
  }
2907
- await this.onDirty?.(this.name, id, "put", version);
2908
- this.emitter.emit("change", {
2909
- vault: this.vault,
2910
- collection: this.name,
2911
- id,
2912
- action: "put"
2913
- });
2914
- this.searchIndexStore?.markDirty();
2915
- await this.onAccess?.("put", id);
2916
- await this.dispatchDerivations(id, record, version);
2917
- await this.dispatchMaterializedViews(id, record);
3577
+ await this._onRecordMutated(id, "put", "local-write", { record, version });
2918
3578
  }
2919
3579
  /**
2920
3580
  * Fire registered MV strategies whose dependency set includes this
@@ -2926,10 +3586,10 @@ var Collection = class {
2926
3586
  * MV-emitted row (carries `_materializedFrom`) — defensive guard
2927
3587
  * against missed cycle detection.
2928
3588
  *
2929
- * @internal
3589
+ * @internal `wave` (#638 Task 4): when present (the sync/cutover/restore dispatch wave),
3590
+ * an eager MV already refreshed this wave is skipped (per-target dedup, keyed on spec name).
2930
3591
  */
2931
- async dispatchMaterializedViews(id, record) {
2932
- void id;
3592
+ async dispatchMaterializedViews(id, record, wave) {
2933
3593
  if (this.materializedViewSource === void 0) return;
2934
3594
  const incoming = record;
2935
3595
  if (incoming && typeof incoming === "object" && "_materializedFrom" in incoming) return;
@@ -2941,18 +3601,20 @@ var Collection = class {
2941
3601
  for (const reg of mvs) {
2942
3602
  const mode = reg.spec.refresh;
2943
3603
  if (mode === "eager") {
3604
+ if (wave?.seen(`mv\0${reg.spec.name}`)) continue;
2944
3605
  if (executor === null) {
2945
3606
  ;
2946
- ({ MaterializedViewExecutor: executor } = await import("./executor-6LHOV7MN.js"));
3607
+ ({ MaterializedViewExecutor: executor } = await import("./executor-IL7SRNUZ.js"));
2947
3608
  }
2948
3609
  await executor.refresh(reg, {
2949
3610
  getCollection: (name) => this.materializedViewSource.getCollection(name),
2950
3611
  getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
2951
- getQueryContext: () => this.materializedViewSource.getQueryContext()
3612
+ getQueryContext: () => this.materializedViewSource.getQueryContext(),
3613
+ dispatchCtx: this.#dispatchCtx({ collection: this.name, id })
2952
3614
  });
2953
3615
  } else if (mode === "lazy") {
2954
3616
  if (staleHelpers === null) {
2955
- staleHelpers = await import("./stale-DP66HFT3.js");
3617
+ staleHelpers = await import("./stale-IYYXEH3U.js");
2956
3618
  }
2957
3619
  staleHelpers.markMVStale(registry, reg.spec.name);
2958
3620
  }
@@ -3004,7 +3666,15 @@ var Collection = class {
3004
3666
  raw = (await this.resolvePriorValues(id))?.record ?? null;
3005
3667
  }
3006
3668
  if (raw === null) return null;
3007
- return this.moneyFields ? moneyRuntime().canonicalizeStoredMoney(raw, this.moneyFields) : raw;
3669
+ return this.via ? this.via.canonicalizeStored(raw) : raw;
3670
+ }
3671
+ /** @internal #638 Task 4 — decrypted STORED-form record + envelope version for the sync/cutover/
3672
+ * restore dispatch wave (id threaded into decrypt, matching `_invalidateCacheEntry`'s contract). */
3673
+ async _getStoredRecordForDispatch(id) {
3674
+ const env = await this.adapter.get(this.vault, this.name, id);
3675
+ if (!env || isTombstone(env, this.storeCiphertext) || isDeleteMarker(env)) return null;
3676
+ const record = await this.codec.decryptRecord(env, { id });
3677
+ return record === null ? null : { record, version: env._v };
3008
3678
  }
3009
3679
  /**
3010
3680
  * @internal Ids of records whose top-level `field` equals `value`.
@@ -3035,19 +3705,22 @@ var Collection = class {
3035
3705
  }
3036
3706
  return out;
3037
3707
  }
3038
- /**
3039
- * @internal Recompute a rollup aggregate onto the parent.
3040
- * Gathers every child of `parentId`, runs `compute`, and patches only the
3041
- * rollup `field` onto the parent's raw stored record (value-equality
3042
- * guarded). No-op when the parent record does not exist.
3043
- */
3044
- async recomputeRollup(spec, parentId) {
3045
- if (this.derivationSource === void 0 || spec.rollup === void 0) return;
3708
+ /** @internal Recompute a rollup aggregate onto the parent from `parentId`'s current children
3709
+ * (value-equality guarded; no-op absent parent). `wave` (#638 T4): per-target dedup. Returns
3710
+ * the `putDerivedOutput` outcome, or `'noop'` when nothing needed recomputing (#638 T6 —
3711
+ * `dispatchRollupsOnDelete`'s forget-fanout caller needs this to fill the forget report). */
3712
+ /** @internal ctx for `putDerivedOutput`'s frozen-period skip+audit (#638 Task 5). */
3713
+ #dispatchCtx(source) {
3714
+ return { emit: (e, p) => this.emitter.emit(e, p), source, audit: ledgerAuditHook(this.ledger, this.keyring.userId) };
3715
+ }
3716
+ async recomputeRollup(spec, parentId, source, wave) {
3717
+ if (this.derivationSource === void 0 || spec.rollup === void 0) return "noop";
3046
3718
  const { from, key, field, compute } = spec.rollup;
3047
3719
  const into = spec.source;
3720
+ if (wave?.seen(`rollup\0${into}\0${parentId}\0${field}`)) return "noop";
3048
3721
  const intoColl = this.derivationSource.getCollection(into);
3049
3722
  const base = await intoColl._getStoredRecord(parentId);
3050
- if (base === null) return;
3723
+ if (base === null) return "noop";
3051
3724
  const fromColl = this.derivationSource.getCollection(from);
3052
3725
  const childIds = await fromColl._findMatchingIds(key, parentId);
3053
3726
  const children = [];
@@ -3056,7 +3729,7 @@ var Collection = class {
3056
3729
  if (c !== null && c !== void 0) children.push(c);
3057
3730
  }
3058
3731
  const newValue = compute(children);
3059
- if (selfWriteFieldEqual(base[field], newValue)) return;
3732
+ if (selfWriteFieldEqual(base[field], newValue)) return "noop";
3060
3733
  const patched = { ...base, [field]: newValue };
3061
3734
  const txCtx = this.derivationSource.getActiveTxContext();
3062
3735
  if (txCtx !== null) {
@@ -3066,28 +3739,32 @@ var Collection = class {
3066
3739
  priorEnvelope: prior
3067
3740
  });
3068
3741
  }
3069
- await intoColl.put(parentId, patched);
3742
+ return putDerivedOutput(intoColl, parentId, patched, this.#dispatchCtx(source));
3070
3743
  }
3071
3744
  /**
3072
- * @internal Fire any rollups for which THIS collection is the
3073
- * child `from`, recomputing the affected parent after a child delete. Called
3074
- * from the delete path with the just-removed record's key value. Other
3075
- * derivation kinds do not react to deletes (unchanged).
3745
+ * @internal Fire any rollups for which THIS collection is the child `from`, recomputing the
3746
+ * affected parent after a child delete/forget. Called from the delete path (return discarded)
3747
+ * and from `forgetDerivedFanout` (#638 Task 6), which needs the per-target outcome to fill
3748
+ * `ForgetResult.derivedAggregatesRecomputed`/`derivedResidueFrozen`.
3076
3749
  */
3077
- async dispatchRollupsOnDelete(deleted) {
3078
- if (this.derivationSource === void 0) return;
3750
+ async dispatchRollupsOnDelete(id, deleted) {
3751
+ if (this.derivationSource === void 0) return [];
3079
3752
  const registry = this.derivationSource.registry();
3080
3753
  const rec = deleted;
3754
+ const results = [];
3081
3755
  for (const { spec } of registry.strategiesForSource(this.name)) {
3082
3756
  if (!spec.rollup || spec.rollup.from !== this.name) continue;
3083
3757
  const kv = rec[spec.rollup.key];
3084
3758
  if (typeof kv !== "string" && typeof kv !== "number") continue;
3085
- await this.recomputeRollup(spec, String(kv));
3759
+ results.push({ into: spec.source, parentId: String(kv), outcome: await this.recomputeRollup(spec, String(kv), { collection: this.name, id }) });
3086
3760
  }
3761
+ return results;
3087
3762
  }
3088
- async dispatchDerivations(id, record, version) {
3763
+ /** @internal `wave` (#638 Task 4) — threaded to `recomputeRollup` for the sync/cutover/restore
3764
+ * dispatch wave's per-target dedup; `undefined` on the local-write path (byte-identical). */
3765
+ async dispatchDerivations(id, record, version, wave) {
3089
3766
  if (this.derivationSource === void 0) return;
3090
- const incoming = this.moneyFields ? moneyRuntime().canonicalizeStoredMoney(record, this.moneyFields) : record;
3767
+ const incoming = this.via ? this.via.canonicalizeStored(record) : record;
3091
3768
  if (incoming && typeof incoming === "object" && "_derivedFrom" in incoming) return;
3092
3769
  const registry = this.derivationSource.registry();
3093
3770
  const strategies = registry.strategiesForSource(this.name);
@@ -3104,7 +3781,7 @@ var Collection = class {
3104
3781
  } else {
3105
3782
  parentId = id;
3106
3783
  }
3107
- if (parentId !== null) await this.recomputeRollup(spec, parentId);
3784
+ if (parentId !== null) await this.recomputeRollup(spec, parentId, { collection: this.name, id }, wave);
3108
3785
  continue;
3109
3786
  }
3110
3787
  const isSource = spec.source === this.name;
@@ -3137,10 +3814,11 @@ var Collection = class {
3137
3814
  continue;
3138
3815
  }
3139
3816
  if (DerivationExecutor === null) {
3140
- ({ DerivationExecutor } = await import("./executor-EUPYHCLU.js"));
3817
+ ({ DerivationExecutor } = await import("./executor-WAEI6PDV.js"));
3141
3818
  }
3142
3819
  for (const run of runs) {
3143
3820
  const ctx = { vault: this.derivationSource.getReadOnlyFacade() };
3821
+ const outCtx = this.#dispatchCtx({ collection: spec.source, id: run.runId });
3144
3822
  const result = await DerivationExecutor.run(spec, run.input, run.version, strategyHash, ctx);
3145
3823
  for (const key of Object.keys(spec.outputs)) {
3146
3824
  const out = result.outputs[key];
@@ -3156,7 +3834,7 @@ var Collection = class {
3156
3834
  const outputCollection = this.derivationSource.getCollection(outSpec.collection);
3157
3835
  const txCtx = this.derivationSource.getActiveTxContext();
3158
3836
  if (out.kind === "array") {
3159
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-BDKLP5GG.js");
3837
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-CI5BCMQM.js");
3160
3838
  const prior = await loadFanoutSidecar(this.adapter, this.vault, spec.source, run.runId, key, this.getDEK, this.storeCiphertext);
3161
3839
  const prevKeys = new Set(prior?.keys ?? []);
3162
3840
  const newKeysList = out.entries.map((e) => e.key);
@@ -3178,7 +3856,7 @@ var Collection = class {
3178
3856
  priorEnvelope
3179
3857
  });
3180
3858
  }
3181
- await outputCollection.put(entry.key, entry.value, { source: "derived" });
3859
+ await putDerivedOutput(outputCollection, entry.key, entry.value, outCtx, { source: "derived" });
3182
3860
  }
3183
3861
  await saveFanoutSidecar(this.adapter, this.vault, {
3184
3862
  source: spec.source,
@@ -3211,7 +3889,7 @@ var Collection = class {
3211
3889
  priorEnvelope: prior
3212
3890
  });
3213
3891
  }
3214
- await outputCollection.put(run.runId, patched, { source: "derived" });
3892
+ await putDerivedOutput(outputCollection, run.runId, patched, outCtx, { source: "derived" });
3215
3893
  continue;
3216
3894
  }
3217
3895
  if (txCtx !== null) {
@@ -3226,7 +3904,7 @@ var Collection = class {
3226
3904
  priorEnvelope: prior
3227
3905
  });
3228
3906
  }
3229
- await outputCollection.put(run.runId, out.value, { source: "derived" });
3907
+ await putDerivedOutput(outputCollection, run.runId, out.value, outCtx, { source: "derived" });
3230
3908
  }
3231
3909
  }
3232
3910
  }
@@ -3283,9 +3961,9 @@ var Collection = class {
3283
3961
  const next = transform(record);
3284
3962
  const nextVersion = (env._v ?? 0) + 1;
3285
3963
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
3286
- const newEnv = await this.codec.encryptRecord(next, nextVersion, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: env } : void 0);
3964
+ const newEnv = await this.codec.encryptRecord(next, nextVersion, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: env } : void 0, id);
3287
3965
  await this.adapter.put(this.vault, this.name, id, newEnv);
3288
- await this._invalidateCacheEntry(id);
3966
+ await this._onRecordMutated(id, "put", "cutover");
3289
3967
  if (this.ledger) {
3290
3968
  await this.ledger.append({
3291
3969
  op: "migration",
@@ -3314,9 +3992,8 @@ var Collection = class {
3314
3992
  * history snapshot still fire so backup integrity and time-travel
3315
3993
  * reconstruction stay consistent.
3316
3994
  *
3317
- * Returns silently for delete-of-absent (idempotent contract — both
3318
- * paths honour this: the `txCtx === null` path also reads the prior
3319
- * envelope and short-circuits before the ledger/event side-effects).
3995
+ * Returns `true` when it erased a live record, `false` for delete-of-absent (idempotent
3996
+ * contract both txCtx-aware and txCtx===null callers honour this, short-circuiting before any side-effect).
3320
3997
  *
3321
3998
  * When a `txCtx` is supplied, the prior envelope is captured and
3322
3999
  * pushed onto `txCtx._executed` BEFORE the delete fires — mirrors
@@ -3345,7 +4022,7 @@ var Collection = class {
3345
4022
  */
3346
4023
  async _internalDelete(id, txCtx = null) {
3347
4024
  const prior = await this.adapter.get(this.vault, this.name, id);
3348
- if (prior === null) return;
4025
+ if (prior === null) return false;
3349
4026
  if (txCtx !== null) {
3350
4027
  txCtx._executed.push({
3351
4028
  op: {
@@ -3358,6 +4035,7 @@ var Collection = class {
3358
4035
  });
3359
4036
  }
3360
4037
  await this._doDelete(id, true);
4038
+ return true;
3361
4039
  }
3362
4040
  async _doDelete(id, internal) {
3363
4041
  if (!hasWritePermission(this.keyring, this.name)) {
@@ -3370,7 +4048,7 @@ var Collection = class {
3370
4048
  vault: this.vault,
3371
4049
  collection: this.name,
3372
4050
  docId: id,
3373
- existing: this.moneyFields ? moneyRuntime().canonicalizeStoredMoney(existingRecord, this.moneyFields) : existingRecord,
4051
+ existing: this.via ? this.via.canonicalizeStored(existingRecord) : existingRecord,
3374
4052
  existingVersion: existingEnv?._v ?? 0,
3375
4053
  existingTs: existingEnv?._ts,
3376
4054
  internal,
@@ -3388,7 +4066,7 @@ var Collection = class {
3388
4066
  if (!existing && this.historyConfig.enabled !== false) {
3389
4067
  const previousEnvelope2 = await this.adapter.get(this.vault, this.name, id);
3390
4068
  if (previousEnvelope2) {
3391
- const previousRecord = await this.codec.decryptRecord(previousEnvelope2);
4069
+ const previousRecord = await this.codec.decryptRecord(previousEnvelope2, { id });
3392
4070
  if (previousRecord !== null) {
3393
4071
  existing = { record: previousRecord, version: previousEnvelope2._v };
3394
4072
  }
@@ -3400,7 +4078,7 @@ var Collection = class {
3400
4078
  if (existing && this.historyConfig.enabled !== false) {
3401
4079
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
3402
4080
  const prevForVdig = this.vdigFields !== null ? await this.adapter.get(this.vault, this.name, id) : null;
3403
- const historyEnvelope = await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0);
4081
+ const historyEnvelope = await this.codec.encryptRecord(existing.record, existing.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0, id);
3404
4082
  await this.historyStrategy.saveHistory(this.adapter, this.vault, this.name, id, historyEnvelope);
3405
4083
  }
3406
4084
  const previousEnvelope = await this.adapter.get(this.vault, this.name, id);
@@ -3436,19 +4114,11 @@ var Collection = class {
3436
4114
  this.uniqueConstraints?.remove(id, existing.record);
3437
4115
  }
3438
4116
  }
3439
- await this.onDirty?.(this.name, id, "put", markerVersion ?? (existing?.version ?? 0) + 1);
3440
- this.emitter.emit("change", {
3441
- vault: this.vault,
3442
- collection: this.name,
3443
- id,
3444
- action: "delete"
3445
- });
3446
- this.searchIndexStore?.markDirty();
3447
- await this.onAccess?.("delete", id);
4117
+ await this._onRecordMutated(id, "delete", "local-delete", { version: markerVersion ?? (existing?.version ?? 0) + 1 });
3448
4118
  if (!internal) {
3449
4119
  await this.dispatchMaterializedViewsOnDelete(id);
3450
4120
  await this.dispatchArrayDerivationsOnDelete(id);
3451
- if (existing) await this.dispatchRollupsOnDelete(existing.record);
4121
+ if (existing) await this.dispatchRollupsOnDelete(id, existing.record);
3452
4122
  }
3453
4123
  }
3454
4124
  /**
@@ -3497,75 +4167,81 @@ var Collection = class {
3497
4167
  return { previousVersion: live._v };
3498
4168
  }
3499
4169
  /**
3500
- * Cascade deletes of array-shape derived rows when a source row is
3501
- * deleted. Reads each registered strategy's fanout sidecar
3502
- * for this source id, deletes every listed derived row, then
3503
- * deletes the sidecar itself.
3504
- *
3505
- * Record-shape derivations are skipped — see _doDelete's comment
3506
- * for why the asymmetry is correct.
4170
+ * Cascade deletes of array-shape derived rows when a source row is deleted. Reads each
4171
+ * registered strategy's fanout sidecar for this source id, deletes every listed derived
4172
+ * row, then deletes the sidecar itself. Returns the REAL erased count (#622 review Finding 1).
3507
4173
  *
4174
+ * Record-shape derivations are skipped on the ordinary delete path (see _doDelete's comment).
4175
+ * `eraseRecordShapeToo` (#638 T6, default `false`) opts a same-id record-shape copy into
4176
+ * erasure too — forget()'s fanout, GDPR residue. A delete-of-absent contributes 0 either way.
3508
4177
  * @internal
3509
4178
  */
3510
- async dispatchArrayDerivationsOnDelete(id) {
3511
- if (this.derivationSource === void 0) return;
4179
+ async dispatchArrayDerivationsOnDelete(id, eraseRecordShapeToo = false) {
4180
+ if (this.derivationSource === void 0) return 0;
3512
4181
  const registry = this.derivationSource.registry();
3513
4182
  const strategies = registry.strategiesForSource(this.name);
3514
- if (strategies.length === 0) return;
4183
+ if (strategies.length === 0) return 0;
3515
4184
  let helpers = null;
3516
4185
  const txCtx = this.derivationSource.getActiveTxContext();
4186
+ let erased = 0;
3517
4187
  for (const { spec } of strategies) {
3518
4188
  for (const [outputKey, outSpec] of Object.entries(spec.outputs)) {
3519
- if (outSpec.shape !== "array") continue;
4189
+ if (outSpec.shape === "record") {
4190
+ if (eraseRecordShapeToo && spec.source === this.name && outSpec.collection !== this.name) {
4191
+ if (await this.derivationSource.getCollection(outSpec.collection)._internalDelete(id, txCtx)) erased += 1;
4192
+ }
4193
+ continue;
4194
+ }
3520
4195
  if (helpers === null) {
3521
- helpers = await import("./fanout-sidecar-BDKLP5GG.js");
4196
+ helpers = await import("./fanout-sidecar-CI5BCMQM.js");
3522
4197
  }
3523
4198
  const sidecar = await helpers.loadFanoutSidecar(this.adapter, this.vault, spec.source, id, outputKey, this.getDEK, this.storeCiphertext);
3524
4199
  if (!sidecar) continue;
3525
4200
  const outputCollection = this.derivationSource.getCollection(outSpec.collection);
3526
4201
  for (const derivedId of sidecar.keys) {
3527
- await outputCollection._internalDelete(derivedId, txCtx);
4202
+ if (await outputCollection._internalDelete(derivedId, txCtx)) erased += 1;
3528
4203
  }
3529
4204
  await helpers.deleteFanoutSidecar(this.adapter, this.vault, spec.source, id, outputKey);
3530
4205
  }
3531
4206
  }
4207
+ return erased;
3532
4208
  }
3533
4209
  /**
3534
- * Mirror of {@link dispatchMaterializedViews} for the delete path.
3535
- * No record content is available (it's gone), so the
3536
- * `_materializedFrom` skip used by the put-side dispatch doesn't
3537
- * apply here instead, the recursion guard is the `internal` gate
3538
- * at the `_doDelete` call site above.
3539
- *
4210
+ * Mirror of {@link dispatchMaterializedViews} for the delete/forget path — no `_materializedFrom`
4211
+ * skip (record's gone); the `internal` gate at `_doDelete` is the recursion guard. Returns the
4212
+ * row count TOMBSTONED across every eager MV sourced here (#638 T6 — `forgetDerivedFanout`'s
4213
+ * `derivedRecordsErased`); a lazy MV only flips its stale bit (0 contribution — resolved on read).
3540
4214
  * @internal
3541
4215
  */
3542
4216
  async dispatchMaterializedViewsOnDelete(id) {
3543
- void id;
3544
- if (this.materializedViewSource === void 0) return;
4217
+ if (this.materializedViewSource === void 0) return 0;
3545
4218
  const registry = this.materializedViewSource.registry();
3546
4219
  const mvs = registry.mvsForSource(this.name);
3547
- if (mvs.length === 0) return;
4220
+ if (mvs.length === 0) return 0;
3548
4221
  let executor = null;
3549
4222
  let staleHelpers = null;
4223
+ let deleted = 0;
3550
4224
  for (const reg of mvs) {
3551
4225
  const mode = reg.spec.refresh;
3552
4226
  if (mode === "eager") {
3553
4227
  if (executor === null) {
3554
4228
  ;
3555
- ({ MaterializedViewExecutor: executor } = await import("./executor-6LHOV7MN.js"));
4229
+ ({ MaterializedViewExecutor: executor } = await import("./executor-IL7SRNUZ.js"));
3556
4230
  }
3557
- await executor.refresh(reg, {
4231
+ deleted += (await executor.refresh(reg, {
3558
4232
  getCollection: (name) => this.materializedViewSource.getCollection(name),
3559
4233
  getActiveTxContext: () => this.materializedViewSource.getActiveTxContext(),
3560
- getQueryContext: () => this.materializedViewSource.getQueryContext()
3561
- });
4234
+ getQueryContext: () => this.materializedViewSource.getQueryContext(),
4235
+ dispatchCtx: this.#dispatchCtx({ collection: this.name, id })
4236
+ })).deleted;
3562
4237
  } else if (mode === "lazy") {
3563
4238
  if (staleHelpers === null) {
3564
- staleHelpers = await import("./stale-DP66HFT3.js");
4239
+ staleHelpers = await import("./stale-IYYXEH3U.js");
3565
4240
  }
3566
4241
  staleHelpers.markMVStale(registry, reg.spec.name);
3567
4242
  }
3568
4243
  }
4244
+ return deleted;
3569
4245
  }
3570
4246
  /**
3571
4247
  * List all records in the collection.
@@ -3584,22 +4260,14 @@ var Collection = class {
3584
4260
  );
3585
4261
  }
3586
4262
  if (this.materializedViewSource !== void 0) {
3587
- const { resolveStaleMVOnRead } = await import("./stale-DP66HFT3.js");
4263
+ const { resolveStaleMVOnRead } = await import("./stale-IYYXEH3U.js");
3588
4264
  await resolveStaleMVOnRead(this.materializedViewSource, this.name);
3589
4265
  }
3590
4266
  await this.ensureHydrated();
3591
4267
  const records = [...this.cache.values()].map((e) => e.record);
3592
- if (!this.hasReadTransforms()) return records;
4268
+ if (!this.via) return records;
3593
4269
  return Promise.all(records.map((r) => this.applyLocaleToRecord(r, locale)));
3594
4270
  }
3595
- /**
3596
- * @internal — whether any read-side record transform is registered
3597
- * (money decode, i18nText resolution, dictKey labels). Gates the
3598
- * no-transform fast path in {@link list}.
3599
- */
3600
- hasReadTransforms() {
3601
- return this.moneyFields !== void 0 && Object.keys(this.moneyFields).length > 0 || this.i18nFields !== void 0 && Object.keys(this.i18nFields).length > 0 || this.dictKeyFields !== void 0 && Object.keys(this.dictKeyFields).length > 0;
3602
- }
3603
4271
  /**
3604
4272
  * Scan-mode full-text search over a plain-text `field`. Decrypts the
3605
4273
  * collection in memory and ranks records by BM25 against the tokenized query.
@@ -3826,7 +4494,7 @@ var Collection = class {
3826
4494
  getIndexes: () => this.getIndexes(),
3827
4495
  lookupById: (id) => this.cache.get(id)?.record,
3828
4496
  snapshotEntries: () => [...this.cache.entries()].map(([id, e]) => ({ id, record: e.record })),
3829
- ...this.moneyFields ? { moneyFields: this.moneyFields } : {}
4497
+ ...this.via ? { via: this.via } : {}
3830
4498
  };
3831
4499
  const resolver = this.joinResolver;
3832
4500
  const leftCollection = this.name;
@@ -3906,8 +4574,7 @@ var Collection = class {
3906
4574
  * silently miss records — consistent with the `query()` /
3907
4575
  * `list()` lazy-mode policy. If this becomes a blocker for a
3908
4576
  * real consumer, the fix is to add an async `scan()`-backed
3909
- * variant of this method, which is exactly what streaming
3910
- * joins will need anyway.
4577
+ * variant of this method, which is exactly what streaming joins will need anyway.
3911
4578
  */
3912
4579
  querySourceForJoin() {
3913
4580
  if (this.lazy) {
@@ -3927,9 +4594,8 @@ var Collection = class {
3927
4594
  this.emitter.on("change", handler);
3928
4595
  return () => this.emitter.off("change", handler);
3929
4596
  },
3930
- // Expose this (right-side) collection's i18nText descriptors so
3931
- // the join executor can resolve joined i18n fields at the `join` layer.
3932
- ...this.i18nFields !== void 0 ? { i18nFields: this.i18nFields } : {}
4597
+ // Sync join dressing (#650 Task 6, #626 retirement) — i18n-text + lookup-label, from this collection's own bindings.
4598
+ ...this.presentForJoin !== void 0 ? { presentForJoin: this.presentForJoin } : {}
3933
4599
  };
3934
4600
  }
3935
4601
  /**
@@ -3965,7 +4631,7 @@ var Collection = class {
3965
4631
  );
3966
4632
  const entries = [];
3967
4633
  for (const env of envelopes) {
3968
- const record = await this.codec.decryptRecord(env, { skipValidation: true });
4634
+ const record = await this.codec.decryptRecord(env, { skipValidation: true, id });
3969
4635
  if (record === null) continue;
3970
4636
  entries.push({
3971
4637
  version: env._v,
@@ -3994,7 +4660,7 @@ var Collection = class {
3994
4660
  version
3995
4661
  );
3996
4662
  if (!envelope) return null;
3997
- return this.codec.decryptRecord(envelope, { skipValidation: true });
4663
+ return this.codec.decryptRecord(envelope, { skipValidation: true, id });
3998
4664
  }
3999
4665
  /** Revert a record to a past version. Creates a new version with the old content. */
4000
4666
  async revert(id, version) {
@@ -4102,7 +4768,7 @@ var Collection = class {
4102
4768
  const id = ids[i];
4103
4769
  const envelope = await this.adapter.get(this.vault, this.name, id);
4104
4770
  if (envelope) {
4105
- const record = await this.codec.decryptRecord(envelope, { sealedAsHandles: true });
4771
+ const record = await this.codec.decryptRecord(envelope, { id, sealedAsHandles: true });
4106
4772
  if (record === null) continue;
4107
4773
  items.push(record);
4108
4774
  if (!this.lazy && !this.cache.has(id)) {
@@ -4176,14 +4842,14 @@ var Collection = class {
4176
4842
  [],
4177
4843
  [],
4178
4844
  joinContext,
4179
- this.moneyFields
4845
+ this.via
4180
4846
  );
4181
4847
  }
4182
4848
  /** Decrypt a page of envelopes returned by `adapter.listPage`. */
4183
4849
  async decryptPage(items) {
4184
4850
  const out = [];
4185
4851
  for (const { id, envelope } of items) {
4186
- const record = await this.codec.decryptRecord(envelope, { sealedAsHandles: true });
4852
+ const record = await this.codec.decryptRecord(envelope, { id, sealedAsHandles: true });
4187
4853
  if (record === null) continue;
4188
4854
  out.push({ id, record, version: envelope._v });
4189
4855
  }
@@ -4243,7 +4909,7 @@ var Collection = class {
4243
4909
  }
4244
4910
  return;
4245
4911
  }
4246
- const record = await this.codec.decryptRecord(envelope, { sealedAsHandles: true });
4912
+ const record = await this.codec.decryptRecord(envelope, { id, sealedAsHandles: true });
4247
4913
  if (record === null) {
4248
4914
  this.cache.delete(id);
4249
4915
  if (previous) {
@@ -4263,9 +4929,63 @@ var Collection = class {
4263
4929
  * Never writes to the store and never fires write hooks, so it cannot loop.
4264
4930
  */
4265
4931
  async _applyRemoteChange(id, action) {
4266
- await this._invalidateCacheEntry(id);
4267
- this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
4268
- this.searchIndexStore?.markDirty();
4932
+ await this._onRecordMutated(id, action, "tab-mirror");
4933
+ }
4934
+ /**
4935
+ * Origin-tagged mutation choke point (#623 task 10) — the single dispatch
4936
+ * point every put/delete path funnels through, AFTER its own store write,
4937
+ * to fire the side-effects that path performs today. Each case below
4938
+ * performs EXACTLY the side-effect set the seam-map Part-3 table
4939
+ * (`.superpowers/sdd/seam-map-i18n-pipeline.md`) records for `origin` —
4940
+ * this is a pure parity extraction of existing tails, not a behavior
4941
+ * change. Phase C (#621, #622) plugs the dependency-graph dispatch into
4942
+ * this socket, keyed off `origin`, without every call site having to
4943
+ * learn the graph.
4944
+ *
4945
+ * `restore` is never reached: `Vault.load` / `backup.ts#loadVault` drops
4946
+ * the whole `collectionCache` instead of dispatching per record — the
4947
+ * origin is reserved for phase C.
4948
+ *
4949
+ * @internal
4950
+ */
4951
+ async _onRecordMutated(id, action, origin, ctx) {
4952
+ switch (origin) {
4953
+ case "local-write": {
4954
+ const record = ctx.record;
4955
+ const version = ctx.version;
4956
+ await this.onDirty?.(this.name, id, "put", version);
4957
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "put" });
4958
+ this.searchIndexStore?.markDirty();
4959
+ await this.onAccess?.("put", id);
4960
+ await this.dispatchDerivations(id, record, version);
4961
+ await this.dispatchMaterializedViews(id, record);
4962
+ return;
4963
+ }
4964
+ case "local-delete": {
4965
+ await this.onDirty?.(this.name, id, "put", ctx.version);
4966
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action: "delete" });
4967
+ this.searchIndexStore?.markDirty();
4968
+ await this.onAccess?.("delete", id);
4969
+ return;
4970
+ }
4971
+ case "tab-mirror":
4972
+ await this._invalidateCacheEntry(id);
4973
+ this.emitter.emit("change", { vault: this.vault, collection: this.name, id, action });
4974
+ this.searchIndexStore?.markDirty();
4975
+ return;
4976
+ case "sync-apply":
4977
+ this._invalidateCekCacheEntry(id);
4978
+ await this._invalidateCacheEntry(id);
4979
+ this.graphDispatch?.collect(this.name, id);
4980
+ return;
4981
+ case "cutover":
4982
+ await this._invalidateCacheEntry(id);
4983
+ this.graphDispatch?.collect(this.name, id);
4984
+ return;
4985
+ case "restore":
4986
+ this.graphDispatch?.collect(this.name, id);
4987
+ return;
4988
+ }
4269
4989
  }
4270
4990
  /** @internal — the current in-memory record without a store read (for conflict capture). */
4271
4991
  _peekCached(id) {
@@ -4423,7 +5143,7 @@ var Collection = class {
4423
5143
  for (const [id, entry] of this.cache) {
4424
5144
  const cek = this.perRecordCek ? await this.resolveRecordCek(id) : void 0;
4425
5145
  const prevForVdig = this.vdigFields !== null ? await this.adapter.get(this.vault, this.name, id) : null;
4426
- result[id] = await this.codec.encryptRecord(entry.record, entry.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0);
5146
+ result[id] = await this.codec.encryptRecord(entry.record, entry.version, cek, void 0, void 0, this.vdigFields !== null ? { id, prev: prevForVdig } : void 0, id);
4427
5147
  }
4428
5148
  return result;
4429
5149
  }
@@ -4435,93 +5155,22 @@ var Collection = class {
4435
5155
  * `this.defaultLocale`.
4436
5156
  *
4437
5157
  * - i18nText fields: replaced with the resolved string (or the full
4438
- * map when `locale === 'raw'`).
4439
- * - dictKey fields: `<field>Label` virtual fields added.
4440
- *
4441
- * Returns the record unchanged when no locale is active and no i18n/dict
4442
- * fields are registered.
4443
- */
4444
- async applyLocaleToRecord(record, localeOpts) {
4445
- const hasI18n = this.i18nFields && Object.keys(this.i18nFields).length > 0;
4446
- const hasDict = this.dictKeyFields && Object.keys(this.dictKeyFields).length > 0;
4447
- const hasMoney = this.moneyFields && Object.keys(this.moneyFields).length > 0;
4448
- if (!hasI18n && !hasDict && !hasMoney) return record;
4449
- const locale = localeOpts?.locale ?? this.defaultLocale;
4450
- let result = record;
4451
- if (hasMoney && this.moneyFields) {
4452
- result = moneyRuntime().decodeMoneyFields(result, this.moneyFields, typeof locale === "string" ? locale : void 0);
4453
- }
4454
- const hasStaticDisplay = hasDict && this.dictKeyFields !== void 0 && Object.values(this.dictKeyFields).some(
4455
- (d) => isStaticDictDescriptor(d) && d.displayLocale !== void 0
4456
- );
4457
- if (!locale && !hasStaticDisplay) return stripI18nFilled(result);
4458
- const layer = localeOpts?._layer ?? "read";
4459
- if (locale && hasI18n && this.i18nFields) {
4460
- result = this.i18nStrategy.applyI18nLocale(result, this.i18nFields, locale, localeOpts?.fallback, layer);
4461
- }
4462
- if (hasDict && this.dictKeyFields && this.dictLabelResolver && locale !== "raw") {
4463
- const withLabels = { ...result };
4464
- const resolver = this.dictLabelResolver;
4465
- for (const [field, desc] of Object.entries(this.dictKeyFields)) {
4466
- const policy = desc.onMissing ? resolvePolicy(desc.onMissing, layer) : "null";
4467
- const fallback = policy === "substitute" ? localeOpts?.fallback ?? desc.substitute : localeOpts?.fallback;
4468
- const effLocale = locale ?? (isStaticDictDescriptor(desc) ? desc.displayLocale : void 0);
4469
- const resolveKey = async (key) => {
4470
- if (!effLocale) {
4471
- if (policy === "throw") {
4472
- throw new LocaleNotSpecifiedError(
4473
- field,
4474
- `dictKey "${field}": no locale active to resolve key "${key}".`
4475
- );
4476
- }
4477
- return null;
4478
- }
4479
- const label = await resolver(desc.name, key, effLocale, fallback);
4480
- if (label === void 0) {
4481
- if (policy === "throw") {
4482
- throw new LocaleNotSpecifiedError(
4483
- field,
4484
- `dictKey "${field}": no label for key "${key}" in locale "${effLocale}".`
4485
- );
4486
- }
4487
- return null;
4488
- }
4489
- return label;
4490
- };
4491
- if (field.includes("[].")) {
4492
- const parts = field.split("[].");
4493
- const arrayKey = parts[0];
4494
- const leaf = parts[1];
4495
- if (!leaf || leaf.includes(".")) continue;
4496
- const arr = withLabels[arrayKey];
4497
- if (!Array.isArray(arr)) continue;
4498
- const labelKey = `${leaf}Label`;
4499
- withLabels[arrayKey] = await Promise.all(
4500
- arr.map(async (el) => {
4501
- if (!el || typeof el !== "object" || Array.isArray(el)) return el;
4502
- const k = el[leaf];
4503
- if (typeof k !== "string") return el;
4504
- return { ...el, [labelKey]: await resolveKey(k) };
4505
- })
4506
- );
4507
- continue;
4508
- }
4509
- const val = result[field];
4510
- if (Array.isArray(val)) {
4511
- withLabels[`${field}Label`] = await Promise.all(
4512
- val.map(async (k) => ({
4513
- key: k,
4514
- label: typeof k === "string" ? await resolveKey(k) : null
4515
- }))
4516
- );
4517
- } else if (typeof val === "string") {
4518
- const label = await resolveKey(val);
4519
- if (label !== null) withLabels[`${field}Label`] = label;
4520
- }
4521
- }
4522
- result = withLabels;
4523
- }
4524
- return stripI18nFilled(result);
5158
+ * map when `locale === 'raw'`).
5159
+ * - dictKey fields: `<field>Label` virtual fields added.
5160
+ *
5161
+ * Returns the record unchanged when no Via pipeline is compiled (#638 Task 7:
5162
+ * was money/i18n/dictKey-flag-gated — missed any OTHER binding's `present`
5163
+ * hook, e.g. the `computed` via-binding's virtual-mode read-time compute;
5164
+ * `this.via` truthiness is the general condition every binding's presence
5165
+ * already implies).
5166
+ */
5167
+ async applyLocaleToRecord(record, localeOpts) {
5168
+ if (!this.via) return record;
5169
+ const locale = localeOpts?.locale ?? this.defaultLocale;
5170
+ const layer = localeOpts?._layer ?? "read";
5171
+ let result = record;
5172
+ result = await this.via.present(result, { locale, ...localeOpts?.fallback !== void 0 ? { fallback: localeOpts.fallback } : {}, layer });
5173
+ return result;
4525
5174
  }
4526
5175
  /**
4527
5176
  * Low-level: encrypt a pre-serialised JSON string into an EncryptedEnvelope.
@@ -4738,14 +5387,14 @@ var Collection = class {
4738
5387
  /** C-A/R10: persist the classified marker once per classified handle (store I/O in config-drift.ts). */
4739
5388
  async _ensureClassifiedMarker() {
4740
5389
  if (this._markerPersisted || this.vdigFields === null) return;
4741
- const { persistClassifiedMarkerForFields } = await import("./config-drift-LRKCC3VZ.js");
5390
+ const { persistClassifiedMarkerForFields } = await import("./classified-marker-IO5EBW7Q.js");
4742
5391
  await persistClassifiedMarkerForFields(this.adapter, this.vault, this.name, this.vdigFields, await this.getDEK(this.name));
4743
5392
  this._markerPersisted = true;
4744
5393
  }
4745
5394
  /** C-A/R10 drift signal — the marker's declared digest-only set, memoized to one store read per handle (see config-drift.ts). */
4746
5395
  async _classifiedMarkerDigestOnly() {
4747
5396
  if (this._markerDigestOnlyCache !== void 0) return this._markerDigestOnlyCache;
4748
- const { readClassifiedMarkerDigestOnly } = await import("./config-drift-LRKCC3VZ.js");
5397
+ const { readClassifiedMarkerDigestOnly } = await import("./classified-marker-IO5EBW7Q.js");
4749
5398
  this._markerDigestOnlyCache = await readClassifiedMarkerDigestOnly(this.adapter, this.vault, this.name, await this.getDEK(this.name));
4750
5399
  return this._markerDigestOnlyCache;
4751
5400
  }
@@ -4824,6 +5473,10 @@ var Collection = class {
4824
5473
  _classifySealedShred(live) {
4825
5474
  return classifySealedShred(this.tiersContext(), live);
4826
5475
  }
5476
+ async _onViaErase(id, live) {
5477
+ return this.via ? this.via.eraseSealed({ id, vault: this.vault, live, crypto: await this.codec.eraseCryptoCtx(id, live) }) : void 0;
5478
+ }
5479
+ // @internal forget()'s per-ref via-erase fold (#629 T10)
4827
5480
  /**
4828
5481
  * Bind the {@link TiersContext} the tier ops need. The `cekCache` is passed
4829
5482
  * by reference (the SAME `Lru` the kernel's read/write path owns) so an
@@ -5224,7 +5877,7 @@ var VaultPeriods = class {
5224
5877
  ...anchor.priorPeriodName !== void 0 && { priorPeriodName: anchor.priorPeriodName },
5225
5878
  ...options.dateField !== void 0 && { dateField: options.dateField }
5226
5879
  };
5227
- const envelope = await this.writePeriodRecord(record);
5880
+ const envelope = await this.writeReserved(PERIODS_COLLECTION, record.name, record);
5228
5881
  await this.deps.strategy.appendPeriodLedgerEntry(this.deps.getLedgerOrNull(), this.deps.userId(), envelope, record.name);
5229
5882
  existing.push(record);
5230
5883
  this.periodCache = existing;
@@ -5279,20 +5932,208 @@ var VaultPeriods = class {
5279
5932
  priorPeriodName: anchor.priorPeriodName ?? prior.name,
5280
5933
  ...openingCollections.length > 0 && { openingCollections }
5281
5934
  };
5282
- const envelope = await this.writePeriodRecord(record);
5935
+ const envelope = await this.writeReserved(PERIODS_COLLECTION, record.name, record);
5283
5936
  await this.deps.strategy.appendPeriodLedgerEntry(this.deps.getLedgerOrNull(), this.deps.userId(), envelope, record.name);
5284
5937
  existing.push(record);
5285
5938
  this.periodCache = existing;
5286
5939
  return record;
5287
5940
  }
5288
- /** Return every closed / opened period in `closedAt` order. */
5941
+ /**
5942
+ * Freeze a closed period: physically purges delete markers whose `_ts`
5943
+ * falls inside the period's window (#604) and records the fact in a
5944
+ * companion `_period_freezes/<name>` record. NEVER mutates the
5945
+ * hash-chained `_periods/<name>` record's stored bytes — `frozenAt` /
5946
+ * `frozenBy` / `purgedMarkerCount` are merged into `PeriodRecord`s on
5947
+ * read only. Idempotent: a second call is a no-op that returns the
5948
+ * same merged record without re-purging or re-appending a ledger entry.
5949
+ */
5950
+ async freezePeriod(name) {
5951
+ const existing = await this.loadPeriodsCache();
5952
+ const period = existing.find((p) => p.name === name);
5953
+ if (!period) throw new ValidationError(`freezePeriod: no period named "${name}".`);
5954
+ if (period.kind !== "closed") {
5955
+ throw new ValidationError(
5956
+ `freezePeriod: period "${name}" is "${period.kind}"; only a closed period can be frozen.`
5957
+ );
5958
+ }
5959
+ const before = periodExclusiveUpperBound(period.endDate);
5960
+ if (Date.parse(before) > Date.now()) {
5961
+ throw new ValidationError(
5962
+ `freezePeriod: period "${name}" purge window ends at ${before}, in the future; only a period whose window is fully in the past can be frozen (recent delete markers may not have converged).`
5963
+ );
5964
+ }
5965
+ const prior = await this.readReserved(PERIOD_FREEZES_COLLECTION, name);
5966
+ if (prior) return this.mergeFreeze(period, prior);
5967
+ const purgedMarkerCount = await this.deps.purgeDeleteMarkers(before);
5968
+ const freeze = {
5969
+ period: name,
5970
+ frozenAt: (/* @__PURE__ */ new Date()).toISOString(),
5971
+ frozenBy: this.deps.userId(),
5972
+ purgedMarkerCount
5973
+ };
5974
+ const envelope = await this.writeReserved(PERIOD_FREEZES_COLLECTION, name, freeze);
5975
+ await this.deps.strategy.appendPeriodLedgerEntry(
5976
+ this.deps.getLedgerOrNull(),
5977
+ this.deps.userId(),
5978
+ envelope,
5979
+ name,
5980
+ PERIOD_FREEZES_COLLECTION
5981
+ );
5982
+ return this.mergeFreeze(period, freeze);
5983
+ }
5984
+ /**
5985
+ * Archive a closed period (#613): physically relocates its in-window
5986
+ * records (those with `_ts < periodExclusiveUpperBound(endDate)`) from the
5987
+ * hot store to the configured cold tier via `archiveRecords`, and records
5988
+ * the fact in a companion `_period_archives/<name>` record. NEVER mutates
5989
+ * the hash-chained `_periods/<name>` record. Non-destructive (reads fall
5990
+ * through to cold) and idempotent: a second call is a no-op returning the
5991
+ * same merged record.
5992
+ */
5993
+ async archivePeriod(name) {
5994
+ const existing = await this.loadPeriodsCache();
5995
+ const period = existing.find((p) => p.name === name);
5996
+ if (!period) throw new ValidationError(`archivePeriod: no period named "${name}".`);
5997
+ if (period.kind !== "closed") {
5998
+ throw new ValidationError(
5999
+ `archivePeriod: period "${name}" is "${period.kind}"; only a closed period can be archived.`
6000
+ );
6001
+ }
6002
+ const prior = await this.readReserved(PERIOD_ARCHIVES_COLLECTION, name);
6003
+ if (prior) return this.mergeArchive(period, prior);
6004
+ const before = periodExclusiveUpperBound(period.endDate);
6005
+ const archivedRecordCount = await this.deps.archiveRecords(before);
6006
+ const archive = {
6007
+ period: name,
6008
+ archivedAt: (/* @__PURE__ */ new Date()).toISOString(),
6009
+ archivedBy: this.deps.userId(),
6010
+ archivedRecordCount
6011
+ };
6012
+ const envelope = await this.writeReserved(PERIOD_ARCHIVES_COLLECTION, name, archive);
6013
+ await this.deps.strategy.appendPeriodLedgerEntry(
6014
+ this.deps.getLedgerOrNull(),
6015
+ this.deps.userId(),
6016
+ envelope,
6017
+ name,
6018
+ PERIOD_ARCHIVES_COLLECTION
6019
+ );
6020
+ return this.mergeArchive(period, archive);
6021
+ }
6022
+ /**
6023
+ * Target-purge a closed period (#615): sweeps delete markers off the vault's
6024
+ * PUSH-ONLY sync targets (`backup`/`archive`) via `purgeTargets`, recording a
6025
+ * companion `_period_target_purges/<name>` record. `sync-peer` targets are
6026
+ * skipped (resurrection risk). NEVER mutates the chained `_periods/<name>`
6027
+ * record. Requires the period be frozen first (closed → frozen → target-purged).
6028
+ * Idempotent once run; with no push-only targets it writes no companion and
6029
+ * is re-runnable.
6030
+ */
6031
+ async purgePeriodTargets(name) {
6032
+ const existing = await this.loadPeriodsCache();
6033
+ const period = existing.find((p) => p.name === name);
6034
+ if (!period) throw new ValidationError(`purgePeriodTargets: no period named "${name}".`);
6035
+ if (period.kind !== "closed") {
6036
+ throw new ValidationError(
6037
+ `purgePeriodTargets: period "${name}" is "${period.kind}"; only a closed period can be target-purged.`
6038
+ );
6039
+ }
6040
+ const frozen = await this.readReserved(PERIOD_FREEZES_COLLECTION, name);
6041
+ if (!frozen) {
6042
+ throw new ValidationError(
6043
+ `purgePeriodTargets: period "${name}" must be frozen first (closed \u2192 frozen \u2192 target-purged).`
6044
+ );
6045
+ }
6046
+ const prior = await this.readReserved(PERIOD_TARGET_PURGES_COLLECTION, name);
6047
+ if (prior) return this.mergeTargetPurge(period, prior);
6048
+ const before = periodExclusiveUpperBound(period.endDate);
6049
+ const targets = await this.deps.purgeTargets(before);
6050
+ if (targets.length === 0) return period;
6051
+ const record = {
6052
+ period: name,
6053
+ purgedAt: (/* @__PURE__ */ new Date()).toISOString(),
6054
+ purgedBy: this.deps.userId(),
6055
+ targets
6056
+ };
6057
+ const envelope = await this.writeReserved(PERIOD_TARGET_PURGES_COLLECTION, name, record);
6058
+ await this.deps.strategy.appendPeriodLedgerEntry(
6059
+ this.deps.getLedgerOrNull(),
6060
+ this.deps.userId(),
6061
+ envelope,
6062
+ name,
6063
+ PERIOD_TARGET_PURGES_COLLECTION
6064
+ );
6065
+ return this.mergeTargetPurge(period, record);
6066
+ }
6067
+ /** Merge target-purge companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
6068
+ mergeTargetPurge(period, record) {
6069
+ return {
6070
+ ...period,
6071
+ targetsPurgedAt: record.purgedAt,
6072
+ targetsPurgedBy: record.purgedBy,
6073
+ targetsPurged: record.targets
6074
+ };
6075
+ }
6076
+ /** Merge archive companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
6077
+ mergeArchive(period, archive) {
6078
+ return {
6079
+ ...period,
6080
+ archivedAt: archive.archivedAt,
6081
+ archivedBy: archive.archivedBy,
6082
+ archivedRecordCount: archive.archivedRecordCount
6083
+ };
6084
+ }
6085
+ /** Merge freeze companion fields into a fresh `PeriodRecord` copy — never mutates `periodCache`. */
6086
+ mergeFreeze(period, freeze) {
6087
+ return {
6088
+ ...period,
6089
+ frozenAt: freeze.frozenAt,
6090
+ frozenBy: freeze.frozenBy,
6091
+ purgedMarkerCount: freeze.purgedMarkerCount
6092
+ };
6093
+ }
6094
+ /** Return every closed / opened period in `closedAt` order, merged with any freeze + archive companions. */
5289
6095
  async listPeriods() {
5290
- return [...await this.loadPeriodsCache()];
6096
+ const all = await this.loadPeriodsCache();
6097
+ const freezeIds = await this.deps.adapter.list(this.deps.vault, PERIOD_FREEZES_COLLECTION);
6098
+ const freezes = /* @__PURE__ */ new Map();
6099
+ for (const id of freezeIds) {
6100
+ const f = await this.readReserved(PERIOD_FREEZES_COLLECTION, id);
6101
+ if (f) freezes.set(f.period, f);
6102
+ }
6103
+ const archiveIds = await this.deps.adapter.list(this.deps.vault, PERIOD_ARCHIVES_COLLECTION);
6104
+ const archives = /* @__PURE__ */ new Map();
6105
+ for (const id of archiveIds) {
6106
+ const a = await this.readReserved(PERIOD_ARCHIVES_COLLECTION, id);
6107
+ if (a) archives.set(a.period, a);
6108
+ }
6109
+ const targetPurgeIds = await this.deps.adapter.list(this.deps.vault, PERIOD_TARGET_PURGES_COLLECTION);
6110
+ const targetPurges = /* @__PURE__ */ new Map();
6111
+ for (const id of targetPurgeIds) {
6112
+ const tp = await this.readReserved(PERIOD_TARGET_PURGES_COLLECTION, id);
6113
+ if (tp) targetPurges.set(tp.period, tp);
6114
+ }
6115
+ return all.map((p) => {
6116
+ const f = freezes.get(p.name);
6117
+ let merged = f ? this.mergeFreeze(p, f) : p;
6118
+ const a = archives.get(p.name);
6119
+ if (a) merged = this.mergeArchive(merged, a);
6120
+ const tp = targetPurges.get(p.name);
6121
+ if (tp) merged = this.mergeTargetPurge(merged, tp);
6122
+ return merged;
6123
+ });
5291
6124
  }
5292
- /** Look up a single period by name. Returns `null` if not found. */
6125
+ /** Look up a single period by name, merged with its freeze + archive + target-purge companions if any. Returns `null` if not found. */
5293
6126
  async getPeriod(name) {
5294
6127
  const all = await this.loadPeriodsCache();
5295
- return all.find((p) => p.name === name) ?? null;
6128
+ const period = all.find((p) => p.name === name);
6129
+ if (!period) return null;
6130
+ const freeze = await this.readReserved(PERIOD_FREEZES_COLLECTION, name);
6131
+ const archive = await this.readReserved(PERIOD_ARCHIVES_COLLECTION, name);
6132
+ let merged = freeze ? this.mergeFreeze(period, freeze) : period;
6133
+ if (archive) merged = this.mergeArchive(merged, archive);
6134
+ const targetPurge = await this.readReserved(PERIOD_TARGET_PURGES_COLLECTION, name);
6135
+ if (targetPurge) merged = this.mergeTargetPurge(merged, targetPurge);
6136
+ return merged;
5296
6137
  }
5297
6138
  /** Called by the gate bus before put/delete. */
5298
6139
  async assertTsWritable(existing, incoming) {
@@ -5317,11 +6158,12 @@ var VaultPeriods = class {
5317
6158
  this.periodCache = loaded;
5318
6159
  return loaded;
5319
6160
  }
5320
- async writePeriodRecord(record) {
5321
- const json = JSON.stringify(record);
6161
+ /** Generic reserved-collection writer — serves `_periods` and `_period_freezes` alike. */
6162
+ async writeReserved(collection, key, value) {
6163
+ const json = JSON.stringify(value);
5322
6164
  let envelope;
5323
6165
  if (this.deps.encrypted) {
5324
- const dek = await this.deps.getDEK(PERIODS_COLLECTION);
6166
+ const dek = await this.deps.getDEK(collection);
5325
6167
  const { iv, data } = await encrypt(json, dek);
5326
6168
  envelope = {
5327
6169
  _noydb: NOYDB_FORMAT_VERSION,
@@ -5341,9 +6183,16 @@ var VaultPeriods = class {
5341
6183
  _by: this.deps.userId()
5342
6184
  };
5343
6185
  }
5344
- await this.deps.adapter.put(this.deps.vault, PERIODS_COLLECTION, record.name, envelope);
6186
+ await this.deps.adapter.put(this.deps.vault, collection, key, envelope);
5345
6187
  return envelope;
5346
6188
  }
6189
+ /** Generic reserved-collection reader — serves `_period_freezes` companion reads. */
6190
+ async readReserved(collection, key) {
6191
+ const env = await this.deps.adapter.get(this.deps.vault, collection, key);
6192
+ if (!env) return null;
6193
+ const json = this.deps.encrypted ? await openEnvelopeJson(env, await this.deps.getDEK(collection)) : env._data;
6194
+ return JSON.parse(json);
6195
+ }
5347
6196
  async decryptPeriodRecord(envelope) {
5348
6197
  let json;
5349
6198
  if (this.deps.encrypted) {
@@ -5577,11 +6426,12 @@ var VaultLinks = class {
5577
6426
  * for the same (collection, id) returns immediately so two
5578
6427
  * mutually-cascading collections don't recurse forever.
5579
6428
  */
5580
- async enforceRefsOnDelete(collectionName, id) {
6429
+ async enforceRefsOnDelete(graph, collectionName, id) {
5581
6430
  const key = `${collectionName}/${id}`;
5582
6431
  if (this.cascadeInProgress.has(key)) return;
5583
6432
  this.cascadeInProgress.add(key);
5584
6433
  try {
6434
+ await this.enforceLookupRefsOnDelete(graph, collectionName, collectionName, id);
5585
6435
  const inbound = this.deps.refRegistry.getInbound(collectionName);
5586
6436
  for (const rule of inbound) {
5587
6437
  const fromCollection = this.deps.collection(rule.collection);
@@ -5636,6 +6486,109 @@ var VaultLinks = class {
5636
6486
  this.cascadeInProgress.delete(key);
5637
6487
  }
5638
6488
  }
6489
+ /**
6490
+ * Find the records in `collection` whose `field` currently equals `key` — the SAME
6491
+ * list()+filter machinery `enforceRefsOnDelete`'s classic ref-restrict/cascade loop above
6492
+ * already uses (bounded by the referencing collection's size, never by the whole vault or by
6493
+ * "which collections reference this" — that part is the graph's O(1) `_out` reverse index).
6494
+ */
6495
+ async findLookupReferencingRecords(collection, field, key) {
6496
+ const all = await collection.list();
6497
+ return all.filter((rec) => String(rec[field]) === key);
6498
+ }
6499
+ /**
6500
+ * Resolve the VALUE a referencing field actually stores for the row being deleted (`rawKey`,
6501
+ * the backing row's PUT-id) under a given edge's `keyField` — a referencing field always stores
6502
+ * `row[keyField]`, never the PUT-id when the two differ (matrix tier's `lookup(dim, {key})`;
6503
+ * mirrors Task 3's `checkLookupMembership` review fix, Important 1). `keyField === 'id'`
6504
+ * (reserved/static tiers, and the matrix default) needs no row read: `rawKey` IS the value.
6505
+ * Reads the backing row AT MOST ONCE per distinct non-`'id'` `keyField`, even across multiple
6506
+ * edges — `backingCollection` is only `.get()`-able for the matrix tier, which is exactly the
6507
+ * only tier that can ever have `keyField !== 'id'`.
6508
+ */
6509
+ async resolveLookupCompareKey(backingCollection, rawKey, keyField, cache) {
6510
+ if (keyField === "id") return rawKey;
6511
+ if (cache.has(keyField)) return cache.get(keyField);
6512
+ const row = await this.deps.collection(backingCollection).get(rawKey);
6513
+ const raw = row?.[keyField];
6514
+ const resolved = typeof raw === "string" || typeof raw === "number" ? String(raw) : void 0;
6515
+ cache.set(keyField, resolved);
6516
+ return resolved;
6517
+ }
6518
+ /**
6519
+ * Phase 1 (#650 Task 5, #648) — restrict check, never mutates. Throws
6520
+ * `DictKeyInUseError(dimension, key, usedBy, count)` naming the FIRST referencing collection
6521
+ * still holding a matching record, for every `onDelete:'restrict'` edge the graph's O(1)
6522
+ * `referencingEdgesOf(backingCollection)` reverse lookup finds pointing at this dimension. A
6523
+ * dimension with no declared lookup-referencing edges is a no-op — undeclared refs keep
6524
+ * dangling (today's behavior, unaffected). Called by `Vault.forget()` BEFORE any shred (the
6525
+ * spec §4 "restrict refuses before any shred" design decision) — cascade/nullify propagation
6526
+ * for the SAME edges happens separately, after the shred, via `forgetDerivedFanout`.
6527
+ *
6528
+ * Also resolves (and RETURNS) the live compare-key for EVERY referencing edge, restrict AND
6529
+ * cascade/nullify alike (#650 Task 5 review, Important fix): `forgetDerivedFanout`'s cascade/
6530
+ * nullify propagation runs AFTER `_writeTombstone`, when a non-`'id'` `keyField`'s value can no
6531
+ * longer be read off the (now-shredded) row — resolving it HERE, at the one point every
6532
+ * referencing edge's backing row is still guaranteed live, eliminates that post-shred
6533
+ * dependency. A `keyField` this can't resolve (row unreadable) maps to `undefined` in the
6534
+ * returned map so the caller can report it as residue instead of silently skipping the edge.
6535
+ */
6536
+ async checkLookupRefsRestrict(graph, dimension, backingCollection, key) {
6537
+ const keyCache = /* @__PURE__ */ new Map();
6538
+ for (const { referencing, onDelete, keyField } of graph.referencingEdgesOf(backingCollection)) {
6539
+ const compareKey = await this.resolveLookupCompareKey(backingCollection, key, keyField, keyCache);
6540
+ if (onDelete !== "restrict" || compareKey === void 0) continue;
6541
+ const coll = this.deps.collection(referencing.collection);
6542
+ const matches = await this.findLookupReferencingRecords(coll, referencing.field, compareKey);
6543
+ if (matches.length > 0) {
6544
+ throw new DictKeyInUseError(dimension, key, referencing.collection, matches.length);
6545
+ }
6546
+ }
6547
+ return keyCache;
6548
+ }
6549
+ /**
6550
+ * Phase 2 (#650 Task 5, #648) — apply `cascade`/`nullify` propagation for this dimension's
6551
+ * non-restrict referencing edges (restrict edges are skipped — phase 1 above already refused
6552
+ * or the caller already checked). `cascade` deletes each matching record through its ordinary
6553
+ * `collection.delete()` (an origin-tagged, fanout-visible delete — participates in sync/history/
6554
+ * the graph dispatch wave exactly like any other delete); `nullify` clears the referencing field
6555
+ * via an ordinary `collection.put()`. Returns the counts so callers (forget's `ForgetFanoutStats`)
6556
+ * can report the propagation additively.
6557
+ */
6558
+ async applyLookupRefsPropagation(graph, backingCollection, key) {
6559
+ let cascaded = 0;
6560
+ let nullified = 0;
6561
+ const keyCache = /* @__PURE__ */ new Map();
6562
+ for (const { referencing, onDelete, keyField } of graph.referencingEdgesOf(backingCollection)) {
6563
+ if (onDelete === "restrict") continue;
6564
+ const compareKey = await this.resolveLookupCompareKey(backingCollection, key, keyField, keyCache);
6565
+ if (compareKey === void 0) continue;
6566
+ const coll = this.deps.collection(referencing.collection);
6567
+ const matches = await this.findLookupReferencingRecords(coll, referencing.field, compareKey);
6568
+ for (const rec of matches) {
6569
+ const id = rec["id"];
6570
+ if (id === void 0) continue;
6571
+ if (onDelete === "cascade") {
6572
+ await coll.delete(id);
6573
+ cascaded++;
6574
+ } else if (onDelete === "nullify") {
6575
+ await coll.put(id, { ...rec, [referencing.field]: null });
6576
+ nullified++;
6577
+ }
6578
+ }
6579
+ }
6580
+ return { cascaded, nullified };
6581
+ }
6582
+ /**
6583
+ * Convenience: the full restrict-then-propagate sequence for an ORDINARY (non-forget) delete of
6584
+ * a lookup-referenced row — `checkLookupRefsRestrict` then `applyLookupRefsPropagation`. Used by
6585
+ * `enforceRefsOnDelete` above (matrix-tier: the backing row IS an ordinary `Collection` record)
6586
+ * and by `LookupHandle.delete()`'s injected `checkReferencesOnDelete` callback (reserved tier).
6587
+ */
6588
+ async enforceLookupRefsOnDelete(graph, dimension, backingCollection, key) {
6589
+ await this.checkLookupRefsRestrict(graph, dimension, backingCollection, key);
6590
+ return this.applyLookupRefsPropagation(graph, backingCollection, key);
6591
+ }
5639
6592
  /**
5640
6593
  * @internal — apply link `onDelete` policy when an endpoint record is
5641
6594
  * deleted. `'strict'` throws (blocks the delete), `'cascade'`
@@ -5792,7 +6745,7 @@ var VaultLinks = class {
5792
6745
  // src/with-shape/links/lazy-handle.ts
5793
6746
  function makeLazyLinkSetHandle(d) {
5794
6747
  let engine = null;
5795
- const load = () => engine ??= import("./link-set-62Y36TOG.js").then(
6748
+ const load = () => engine ??= import("./link-set-YBAC2DQT.js").then(
5796
6749
  ({ LinkSet }) => new LinkSet(d.adapter, d.vault, d.name, d.spec, d.encrypted, d.getDEK, d.actor, d.emitter, d.endpointExists)
5797
6750
  );
5798
6751
  return {
@@ -5806,6 +6759,166 @@ function makeLazyLinkSetHandle(d) {
5806
6759
  };
5807
6760
  }
5808
6761
 
6762
+ // src/kernel/via-taint-binding.ts
6763
+ var EMPTY_STRING_SET = /* @__PURE__ */ new Set();
6764
+ function isDefaultPosture(p) {
6765
+ return p.encryptedAtRest === DEFAULT_POSTURE.encryptedAtRest && p.queryable === DEFAULT_POSTURE.queryable && p.exportable === DEFAULT_POSTURE.exportable && p.forgettable === DEFAULT_POSTURE.forgettable;
6766
+ }
6767
+ function buildTaintOverlay(rawPostures, sealFields) {
6768
+ let out;
6769
+ for (const [field, posture] of rawPostures) {
6770
+ if (isDefaultPosture(posture)) continue;
6771
+ out ??= /* @__PURE__ */ new Map();
6772
+ out.set(field, sealFields.has(field) ? { ...posture, queryable: "none" } : posture);
6773
+ }
6774
+ return out;
6775
+ }
6776
+ async function encodeTaintAtRest(record, crypto, fields) {
6777
+ let open = record;
6778
+ let sealed;
6779
+ for (const field of fields) {
6780
+ if (!(field in record)) continue;
6781
+ const value = record[field];
6782
+ if (value === void 0) continue;
6783
+ const ref2 = await crypto.sealedSlots.seal(field, value);
6784
+ if (open === record) open = { ...record };
6785
+ delete open[field];
6786
+ sealed ??= {};
6787
+ sealed[field] = ref2;
6788
+ }
6789
+ return sealed ? { record: open, sealed } : { record };
6790
+ }
6791
+ async function decodeTaintAtRest(record, sealed, crypto, opts, fields) {
6792
+ let out = record;
6793
+ for (const field of fields) {
6794
+ const ref2 = sealed[field];
6795
+ if (ref2 === void 0) continue;
6796
+ if (out === record) out = { ...record };
6797
+ out[field] = opts.asHandles ? new SealedHandle(() => crypto.sealedSlots.unseal(field, ref2)) : await crypto.sealedSlots.unseal(field, ref2);
6798
+ }
6799
+ return out;
6800
+ }
6801
+ function taintBinding(sealFields, presentRedactFields = EMPTY_STRING_SET) {
6802
+ const fields = [...sealFields];
6803
+ const redactFields = [...presentRedactFields];
6804
+ return {
6805
+ brand: "taint",
6806
+ posture: { encryptedAtRest: "sealed", queryable: "none", exportable: false, forgettable: true },
6807
+ covers: (field) => sealFields.has(field) || presentRedactFields.has(field),
6808
+ ...fields.length > 0 ? {
6809
+ encodeAtRest: (record, crypto) => encodeTaintAtRest(record, crypto, fields),
6810
+ decodeAtRest: (record, sealed, crypto, opts) => decodeTaintAtRest(record, sealed, crypto, opts, fields)
6811
+ } : {},
6812
+ ...redactFields.length > 0 ? {
6813
+ present: (record) => {
6814
+ let out = record;
6815
+ for (const field of redactFields) {
6816
+ if (field in out) {
6817
+ if (out === record) out = { ...record };
6818
+ out[field] = EXPORT_REDACTION_MARKER;
6819
+ }
6820
+ }
6821
+ return out;
6822
+ }
6823
+ } : {}
6824
+ };
6825
+ }
6826
+
6827
+ // src/kernel/via-graph-wiring.ts
6828
+ var CLASSIFIED_POSTURE = { encryptedAtRest: "sealed", queryable: "det-exact", exportable: false, forgettable: true };
6829
+ function registerCollectionGraphSources(graph, name, opts) {
6830
+ const cfg = resolveCollectionConfig(opts);
6831
+ const bindings = cfg.via?.bindings ?? [];
6832
+ const knownFields = /* @__PURE__ */ new Set([
6833
+ ...Object.keys(cfg.moneyFields ?? {}),
6834
+ ...Object.keys(cfg.i18nFields ?? {}),
6835
+ ...Object.keys(cfg.dictKeyFields ?? {}),
6836
+ // #650 Task 2 — native lookup()/enumOf()/dict() fields register their
6837
+ // posture (`lookupBinding().posture`) the same generic way every other
6838
+ // binding does; no `'ref'` EdgeKind / graph edge yet (Task 5).
6839
+ ...Object.keys(cfg.lookupFields ?? {}),
6840
+ ...cfg.classified !== void 0 ? Object.keys(cfg.classified.byField) : []
6841
+ ]);
6842
+ for (const field of knownFields) {
6843
+ for (const binding of bindings) {
6844
+ if (binding.covers?.(field)) {
6845
+ graph.registerField(name, field, binding.posture);
6846
+ break;
6847
+ }
6848
+ }
6849
+ }
6850
+ for (const edge of cfg.computedEdges) graph.registerDerived(edge.target, edge.sources, "computed", edge.grain ?? "record");
6851
+ for (const edge of cfg.viaDepsEdges) graph.registerDerived(edge.target, edge.sources, "computed", edge.grain ?? "record");
6852
+ if (cfg.classified !== void 0) graph.markClassified(name);
6853
+ const depFields = new Set(cfg.computedEdges.map((edge) => edge.target.field));
6854
+ for (const field of cfg.computedFieldNames) {
6855
+ if (!depFields.has(field)) graph.markDepslessComputed(name, field);
6856
+ }
6857
+ }
6858
+ function validateReconcileGraphEdges(graph, name, options) {
6859
+ const resolvedClassified = options.classifiedFields !== void 0 ? resolveClassifiedFields(name, options.classifiedFields) : void 0;
6860
+ if (resolvedClassified !== void 0 && Object.values(resolvedClassified.byField).some((spec) => spec.storage !== "never")) {
6861
+ const riderNames = new Set(Object.keys(resolvedClassified.riderComputed));
6862
+ const leaking = [...graph.depslessComputedFields(name)].filter((field) => !riderNames.has(field));
6863
+ if (leaking.length > 0) {
6864
+ const field = leaking[0];
6865
+ throw new ValidationError(
6866
+ `Collection "${name}": computed field "${field}" has no declared \`deps\` and the collection declares classified fields \u2014 an opaque computed function could silently copy a classified field's plaintext into an ordinary, unredacted field. Declare \`deps\` naming the source fields it reads, e.g. computed: { ${field}: { fn, deps: [...] } }.`
6867
+ );
6868
+ }
6869
+ }
6870
+ const combinedHasClassified = graph.isClassified(name) || resolvedClassified !== void 0;
6871
+ let edges = [];
6872
+ let depslessComputedFields = [];
6873
+ if (options.computed !== void 0) {
6874
+ for (const [field, entry] of Object.entries(options.computed)) {
6875
+ if (computedEntryParts(entry).mode === "virtual") {
6876
+ throw new ValidationError(
6877
+ `Collection "${name}": computed field "${field}" declares mode: 'virtual' on a reconcile call \u2014 virtual computed fields are construction-only (no late-attach reconcile door); declare them in the collection's first vault.collection("${name}", { ... }) call.`
6878
+ );
6879
+ }
6880
+ }
6881
+ const knownFields = collectKnownFieldNames({
6882
+ moneyFields: options.moneyFields,
6883
+ classifiedFields: resolvedClassified?.byField,
6884
+ computed: options.computed
6885
+ });
6886
+ edges = resolveComputedEdges(name, options.computed, combinedHasClassified, knownFields);
6887
+ const depFields = new Set(edges.map((edge) => edge.target.field));
6888
+ depslessComputedFields = Object.keys(options.computed).filter((field) => !depFields.has(field));
6889
+ }
6890
+ return {
6891
+ edges,
6892
+ depslessComputedFields,
6893
+ classifiedFieldNames: resolvedClassified !== void 0 ? Object.keys(resolvedClassified.byField) : []
6894
+ };
6895
+ }
6896
+ function commitReconcileGraphEdges(graph, name, plan) {
6897
+ for (const edge of plan.edges) {
6898
+ if (!graph.hasDerived(edge.target)) graph.registerDerived(edge.target, edge.sources, "computed", edge.grain ?? "record");
6899
+ }
6900
+ for (const field of plan.depslessComputedFields) graph.markDepslessComputed(name, field);
6901
+ for (const field of plan.classifiedFieldNames) graph.registerField(name, field, CLASSIFIED_POSTURE);
6902
+ if (plan.classifiedFieldNames.length > 0) graph.markClassified(name);
6903
+ }
6904
+ function applyTaintOverlay(coll, graph, name) {
6905
+ const postures = buildTaintOverlay(graph.taintedPostures(name), graph.taintSealedFields(name));
6906
+ if (!postures) return;
6907
+ const sealFields = graph.taintSealedFields(name);
6908
+ const provenance = graph.taintProvenance(name);
6909
+ const virtualFields = graph.virtualFields(name);
6910
+ const virtualExportRedact = /* @__PURE__ */ new Set();
6911
+ for (const [field, posture] of postures) {
6912
+ if (posture.exportable === false && virtualFields.has(field)) virtualExportRedact.add(field);
6913
+ }
6914
+ const c = coll;
6915
+ const needsTaintBinding = sealFields.size > 0 || virtualExportRedact.size > 0;
6916
+ const bindings = needsTaintBinding ? [...c.via?.bindings ?? [], taintBinding(sealFields, virtualExportRedact)] : c.via?.bindings ?? [];
6917
+ const taint = { postures, sealFields, provenance };
6918
+ c.via = ViaPipeline.build(bindings, taint);
6919
+ c.codec.setVia(c.via);
6920
+ }
6921
+
5809
6922
  // src/with-shape/satellites/registry.ts
5810
6923
  var SatelliteRegistry = class {
5811
6924
  byBase = /* @__PURE__ */ new Map();
@@ -5916,7 +7029,7 @@ function declareSatellite(ctx, collectionName, options, existingRegistry) {
5916
7029
  ctx.forEachSyncEngine((e) => e.remirrorPairResolvers([spec.base, spec.satellite]));
5917
7030
  const registry = reg;
5918
7031
  const baseSchema = ctx.getBaseSchema(spec.base);
5919
- void import("./post-register-DCJ2SRVG.js").then((m) => m.postRegister(ctx.adapter, ctx.vaultName, spec, ctx.getDEK, baseSchema, registry)).catch(() => {
7032
+ void import("./post-register-MROIUA5O.js").then((m) => m.postRegister(ctx.adapter, ctx.vaultName, spec, ctx.getDEK, baseSchema, registry)).catch(() => {
5920
7033
  });
5921
7034
  return reg;
5922
7035
  }
@@ -6409,50 +7522,25 @@ var VaultAttestation = class {
6409
7522
  };
6410
7523
 
6411
7524
  // src/kernel/vault.ts
6412
- function resolveLabelFromMap(labels, locale, fallback) {
6413
- if (labels[locale] !== void 0) return labels[locale];
6414
- const chain = Array.isArray(fallback) ? fallback : fallback ? [fallback] : [];
6415
- for (const fb of chain) {
6416
- if (fb === "any") {
6417
- const any = Object.values(labels)[0];
6418
- if (any !== void 0) return any;
6419
- } else if (labels[fb] !== void 0) {
6420
- return labels[fb];
6421
- }
6422
- }
6423
- return void 0;
6424
- }
6425
7525
  var Vault = class {
6426
7526
  adapter;
6427
7527
  /** The vault's name as passed to `openVault()`. Stable for the instance lifetime. */
6428
7528
  name;
6429
- /**
6430
- * Backreference to the parent `Noydb`. Lets vault-scoped services
6431
- * (e.g. `as-*` reader `apply()` paths gating on `withTransactions()`)
6432
- * reach the strategy seam without threading `db` through every API.
6433
- *
6434
- * Type-only Noydb import keeps the module graph acyclic at runtime.
6435
- */
7529
+ /** Backreference to the parent `Noydb` — lets vault-scoped services reach the strategy seam
7530
+ * without threading `db` through every API (type-only import keeps the module graph acyclic). */
6436
7531
  noydb;
6437
- /**
6438
- * The active in-memory keyring. NOT readonly because `load()`
6439
- * needs to refresh it after restoring a different keyring file —
6440
- * otherwise the in-memory DEKs (from the pre-load session) and
6441
- * the on-disk wrapped DEKs (from the loaded backup) drift apart
6442
- * and every subsequent decrypt fails with TamperedError.
6443
- */
7532
+ /** The active in-memory keyring. NOT readonly — `load()` refreshes it after restoring a
7533
+ * different keyring file, or in-memory/on-disk DEKs drift apart and decrypt fails with TamperedError. */
6444
7534
  keyring;
6445
7535
  encrypted;
6446
7536
  emitter;
6447
7537
  onDirty;
6448
7538
  onRegisterConflictResolver;
6449
7539
  syncAdapter;
7540
+ getPurgeableTargets;
6450
7541
  historyConfig;
6451
- /**
6452
- * tree-shake seam for the optional blob service. Undefined
6453
- * means "blobs are off for this vault"; every `collection.blob(id)`
6454
- * call throws with a pointer at `@noy-db/hub/blobs`.
6455
- */
7542
+ /** tree-shake seam for the optional blob service; `undefined` means "blobs are off for this
7543
+ * vault" every `collection.blob(id)` call throws with a pointer at `@noy-db/hub/blobs`. */
6456
7544
  blobStrategy;
6457
7545
  objectStore;
6458
7546
  /** Cold-storage archival strategy (the archive target store). */
@@ -6465,11 +7553,8 @@ var Vault = class {
6465
7553
  crdtStrategy;
6466
7554
  tiersStrategy;
6467
7555
  searchStrategy;
6468
- /**
6469
- * Cargo (partition extraction) strategy `NO_CARGO` (throwing) unless
6470
- * `withCargo()` was passed. Public so the `extractPartition` free function
6471
- * (which takes a `Vault`) routes through it.
6472
- */
7556
+ /** Cargo (partition extraction) strategy — `NO_CARGO` (throwing) unless `withCargo()` was passed.
7557
+ * Public so the `extractPartition` free function (which takes a `Vault`) routes through it. */
6473
7558
  cargoStrategy;
6474
7559
  sealedRecordStrategy;
6475
7560
  portabilityStrategy;
@@ -6483,42 +7568,26 @@ var Vault = class {
6483
7568
  i18nStrategy;
6484
7569
  syncStrategy;
6485
7570
  classifiedStrategy;
6486
- /**
6487
- * Per-vault guard registry. `null` until `_initGuards()` runs; stays
6488
- * `null` for vaults that never register any guard strategy. The
6489
- * runtime class is dynamic-imported on demand so consumers that
6490
- * never use guards don't pull `GuardRegistry`/`GuardExecutor` into
6491
- * their bundle.
6492
- */
7571
+ /** Per-vault guard registry; `null` until `_initGuards()` runs (or for vaults that never register
7572
+ * one) — the runtime class is dynamic-imported on demand to keep it out of the floor bundle. */
6493
7573
  guardRegistry = null;
6494
- /**
6495
- * Per-vault derivation registry. Same lazy-load contract as
6496
- * `guardRegistry` `null` until `_initDerivations()` runs with at
6497
- * least one strategy handle.
6498
- */
7574
+ /** Per-vault derivation/MV/overlay registries — same lazy-load contract as
7575
+ * `guardRegistry`: each stays `null` until its `_init*()` runs with at
7576
+ * least one strategy/MV/overlay handle. */
6499
7577
  derivationRegistry = null;
6500
- /**
6501
- * Per-vault materialized-view registry. Same lazy-load
6502
- * contract as `derivationRegistry` — `null` until
6503
- * `_initMaterializedViews()` runs with at least one MV handle.
6504
- */
6505
7578
  materializedViewRegistry = null;
6506
- /**
6507
- * Per-vault overlay registry. Same lazy-load contract as
6508
- * `materializedViewRegistry` — `null` until `_initOverlayedViews()`
6509
- * runs with at least one handle.
6510
- */
6511
7579
  overlayedViewRegistry = null;
6512
- /**
6513
- * Cached read-only facades handed to guard callbacks via `ctx.vault`
6514
- * and to derivation callbacks via `derive(source, ctx)`. Split by
6515
- * resolution layer: the guard facade reads at `layer:'guard'`,
6516
- * the derivation facade at `layer:'derivation'`, so i18nText / dictKey
6517
- * fields resolve under that layer's `onMissing` policy. Allocated
6518
- * eagerly inside `_initGuards()` / `_initDerivations()` so read
6519
- * accessors stay synchronous (callers in `tx/transaction.ts` rely on
6520
- * that). Each stays `null` for vaults without that service.
6521
- */
7580
+ /** Per-vault dependency graph (#638) — field postures + derivation/MV/overlay/
7581
+ * computed/via-deps edges. Metadata-only (field names, postures, grains
7582
+ * never values or key material); always present, unlike the registries above. */
7583
+ graph;
7584
+ /** #638 Task 4 open sync/cutover/restore touch collector, or `undefined` between batches. */
7585
+ _graphBatch;
7586
+ /** Cached read-only facades for guard/derivation callbacks split by
7587
+ * resolution layer (`layer:'guard'` vs `'derivation'`, each field's
7588
+ * `onMissing` policy differs). Allocated eagerly inside `_initGuards()`/
7589
+ * `_initDerivations()` so read accessors stay synchronous; `null` for
7590
+ * vaults without that service. */
6522
7591
  guardFacade = null;
6523
7592
  derivationFacade = null;
6524
7593
  getDEK;
@@ -6547,18 +7616,9 @@ var Vault = class {
6547
7616
  * @see docs/superpowers/specs/2026-06-17-fr6-deed-custodian-liberate-design.md
6548
7617
  */
6549
7618
  custody;
6550
- /**
6551
- * Optional callback that re-derives an UnlockedKeyring from the
6552
- * adapter using the active user's passphrase. Called by `load()`
6553
- * after the on-disk keyring file has been replaced — refreshes
6554
- * `this.keyring` so the next DEK access uses the loaded wrapped
6555
- * DEKs instead of the stale pre-load ones.
6556
- *
6557
- * Provided by Noydb at openVault() time. Tests that
6558
- * construct Vault directly can pass `undefined`; load()
6559
- * skips the refresh in that case (which is fine for plaintext
6560
- * compartments — there's nothing to re-unwrap).
6561
- */
7619
+ /** Re-derives an UnlockedKeyring from the adapter using the active user's passphrase; called by
7620
+ * `load()` after the on-disk keyring file has been replaced. Provided by Noydb at openVault()
7621
+ * time; `undefined` in tests that construct Vault directly (load() then skips the refresh). */
6562
7622
  reloadKeyring;
6563
7623
  collectionCache = /* @__PURE__ */ new Map();
6564
7624
  satelliteRegistry = null;
@@ -6640,46 +7700,22 @@ var Vault = class {
6640
7700
  * should use separate Vault instances.
6641
7701
  */
6642
7702
  consentContext = null;
6643
- /**
6644
- * Registry of dictKey fields declared across all collections in this
6645
- * vault. Keyed by collection name → field name → dictionary name.
6646
- * Used by `DictionaryHandle.rename()` to find and update all records
6647
- * referencing a renamed key.
6648
- *
6649
- * Populated by `collection()` when the `dictKeyFields` option is passed.
6650
- */
7703
+ /** dictKeyField registry: collection name → field name → dictionary name — `DictionaryHandle.rename()`'s reference-update source; populated by `collection()` from `dictKeyFields`. */
6651
7704
  dictKeyFieldRegistry = /* @__PURE__ */ new Map();
6652
- /**
6653
- * Names of dictionaries backed by a `staticDict()` descriptor.
6654
- * A static dict skips the `dictKeyFieldRegistry` rename machinery, but the
6655
- * vault must still *know* a name is static so `vault.dictionary(name)` can
6656
- * refuse mutation (`StaticDictReadonlyError`). Populated at `collection()`
6657
- * config time whenever a `StaticDictDescriptor` is seen.
6658
- */
7705
+ /** Names of `staticDict()`-backed dictionaries (skip rename tracking; `vault.dictionary(name)` refuses mutation on these via `StaticDictReadonlyError`). Populated at `collection()` config time. */
6659
7706
  staticDictNames = /* @__PURE__ */ new Set();
6660
- /**
6661
- * Static-dict descriptors keyed by dictionary name. Backs the
6662
- * read-path label resolver (resolve from the in-memory table) and the
6663
- * query-seam `resolveDictSource` snapshot. Last writer wins when the same
6664
- * name is registered by multiple collections (identical-across-vaults by
6665
- * construction, so the tables match).
6666
- */
7707
+ /** Static-dict descriptors by dictionary name — backs the read-path label resolver and the `resolveDictSource` snapshot. Last writer wins across collections (tables match by construction). */
6667
7708
  staticByName = /* @__PURE__ */ new Map();
6668
- /**
6669
- * Per-collection map of field name → StaticDictDescriptor. Used by
6670
- * `enforceStaticDictOnPut` to validate stored codes against `desc.keys`.
6671
- */
7709
+ /** Per-collection field name → StaticDictDescriptor, validated by `enforceStaticDictOnPut`. */
6672
7710
  staticDescriptorByField = /* @__PURE__ */ new Map();
6673
- /**
6674
- * Registry of i18nText fields declared across all collections. Keyed
6675
- * by collection name → field name → I18nTextDescriptor. Used by
6676
- * `applyI18nLocale` on reads and by `validateI18nTextValue` on puts.
6677
- *
6678
- * Populated by `collection()` when the `i18nFields` option is passed.
6679
- */
7711
+ /** i18nText fields: collection name → field name → descriptor. Used by `applyI18nLocale`/`validateI18nTextValue`; populated by `collection()` from `i18nFields`. */
6680
7712
  i18nFieldRegistry = /* @__PURE__ */ new Map();
6681
7713
  /** Cache of DictionaryHandle instances, one per dictionary name. */
6682
7714
  dictionaryCache = /* @__PURE__ */ new Map();
7715
+ /** #650 Task 4 (#647) — declared reserved-lookup collection name → dimension name, for dimensions
7716
+ * with `backing:'reserved'` (dict()/dictKey()). The explicit sync-pull registry: NOT a blanket
7717
+ * underscore-glob — populated at `collection()`-declare time and at `dictionary()`-call time. */
7718
+ reservedLookupCollections = /* @__PURE__ */ new Map();
6683
7719
  /** Registered link specs, keyed by link name; set by `vault.link()`. */
6684
7720
  linkRegistry = /* @__PURE__ */ new Map();
6685
7721
  /** Cache of link-set handles, one per link name (lazy -- see links()). */
@@ -6712,6 +7748,7 @@ var Vault = class {
6712
7748
  this.onDirty = opts.onDirty;
6713
7749
  this.onRegisterConflictResolver = opts.onRegisterConflictResolver;
6714
7750
  this.syncAdapter = opts.syncAdapter;
7751
+ this.getPurgeableTargets = opts.getPurgeableTargets ?? (() => []);
6715
7752
  this.blobStrategy = opts.blobStrategy;
6716
7753
  this.objectStore = opts.objectStore;
6717
7754
  this.archiveStrategy = opts.archiveStrategy;
@@ -6734,7 +7771,10 @@ var Vault = class {
6734
7771
  userId: () => this.keyring.userId,
6735
7772
  getDEK: (collection) => this.getDEK(collection),
6736
7773
  getLedgerOrNull: () => this.getLedgerOrNull(),
6737
- collection: (name) => this.collection(name)
7774
+ collection: (name) => this.collection(name),
7775
+ purgeDeleteMarkers: (before) => this._purgeDeleteMarkers(before),
7776
+ archiveRecords: (before) => this._archiveClosedPeriod(before),
7777
+ purgeTargets: (before) => this._purgePeriodTargets(before)
6738
7778
  });
6739
7779
  this.linksEnforcer = new VaultLinks({
6740
7780
  refRegistry: this.refRegistry,
@@ -6752,6 +7792,7 @@ var Vault = class {
6752
7792
  this.i18nStrategy = opts.i18nStrategy ?? NO_I18N;
6753
7793
  this.syncStrategy = opts.syncStrategy ?? NO_SYNC;
6754
7794
  this.classifiedStrategy = opts.classifiedStrategy ?? NO_CLASSIFIED;
7795
+ this.graph = new ViaGraph();
6755
7796
  void opts.guardStrategies;
6756
7797
  this.historyConfig = opts.historyConfig ?? { enabled: true };
6757
7798
  this.reloadKeyring = opts.reloadKeyring;
@@ -6878,6 +7919,7 @@ var Vault = class {
6878
7919
  }, collectionName, { ...options, satelliteOf: options.satelliteOf }, this.satelliteRegistry);
6879
7920
  }
6880
7921
  let coll = this.collectionCache.get(collectionName);
7922
+ const reconcilePlan = coll && (options?.computed || options?.classifiedFields) ? validateReconcileGraphEdges(this.graph, collectionName, options) : void 0;
6881
7923
  if (coll && options?.moneyFields) {
6882
7924
  coll._applyMoneyFields(options.moneyFields);
6883
7925
  }
@@ -6893,12 +7935,17 @@ var Vault = class {
6893
7935
  if (coll && options?.classifiedFields) {
6894
7936
  coll._applyClassifiedFields(options.classifiedFields);
6895
7937
  }
7938
+ if (reconcilePlan) {
7939
+ commitReconcileGraphEdges(this.graph, collectionName, reconcilePlan);
7940
+ applyTaintOverlay(coll, this.graph, collectionName);
7941
+ }
6896
7942
  if (!coll) {
7943
+ const effectiveViaFields = mergeViaFields({ moneyFields: options?.moneyFields, i18nFields: options?.i18nFields, dictKeyFields: options?.dictKeyFields, lookupFields: options?.lookupFields, viaFields: options?.viaFields });
6897
7944
  if (options?.refs) {
6898
7945
  this.refRegistry.register(collectionName, options.refs);
6899
7946
  }
6900
- if (options?.i18nFields) {
6901
- this.i18nFieldRegistry.set(collectionName, options.i18nFields);
7947
+ if (effectiveViaFields.i18nFields) {
7948
+ this.i18nFieldRegistry.set(collectionName, effectiveViaFields.i18nFields);
6902
7949
  }
6903
7950
  if (options?.blobFields) {
6904
7951
  this.blobFieldsRegistry.set(collectionName, options.blobFields);
@@ -6909,10 +7956,10 @@ var Vault = class {
6909
7956
  if (options?.attestation !== void 0) {
6910
7957
  this.attestation.register(collectionName, options.attestation);
6911
7958
  }
6912
- if (options?.dictKeyFields) {
7959
+ if (effectiveViaFields.dictKeyFields || effectiveViaFields.lookupFields) {
6913
7960
  const dictFieldMap = {};
6914
7961
  const staticFieldMap = {};
6915
- for (const [field, desc] of Object.entries(options.dictKeyFields)) {
7962
+ for (const [field, desc] of Object.entries(effectiveViaFields.dictKeyFields ?? {})) {
6916
7963
  if (isStaticDictDescriptor(desc)) {
6917
7964
  staticFieldMap[field] = desc;
6918
7965
  this.staticDictNames.add(desc.name);
@@ -6921,8 +7968,18 @@ var Vault = class {
6921
7968
  dictFieldMap[field] = desc.name;
6922
7969
  }
6923
7970
  }
7971
+ const lookupCompat = collectLookupDictCompat(effectiveViaFields.lookupFields);
7972
+ Object.assign(dictFieldMap, lookupCompat.dictFieldMap);
7973
+ for (const [field, desc] of lookupCompat.staticEntries) {
7974
+ staticFieldMap[field] = desc;
7975
+ this.staticDictNames.add(desc.name);
7976
+ this.staticByName.set(desc.name, desc);
7977
+ }
6924
7978
  if (Object.keys(dictFieldMap).length > 0) {
6925
7979
  this.dictKeyFieldRegistry.set(collectionName, dictFieldMap);
7980
+ for (const dictName of new Set(Object.values(dictFieldMap))) {
7981
+ this.reservedLookupCollections.set(dictCollectionName(dictName), dictName);
7982
+ }
6926
7983
  }
6927
7984
  if (Object.keys(staticFieldMap).length > 0) {
6928
7985
  this.staticDescriptorByField.set(collectionName, staticFieldMap);
@@ -6937,7 +7994,7 @@ var Vault = class {
6937
7994
  const strategies = options.schemaUpdate ?? [];
6938
7995
  const work = (async () => {
6939
7996
  const dek = await this.getDEK(collectionName);
6940
- const { persistSchemaIfNeeded } = await import("./register-5NRE7HBZ.js");
7997
+ const { persistSchemaIfNeeded } = await import("./register-OVJKW7BS.js");
6941
7998
  const result = await persistSchemaIfNeeded({
6942
7999
  store: this.adapter,
6943
8000
  vault: this.name,
@@ -7002,6 +8059,7 @@ var Vault = class {
7002
8059
  defaultLocale: this.locale,
7003
8060
  onRegisterConflictResolver: this.onRegisterConflictResolver,
7004
8061
  onAccess: (op, id) => this._logConsent(op, collectionName, id),
8062
+ graphDispatch: { collect: (collection, id) => this._collectGraphTouch(collection, id) },
7005
8063
  // Derivation source is only wired when the corresponding registry
7006
8064
  // has been initialised. Guard source was removed in Track A slice 3b
7007
8065
  // — guards now run via the gate bus in Noydb.#registerGuardGate.
@@ -7072,9 +8130,10 @@ var Vault = class {
7072
8130
  if (options?.warmIndexOnOpen !== void 0) collOpts.warmIndexOnOpen = options.warmIndexOnOpen;
7073
8131
  if (options?.textIndexPersist !== void 0) collOpts.textIndexPersist = options.textIndexPersist;
7074
8132
  if (options?.moneyFields !== void 0) collOpts.moneyFields = options.moneyFields;
8133
+ if (options?.viaFields !== void 0) collOpts.viaFields = options.viaFields;
7075
8134
  if (options?.computed !== void 0) collOpts.computed = options.computed;
7076
8135
  if (options?.classifiedFields !== void 0) collOpts.classifiedFields = options.classifiedFields;
7077
- if (options?.dictKeyFields !== void 0) {
8136
+ if (effectiveViaFields.dictKeyFields !== void 0 || effectiveViaFields.lookupFields !== void 0) {
7078
8137
  collOpts.dictLabelResolver = async (dictName, key, locale, fallback) => {
7079
8138
  const stat = this.staticByName.get(dictName);
7080
8139
  if (stat) {
@@ -7085,15 +8144,23 @@ var Vault = class {
7085
8144
  return handle.resolveLabel(key, locale, fallback);
7086
8145
  };
7087
8146
  collOpts.getDictionary = async (name) => this.dictionary(name);
7088
- collOpts.dictKeyFields = options.dictKeyFields;
8147
+ collOpts.dictKeyFields = options?.dictKeyFields;
8148
+ }
8149
+ if (effectiveViaFields.lookupFields !== void 0) {
8150
+ collOpts.lookupLabelResolver = collOpts.dictLabelResolver;
8151
+ collOpts.getLookupBacking = (dimension) => async (key) => await this.collection(dimension).get(key) ?? void 0;
8152
+ collOpts.lookupFields = options?.lookupFields;
8153
+ collOpts.membership = (field, key) => checkLookupMembership(effectiveViaFields.lookupFields[field], key, (n) => this.dictionary(n), (n) => this.collection(n));
8154
+ collOpts.getAltIndex = (d) => buildLookupAltIndex(d, (n) => this.dictionary(n), (n) => this.collection(n));
8155
+ collOpts.snapshotFor = (d) => buildLookupSnapshotRows(d, (n) => this.reservedLookupCollections.has(dictCollectionName(n)), (n) => this.dictionary(n), (n) => this.collection(n));
7089
8156
  }
7090
- if (options?.i18nFields !== void 0 || options?.dictKeyFields !== void 0) {
8157
+ if (effectiveViaFields.i18nFields !== void 0 || effectiveViaFields.dictKeyFields !== void 0) {
7091
8158
  collOpts.i18nPutValidator = (record) => {
7092
8159
  this.enforceI18nOnPut(collectionName, record);
7093
8160
  this.enforceStaticDictOnPut(collectionName, record);
7094
8161
  };
7095
8162
  }
7096
- if (options?.i18nFields !== void 0 && this.translateText) {
8163
+ if (effectiveViaFields.i18nFields !== void 0 && this.translateText) {
7097
8164
  collOpts.autoTranslateHook = this.translateText;
7098
8165
  }
7099
8166
  if (options?.fieldMeta !== void 0) {
@@ -7107,6 +8174,9 @@ var Vault = class {
7107
8174
  }
7108
8175
  coll = new Collection(collOpts);
7109
8176
  this.collectionCache.set(collectionName, coll);
8177
+ registerCollectionGraphSources(this.graph, collectionName, collOpts);
8178
+ registerLookupRefEdges(this.graph, collectionName, effectiveViaFields.lookupFields);
8179
+ applyTaintOverlay(coll, this.graph, collectionName);
7110
8180
  if (options?.warmIndexOnOpen === true && options.prefetch !== false) {
7111
8181
  void coll.warmIndex().catch(() => {
7112
8182
  });
@@ -7116,7 +8186,7 @@ var Vault = class {
7116
8186
  const work = (async () => {
7117
8187
  try {
7118
8188
  const dek = await this.getDEK(collectionName);
7119
- await (await import("./register-5NRE7HBZ.js")).persistSchemaIfNeeded({
8189
+ await (await import("./register-OVJKW7BS.js")).persistSchemaIfNeeded({
7120
8190
  store: this.adapter,
7121
8191
  vault: this.name,
7122
8192
  collectionName,
@@ -7179,7 +8249,9 @@ var Vault = class {
7179
8249
  async #runCutoverTransform(collectionName, transform) {
7180
8250
  const coll = this.collectionCache.get(collectionName);
7181
8251
  if (!coll) return;
8252
+ this._beginGraphBatch();
7182
8253
  await coll._applyCutoverTransform(transform);
8254
+ await this._flushGraphBatch();
7183
8255
  }
7184
8256
  /**
7185
8257
  * Refresh a loaded collection's view of one document from a peer
@@ -7191,12 +8263,45 @@ var Vault = class {
7191
8263
  if (!coll) return;
7192
8264
  await coll._applyRemoteChange(docId, action);
7193
8265
  }
7194
- /** @internal #598: refresh cache entries a sync-applied write rewrote underneath us. No-op if not loaded this session. */
8266
+ /** @internal #598: refresh cache entries a sync-applied write rewrote underneath us. No-op if not loaded this session.
8267
+ * #650 Task 4 (#647): a reserved-lookup collection has no `Collection` instance — instead, refresh
8268
+ * its already-warmed `LookupHandle._syncCache` (membership/altIndex/snapshot reads never see a
8269
+ * stale verdict for a pulled vocabulary edit) and collect the touch into any open graph batch
8270
+ * (the sync-apply wave-reachability seam Task 5's ref edges will use). No-op if never warmed. */
7195
8271
  async _invalidateSyncApplied(collection, id) {
7196
8272
  const coll = this.collectionCache.get(collection);
7197
- if (!coll) return;
7198
- coll._invalidateCekCacheEntry(id);
7199
- await coll._invalidateCacheEntry(id);
8273
+ if (coll) {
8274
+ await coll._onRecordMutated(id, "put", "sync-apply");
8275
+ return;
8276
+ }
8277
+ const dimName = this.reservedLookupCollections.get(collection);
8278
+ if (dimName === void 0) return;
8279
+ await this.dictionaryCache.get(dimName)?._refreshSyncCache(id);
8280
+ this._collectGraphTouch(collection, id);
8281
+ }
8282
+ /** @internal #650 Task 4 (#647) — declared reserved-lookup collection names, for `SyncEngine.pull()`'s explicit prefix registry. */
8283
+ _reservedLookupCollectionNames() {
8284
+ return [...this.reservedLookupCollections.keys()];
8285
+ }
8286
+ /** @internal #638 Task 4 — open a graph-dispatch touch batch (sync pull()/push()/cutover). */
8287
+ _beginGraphBatch() {
8288
+ this._graphBatch = /* @__PURE__ */ new Map();
8289
+ }
8290
+ /** @internal #638 Task 4 — `Collection._onRecordMutated`'s sync-apply/cutover/restore hook; a
8291
+ * no-op when no batch is open (`_beginGraphBatch()` was never called for this call chain). */
8292
+ _collectGraphTouch(collection, id) {
8293
+ if (!this._graphBatch) return;
8294
+ this._graphBatch.set(collection, (this._graphBatch.get(collection) ?? /* @__PURE__ */ new Set()).add(id));
8295
+ }
8296
+ /** @internal #638 Task 4 — close the open batch and run ONE dispatch wave over it. */
8297
+ async _flushGraphBatch() {
8298
+ const batch = this._graphBatch;
8299
+ this._graphBatch = void 0;
8300
+ if (batch && batch.size > 0) await runGraphDispatchWave(this, batch);
8301
+ }
8302
+ /** @internal #638 Task 4 — cached-collection lookup for `runGraphDispatchWave` (`VaultLike`); never constructs. */
8303
+ _getCollection(name) {
8304
+ return this.collectionCache.get(name);
7200
8305
  }
7201
8306
  /**
7202
8307
  * @internal #589 → #604. Physically remove delete markers with `_ts` strictly
@@ -7209,27 +8314,65 @@ var Vault = class {
7209
8314
  * ONLY; #604's period-close lifecycle is what earns that assertion. Do not call
7210
8315
  * it on live/unsettled data.
7211
8316
  *
7212
- * Emits no ledger/event yet — #604's period-close, the only intended caller, owns
7213
- * the audit record.
8317
+ * Emits no ledger/event itselfits callers (#604's `freezePeriod`, #615's
8318
+ * `purgePeriodTargets`) own the audit record. The sweep body is shared via
8319
+ * {@link _purgeMarkersOn}, which #615 also aims at push-only sync targets.
7214
8320
  *
7215
- * Purges the local adapter only; the operator must purge every sync target too, or
7216
- * the next pull re-imports the markers (benign they still read deleted — but no
7217
- * space is reclaimed). #604 owns cross-target purge.
8321
+ * Purges the local adapter only. Markers already pushed to the vault's
8322
+ * push-only (`backup`/`archive`) sync targets are reclaimed by #615's
8323
+ * `vault.purgePeriodTargets`; `sync-peer` targets are left untouched (purging
8324
+ * them would re-open the resurrection window — the deferred half of #611).
7218
8325
  */
7219
8326
  async _purgeDeleteMarkers(before, collections) {
7220
- const snapshot = await this.adapter.loadAll(this.name);
8327
+ return this._purgeMarkersOn(this.adapter, before, collections);
8328
+ }
8329
+ /**
8330
+ * @internal #615. Sweep delete markers with `_ts < before` off ANY store
8331
+ * (local adapter or a push-only sync target). Returns the count removed.
8332
+ */
8333
+ async _purgeMarkersOn(store, before, collections) {
8334
+ const snapshot = await store.loadAll(this.name);
7221
8335
  let removed = 0;
7222
8336
  for (const [coll, records] of Object.entries(snapshot)) {
7223
8337
  if (collections && !collections.includes(coll)) continue;
7224
8338
  for (const [id, env] of Object.entries(records)) {
7225
8339
  if (isDeleteMarker(env) && env._ts < before) {
7226
- await this.adapter.delete(this.name, coll, id);
8340
+ await store.delete(this.name, coll, id);
7227
8341
  removed++;
7228
8342
  }
7229
8343
  }
7230
8344
  }
7231
8345
  return removed;
7232
8346
  }
8347
+ /**
8348
+ * @internal #613. Relocate this closed period's in-window records (those
8349
+ * with `_ts < before`) from the hot store to the configured cold tier,
8350
+ * via the routeStore's `compact({ before })`. Returns the count moved.
8351
+ * Requires a `routeStore` with a cold route (the `coldArchival`
8352
+ * capability); throws a clear error otherwise. Non-destructive — reads
8353
+ * fall through to cold, so this does NOT re-open the #589 window.
8354
+ */
8355
+ async _archiveClosedPeriod(before) {
8356
+ const store = this.adapter;
8357
+ if (store.capabilities?.coldArchival !== true || typeof store.compact !== "function") {
8358
+ throw new ValidationError("archivePeriod: cold archival requires a routeStore with a cold route.");
8359
+ }
8360
+ return store.compact(this.name, { before });
8361
+ }
8362
+ /**
8363
+ * @internal #615. Sweep delete markers with `_ts < before` off each of the
8364
+ * vault's push-only sync targets, returning a per-target count. Skips
8365
+ * `sync-peer` targets by construction (getPurgeableTargets yields only
8366
+ * backup/archive). Never touches the local adapter.
8367
+ */
8368
+ async _purgePeriodTargets(before) {
8369
+ const out = [];
8370
+ for (const t of this.getPurgeableTargets()) {
8371
+ const purgedCount = await this._purgeMarkersOn(t.store, before);
8372
+ out.push({ ...t.label !== void 0 ? { label: t.label } : {}, role: t.role, purgedCount });
8373
+ }
8374
+ return out;
8375
+ }
7233
8376
  /**
7234
8377
  * For a detected conflict: capture this tab's clobbered record,
7235
8378
  * read the common ancestor from history, converge the cache to the store's
@@ -7300,8 +8443,12 @@ var Vault = class {
7300
8443
  * Validate i18nText fields on a `put()`. Called by Collection just
7301
8444
  * before the adapter write, after schema validation. Throws
7302
8445
  * `MissingTranslationError` when a required translation is absent.
8446
+ *
8447
+ * Delegates through the Via registry (#623) — a no-op only when i18n was
8448
+ * never declared (mirrors the registry check below).
7303
8449
  */
7304
8450
  enforceI18nOnPut(collectionName, record) {
8451
+ if (!isViaInstalled("i18n")) return;
7305
8452
  const i18nFields = this.i18nFieldRegistry.get(collectionName);
7306
8453
  if (!i18nFields || Object.keys(i18nFields).length === 0) return;
7307
8454
  if (!record || typeof record !== "object") return;
@@ -7318,79 +8465,15 @@ var Vault = class {
7318
8465
  * Validate staticDict codes on a `put()`. For each `staticDict()`
7319
8466
  * field, every stored code must be a declared key of the descriptor's
7320
8467
  * table, else `UnknownDictCodeError`. Opt out per descriptor with
7321
- * `{ validateCodes: false }`. Supports scalar, dotted, and `[].`-wildcard
7322
- * field paths via `getAtPath` (same path support as i18n validation).
7323
- */
7324
- enforceStaticDictOnPut(collectionName, record) {
7325
- const staticFields = this.staticDescriptorByField.get(collectionName);
7326
- if (!staticFields || Object.keys(staticFields).length === 0) return;
7327
- if (!record || typeof record !== "object") return;
7328
- const obj = record;
7329
- for (const [field, desc] of Object.entries(staticFields)) {
7330
- if (desc.validateCodes === false) continue;
7331
- const known = new Set(desc.keys);
7332
- const values = getAtPath(obj, field);
7333
- for (const value of values) {
7334
- if (value === void 0 || value === null) continue;
7335
- const codes = Array.isArray(value) ? value : [value];
7336
- for (const code of codes) {
7337
- if (typeof code !== "string") continue;
7338
- if (!known.has(code)) {
7339
- throw new UnknownDictCodeError(desc.name, field, code);
7340
- }
7341
- }
7342
- }
7343
- }
7344
- }
7345
- /**
7346
- * Apply locale resolution to a record for the given collection.
8468
+ * `{ validateCodes: false }`.
7347
8469
  *
7348
- * Called by Collection after decryption when locale options are present.
7349
- * Returns a new object (never mutates the cached record).
8470
+ * Delegates through the Via registry see {@link enforceI18nOnPut}. The
8471
+ * per-field validation itself lives in `shape/via-lookup/registry.ts`
8472
+ * (#650 Task 1 extraction), reached via `port/with/lookup-strategy.ts`.
7350
8473
  */
7351
- async applyLocale(collectionName, record, localeOpts) {
7352
- const locale = localeOpts.locale ?? this.locale;
7353
- const staticFields = this.staticDescriptorByField.get(collectionName);
7354
- const hasStaticDisplay = staticFields !== void 0 && Object.values(staticFields).some((d) => d.displayLocale !== void 0);
7355
- if (!locale && !hasStaticDisplay) return record;
7356
- let result = record;
7357
- if (locale) {
7358
- const i18nFields = this.i18nFieldRegistry.get(collectionName);
7359
- if (i18nFields && Object.keys(i18nFields).length > 0) {
7360
- result = this.i18nStrategy.applyI18nLocale(result, i18nFields, locale, localeOpts.fallback);
7361
- }
7362
- }
7363
- const dictFields = this.dictKeyFieldRegistry.get(collectionName);
7364
- if (locale && dictFields && Object.keys(dictFields).length > 0 && locale !== "raw") {
7365
- const withLabels = { ...result };
7366
- for (const [field, dictName] of Object.entries(dictFields)) {
7367
- const key = result[field];
7368
- if (typeof key !== "string") continue;
7369
- const handle = this.dictionary(dictName);
7370
- const label = await handle.resolveLabel(key, locale, localeOpts.fallback);
7371
- if (label !== void 0) {
7372
- withLabels[`${field}Label`] = label;
7373
- }
7374
- }
7375
- result = withLabels;
7376
- }
7377
- if (staticFields && Object.keys(staticFields).length > 0 && locale !== "raw") {
7378
- const withLabels = { ...result };
7379
- for (const [field, desc] of Object.entries(staticFields)) {
7380
- const effLocale = locale ?? desc.displayLocale;
7381
- if (!effLocale) continue;
7382
- const key = result[field];
7383
- if (typeof key !== "string") continue;
7384
- const labels = desc.table[key];
7385
- if (!labels) continue;
7386
- const label = resolveLabelFromMap(labels, effLocale, localeOpts.fallback ?? desc.substitute);
7387
- if (label !== void 0) {
7388
- withLabels[`${field}Label`] = label;
7389
- }
7390
- }
7391
- result = withLabels;
7392
- }
7393
- return result;
8474
+ enforceStaticDictOnPut(collectionName, record) {
8475
+ if (!isViaInstalled("i18n")) return;
8476
+ enforceStaticDictOnPut(this.staticDescriptorByField.get(collectionName), record);
7394
8477
  }
7395
8478
  /**
7396
8479
  * Open a dictionary by name. Returns a `DictionaryHandle` for CRUD
@@ -7414,6 +8497,7 @@ var Vault = class {
7414
8497
  if (this.staticDictNames.has(name)) {
7415
8498
  throw new StaticDictReadonlyError(name);
7416
8499
  }
8500
+ this.reservedLookupCollections.set(dictCollectionName(name), name);
7417
8501
  let handle = this.dictionaryCache.get(name);
7418
8502
  if (!handle) {
7419
8503
  handle = this.i18nStrategy.buildDictionaryHandle({
@@ -7421,37 +8505,36 @@ var Vault = class {
7421
8505
  compartmentName: this.name,
7422
8506
  dictionaryName: name,
7423
8507
  keyring: this.keyring,
7424
- getDEK: this.getDEK,
8508
+ reservedEnvelopes: makeReservedEnvelopes((collection) => this.getDEK(collection), ["_dict_"])("_dict_"),
7425
8509
  encrypted: this.encrypted,
7426
8510
  ledger: this.getLedgerOrNull() ?? void 0,
7427
8511
  options,
7428
8512
  // findAndUpdateReferences: rewrite dictKey fields in all
7429
- // registered collections when rename() is called
8513
+ // registered collections when rename() is called. Body extracted to
8514
+ // shape/via-lookup/registry.ts (#650 Task 1) — reached via the
8515
+ // port/with/lookup-strategy.ts seam.
7430
8516
  findAndUpdateReferences: async (dictionaryName, oldKey, newKey) => {
7431
- for (const [collectionName, dictFields] of this.dictKeyFieldRegistry) {
7432
- const fields = Object.entries(dictFields).filter(([, dn]) => dn === dictionaryName).map(([field]) => field);
7433
- if (fields.length === 0) continue;
7434
- const coll = this.collection(collectionName);
7435
- const records = await coll.list();
7436
- for (const record of records) {
7437
- let changed = false;
7438
- const updated = { ...record };
7439
- for (const field of fields) {
7440
- if (updated[field] === oldKey) {
7441
- updated[field] = newKey;
7442
- changed = true;
7443
- }
7444
- }
7445
- if (changed) {
7446
- const id = record["id"];
7447
- if (id !== void 0) {
7448
- await coll.put(id, updated);
7449
- }
7450
- }
7451
- }
7452
- }
8517
+ await updateReferencingRecords(
8518
+ this.dictKeyFieldRegistry,
8519
+ (collectionName) => this.collection(collectionName),
8520
+ dictionaryName,
8521
+ oldKey,
8522
+ newKey
8523
+ );
8524
+ },
8525
+ emitter: this.emitter,
8526
+ buildDeleteMarker,
8527
+ // #647 fix wave 1 — real kernel/enclave builder; LookupHandle can't import it directly
8528
+ // #650 Task 4 (#647) — choke-point participation for LOCAL writes: dirty-log tracking + a
8529
+ // one-shot graph-dispatch wave (local-write's `graphDispatch.collect`-equivalent; Task 5's ref edges give it dependents).
8530
+ onDirty: this.onDirty,
8531
+ onRecordMutated: async (collection, id) => {
8532
+ this._beginGraphBatch();
8533
+ this._collectGraphTouch(collection, id);
8534
+ await this._flushGraphBatch();
7453
8535
  },
7454
- emitter: this.emitter
8536
+ checkReferencesOnDelete: (key) => this.linksEnforcer.enforceLookupRefsOnDelete(this.graph, name, dictCollectionName(name), key)
8537
+ // #650 Task 5
7455
8538
  });
7456
8539
  this.dictionaryCache.set(name, handle);
7457
8540
  }
@@ -7520,65 +8603,24 @@ var Vault = class {
7520
8603
  return handle;
7521
8604
  }
7522
8605
  /**
7523
- * Build a `JoinableSource` for a dictKey field, for use in dict joins
7524
- *. Returns a source whose snapshot contains `{ key, ...labels }`
7525
- * records — one per dictionary entry — keyed by the stable key.
7526
- *
7527
- * Returns `null` when `field` is not a dictKey in `leftCollection`.
7528
- *
7529
- * The snapshot is built synchronously from whatever the dictionary
7530
- * handle has in its cached state. For empty dictionaries this returns
7531
- * an empty snapshot rather than `null`.
7532
- */
7533
- /**
7534
- * Build a `JoinableSource` for a dictKey field, for use in dict joins
7535
- *. Returns a source whose snapshot contains
7536
- * `{ key, labels, ...labels }` records — one per dictionary entry —
7537
- * keyed by the stable key.
7538
- *
7539
- * The snapshot is built synchronously from the DictionaryHandle's
7540
- * write-through cache, which is populated on every `put()`, `rename()`,
7541
- * `delete()`, and `list()` call. For pre-existing data not yet touched
7542
- * this session, call `await vault.dictionary(name).list()` first
7543
- * to warm the cache.
8606
+ * Build a `JoinableSource` for a dictKey field, for use in dict joins.
8607
+ * Returns a source whose snapshot contains `{ key, labels, ...labels }`
8608
+ * records — one per dictionary entry — keyed by the stable key. Returns
8609
+ * `null` when `field` is not a dictKey in `leftCollection`.
7544
8610
  *
7545
- * Returns `null` when `field` is not a dictKey in `leftCollection`.
8611
+ * Body extracted to `shape/via-lookup/registry.ts` (#650 Task 1)
8612
+ * reached via the `port/with/lookup-strategy.ts` seam. See that
8613
+ * function's doc comment for the static-vs-dynamic-dict split and the
8614
+ * cache-warming caveat.
7546
8615
  */
7547
8616
  resolveDictSource(leftCollection, field) {
7548
- const staticFields = this.staticDescriptorByField.get(leftCollection);
7549
- if (staticFields && field in staticFields) {
7550
- const desc = staticFields[field];
7551
- const rows = Object.entries(desc.table).map(
7552
- ([key, labels]) => ({ key, labels, ...labels })
7553
- );
7554
- const source = {
7555
- snapshot() {
7556
- return rows;
7557
- },
7558
- lookupById(id) {
7559
- return rows.find((e) => e["key"] === id);
7560
- }
7561
- };
7562
- if (desc.displayLocale !== void 0) {
7563
- ;
7564
- source.displayLocale = desc.displayLocale;
7565
- }
7566
- return source;
7567
- }
7568
- const dictFields = this.dictKeyFieldRegistry.get(leftCollection);
7569
- if (!dictFields || !(field in dictFields)) return null;
7570
- const dictName = dictFields[field];
7571
- if (!dictName) return null;
7572
- const handle = this.dictionary(dictName);
7573
- return {
7574
- snapshot() {
7575
- return handle.snapshotEntries();
7576
- },
7577
- lookupById(id) {
7578
- const entries = handle.snapshotEntries();
7579
- return entries.find((e) => e["key"] === id);
7580
- }
7581
- };
8617
+ return resolveDictSource(
8618
+ leftCollection,
8619
+ field,
8620
+ this.staticDescriptorByField,
8621
+ this.dictKeyFieldRegistry,
8622
+ (name) => this.dictionary(name)
8623
+ );
7582
8624
  }
7583
8625
  /**
7584
8626
  * Set or update the vault-default locale at runtime.
@@ -7782,7 +8824,9 @@ var Vault = class {
7782
8824
  listRecordIds: (c) => this.adapter.list(this.name, c),
7783
8825
  getRecord: async (c, id) => await this.collection(c).get(id, { locale: "raw" }),
7784
8826
  getEnvelope: (c, id) => this.adapter.get(this.name, c, id),
7785
- removeFromPrimary: (c, id) => this.collection(c)._internalDelete(id),
8827
+ removeFromPrimary: async (c, id) => {
8828
+ await this.collection(c)._internalDelete(id);
8829
+ },
7786
8830
  restoreToPrimary: async (c, id, env) => {
7787
8831
  await this.adapter.put(this.name, c, id, env);
7788
8832
  await this.collection(c)._invalidateCacheEntry(id);
@@ -7869,7 +8913,7 @@ var Vault = class {
7869
8913
  * cascade guard. See `with-shape/links/vault-facade.ts`.
7870
8914
  */
7871
8915
  async enforceRefsOnDelete(collectionName, id) {
7872
- return this.linksEnforcer.enforceRefsOnDelete(collectionName, id);
8916
+ return this.linksEnforcer.enforceRefsOnDelete(this.graph, collectionName, id);
7873
8917
  }
7874
8918
  // ─── Join resolver) ────────────────────
7875
8919
  /**
@@ -7973,23 +9017,19 @@ var Vault = class {
7973
9017
  );
7974
9018
  }
7975
9019
  /**
7976
- * GDPR crypto-shred of a data subject. Consults the encrypted subject
7977
- * index and, per matching record:
7978
- * - rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`),
7979
- * - tombstones every `_history` version of the record,
7980
- * so the body and all prior versions become permanently undecryptable while
7981
- * the collection DEK and every OTHER record stay intact. Then appends ONE
7982
- * `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` —
7983
- * the chain still `verify()`s, PROVING the subject existed and was erased
7984
- * without retaining any plaintext.
7985
- *
7986
- * Reports but does not silently swallowtwo completeness gaps:
7987
- * - `unmigratedRecords`: a record whose body was NOT yet migrated to a
7988
- * per-record CEK (legacy body still under the shared collection DEK). It
7989
- * is still tombstoned, but its pre-shred ciphertext (if leaked to a
7990
- * backup before migration) stays decryptable. Migrate, then re-forget.
7991
- * - `blobResidueCollections`: a shredded record still has blob attachments,
7992
- * which are keyed off a separate `_blob` DEK and are out of scope here.
9020
+ * GDPR crypto-shred of a data subject. Consults the encrypted subject index and, per matching
9021
+ * record: rewrites the LIVE envelope to a tombstone (drops `_iv`/`_data`/`_cek`/`_det`), tombstones
9022
+ * every `_history` version, and fans out to derived residue via the graph (#622) — record-grain
9023
+ * artifacts (MV rows, derivation copies) ERASED, aggregate-grain rollups RECOMPUTED without the
9024
+ * forgotten contribution (skip+audit in a frozen period). The body/history become permanently
9025
+ * undecryptable while the collection DEK and every OTHER record stay intact. Then appends ONE
9026
+ * `op:'forget'` ledger entry whose `payloadHash` is `sha256Hex(subjectId)` — the chain still
9027
+ * `verify()`s, PROVING the subject existed and was erased without retaining any plaintext.
9028
+ *
9029
+ * Reports — but does not silently swallow — two completeness gaps: `unmigratedRecords` (a record
9030
+ * whose body was NOT yet migrated to a per-record CEK still tombstoned, but pre-shred backups
9031
+ * stay decryptable; migrate, then re-forget) and `blobResidueCollections` (blob attachments, keyed
9032
+ * off a separate `_blob` DEK, out of scope here).
7993
9033
  *
7994
9034
  * @throws ForgetStrategyNotConfiguredError when no `withForgetCascade` was set.
7995
9035
  */
@@ -8014,23 +9054,29 @@ var Vault = class {
8014
9054
  const indexResidue = [];
8015
9055
  const blobsEnabled = this.blobStrategy !== void 0;
8016
9056
  const actor = this.keyring.userId;
9057
+ const fanoutStats = { recordsErased: 0, aggregatesRecomputed: 0, residueFrozen: [], lookupReferencesCascaded: 0, lookupReferencesNullified: 0, lookupReferencesResidue: [] };
8017
9058
  for (const ref2 of allRefs) {
8018
9059
  const coll = this.collection(ref2.collection);
8019
9060
  const satelliteOf = ref2.satelliteOf;
8020
9061
  const perRecordKeys = this.forgetStrategy.subjects[satelliteOf ?? ref2.collection] !== void 0;
9062
+ const lookupCompareKeys = await this.linksEnforcer.checkLookupRefsRestrict(this.graph, ref2.collection, ref2.collection, ref2.id);
8021
9063
  const live = await this.adapter.get(this.name, ref2.collection, ref2.id);
8022
9064
  if (perRecordKeys && live && live._data && !hasPerRecordKey(live)) {
8023
9065
  unmigratedRecords.push(`${ref2.collection}:${ref2.id}`);
8024
9066
  }
8025
9067
  if (live?._sealed !== void 0) {
8026
9068
  try {
8027
- const cls = await coll._classifySealedShred(live);
8028
- for (const slot of cls.slots) {
8029
- if (slot.class === "shreddable" || slot.class === "live-shreddable+dekResidue-in-backups") {
8030
- sealedFieldsShredded += 1;
9069
+ const viaReport = await coll._onViaErase(ref2.id, live);
9070
+ if (viaReport) {
9071
+ sealedFieldsShredded += viaReport.shredded;
9072
+ for (const entry of viaReport.residue) {
9073
+ if (entry.kind === "classified-sealed-dek-residue" && entry.field !== void 0) sealedResidue.push(`${ref2.collection}:${ref2.id}:${entry.field}`);
8031
9074
  }
8032
- if (slot.class === "dekResidue" || slot.class === "live-shreddable+dekResidue-in-backups") {
8033
- sealedResidue.push(`${ref2.collection}:${ref2.id}:${slot.field}`);
9075
+ } else {
9076
+ const cls = await coll._classifySealedShred(live);
9077
+ for (const slot of cls.slots) {
9078
+ if (slot.class === "shreddable" || slot.class === "live-shreddable+dekResidue-in-backups") sealedFieldsShredded += 1;
9079
+ if (slot.class === "dekResidue" || slot.class === "live-shreddable+dekResidue-in-backups") sealedResidue.push(`${ref2.collection}:${ref2.id}:${slot.field}`);
8034
9080
  }
8035
9081
  }
8036
9082
  } catch {
@@ -8063,6 +9109,7 @@ var Vault = class {
8063
9109
  if (shred !== null) {
8064
9110
  recordsShredded++;
8065
9111
  collections.add(ref2.collection);
9112
+ await forgetDerivedFanout(this, { collection: ref2.collection, id: ref2.id }, live, fanoutStats, lookupCompareKeys);
8066
9113
  }
8067
9114
  historyVersionsShredded += await this.historyStrategy.tombstoneHistory(
8068
9115
  this.adapter,
@@ -8146,7 +9193,14 @@ var Vault = class {
8146
9193
  sealedCekEnvelopesPurged,
8147
9194
  sealedCekResidue,
8148
9195
  sealedResidue,
8149
- ledgerEntry
9196
+ ledgerEntry,
9197
+ derivedRecordsErased: fanoutStats.recordsErased,
9198
+ derivedAggregatesRecomputed: fanoutStats.aggregatesRecomputed,
9199
+ derivedResidueFrozen: fanoutStats.residueFrozen,
9200
+ lookupReferencesCascaded: fanoutStats.lookupReferencesCascaded,
9201
+ lookupReferencesNullified: fanoutStats.lookupReferencesNullified,
9202
+ lookupReferencesResidue: fanoutStats.lookupReferencesResidue
9203
+ // #650 Task 5 (+ review Important fix: residue)
8150
9204
  };
8151
9205
  }
8152
9206
  // ─── Record-scoped CEK sealing ─────────────────────────────────────
@@ -8242,7 +9296,7 @@ var Vault = class {
8242
9296
  if (handles.length === 0) return;
8243
9297
  const [{ GuardRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
8244
9298
  import("./registry-VW6P6A3J.js"),
8245
- import("./read-only-facade-5ADRJVTM.js")
9299
+ import("./read-only-facade-UCYNRE56.js")
8246
9300
  ]);
8247
9301
  const registry = new GuardRegistry();
8248
9302
  for (const h of handles) registry.register(h.spec);
@@ -8269,14 +9323,15 @@ var Vault = class {
8269
9323
  async _initDerivations(handles) {
8270
9324
  if (handles.length === 0) return;
8271
9325
  const [{ DerivationRegistry }, { ReadOnlyVaultFacade }] = await Promise.all([
8272
- import("./registry-5QLETK3Q.js"),
8273
- import("./read-only-facade-5ADRJVTM.js")
9326
+ import("./registry-DQRNTWRL.js"),
9327
+ import("./read-only-facade-UCYNRE56.js")
8274
9328
  ]);
8275
9329
  const registry = new DerivationRegistry();
8276
9330
  for (const h of handles) {
8277
9331
  await registry.register(h.spec);
8278
9332
  }
8279
- registry.validate();
9333
+ for (const edge of registry.edges()) this.graph.registerDerived(edge.target, edge.sources, edge.kind, edge.grain);
9334
+ registry.validate(this.graph);
8280
9335
  this.derivationRegistry = registry;
8281
9336
  if (this.derivationFacade === null) {
8282
9337
  this.derivationFacade = new ReadOnlyVaultFacade(this, "derivation");
@@ -8300,14 +9355,15 @@ var Vault = class {
8300
9355
  */
8301
9356
  async _initMaterializedViews(handles) {
8302
9357
  if (handles.length === 0) return;
8303
- const { MaterializedViewRegistry } = await import("./registry-DYTNLOJC.js");
9358
+ const { MaterializedViewRegistry } = await import("./registry-LTGLERS7.js");
8304
9359
  const registry = new MaterializedViewRegistry();
8305
9360
  this.materializedViewRegistry = registry;
8306
9361
  const db = this;
8307
9362
  for (const h of handles) {
8308
9363
  await registry.register(h.spec, db);
8309
9364
  }
8310
- registry.validate(this.derivationRegistry);
9365
+ for (const edge of registry.edges()) this.graph.registerDerived(edge.target, edge.sources, edge.kind, edge.grain);
9366
+ registry.validate(this.graph);
8311
9367
  }
8312
9368
  /**
8313
9369
  * @internal — consumed by `Collection.put` at write-time. Returns
@@ -8324,7 +9380,7 @@ var Vault = class {
8324
9380
  */
8325
9381
  async _initOverlayedViews(handles) {
8326
9382
  if (handles.length === 0) return;
8327
- const { OverlayedViewRegistry } = await import("./registry-HSNQ4NBP.js");
9383
+ const { OverlayedViewRegistry } = await import("./registry-5RJSK5AJ.js");
8328
9384
  const registry = new OverlayedViewRegistry();
8329
9385
  const mvRegistry = this.materializedViewRegistry;
8330
9386
  const overlayNames = /* @__PURE__ */ new Set();
@@ -8341,6 +9397,7 @@ var Vault = class {
8341
9397
  isOverlayName: (n) => overlayNames.has(n) && n !== h.spec.name,
8342
9398
  isMVOutput
8343
9399
  });
9400
+ this.graph.registerDerived({ collection: h.spec.name, field: "*" }, [{ collection: h.spec.base, field: "*" }], "overlay", "record");
8344
9401
  }
8345
9402
  this.overlayedViewRegistry = registry;
8346
9403
  }
@@ -8351,16 +9408,15 @@ var Vault = class {
8351
9408
  _getOverlayedViewRegistry() {
8352
9409
  return this.overlayedViewRegistry;
8353
9410
  }
9411
+ /** @internal — ctx for `putDerivedOutput`'s frozen-period skip+audit (#638 Task 5). */
9412
+ _dispatchCtx(source) {
9413
+ return { emit: (e, p) => this.emitter.emit(e, p), source, audit: ledgerAuditHook(this.getLedgerOrNull() ?? void 0, this.keyring.userId) };
9414
+ }
8354
9415
  /**
8355
- * Manual re-materialize for a single registered MV. Useful
8356
- * for `refresh: 'manual'` MVs (whose consumer drives refreshes
8357
- * externally), for stale-bit recovery on vault re-open, and as the
8358
- * explicit bulk-recompute escape hatch after a strategy change.
8359
- *
8360
- * Returns `{ written, deleted, failed }`. `deleted` is always 0
8361
- * when tombstoning is not enabled.
8362
- *
8363
- * Throws if `name` is not a registered MV.
9416
+ * Manual re-materialize for a single registered MV (`refresh: 'manual'` consumers,
9417
+ * stale-bit recovery on vault reopen, bulk-recompute escape hatch after a strategy
9418
+ * change). Returns `{ written, deleted, failed }` (`deleted` always 0 without
9419
+ * tombstoning). Throws if `name` is not a registered MV.
8364
9420
  */
8365
9421
  async refreshView(name) {
8366
9422
  const registry = this.materializedViewRegistry;
@@ -8371,42 +9427,42 @@ var Vault = class {
8371
9427
  if (!reg) {
8372
9428
  throw new Error(`refreshView: no MV registered with name "${name}"`);
8373
9429
  }
8374
- const { MaterializedViewExecutor } = await import("./executor-6LHOV7MN.js");
9430
+ const { MaterializedViewExecutor } = await import("./executor-IL7SRNUZ.js");
8375
9431
  const result = await MaterializedViewExecutor.refresh(reg, {
8376
9432
  getCollection: (n) => this.collection(n),
8377
9433
  getActiveTxContext: () => this.noydb._activeTxContextOrNull,
8378
- getQueryContext: () => this
9434
+ getQueryContext: () => this,
9435
+ dispatchCtx: this._dispatchCtx({ collection: name, id: "refreshView" })
8379
9436
  });
8380
- const { clearMVStale } = await import("./stale-DP66HFT3.js");
9437
+ const { clearMVStale } = await import("./stale-IYYXEH3U.js");
8381
9438
  clearMVStale(registry, name);
8382
9439
  return result;
8383
9440
  }
8384
9441
  /**
8385
- * Re-derive every record in the named source collection. Useful
8386
- * after a strategy change to bring previously-derived records
8387
- * up-to-date.
8388
- *
8389
- * Sequential in v1; parallelisation deferred to v2.
9442
+ * Re-derive every record in the named source collection (useful after a strategy
9443
+ * change to bring previously-derived records up-to-date). Sequential in v1.
8390
9444
  */
8391
9445
  async deriveAll(sourceCollection) {
8392
9446
  const registry = this._getDerivationRegistry();
8393
- if (registry === null) return { derived: 0, failed: 0 };
9447
+ if (registry === null) return { derived: 0, failed: 0, skippedFrozen: 0 };
8394
9448
  const strategies = registry.strategiesForSource(sourceCollection);
8395
- if (strategies.length === 0) return { derived: 0, failed: 0 };
8396
- const { DerivationExecutor } = await import("./executor-EUPYHCLU.js");
9449
+ if (strategies.length === 0) return { derived: 0, failed: 0, skippedFrozen: 0 };
9450
+ const { DerivationExecutor } = await import("./executor-WAEI6PDV.js");
8397
9451
  const sourceColl = this.collection(sourceCollection);
8398
9452
  const records = await sourceColl.list();
8399
- const ctx = { vault: this.derivationFacade ?? new (await import("./read-only-facade-5ADRJVTM.js")).ReadOnlyVaultFacade(this, "derivation") };
9453
+ const ctx = { vault: this.derivationFacade ?? new (await import("./read-only-facade-UCYNRE56.js")).ReadOnlyVaultFacade(this, "derivation") };
8400
9454
  let derived = 0;
8401
9455
  let failed = 0;
9456
+ let skippedFrozen = 0;
8402
9457
  for (const record of records) {
8403
9458
  if (typeof record !== "object" || record === null) continue;
8404
9459
  const id = record.id;
8405
9460
  if (typeof id !== "string") continue;
9461
+ const dispatchCtx = this._dispatchCtx({ collection: sourceCollection, id });
8406
9462
  for (const { spec, strategyHash } of strategies) {
8407
9463
  const sourceWithId = { ...record, id };
8408
9464
  const result = await DerivationExecutor.run(spec, sourceWithId, 0, strategyHash, ctx);
8409
- let anyFailed = false;
9465
+ let anyFailed = false, anyFrozenSkip = false;
8410
9466
  for (const key of Object.keys(spec.outputs)) {
8411
9467
  const out = result.outputs[key];
8412
9468
  if (!out) continue;
@@ -8418,7 +9474,7 @@ var Vault = class {
8418
9474
  if (!outSpec) continue;
8419
9475
  const outputColl = this.collection(outSpec.collection);
8420
9476
  if (out.kind === "array") {
8421
- const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-BDKLP5GG.js");
9477
+ const { loadFanoutSidecar, saveFanoutSidecar } = await import("./fanout-sidecar-CI5BCMQM.js");
8422
9478
  const prior = await loadFanoutSidecar(this.adapter, this.name, spec.source, id, key, this.getDEK, this.encrypted);
8423
9479
  const prevKeys = new Set(prior?.keys ?? []);
8424
9480
  const newKeysList = out.entries.map((e) => e.key);
@@ -8428,7 +9484,7 @@ var Vault = class {
8428
9484
  await outputColl._internalDelete(k);
8429
9485
  }
8430
9486
  for (const entry of out.entries) {
8431
- await outputColl.put(entry.key, entry.value);
9487
+ if (await putDerivedOutput(outputColl, entry.key, entry.value, dispatchCtx) === "skipped-frozen") anyFrozenSkip = true;
8432
9488
  }
8433
9489
  await saveFanoutSidecar(this.adapter, this.name, {
8434
9490
  source: spec.source,
@@ -8443,13 +9499,14 @@ var Vault = class {
8443
9499
  await outputColl._internalDelete(id);
8444
9500
  continue;
8445
9501
  }
8446
- await outputColl.put(id, out.value);
9502
+ if (await putDerivedOutput(outputColl, id, out.value, dispatchCtx) === "skipped-frozen") anyFrozenSkip = true;
8447
9503
  }
8448
9504
  if (anyFailed) failed++;
9505
+ else if (anyFrozenSkip) skippedFrozen++;
8449
9506
  else derived++;
8450
9507
  }
8451
9508
  }
8452
- return { derived, failed };
9509
+ return { derived, failed, skippedFrozen };
8453
9510
  }
8454
9511
  /**
8455
9512
  * @internal — exposed for `runTransaction({ amendment: true })` so
@@ -8640,7 +9697,7 @@ var Vault = class {
8640
9697
  * collection.
8641
9698
  */
8642
9699
  async delegate(opts) {
8643
- const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FDOXXHNG.js");
9700
+ const { issueDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-ZKII5UBI.js");
8644
9701
  if (!this.keyring.kek) {
8645
9702
  throw new ValidationError(
8646
9703
  "issueDelegation: keyring.kek is null \u2014 issuing a delegation requires a tier-1 unlock. Re-authenticate at tier 1 (passphrase) first."
@@ -8662,7 +9719,7 @@ var Vault = class {
8662
9719
  * if the id does not exist.
8663
9720
  */
8664
9721
  async revokeDelegation(id) {
8665
- const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-FDOXXHNG.js");
9722
+ const { revokeDelegation, DELEGATIONS_COLLECTION } = await import("./delegation-ZKII5UBI.js");
8666
9723
  await revokeDelegation(this.adapter, this.name, id);
8667
9724
  void DELEGATIONS_COLLECTION;
8668
9725
  }
@@ -8849,6 +9906,38 @@ var Vault = class {
8849
9906
  async getPeriod(name) {
8850
9907
  return this.periods.getPeriod(name);
8851
9908
  }
9909
+ /**
9910
+ * Freeze a closed period (#604): purges in-window delete markers and
9911
+ * records a `_period_freezes` companion, never mutating the chained
9912
+ * `_periods` record. Idempotent. Purges the LOCAL adapter only — a
9913
+ * synced target's markers survive there and re-import on pull (benign),
9914
+ * and purging re-opens the #589 resurrection window for a peer offline
9915
+ * since before the cutoff (see periods.ts's "Freeze" section).
9916
+ */
9917
+ async freezePeriod(name) {
9918
+ return this.periods.freezePeriod(name);
9919
+ }
9920
+ /**
9921
+ * Archive a closed period (#613): relocates its in-window records from the
9922
+ * hot store to the configured cold tier (via the routeStore's cold route),
9923
+ * recording a `_period_archives` companion + ledger entry, never mutating
9924
+ * the chained `_periods` record. Non-destructive (reads fall through to
9925
+ * cold) and idempotent. Requires a routeStore with a cold route.
9926
+ */
9927
+ async archivePeriod(name) {
9928
+ return this.periods.archivePeriod(name);
9929
+ }
9930
+ /**
9931
+ * Target-purge a closed+frozen period (#615): sweeps delete markers off the
9932
+ * vault's push-only sync targets (`backup`/`archive`), recording a
9933
+ * `_period_target_purges` companion + ledger entry, never mutating the chained
9934
+ * `_periods` record. `sync-peer` targets are skipped. Requires the period be
9935
+ * frozen first. Idempotent; a vault with no push-only targets writes no
9936
+ * companion and is re-runnable.
9937
+ */
9938
+ async purgePeriodTargets(name) {
9939
+ return this.periods.purgePeriodTargets(name);
9940
+ }
8852
9941
  /** @internal — called by the gate bus before put/delete. */
8853
9942
  async _assertTsWritable(existing, incoming) {
8854
9943
  return this.periods.assertTsWritable(existing, incoming);
@@ -8878,7 +9967,7 @@ var Vault = class {
8878
9967
  * @see docs/superpowers/specs/2026-05-22-schema-dump-design.md
8879
9968
  */
8880
9969
  async dumpSchema(opts = {}) {
8881
- const { dumpVaultSchema } = await import("./walk-3SB25HF2.js");
9970
+ const { dumpVaultSchema } = await import("./walk-76UCFXYV.js");
8882
9971
  return dumpVaultSchema(this, opts);
8883
9972
  }
8884
9973
  /**
@@ -9013,7 +10102,7 @@ var Vault = class {
9013
10102
  * @see https://github.com/vLannaAi/noy-db-docs/blob/main/content/docs/services/public-envelope.md
9014
10103
  */
9015
10104
  async getPublicEnvelope(opts = {}) {
9016
- const { readPublicEnvelope } = await import("./public-envelope-CBRIJORQ.js");
10105
+ const { readPublicEnvelope } = await import("./public-envelope-J5PLQOW2.js");
9017
10106
  return readPublicEnvelope(this.adapter, this.name, opts);
9018
10107
  }
9019
10108
  /**
@@ -9028,7 +10117,7 @@ var Vault = class {
9028
10117
  * both modes round-trip cleanly.
9029
10118
  */
9030
10119
  async dump() {
9031
- const { dumpVault } = await import("./backup-7PX6NDKT.js");
10120
+ const { dumpVault } = await import("./backup-ODXO7OGA.js");
9032
10121
  return dumpVault(this.backupContext());
9033
10122
  }
9034
10123
  /**
@@ -9080,7 +10169,7 @@ var Vault = class {
9080
10169
  * — there's no chain to verify against.
9081
10170
  */
9082
10171
  async load(backupJson) {
9083
- const { loadVault } = await import("./backup-7PX6NDKT.js");
10172
+ const { loadVault } = await import("./backup-ODXO7OGA.js");
9084
10173
  return loadVault(this.backupContext(), backupJson);
9085
10174
  }
9086
10175
  /**
@@ -9112,7 +10201,7 @@ var Vault = class {
9112
10201
  * way to detect tampering of an in-place vault.
9113
10202
  */
9114
10203
  async verifyBackupIntegrity() {
9115
- const { verifyBackupIntegrity } = await import("./backup-7PX6NDKT.js");
10204
+ const { verifyBackupIntegrity } = await import("./backup-ODXO7OGA.js");
9116
10205
  return verifyBackupIntegrity(this.backupContext());
9117
10206
  }
9118
10207
  /**
@@ -9218,7 +10307,7 @@ var Vault = class {
9218
10307
  const records = [];
9219
10308
  for (const id of ids) {
9220
10309
  const record = await coll.get(id, localeOpts);
9221
- if (record !== null) records.push(record);
10310
+ if (record !== null) records.push(exportRedact(coll, record));
9222
10311
  }
9223
10312
  const chunk = {
9224
10313
  collection: collectionName,
@@ -9237,7 +10326,7 @@ var Vault = class {
9237
10326
  collection: collectionName,
9238
10327
  schema,
9239
10328
  refs,
9240
- records: [record],
10329
+ records: [exportRedact(coll, record)],
9241
10330
  ...dictionaries !== void 0 ? { dictionaries } : {},
9242
10331
  ...ledgerHead ? { ledgerHead } : {}
9243
10332
  };
@@ -9330,7 +10419,7 @@ var Vault = class {
9330
10419
  * is the live validator object, not a serialization of it).
9331
10420
  */
9332
10421
  async exportJSON(opts = {}) {
9333
- const { exportVaultJSON } = await import("./backup-7PX6NDKT.js");
10422
+ const { exportVaultJSON } = await import("./backup-ODXO7OGA.js");
9334
10423
  return exportVaultJSON(this.backupContext(), opts);
9335
10424
  }
9336
10425
  };
@@ -10044,17 +11133,17 @@ function memoryStore() {
10044
11133
  }
10045
11134
 
10046
11135
  // src/with-party/session/strategy.ts
10047
- function notEnabled4(op) {
11136
+ function notEnabled3(op) {
10048
11137
  return new Error(
10049
11138
  `${op} requires the session strategy. Import \`{ withSession }\` from "@noy-db/hub/session" and pass it to \`createNoydb({ sessionStrategy: withSession() })\`.`
10050
11139
  );
10051
11140
  }
10052
11141
  var NO_SESSION = {
10053
11142
  validateSessionPolicy() {
10054
- throw notEnabled4("sessionPolicy");
11143
+ throw notEnabled3("sessionPolicy");
10055
11144
  },
10056
11145
  createEnforcer() {
10057
- throw notEnabled4("session policy enforcement");
11146
+ throw notEnabled3("session policy enforcement");
10058
11147
  },
10059
11148
  revokeAllSessions() {
10060
11149
  }
@@ -10958,6 +12047,7 @@ function createPlaintextKeyring(userId, debugPlaintext = false) {
10958
12047
  ...debugPlaintext ? { debugPlaintext } : {}
10959
12048
  };
10960
12049
  }
12050
+ var emptyPullResult = () => ({ pulled: 0, conflicts: [], errors: [] });
10961
12051
  var Noydb = class {
10962
12052
  options;
10963
12053
  emitter = new NoydbEventEmitter();
@@ -11202,7 +12292,7 @@ var Noydb = class {
11202
12292
  if (!facade) return;
11203
12293
  const ctx = { existing, vault: facade, userId: e.userId, role: e.role };
11204
12294
  await registry.runChecks(e.collection, incoming, ctx);
11205
- const { GuardExecutor } = await import("./executor-KZ4B3PRT.js");
12295
+ const { GuardExecutor } = await import("./executor-OUA2Y6YC.js");
11206
12296
  for (const g of guards) {
11207
12297
  await GuardExecutor.checkFrozenFields(g, e.docId, existing, incoming, e.computedFieldNames);
11208
12298
  }
@@ -11374,6 +12464,7 @@ var Noydb = class {
11374
12464
  } : void 0,
11375
12465
  onRegisterConflictResolver: syncEngine ? (resolverName, resolver) => syncEngine.registerConflictResolver(resolverName, resolver) : void 0,
11376
12466
  syncAdapter: targets.length > 0 ? targets[0].store : void 0,
12467
+ getPurgeableTargets: () => targets.filter((t) => t.role === "backup" || t.role === "archive").map((t) => ({ store: t.store, role: t.role, ...t.label !== void 0 ? { label: t.label } : {} })),
11377
12468
  historyConfig: this.options.history,
11378
12469
  ...this.options.blobStrategy !== void 0 ? { blobStrategy: this.options.blobStrategy } : {},
11379
12470
  ...this.options.objectStore !== void 0 ? { objectStore: this.options.objectStore } : {},
@@ -11422,6 +12513,8 @@ var Noydb = class {
11422
12513
  });
11423
12514
  this._forEachSyncEngine(name, (engine) => {
11424
12515
  engine.setCacheInvalidator((collection, id) => comp._invalidateSyncApplied(collection, id));
12516
+ engine.setGraphBatchController({ begin: () => comp._beginGraphBatch(), flush: () => comp._flushGraphBatch() });
12517
+ engine.setReservedLookupSource({ collections: () => comp._reservedLookupCollectionNames() });
11425
12518
  });
11426
12519
  await comp._initGuards(this.options.guardStrategies ?? []);
11427
12520
  await comp._initDerivations(this.options.derivationStrategies ?? []);
@@ -11894,9 +12987,13 @@ var Noydb = class {
11894
12987
  const engine = this.getSyncEngine(vault);
11895
12988
  return engine.push(options);
11896
12989
  }
11897
- /** Pull remote changes to local for a vault. */
12990
+ /**
12991
+ * Pull remote changes to local for a vault. A `backup`/`archive` primary is a
12992
+ * push-only sink and is never pulled from (#616) — returns an empty result.
12993
+ */
11898
12994
  async pull(vault, options) {
11899
12995
  const engine = this.getSyncEngine(vault);
12996
+ if (engine.role !== "sync-peer") return emptyPullResult();
11900
12997
  return engine.pull(options);
11901
12998
  }
11902
12999
  /**
@@ -11905,7 +13002,7 @@ var Noydb = class {
11905
13002
  */
11906
13003
  async sync(vault, options) {
11907
13004
  const primary = this.getSyncEngine(vault);
11908
- const result = await primary.sync(options);
13005
+ const result = primary.role === "sync-peer" ? await primary.sync(options) : { pull: emptyPullResult(), push: await primary.push(options?.push) };
11909
13006
  for (const [key, engine] of this.syncEngines) {
11910
13007
  if (key === vault) continue;
11911
13008
  if (!key.startsWith(`${vault}::`)) continue;
@@ -12462,7 +13559,7 @@ var Noydb = class {
12462
13559
  "setPublicEnvelope: the public-envelope feature is not enabled. Pass `publicEnvelope: true` (or a schema object) to `createNoydb`."
12463
13560
  );
12464
13561
  }
12465
- const { loadPublicEnvelope, savePublicEnvelope, resolveSchema, validatePublicEnvelopeInput } = await import("./public-envelope-CBRIJORQ.js");
13562
+ const { loadPublicEnvelope, savePublicEnvelope, resolveSchema, validatePublicEnvelopeInput } = await import("./public-envelope-J5PLQOW2.js");
12466
13563
  const schema = resolveSchema(this.options.publicEnvelope);
12467
13564
  validatePublicEnvelopeInput(input, schema);
12468
13565
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -12491,7 +13588,7 @@ var Noydb = class {
12491
13588
  * regardless).
12492
13589
  */
12493
13590
  async getPublicEnvelope(vault, opts = {}) {
12494
- const { readPublicEnvelope } = await import("./public-envelope-CBRIJORQ.js");
13591
+ const { readPublicEnvelope } = await import("./public-envelope-J5PLQOW2.js");
12495
13592
  return readPublicEnvelope(this.options.store, vault, opts);
12496
13593
  }
12497
13594
  // ─── Auth introspection ─────────────────────────────────────────
@@ -12682,9 +13779,9 @@ async function createNoydb(options) {
12682
13779
  throw new ValidationError("A secret (passphrase) or getKeyring callback is required when encryption is enabled");
12683
13780
  }
12684
13781
  if (!options.coordinationStrategy) options = { ...options, coordinationStrategy: await createDefaultCoordinationProvider(options.store) };
12685
- if (!options.userApiFactory) options = { ...options, userApiFactory: (await import("./api-Z66JMGOU.js")).createUserApi };
13782
+ if (!options.userApiFactory) options = { ...options, userApiFactory: (await import("./api-HV4PDUWF.js")).createUserApi };
12686
13783
  if (!options.policyFactory) {
12687
- const policyModule = await import("./policy-SXPLTQS4.js");
13784
+ const policyModule = await import("./policy-SDGRWVIL.js");
12688
13785
  options = { ...options, policyFactory: policyModule.createNoydbPolicy, policyCheckGateFn: policyModule.checkGate };
12689
13786
  }
12690
13787
  return new Noydb(options);
@@ -12701,6 +13798,9 @@ function normalizeSyncTargets(sync) {
12701
13798
  export {
12702
13799
  memoryStore,
12703
13800
  withArchive,
13801
+ buildLookupSnapshot,
13802
+ via,
13803
+ isViaFieldSpec,
12704
13804
  Collection,
12705
13805
  ELEVATION_AUDIT_COLLECTION,
12706
13806
  ElevatedHandle,
@@ -12719,4 +13819,4 @@ export {
12719
13819
  Noydb,
12720
13820
  createNoydb
12721
13821
  };
12722
- //# sourceMappingURL=chunk-5K7KZMXG.js.map
13822
+ //# sourceMappingURL=chunk-4C4WBS4V.js.map