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