@noy-db/hub 0.6.0-pre.9 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7079 -0
- package/codemods/0.6.0-pre.json +211 -0
- package/dist/api-6X2VLZ7J.js +20 -0
- package/dist/attestation/index.js +19 -19
- package/dist/{backup-KULFZ2T4.js → backup-VL4GRIEW.js} +36 -18
- package/dist/backup-VL4GRIEW.js.map +1 -0
- package/dist/blobs/index.js +22 -20
- package/dist/blobs/index.js.map +1 -1
- package/dist/broker/index.js +12 -12
- package/dist/cargo/index.js +35 -35
- package/dist/{chunk-KUDZVJKQ.js → chunk-22GR3O4J.js} +2 -2
- package/dist/{chunk-CCZJJ4I2.js → chunk-2CBFYXJX.js} +6 -6
- package/dist/{chunk-JJNOLSG5.js → chunk-2J5IT37K.js} +15 -16
- package/dist/chunk-2J5IT37K.js.map +1 -0
- package/dist/{chunk-PNKHAKQ7.js → chunk-2ZCWHCLF.js} +17 -10
- package/dist/chunk-2ZCWHCLF.js.map +1 -0
- package/dist/{chunk-2BMRJ6LQ.js → chunk-2ZT4INZ4.js} +8 -11
- package/dist/{chunk-2BMRJ6LQ.js.map → chunk-2ZT4INZ4.js.map} +1 -1
- package/dist/{chunk-FAXZQ5UF.js → chunk-323MN67Z.js} +2 -2
- package/dist/{chunk-VMCCGUQB.js → chunk-36CEG6CM.js} +2 -2
- package/dist/{chunk-PDLLCOGD.js → chunk-3OE2QNSM.js} +20 -17
- package/dist/chunk-3OE2QNSM.js.map +1 -0
- package/dist/{chunk-AHLJSVRL.js → chunk-3YGUHUJA.js} +5 -7
- package/dist/chunk-3YGUHUJA.js.map +1 -0
- package/dist/{chunk-ZJADGNYF.js → chunk-4AOERLTV.js} +1 -1
- package/dist/chunk-4AOERLTV.js.map +1 -0
- package/dist/{chunk-BRVW2GS5.js → chunk-4ATF4BLF.js} +87 -34
- package/dist/chunk-4ATF4BLF.js.map +1 -0
- package/dist/{chunk-747LRSOO.js → chunk-4K6BIMLF.js} +2 -2
- package/dist/{chunk-5O5HRRTV.js → chunk-5ATC6UT4.js} +7 -7
- package/dist/{chunk-5O5HRRTV.js.map → chunk-5ATC6UT4.js.map} +1 -1
- package/dist/{chunk-4GZ6XOVW.js → chunk-5F7BZAGV.js} +44 -42
- package/dist/chunk-5F7BZAGV.js.map +1 -0
- package/dist/{chunk-FNSMZQS4.js → chunk-5IHGJHWC.js} +2 -2
- package/dist/{chunk-4I72F5DF.js → chunk-5LZZQMIC.js} +11 -14
- package/dist/chunk-5LZZQMIC.js.map +1 -0
- package/dist/{chunk-HPLWFNG4.js → chunk-5O5ZSW7N.js} +2 -2
- package/dist/{chunk-CEJWMBXO.js → chunk-5OT4G5N5.js} +6 -2
- package/dist/chunk-5OT4G5N5.js.map +1 -0
- package/dist/{chunk-GAG7QOVO.js → chunk-64WHKBMP.js} +53 -18
- package/dist/chunk-64WHKBMP.js.map +1 -0
- package/dist/{chunk-7TSYBD3E.js → chunk-65JXA63J.js} +2 -2
- package/dist/{chunk-Z45YRGFO.js → chunk-6NR36UNB.js} +2 -2
- package/dist/{chunk-W77KZHCW.js → chunk-6RH7PRTL.js} +16 -17
- package/dist/chunk-6RH7PRTL.js.map +1 -0
- package/dist/{chunk-3JVXVJ7L.js → chunk-7OBIBPCV.js} +10 -10
- package/dist/{chunk-AR2JDFAE.js → chunk-7Q3RHOA5.js} +2 -2
- package/dist/chunk-7ZGPQOFV.js +190 -0
- package/dist/chunk-7ZGPQOFV.js.map +1 -0
- package/dist/{chunk-QULVCV3M.js → chunk-AJGGBXWA.js} +4 -4
- package/dist/{chunk-CADUYBQD.js → chunk-ATL3ZVXT.js} +2 -2
- package/dist/{chunk-DMQCLZ4Z.js → chunk-ATY3ZASK.js} +2 -2
- package/dist/{chunk-4HDYQL3J.js → chunk-B4CRUZPR.js} +2 -2
- package/dist/{chunk-RV3ARMPN.js → chunk-BMU2DS3S.js} +30 -21
- package/dist/chunk-BMU2DS3S.js.map +1 -0
- package/dist/{chunk-6SAFL5YE.js → chunk-BZ3WMOYX.js} +3 -3
- package/dist/{chunk-UHXCLPGC.js → chunk-C3XWJCG7.js} +2 -2
- package/dist/chunk-C3XWJCG7.js.map +1 -0
- package/dist/{chunk-HXGUIPQB.js → chunk-CKKN25P2.js} +3 -3
- package/dist/{chunk-3URQC7EH.js → chunk-CXPOG3ZY.js} +355 -132
- package/dist/chunk-CXPOG3ZY.js.map +1 -0
- package/dist/{chunk-3FSX6LDG.js → chunk-D2YFQ7XG.js} +18 -3
- package/dist/{chunk-3FSX6LDG.js.map → chunk-D2YFQ7XG.js.map} +1 -1
- package/dist/{chunk-52JPHN5N.js → chunk-D6SXRMZZ.js} +27 -149
- package/dist/chunk-D6SXRMZZ.js.map +1 -0
- package/dist/{chunk-2DHSSLOD.js → chunk-DECNV4QK.js} +13 -15
- package/dist/chunk-DECNV4QK.js.map +1 -0
- package/dist/{chunk-6BU3L4UE.js → chunk-DNKVKO7K.js} +7 -10
- package/dist/chunk-DNKVKO7K.js.map +1 -0
- package/dist/{chunk-6HFM7EIT.js → chunk-EHRIDNZX.js} +2 -2
- package/dist/{chunk-3737A6EA.js → chunk-EHW7NLGD.js} +127 -31
- package/dist/chunk-EHW7NLGD.js.map +1 -0
- package/dist/{chunk-BEFVYAR5.js → chunk-EQOTJL76.js} +4 -4
- package/dist/chunk-EQOTJL76.js.map +1 -0
- package/dist/{chunk-E6427JU2.js → chunk-EV2TDHQZ.js} +19 -3
- package/dist/chunk-EV2TDHQZ.js.map +1 -0
- package/dist/{chunk-WFUIE565.js → chunk-FNYCT27B.js} +17 -14
- package/dist/chunk-FNYCT27B.js.map +1 -0
- package/dist/{chunk-RTAMLDWP.js → chunk-FU5VYEYK.js} +2 -2
- package/dist/chunk-FX3UIDCS.js +66 -0
- package/dist/chunk-FX3UIDCS.js.map +1 -0
- package/dist/{chunk-7F4DBNSP.js → chunk-GANFIM2M.js} +135 -18
- package/dist/chunk-GANFIM2M.js.map +1 -0
- package/dist/{chunk-4VUPLMZ6.js → chunk-GCU42IG7.js} +66 -71
- package/dist/chunk-GCU42IG7.js.map +1 -0
- package/dist/{chunk-RXYYFSBB.js → chunk-GKTW3KLV.js} +4 -4
- package/dist/chunk-HK67PX4J.js +27 -0
- package/dist/chunk-HK67PX4J.js.map +1 -0
- package/dist/{chunk-OF3L2SD6.js → chunk-HOPNIR5R.js} +2 -2
- package/dist/{chunk-WLZ47GSL.js → chunk-HSJBJIB2.js} +12 -10
- package/dist/{chunk-WLZ47GSL.js.map → chunk-HSJBJIB2.js.map} +1 -1
- package/dist/{chunk-ADW22QSB.js → chunk-HWI7GHJZ.js} +4 -4
- package/dist/{chunk-GQ3BX5EA.js → chunk-I3ISCYT7.js} +2 -2
- package/dist/{chunk-YJ6IONGA.js → chunk-IWKCJCLP.js} +11 -3
- package/dist/{chunk-YJ6IONGA.js.map → chunk-IWKCJCLP.js.map} +1 -1
- package/dist/{chunk-NJDQ4YSV.js → chunk-IWQPYT6S.js} +4 -4
- package/dist/{chunk-JRKZTEZZ.js → chunk-J6I5VR6P.js} +444 -73
- package/dist/chunk-J6I5VR6P.js.map +1 -0
- package/dist/{chunk-2Z3NCLNX.js → chunk-JHYKSMVX.js} +7 -7
- package/dist/{chunk-6FJWSWFT.js → chunk-JIHWBKMQ.js} +3 -3
- package/dist/{chunk-YAWK6AWF.js → chunk-JKATP6OE.js} +2 -2
- package/dist/{chunk-PL2GHSCM.js → chunk-JMJSCI7U.js} +157 -90
- package/dist/chunk-JMJSCI7U.js.map +1 -0
- package/dist/{chunk-Q6TEJXBX.js → chunk-JXHNUYQO.js} +31 -49
- package/dist/chunk-JXHNUYQO.js.map +1 -0
- package/dist/{chunk-2GAJGUNM.js → chunk-JYPRZWCZ.js} +343 -229
- package/dist/chunk-JYPRZWCZ.js.map +1 -0
- package/dist/{chunk-5ZKS6ZE2.js → chunk-K6CPIK3R.js} +4 -4
- package/dist/chunk-K6CPIK3R.js.map +1 -0
- package/dist/{chunk-CDB2HVQW.js → chunk-KM6ZXG7U.js} +14 -15
- package/dist/chunk-KM6ZXG7U.js.map +1 -0
- package/dist/{chunk-DG27IUVI.js → chunk-LC6NE4IQ.js} +2 -2
- package/dist/{chunk-WASHZV34.js → chunk-LIR2DJWP.js} +63 -25
- package/dist/chunk-LIR2DJWP.js.map +1 -0
- package/dist/{chunk-VZ3XFUQ7.js → chunk-LUYPYCHY.js} +16 -22
- package/dist/chunk-LUYPYCHY.js.map +1 -0
- package/dist/{chunk-G6S5VVHP.js → chunk-M7WBE3OR.js} +10 -10
- package/dist/chunk-M7WBE3OR.js.map +1 -0
- package/dist/{chunk-VJFMXYKP.js → chunk-MPKP6L5E.js} +2 -2
- package/dist/{chunk-IGJCYUT5.js → chunk-NSITFOCJ.js} +1 -1
- package/dist/chunk-NSITFOCJ.js.map +1 -0
- package/dist/{chunk-FUHKW252.js → chunk-OCJ3JA4O.js} +2 -2
- package/dist/{chunk-VCETL77O.js → chunk-ONPVHX2Y.js} +36 -7
- package/dist/chunk-ONPVHX2Y.js.map +1 -0
- package/dist/{chunk-WLYIROCJ.js → chunk-OQMCFHUX.js} +2 -2
- package/dist/{chunk-JRCYZEHU.js → chunk-OR2FYRYR.js} +2 -2
- package/dist/{chunk-HNHCQ556.js → chunk-PFWE7FJG.js} +2 -2
- package/dist/{chunk-NPP5R7FF.js → chunk-PJX72ICF.js} +22 -18
- package/dist/chunk-PJX72ICF.js.map +1 -0
- package/dist/{chunk-U5O6GIWZ.js → chunk-POFYIGQ2.js} +3 -3
- package/dist/{chunk-35EBKWQU.js → chunk-QH47SQLD.js} +4 -4
- package/dist/{chunk-FZ6NYJ7L.js → chunk-QQRRKO7W.js} +11 -8
- package/dist/chunk-QQRRKO7W.js.map +1 -0
- package/dist/{chunk-HVQGCKY4.js → chunk-QVXZFJBU.js} +2 -2
- package/dist/{chunk-FMQJZH4R.js → chunk-RDB66EKD.js} +6 -6
- package/dist/{chunk-X56RTUK3.js → chunk-RJZQ2PVD.js} +3 -3
- package/dist/{chunk-SUFG5E73.js → chunk-RPRASGEY.js} +2 -2
- package/dist/{chunk-XWX7GL56.js → chunk-S2I5MYSA.js} +7 -10
- package/dist/chunk-S2I5MYSA.js.map +1 -0
- package/dist/{chunk-FFDNKRCY.js → chunk-S6PJ6ZKE.js} +2 -2
- package/dist/chunk-S6PJ6ZKE.js.map +1 -0
- package/dist/{chunk-JIUVJ4M7.js → chunk-S7PM7Z4K.js} +3 -3
- package/dist/{chunk-55LKVALY.js → chunk-T7IR4N2S.js} +1 -3
- package/dist/chunk-T7IR4N2S.js.map +1 -0
- package/dist/{chunk-N35NC4JH.js → chunk-TKBB62DN.js} +2 -2
- package/dist/{chunk-YPYHVOXP.js → chunk-UJ4Z67UQ.js} +2 -2
- package/dist/chunk-UJ4Z67UQ.js.map +1 -0
- package/dist/{chunk-7MNBICB5.js → chunk-W3HAIN6U.js} +16 -14
- package/dist/chunk-W3HAIN6U.js.map +1 -0
- package/dist/{chunk-4UVDI4XE.js → chunk-WPZ5FVAS.js} +4 -4
- package/dist/{chunk-MSWYQLMQ.js → chunk-XINT4VJA.js} +2 -2
- package/dist/{chunk-7G6VDWXB.js → chunk-XPEPMK4Y.js} +3 -3
- package/dist/chunk-XPEPMK4Y.js.map +1 -0
- package/dist/{chunk-W3TVWF6N.js → chunk-YCGLAANM.js} +2 -2
- package/dist/{chunk-O2ERS4UJ.js → chunk-YTUFV7U7.js} +80 -51
- package/dist/chunk-YTUFV7U7.js.map +1 -0
- package/dist/{chunk-FDX7OWWI.js → chunk-YYV53EHO.js} +2 -2
- package/dist/{chunk-HNGSV2YC.js → chunk-Z3BZXPRO.js} +67 -10
- package/dist/chunk-Z3BZXPRO.js.map +1 -0
- package/dist/{chunk-6NVPFKN3.js → chunk-ZIMQDXTQ.js} +92 -35
- package/dist/chunk-ZIMQDXTQ.js.map +1 -0
- package/dist/{chunk-5JY5TSVY.js → chunk-ZJUZOJVW.js} +25 -31
- package/dist/chunk-ZJUZOJVW.js.map +1 -0
- package/dist/{chunk-3IXIU6FP.js → chunk-ZY6P7M52.js} +7 -10
- package/dist/chunk-ZY6P7M52.js.map +1 -0
- package/dist/{chunk-BWVKEOPE.js → chunk-ZYZJLYNS.js} +7 -11
- package/dist/chunk-ZYZJLYNS.js.map +1 -0
- package/dist/classified/index.js +5 -5
- package/dist/{classified-marker-2KJAXRRO.js → classified-marker-AZDSDG6V.js} +3 -3
- package/dist/collection-facade-WSUAYFJ4.js +45 -0
- package/dist/{computed-6G5M3UPH.js → computed-VG2PCM4L.js} +3 -3
- package/dist/consent/index.js +10 -10
- package/dist/cover/index.js +10 -3
- package/dist/custody/index.js +12 -11
- package/dist/{dead-filter-DCB62VWW.js → dead-filter-JNQNB3TA.js} +2 -2
- package/dist/debug/index.js +1 -1
- package/dist/{delegation-MO7YZYJS.js → delegation-5F36DRDH.js} +24 -23
- package/dist/delegation-5F36DRDH.js.map +1 -0
- package/dist/derivations/index.js +12 -12
- package/dist/derive-MPOGZ4NR.js +22 -0
- package/dist/directory/index.js +10 -10
- package/dist/{dispatch-MJ6SHB3Y.js → dispatch-3ZA733EY.js} +18 -18
- package/dist/{dispatch-FZEAWKVM.js → dispatch-TNM5CRLZ.js} +9 -6
- package/dist/dispatch-TNM5CRLZ.js.map +1 -0
- package/dist/{enclave-JU4YIEPB.js → enclave-Y2ALNDRD.js} +31 -9
- package/dist/executor-BBEEUFO3.js +9 -0
- package/dist/executor-G6G3YEWR.js +9 -0
- package/dist/executor-ZUTUMTPA.js +29 -0
- package/dist/export-accessible-S3KEB763.js +23 -0
- package/dist/extract-partition-KVCTSUVE.js +38 -0
- package/dist/{fanout-sidecar-NNAJQXMX.js → fanout-sidecar-3QZU7ZZX.js} +18 -27
- package/dist/fanout-sidecar-3QZU7ZZX.js.map +1 -0
- package/dist/find-V4FN5ITT.js +11 -0
- package/dist/forget/index.js +9 -9
- package/dist/guards/index.js +7 -7
- package/dist/history/index.js +23 -20
- package/dist/history/index.js.map +1 -1
- package/dist/i18n/index.js +14 -14
- package/dist/index.d.ts +30 -13
- package/dist/index.js +180 -188
- package/dist/index.js.map +1 -1
- package/dist/indexing/index.js +4 -4
- package/dist/introspection/index.js +11 -11
- package/dist/issue-5CFBWBRX.js +19 -0
- package/dist/kernel/collection.d.ts +8 -2
- package/dist/kernel/constants.d.ts +36 -0
- package/dist/kernel/enclave/crypto.d.ts +52 -4
- package/dist/kernel/enclave/index.d.ts +10 -3
- package/dist/kernel/enclave/record-aad.d.ts +131 -0
- package/dist/kernel/enclave/record-envelope.d.ts +76 -0
- package/dist/kernel/enclave/record-keys/envelope-body.d.ts +34 -2
- package/dist/kernel/enclave/record-keys/lifecycle.d.ts +22 -5
- package/dist/kernel/enclave/record-keys/record-codec.d.ts +47 -14
- package/dist/kernel/enclave/record-keys/rekey-blob.d.ts +88 -0
- package/dist/kernel/enclave/record-keys/rekey.d.ts +91 -0
- package/dist/kernel/enclave/record-keys/tombstone.d.ts +3 -2
- package/dist/kernel/errors.d.ts +132 -12
- package/dist/kernel/merge-authority.d.ts +25 -0
- package/dist/kernel/noydb.d.ts +27 -4
- package/dist/kernel/query/builder.d.ts +51 -4
- package/dist/kernel/query/join.d.ts +27 -0
- package/dist/kernel/query/live.d.ts +31 -9
- package/dist/kernel/query/predicate.d.ts +9 -0
- package/dist/kernel/refs.d.ts +3 -1
- package/dist/kernel/sync-policy.d.ts +0 -2
- package/dist/kernel/tier-visibility.d.ts +34 -0
- package/dist/kernel/types.d.ts +111 -13
- package/dist/kernel/vault.d.ts +6 -6
- package/dist/kernel/via/index.d.ts +11 -2
- package/dist/kernel/via/reconcile.d.ts +12 -0
- package/dist/{ledger-AETNF4OM.js → ledger-MQIIOTZB.js} +11 -11
- package/dist/liberate-LW7OKVBH.js +25 -0
- package/dist/{link-set-E5HNY67I.js → link-set-5PJBBR63.js} +11 -11
- package/dist/{managed-secret-CWQ6PND5.js → managed-secret-ETDG6AAE.js} +11 -3
- package/dist/materialized-views/index.js +24 -24
- package/dist/migrate-cek-V3VDRV24.js +19 -0
- package/dist/migrate-cek-V3VDRV24.js.map +1 -0
- package/dist/money/index.js +5 -5
- package/dist/noydb-LFDRMYZ5.js +85 -0
- package/dist/overlay-views/index.js +4 -4
- package/dist/periods/index.js +12 -12
- package/dist/pod/index.js +79 -89
- package/dist/pod-handle-AZ3CXY5Y.js +41 -0
- package/dist/pod-handle-AZ3CXY5Y.js.map +1 -0
- package/dist/policy/index.js +11 -4
- package/dist/port/to/index.d.ts +4 -4
- package/dist/port/to/locator.d.ts +54 -8
- package/dist/port/with/i18n-strategy.d.ts +4 -1
- package/dist/port/with/lookup-strategy.d.ts +4 -1
- package/dist/port/with/merge-authority.d.ts +52 -0
- package/dist/port/with/service-bus.d.ts +0 -4
- package/dist/port/with/strategies.d.ts +2 -0
- package/dist/port/with/team-strategy.d.ts +5 -1
- package/dist/portability/index.js +8 -8
- package/dist/{post-register-2XSSMGEQ.js → post-register-QCJYRMIK.js} +6 -6
- package/dist/query/index.js +6 -6
- package/dist/reduce/index.js +6 -6
- package/dist/register-JBOEHTVX.js +32 -0
- package/dist/registry-5DYHQFJS.js +9 -0
- package/dist/registry-EANV44W2.js +9 -0
- package/dist/registry-J25H6VB4.js +19 -0
- package/dist/registry-R23SQUQH.js +18 -0
- package/dist/request-withdrawal-KQ4W4UT5.js +31 -0
- package/dist/{reveal-BW36KCOQ.js → reveal-W7BKZSUP.js} +6 -6
- package/dist/revoke-H3VSTAIE.js +24 -0
- package/dist/satellites/index.js +1 -1
- package/dist/schema-update/index.js +2 -2
- package/dist/sealed-record/index.js +20 -13
- package/dist/sealed-record/index.js.map +1 -1
- package/dist/search/index.js +4 -4
- package/dist/{seed-GDV5PT4R.js → seed-55FPOMYP.js} +20 -22
- package/dist/seed-55FPOMYP.js.map +1 -0
- package/dist/sequence/index.js +9 -9
- package/dist/session/index.js +10 -10
- package/dist/shadow/index.js +2 -2
- package/dist/share-link/index.js +1 -1
- package/dist/signer-53AQ3BU2.js +25 -0
- package/dist/snapshots/index.js +10 -10
- package/dist/snapshots/index.js.map +1 -1
- package/dist/{stale-ARHFVL5U.js → stale-TH7SSVRV.js} +10 -10
- package/dist/storage-WSF7ZKTL.js +23 -0
- package/dist/{store-coordination-provider-GFCUOBR6.js → store-coordination-provider-R27VRD6K.js} +18 -12
- package/dist/store-coordination-provider-R27VRD6K.js.map +1 -0
- package/dist/sync/index.js +11 -11
- package/dist/team/index.js +15 -14
- package/dist/tiers/index.js +11 -10
- package/dist/to/index.js +17 -9
- package/dist/to/index.js.map +1 -1
- package/dist/transactions/index.js +4 -4
- package/dist/{ulid-PTAIMDG4.js → ulid-VQW6ERQN.js} +2 -2
- package/dist/util/index.js +1 -1
- package/dist/vault-head/index.js +115 -0
- package/dist/vault-head/index.js.map +1 -0
- package/dist/{verify-RO5RTZOJ.js → verify-M5LQH43C.js} +5 -5
- package/dist/via/blob/index.d.ts +3 -0
- package/dist/via/lookup/handle.d.ts +4 -1
- package/dist/walk-NABCHDZN.js +21 -0
- package/dist/with-audit/periods/periods.d.ts +1 -1
- package/dist/with-audit/periods/strategy.d.ts +1 -1
- package/dist/with-audit/sealed-record/index.d.ts +13 -0
- package/dist/with-audit/tiers/index.d.ts +17 -2
- package/dist/with-cargo/adopt-partition.d.ts +1 -1
- package/dist/with-cargo/extract-partition.d.ts +1 -1
- package/dist/with-cargo/index.d.ts +1 -1
- package/dist/with-commit/history/history.d.ts +26 -0
- package/dist/with-commit/history/index.d.ts +2 -1
- package/dist/with-commit/history/ledger/store.d.ts +7 -1
- package/dist/with-commit/history/strategy.d.ts +11 -1
- package/dist/with-commit/numbering/index.d.ts +0 -5
- package/dist/with-commit/vault-head/active.d.ts +2 -0
- package/dist/with-commit/vault-head/head.d.ts +28 -0
- package/dist/with-commit/vault-head/index.d.ts +10 -0
- package/dist/with-commit/vault-head/strategy.d.ts +127 -0
- package/dist/with-commit/vault-head/verify.d.ts +90 -0
- package/dist/with-formula/materialized-views/dependency-analyzer.d.ts +4 -0
- package/dist/with-formula/materialized-views/projection-legs.d.ts +59 -0
- package/dist/with-formula/materialized-views/registry.d.ts +50 -0
- package/dist/with-formula/materialized-views/types.d.ts +68 -10
- package/dist/with-party/directory/cover/storage.d.ts +0 -8
- package/dist/with-party/directory/storage.d.ts +0 -11
- package/dist/with-party/directory/visibility.d.ts +0 -21
- package/dist/with-party/policy/storage.d.ts +0 -10
- package/dist/with-party/team/index.d.ts +1 -1
- package/dist/with-party/team/keyring.d.ts +139 -2
- package/dist/with-party/team/noydb-facade.d.ts +5 -1
- package/dist/with-party/team/roster-tag.d.ts +101 -0
- package/dist/with-party/team/tiers.d.ts +2 -1
- package/dist/with-pod/format.d.ts +10 -12
- package/dist/with-pod/index.d.ts +10 -10
- package/dist/with-pod/open.d.ts +2 -2
- package/dist/with-pod/pod-handle.d.ts +19 -0
- package/dist/with-pod/pod-store.d.ts +0 -8
- package/dist/with-pod/{bundle.d.ts → pod.d.ts} +11 -19
- package/dist/with-pod/ulid.d.ts +1 -1
- package/dist/with-shape/blobs/blob-compaction.d.ts +13 -0
- package/dist/with-shape/blobs/blob-set.d.ts +83 -11
- package/dist/with-shape/blobs/orphan-report.d.ts +62 -0
- package/dist/with-sync/engine.d.ts +54 -5
- package/dist/with-sync/strategy.d.ts +3 -0
- package/dist/withdraw-accessible-VCINIG6V.js +28 -0
- package/package.json +10 -4
- package/dist/api-RXUMEKT3.js +0 -20
- package/dist/backup-KULFZ2T4.js.map +0 -1
- package/dist/bundle-handle-GFNHT52M.js +0 -36
- package/dist/bundle-handle-GFNHT52M.js.map +0 -1
- package/dist/chunk-2DHSSLOD.js.map +0 -1
- package/dist/chunk-2GAJGUNM.js.map +0 -1
- package/dist/chunk-3737A6EA.js.map +0 -1
- package/dist/chunk-3IXIU6FP.js.map +0 -1
- package/dist/chunk-3URQC7EH.js.map +0 -1
- package/dist/chunk-4GZ6XOVW.js.map +0 -1
- package/dist/chunk-4I72F5DF.js.map +0 -1
- package/dist/chunk-4VUPLMZ6.js.map +0 -1
- package/dist/chunk-52JPHN5N.js.map +0 -1
- package/dist/chunk-55LKVALY.js.map +0 -1
- package/dist/chunk-5JY5TSVY.js.map +0 -1
- package/dist/chunk-5ZKS6ZE2.js.map +0 -1
- package/dist/chunk-6BU3L4UE.js.map +0 -1
- package/dist/chunk-6NVPFKN3.js.map +0 -1
- package/dist/chunk-7F4DBNSP.js.map +0 -1
- package/dist/chunk-7G6VDWXB.js.map +0 -1
- package/dist/chunk-7H73RIE3.js +0 -45
- package/dist/chunk-7H73RIE3.js.map +0 -1
- package/dist/chunk-7MNBICB5.js.map +0 -1
- package/dist/chunk-AFHQATY4.js +0 -47
- package/dist/chunk-AFHQATY4.js.map +0 -1
- package/dist/chunk-AHLJSVRL.js.map +0 -1
- package/dist/chunk-BEFVYAR5.js.map +0 -1
- package/dist/chunk-BRVW2GS5.js.map +0 -1
- package/dist/chunk-BWVKEOPE.js.map +0 -1
- package/dist/chunk-CDB2HVQW.js.map +0 -1
- package/dist/chunk-CEJWMBXO.js.map +0 -1
- package/dist/chunk-E6427JU2.js.map +0 -1
- package/dist/chunk-FFDNKRCY.js.map +0 -1
- package/dist/chunk-FZ6NYJ7L.js.map +0 -1
- package/dist/chunk-G6S5VVHP.js.map +0 -1
- package/dist/chunk-GAG7QOVO.js.map +0 -1
- package/dist/chunk-HNGSV2YC.js.map +0 -1
- package/dist/chunk-IGJCYUT5.js.map +0 -1
- package/dist/chunk-JJNOLSG5.js.map +0 -1
- package/dist/chunk-JRKZTEZZ.js.map +0 -1
- package/dist/chunk-NPP5R7FF.js.map +0 -1
- package/dist/chunk-O2ERS4UJ.js.map +0 -1
- package/dist/chunk-PDLLCOGD.js.map +0 -1
- package/dist/chunk-PL2GHSCM.js.map +0 -1
- package/dist/chunk-PNKHAKQ7.js.map +0 -1
- package/dist/chunk-Q6TEJXBX.js.map +0 -1
- package/dist/chunk-RV3ARMPN.js.map +0 -1
- package/dist/chunk-UHXCLPGC.js.map +0 -1
- package/dist/chunk-VCETL77O.js.map +0 -1
- package/dist/chunk-VZ3XFUQ7.js.map +0 -1
- package/dist/chunk-W77KZHCW.js.map +0 -1
- package/dist/chunk-WASHZV34.js.map +0 -1
- package/dist/chunk-WFUIE565.js.map +0 -1
- package/dist/chunk-XWX7GL56.js.map +0 -1
- package/dist/chunk-YPYHVOXP.js.map +0 -1
- package/dist/chunk-ZJADGNYF.js.map +0 -1
- package/dist/collection-facade-QEJIM6BC.js +0 -45
- package/dist/delegation-MO7YZYJS.js.map +0 -1
- package/dist/derive-IUA6JFLB.js +0 -22
- package/dist/dispatch-FZEAWKVM.js.map +0 -1
- package/dist/executor-CYJQLKKI.js +0 -9
- package/dist/executor-S4QIKEHW.js +0 -9
- package/dist/executor-YIQB2S6N.js +0 -29
- package/dist/export-accessible-4S36PQNR.js +0 -23
- package/dist/extract-partition-IO7FDZ2P.js +0 -38
- package/dist/fanout-sidecar-NNAJQXMX.js.map +0 -1
- package/dist/find-RBBYKXI7.js +0 -11
- package/dist/issue-RIVHSY2S.js +0 -19
- package/dist/liberate-GFN4C7RR.js +0 -24
- package/dist/noydb-ASEXNJS6.js +0 -83
- package/dist/register-4EFJCVYD.js +0 -32
- package/dist/registry-4SDHHOJ7.js +0 -18
- package/dist/registry-CZVINVQH.js +0 -9
- package/dist/registry-IKUR2MXD.js +0 -9
- package/dist/registry-OYTFGWVK.js +0 -19
- package/dist/request-withdrawal-KGXGDPVQ.js +0 -31
- package/dist/revoke-JFN773KF.js +0 -24
- package/dist/seed-GDV5PT4R.js.map +0 -1
- package/dist/signer-TA55QB6R.js +0 -25
- package/dist/storage-V7MTFAQJ.js +0 -23
- package/dist/store-coordination-provider-GFCUOBR6.js.map +0 -1
- package/dist/walk-3ZECBFDA.js +0 -21
- package/dist/with-pod/bundle-handle.d.ts +0 -9
- package/dist/withdraw-accessible-7PG4DYQK.js +0 -28
- /package/dist/{api-RXUMEKT3.js.map → api-6X2VLZ7J.js.map} +0 -0
- /package/dist/{chunk-KUDZVJKQ.js.map → chunk-22GR3O4J.js.map} +0 -0
- /package/dist/{chunk-CCZJJ4I2.js.map → chunk-2CBFYXJX.js.map} +0 -0
- /package/dist/{chunk-FAXZQ5UF.js.map → chunk-323MN67Z.js.map} +0 -0
- /package/dist/{chunk-VMCCGUQB.js.map → chunk-36CEG6CM.js.map} +0 -0
- /package/dist/{chunk-747LRSOO.js.map → chunk-4K6BIMLF.js.map} +0 -0
- /package/dist/{chunk-FNSMZQS4.js.map → chunk-5IHGJHWC.js.map} +0 -0
- /package/dist/{chunk-HPLWFNG4.js.map → chunk-5O5ZSW7N.js.map} +0 -0
- /package/dist/{chunk-7TSYBD3E.js.map → chunk-65JXA63J.js.map} +0 -0
- /package/dist/{chunk-Z45YRGFO.js.map → chunk-6NR36UNB.js.map} +0 -0
- /package/dist/{chunk-3JVXVJ7L.js.map → chunk-7OBIBPCV.js.map} +0 -0
- /package/dist/{chunk-AR2JDFAE.js.map → chunk-7Q3RHOA5.js.map} +0 -0
- /package/dist/{chunk-QULVCV3M.js.map → chunk-AJGGBXWA.js.map} +0 -0
- /package/dist/{chunk-CADUYBQD.js.map → chunk-ATL3ZVXT.js.map} +0 -0
- /package/dist/{chunk-DMQCLZ4Z.js.map → chunk-ATY3ZASK.js.map} +0 -0
- /package/dist/{chunk-4HDYQL3J.js.map → chunk-B4CRUZPR.js.map} +0 -0
- /package/dist/{chunk-6SAFL5YE.js.map → chunk-BZ3WMOYX.js.map} +0 -0
- /package/dist/{chunk-HXGUIPQB.js.map → chunk-CKKN25P2.js.map} +0 -0
- /package/dist/{chunk-6HFM7EIT.js.map → chunk-EHRIDNZX.js.map} +0 -0
- /package/dist/{chunk-RTAMLDWP.js.map → chunk-FU5VYEYK.js.map} +0 -0
- /package/dist/{chunk-RXYYFSBB.js.map → chunk-GKTW3KLV.js.map} +0 -0
- /package/dist/{chunk-OF3L2SD6.js.map → chunk-HOPNIR5R.js.map} +0 -0
- /package/dist/{chunk-ADW22QSB.js.map → chunk-HWI7GHJZ.js.map} +0 -0
- /package/dist/{chunk-GQ3BX5EA.js.map → chunk-I3ISCYT7.js.map} +0 -0
- /package/dist/{chunk-NJDQ4YSV.js.map → chunk-IWQPYT6S.js.map} +0 -0
- /package/dist/{chunk-2Z3NCLNX.js.map → chunk-JHYKSMVX.js.map} +0 -0
- /package/dist/{chunk-6FJWSWFT.js.map → chunk-JIHWBKMQ.js.map} +0 -0
- /package/dist/{chunk-YAWK6AWF.js.map → chunk-JKATP6OE.js.map} +0 -0
- /package/dist/{chunk-DG27IUVI.js.map → chunk-LC6NE4IQ.js.map} +0 -0
- /package/dist/{chunk-VJFMXYKP.js.map → chunk-MPKP6L5E.js.map} +0 -0
- /package/dist/{chunk-FUHKW252.js.map → chunk-OCJ3JA4O.js.map} +0 -0
- /package/dist/{chunk-WLYIROCJ.js.map → chunk-OQMCFHUX.js.map} +0 -0
- /package/dist/{chunk-JRCYZEHU.js.map → chunk-OR2FYRYR.js.map} +0 -0
- /package/dist/{chunk-HNHCQ556.js.map → chunk-PFWE7FJG.js.map} +0 -0
- /package/dist/{chunk-U5O6GIWZ.js.map → chunk-POFYIGQ2.js.map} +0 -0
- /package/dist/{chunk-35EBKWQU.js.map → chunk-QH47SQLD.js.map} +0 -0
- /package/dist/{chunk-HVQGCKY4.js.map → chunk-QVXZFJBU.js.map} +0 -0
- /package/dist/{chunk-FMQJZH4R.js.map → chunk-RDB66EKD.js.map} +0 -0
- /package/dist/{chunk-X56RTUK3.js.map → chunk-RJZQ2PVD.js.map} +0 -0
- /package/dist/{chunk-SUFG5E73.js.map → chunk-RPRASGEY.js.map} +0 -0
- /package/dist/{chunk-JIUVJ4M7.js.map → chunk-S7PM7Z4K.js.map} +0 -0
- /package/dist/{chunk-N35NC4JH.js.map → chunk-TKBB62DN.js.map} +0 -0
- /package/dist/{chunk-4UVDI4XE.js.map → chunk-WPZ5FVAS.js.map} +0 -0
- /package/dist/{chunk-MSWYQLMQ.js.map → chunk-XINT4VJA.js.map} +0 -0
- /package/dist/{chunk-W3TVWF6N.js.map → chunk-YCGLAANM.js.map} +0 -0
- /package/dist/{chunk-FDX7OWWI.js.map → chunk-YYV53EHO.js.map} +0 -0
- /package/dist/{classified-marker-2KJAXRRO.js.map → classified-marker-AZDSDG6V.js.map} +0 -0
- /package/dist/{collection-facade-QEJIM6BC.js.map → collection-facade-WSUAYFJ4.js.map} +0 -0
- /package/dist/{computed-6G5M3UPH.js.map → computed-VG2PCM4L.js.map} +0 -0
- /package/dist/{dead-filter-DCB62VWW.js.map → dead-filter-JNQNB3TA.js.map} +0 -0
- /package/dist/{derive-IUA6JFLB.js.map → derive-MPOGZ4NR.js.map} +0 -0
- /package/dist/{dispatch-MJ6SHB3Y.js.map → dispatch-3ZA733EY.js.map} +0 -0
- /package/dist/{enclave-JU4YIEPB.js.map → enclave-Y2ALNDRD.js.map} +0 -0
- /package/dist/{executor-CYJQLKKI.js.map → executor-BBEEUFO3.js.map} +0 -0
- /package/dist/{executor-S4QIKEHW.js.map → executor-G6G3YEWR.js.map} +0 -0
- /package/dist/{executor-YIQB2S6N.js.map → executor-ZUTUMTPA.js.map} +0 -0
- /package/dist/{export-accessible-4S36PQNR.js.map → export-accessible-S3KEB763.js.map} +0 -0
- /package/dist/{extract-partition-IO7FDZ2P.js.map → extract-partition-KVCTSUVE.js.map} +0 -0
- /package/dist/{find-RBBYKXI7.js.map → find-V4FN5ITT.js.map} +0 -0
- /package/dist/{issue-RIVHSY2S.js.map → issue-5CFBWBRX.js.map} +0 -0
- /package/dist/{ledger-AETNF4OM.js.map → ledger-MQIIOTZB.js.map} +0 -0
- /package/dist/{liberate-GFN4C7RR.js.map → liberate-LW7OKVBH.js.map} +0 -0
- /package/dist/{link-set-E5HNY67I.js.map → link-set-5PJBBR63.js.map} +0 -0
- /package/dist/{managed-secret-CWQ6PND5.js.map → managed-secret-ETDG6AAE.js.map} +0 -0
- /package/dist/{noydb-ASEXNJS6.js.map → noydb-LFDRMYZ5.js.map} +0 -0
- /package/dist/{post-register-2XSSMGEQ.js.map → post-register-QCJYRMIK.js.map} +0 -0
- /package/dist/{register-4EFJCVYD.js.map → register-JBOEHTVX.js.map} +0 -0
- /package/dist/{registry-4SDHHOJ7.js.map → registry-5DYHQFJS.js.map} +0 -0
- /package/dist/{registry-CZVINVQH.js.map → registry-EANV44W2.js.map} +0 -0
- /package/dist/{registry-IKUR2MXD.js.map → registry-J25H6VB4.js.map} +0 -0
- /package/dist/{registry-OYTFGWVK.js.map → registry-R23SQUQH.js.map} +0 -0
- /package/dist/{request-withdrawal-KGXGDPVQ.js.map → request-withdrawal-KQ4W4UT5.js.map} +0 -0
- /package/dist/{reveal-BW36KCOQ.js.map → reveal-W7BKZSUP.js.map} +0 -0
- /package/dist/{revoke-JFN773KF.js.map → revoke-H3VSTAIE.js.map} +0 -0
- /package/dist/{signer-TA55QB6R.js.map → signer-53AQ3BU2.js.map} +0 -0
- /package/dist/{stale-ARHFVL5U.js.map → stale-TH7SSVRV.js.map} +0 -0
- /package/dist/{storage-V7MTFAQJ.js.map → storage-WSF7ZKTL.js.map} +0 -0
- /package/dist/{ulid-PTAIMDG4.js.map → ulid-VQW6ERQN.js.map} +0 -0
- /package/dist/{verify-RO5RTZOJ.js.map → verify-M5LQH43C.js.map} +0 -0
- /package/dist/{walk-3ZECBFDA.js.map → walk-NABCHDZN.js.map} +0 -0
- /package/dist/{withdraw-accessible-7PG4DYQK.js.map → withdraw-accessible-VCINIG6V.js.map} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
dictCollectionName
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-FNYCT27B.js";
|
|
4
4
|
import {
|
|
5
5
|
getAtPath,
|
|
6
6
|
presentI18nForJoin
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-XPEPMK4Y.js";
|
|
8
8
|
import {
|
|
9
9
|
PeriodClosedError,
|
|
10
10
|
UnknownDictCodeError,
|
|
11
11
|
ValidationError
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-Z3BZXPRO.js";
|
|
13
13
|
|
|
14
14
|
// src/via/lookup/registry.ts
|
|
15
15
|
function enforceStaticDictOnPut(staticFields, record) {
|
|
@@ -487,4 +487,4 @@ export {
|
|
|
487
487
|
forgetDerivedFanout,
|
|
488
488
|
selfWriteFieldEqual
|
|
489
489
|
};
|
|
490
|
-
//# sourceMappingURL=chunk-
|
|
490
|
+
//# sourceMappingURL=chunk-K6CPIK3R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/via/lookup/registry.ts","../src/via/lookup/snapshot.ts","../src/port/with/lookup-strategy.ts","../src/kernel/via/dispatch.ts"],"sourcesContent":["/**\n * Pure dict-registry helpers — extracted from `kernel/vault.ts`'s dict\n * bodies (#650 Task 1 — via-lookup extraction, phase D).\n *\n * These take the vault-resident registry Maps (and a couple of vault-bound\n * callbacks) as ARGUMENTS instead of closing over `this` — no `Vault`\n * import here, so this module stays a plain, testable function library.\n * `kernel/vault.ts` keeps the registry Maps themselves (they're populated\n * by `vault.collection()` and read by the backup path) and calls these\n * helpers through the `port/with/lookup-strategy.ts` seam.\n */\n\nimport { getAtPath } from '../../kernel/paths.js'\nimport { UnknownDictCodeError, ValidationError } from '../../kernel/errors.js'\nimport type { JoinableSource } from '../../kernel/query/index.js'\nimport type { FieldRef, ViaGraph } from '../../kernel/via/graph.js'\nimport type { StaticDictDescriptor } from '../../port/with/i18n-strategy.js'\nimport { dictCollectionName, type LookupHandle } from './handle.js'\nimport type { LookupDescriptor, OnDelete } from './descriptor.js'\n\n/**\n * Validate staticDict codes on a `put()`. For each `staticDict()` field,\n * every stored code must be a declared key of the descriptor's table, else\n * `UnknownDictCodeError`. Opt out per descriptor with `{ validateCodes:\n * false }`. Supports scalar, dotted, and `[].`-wildcard field paths via\n * `getAtPath` (same path support as i18n validation).\n *\n * `staticFields` is the collection's `field → StaticDictDescriptor` map\n * (`Vault#staticDescriptorByField.get(collectionName)`) — `undefined`/empty\n * is a no-op.\n */\nexport function enforceStaticDictOnPut(\n staticFields: Record<string, StaticDictDescriptor> | undefined,\n record: unknown,\n): void {\n if (!staticFields || Object.keys(staticFields).length === 0) return\n if (!record || typeof record !== 'object') return\n\n const obj = record as Record<string, unknown>\n for (const [field, desc] of Object.entries(staticFields)) {\n if (desc.validateCodes === false) continue\n const known = new Set<string>(desc.keys)\n const values = getAtPath(obj, field)\n for (const value of values) {\n if (value === undefined || value === null) continue\n const codes = Array.isArray(value) ? value : [value]\n for (const code of codes) {\n if (typeof code !== 'string') continue\n if (!known.has(code)) {\n throw new UnknownDictCodeError(desc.name, field, code)\n }\n }\n }\n }\n}\n\n/**\n * Build a `JoinableSource` for a dictKey field, for use in dict joins.\n * Returns a source whose snapshot contains `{ key, labels, ...labels }`\n * records — one per dictionary entry — keyed by the stable key.\n *\n * staticDict: a code-table-backed source — snapshot() materialises the\n * in-memory table into rows, mirroring `LookupHandle.snapshotEntries()`.\n * Carries `displayLocale` so a locale-less `{ by: 'label' }` query has a\n * default locale to resolve at.\n *\n * Plain dictKey: the snapshot is built synchronously from the\n * `LookupHandle`'s write-through cache, which is populated on every\n * `put()`, `rename()`, `delete()`, and `list()` call. For pre-existing\n * data not yet touched this session, call `await vault.dictionary(name).list()`\n * first to warm the cache.\n *\n * Returns `null` when `field` is not a dictKey in `leftCollection`.\n */\nexport function resolveDictSource(\n leftCollection: string,\n field: string,\n staticDescriptorByField: ReadonlyMap<string, Record<string, StaticDictDescriptor>>,\n dictKeyFieldRegistry: ReadonlyMap<string, Record<string, string>>,\n getDictionaryHandle: (name: string) => LookupHandle,\n): JoinableSource | null {\n const staticFields = staticDescriptorByField.get(leftCollection)\n if (staticFields && field in staticFields) {\n const desc = staticFields[field]!\n const rows: readonly Record<string, unknown>[] = Object.entries(desc.table).map(\n ([key, labels]) => ({ key, labels, ...(labels as Record<string, string>) }),\n )\n const source: JoinableSource = {\n snapshot(): readonly unknown[] {\n return rows\n },\n lookupById(id: string): unknown {\n return rows.find((e) => e['key'] === id)\n },\n }\n if (desc.displayLocale !== undefined) {\n ;(source as { displayLocale?: string }).displayLocale = desc.displayLocale\n }\n return source\n }\n\n const dictFields = dictKeyFieldRegistry.get(leftCollection)\n if (!dictFields || !(field in dictFields)) return null\n const dictName = dictFields[field]\n if (!dictName) return null\n const handle = getDictionaryHandle(dictName)\n return {\n snapshot(): readonly unknown[] {\n return handle.snapshotEntries()\n },\n lookupById(id: string): unknown {\n const entries = handle.snapshotEntries()\n return entries.find((e) => e['key'] === id)\n },\n }\n}\n\n/** The minimal collection surface `updateReferencingRecords` needs. */\nexport interface DictReferencingCollection {\n list(): Promise<Record<string, unknown>[]>\n put(id: string, record: Record<string, unknown>): Promise<unknown>\n}\n\n/**\n * Find and rewrite records in every registered collection whose\n * dictKeyField points at `name`, replacing `oldKey` with `newKey`. Used by\n * `LookupHandle.rename()` (the only sanctioned mass-mutation path for\n * dictKey fields) via the vault's `findAndUpdateReferences` callback.\n *\n * `registry` is `Vault#dictKeyFieldRegistry` (collection name → field name\n * → dictionary name); `getCollection` is the vault's collection accessor —\n * rewrites go through the public `coll.put()` choke point, same as before.\n */\nexport async function updateReferencingRecords(\n registry: ReadonlyMap<string, Record<string, string>>,\n getCollection: (collectionName: string) => DictReferencingCollection,\n name: string,\n oldKey: string,\n newKey: string,\n): Promise<void> {\n for (const [collectionName, dictFields] of registry) {\n // Find fields that point at this dictionary\n const fields = Object.entries(dictFields)\n .filter(([, dn]) => dn === name)\n .map(([field]) => field)\n if (fields.length === 0) continue\n\n const coll = getCollection(collectionName)\n const records = await coll.list()\n for (const record of records) {\n let changed = false\n const updated = { ...record }\n for (const field of fields) {\n if (updated[field] === oldKey) {\n updated[field] = newKey\n changed = true\n }\n }\n if (changed) {\n const id = record['id'] as string | undefined\n if (id !== undefined) {\n await coll.put(id, updated)\n }\n }\n }\n }\n}\n\n/**\n * Resolve a label from an in-memory `{ locale -> label }` map, walking the\n * same fallback chain semantics as `LookupHandle.resolveLabel` (#650 Task 2\n * — moved here from `kernel/vault.ts` so the SAME chain serves both the\n * i18n binding's `dictLabelResolver` and the lookup binding's\n * `lookupLabelResolver`, which #650 Task 2 wires to the identical closure).\n */\nexport function resolveLabelFromMap(\n labels: Readonly<Record<string, string>>,\n locale: string,\n fallback?: string | readonly string[],\n): string | undefined {\n if (labels[locale] !== undefined) return labels[locale]\n const chain = Array.isArray(fallback)\n ? (fallback as readonly string[])\n : fallback\n ? [fallback as string]\n : []\n for (const fb of chain) {\n if (fb === 'any') {\n const any = Object.values(labels)[0]\n if (any !== undefined) return any\n } else if (labels[fb] !== undefined) {\n return labels[fb]\n }\n }\n return undefined\n}\n\n/**\n * Project a native `lookup(dimension, { backing:'static', table, … })`\n * descriptor into the legacy `StaticDictDescriptor` shape — the\n * alias-equivalence compat seam (#650 Task 2) that lets a native\n * static-tier lookup field reuse the SAME vault registries\n * (`staticByName`/`staticDescriptorByField`) — and therefore the same\n * `dictLabelResolver`/`resolveDictSource` machinery — as its `staticDict()`\n * alias. `vocabulary:'closed'` maps to `validateCodes:true` (closed = only\n * declared codes are legal); `'open'` maps to `validateCodes:false`.\n * Returns `undefined` for non-static or table-less (bare `enumOf`)\n * descriptors — those have nothing to register.\n */\nexport function lookupToStaticDictCompat(desc: LookupDescriptor): StaticDictDescriptor | undefined {\n if (desc.backing !== 'static' || desc.table === undefined) return undefined\n return {\n _noydbStaticDict: true,\n _viaBrand: 'i18n',\n name: desc.dimension,\n table: desc.table,\n keys: desc.keys ?? Object.keys(desc.table),\n ...(desc.displayLocale !== undefined ? { displayLocale: desc.displayLocale } : {}),\n ...(desc.onMissing !== undefined ? { onMissing: desc.onMissing } : {}),\n ...(desc.substitute !== undefined ? { substitute: desc.substitute } : {}),\n validateCodes: desc.vocabulary === 'closed',\n }\n}\n\n/** The vault-registry entries a collection's `lookupFields` contribute — the alias-equivalence bridge (#650 Task 2). */\nexport interface LookupDictCompat {\n /** Reserved-tier fields: field name -> dimension (dictionary) name — merges into `dictKeyFieldRegistry`. */\n readonly dictFieldMap: Record<string, string>\n /** Static-tier (table-bearing) fields, projected — merges into `staticDescriptorByField`/`staticByName`. */\n readonly staticEntries: ReadonlyArray<readonly [string, StaticDictDescriptor]>\n}\n\n/**\n * Bridge a collection's `lookupFields` into the SAME shape the legacy dict\n * registries expect, so `resolveDictSource`/`dictLabelResolver` (and\n * therefore `.join()`/`orderBy({by:'label'})`) serve a native `dict()`/\n * `lookup(static)` field identically to its `dictKey()`/`staticDict()`\n * alias — the reserved-vs-first-class-backing \"matrix\" tier is NOT bridged\n * here (no vault registry backs it; Task 5/6 build its own graph edge /\n * snapshot seam).\n */\nexport function collectLookupDictCompat(\n lookupFields: Record<string, LookupDescriptor> | undefined,\n): LookupDictCompat {\n const dictFieldMap: Record<string, string> = {}\n const staticEntries: Array<readonly [string, StaticDictDescriptor]> = []\n for (const [field, desc] of Object.entries(lookupFields ?? {})) {\n if (desc.backing === 'reserved') {\n dictFieldMap[field] = desc.dimension\n } else {\n const compat = lookupToStaticDictCompat(desc)\n if (compat) staticEntries.push([field, compat])\n }\n }\n return { dictFieldMap, staticEntries }\n}\n\n/**\n * Reserved-dict deps of a set of named collections (#653) — the `_dict_*`\n * collections partial sync must auto-include alongside a `collections`\n * filter, mirroring `SyncEngine`'s existing satellite-pair expansion.\n * One hop is provably complete: `vault.collection()` refuses `_dict_*`\n * names, so a dict can never itself declare lookup fields — no\n * dict-of-dict chain to recurse through.\n */\nexport function reservedDictDepsOf(\n names: readonly string[],\n dictKeyFieldRegistry: ReadonlyMap<string, Record<string, string>>,\n): readonly string[] {\n const out = new Set<string>()\n for (const name of names) {\n const fieldMap = dictKeyFieldRegistry.get(name)\n if (!fieldMap) continue\n for (const dictName of Object.values(fieldMap)) out.add(dictCollectionName(dictName))\n }\n return [...out]\n}\n\n/**\n * A lookup dimension's sync membership/altKey table, materialized from its\n * backing rows (#650 Task 3). `keys` is every canonical key present;\n * `altIndex` maps an altKey candidate VALUE to its owning canonical key.\n */\nexport interface MaterializedBacking {\n /** Canonical key values present in the materialized rows. */\n readonly keys: ReadonlySet<string>\n /** altKey candidate value -> canonical key. */\n readonly altIndex: ReadonlyMap<string, string>\n}\n\n/**\n * Build a lookup dimension's altKey index from its backing rows, enforcing\n * declare/warm-time uniqueness across `key ∪ altKeys` values (#650 Task 3 —\n * the CHE/SWZ drift class: two different rows must never claim the same\n * candidate key). `rows` is keyed by canonical key (`row[descriptor.key]`\n * for the matrix tier; the dimension's own key for static/reserved).\n * An altKey candidate VALUE may be a string or number — both normalize via\n * `coerceLookupKey` (#651 Task 3); non-scalar/absent values are skipped.\n * Pure — no I/O. Throws `ValidationError` on collision.\n */\nexport function materializeBackingTable(\n descriptor: LookupDescriptor,\n rows: ReadonlyMap<string, Record<string, unknown>>,\n): MaterializedBacking {\n const keys = new Set<string>(rows.keys())\n // Every value that has claimed ownership so far (canonical keys seed it) —\n // the union `key ∪ altKeys` uniqueness set the spec requires.\n const owner = new Map<string, string>()\n for (const key of keys) owner.set(key, key)\n\n const altIndex = new Map<string, string>()\n const altFields = descriptor.altKeys ?? []\n for (const [canonicalKey, row] of rows) {\n for (const altField of altFields) {\n const value = coerceLookupKey(row[altField])\n if (value === undefined || value === '') continue\n const existingOwner = owner.get(value)\n if (existingOwner !== undefined && existingOwner !== canonicalKey) {\n throw new ValidationError(\n `lookup \"${descriptor.dimension}\": altKey field \"${altField}\" value \"${value}\" is claimed by ` +\n `both \"${existingOwner}\" and \"${canonicalKey}\" — key/altKey values must be unique across the dimension.`,\n )\n }\n owner.set(value, canonicalKey)\n altIndex.set(value, canonicalKey)\n }\n }\n return { keys, altIndex }\n}\n\n/**\n * Closed-vocabulary membership test for one candidate key (#650 Task 3).\n * Static tier: sync, against the in-config key set (declared `keys`, or the\n * table's own keys when table-bearing). Reserved tier: sync — the declared\n * `keys` union the reserved handle's live write-through snapshot (closes\n * #649 for the native `dict()` spelling: the declared-keys promise the old\n * `dictKey()` doc comment made falsely). Matrix (collection) tier: sync —\n * delegates to `buildLookupAltIndex` so membership is checked against the\n * SAME `row[descriptor.key]` keying the altKey index uses (review fix,\n * Important 1: an earlier PUT-id `.get(key)` scan disagreed with the\n * altIndex whenever `descriptor.key !== 'id'`, wrongly rejecting valid\n * non-id candidates and wrongly accepting an unrelated row's PUT-id).\n */\nexport function checkLookupMembership(\n descriptor: LookupDescriptor,\n key: string,\n getDictionary: (dimension: string) => LookupHandle,\n getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource },\n): boolean {\n if (descriptor.backing === 'static') {\n const known = descriptor.keys ?? (descriptor.table ? Object.keys(descriptor.table) : [])\n return known.includes(key)\n }\n if (descriptor.backing === 'reserved') {\n if ((descriptor.keys ?? []).includes(key)) return true\n return getDictionary(descriptor.dimension).snapshotEntries().some((e) => e['key'] === key)\n }\n return buildLookupAltIndex(descriptor, getDictionary, getCollection).keys.has(key)\n}\n\n/**\n * The ONE guarded key coercion (#651 Task 3, dm12) — string/number values\n * coerce to their canonical `String()` form; everything else (`null`,\n * `undefined`, objects, …) coerces to `undefined`. Every consumer that turns\n * a raw record field into a lookup key routes through this, closing the\n * bare-`String()` `\"undefined\"`/`\"null\"`-key poisoning class (seam map\n * finding 6): a row whose key field is genuinely absent must never mint the\n * literal candidate string `\"undefined\"`.\n */\nexport function coerceLookupKey(raw: unknown): string | undefined {\n return typeof raw === 'string' || typeof raw === 'number' ? String(raw) : undefined\n}\n\n/**\n * Resolve a backing row's canonical key VALUE — `coerceLookupKey(row[descriptor.key])`\n * (#651 Task 3). The matrix tier's `row[descriptor.key]`, never the row's own PUT-id\n * when the two differ (`descriptor.key !== 'id'`).\n */\nexport function resolveBackingRowKey(\n descriptor: LookupDescriptor,\n row: Record<string, unknown>,\n): string | undefined {\n return coerceLookupKey(row[descriptor.key])\n}\n\n/**\n * The referencing-side match predicate — does `rec[field]` (coerced) equal an\n * already-coerced `compareKey` (#651 Task 3)? Shared by every site that scans a\n * referencing collection for rows pointing at a given backing key\n * (`with-shape/links/vault-facade.ts`'s ref-delete propagation, `kernel/via/dispatch.ts`'s\n * forget-fanout twin).\n */\nexport function matchesReferencingValue(\n rec: Record<string, unknown>,\n field: string,\n compareKey: string,\n): boolean {\n return coerceLookupKey(rec[field]) === compareKey\n}\n\n/**\n * Materialize a lookup dimension's altKey index from whatever backing data\n * is synchronously available (#650 Task 3 — the `ingest` source). Static:\n * the in-config table. Reserved: the reserved handle's live write-through\n * cache (the same warm-via-put()/list() cache `resolveDictSource` already\n * relies on). Matrix (collection): the backing collection's own in-memory\n * eager cache via `querySourceForJoin()` — already public, already the\n * mechanism `.join()` uses (`with-shape/links/vault-facade.ts`'s\n * `resolveSource`); no new I/O, no fire-and-forget warm step. Like that\n * existing join precedent, a dimension collection this vault session has\n * not yet opened/populated sees an empty snapshot (no altKey normalization\n * until it has rows) — open/populate it first for normalization to apply.\n * A matrix row whose `descriptor.key` field coerces to `undefined` (missing/\n * non-scalar) is SKIPPED — never enters the index under a poisoned\n * `\"undefined\"` key (#651 Task 3, dm12).\n */\nexport function buildLookupAltIndex(\n descriptor: LookupDescriptor,\n getDictionary: (dimension: string) => LookupHandle,\n getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource },\n): MaterializedBacking {\n if (descriptor.backing === 'static') {\n return materializeBackingTable(descriptor, new Map(Object.entries(descriptor.table ?? {})))\n }\n if (descriptor.backing === 'reserved') {\n const entries = getDictionary(descriptor.dimension).snapshotEntries()\n return materializeBackingTable(descriptor, new Map(entries.map((e) => [String(e['key']), e])))\n }\n const rows = getCollection(descriptor.dimension).querySourceForJoin().snapshot()\n const keyed = new Map<string, Record<string, unknown>>()\n for (const r of rows) {\n const row = r as Record<string, unknown>\n const key = resolveBackingRowKey(descriptor, row)\n if (key !== undefined) keyed.set(key, row)\n }\n return materializeBackingTable(descriptor, keyed)\n}\n\n/** One cross-collection `'ref'` graph edge a lookup field declares (#650 Task 5). Module-private —\n * only `registerLookupRefEdges` below (the exported entry point) consumes it. */\ninterface LookupRefEdge {\n readonly referencing: FieldRef\n readonly sources: readonly FieldRef[]\n readonly onDelete: OnDelete\n /** The backing dimension's canonical-key FIELD NAME on its own row (`desc.key` — matrix tier\n * only varies this; reserved/static tiers are always `'id'`). A referencing field always\n * stores THIS field's value, never the backing row's PUT-id when the two differ. */\n readonly keyField: string\n}\n\n/**\n * Compute the cross-collection `'ref'` edges a collection's `lookupFields` declare (#650 Task 5,\n * spec §4) — one edge per non-static-backing field: target = the referencing field; sources =\n * the backing dimension's whole-collection node (`field:'*'`, the wildcard key\n * `ViaGraph.referencingEdgesOf` does its O(1) reverse lookup against) PLUS, when the descriptor\n * names a presentation field (`present.label`), that SPECIFIC field too. `foldPosture`'s\n * `DEFAULT_POSTURE` is the fold's identity element, so adding the wildcard alongside a real field\n * source changes nothing when that field is plain — but folds in a classified/money posture when\n * it isn't (taint composition, spec §4: \"a lookup edge whose source names a classified field\n * contributes that field's posture\"). Static tier (`backing:'static'`) has no backing collection/\n * dimension rows to reference-check — excluded. Pure; consumed only by `registerLookupRefEdges` below.\n */\nfunction collectLookupRefEdges(\n collectionName: string,\n lookupFields: Record<string, LookupDescriptor> | undefined,\n): readonly LookupRefEdge[] {\n const edges: LookupRefEdge[] = []\n for (const [field, desc] of Object.entries(lookupFields ?? {})) {\n if (desc.backing === 'static') continue\n const backing = desc.backing === 'reserved' ? dictCollectionName(desc.dimension) : desc.dimension\n edges.push({\n referencing: { collection: collectionName, field },\n sources: [\n { collection: backing, field: '*' },\n ...(desc.present?.label !== undefined ? [{ collection: backing, field: desc.present.label }] : []),\n ],\n onDelete: desc.onDelete,\n keyField: desc.key,\n })\n }\n return edges\n}\n\n/** Thin wrapper — `collectLookupRefEdges` + one `graph.registerDerived` call per edge. Lets\n * `vault.collection()` (kernel-surface-budgeted, #650 Task 5) register a collection's lookup-ref\n * edges with a single line, keeping the ceiling-guarded call site a thin call (route logic here). */\nexport function registerLookupRefEdges(\n graph: ViaGraph,\n collectionName: string,\n lookupFields: Record<string, LookupDescriptor> | undefined,\n): void {\n for (const e of collectLookupRefEdges(collectionName, lookupFields)) {\n graph.registerDerived(e.referencing, e.sources, 'ref', 'record', e.onDelete, e.keyField)\n }\n}\n\n/**\n * `LookupViaConfig.snapshotFor`'s vault-built row source (#650 Task 6, spec\n * §5; matrix-tier coverage added #650 Task 7, spec §6 — Task 6 deferred it,\n * see `task-6-report.md`'s Concerns #1; the reviewer's Task-7 dispatch\n * refuted the \"needs a new vault-resident registry\" premise: the descriptor\n * is already in hand at both call sites, so `snapshotFor` just needs to\n * ACCEPT it). Takes the full `descriptor` (not a bare dimension name) so it\n * can route the matrix tier's `key` field, which — unlike reserved tier's\n * hardcoded `'id'` — varies per collection. Routes on `descriptor.backing`:\n *\n * - **reserved**: rows come straight from the SAME `LookupHandle.\n * snapshotEntries()` write-through cache `dictLabelResolver`/\n * `resolveDictSource` already read (no second copy), keyed by each\n * entry's own canonical `key` (always `'id'` by construction for this\n * tier — `dict()`'s factory hardcodes it).\n * - **collection** (matrix): rows come from `getCollection(dimension).\n * querySourceForJoin().snapshot()` — the SAME sync, already-live cache\n * `buildLookupAltIndex`'s matrix branch (above, this file) and `.join()`\n * itself already read — re-keyed via `resolveBackingRowKey(descriptor, row)`,\n * NOT the row's own PUT-id (which may differ when `key !== 'id'`; the exact\n * distinction the #650 Task 3 review fix already applies to\n * `checkLookupMembership`'s matrix branch). A row whose `descriptor.key`\n * field coerces to `undefined` is skipped (#651 Task 3, dm12).\n * - **static**: never routed here — `descriptor.table` is read directly by\n * the caller (`binding.ts`'s `compareLookupOrder`/`resolveLookupOrderLabel`,\n * `snapshot.ts`'s `presentLookupForJoin`); no vault call needed.\n *\n * `isReservedDimension` is the vault's `reservedLookupCollections`\n * membership test.\n */\nexport function buildLookupSnapshotRows(\n descriptor: LookupDescriptor,\n isReservedDimension: (dimension: string) => boolean,\n getDictionary: (dimension: string) => LookupHandle,\n getCollection: (dimension: string) => { querySourceForJoin(): JoinableSource },\n): ReadonlyMap<string, Record<string, unknown>> | undefined {\n const dimension = descriptor.dimension\n if (isReservedDimension(dimension)) {\n const rows = new Map<string, Record<string, unknown>>()\n for (const entry of getDictionary(dimension).snapshotEntries()) {\n const key = entry['key']\n if (typeof key === 'string') rows.set(key, entry)\n }\n return rows\n }\n if (descriptor.backing === 'collection') {\n const rawRows = getCollection(dimension).querySourceForJoin().snapshot()\n const rows = new Map<string, Record<string, unknown>>()\n for (const r of rawRows) {\n const row = r as Record<string, unknown>\n const key = resolveBackingRowKey(descriptor, row)\n if (key !== undefined) rows.set(key, row)\n }\n return rows\n }\n return undefined\n}\n","/**\n * The sync lookup snapshot + join/locale seam (#650 Task 6, spec §5 — \"the\n * snapshot+locale seam\"). Retires the #626 kernel→via grandfather:\n * `kernel/query/join.ts` no longer imports `via/i18n/core.js` directly\n * — it calls the `presentForJoin` hook this file's `buildPresentForJoin`\n * builds instead (seam map Part 2 item 4, the #626 reviewer-spec'd shape:\n * a sync `presentI18nForJoin`-class hook on `JoinableSource`).\n *\n * `LookupSnapshot` is the sync materialized `key -> row` view over ONE\n * lookup dimension's ALREADY-LIVE backing data (the `active.ts`\n * `_syncCache`/`snapshotEntries` write-through-cache pattern — never a\n * second copy of the data: `LookupViaConfig.snapshotFor`'s vault-built\n * closure reads the SAME `LookupHandle._syncCache` /\n * first-class-collection cache every other lookup consumer\n * (`dictLabelResolver`, `resolveDictSource`, `getAltIndex`) already reads).\n * Serves (reserved AND matrix tier since #650 Task 7 — `registry.ts`'s\n * `buildLookupSnapshotRows` routes both; static tier is read straight off\n * `descriptor.table` by every consumer below, never through this cache):\n * - join dressing (`presentForJoin`, consumed by `kernel/query/join.ts`\n * via `JoinableSource.presentForJoin`)\n * - dimension sort (`compareForOrder`, consumed by\n * `via/lookup/binding.ts`'s `ViaBinding.compareForOrder` closure)\n * - per-call-locale order-label resolution (`resolveOrderLabel`, #650\n * Task 7 — the `orderBy(..., {by:'label'})` channel `compareForOrder`\n * structurally can't serve, no locale param; consumed by\n * `kernel/query/builder.ts`'s `buildOrderLabelMaps`)\n * - membership: reserved/static-tier membership (#650 Task 3,\n * `checkLookupMembership`) already reads the identical sync caches\n * directly — not re-plumbed through this file; see\n * `.superpowers/sdd/task-6-report.md`'s \"bridge disposition\" section.\n *\n * Sync end-to-end (#553) — every function here is a pure, synchronous\n * transform over already-materialized rows; no store read, no Promise.\n */\nimport type { LookupDescriptor } from './descriptor.js'\nimport { presentI18nForJoin, type I18nTextDescriptor } from '../i18n/core.js'\n\n/** A lookup dimension's sync materialized view — see file header. */\nexport interface LookupSnapshot {\n /** The full backing row for `key`, or `undefined` when `key` isn't (yet) present in the snapshot. */\n row(key: string): Record<string, unknown> | undefined\n /** The dimension's declared presentation label for `key` at `locale` — mirrors `binding.ts`'s `fetchLookupLabel` (matrix-row branch), generalized to any tier's already-materialized rows. */\n label(key: string, locale: string, fallback?: unknown): string | undefined\n /**\n * Exact ordering for two canonical keys against `descriptor.sortBy`\n * (falls back to `present.label`, then to the raw keys) at `locale`.\n * Never throws — degrades to comparing the raw keys when no sortable\n * value resolves for either side.\n */\n compareKeys(a: string, b: string, locale: string): number\n}\n\n/** Read one row field, resolving a `by`-keyed (locale-map) value when `by` is declared. */\nfunction readRowField(\n row: Record<string, unknown> | undefined,\n field: string | undefined,\n by: string | undefined,\n locale: string,\n fallback?: unknown,\n): string | undefined {\n if (!row || field === undefined) return undefined\n const raw = row[field]\n if (by === undefined) return typeof raw === 'string' ? raw : undefined\n if (!raw || typeof raw !== 'object') return undefined\n const map = raw as Record<string, unknown>\n const val = map[locale]\n if (typeof val === 'string') return val\n if (typeof fallback === 'string' && typeof map[fallback] === 'string') return map[fallback]\n return undefined\n}\n\n/**\n * Build a sync `LookupSnapshot` over one dimension's already-materialized\n * rows (canonical-key -> row, the SAME keying `materializeBackingTable`\n * (`registry.ts`, #650 Task 3) uses). Pure — never reads a store.\n */\nexport function buildLookupSnapshot(\n dimension: string,\n rows: ReadonlyMap<string, Record<string, unknown>>,\n descriptor: LookupDescriptor,\n): LookupSnapshot {\n void dimension // identity only — kept for parity with materializeBackingTable/buildLookupAltIndex's signature and future diagnostics\n return {\n row: (key) => rows.get(key),\n label: (key, locale, fallback) =>\n readRowField(rows.get(key), descriptor.present?.label, descriptor.present?.by, locale, fallback),\n compareKeys: (a, b, locale) => {\n const sortField = descriptor.sortBy ?? descriptor.present?.label\n const av = readRowField(rows.get(a), sortField, descriptor.present?.by, locale) ?? a\n const bv = readRowField(rows.get(b), sortField, descriptor.present?.by, locale) ?? b\n return av < bv ? -1 : av > bv ? 1 : 0\n },\n }\n}\n\n/**\n * The lookup-label HALF of `presentForJoin` (#626 retirement, spec §5;\n * matrix-tier coverage added #650 Task 7) — resolves `<field>Label` for\n * every declared lookup field with a `present` dressing dimension, sync,\n * from `getSnapshotRows` (the vault-built `LookupViaConfig.snapshotFor`\n * closure — now descriptor-routed, see `registry.ts`'s\n * `buildLookupSnapshotRows`; static tier reads its own in-config `table`\n * directly, no vault call — never `undefined` for a declared static table).\n * Mirrors `binding.ts`'s `runLookupPresent` (the async `present()` hook)\n * minus the array/`[].`-wildcard handling that hook needs for full-record\n * reads — join dressing only ever sees the joined RIGHT-side record's\n * scalar fields, so that complexity doesn't apply here.\n */\nfunction presentLookupForJoin(\n record: Record<string, unknown>,\n locale: string,\n lookupFields: Record<string, LookupDescriptor>,\n getSnapshotRows: (descriptor: LookupDescriptor) => ReadonlyMap<string, Record<string, unknown>> | undefined,\n): Record<string, unknown> {\n let result = record\n for (const [field, desc] of Object.entries(lookupFields)) {\n if (desc.present === undefined) continue\n const raw = record[field]\n if (typeof raw !== 'string') continue\n const rows = desc.backing === 'static'\n ? (desc.table ? new Map(Object.entries(desc.table)) : undefined)\n : getSnapshotRows(desc)\n if (!rows) continue\n const label = buildLookupSnapshot(desc.dimension, rows, desc).label(raw, locale)\n if (label === undefined) continue\n if (result === record) result = { ...record }\n result[`${field}Label`] = label\n }\n return result\n}\n\n/**\n * Build the combined sync `presentForJoin(record, locale)` hook a\n * `Collection` attaches to the `JoinableSource` it exposes\n * (`querySourceForJoin()`) — the i18n-text half (`presentI18nForJoin`, the\n * exact `applyI18nLocale(..., 'join')` partial application\n * `kernel/query/join.ts` used to call directly, #626) composed with the\n * lookup-label half above. `undefined` when the collection declares\n * neither — `JoinableSource.presentForJoin` then stays unset, matching\n * today's `i18nFields`-absent behavior exactly (#626 parity lock).\n */\nexport function buildPresentForJoin(\n i18nFields: Record<string, I18nTextDescriptor> | undefined,\n lookupFields: Record<string, LookupDescriptor> | undefined,\n getSnapshotRows: ((descriptor: LookupDescriptor) => ReadonlyMap<string, Record<string, unknown>> | undefined) | undefined,\n): ((record: unknown, locale: string) => unknown) | undefined {\n const hasI18n = i18nFields !== undefined && Object.keys(i18nFields).length > 0\n const hasLookup = lookupFields !== undefined && Object.keys(lookupFields).length > 0\n if (!hasI18n && !hasLookup) return undefined\n const resolveRows = getSnapshotRows ?? (() => undefined)\n return (record, locale) => {\n if (record === null || typeof record !== 'object') return record\n let result = record as Record<string, unknown>\n if (hasI18n) result = presentI18nForJoin(result, i18nFields, locale)\n if (hasLookup) result = presentLookupForJoin(result, locale, lookupFields, resolveRows)\n return result\n }\n}\n","/**\n * Lookup strategy seam (#650 Task 1 — via-lookup extraction, phase D of the\n * Via port; precedent: `port/with/i18n-strategy.ts`). Lives on the `/with`\n * port (the one seam the kernel spine may import statically) so `Vault` can\n * reach the dict-registry pure helpers and the `LookupHandle`/`NO_LOOKUP`\n * types without a spine→`via/` static import (Check 14 via-layering bans\n * that; `port/with/` is always allowed, Check 9's sanctioned exception).\n *\n * `kernel/vault.ts` imports ONLY this module for lookup — never\n * `via/lookup/*` directly.\n *\n * This task (#650 Task 1) is a pure move: `Vault.dictionary()` still\n * constructs its handle through `i18nStrategy.buildDictionaryHandle`\n * (`port/with/i18n-strategy.ts`), which now internally delegates to\n * `withLookup().buildLookupHandle` (same handle, new home). `LookupStrategy`\n * / `NO_LOOKUP` / `isLookupCollectionName` here are the seam later phase-D\n * tasks bind `vault.collection()`'s lookup fields onto — unused by Task 1's\n * wiring, but their exact shapes are frozen now so later tasks don't\n * re-litigate them.\n */\n\nimport type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'\nimport type { LedgerStore } from '../../with-commit/history/ledger/store.js'\nimport type { UnlockedKeyring } from '../../with-party/team/keyring.js'\nimport type { NoydbEventEmitter } from '../../kernel/events.js'\nimport type { ViaCryptoCtx } from '../../kernel/via/index.js'\nimport type { LookupHandle, DictionaryOptions } from '../../via/lookup/handle.js'\nimport {\n enforceStaticDictOnPut,\n resolveDictSource,\n updateReferencingRecords,\n resolveLabelFromMap,\n collectLookupDictCompat,\n lookupToStaticDictCompat,\n reservedDictDepsOf,\n materializeBackingTable,\n checkLookupMembership,\n buildLookupAltIndex,\n registerLookupRefEdges,\n buildLookupSnapshotRows,\n coerceLookupKey,\n resolveBackingRowKey,\n matchesReferencingValue,\n type DictReferencingCollection,\n type LookupDictCompat,\n type MaterializedBacking,\n} from '../../via/lookup/registry.js'\nimport { buildPresentForJoin } from '../../via/lookup/snapshot.js'\nimport type { LookupDescriptor } from '../../via/lookup/descriptor.js'\n\n/**\n * Backing options for `LookupStrategy.buildLookupHandle` — same shape as\n * `DictionaryOptions` (kept as a distinct alias since the \"lookup\" name is\n * the forward-looking one; `DictionaryOptions` stays the dict-tier name).\n */\nexport type LookupBackingOptions = DictionaryOptions\n\n/**\n * Options accepted by `LookupStrategy.buildLookupHandle`. Mirrors the\n * `LookupHandle` constructor verbatim, plus the two choke-point\n * participation hooks (`onDirty`/`onRecordMutated`) #647 (Task 4) wires —\n * both `undefined` in this task (pure move, no new call sites).\n */\nexport interface BuildLookupHandleOptions<Keys extends string = string> {\n readonly adapter: NoydbStore\n readonly compartmentName: string\n readonly dimensionName: string\n readonly keyring: UnlockedKeyring\n /**\n * The `reservedEnvelopes('_dict_')` capability (#629 Task 4) — the\n * handle's sanctioned crypto door onto its `_dict_<name>` collection.\n * Bound by the Vault to its own `getDEK`, the same per-collection-name\n * DEK resolver every other collection uses.\n */\n readonly reservedEnvelopes: ReturnType<ViaCryptoCtx['reservedEnvelopes']>\n readonly encrypted: boolean\n readonly ledger: LedgerStore | undefined\n readonly options: LookupBackingOptions\n readonly findAndUpdateReferences:\n | ((dimension: string, oldKey: string, newKey: string) => Promise<void>)\n | undefined\n readonly emitter: NoydbEventEmitter\n /**\n * #647 fix wave 1 — mints a version-ordered delete-marker envelope. Bound by the Vault to the\n * real `kernel/enclave` `buildDeleteMarker` function — `LookupHandle` (`via/lookup/**`)\n * may not import `kernel/enclave/` itself (Check 11/15), so this capability is injected the\n * same way `reservedEnvelopes` above is.\n */\n readonly buildDeleteMarker: (identity: { collection: string; id: string }, version: number, actor: string) => EncryptedEnvelope\n /** #650 Task 4 (#647) — choke-point participation hooks. */\n readonly onDirty?: ((collection: string, id: string, action: 'put' | 'delete', version: number) => Promise<void>) | undefined\n readonly onRecordMutated?: ((collection: string, id: string, action: 'put' | 'delete', version: number) => Promise<void>) | undefined\n /**\n * #650 Task 5 (#648) — the real reference check `LookupHandle.delete()`'s strict branch calls:\n * restrict throws `DictKeyInUseError` naming the referencing collection, cascade/nullify apply\n * their propagation. Bound by the Vault (never a `Collection`/keyring/DEK reach-around); a\n * no-op when the dimension has no declared lookup-referencing edges (today's dangling behavior\n * for undeclared refs is unaffected). Return value is discarded by the handle — callers who\n * need cascade/nullify counts (forget) go through `VaultLinks` directly.\n */\n readonly checkReferencesOnDelete?: ((key: string) => Promise<unknown>) | undefined\n /**\n * Used by the active strategy to satisfy the generic-key parameter on the\n * returned handle. The NO_LOOKUP stub never reads it. Mirrors\n * `BuildDictionaryHandleOptions._keyMarker` (`port/with/i18n-strategy.ts`).\n */\n // marker generic — runtime sees no value\n _keyMarker?: Keys\n}\n\nexport interface LookupStrategy {\n /**\n * Construct a typed `LookupHandle` for the named dimension. Throws under\n * `NO_LOOKUP`.\n */\n buildLookupHandle<Keys extends string = string>(\n opts: BuildLookupHandleOptions<Keys>,\n ): LookupHandle<Keys>\n}\n\nfunction notEnabled(op: string): Error {\n return new Error(\n `${op}: the NO_LOOKUP stub was reached, which should be unreachable today — there is no ` +\n '`lookupStrategy` `createNoydb({ ... })` option yet to select this stub over the real one; ' +\n '`vault.dictionary()` always resolves through `withLookup()` (see `via/i18n/active.ts`\\'s ' +\n 'delegation). This is forward scaffolding for a later task (mirrors `NO_I18N`\\'s shape) — if you ' +\n 'hit this, it indicates a bug, not a missing opt-in.',\n )\n}\n\n/**\n * No-lookup stub. Mirrors `NO_I18N.buildDictionaryHandle`'s shape, but unlike `NO_I18N` (wired as\n * `vault.ts`'s `opts.i18nStrategy ?? NO_I18N` default), nothing selects this stub today — there is\n * no `lookupStrategy` option on `createNoydb()`; `vault.dictionary()`'s only construction path\n * (`Vault.i18nStrategy.buildDictionaryHandle` → `withLookup().buildLookupHandle`) never reaches\n * here. Kept as forward scaffolding for a later task that wires an independent lookup opt-in.\n */\nexport const NO_LOOKUP: LookupStrategy = {\n buildLookupHandle() {\n throw notEnabled('vault.dictionary()')\n },\n}\n\n/** `_dict_*` (legacy dict tier) and `_lookup_*` (the phase-D reserved backing). */\nexport const LOOKUP_COLLECTION_PREFIXES = ['_dict_', '_lookup_'] as const\n\n/** Return true when a collection name is a reserved lookup-backing collection. */\nexport function isLookupCollectionName(name: string): boolean {\n return LOOKUP_COLLECTION_PREFIXES.some((prefix) => name.startsWith(prefix))\n}\n\n// Re-exported pure registry helpers (#650 Task 1) — `kernel/vault.ts`'s\n// thin delegators call these directly; small always-on functions, not\n// behind the tree-shake seam (same bundling class as `isLookupCollectionName`\n// above, just too large to duplicate inline like that one is).\nexport { enforceStaticDictOnPut, resolveDictSource, updateReferencingRecords }\nexport type { DictReferencingCollection }\n\n// #650 Task 4 (#647) — the reserved-collection naming helper `vault.ts`'s sync-registry\n// bookkeeping needs (mapping a declared dimension name to its `_dict_<name>` collection name).\nexport { dictCollectionName } from '../../via/lookup/handle.js'\n\n// #650 Task 2 — the alias-equivalence bridge (`resolveLabelFromMap` +\n// `collectLookupDictCompat`/`lookupToStaticDictCompat`) + the runtime\n// brand/shape predicates `via/compose.ts` needs to route `'lookup'`-branded\n// descriptors, mirroring `isI18nTextDescriptor`/`isDictKeyDescriptor` below.\nexport { resolveLabelFromMap, collectLookupDictCompat, lookupToStaticDictCompat }\nexport type { LookupDictCompat }\n\n// #653 — partial-sync reserved-dict expansion: maps a set of named collections to the\n// one-hop `_dict_*` deps their lookup fields reference, off `dictKeyFieldRegistry`.\nexport { reservedDictDepsOf }\n\n// #650 Task 3 — altKeys ingest normalization + open/closed vocabulary\n// governance: the declare/warm-time altIndex builder + the membership test\n// vault.ts's `membership`/`getAltIndex` closures delegate to.\nexport { materializeBackingTable, checkLookupMembership, buildLookupAltIndex }\nexport type { MaterializedBacking }\n\n// #650 Task 5 — registers a collection's lookup-fields' cross-collection 'ref' graph edges.\nexport { registerLookupRefEdges }\n\n// #650 Task 6 — the sync snapshot+locale seam (#626 retirement, spec §5):\n// `snapshotFor`'s vault-built row source + the combined presentForJoin\n// builder `kernel/collection-config.ts` calls to build the\n// `JoinableSource.presentForJoin` hook `kernel/query/join.ts` consumes.\nexport { buildLookupSnapshotRows, buildPresentForJoin }\nexport type { LookupSnapshot } from '../../via/lookup/snapshot.js'\n\n// #651 Task 3 — the ONE descriptor-keyed key-resolution core (guarded coercion +\n// backing-row-key resolve + referencing-value match) — every consumer (vault.ts,\n// with-shape/links/vault-facade.ts, kernel/via/dispatch.ts) routes through here,\n// never a bare `String()`, ending the dm12 dialect drift.\nexport { coerceLookupKey, resolveBackingRowKey, matchesReferencingValue }\n\n/** Runtime predicate for detecting a `LookupDescriptor` (any of the three tiers). */\nexport function isLookupDescriptor(x: unknown): x is LookupDescriptor {\n return (\n typeof x === 'object' &&\n x !== null &&\n (x as { _viaBrand?: unknown })._viaBrand === 'lookup'\n )\n}\n\n/** Runtime predicate for the bare enum tier (`backing:'static'`, no in-code `table` — no label source). */\nexport function isEnumDescriptor(x: unknown): x is LookupDescriptor {\n return isLookupDescriptor(x) && x.backing === 'static' && x.table === undefined\n}\n\n/**\n * Type-only re-exports — the kernel spine imports these descriptor/handle\n * types through the port instead of reaching into `via/lookup/*` or\n * `via/i18n/*` directly. `isolatedModules: true` erases these at\n * build time — no runtime coupling.\n */\nexport type { LookupHandle, DictEntry, DictionaryOptions } from '../../via/lookup/handle.js'\nexport type { LookupDescriptor, Vocabulary, LookupBacking, OnDelete } from '../../via/lookup/descriptor.js'\nexport type { LookupViaConfig } from '../../via/lookup/binding.js'\nexport type { DictKeyDescriptor, StaticDictDescriptor } from '../../via/i18n/dictionary.js'\n","// kernel/via/dispatch.ts — the batched, origin-aware sync/cutover/restore dispatch wave\n// (Via port phase C, #638 Task 4, fixes #621).\n//\n// `Collection._onRecordMutated`'s `sync-apply`/`cutover`/`restore` cases feed touched\n// (collection, id) pairs into a per-session `GraphBatch` (owned by `Vault._collectGraphTouch`)\n// instead of dispatching inline — the `local-write` path keeps its own byte-identical inline\n// dispatch (`wave` stays `undefined` there, per the spec's behavior lock). At batch-flush time\n// `runGraphDispatchWave` decrypts each touched record (id threaded into the decrypt, matching\n// the phase-B at-rest contract) and re-runs the SAME `dispatchDerivations`/\n// `dispatchMaterializedViews` the local-write path uses, sharing one `WaveContext` so N touched\n// records feeding the SAME rollup/MV target recompute exactly once (per-target dedup).\n// See design-history/2026-07-11-via-phase-c-design.md §3.\n\nimport type { ViaGraph } from './graph.js'\nimport type { Collection } from '../collection.js'\nimport type { EncryptedEnvelope } from '../types.js'\nimport { PeriodClosedError } from '../errors.js'\nimport { matchesReferencingValue } from '../../port/with/lookup-strategy.js'\n\n/** One deleted child's resolved rollup PARENT intent (#640) — ids + a field name only. A\n * resolved `parentId` is an id, same class as any touched record id — never a stored value. */\nexport interface RollupDeleteIntent {\n readonly into: string\n readonly parentId: string\n readonly field: string\n}\n\n/** One collection's batched touches this session (#640 widens the #638 `Set<string>` shape):\n * `puts` — the original semantics, unchanged; `deletes` — a deleted record's id mapped to its\n * resolved rollup-parent intents, captured PRE-invalidation by `Collection._onRecordMutated`'s\n * sync-apply delete case (the FK is only readable there — see `kernel/collection.ts\n * #_rollupDeleteIntents`). */\nexport interface GraphTouch {\n readonly puts: Set<string>\n readonly deletes: Map<string, readonly RollupDeleteIntent[]>\n}\n\n/** Per-session touched set — collection → its `GraphTouch`. Metadata only — ids (collection\n * names, record ids INCLUDING resolved rollup parent ids) and field names; NEVER record payload\n * or key material. A resolved parentId is an id, same class as the touched ids — not a stored\n * value. */\nexport type GraphBatch = Map<string, GraphTouch>\n\n/** Get-or-create `batch`'s `GraphTouch` entry for `collection` (#640) — shared by\n * `Vault._collectGraphTouch`/`_collectGraphDelete` so each stays a one-line call. */\nexport function touchFor(batch: GraphBatch, collection: string): GraphTouch {\n let t = batch.get(collection)\n if (!t) {\n t = { puts: new Set(), deletes: new Map() }\n batch.set(collection, t)\n }\n return t\n}\n\n/** #640 — sync, I/O-free: `deleted`'s resolved rollup PARENT intents (the FK is only readable\n * NOW, before the record is gone) — a pure function so `Collection._rollupDeleteIntents` stays\n * a one-line delegator under the kernel-surface ceiling. `registry` is `this.derivationSource\n * ?.registry()`; `collectionName` is `this.name` (the CHILD/`rollup.from` side). Generic over\n * the registry's own spec shape so this file never imports a `with-formula` type (port-layering\n * — via/dispatch.ts must not gain a with-* import). */\nexport function resolveRollupDeleteIntents<S extends { source: string; rollup?: { from: string; key: string; field: string } }>(\n registry: { strategiesForSource(name: string): ReadonlyArray<{ spec: S }> } | undefined,\n collectionName: string,\n deleted: Record<string, unknown>,\n): RollupDeleteIntent[] {\n if (!registry) return []\n const intents: RollupDeleteIntent[] = []\n for (const { spec } of registry.strategiesForSource(collectionName)) {\n if (!spec.rollup || spec.rollup.from !== collectionName) continue\n const kv = deleted[spec.rollup.key]\n if (typeof kv === 'string' || typeof kv === 'number') intents.push({ into: spec.source, parentId: String(kv), field: spec.rollup.field })\n }\n return intents\n}\n\n/** #640 — resolve a `RollupDeleteIntent` back to its registry `spec`. The wave's per-intent\n * driver needs `spec.rollup.compute`, which `GraphBatch`'s metadata-only pin forbids batching —\n * so it's re-resolved here, post-boundary, instead of carried across it. `undefined` if the\n * intent's originating strategy was unregistered between collect time and wave time (residual\n * gap, freshness-only). */\nexport function findRollupSpecForIntent<S extends { source: string; rollup?: { from: string; field: string } }>(\n registry: { strategiesForSource(name: string): ReadonlyArray<{ spec: S }> } | undefined,\n collectionName: string,\n intent: RollupDeleteIntent,\n): S | undefined {\n return registry?.strategiesForSource(collectionName).find((s) => s.spec.source === intent.into && s.spec.rollup?.field === intent.field && s.spec.rollup?.from === collectionName)?.spec\n}\n\n/** One dedup ledger for a single wave — a target is recomputed at most once (mark-on-check). */\nexport class WaveContext {\n private readonly _seen = new Set<string>()\n seen(targetKey: string): boolean {\n if (this._seen.has(targetKey)) return true\n this._seen.add(targetKey)\n return false\n }\n}\n\n/** The slice of `Vault` the wave needs: the shared graph (the #553 zero-cost skip) and cached-\n * collection lookup — never constructs, since a touched collection is, by construction,\n * already open (it just fired `_onRecordMutated`). */\nexport interface VaultLike {\n readonly graph: ViaGraph\n _getCollection(name: string): Collection<Record<string, unknown>> | undefined\n /** #640 (#644 item 3) — structured wave-error surfacing, additive to the existing console.warn. */\n _emit(event: string, payload: unknown): void\n}\n\n/**\n * Run ONE dispatch wave for a completed batch: for each touched (collection, id), decrypt the\n * applied envelope (id threaded), then run the SAME `dispatchDerivations` +\n * `dispatchMaterializedViews` the local-write path uses — with a shared `WaveContext` so N\n * touched records feeding the SAME target recompute once. #553: a collection with no graph\n * out-edges (e.g. money-only) is skipped before any decrypt/dispatch async work.\n */\nexport async function runGraphDispatchWave(vault: VaultLike, batch: GraphBatch): Promise<void> {\n const wave = new WaveContext()\n for (const [collectionName, touch] of batch) {\n if (vault.graph.dependentsOf(collectionName).length === 0) continue\n const coll = vault._getCollection(collectionName)\n if (!coll) continue\n for (const id of touch.puts) {\n try {\n // Decrypt is INSIDE the per-id isolation boundary (whole-branch review Important\n // finding, #638): an undecryptable synced envelope (`TamperedError`) must not\n // escape `_getStoredRecordForDispatch` and abort the wave — that would propagate\n // through `_flushGraphBatch` and reject the surrounding `SyncEngine.pull`/`push`\n // AFTER records were already applied + meta persisted, starving every other\n // touched target in the same batch.\n const stored = await coll._getStoredRecordForDispatch(id)\n if (!stored) continue\n await coll.dispatchDerivations(id, stored.record, stored.version, wave)\n await coll.dispatchMaterializedViews(id, stored.record, wave)\n } catch (err) {\n // #638 Task 5 review mandate: one touched record's recompute must not abort the\n // whole wave (starving co-batched healthy targets) or the pull/push it's nested\n // inside. `PeriodClosedError` never reaches here — `putDerivedOutput` already\n // intercepts it at every output-write call site and turns it into a skip+event.\n // Anything else (a genuine decrypt failure, a derive()/executor bug, a schema\n // violation on the output, ...) is surfaced — not silently swallowed — via the\n // SAME console.warn channel `dispatchDerivations`/the MV executor already use for\n // their own non-strict-mode output failures, then isolated to just this one id.\n // #644 item 3: ADDITIVELY (never in place of the warn) emit a structured event too,\n // so a listener doesn't have to scrape console output to react to a wave failure.\n console.warn(`[via-dispatch] wave recompute failed for ${collectionName}/${id}:`, err)\n vault._emit('derivation:wave-error', { collection: collectionName, id, error: err })\n }\n }\n // #640 — delete-kind touches: the deleted child's rollup-parent intents, resolved (sync,\n // pre-invalidation) at collect time by `Collection._rollupDeleteIntents`. Same per-id\n // isolation as the puts loop above. #658: the wave now ALSO heals MV rows and array-shape\n // derivation output rows for the deleted child — parity with the local-delete boundary\n // (`Collection._doDelete`'s `!internal` block, which already calls all three). Still never\n // routed through `dispatchDerivations` (the mutation-choke-point.test.ts:85-99 pin — sync-\n // applied deletes never RE-DERIVE); `dispatchMaterializedViewsOnDelete`/\n // `dispatchArrayDerivationsOnDelete` only need `id` (MV refresh recomputes+diffs; array-\n // derivation reads its per-source sidecar) and never call `derive()`, so the pin holds.\n for (const [id, intents] of touch.deletes) {\n try {\n await coll._recomputeDeletedRollups(intents, wave)\n // #658: record-shape same-id outputs are left untouched (eraseRecordShapeToo defaults\n // false), matching local delete — the user deletes those directly if wanted.\n // #658: TODO dedup MV refresh per wave — `dispatchMaterializedViewsOnDelete` doesn't\n // accept a `WaveContext` (unlike the put-path `dispatchMaterializedViews`), so N deleted\n // children of one eager MV in a batch each trigger a full refresh pass. Threading `wave`\n // through would touch collection.ts's zero-slack kernel-surface ceiling for a perf-only\n // win; correctness doesn't depend on it (idempotent per source-id) — left as a follow-up.\n await coll.dispatchMaterializedViewsOnDelete(id)\n await coll.dispatchArrayDerivationsOnDelete(id)\n } catch (err) {\n console.warn(`[via-dispatch] wave delete-recompute failed for ${collectionName}/${id}:`, err)\n vault._emit('derivation:wave-error', { collection: collectionName, id, error: err })\n }\n }\n }\n}\n\n/** A record's post-freeze source mutation, for the `'derivation:skipped-frozen'` event\n * and the optional audit-trail entry. See {@link putDerivedOutput}. */\nexport interface DerivationSkippedFrozen {\n readonly source: { readonly collection: string; readonly id: string }\n readonly target: { readonly collection: string; readonly id: string }\n readonly period: string\n readonly endDate: string\n}\n\n/** The minimal put-capable shape `putDerivedOutput` needs. Any `Collection<T>` structurally\n * satisfies this — its real `options` type is a superset of what's declared here. */\nexport interface CollectionLike {\n put(id: string, value: unknown, options?: { readonly source?: string }): Promise<void>\n}\n\nexport interface PutDerivedOutputCtx {\n readonly emit: (ev: string, p: unknown) => void\n readonly source: { readonly collection: string; readonly id: string }\n readonly audit?: ((e: DerivationSkippedFrozen) => Promise<void>) | undefined\n}\n\n/**\n * Attempt a dispatch-driven output write. On `PeriodClosedError` (the closed-period\n * `beforePut` gate — `freezePeriod`/`archivePeriod` add no separate gate, per seam map\n * Part 7): SKIP (no `_ts` stamped — the gate throws before any write happens), emit\n * `'derivation:skipped-frozen'` on the event bus (ALWAYS), and append an audit-trail entry\n * when the with-history ledger is active. Returns `'written' | 'skipped-frozen'`. Any OTHER\n * error propagates unchanged — this helper narrows exactly one error class. The SOURCE\n * write is never wrapped through this helper — only derived-output writes are (§7).\n */\nexport async function putDerivedOutput(\n outColl: CollectionLike, id: string, value: unknown,\n ctx: PutDerivedOutputCtx,\n options?: { readonly source?: string },\n): Promise<'written' | 'skipped-frozen'> {\n try {\n await outColl.put(id, value, options)\n return 'written'\n } catch (err) {\n if (!(err instanceof PeriodClosedError)) throw err\n // Reach-around for the target collection's private `name` — the SAME pattern\n // `with-formula/materialized-views/executor.ts`'s `listOutputIds` already uses to\n // read a `Collection`'s private `adapter`/`vault`/`name` fields from outside the class.\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const targetCollection = (outColl as any).name as string\n const event: DerivationSkippedFrozen = {\n source: ctx.source,\n target: { collection: targetCollection, id },\n period: err.periodName,\n endDate: err.endDate,\n }\n ctx.emit('derivation:skipped-frozen', event)\n if (ctx.audit) await ctx.audit(event)\n return 'skipped-frozen'\n }\n}\n\n/** Structural (no static import — kernel spine may not statically reach a with-* service,\n * the S4 gate recipe `check-architecture.mjs`'s port-layering check enforces) shape of the\n * with-history `LedgerStore.append` seam this helper needs. */\ninterface AuditLedgerLike {\n append(input: {\n readonly op: 'lifecycle'\n readonly collection: string\n readonly id: string\n readonly version: number\n readonly actor: string\n readonly payloadHash: string\n readonly reason?: string\n }): Promise<unknown>\n}\n\n/** @internal — the optional with-history ledger audit hook for `putDerivedOutput`, present\n * only when the ledger is active (mirrors every other kernel event's `if (this.ledger)`\n * gate). Encoded as a `'lifecycle'` entry — the existing \"non-data audit event\" op (the\n * same convention `forget`'s JSON-summary-in-`reason` entry uses; `ledger/entry.ts:88-104`). */\nexport function ledgerAuditHook(\n ledger: AuditLedgerLike | undefined, actor: string,\n): ((e: DerivationSkippedFrozen) => Promise<void>) | undefined {\n if (!ledger) return undefined\n return async (e) => {\n await ledger.append({\n op: 'lifecycle', collection: e.target.collection, id: e.target.id, version: 0, actor, payloadHash: '',\n reason: JSON.stringify({ event: 'derivation-skipped-frozen', ...e }),\n })\n }\n}\n\n/** `recomputeRollup`/`dispatchRollupsOnDelete`'s per-target write outcome (#638 Task 6):\n * `'written'`/`'skipped-frozen'` mirror {@link putDerivedOutput}'s result; `'noop'` covers\n * no-parent/no-change/deduped-by-wave — nothing to report either way. */\nexport type RollupOutcome = 'written' | 'skipped-frozen' | 'noop'\n\n/** Mutable accumulator `forgetDerivedFanout` writes into, one per `Vault.forget()` call — keeps\n * the per-ref loop's call site to a single line under vault.ts's tight kernel-surface ceiling.\n * Maps 1:1 onto `ForgetResult`'s additive fields (`with-audit/forget/strategy.ts`). */\nexport interface ForgetFanoutStats {\n recordsErased: number\n aggregatesRecomputed: number\n readonly residueFrozen: string[]\n /** #650 Task 5 (#648) — referencing records tombstoned via a `'ref'` edge's `cascade` policy. */\n lookupReferencesCascaded: number\n /** #650 Task 5 (#648) — referencing fields cleared via a `'ref'` edge's `nullify` policy. */\n lookupReferencesNullified: number\n /** #650 Task 5 review (Important fix) — `'ref'` edges whose compare-key could NOT be resolved,\n * even from the LIVE pre-shred row (the backing row itself is unreadable) — propagation was\n * skipped for these, reported here so the skip is never silent. `backing:key:collection.field`\n * entries, one per un-propagated edge. */\n readonly lookupReferencesResidue: string[]\n /** #776/#782/#785 — `outputCollection:id` MV-output rows that survived erasure invalidation\n * (eager tombstone leg AND lazy/manual `invalidateMVAtRest`) despite belonging to the\n * forgotten subject, because the ownership stamp could not be decoded (undecodable under\n * the default DEK — e.g. elevated above tier 0 on a tiered output collection). Ownership\n * UNCONFIRMED. Never erased, but surfaced here rather than silently skipped. */\n readonly derivedResidueUndecodable: string[]\n /** #782/#785 — `outputCollection:id` MV-output rows that decoded and stamp-matched but whose\n * `_internalDelete` declined (the #718 tier-elevation gate). Ownership CONFIRMED, erasure\n * declined — a real silent survival, surfaced here rather than silently skipped. */\n readonly derivedResidueDeclined: string[]\n}\n\n/**\n * #622 — after `_writeTombstone(ref.id, actor)` erases the forgotten subject's own record, fan\n * out to its derived residue via the graph (spec §5): record-grain artifacts (MV rows,\n * array-shape derivation rows, same-id record-shape derivation copies) are ERASED through the\n * SAME `!internal` housekeeping-bypass machinery the ordinary delete path uses (no user\n * `onDelete` re-fires — the shred-is-not-a-domain-delete property `_writeTombstone` protects);\n * aggregate-grain rollups are RECOMPUTED without the forgotten contribution in open periods, or\n * skip+audit (via `putDerivedOutput`, already wired into the rollup/MV output paths) in frozen\n * ones. Mutates `stats` in place. `envelope` is `ref`'s PRE-tombstone envelope, decoded only if\n * a rollup edge is actually present — the #553 zero-cost-skip discipline: no decrypt for the\n * common case of a forgotten record with no aggregate-grain consumer, and no work at all when\n * `ref.collection` has no graph out-edges. `lookupCompareKeys` is the `'ref'` edges' compare-key\n * map, resolved from the LIVE row BEFORE any shred (`VaultLinks.checkLookupRefsRestrict`'s return\n * value — `Vault.forget()`'s pre-shred restrict check doubles as the live-resolve pass) — see\n * {@link applyLookupRefsFanout}.\n */\nexport async function forgetDerivedFanout(\n vault: VaultLike,\n ref: { readonly collection: string; readonly id: string },\n envelope: EncryptedEnvelope | null,\n stats: ForgetFanoutStats,\n lookupCompareKeys: ReadonlyMap<string, string | undefined>,\n): Promise<void> {\n const coll = vault._getCollection(ref.collection)\n\n // #761 item 4 — drive the MV arm off `dispatchMaterializedViewsOnDelete` UNCONDITIONALLY,\n // ABOVE the `edges` check below: `registry.edges()` drops the same-collection\n // partition-disjoint MV edge (registry.ts's `sources.length === 0 → continue`), so a\n // same-collection MV's source collection never appears in `vault.graph`'s derived-artifact\n // edges even though the MV IS registered — forget() would otherwise never reach it.\n // `dispatchMaterializedViewsOnDelete` self-guards O(1) via `mvsForSource` (a no-op when\n // this collection isn't an MV source), so this is safe to call unconditionally. Safe to\n // route through the eager executor's tombstone leg ONLY because that leg is now\n // stamp-scoped (#762) — before that fix this would have opened a NEW forget-time\n // data-loss path into the unscoped tombstone diff.\n if (coll) {\n const mv = await coll.dispatchMaterializedViewsOnDelete(ref.id)\n stats.recordsErased += mv.deleted\n stats.derivedResidueUndecodable.push(...mv.residueUndecodable)\n stats.derivedResidueDeclined.push(...mv.residueDeclined)\n }\n\n const edges = vault.graph.derivedArtifactsOf(ref.collection)\n if (edges.length === 0 || !coll) return\n\n // #650 Task 5 (#648) — 'ref' cascade/nullify propagate ADDITIVELY, here, AFTER the shred\n // (restrict already refused BEFORE any shred — the caller's pre-tombstone check, spec §4).\n // The I/O shell (loop shape, collection accessor) is duplicated, not imported, from\n // `VaultLinks.applyLookupRefsPropagation`/`checkLookupRefsRestrict` (with-shape/links/\n // vault-facade.ts) — the kernel spine may not statically import a with-* service\n // (port-layering, the #638 Task 5 via/dispatch.ts precedent); `vault._getCollection` is\n // cached-only while `VaultLinks`' accessor constructs. The pure match predicate itself is\n // shared through the port seam (#651 Task 3 — `matchesReferencingValue`, `port/with/\n // lookup-strategy.ts`), so only the shell, not the coercion logic, stays duplicated. A\n // referencing edge whose dimension uses a non-default `key` (matrix tier only) needs the\n // backing row's OWN value at that field, not its PUT-id — the row is ALREADY tombstoned by\n // now, so it's read from `lookupCompareKeys`, resolved by the caller from the LIVE row\n // BEFORE the shred (#650 Task 5 review, Important fix — eliminates the post-shred\n // envelope-decode dependency this used to have, which silently skipped propagation whenever\n // that decode failed).\n if (edges.some((e) => e.kind === 'ref')) {\n const { cascaded, nullified, residue } = await applyLookupRefsFanout(vault, ref.collection, ref.id, lookupCompareKeys)\n stats.lookupReferencesCascaded += cascaded\n stats.lookupReferencesNullified += nullified\n stats.lookupReferencesResidue.push(...residue)\n }\n\n if (edges.some((e) => e.kind === 'derivation')) {\n // #622 review Finding 1: count REAL erasures (dispatchArrayDerivationsOnDelete's own\n // `_internalDelete`-backed tally), not the derivation EDGE count — an edge exists whenever\n // this collection is ANY trigger (source/sources[]/triggerBy), but the same-id record-shape\n // guard only erases for `spec.source === this.name`, and `derive()` may never have produced\n // an output row (optional-skip) in the first place. Both cases must contribute 0, not +1.\n stats.recordsErased += await coll.dispatchArrayDerivationsOnDelete(ref.id, true)\n }\n\n if (envelope && edges.some((e) => e.kind === 'rollup')) {\n const priorRecord = await coll._decodeEnvelope(envelope, ref.id)\n if (priorRecord) {\n for (const r of await coll.dispatchRollupsOnDelete(ref.id, priorRecord)) {\n if (r.outcome === 'written') stats.aggregatesRecomputed += 1\n else if (r.outcome === 'skipped-frozen') stats.residueFrozen.push(`${r.into}:${r.parentId}`)\n }\n }\n }\n}\n\n/**\n * Apply `cascade`/`nullify` propagation for `backing`'s non-restrict `'ref'` edges — the\n * forget-path counterpart of `VaultLinks.applyLookupRefsPropagation` (I/O shell duplicated, not\n * imported, match predicate shared via the port — see {@link forgetDerivedFanout}'s call site\n * comment). `restrict` edges are skipped here: the\n * forget loop's `checkLookupRefsRestrict`-equivalent call already refused (or the reference no\n * longer existed) BEFORE `_writeTombstone` ran, so by the time this runs only cascade/nullify\n * remain to propagate. `compareKeys` is the non-`'id'`-`keyField` compare-value map, resolved by\n * the caller from the LIVE row BEFORE the shred (#650 Task 5 review, Important fix) — the live row\n * is already shredded by the time THIS function runs, so it can no longer resolve one itself. A\n * `keyField` absent from (or `undefined` in) the map means that live resolve failed too — the edge\n * is reported as residue instead of silently skipped (never a bare `continue` with no trace).\n */\nasync function applyLookupRefsFanout(\n vault: VaultLike,\n backing: string,\n key: string,\n compareKeys: ReadonlyMap<string, string | undefined>,\n): Promise<{ cascaded: number; nullified: number; residue: string[] }> {\n let cascaded = 0\n let nullified = 0\n const residue: string[] = []\n for (const { referencing, onDelete, keyField } of vault.graph.referencingEdgesOf(backing)) {\n if (onDelete === 'restrict') continue\n const compareKey = keyField === 'id' ? key : compareKeys.get(keyField)\n if (compareKey === undefined) {\n residue.push(`${backing}:${key}:${referencing.collection}.${referencing.field}`)\n continue\n }\n const coll = vault._getCollection(referencing.collection)\n if (!coll) continue\n const matches = (await coll.list()).filter((rec) => matchesReferencingValue(rec, referencing.field, compareKey))\n for (const rec of matches) {\n const id = rec['id'] as string | undefined\n if (id === undefined) continue\n if (onDelete === 'cascade') {\n await coll.delete(id)\n cascaded++\n } else if (onDelete === 'nullify') {\n await coll.put(id, { ...rec, [referencing.field]: null })\n nullified++\n }\n }\n }\n return { cascaded, nullified, residue }\n}\n\n/**\n * Value-equality for a single self-write reverse-denorm field. Scalars\n * compare by identity; objects by canonical JSON (denorm values should be\n * deterministically shaped). Used as the cycle guard — when every denorm\n * field already matches, no write is issued and the self-write recursion ends.\n */\nexport function selfWriteFieldEqual(a: unknown, b: unknown): boolean {\n if (a === b) return true\n if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') return false\n try {\n return JSON.stringify(a) === JSON.stringify(b)\n } catch {\n return false\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
|
+
buildRecordAad,
|
|
2
3
|
openEnvelopeJson
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-CXPOG3ZY.js";
|
|
4
5
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
buildRecordEnvelope
|
|
7
|
+
} from "./chunk-FX3UIDCS.js";
|
|
7
8
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
encrypt
|
|
10
|
+
} from "./chunk-ONPVHX2Y.js";
|
|
10
11
|
|
|
11
12
|
// src/with-shape/persisted-schemas/storage.ts
|
|
12
13
|
var SCHEMAS_COLLECTION = "_schemas";
|
|
@@ -15,7 +16,7 @@ async function loadPersistedSchemaEntry(store, vault, collection, dek) {
|
|
|
15
16
|
if (!envelope) return { version: 0, payload: void 0 };
|
|
16
17
|
const version = envelope._v;
|
|
17
18
|
try {
|
|
18
|
-
const plaintext = await openEnvelopeJson(envelope, dek);
|
|
19
|
+
const plaintext = await openEnvelopeJson({ collection: SCHEMAS_COLLECTION, id: collection }, envelope, dek);
|
|
19
20
|
const parsed = JSON.parse(plaintext);
|
|
20
21
|
if (parsed._noydb_schema !== 1) return { version, payload: void 0 };
|
|
21
22
|
return { version, payload: parsed };
|
|
@@ -28,15 +29,13 @@ async function loadPersistedSchema(store, vault, collection, dek) {
|
|
|
28
29
|
}
|
|
29
30
|
async function savePersistedSchema(store, vault, collection, dek, payload, expectedVersion) {
|
|
30
31
|
const json = JSON.stringify(payload);
|
|
31
|
-
const { iv, data } = await encrypt(json, dek);
|
|
32
32
|
const baseVersion = expectedVersion ?? (await store.get(vault, SCHEMAS_COLLECTION, collection))?._v ?? 0;
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
33
|
+
const identity = { collection: SCHEMAS_COLLECTION, id: collection, version: baseVersion + 1 };
|
|
34
|
+
const { iv, data } = await encrypt(json, dek, buildRecordAad(identity));
|
|
35
|
+
const env = buildRecordEnvelope(
|
|
36
|
+
identity,
|
|
37
|
+
{ iv, data }
|
|
38
|
+
);
|
|
40
39
|
await store.put(vault, SCHEMAS_COLLECTION, collection, env, expectedVersion);
|
|
41
40
|
}
|
|
42
41
|
|
|
@@ -46,4 +45,4 @@ export {
|
|
|
46
45
|
loadPersistedSchema,
|
|
47
46
|
savePersistedSchema
|
|
48
47
|
};
|
|
49
|
-
//# sourceMappingURL=chunk-
|
|
48
|
+
//# sourceMappingURL=chunk-KM6ZXG7U.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/with-shape/persisted-schemas/storage.ts"],"sourcesContent":["/**\n * Read / write the per-collection persisted-schema envelope. Mirrors the\n * standard noy-db record envelope shape and is **AES-GCM encrypted with\n * the collection's DEK** — the schema body (field names, enum values,\n * constraints) is sensitive metadata, so it gets the same encryption\n * envelope as the records it describes.\n *\n * Storage layout:\n *\n * <vault>/_schemas/<collection> → EncryptedEnvelope\n *\n * The DEK passed to {@link savePersistedSchema} / {@link loadPersistedSchema}\n * is the same key the collection uses for its records.\n *\n * @module\n */\n\nimport { buildRecordAad, buildRecordEnvelope, encrypt, openEnvelopeJson, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { PersistedSchemaEnvelope } from './types.js'\n\n/** Reserved collection name where persisted schemas live. */\nexport const SCHEMAS_COLLECTION = '_schemas' as const\n\n/**\n * A decrypted persisted-schema read paired with the wrapping envelope's\n * `_v`. The version is the optimistic-concurrency token: pass it back to\n * {@link savePersistedSchema} as `expectedVersion` to make the write a CAS\n * that rejects a stale put (see #583 — the `_schemas` record is shared by\n * the JSON-Schema writer and the classified-marker writer).\n */\nexport interface PersistedSchemaEntry {\n /** Wrapping envelope `_v` (0 when no record exists yet). */\n readonly version: number\n /** Decrypted payload, or `undefined` when absent/corrupt/wrong-DEK. */\n readonly payload: PersistedSchemaEnvelope | undefined\n}\n\n/**\n * Read the persisted-schema envelope for one collection together with its\n * wrapping `_v`. The version reflects the raw stored envelope even when the\n * payload cannot be decrypted/parsed (so a CAS still guards a corrupt record\n * rather than silently clobbering it).\n */\nexport async function loadPersistedSchemaEntry(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n): Promise<PersistedSchemaEntry> {\n const envelope = await store.get(vault, SCHEMAS_COLLECTION, collection)\n if (!envelope) return { version: 0, payload: undefined }\n const version = envelope._v\n try {\n const plaintext = await openEnvelopeJson({ collection: SCHEMAS_COLLECTION, id: collection }, envelope, dek)\n const parsed = JSON.parse(plaintext) as PersistedSchemaEnvelope\n if (parsed._noydb_schema !== 1) return { version, payload: undefined }\n return { version, payload: parsed }\n } catch {\n return { version, payload: undefined }\n }\n}\n\n/**\n * Read and decrypt the persisted-schema envelope for one collection.\n * Returns `undefined` when no envelope has been written or when decryption\n * fails (e.g. wrong DEK passed). Tolerates corrupted records — JSON parse\n * failures surface as `undefined`, mirroring `_meta/handle`'s contract.\n */\nexport async function loadPersistedSchema(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n): Promise<PersistedSchemaEnvelope | undefined> {\n return (await loadPersistedSchemaEntry(store, vault, collection, dek)).payload\n}\n\n/**\n * Encrypt and persist a schema envelope for one collection. Always\n * overwrites any prior write (callers gate on hash equality before calling\n * to avoid no-op writes).\n *\n * Pass `expectedVersion` — the `version` from {@link loadPersistedSchemaEntry}\n * captured at load time — to make the write an optimistic CAS: on a store with\n * `casAtomic`, `store.put` throws {@link ConflictError} when the record was\n * bumped concurrently, so a lost update on the shared `_schemas` record is\n * rejected instead of silently clobbering the other writer's field (#583).\n * When omitted, keeps the legacy unconditional last-writer-wins behaviour.\n */\nexport async function savePersistedSchema(\n store: NoydbStore,\n vault: string,\n collection: string,\n dek: EnclaveKey,\n payload: PersistedSchemaEnvelope,\n expectedVersion?: number,\n): Promise<void> {\n const json = JSON.stringify(payload)\n const baseVersion = expectedVersion ?? (await store.get(vault, SCHEMAS_COLLECTION, collection))?._v ?? 0\n const identity = { collection: SCHEMAS_COLLECTION, id: collection, version: baseVersion + 1 }\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n const env = buildRecordEnvelope(\n identity,\n { iv, data },\n )\n await store.put(vault, SCHEMAS_COLLECTION, collection, env, expectedVersion)\n}\n"],"mappings":";;;;;;;;;;;;AAsBO,IAAM,qBAAqB;AAsBlC,eAAsB,yBACpB,OACA,OACA,YACA,KAC+B;AAC/B,QAAM,WAAW,MAAM,MAAM,IAAI,OAAO,oBAAoB,UAAU;AACtE,MAAI,CAAC,SAAU,QAAO,EAAE,SAAS,GAAG,SAAS,OAAU;AACvD,QAAM,UAAU,SAAS;AACzB,MAAI;AACF,UAAM,YAAY,MAAM,iBAAiB,EAAE,YAAY,oBAAoB,IAAI,WAAW,GAAG,UAAU,GAAG;AAC1G,UAAM,SAAS,KAAK,MAAM,SAAS;AACnC,QAAI,OAAO,kBAAkB,EAAG,QAAO,EAAE,SAAS,SAAS,OAAU;AACrE,WAAO,EAAE,SAAS,SAAS,OAAO;AAAA,EACpC,QAAQ;AACN,WAAO,EAAE,SAAS,SAAS,OAAU;AAAA,EACvC;AACF;AAQA,eAAsB,oBACpB,OACA,OACA,YACA,KAC8C;AAC9C,UAAQ,MAAM,yBAAyB,OAAO,OAAO,YAAY,GAAG,GAAG;AACzE;AAcA,eAAsB,oBACpB,OACA,OACA,YACA,KACA,SACA,iBACe;AACf,QAAM,OAAO,KAAK,UAAU,OAAO;AACnC,QAAM,cAAc,oBAAoB,MAAM,MAAM,IAAI,OAAO,oBAAoB,UAAU,IAAI,MAAM;AACvG,QAAM,WAAW,EAAE,YAAY,oBAAoB,IAAI,YAAY,SAAS,cAAc,EAAE;AAC5F,QAAM,EAAE,IAAI,KAAK,IAAI,MAAM,QAAQ,MAAM,KAAK,eAAe,QAAQ,CAAC;AACtE,QAAM,MAAM;AAAA,IACV;AAAA,IACA,EAAE,IAAI,KAAK;AAAA,EACb;AACA,QAAM,MAAM,IAAI,OAAO,oBAAoB,YAAY,KAAK,eAAe;AAC7E;","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CargoNotEnabledError,
|
|
3
3
|
QuiesceTimeoutError
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-Z3BZXPRO.js";
|
|
5
5
|
|
|
6
6
|
// src/with-cargo/strategy.ts
|
|
7
7
|
var NO_CARGO = {
|
|
@@ -60,4 +60,4 @@ export {
|
|
|
60
60
|
isQuorum,
|
|
61
61
|
runDrainBarrier
|
|
62
62
|
};
|
|
63
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-LC6NE4IQ.js.map
|
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BLOB_CHUNKS_COLLECTION,
|
|
3
|
+
BLOB_INDEX_COLLECTION
|
|
4
|
+
} from "./chunk-JMJSCI7U.js";
|
|
1
5
|
import {
|
|
2
6
|
installViaBinder
|
|
3
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-C3XWJCG7.js";
|
|
4
8
|
import {
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
9
|
+
buildRecordAad
|
|
10
|
+
} from "./chunk-CXPOG3ZY.js";
|
|
11
|
+
import {
|
|
12
|
+
buildRecordEnvelope
|
|
13
|
+
} from "./chunk-FX3UIDCS.js";
|
|
7
14
|
import {
|
|
8
|
-
|
|
9
|
-
} from "./chunk-
|
|
15
|
+
encrypt
|
|
16
|
+
} from "./chunk-ONPVHX2Y.js";
|
|
10
17
|
import {
|
|
11
18
|
ValidationError
|
|
12
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-Z3BZXPRO.js";
|
|
13
20
|
|
|
14
21
|
// src/via/blob/binding.ts
|
|
15
22
|
async function eraseBlobs(ctx, cfg) {
|
|
@@ -164,9 +171,50 @@ function generateBatchId() {
|
|
|
164
171
|
return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`;
|
|
165
172
|
}
|
|
166
173
|
|
|
174
|
+
// src/with-shape/blobs/orphan-report.ts
|
|
175
|
+
var SAMPLE_LIMIT = 10;
|
|
176
|
+
function parseChunkId(id) {
|
|
177
|
+
const cut = id.lastIndexOf("_");
|
|
178
|
+
if (cut <= 0 || cut === id.length - 1) return null;
|
|
179
|
+
const suffix = id.slice(cut + 1);
|
|
180
|
+
if (!/^\d+$/.test(suffix)) return null;
|
|
181
|
+
return { eTag: id.slice(0, cut), index: Number(suffix) };
|
|
182
|
+
}
|
|
183
|
+
async function reportOrphanBlobChunks(adapter, vault) {
|
|
184
|
+
const empty = { chunks: 0, eTags: 0, sampleETags: [], unparseable: 0 };
|
|
185
|
+
let liveETags;
|
|
186
|
+
let chunkIds;
|
|
187
|
+
try {
|
|
188
|
+
liveETags = new Set(await adapter.list(vault, BLOB_INDEX_COLLECTION));
|
|
189
|
+
chunkIds = await adapter.list(vault, BLOB_CHUNKS_COLLECTION);
|
|
190
|
+
} catch {
|
|
191
|
+
return empty;
|
|
192
|
+
}
|
|
193
|
+
let chunks = 0;
|
|
194
|
+
let unparseable = 0;
|
|
195
|
+
const orphanETags = /* @__PURE__ */ new Set();
|
|
196
|
+
for (const id of chunkIds) {
|
|
197
|
+
const parsed = parseChunkId(id);
|
|
198
|
+
if (parsed === null) {
|
|
199
|
+
unparseable += 1;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (liveETags.has(parsed.eTag)) continue;
|
|
203
|
+
chunks += 1;
|
|
204
|
+
orphanETags.add(parsed.eTag);
|
|
205
|
+
}
|
|
206
|
+
return {
|
|
207
|
+
chunks,
|
|
208
|
+
eTags: orphanETags.size,
|
|
209
|
+
sampleETags: [...orphanETags].slice(0, SAMPLE_LIMIT),
|
|
210
|
+
unparseable
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
167
214
|
// src/with-shape/blobs/blob-compaction.ts
|
|
168
215
|
var BLOB_EVICTION_AUDIT_COLLECTION = "_blob_eviction_audit";
|
|
169
216
|
async function runCompaction(ctx, options = {}) {
|
|
217
|
+
const orphanBlobChunks = await reportOrphanBlobChunks(ctx.adapter, ctx.vault);
|
|
170
218
|
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
171
219
|
const maxEvictions = options.maxEvictions ?? Infinity;
|
|
172
220
|
const dryRun = options.dryRun === true;
|
|
@@ -251,7 +299,8 @@ async function runCompaction(ctx, options = {}) {
|
|
|
251
299
|
pinned,
|
|
252
300
|
budgetEvicted,
|
|
253
301
|
budgetBytesFreed,
|
|
254
|
-
byCollection
|
|
302
|
+
byCollection,
|
|
303
|
+
orphanBlobChunks
|
|
255
304
|
};
|
|
256
305
|
}
|
|
257
306
|
async function runBudgetPass(ctx, maxBytes, opts) {
|
|
@@ -367,27 +416,15 @@ function generateEvictionId(collection, recordId, slotName) {
|
|
|
367
416
|
}
|
|
368
417
|
async function writeAuditEntry(ctx, entry) {
|
|
369
418
|
const json = JSON.stringify(entry);
|
|
419
|
+
const identity = { collection: BLOB_EVICTION_AUDIT_COLLECTION, id: entry.id, by: entry.actor, version: 1 };
|
|
420
|
+
const body = { ts: entry.evictedAt };
|
|
370
421
|
let envelope;
|
|
371
422
|
if (ctx.encrypted) {
|
|
372
423
|
const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION);
|
|
373
|
-
const { iv, data } = await encrypt(json, dek);
|
|
374
|
-
envelope = {
|
|
375
|
-
_noydb: NOYDB_FORMAT_VERSION,
|
|
376
|
-
_v: 1,
|
|
377
|
-
_ts: entry.evictedAt,
|
|
378
|
-
_iv: iv,
|
|
379
|
-
_data: data,
|
|
380
|
-
_by: entry.actor
|
|
381
|
-
};
|
|
424
|
+
const { iv, data } = await encrypt(json, dek, buildRecordAad(identity));
|
|
425
|
+
envelope = buildRecordEnvelope(identity, { ...body, iv, data });
|
|
382
426
|
} else {
|
|
383
|
-
envelope = {
|
|
384
|
-
_noydb: NOYDB_FORMAT_VERSION,
|
|
385
|
-
_v: 1,
|
|
386
|
-
_ts: entry.evictedAt,
|
|
387
|
-
_iv: "",
|
|
388
|
-
_data: json,
|
|
389
|
-
_by: entry.actor
|
|
390
|
-
};
|
|
427
|
+
envelope = buildRecordEnvelope(identity, { ...body, iv: "", data: json });
|
|
391
428
|
}
|
|
392
429
|
await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope);
|
|
393
430
|
}
|
|
@@ -397,7 +434,8 @@ export {
|
|
|
397
434
|
ExportBlobsAbortedError,
|
|
398
435
|
EXPORT_AUDIT_COLLECTION,
|
|
399
436
|
createExportBlobsHandle,
|
|
437
|
+
reportOrphanBlobChunks,
|
|
400
438
|
BLOB_EVICTION_AUDIT_COLLECTION,
|
|
401
439
|
runCompaction
|
|
402
440
|
};
|
|
403
|
-
//# sourceMappingURL=chunk-
|
|
441
|
+
//# sourceMappingURL=chunk-LIR2DJWP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/via/blob/binding.ts","../src/port/with/blob-strategy.ts","../src/with-shape/blobs/export-blobs.ts","../src/with-shape/blobs/orphan-report.ts","../src/with-shape/blobs/blob-compaction.ts"],"sourcesContent":["/**\n * The blob `ViaBinding` (#629 Task 7) — wires the `blobFields` declaration\n * into the kernel's generic Via port as a deliberately THIN binding:\n * declaration + posture + `describeFragment` + `erase` glue, and nothing\n * else. Blob content is fully out-of-band (`collection.blob(id)` →\n * `BlobSet` writing `_blob_*` side-collections; it never touches\n * `_putInternal` or the record codec), so this binding declares **no\n * write/read pipeline hooks** — in particular no `encodeAtRest`/\n * `decodeAtRest`: a blobFields collection must not flip the pipeline's\n * `hasAtRestHooks`, or the codec would abandon its inline seal path for a\n * feature that never seals record fields. The routing/compaction/TTL/\n * legal-hold/exportBlobs machinery stays service-side in\n * `with-shape/blobs/` (the i18n-dictionary precedent — that machinery\n * does real chunk AEAD + key-lifecycle work `via-enclave-isolation`\n * forbids under `via/*`).\n *\n * Like classified (see `port/with/classified-strategy.ts`), the link is\n * EAGER — `port/with/blob-strategy.ts` calls {@link linkBlobVia} at module\n * load. `blobFields` policies are plain object literals (there is no\n * `blob.*()` declaration factory to hang a lazy link on), so the binder\n * must be installed before `compileViaBindings` ever sees one.\n *\n * There is no `declare`-time validation: `blobFields` has never had a\n * construction-time refusal matrix (policies are consulted lazily by\n * `vault.compact()` and `BlobSet.put`), and inventing one here would\n * break the behavior lock.\n *\n * `covers` (#629 Task 8) is a passive coverage predicate, not a pipeline\n * hook — it lets `ViaPipeline.postureFor` recognize a `blobFields` slot so\n * `.where()`/`.orderBy()`/`.aggregate()` can refuse it (posture\n * `queryable: 'none'`); it does not participate in clause building or\n * evaluation.\n */\nimport type { ViaBinding, ViaEraseCtx, ViaEraseReport } from '../../kernel/via/index.js'\nimport { installViaBinder } from '../../kernel/via/index.js'\nimport type { BlobFieldsConfig } from '../../with-shape/blobs/blob-compaction.js'\n\n/**\n * Config a blob collection's `blobFields` declaration resolves to — the\n * binding's construction input. `purgeBlobsForRecord` is an OPTIONAL\n * vault/service-provided closure for the `erase` hook, mapping\n * `collection.blob(id).shredAllForRecord()`'s `{shredded, retainedShared,\n * residue}: string[]` accounting onto `ViaEraseReport` — `shredded`/\n * `retainedShared` become counts, each residue eTag becomes a tagged\n * `{kind: 'blob-residue', eTag}` entry.\n *\n * DELIBERATELY left unwired by `compileViaBindings` (#629 Task 10, resolved\n * within parity-first — see task-10-report.md \"blob purge\" section):\n * `per-blob-cek.test.ts`/`forget.test.ts` prove blob-shred-on-forget is\n * gated SOLELY on the vault's `blobsStrategy` being configured, never on a\n * given collection's OWN `blobFields` declaration (`.blob(id)` is callable,\n * and IS exercised by those tests, on collections with no `blobFields` at\n * all). Routing it exclusively through this binding would silently stop\n * blob crypto-shredding for every such collection; `vault.forget()` keeps\n * calling `collection.blob(id).shredAllForRecord()` directly for that\n * reason. The hook stays real/tested (see `blob-binding.test.ts`) for a\n * future caller that wants it.\n */\nexport interface BlobViaConfig {\n readonly fields: BlobFieldsConfig\n readonly collectionName: string\n /** Purge one record's blob slots (crypto-shred participation). Returns the erase report for `forget()`'s summary. */\n readonly purgeBlobsForRecord?: (id: string) => Promise<ViaEraseReport>\n}\n\n/**\n * `forget()`'s per-ref blob-purge participation: delegate to the wired\n * closure, or report a clean zero-shredded, zero-residue no-op — the\n * default in production too (#629 Task 10 deliberately leaves this unwired;\n * see `BlobViaConfig`'s doc comment).\n */\nasync function eraseBlobs(ctx: ViaEraseCtx, cfg: BlobViaConfig): Promise<ViaEraseReport> {\n if (cfg.purgeBlobsForRecord) return cfg.purgeBlobsForRecord(ctx.id)\n return { shredded: 0, residue: [] }\n}\n\n/**\n * One field's blob knobs as they appear on `describeFragment()`'s payload\n * (#657 — the second real `ViaBinding.describeFragment` consumer after\n * `lookup`'s; see `with-shape/introspection/describe.ts`'s `buildDescription`).\n * Declarative scalars (`retainDays`/`external`/`public`/`backlink`) verbatim;\n * predicate knobs (`evictWhen`/`legalHold`/`retainUntil` — functions over the\n * decrypted record) as presence flags, since a predicate has no serializable\n * form — mirrors `buildBlobDescribeFragment`'s per-field shape below.\n */\nexport interface BlobDescribeFragmentEntry {\n readonly retainDays?: number\n readonly evictWhen?: true\n readonly legalHold?: true\n readonly retainUntil?: true\n readonly external?: true\n readonly public?: true\n readonly backlink?: string\n}\n\n/** The `'blob'` binding's `describeFragment()` payload shape. */\nexport interface BlobDescribeFragment {\n readonly blobFields: Record<string, BlobDescribeFragmentEntry>\n}\n\n/**\n * `{ blobFields: { <field>: <knobs> } }` — declarative scalars\n * (`retainDays`/`external`/`public`/`backlink`) verbatim; predicate knobs\n * (`evictWhen`/`legalHold`/`retainUntil` — functions over the decrypted\n * record) as presence flags, since a predicate has no serializable form.\n */\nfunction buildBlobDescribeFragment(fields: BlobFieldsConfig): Record<string, unknown> {\n return {\n blobFields: Object.fromEntries(\n Object.entries(fields).map(([field, p]) => [field, {\n ...(p.retainDays !== undefined ? { retainDays: p.retainDays } : {}),\n ...(p.evictWhen !== undefined ? { evictWhen: true } : {}),\n ...(p.legalHold !== undefined ? { legalHold: true } : {}),\n ...(p.retainUntil !== undefined ? { retainUntil: true } : {}),\n ...(p.external === true ? { external: true } : {}),\n ...(p.public === true ? { public: true } : {}),\n ...(p.backlink !== undefined ? { backlink: p.backlink } : {}),\n }]),\n ),\n }\n}\n\nexport function blobBinding(cfg: BlobViaConfig): ViaBinding {\n return {\n brand: 'blob',\n // encryptedAtRest: 'envelope' — blob chunks are AEAD-encrypted envelopes\n // in `_blob_chunks` (service-side), not sealed record fields; queryable:\n // 'none' — nothing indexes blob content; exportable — vault.exportBlobs()\n // is a first-class door; forgettable — shredAllForRecord participates in\n // vault.forget().\n posture: { encryptedAtRest: 'envelope', queryable: 'none', exportable: true, forgettable: true },\n covers: (field) => field in cfg.fields,\n erase: (ctx) => eraseBlobs(ctx, cfg),\n describeFragment: () => buildBlobDescribeFragment(cfg.fields),\n }\n}\n\nexport function linkBlobVia(): void {\n installViaBinder('blob', (c) => blobBinding(c as BlobViaConfig))\n}\n","/**\n * Strategy seam between core Collection and the optional blob service\n * (#629 Task 7 — moved out of `with-shape/blobs/strategy.ts`, precedent:\n * `port/with/classified-strategy.ts`/`i18n-strategy.ts`). Lives on the\n * `/with` port (the one seam the kernel spine may import statically) so\n * `Collection`/`Vault` can hold the `NO_BLOBS` default without a\n * spine→service static import.\n *\n * Core imports `BlobsStrategy` as a TYPE-ONLY symbol and `NO_BLOBS` as a\n * minimal runtime stub. Neither pulls in the heavy `BlobSet` / chunk /\n * MIME machinery — those only arrive when the consumer explicitly\n * imports `@noy-db/hub/blobs` (see `via/blob/index.ts` →\n * `withBlobs()` factory).\n *\n * This file is intentionally tiny so the bundler keeps it in the graph\n * without dragging everything else in.\n *\n * @internal\n */\n\nimport type { BlobSet } from '../../with-shape/blobs/blob-set.js'\nimport type { NoydbStore } from '../../kernel/types.js'\nimport type { ObjectProjection } from '../../with-shape/blobs/object-projection.js'\nimport type { BlobFieldsConfig } from '../../with-shape/blobs/blob-compaction.js'\nimport type { EnclaveKey } from '../../kernel/enclave/index.js'\nimport type { BlobCacheStats } from '../../with-shape/blobs/blob-pinning.js'\nimport type { UnlockedKeyring } from '../../with-party/team/keyring.js'\nimport { linkBlobVia } from '../../via/blob/binding.js'\n\n// Install the blob Via binder EAGERLY (#629 Task 7) — `blobFields` policies\n// are plain object literals (there is no `blob.*()` declaration factory to\n// hang a lazy money/i18n-style link on), so the binder must be installed\n// before `compileViaBindings` ever compiles one. This port module is\n// already unconditionally imported by the kernel spine (for the `NO_BLOBS`\n// strategy default below), so linking here guarantees it — same rationale\n// as `port/with/classified-strategy.ts`'s eager `linkClassifiedVia()`. The\n// binding module is hook-free glue (declare/describe/erase) and imports no\n// machinery, so the eager cost is a few hundred bytes, not `BlobSet`.\nlinkBlobVia()\n\n/**\n * Args forwarded by `Collection.blob(id)` to the active strategy's\n * `openSlot`. The strategy is responsible for returning a live\n * `BlobSet` bound to the given record.\n *\n * @internal\n */\nexport interface BlobsStrategyOpenArgs {\n readonly store: NoydbStore\n readonly vault: string\n readonly collection: string\n readonly recordId: string\n readonly getDEK: (collectionName: string) => Promise<EnclaveKey>\n readonly encrypted: boolean\n readonly userId: string\n /**\n * Collection opts into per-record keys (`perRecordKeys`), so its blobs are\n * erasable: new uploads mint a per-blob content CEK (crypto-shreddable at\n * `refCount → 0`). Off → legacy shared-`_blob`-DEK chunks. See the per-blob\n * CEK design spec.\n */\n readonly erasableBlobs?: boolean\n /**\n * Collection has `tiers` active (#724 I1 completion). Combined with\n * `erasableBlobs` false, a content write is refused —\n * `UnsupportedTierCompositionError` — since a legacy blob has no\n * per-blob `_cek` and can never be tier-isolated on elevate/demote.\n */\n readonly tiersActive?: boolean\n /**\n * Vault is in debug-plaintext mode (`encrypt: false` + `debugPlaintext: true`).\n * Blobs are then written as a single un-gzipped object (one base64 chunk) so\n * the stored object is directly recoverable with native tools (`base64 -d`),\n * matching the directly-inspectable record layout. Dev-only — see the\n * plaintext/debug-store-mode design.\n */\n readonly debugPlaintext?: boolean\n /**\n * Object projection for `external` blob fields (`createNoydb({ objectStore })`).\n * When present, fields declared `external` in `blobFields` route their raw\n * bytes here instead of the encrypted-chunk path.\n */\n readonly objectStore?: ObjectProjection\n /** Per-collection blob field policies — used to resolve `external` / `public`. */\n readonly blobFields?: BlobFieldsConfig\n /**\n * The caller's unlocked keyring (#749). Threaded through so `BlobSet.atTier()`\n * can run `assertTierAccess` — the SAME clearance gate `putAtTier`/`elevate`/\n * `demote` run before any tier `getDEK` call — rather than relying on a bare\n * `getDEK`, which auto-mints a DEK for ANY role and never throws (verified: it\n * is not itself a gate). Omitted only by a construction path that never calls\n * `atTier()`.\n */\n readonly keyring?: UnlockedKeyring\n}\n\n/**\n * The seam interface. `@internal` — do not build public APIs on this\n * shape; it can evolve freely until blobs are extracted into their\n * own package, at which point it will be promoted to public.\n *\n * @internal\n */\nexport interface BlobsStrategy {\n openSlot(args: BlobsStrategyOpenArgs): BlobSet\n /**\n * Snapshot of this device's blob-cache KPI counters: local-read hits,\n * network misses, and total bytes downloaded from the object store (#808).\n *\n * Lives on the seam rather than on the `withBlobs()` return type so the\n * service has exactly one name (#844b) — `NO_BLOBS` answers with zeros,\n * which is the truthful reading of \"blobs were never enabled\".\n */\n cacheStats(): BlobCacheStats\n}\n\n/**\n * Default strategy for collections that did not opt into blob storage.\n * Every operation surfaces an actionable error that points the caller\n * at the opt-in path.\n *\n * @internal\n */\nexport const NO_BLOBS: BlobsStrategy = {\n openSlot() {\n throw new Error(\n 'Blob storage is not enabled on this Noydb instance. ' +\n 'Import `{ withBlobs }` from \"@noy-db/hub/blobs\" and pass `withBlobs()` to `createNoydb({ blobsStrategy: withBlobs() })`.',\n )\n },\n cacheStats() {\n return { hits: 0, misses: 0, bytesDownloaded: 0 }\n },\n}\n","/**\n * `vault.exportBlobs()` — bulk blob extraction primitive.\n *\n * Async-iterable handle over every blob attached to records in a\n * vault, optionally filtered by collection allowlist and per-record\n * predicate. Emits tuples of `{ blobId, recordRef, bytes, meta }` so\n * the consumer can pipe into any sink (zip stream, S3 multipart, USB\n * copy, cold-storage tape) without pulling the whole export into\n * memory.\n *\n * ## Auth + audit\n *\n * - Capability check runs **once** at handle creation via\n * `Vault.assertCanExport('plaintext', 'blob')`. An operator whose\n * keyring lacks that bit fails before a single byte of ciphertext\n * is decrypted.\n * - Audit entry lands in `_export_audit` at handle creation: the\n * actor, start timestamp, target collections, predicate presence,\n * and batch mechanism. **No content hashes** — per the spec\n * non-correlation invariant.\n *\n * ## Abort + resume\n *\n * - `handle.abort()` flips the internal signal; the next iteration\n * boundary throws `AbortError`. Consumers already in `for await`\n * can catch and exit cleanly.\n * - Restart after a partial failure with `{ afterBlobId }` — the\n * iterator skips tuples up to (and including) that blob id before\n * yielding again. Combined with a blob-count ceiling it supports\n * idempotent batch re-runs.\n *\n * @module\n */\n\nimport type { Collection } from '../../kernel/collection.js'\nimport type { SlotInfo } from '../../kernel/types.js'\n\n// ─── Types ──────────────────────────────────────────────────────────────\n\nexport interface ExportBlobsOptions {\n /**\n * Collection allowlist. Omit to export blobs from every collection\n * the caller has read access to.\n */\n readonly collections?: readonly string[]\n /**\n * Per-record predicate. Called on the decrypted record BEFORE any\n * blob bytes are read for that record — returning false skips the\n * record and all its slots without touching their chunks.\n */\n readonly where?: (record: unknown, context: { collection: string; id: string }) => boolean\n /**\n * Resume after a specific blob id. The iterator skips tuples up to\n * and including this id, then yields. Format of the id is the same\n * as `ExportedBlob.blobId` (the HMAC-keyed eTag).\n */\n readonly afterBlobId?: string\n /**\n * External abort signal. When fired, the next iterator tick throws\n * `ExportBlobsAbortedError`. Honored alongside `handle.abort()`.\n */\n readonly signal?: AbortSignal\n}\n\nexport interface ExportedBlob {\n /** Opaque blob identifier — HMAC-keyed eTag, stable across vaults. */\n readonly blobId: string\n /** Where this blob came from in the vault. */\n readonly recordRef: {\n readonly collection: string\n readonly id: string\n readonly slot: string\n }\n /** Decrypted plaintext bytes. */\n readonly bytes: Uint8Array\n /** Best-effort metadata (from the blob slot record). */\n readonly meta: {\n readonly size: number\n /**\n * User-visible filename stored on the slot. Often equal to the\n * slot name; differs when the caller supplied an explicit\n * `filename` to `BlobSet.put()`.\n */\n readonly filename: string\n readonly mimeType?: string\n readonly createdAt?: string\n }\n}\n\nexport interface ExportBlobsHandle extends AsyncIterable<ExportedBlob> {\n /** Abort the export. Safe to call multiple times. */\n abort(): void\n /** True once `abort()` has fired or the external signal aborted. */\n readonly aborted: boolean\n}\n\nexport class ExportBlobsAbortedError extends Error {\n constructor(reason: string) {\n super(`exportBlobs aborted: ${reason}`)\n this.name = 'ExportBlobsAbortedError'\n }\n}\n\n// ─── Audit ──────────────────────────────────────────────────────────────\n\nexport const EXPORT_AUDIT_COLLECTION = '_export_audit'\n\nexport interface ExportBlobsAuditEntry {\n readonly id: string\n readonly mechanism: 'exportBlobs'\n readonly actor: string\n readonly startedAt: string\n readonly collections: readonly string[] | null\n readonly predicate: boolean\n readonly afterBlobId: string | null\n}\n\n// ─── Implementation ─────────────────────────────────────────────────────\n\n/**\n * Build the handle. Factored out of `Vault.exportBlobs` so the\n * implementation can be unit-tested without going through the\n * compartment lifecycle.\n */\nexport function createExportBlobsHandle(\n actor: string,\n listAccessibleCollections: () => Promise<string[]>,\n getCollection: <T>(name: string) => Collection<T>,\n writeAudit: (entry: ExportBlobsAuditEntry) => Promise<void>,\n options: ExportBlobsOptions,\n): ExportBlobsHandle {\n let aborted = false\n\n const abort = (): void => {\n aborted = true\n }\n\n if (options.signal) {\n if (options.signal.aborted) aborted = true\n options.signal.addEventListener('abort', () => { aborted = true })\n }\n\n function assertLive(): void {\n if (aborted) throw new ExportBlobsAbortedError('aborted by caller')\n }\n\n const allowlist = options.collections ? new Set(options.collections) : null\n\n // Write the audit entry BEFORE the first yield so a blocked\n // iteration still leaves an audit trail that the export started.\n let auditPromise: Promise<void> | null = null\n function writeAuditOnce(): Promise<void> {\n if (!auditPromise) {\n auditPromise = writeAudit({\n id: generateBatchId(),\n mechanism: 'exportBlobs',\n actor,\n startedAt: new Date().toISOString(),\n collections: options.collections ?? null,\n predicate: Boolean(options.where),\n afterBlobId: options.afterBlobId ?? null,\n })\n }\n return auditPromise\n }\n\n async function* generate(): AsyncGenerator<ExportedBlob> {\n await writeAuditOnce()\n assertLive()\n\n // Resolve target collections lazily — also keeps the call async.\n const allCollections = await listAccessibleCollections()\n const targets = allCollections.filter(name => {\n if (name.startsWith('_')) return false\n if (allowlist && !allowlist.has(name)) return false\n return true\n })\n\n let resumeCursorHit = options.afterBlobId === undefined\n\n for (const collectionName of targets) {\n if (aborted) return\n\n const coll = getCollection<Record<string, unknown>>(collectionName)\n const records = await coll.list().catch(() => [])\n for (const record of records) {\n if (aborted) return\n assertLive()\n\n const idField = (record as { id?: unknown }).id\n if (typeof idField !== 'string') continue\n\n if (options.where && !options.where(record, { collection: collectionName, id: idField })) continue\n\n const blobSet = coll.blob(idField)\n const slots = await blobSet.list().catch(() => [] as SlotInfo[])\n for (const slot of slots) {\n if (aborted) return\n\n if (!resumeCursorHit) {\n if (slot.eTag === options.afterBlobId) {\n resumeCursorHit = true\n }\n continue\n }\n\n const bytes = await blobSet.get(slot.name)\n if (!bytes) continue\n\n const item: ExportedBlob = {\n blobId: slot.eTag,\n recordRef: { collection: collectionName, id: idField, slot: slot.name },\n bytes,\n meta: {\n size: slot.size,\n filename: slot.filename,\n ...(slot.mimeType !== undefined && { mimeType: slot.mimeType }),\n ...(slot.uploadedAt !== undefined && { createdAt: slot.uploadedAt }),\n },\n }\n yield item\n }\n }\n }\n }\n\n const handle: ExportBlobsHandle = {\n abort,\n get aborted() { return aborted },\n [Symbol.asyncIterator]: () => generate(),\n }\n return handle\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────\n\nfunction generateBatchId(): string {\n // 16 bytes of crypto randomness, URL-safe base64, no padding.\n const raw = globalThis.crypto.getRandomValues(new Uint8Array(16))\n let s = ''\n for (const b of raw) s += b.toString(16).padStart(2, '0')\n return `batch-${Date.now().toString(36)}-${s.slice(0, 12)}`\n}\n","/**\n * Orphan blob-chunk REPORT (#1133) — the read-only half of the residue #1127\n * stopped producing.\n *\n * Before #1127, `releaseRef` deleted a blob's index row BEFORE its chunks, so a\n * crash in between stranded chunk bodies nothing can ever reach: `loadBlobObject`\n * returns null so no reader addresses them, and `rekeyBlobSet` derives its chunk\n * ids from each index entry's `chunkCount` so a rotation never visits them. For a\n * LEGACY blob (bytes under the shared `_blob` DEK rather than a per-blob content\n * CEK) those bodies then stay openable under the retired DEK indefinitely.\n *\n * #1127 reversed the order, which removes the class at the source. It does\n * nothing for orphans created before the fix, and this module does not delete\n * them either. It counts them.\n *\n * ## Why this REPORTS and does not RECLAIM\n *\n * Deciding \"orphaned\" requires `store.list(_blob_index)`, and the store is\n * untrusted — that is the premise of the product, not a caveat on it. A store\n * that withholds a single index row makes a LIVE blob's chunks look orphaned. A\n * reclaim pass acting on that would convert **withholding**, which is reversible\n * the moment the store stops lying, into **permanent destruction** — handing an\n * untrusted store a data-loss primitive it does not currently have, in order to\n * tidy up residue.\n *\n * Reporting inverts the risk to something harmless: a lying store can only\n * inflate this count. Nothing is deleted on its word, so the worst outcome is an\n * operator investigating residue that turns out not to exist.\n *\n * Re-keying is not an alternative either: the chunk AAD is\n * `{eTag}:{chunkIndex}:{chunkCount}`, and `chunkCount` lives only in the index\n * row that is already gone — an orphan's AAD is unreconstructable, so it cannot\n * be decrypted-and-re-encrypted at all.\n */\n\nimport type { NoydbStore } from '../../kernel/types.js'\nimport { BLOB_INDEX_COLLECTION, BLOB_CHUNKS_COLLECTION } from './blob-set.js'\n\n/** How many distinct orphaned eTags to name in the report. */\nconst SAMPLE_LIMIT = 10\n\nexport interface OrphanChunkReport {\n /** Chunk rows whose eTag has no `_blob_index` entry. */\n readonly chunks: number\n /** Distinct eTags those chunks belong to. */\n readonly eTags: number\n /**\n * Up to {@link SAMPLE_LIMIT} of those eTags, so an operator can inspect a real\n * one rather than only a count. Deterministic (store list order), not random.\n */\n readonly sampleETags: readonly string[]\n /**\n * Chunk ids that do not match the `{eTag}_{index}` grammar at all. Counted\n * rather than skipped: an id this module cannot parse is a thing it cannot\n * classify either way, and reporting zero orphans while silently dropping\n * such ids would be the reassuring-but-wrong answer.\n */\n readonly unparseable: number\n}\n\n/**\n * Split a chunk id into `{eTag, index}`, or `null` if it does not fit the\n * grammar `BlobSet` writes (`${eTag}_${i}`).\n *\n * Splits on the LAST underscore and requires a digits-only suffix. eTags are\n * HMAC-SHA-256 hex today and contain no underscore, so a simpler split would\n * work — but it would break silently if that ever changed, and this rule holds\n * either way.\n */\nfunction parseChunkId(id: string): { eTag: string; index: number } | null {\n const cut = id.lastIndexOf('_')\n if (cut <= 0 || cut === id.length - 1) return null\n const suffix = id.slice(cut + 1)\n if (!/^\\d+$/.test(suffix)) return null\n return { eTag: id.slice(0, cut), index: Number(suffix) }\n}\n\n/**\n * Count chunk rows with no surviving index row. Read-only: two `list` calls and\n * no `get`, no `delete`, and no decryption — an orphan cannot be opened anyway.\n *\n * A store that cannot list either collection yields an empty report rather than\n * throwing: this is a diagnostic riding along with a maintenance pass, and it\n * must not be able to fail one.\n */\nexport async function reportOrphanBlobChunks(\n adapter: NoydbStore,\n vault: string,\n): Promise<OrphanChunkReport> {\n const empty: OrphanChunkReport = { chunks: 0, eTags: 0, sampleETags: [], unparseable: 0 }\n let liveETags: Set<string>\n let chunkIds: string[]\n try {\n liveETags = new Set(await adapter.list(vault, BLOB_INDEX_COLLECTION))\n chunkIds = await adapter.list(vault, BLOB_CHUNKS_COLLECTION)\n } catch {\n return empty\n }\n\n let chunks = 0\n let unparseable = 0\n const orphanETags = new Set<string>()\n for (const id of chunkIds) {\n const parsed = parseChunkId(id)\n if (parsed === null) { unparseable += 1; continue }\n if (liveETags.has(parsed.eTag)) continue\n chunks += 1\n orphanETags.add(parsed.eTag)\n }\n\n return {\n chunks,\n eTags: orphanETags.size,\n sampleETags: [...orphanETags].slice(0, SAMPLE_LIMIT),\n unparseable,\n }\n}\n","/**\n * Blob retention + compaction.\n *\n * Declarative per-collection / per-slot eviction policy. Two\n * triggers:\n *\n * - **`retainDays`** — age-based TTL. A slot uploaded more than N\n * days ago is evicted.\n * - **`evictWhen(record)`** — predicate over the **decrypted**\n * record. Lets consumers express \"the image is safe to drop once\n * the structured invoice has been reviewed and confirmed.\"\n *\n * Either trigger (or both) causes the slot to evict. Eviction removes\n * the slot entry from `_blob_slots_{collection}`, decrements the\n * blob's refCount (so unreferenced chunks can be GC'd by the next\n * sweep), and writes one entry to the `_blob_eviction_audit`\n * collection for tamper-evident record-keeping.\n *\n * The audit entry carries the eTag of the evicted blob (opaque HMAC\n * of plaintext under the vault's `_blob` DEK) — no plaintext leakage,\n * per the SPEC non-correlation invariant. Consumers reconstructing\n * \"what used to be attached\" can look up the audit entry by record\n * id.\n *\n * Compaction is **consumer-scheduled** — noy-db never runs a\n * background daemon. Call `vault.compact()` whenever your workflow\n * allows (cron, manual \"tidy\" button, cold-storage export prep, …).\n *\n * @module\n */\n\nimport type { NoydbStore, EncryptedEnvelope, SlotInfo } from '../../kernel/types.js'\nimport { ValidationError } from '../../kernel/errors.js'\nimport { buildRecordAad, buildRecordEnvelope, encrypt, type EnclaveKey } from '../../kernel/enclave/index.js'\nimport { reportOrphanBlobChunks, type OrphanChunkReport } from './orphan-report.js'\n\n// ─── Config types ───────────────────────────────────────────────────────\n\nexport interface BlobFieldPolicy<T = unknown> {\n /**\n * Via port brand marker — lets a `BlobFieldPolicy` satisfy the kernel's\n * opaque `ViaDescriptor` (#629 Task 7). Optional (the\n * `ClassifiedFieldSpec._viaBrand` precedent): `blobFields` policies are\n * plain object literals with no declaration factory to stamp it, so a\n * mandatory field would break every existing declaration.\n */\n readonly _viaBrand?: 'blob'\n /**\n * Age-based TTL in days. A slot whose `uploadedAt` is older than\n * `now - retainDays × 86400s` evicts on the next `vault.compact()`.\n * Omit to disable age-based eviction.\n */\n readonly retainDays?: number\n /**\n * Predicate evaluated against the decrypted record. When it returns\n * `true`, every matching slot on that record evicts. Omit to\n * disable predicate-based eviction.\n */\n readonly evictWhen?: (record: T) => boolean\n /**\n * **Legal hold.** When this predicate returns `true`, the slot is\n * never evicted — `retainDays`/`evictWhen` are overridden. Use for a\n * litigation / audit hold on a fiscal document: the blob stays until\n * the predicate returns `false` (the hold is released). Fail-closed:\n * if the predicate throws, the slot is treated as held.\n */\n readonly legalHold?: (record: T) => boolean\n /**\n * **Period-bound retention.** Returns the date (Date / ISO string /\n * epoch ms) until which the slot must be retained — typically derived\n * from the record's fiscal period (e.g. period end + 10 years). While\n * `now < retainUntil`, the slot is never evicted, regardless of\n * `retainDays`. Return `null`/`undefined` to impose no floor.\n * Fail-closed: a throwing function holds the slot.\n */\n readonly retainUntil?: (record: T) => Date | string | number | null | undefined\n /**\n * **External projection.** When `true`, this field's bytes are stored in the\n * vault's `ObjectProjection` (`createNoydb({ objectStore })`) as a single raw,\n * **unencrypted** object — servable directly from S3/CDN and processable by\n * native tooling — instead of the encrypted-chunk path. The encrypted slot\n * record remains the catalog (anchoring invariant). Requires an `objectStore`;\n * **outside the zero-knowledge guarantee** — use only for assets meant to\n * leave the vault. See the as-aws-s3 design spec.\n */\n readonly external?: boolean\n /**\n * For an `external` field: make the object world-readable (CDN origin) rather\n * than presigned-only. Default `false` (presigned). Ignored unless `external`.\n */\n readonly public?: boolean\n /**\n * For an `external` field: how to stamp a **backlink** (this record's\n * vault/collection/id/field) onto the object's metadata — the self-describing\n * \"secondary store\" that powers reconcile / DR / import re-pairing.\n * - `'opaque-token'` (default): a random id; preserves the opaque-bucket\n * property (no names leak); the token is also recorded on the slot.\n * - `'encrypted'`: the reference encrypted under the blob DEK (ZK-preserving;\n * falls back to `'opaque-token'` on a plaintext vault).\n * - `'plain'`: the reference in cleartext metadata — **leaks structure** to\n * bucket readers; only for non-sensitive deployments.\n * - `'none'`: no backlink.\n */\n readonly backlink?: 'opaque-token' | 'encrypted' | 'plain' | 'none'\n}\n\nexport type BlobFieldsConfig<T = unknown> = Record<string, BlobFieldPolicy<T>>\n\n// ─── Audit collection ──────────────────────────────────────────────────\n\nexport const BLOB_EVICTION_AUDIT_COLLECTION = '_blob_eviction_audit'\n\nexport interface BlobEvictionEntry {\n readonly id: string\n readonly collection: string\n readonly recordId: string\n readonly slotName: string\n readonly blobHash: string\n /** `'budget'` — evicted by the #808 cache-budget LRU pass, not a policy. */\n readonly reason: 'ttl' | 'predicate' | 'both' | 'budget'\n readonly evictedAt: string\n readonly actor: string\n}\n\n// ─── Compaction result ──────────────────────────────────────────────────\n\nexport interface CompactionResult {\n /** Number of blob slots evicted by the policy pass across all collections. */\n readonly evicted: number\n /** Number of records touched (iterated + policy checked). */\n readonly records: number\n /** Number of collections with `blobFields` configured. */\n readonly collections: number\n /**\n * Number of audit entries written — one per policy eviction plus one per\n * budget SLOT eviction (dropping a device-local cache copy writes none).\n */\n readonly auditEntries: number\n /**\n * Number of slots that would have evicted (TTL/predicate triggered)\n * but were retained by a `legalHold` or `retainUntil` floor.\n */\n readonly held: number\n /**\n * #808: number of slots whose due policy eviction was skipped because they\n * are pinned for offline on THIS device (pin state is device-local).\n */\n readonly pinned: number\n /**\n * #808: number of evictions performed by the `cacheBudget` LRU pass —\n * internal slots evicted through the standard eviction writer plus\n * device-local external cache copies dropped. 0 when no budget was given.\n */\n readonly budgetEvicted: number\n /** #808: plaintext bytes freed by the `cacheBudget` LRU pass. */\n readonly budgetBytesFreed: number\n /** Per-collection breakdown for diagnostics. */\n readonly byCollection: Record<string, { records: number; evicted: number }>\n /**\n * #1133 — chunk rows in `_blob_chunks` with no surviving `_blob_index` entry.\n * Pre-#1127 residue: unreachable by every reader AND by rotation, so for a\n * legacy blob the bytes stay openable under a retired `_blob` DEK.\n *\n * REPORTED, never reclaimed — deciding \"orphaned\" means trusting the store's\n * `list`, and a store that withholds one index row would have us destroy a\n * LIVE blob's chunks. See `orphan-report.ts` for the full argument. A vault\n * that has run `migrate()` has no legacy chunks to strand, and `chunks: 0`\n * is the expected reading everywhere else.\n */\n readonly orphanBlobChunks: OrphanChunkReport\n}\n\n// ─── Core ──────────────────────────────────────────────────────────────\n\nexport interface CompactRunOptions {\n /** Override \"now\" for deterministic testing. */\n readonly now?: Date\n /**\n * Stop after this many evictions. Useful for capped batches / cron\n * jobs that need to fit in a time window. `undefined` = unbounded.\n */\n readonly maxEvictions?: number\n /**\n * Dry-run — evaluate policies and return the counts, but do NOT\n * delete slots or write audit entries. Lets a consumer preview\n * what would happen.\n */\n readonly dryRun?: boolean\n /**\n * #808 mobile cache budget: cap the locally-cached UNPINNED blob bytes.\n * After the policy pass, a dedicated LRU pass walks every collection's\n * slots (pinned slots and `legalHold`/`retainUntil`-held slots are exempt\n * and uncounted) and evicts oldest-access-first — internal slots through\n * the standard eviction writer (`deleteSlot` + a `'budget'` audit entry),\n * external slots by dropping their device-local encrypted cache copy only\n * (the object-store copy is authoritative and untouched) — until the\n * remaining unpinned bytes fit `maxBytes`. Internal slots count their\n * uncompressed `size`; external slots count `cachedBytes` (0 = nothing\n * local, nothing to evict). LRU order comes from the device-local\n * `lastAccessAt` stamp, falling back to `uploadedAt`.\n */\n readonly cacheBudget?: { readonly maxBytes: number }\n}\n\nexport interface CompactionContext {\n readonly adapter: NoydbStore\n readonly vault: string\n readonly actor: string\n readonly encrypted: boolean\n readonly getDEK: (collection: string) => Promise<EnclaveKey>\n /**\n * Resolve a collection's declared `blobFields` config. Returns an\n * empty map for collections without the config — the walk skips\n * those.\n */\n readonly getBlobFields: <T>(collection: string) => BlobFieldsConfig<T> | null\n /** List collection names in the vault. */\n readonly listCollections: () => Promise<string[]>\n /** List record ids in a collection. */\n readonly listRecords: (collection: string) => Promise<string[]>\n /** Decrypt and return the record. Null when absent. */\n readonly getRecord: <T>(collection: string, id: string) => Promise<T | null>\n /** Return the BlobSet-like handle for a record's slots. */\n readonly listSlots: (collection: string, id: string) => Promise<SlotInfo[]>\n /** Delete a slot and decrement its blob's refCount. */\n readonly deleteSlot: (collection: string, id: string, slotName: string) => Promise<void>\n /**\n * #808: drop a slot's DEVICE-LOCAL cached copy (the encrypted external\n * side-cache) without touching the slot or the object store. Returns the\n * plaintext bytes freed. Optional — absent (or a 0 return) means external\n * cache entries simply cannot be budget-evicted through this context.\n */\n readonly dropLocalCache?: (collection: string, id: string, slotName: string) => Promise<number>\n}\n\nexport async function runCompaction(\n ctx: CompactionContext,\n options: CompactRunOptions = {},\n): Promise<CompactionResult> {\n // #1133 — two `list` calls, no decryption, nothing written. Always on rather\n // than opt-in: residue an operator has to know to ask about is residue nobody\n // finds. The cost is negligible beside this pass's own per-record `getRecord`.\n const orphanBlobChunks = await reportOrphanBlobChunks(ctx.adapter, ctx.vault)\n const now = options.now ?? new Date()\n const maxEvictions = options.maxEvictions ?? Infinity\n const dryRun = options.dryRun === true\n if (options.cacheBudget !== undefined\n && (!Number.isFinite(options.cacheBudget.maxBytes) || options.cacheBudget.maxBytes < 0)) {\n throw new ValidationError('compact(): cacheBudget.maxBytes must be a non-negative finite number (#808)')\n }\n\n const allCollections = await ctx.listCollections()\n const byCollection: Record<string, { records: number; evicted: number }> = {}\n let evicted = 0\n let records = 0\n let auditEntries = 0\n let held = 0\n let pinned = 0\n let collectionsWithPolicy = 0\n\n outer: for (const collectionName of allCollections) {\n if (collectionName.startsWith('_')) continue\n const config = ctx.getBlobFields(collectionName)\n if (!config) continue\n const configuredSlots = Object.keys(config)\n if (configuredSlots.length === 0) continue\n collectionsWithPolicy += 1\n byCollection[collectionName] = { records: 0, evicted: 0 }\n\n const ids = await ctx.listRecords(collectionName)\n for (const recordId of ids) {\n if (evicted >= maxEvictions) break outer\n\n const record = await ctx.getRecord(collectionName, recordId).catch(() => null)\n if (record === null) continue\n records += 1\n byCollection[collectionName].records += 1\n\n const slots = await ctx.listSlots(collectionName, recordId).catch(() => [])\n for (const slot of slots) {\n if (evicted >= maxEvictions) break outer\n const policy = config[slot.name]\n if (!policy) continue\n\n const reason = evaluatePolicy(policy, record, slot, now)\n if (!reason) continue\n\n // #808: device-local offline pin — an exemption inside this existing\n // pass, exactly like a hold: counted, never evicted. `slot.pinned` is\n // the device-local annotation `BlobSet.list()` stamps from the\n // withBlobs() pin registry (never synced state).\n if (slot.pinned === true) {\n pinned += 1\n continue\n }\n\n // Retention floor: a legal hold or period-bound retainUntil\n // blocks an otherwise-due eviction. Counted, never evicted.\n if (isHeld(policy, record, now)) {\n held += 1\n continue\n }\n\n if (!dryRun) {\n await ctx.deleteSlot(collectionName, recordId, slot.name)\n await writeAuditEntry(ctx, {\n id: generateEvictionId(collectionName, recordId, slot.name),\n collection: collectionName,\n recordId,\n slotName: slot.name,\n blobHash: slot.eTag,\n reason,\n evictedAt: now.toISOString(),\n actor: ctx.actor,\n })\n auditEntries += 1\n }\n evicted += 1\n byCollection[collectionName].evicted += 1\n }\n }\n }\n\n // #808: cache-budget LRU pass — runs AFTER the policy pass so a slot that\n // just evicted no longer counts toward the budget. Reuses this module's own\n // eviction writer (deleteSlot + audit), never a parallel one.\n let budgetEvicted = 0\n let budgetBytesFreed = 0\n if (options.cacheBudget !== undefined) {\n const pass = await runBudgetPass(ctx, options.cacheBudget.maxBytes, {\n now, dryRun, remainingEvictions: maxEvictions - evicted,\n })\n budgetEvicted = pass.evicted\n budgetBytesFreed = pass.bytesFreed\n auditEntries += pass.auditEntries\n }\n\n return {\n evicted,\n records,\n collections: collectionsWithPolicy,\n auditEntries,\n held,\n pinned,\n budgetEvicted,\n budgetBytesFreed,\n byCollection,\n orphanBlobChunks,\n }\n}\n\n// ─── #808: cache-budget LRU pass ───────────────────────────────────────\n\ninterface BudgetCandidate {\n readonly collection: string\n readonly recordId: string\n readonly slotName: string\n readonly eTag: string\n readonly bytes: number\n /** `'slot'` — internal blob, evicted via `deleteSlot`; `'cache'` — external device-local copy, dropped via `dropLocalCache`. */\n readonly kind: 'slot' | 'cache'\n readonly lastAccessMs: number\n}\n\n/**\n * Enforce `cacheBudget.maxBytes` over the locally-cached UNPINNED blob bytes\n * (see {@link CompactRunOptions.cacheBudget} for the full contract). Walks\n * EVERY collection — unlike the policy pass, the budget is not gated on a\n * declared `blobFields` config — but still honors a declared policy's\n * `legalHold`/`retainUntil` floor where one exists.\n */\nasync function runBudgetPass(\n ctx: CompactionContext,\n maxBytes: number,\n opts: { now: Date; dryRun: boolean; remainingEvictions: number },\n): Promise<{ evicted: number; bytesFreed: number; auditEntries: number }> {\n const candidates: BudgetCandidate[] = []\n let total = 0\n\n for (const collectionName of await ctx.listCollections()) {\n if (collectionName.startsWith('_')) continue\n const config = ctx.getBlobFields(collectionName)\n for (const recordId of await ctx.listRecords(collectionName)) {\n const slots = await ctx.listSlots(collectionName, recordId).catch(() => [])\n if (slots.length === 0) continue\n // The record is only needed to evaluate a declared policy's retention\n // floor — skip the decrypt entirely for policy-less collections.\n const record = config\n ? await ctx.getRecord<unknown>(collectionName, recordId).catch(() => null)\n : null\n for (const slot of slots) {\n if (slot.pinned === true) continue // pinned: exempt AND uncounted (#808)\n const policy = config?.[slot.name]\n if (policy && record !== null && isHeld(policy, record, opts.now)) continue // floor blocks budget too\n const lastAccessMs = Date.parse(slot.lastAccessAt ?? slot.uploadedAt) || 0\n const isExternal = slot.external !== undefined\n const bytes = isExternal ? slot.cachedBytes ?? 0 : slot.size\n if (bytes <= 0) continue // external with no local copy: nothing cached to evict\n total += bytes\n candidates.push({\n collection: collectionName, recordId, slotName: slot.name, eTag: slot.eTag,\n bytes, kind: isExternal ? 'cache' : 'slot', lastAccessMs,\n })\n }\n }\n }\n\n // Oldest access first; deterministic tiebreak so equal timestamps (common\n // within one ms) never make the eviction order flap between runs.\n candidates.sort((a, b) =>\n a.lastAccessMs - b.lastAccessMs\n || a.collection.localeCompare(b.collection)\n || a.recordId.localeCompare(b.recordId)\n || a.slotName.localeCompare(b.slotName))\n\n let evicted = 0\n let bytesFreed = 0\n let auditEntries = 0\n for (const c of candidates) {\n if (total <= maxBytes || evicted >= opts.remainingEvictions) break\n if (!opts.dryRun) {\n if (c.kind === 'slot') {\n await ctx.deleteSlot(c.collection, c.recordId, c.slotName)\n await writeAuditEntry(ctx, {\n id: generateEvictionId(c.collection, c.recordId, c.slotName),\n collection: c.collection,\n recordId: c.recordId,\n slotName: c.slotName,\n blobHash: c.eTag,\n reason: 'budget',\n evictedAt: opts.now.toISOString(),\n actor: ctx.actor,\n })\n auditEntries += 1\n } else {\n // Device-local cache drop only — the slot (catalog) and the\n // object-store copy are untouched, so no eviction audit entry.\n await ctx.dropLocalCache?.(c.collection, c.recordId, c.slotName)\n }\n }\n evicted += 1\n bytesFreed += c.bytes\n total -= c.bytes\n }\n\n return { evicted, bytesFreed, auditEntries }\n}\n\n/**\n * Whether a retention floor (legal hold or period-bound `retainUntil`)\n * currently blocks eviction of this record's slots. Fail-closed: a\n * throwing predicate holds the slot.\n */\nfunction isHeld<T>(policy: BlobFieldPolicy<T>, record: T, now: Date): boolean {\n if (policy.legalHold) {\n try {\n if (policy.legalHold(record)) return true\n } catch {\n return true\n }\n }\n if (policy.retainUntil) {\n try {\n const until = policy.retainUntil(record)\n if (until !== null && until !== undefined) {\n const t = until instanceof Date ? until.getTime() : typeof until === 'number' ? until : Date.parse(String(until))\n if (!Number.isFinite(t)) return true // fail-closed: unparseable retainUntil holds the slot\n if (t > now.getTime()) return true\n }\n } catch {\n return true\n }\n }\n return false\n}\n\nfunction evaluatePolicy<T>(\n policy: BlobFieldPolicy<T>,\n record: T,\n slot: SlotInfo,\n now: Date,\n): 'ttl' | 'predicate' | 'both' | null {\n let ttlTriggered = false\n let predicateTriggered = false\n\n if (policy.retainDays !== undefined && policy.retainDays > 0) {\n const uploadedAt = Date.parse(slot.uploadedAt)\n if (Number.isFinite(uploadedAt)) {\n const ageMs = now.getTime() - uploadedAt\n const limitMs = policy.retainDays * 86_400_000\n if (ageMs > limitMs) ttlTriggered = true\n }\n }\n\n if (policy.evictWhen) {\n try {\n if (policy.evictWhen(record)) predicateTriggered = true\n } catch {\n // Predicate error → do NOT evict. Fail closed.\n }\n }\n\n if (ttlTriggered && predicateTriggered) return 'both'\n if (ttlTriggered) return 'ttl'\n if (predicateTriggered) return 'predicate'\n return null\n}\n\nfunction generateEvictionId(collection: string, recordId: string, slotName: string): string {\n const rand = globalThis.crypto.getRandomValues(new Uint8Array(8))\n let suffix = ''\n for (const b of rand) suffix += b.toString(16).padStart(2, '0')\n return `${collection}__${recordId}__${slotName}__${suffix}`\n}\n\nasync function writeAuditEntry(ctx: CompactionContext, entry: BlobEvictionEntry): Promise<void> {\n const json = JSON.stringify(entry)\n const identity = { collection: BLOB_EVICTION_AUDIT_COLLECTION, id: entry.id, by: entry.actor, version: 1 }\n const body = { ts: entry.evictedAt }\n let envelope: EncryptedEnvelope\n if (ctx.encrypted) {\n const dek = await ctx.getDEK(BLOB_EVICTION_AUDIT_COLLECTION)\n const { iv, data } = await encrypt(json, dek, buildRecordAad(identity))\n envelope = buildRecordEnvelope(identity, { ...body, iv, data })\n } else {\n envelope = buildRecordEnvelope(identity, { ...body, iv: '', data: json })\n }\n await ctx.adapter.put(ctx.vault, BLOB_EVICTION_AUDIT_COLLECTION, entry.id, envelope)\n}\n"],"mappings":"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","names":[]}
|