@noy-db/hub 0.2.0-pre.14 → 0.2.0-pre.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/dist/aggregate/index.cjs +160 -64
  2. package/dist/aggregate/index.cjs.map +1 -1
  3. package/dist/aggregate/index.d.cts +2 -2
  4. package/dist/aggregate/index.d.ts +2 -2
  5. package/dist/aggregate/index.js +3 -3
  6. package/dist/attestation/index.cjs.map +1 -1
  7. package/dist/attestation/index.d.cts +5 -5
  8. package/dist/attestation/index.d.ts +5 -5
  9. package/dist/attestation/index.js +4 -4
  10. package/dist/blobs/index.cjs.map +1 -1
  11. package/dist/blobs/index.d.cts +6 -6
  12. package/dist/blobs/index.d.ts +6 -6
  13. package/dist/blobs/index.js +3 -3
  14. package/dist/bundle/index.cjs +607 -114
  15. package/dist/bundle/index.cjs.map +1 -1
  16. package/dist/bundle/index.d.cts +7 -7
  17. package/dist/bundle/index.d.ts +7 -7
  18. package/dist/bundle/index.js +7 -7
  19. package/dist/{chunk-BIYRQQV6.js → chunk-3YWP3WBP.js} +3 -3
  20. package/dist/{chunk-VU7SWWT5.js → chunk-42FEUPZQ.js} +10 -6
  21. package/dist/chunk-42FEUPZQ.js.map +1 -0
  22. package/dist/{chunk-ACKFRSAH.js → chunk-667MB6AH.js} +132 -54
  23. package/dist/chunk-667MB6AH.js.map +1 -0
  24. package/dist/{chunk-A5ZOOZFB.js → chunk-6H2ZUNR7.js} +2 -2
  25. package/dist/{chunk-7HT2MEZ5.js → chunk-7BQ4QWYX.js} +3 -3
  26. package/dist/{chunk-DQU36Q7I.js → chunk-7Z7KSVA5.js} +13 -4
  27. package/dist/chunk-7Z7KSVA5.js.map +1 -0
  28. package/dist/{chunk-WBAYSNUQ.js → chunk-BI6ETQPF.js} +2 -2
  29. package/dist/{chunk-56DJ7JVK.js → chunk-BR3AMFGS.js} +2 -2
  30. package/dist/chunk-CJORTUJ2.js +524 -0
  31. package/dist/chunk-CJORTUJ2.js.map +1 -0
  32. package/dist/{chunk-YNTBADIY.js → chunk-CZI2A4MQ.js} +2 -2
  33. package/dist/{chunk-COFPAMX6.js → chunk-DLZ2ONOD.js} +3 -3
  34. package/dist/{chunk-KGCORI4L.js → chunk-DUREQF5W.js} +266 -66
  35. package/dist/chunk-DUREQF5W.js.map +1 -0
  36. package/dist/{chunk-PE4AQGFH.js → chunk-E2CDVKMH.js} +3 -3
  37. package/dist/{chunk-GC4V7RU7.js → chunk-F3BPIPLS.js} +1 -1
  38. package/dist/{chunk-GC4V7RU7.js.map → chunk-F3BPIPLS.js.map} +1 -1
  39. package/dist/{chunk-L2FE64BU.js → chunk-FFXM3ZIF.js} +2 -2
  40. package/dist/{chunk-5LQG6ZO2.js → chunk-G4SCICH5.js} +8 -3
  41. package/dist/chunk-G4SCICH5.js.map +1 -0
  42. package/dist/{chunk-WGHU7BLI.js → chunk-GNI5STXQ.js} +2 -2
  43. package/dist/{chunk-UWNYBOOO.js → chunk-HBXJ37ZY.js} +11 -5
  44. package/dist/chunk-HBXJ37ZY.js.map +1 -0
  45. package/dist/{chunk-NP6EZT44.js → chunk-IQLVUT37.js} +2 -2
  46. package/dist/{chunk-7PS7EOCF.js → chunk-IXBIFDEW.js} +2 -2
  47. package/dist/{chunk-LX3CB26H.js → chunk-KABJXG2F.js} +2 -2
  48. package/dist/{chunk-3EWA37FV.js → chunk-L2BNJ6HM.js} +32 -276
  49. package/dist/chunk-L2BNJ6HM.js.map +1 -0
  50. package/dist/{chunk-DKO2QFSA.js → chunk-OB2ZJQ2D.js} +2 -2
  51. package/dist/{chunk-4PEFEETV.js → chunk-OMAMZKKD.js} +2 -2
  52. package/dist/{chunk-EGD5DXFT.js → chunk-OQSRJG6A.js} +13 -1
  53. package/dist/chunk-OQSRJG6A.js.map +1 -0
  54. package/dist/{chunk-KI6HAJWL.js → chunk-QSUK7YWK.js} +2 -2
  55. package/dist/{chunk-YHPM5D7Y.js → chunk-QVIEAYTP.js} +61 -2
  56. package/dist/chunk-QVIEAYTP.js.map +1 -0
  57. package/dist/{chunk-NSCVNK5K.js → chunk-SCJPI4Z5.js} +3 -3
  58. package/dist/{chunk-ZWTNWAO4.js → chunk-TKIY625R.js} +13 -3
  59. package/dist/chunk-TKIY625R.js.map +1 -0
  60. package/dist/{chunk-OHVFWCJP.js → chunk-VLMPU56Q.js} +48 -18
  61. package/dist/chunk-VLMPU56Q.js.map +1 -0
  62. package/dist/{chunk-6AJBSQU4.js → chunk-XL35NSEN.js} +2 -2
  63. package/dist/{chunk-WIBHRONM.js → chunk-XWH4MXIU.js} +2 -2
  64. package/dist/consent/index.d.cts +6 -6
  65. package/dist/consent/index.d.ts +6 -6
  66. package/dist/derivations/index.cjs +24 -3
  67. package/dist/derivations/index.cjs.map +1 -1
  68. package/dist/derivations/index.d.cts +7 -7
  69. package/dist/derivations/index.d.ts +7 -7
  70. package/dist/derivations/index.js +2 -2
  71. package/dist/{dev-unlock-BF4OSxRv.d.cts → dev-unlock-8XzcD2Z4.d.cts} +1 -1
  72. package/dist/{dev-unlock-DV7ujTCI.d.ts → dev-unlock-DR3upLd1.d.ts} +1 -1
  73. package/dist/executor-AZLS3KBK.js +11 -0
  74. package/dist/{fanout-sidecar-N6OJX6QR.js → fanout-sidecar-67CMI3UT.js} +2 -2
  75. package/dist/guards/index.cjs +9 -5
  76. package/dist/guards/index.cjs.map +1 -1
  77. package/dist/guards/index.d.cts +7 -7
  78. package/dist/guards/index.d.ts +7 -7
  79. package/dist/guards/index.js +1 -1
  80. package/dist/{hash-DswxkLtW.d.ts → hash-CDjye9KV.d.ts} +1 -1
  81. package/dist/{hash-BcF5WQXl.d.cts → hash-DuQ88_5W.d.cts} +1 -1
  82. package/dist/history/index.cjs.map +1 -1
  83. package/dist/history/index.d.cts +7 -7
  84. package/dist/history/index.d.ts +7 -7
  85. package/dist/history/index.js +2 -2
  86. package/dist/i18n/index.cjs.map +1 -1
  87. package/dist/i18n/index.d.cts +6 -6
  88. package/dist/i18n/index.d.ts +6 -6
  89. package/dist/i18n/index.js +3 -3
  90. package/dist/{immutable-guard-7KqslW2K.d.cts → immutable-guard-CRPvu24K.d.cts} +16 -1
  91. package/dist/{immutable-guard-C8IYdzfu.d.ts → immutable-guard-Dov3WvwF.d.ts} +16 -1
  92. package/dist/{index-Cqzp4tt9.d.ts → index-C8Bk3-VF.d.cts} +11 -3
  93. package/dist/{index-CUVOMtgg.d.cts → index-nP99bXLg.d.ts} +11 -3
  94. package/dist/index.cjs +840 -122
  95. package/dist/index.cjs.map +1 -1
  96. package/dist/index.d.cts +146 -15
  97. package/dist/index.d.ts +146 -15
  98. package/dist/index.js +153 -35
  99. package/dist/index.js.map +1 -1
  100. package/dist/indexing/index.cjs +92 -31
  101. package/dist/indexing/index.cjs.map +1 -1
  102. package/dist/indexing/index.d.cts +3 -3
  103. package/dist/indexing/index.d.ts +3 -3
  104. package/dist/indexing/index.js +3 -3
  105. package/dist/{issue-ADVS4OVP.js → issue-RZP3VI6O.js} +4 -4
  106. package/dist/{lazy-builder-D5GU14TS.d.ts → lazy-builder-ChSqcF5t.d.ts} +1 -1
  107. package/dist/{lazy-builder-Ci5_YG73.d.cts → lazy-builder-eYZzLEL1.d.cts} +1 -1
  108. package/dist/{ledger-CWSE3BLF.js → ledger-A3LL253R.js} +3 -3
  109. package/dist/materialized-views/index.cjs +409 -7
  110. package/dist/materialized-views/index.cjs.map +1 -1
  111. package/dist/materialized-views/index.d.cts +7 -7
  112. package/dist/materialized-views/index.d.ts +7 -7
  113. package/dist/materialized-views/index.js +6 -6
  114. package/dist/noydb-WCMY2ZOW.js +35 -0
  115. package/dist/overlay-views/index.cjs +47 -17
  116. package/dist/overlay-views/index.cjs.map +1 -1
  117. package/dist/overlay-views/index.d.cts +28 -10
  118. package/dist/overlay-views/index.d.ts +28 -10
  119. package/dist/overlay-views/index.js +1 -1
  120. package/dist/periods/index.cjs.map +1 -1
  121. package/dist/periods/index.d.cts +6 -6
  122. package/dist/periods/index.d.ts +6 -6
  123. package/dist/periods/index.js +3 -3
  124. package/dist/{predicate-Bt5ft-9c.d.cts → predicate-BmhBSPCH.d.cts} +59 -2
  125. package/dist/{predicate-Bt5ft-9c.d.ts → predicate-BmhBSPCH.d.ts} +59 -2
  126. package/dist/{public-envelope-SYHEYQ3X.js → public-envelope-YP2UWMLG.js} +3 -3
  127. package/dist/query/index.cjs +604 -205
  128. package/dist/query/index.cjs.map +1 -1
  129. package/dist/query/index.d.cts +3 -3
  130. package/dist/query/index.d.ts +3 -3
  131. package/dist/query/index.js +5 -5
  132. package/dist/{registry-XGLNADIE.js → registry-EB6SISTA.js} +2 -2
  133. package/dist/{registry-DK5YWAAA.js → registry-UTA4CLQS.js} +2 -2
  134. package/dist/{revoke-ZDFKMR5E.js → revoke-HNMQZSCL.js} +4 -4
  135. package/dist/session/index.d.cts +7 -7
  136. package/dist/session/index.d.ts +7 -7
  137. package/dist/shadow/index.d.cts +6 -6
  138. package/dist/shadow/index.d.ts +6 -6
  139. package/dist/{signer-P5D7Y72U.js → signer-DCMNKXSF.js} +3 -3
  140. package/dist/snapshots/index.d.cts +6 -6
  141. package/dist/snapshots/index.d.ts +6 -6
  142. package/dist/snapshots/index.js +3 -3
  143. package/dist/{stale-7FRJVHN6.js → stale-W5PQTRYH.js} +2 -2
  144. package/dist/store/index.d.cts +6 -6
  145. package/dist/store/index.d.ts +6 -6
  146. package/dist/{strategy-CrS7PnbE.d.cts → strategy-BtW8fAjz.d.cts} +2 -2
  147. package/dist/{strategy-CrS7PnbE.d.ts → strategy-BtW8fAjz.d.ts} +2 -2
  148. package/dist/sync/index.cjs.map +1 -1
  149. package/dist/sync/index.d.cts +5 -5
  150. package/dist/sync/index.d.ts +5 -5
  151. package/dist/sync/index.js +2 -2
  152. package/dist/team/index.cjs.map +1 -1
  153. package/dist/team/index.d.cts +6 -6
  154. package/dist/team/index.d.ts +6 -6
  155. package/dist/team/index.js +5 -5
  156. package/dist/tx/index.cjs +66 -3
  157. package/dist/tx/index.cjs.map +1 -1
  158. package/dist/tx/index.d.cts +24 -8
  159. package/dist/tx/index.d.ts +24 -8
  160. package/dist/tx/index.js +7 -3
  161. package/dist/tx/index.js.map +1 -1
  162. package/dist/{types-V5R2-pd4.d.cts → types-Bze6vkwm.d.cts} +391 -144
  163. package/dist/{types-BFHQUjdy.d.ts → types-DrmBTscX.d.ts} +391 -144
  164. package/dist/{ulid-p2nKiiKg.d.ts → ulid-DbBVrNSt.d.ts} +1 -1
  165. package/dist/{ulid-CwNf9e6-.d.cts → ulid-DfZlAh0u.d.cts} +1 -1
  166. package/dist/{vault-group-W7QC4UYW.js → vault-group-DX2HFQMX.js} +3 -3
  167. package/dist/{with-derivation-C9K43BOB.d.cts → with-derivation-CCqAchD5.d.cts} +1 -1
  168. package/dist/{with-derivation-Ds9yZgCj.d.ts → with-derivation-_lySGdlm.d.ts} +1 -1
  169. package/dist/{with-materialized-view-DgQcAjYv.d.cts → with-materialized-view--4PsvMDu.d.cts} +1 -1
  170. package/dist/{with-materialized-view-DwR4jkV5.d.ts → with-materialized-view-QT1Tp7NO.d.ts} +1 -1
  171. package/dist/{with-overlayed-view-ByyhHdVr.d.ts → with-overlayed-view-BEXfpzSb.d.ts} +1 -1
  172. package/dist/{with-overlayed-view-7-rUB3vD.d.cts → with-overlayed-view-DlH5qmeB.d.cts} +1 -1
  173. package/package.json +3 -3
  174. package/dist/chunk-3EWA37FV.js.map +0 -1
  175. package/dist/chunk-5LQG6ZO2.js.map +0 -1
  176. package/dist/chunk-ACKFRSAH.js.map +0 -1
  177. package/dist/chunk-DQU36Q7I.js.map +0 -1
  178. package/dist/chunk-EGD5DXFT.js.map +0 -1
  179. package/dist/chunk-KGCORI4L.js.map +0 -1
  180. package/dist/chunk-OHVFWCJP.js.map +0 -1
  181. package/dist/chunk-TV3YZ35S.js +0 -90
  182. package/dist/chunk-TV3YZ35S.js.map +0 -1
  183. package/dist/chunk-UWNYBOOO.js.map +0 -1
  184. package/dist/chunk-VU7SWWT5.js.map +0 -1
  185. package/dist/chunk-YHPM5D7Y.js.map +0 -1
  186. package/dist/chunk-ZWTNWAO4.js.map +0 -1
  187. package/dist/executor-723ZP6TH.js +0 -11
  188. package/dist/noydb-VZ4JVW55.js +0 -35
  189. /package/dist/{chunk-BIYRQQV6.js.map → chunk-3YWP3WBP.js.map} +0 -0
  190. /package/dist/{chunk-A5ZOOZFB.js.map → chunk-6H2ZUNR7.js.map} +0 -0
  191. /package/dist/{chunk-7HT2MEZ5.js.map → chunk-7BQ4QWYX.js.map} +0 -0
  192. /package/dist/{chunk-WBAYSNUQ.js.map → chunk-BI6ETQPF.js.map} +0 -0
  193. /package/dist/{chunk-56DJ7JVK.js.map → chunk-BR3AMFGS.js.map} +0 -0
  194. /package/dist/{chunk-YNTBADIY.js.map → chunk-CZI2A4MQ.js.map} +0 -0
  195. /package/dist/{chunk-COFPAMX6.js.map → chunk-DLZ2ONOD.js.map} +0 -0
  196. /package/dist/{chunk-PE4AQGFH.js.map → chunk-E2CDVKMH.js.map} +0 -0
  197. /package/dist/{chunk-L2FE64BU.js.map → chunk-FFXM3ZIF.js.map} +0 -0
  198. /package/dist/{chunk-WGHU7BLI.js.map → chunk-GNI5STXQ.js.map} +0 -0
  199. /package/dist/{chunk-NP6EZT44.js.map → chunk-IQLVUT37.js.map} +0 -0
  200. /package/dist/{chunk-7PS7EOCF.js.map → chunk-IXBIFDEW.js.map} +0 -0
  201. /package/dist/{chunk-LX3CB26H.js.map → chunk-KABJXG2F.js.map} +0 -0
  202. /package/dist/{chunk-DKO2QFSA.js.map → chunk-OB2ZJQ2D.js.map} +0 -0
  203. /package/dist/{chunk-4PEFEETV.js.map → chunk-OMAMZKKD.js.map} +0 -0
  204. /package/dist/{chunk-KI6HAJWL.js.map → chunk-QSUK7YWK.js.map} +0 -0
  205. /package/dist/{chunk-NSCVNK5K.js.map → chunk-SCJPI4Z5.js.map} +0 -0
  206. /package/dist/{chunk-6AJBSQU4.js.map → chunk-XL35NSEN.js.map} +0 -0
  207. /package/dist/{chunk-WIBHRONM.js.map → chunk-XWH4MXIU.js.map} +0 -0
  208. /package/dist/{executor-723ZP6TH.js.map → executor-AZLS3KBK.js.map} +0 -0
  209. /package/dist/{fanout-sidecar-N6OJX6QR.js.map → fanout-sidecar-67CMI3UT.js.map} +0 -0
  210. /package/dist/{issue-ADVS4OVP.js.map → issue-RZP3VI6O.js.map} +0 -0
  211. /package/dist/{ledger-CWSE3BLF.js.map → ledger-A3LL253R.js.map} +0 -0
  212. /package/dist/{noydb-VZ4JVW55.js.map → noydb-WCMY2ZOW.js.map} +0 -0
  213. /package/dist/{public-envelope-SYHEYQ3X.js.map → public-envelope-YP2UWMLG.js.map} +0 -0
  214. /package/dist/{registry-DK5YWAAA.js.map → registry-EB6SISTA.js.map} +0 -0
  215. /package/dist/{registry-XGLNADIE.js.map → registry-UTA4CLQS.js.map} +0 -0
  216. /package/dist/{revoke-ZDFKMR5E.js.map → revoke-HNMQZSCL.js.map} +0 -0
  217. /package/dist/{signer-P5D7Y72U.js.map → signer-DCMNKXSF.js.map} +0 -0
  218. /package/dist/{stale-7FRJVHN6.js.map → stale-W5PQTRYH.js.map} +0 -0
  219. /package/dist/{vault-group-W7QC4UYW.js.map → vault-group-DX2HFQMX.js.map} +0 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/derivations/with-derivation.ts"],"sourcesContent":["import { ValidationError } from '../errors.js'\nimport type { DerivationStrategy, DerivationStrategyHandle } from './types.js'\n\n/**\n * Register a deterministic derivation: one source collection → one or\n * more typed outputs, computed by the user's `derive` function on\n * plaintext after DEK unwrap. Outputs are encrypted with the same DEK\n * as the source and written via the standard `Collection.put` path.\n *\n * See docs/superpowers/specs/2026-05-01-dim14-derivation-v1-design.md.\n */\nexport function withDerivation<\n TSource extends Record<string, unknown>,\n TOutputs extends Record<string, Record<string, unknown>>,\n>(spec: DerivationStrategy<TSource, TOutputs>): DerivationStrategyHandle {\n if (!spec.source || spec.source.length === 0) {\n throw new ValidationError('withDerivation: source collection name is required')\n }\n if (!spec.outputs || Object.keys(spec.outputs).length === 0) {\n throw new ValidationError('withDerivation: outputs map must declare at least one output')\n }\n if (spec.deterministic !== true) {\n throw new ValidationError('withDerivation: v1 only supports deterministic derivations')\n }\n if (typeof spec.derive !== 'function') {\n throw new ValidationError('withDerivation: derive must be a function')\n }\n\n // Validate declared sibling sources (#344). Each must be a non-empty\n // string and must differ from the primary source — a self-reference\n // would double-register the strategy under the same `_bySource` key.\n if (spec.sources !== undefined) {\n for (const extra of spec.sources) {\n if (typeof extra !== 'string' || extra.length === 0) {\n throw new ValidationError('withDerivation: each entry in sources[] must be a non-empty string')\n }\n if (extra === spec.source) {\n throw new ValidationError(\n `withDerivation: sources[] must not contain the primary source \"${spec.source}\"`,\n )\n }\n }\n }\n\n // Validate array-shape outputs.\n const lifecycleMode = typeof spec.lifecycle === 'string' ? spec.lifecycle : spec.lifecycle.mode\n for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {\n if (outputSpec.shape === 'array') {\n if (lifecycleMode !== 'eager') {\n throw new ValidationError(\n `withDerivation: shape 'array' supports lifecycle 'eager' only in this release `\n + `Output \"${outputKey}\" declared lifecycle '${lifecycleMode}'. `\n + 'Switch to `lifecycle: \"eager\"` or use shape: \"record\".',\n )\n }\n if (typeof outputSpec.key !== 'function') {\n throw new ValidationError(\n `withDerivation: shape 'array' output \"${outputKey}\" requires \\`key: (out) => string\\`.`,\n )\n }\n if (outputSpec.maxFanout !== undefined) {\n if (!Number.isInteger(outputSpec.maxFanout) || outputSpec.maxFanout < 1) {\n throw new ValidationError(\n `withDerivation: maxFanout for output \"${outputKey}\" must be a positive integer `\n + `(got ${String(outputSpec.maxFanout)}).`,\n )\n }\n }\n }\n }\n\n return {\n __noydb_strategy: 'derivation',\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n spec: spec as DerivationStrategy<any, any>,\n }\n}\n"],"mappings":";;;;;AAWO,SAAS,eAGd,MAAuE;AACvE,MAAI,CAAC,KAAK,UAAU,KAAK,OAAO,WAAW,GAAG;AAC5C,UAAM,IAAI,gBAAgB,oDAAoD;AAAA,EAChF;AACA,MAAI,CAAC,KAAK,WAAW,OAAO,KAAK,KAAK,OAAO,EAAE,WAAW,GAAG;AAC3D,UAAM,IAAI,gBAAgB,8DAA8D;AAAA,EAC1F;AACA,MAAI,KAAK,kBAAkB,MAAM;AAC/B,UAAM,IAAI,gBAAgB,4DAA4D;AAAA,EACxF;AACA,MAAI,OAAO,KAAK,WAAW,YAAY;AACrC,UAAM,IAAI,gBAAgB,2CAA2C;AAAA,EACvE;AAKA,MAAI,KAAK,YAAY,QAAW;AAC9B,eAAW,SAAS,KAAK,SAAS;AAChC,UAAI,OAAO,UAAU,YAAY,MAAM,WAAW,GAAG;AACnD,cAAM,IAAI,gBAAgB,oEAAoE;AAAA,MAChG;AACA,UAAI,UAAU,KAAK,QAAQ;AACzB,cAAM,IAAI;AAAA,UACR,kEAAkE,KAAK,MAAM;AAAA,QAC/E;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAGA,QAAM,gBAAgB,OAAO,KAAK,cAAc,WAAW,KAAK,YAAY,KAAK,UAAU;AAC3F,aAAW,CAAC,WAAW,UAAU,KAAK,OAAO,QAAQ,KAAK,OAAO,GAAG;AAClE,QAAI,WAAW,UAAU,SAAS;AAChC,UAAI,kBAAkB,SAAS;AAC7B,cAAM,IAAI;AAAA,UACR,yFACa,SAAS,yBAAyB,aAAa;AAAA,QAE9D;AAAA,MACF;AACA,UAAI,OAAO,WAAW,QAAQ,YAAY;AACxC,cAAM,IAAI;AAAA,UACR,yCAAyC,SAAS;AAAA,QACpD;AAAA,MACF;AACA,UAAI,WAAW,cAAc,QAAW;AACtC,YAAI,CAAC,OAAO,UAAU,WAAW,SAAS,KAAK,WAAW,YAAY,GAAG;AACvE,gBAAM,IAAI;AAAA,YACR,yCAAyC,SAAS,qCACxC,OAAO,WAAW,SAAS,CAAC;AAAA,UACxC;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AAAA,IACL,kBAAkB;AAAA;AAAA,IAElB;AAAA,EACF;AACF;","names":[]}
@@ -56,7 +56,7 @@ function validatePeriodName(name, existing) {
56
56
  }
57
57
  async function appendPeriodLedgerEntry(ledger, actor, envelope, name) {
58
58
  if (!ledger) return;
59
- const { envelopePayloadHash } = await import("./ledger-CWSE3BLF.js");
59
+ const { envelopePayloadHash } = await import("./ledger-A3LL253R.js");
60
60
  await ledger.append({
61
61
  op: "put",
62
62
  collection: PERIODS_COLLECTION,
@@ -87,4 +87,4 @@ export {
87
87
  appendPeriodLedgerEntry,
88
88
  withPeriods
89
89
  };
90
- //# sourceMappingURL=chunk-KI6HAJWL.js.map
90
+ //# sourceMappingURL=chunk-QSUK7YWK.js.map
@@ -137,7 +137,7 @@ var TxCollection = class {
137
137
  this._ctx._ops.push(op);
138
138
  }
139
139
  };
140
- async function runTransaction(db, fn, options) {
140
+ async function runTransaction(db, fn, options, txInvariants) {
141
141
  if (options?.amendment) {
142
142
  if (typeof options.reason !== "string" || options.reason.trim().length === 0) {
143
143
  throw new ValidationError(
@@ -161,12 +161,19 @@ async function runTransaction(db, fn, options) {
161
161
  }
162
162
  const priorEnvelopes = /* @__PURE__ */ new Map();
163
163
  const store = db._store;
164
+ const invariants = txInvariants ?? [];
165
+ const watchedScopes = new Set(invariants.map((i) => i.scope));
166
+ const plainBefore = /* @__PURE__ */ new Map();
164
167
  for (const op of ctx._ops) {
165
168
  const key = keyOf(op);
166
169
  if (!priorEnvelopes.has(key)) {
167
170
  const env = await store.get(op.vaultName, op.collectionName, op.id);
168
171
  priorEnvelopes.set(key, env);
169
172
  }
173
+ if (watchedScopes.has(op.collectionName) && !plainBefore.has(key)) {
174
+ const prior = await db.vault(op.vaultName).collection(op.collectionName).get(op.id);
175
+ plainBefore.set(key, prior ?? null);
176
+ }
170
177
  if (op.expectedVersion !== void 0) {
171
178
  const env = priorEnvelopes.get(key) ?? null;
172
179
  const actual = env?._v ?? 0;
@@ -264,6 +271,58 @@ async function runTransaction(db, fn, options) {
264
271
  );
265
272
  }
266
273
  }
274
+ if (invariants.length > 0) {
275
+ const lastOp = /* @__PURE__ */ new Map();
276
+ const order = [];
277
+ for (const op of ctx._ops) {
278
+ const key = keyOf(op);
279
+ if (!lastOp.has(key)) order.push(key);
280
+ lastOp.set(key, op);
281
+ }
282
+ const changesByScope = /* @__PURE__ */ new Map();
283
+ const scopeVault = /* @__PURE__ */ new Map();
284
+ for (const key of order) {
285
+ const op = lastOp.get(key);
286
+ if (!watchedScopes.has(op.collectionName)) continue;
287
+ const before = plainBefore.get(key) ?? null;
288
+ const after = op.type === "delete" ? null : op.record ?? null;
289
+ const change = { before, after };
290
+ const arr = changesByScope.get(op.collectionName);
291
+ if (arr) arr.push(change);
292
+ else changesByScope.set(op.collectionName, [change]);
293
+ scopeVault.set(op.collectionName, op.vaultName);
294
+ }
295
+ try {
296
+ for (const inv of invariants) {
297
+ const changes = changesByScope.get(inv.scope);
298
+ if (changes === void 0 || changes.length === 0) continue;
299
+ const vaultName = scopeVault.get(inv.scope);
300
+ const v = db.vault(vaultName);
301
+ const facade = v._getReadOnlyFacade() ?? {
302
+ collection(name) {
303
+ const c = v.collection(name);
304
+ return {
305
+ get: (id) => c.get(id),
306
+ list: () => c.list(),
307
+ query: () => c.query()
308
+ };
309
+ }
310
+ };
311
+ const ctxForInv = {
312
+ existing: null,
313
+ vault: facade,
314
+ userId: v.userId,
315
+ role: v.role
316
+ };
317
+ await inv.check(changes, ctxForInv);
318
+ }
319
+ } catch (err) {
320
+ await revertExecuted(ctx._executed, store, db);
321
+ throw err instanceof InvariantError ? err : new InvariantError(
322
+ err instanceof Error ? err.message : `invariant violated: ${String(err)}`
323
+ );
324
+ }
325
+ }
267
326
  return bodyResult;
268
327
  }
269
328
  async function revertExecuted(executed, store, db) {
@@ -293,4 +352,4 @@ export {
293
352
  runTransaction,
294
353
  revertExecuted
295
354
  };
296
- //# sourceMappingURL=chunk-YHPM5D7Y.js.map
355
+ //# sourceMappingURL=chunk-QVIEAYTP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/tx/transaction.ts"],"sourcesContent":["/**\n * Multi-record atomic transactions.\n *\n * Lets an application stage writes across two or more collections (or\n * vaults) and commit them all-or-nothing.\n *\n * ```ts\n * await db.transaction(async (tx) => {\n * const inv = tx.vault('acme').collection<Invoice>('invoices')\n * const pay = tx.vault('acme').collection<Payment>('payments')\n * await inv.put(invoiceId, { ...invoice, status: 'paid' })\n * await pay.put(paymentId, { invoiceId, amount, paidAt })\n * })\n * // If the body throws before returning: nothing persisted.\n * // If the body returns: all puts committed; any CAS mismatch rolls\n * // the batch back and surfaces as ConflictError.\n * ```\n *\n * ## Atomicity semantics\n *\n * Ops are buffered during the body. On body-return the hub:\n *\n * 1. **Pre-flight** — re-reads every touched envelope and enforces\n * any caller-supplied `expectedVersion`. A mismatch throws\n * `ConflictError` with *no* writes performed.\n * 2. **Execute** — calls `Collection.put()` / `.delete()` for each\n * staged op in declaration order. History snapshots, ledger\n * appends, and change events fire as normal per op.\n * 3. **Unwind on failure** — if step 2 throws mid-batch, each\n * already-committed op is reverted via the raw store (restoring\n * the captured prior envelope, or deleting if none existed). The\n * ledger is NOT rewritten — audit history preserves the partial\n * commit and the revert.\n *\n * **Crash window.** Steps 2–3 are not a storage-layer transaction —\n * if the process dies between two executed ops, the on-disk state is\n * partial. True all-or-nothing atomicity requires a store that\n * implements `NoydbStore.tx()` (DynamoDB `TransactWriteItems`,\n * IndexedDB `readwrite` transaction, …). This executor declares\n * that future integration point via the `tx?()` method + the\n * `StoreCapabilities.txAtomic` bit, but does not yet delegate\n * to it — the cascade into `Fork · Stores` tracks the per-adapter\n * wire-up.\n *\n * ## Not covered\n *\n * - Cross-sync-peer atomicity. Transactions commit against the\n * primary store only; the sync engine pushes on its normal\n * schedule. For cross-peer two-phase commit use `SyncTransaction`\n * via `db.transaction(vaultName)`.\n * - Read-your-writes within the body. `tx.collection().get(id)`\n * returns the most-recently-staged value for that id when one\n * exists; if no staged op has touched the id, it reads the current\n * committed state. Version numbers returned by `get` reflect the\n * pre-transaction state (staged puts have no version yet).\n *\n * @module\n */\n\nimport type { Noydb } from '../noydb.js'\nimport type { Vault } from '../vault.js'\nimport type { Collection } from '../collection.js'\nimport type { EncryptedEnvelope } from '../types.js'\nimport {\n AmendmentForbiddenError,\n ConflictError,\n InvariantError,\n ValidationError,\n} from '../errors.js'\nimport { generateULID } from '../bundle/ulid.js'\nimport type { GuardExecutor as GuardExecutorModule } from '../guards/executor.js'\nimport type { LedgerEntry } from '../history/ledger/entry.js'\nimport type { TransactionInvariant } from './invariants.js'\nimport type { GuardChange, GuardContext, ReadOnlyVaultFacade } from '../guards/types.js'\n\n/** One op buffered inside a running `TxContext`. @internal */\nexport interface StagedOp {\n type: 'put' | 'delete'\n vaultName: string\n collectionName: string\n id: string\n record?: unknown\n expectedVersion?: number\n /**\n * Optional human-readable tag forwarded to the resulting ledger\n * entry's `reason` field. Set by callers via\n * `tx.vault(v).collection(c).put(id, record, { reason })`.\n */\n reason?: string\n}\n\n/**\n * One executed op (main staged op or recursive side-effect like a\n * derivation output) paired with the envelope captured before the write.\n * `revertExecuted` walks this array in reverse on rollback.\n * @internal\n */\nexport interface ExecutedOp {\n op: StagedOp\n priorEnvelope: EncryptedEnvelope | null\n}\n\n/**\n * Options accepted by `db.transaction({ amendment, reason }, fn)`.\n * Only the amendment variant uses these — a plain `db.transaction(fn)`\n * never sees this shape.\n */\nexport interface AmendmentTxOptions {\n /** Opt into amendment mode. Required to be `true`. */\n readonly amendment: true\n /** Human-readable rationale recorded in the ledger entry. Required. */\n readonly reason: string\n}\n\n/**\n * Transaction handle passed to the user's body. Use\n * `tx.vault(name).collection<T>(name)` to get a per-collection\n * facade; its `put`/`delete`/`get` calls stage ops against the tx.\n */\nexport class TxContext {\n /** Stable id for this transaction; shared by all writes it performs. */\n readonly txId: string = generateULID()\n /** @internal */\n readonly _ops: StagedOp[] = []\n /**\n * @internal — write log built up in Phase 2. Each entry records the\n * envelope captured BEFORE the write so a mid-batch failure can\n * restore prior state via `revertExecuted`. Side-effect writes (e.g.\n * recursive derivation outputs fired inside `Collection.put`) are\n * appended here in execution order so they roll back alongside the\n * main staged ops.\n */\n readonly _executed: ExecutedOp[] = []\n /** @internal */\n readonly _db: Noydb\n /**\n * @internal — true when this TxContext was opened in amendment\n * mode. Toggles the lazy-`beginAmendment` + role-check path on first\n * `tx.vault(name)` and unlocks the post-Phase-2 invariant + audit run.\n */\n readonly _amendment: boolean\n /** @internal — vaults that have already had `beginAmendment` called. */\n readonly _amendmentVaults = new Map<string, Vault>()\n\n /** @internal */\n constructor(db: Noydb, amendment = false) {\n this._db = db\n this._amendment = amendment\n }\n\n /** Scope subsequent `collection()` calls to the named vault. */\n vault(name: string): TxVault {\n const v = this._db.vault(name)\n if (this._amendment && !this._amendmentVaults.has(name)) {\n // Role check is per-vault. The task spec (\"only admin or owner\n // can open an amendment\") is implemented lazy-on-first-touch\n // because the role lives on the vault's keyring, and `tx.vault()`\n // is the first place we know which vault we're addressing. The\n // observable effect is identical to an eager check in the single-\n // vault case the tests exercise; multi-vault amendments check\n // each touched vault as they first appear.\n const role = v.role\n if (role !== 'admin' && role !== 'owner') {\n throw new AmendmentForbiddenError(v.userId, role)\n }\n // Amendments require an initialised guard registry — they\n // produce a structured invariant + change-set audit. A vault\n // opened without `guardStrategies` (or via the sync fallback\n // path) has a null registry and cannot run an amendment.\n const reg = v._getGuardRegistry()\n if (reg === null) {\n throw new ValidationError(\n `Vault \"${name}\": amendment mode requires at least one ` +\n `guardStrategy registered via createNoydb({ guardStrategies }). ` +\n `Open the vault with guardStrategies before calling ` +\n `db.transaction({ amendment: true }).`,\n )\n }\n reg.beginAmendment()\n this._amendmentVaults.set(name, v)\n }\n return new TxVault(this, v)\n }\n}\n\n/** Per-vault facade inside a running transaction. */\nexport class TxVault {\n /** @internal */\n readonly _ctx: TxContext\n /** @internal */\n readonly _vault: Vault\n\n /** @internal */\n constructor(ctx: TxContext, vault: Vault) {\n this._ctx = ctx\n this._vault = vault\n }\n\n /** Scope subsequent op calls to the named collection. */\n collection<T>(name: string): TxCollection<T> {\n const c = this._vault.collection<T>(name)\n return new TxCollection<T>(this._ctx, this._vault, c, name)\n }\n}\n\n/** Per-collection facade inside a running transaction. */\nexport class TxCollection<T> {\n /** @internal */\n readonly _ctx: TxContext\n /** @internal */\n readonly _vault: Vault\n /** @internal */\n readonly _coll: Collection<T>\n /** @internal */\n readonly _name: string\n\n /** @internal */\n constructor(ctx: TxContext, vault: Vault, coll: Collection<T>, name: string) {\n this._ctx = ctx\n this._vault = vault\n this._coll = coll\n this._name = name\n }\n\n /**\n * Read the current committed value, or the most-recently-staged\n * value from the same transaction if one exists.\n */\n async get(id: string): Promise<T | null> {\n for (let i = this._ctx._ops.length - 1; i >= 0; i--) {\n const op = this._ctx._ops[i]!\n if (\n op.vaultName === this._vault.name &&\n op.collectionName === this._name &&\n op.id === id\n ) {\n if (op.type === 'delete') return null\n return op.record as T\n }\n }\n return this._coll.get(id)\n }\n\n /**\n * Stage a put. Does not write until the transaction body returns.\n * Supply `{ expectedVersion }` to enforce optimistic concurrency\n * during the commit pre-flight.\n */\n put(id: string, record: T, options?: { expectedVersion?: number; reason?: string }): void {\n const op: StagedOp = {\n type: 'put',\n vaultName: this._vault.name,\n collectionName: this._name,\n id,\n record,\n }\n if (options?.expectedVersion !== undefined) op.expectedVersion = options.expectedVersion\n if (options?.reason !== undefined) op.reason = options.reason\n this._ctx._ops.push(op)\n }\n\n /**\n * Stage a delete. Does not write until the transaction body returns.\n * Supply `{ expectedVersion }` to enforce optimistic concurrency\n * during the commit pre-flight.\n */\n delete(id: string, options?: { expectedVersion?: number }): void {\n const op: StagedOp = {\n type: 'delete',\n vaultName: this._vault.name,\n collectionName: this._name,\n id,\n }\n if (options?.expectedVersion !== undefined) op.expectedVersion = options.expectedVersion\n this._ctx._ops.push(op)\n }\n}\n\n/**\n * Commit plan: pre-flight check + execution + revert plan.\n *\n * @internal — driven by `withTransactions()` (via `tx/active.ts`) for\n * user-facing `db.transaction(...)` calls and by the `amendment` path\n * in `noydb.ts`. `Collection.putManyAtomic` runs its own Phase 2 loop\n * but shares the `_activeTxContext` mechanism (and the `revertExecuted`\n * helper) so nested side-effect derivation writes get registered for\n * revert alongside the bulk-put source ops.\n */\nexport async function runTransaction<T>(\n db: Noydb,\n fn: (tx: TxContext) => Promise<T> | T,\n options?: AmendmentTxOptions,\n txInvariants?: ReadonlyArray<TransactionInvariant>,\n): Promise<T> {\n // ─── Amendment-mode pre-flight ───────────────────────────────\n // `reason` is the only thing we can validate before the body runs;\n // the per-vault role check happens lazily on first `tx.vault(name)`\n // because we don't know which vaults the body will touch ahead of\n // time. Throwing here keeps the failure mode close to the call site\n // so the developer doesn't have to walk an async stack to find the\n // missing-reason mistake.\n if (options?.amendment) {\n if (typeof options.reason !== 'string' || options.reason.trim().length === 0) {\n throw new ValidationError(\n 'db.transaction({ amendment: true }) requires a non-empty `reason` string.',\n )\n }\n }\n\n const ctx = new TxContext(db, options?.amendment === true)\n const bodyResult = await fn(ctx)\n\n if (ctx._ops.length === 0) {\n // Body produced no ops. If amendment mode was active we still\n // need to close any opened windows so a subsequent (unrelated)\n // write doesn't surprise-collect into a stale change-set. Each\n // `beginAmendment` is matched by exactly one `consumeChanges`.\n if (ctx._amendment) {\n for (const v of ctx._amendmentVaults.values()) {\n // Registry is guaranteed non-null here — `tx.vault(name)`\n // threw above if it was null before adding to\n // `_amendmentVaults`.\n const reg = v._getGuardRegistry()\n if (reg !== null) {\n reg.consumeChanges()\n reg.consumeMeta()\n }\n }\n }\n return bodyResult\n }\n\n // Phase 1 — pre-flight: snapshot every touched envelope and enforce\n // any caller-supplied expectedVersion. Same (vault, coll, id) touched\n // more than once in one tx snapshots only the *initial* committed\n // state; the in-order replay in Phase 2 takes care of successor ops.\n const priorEnvelopes = new Map<string, EncryptedEnvelope | null>()\n const store = db._store\n\n // Commit-time changeset invariants (#342) need PLAINTEXT prior records\n // for `before`, but `priorEnvelopes` holds ENCRYPTED envelopes. So for\n // ops in a watched scope we additionally decrypt the prior record here,\n // in Phase 1, BEFORE Phase 2 overwrites it. Snapshots only the initial\n // committed state per (vault, coll, id), matching the envelope snapshot.\n const invariants = txInvariants ?? []\n const watchedScopes = new Set(invariants.map(i => i.scope))\n const plainBefore = new Map<string, unknown>()\n\n for (const op of ctx._ops) {\n const key = keyOf(op)\n if (!priorEnvelopes.has(key)) {\n const env = await store.get(op.vaultName, op.collectionName, op.id)\n priorEnvelopes.set(key, env)\n }\n if (watchedScopes.has(op.collectionName) && !plainBefore.has(key)) {\n const prior = await db\n .vault(op.vaultName)\n .collection(op.collectionName)\n .get(op.id)\n plainBefore.set(key, prior ?? null)\n }\n if (op.expectedVersion !== undefined) {\n const env = priorEnvelopes.get(key) ?? null\n const actual = env?._v ?? 0\n if (actual !== op.expectedVersion) {\n throw new ConflictError(\n actual,\n `Transaction pre-flight: ${op.vaultName}/${op.collectionName}/${op.id} ` +\n `expected v${op.expectedVersion}, found v${actual}`,\n )\n }\n }\n }\n\n // Phase 2 — execute via the Collection layer so history snapshots,\n // ledger entries, and change events fire normally. We capture each\n // successful op so a mid-batch throw can revert in Phase 3.\n //\n // `_activeTxContext` is published on the Noydb instance for the\n // duration of Phase 2 so recursive writes triggered inside\n // `Collection.put` (today: eager derivation outputs) can register\n // their own envelopes onto `ctx._executed` and roll back alongside\n // the main staged ops. The `finally` clears it before the\n // amendment commit phase runs.\n db._setActiveTxContext(ctx)\n try {\n try {\n for (const op of ctx._ops) {\n const coll = db.vault(op.vaultName).collection(op.collectionName)\n const key = keyOf(op)\n const prior = priorEnvelopes.get(key) ?? null\n // Record the revert plan BEFORE the call so a mid-`coll.put` throw\n // (e.g. strict-mode derivation failure firing after `store.put`\n // has already committed the envelope) still has its source write\n // reverted. `revertExecuted` is best-effort: putting prior back is\n // idempotent when the failing op never actually wrote, and\n // `_invalidateCacheEntry` is a no-op when the collection isn't\n // hydrated.\n ctx._executed.push({ op, priorEnvelope: prior })\n if (op.type === 'put') {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await coll.put(op.id, op.record as any, op.reason !== undefined ? { reason: op.reason } : undefined)\n } else {\n await coll.delete(op.id)\n }\n }\n } catch (err) {\n // Phase 3 — best-effort revert. See helper docstring.\n await revertExecuted(ctx._executed, store, db)\n // Drain amendment windows so the next transaction starts clean.\n if (ctx._amendment) {\n for (const v of ctx._amendmentVaults.values()) {\n const reg = v._getGuardRegistry()\n if (reg !== null) {\n reg.consumeChanges()\n reg.consumeMeta()\n }\n }\n }\n throw err\n }\n } finally {\n db._clearActiveTxContext(ctx)\n }\n\n // ─── Amendment commit phase (only if amendment === true) ────\n // Body succeeded — now run each touched vault's invariants over the\n // collected change-set, then append a structured ledger entry. If\n // any invariant throws, treat it exactly like a mid-Phase-2 failure:\n // revert every executed op and re-throw the InvariantError.\n if (ctx._amendment) {\n // Lazy-load GuardExecutor at the dispatch site — keeps the floor\n // bundle free of the guards subsystem when amendments aren't used.\n // Mirrors the deferred-load pattern from elsewhere in this module.\n const { GuardExecutor } = (await import('../guards/executor.js')) as {\n GuardExecutor: typeof GuardExecutorModule\n }\n try {\n for (const [vaultName, v] of ctx._amendmentVaults) {\n const registry = v._getGuardRegistry()\n // Registry is guaranteed non-null at this point — the\n // `tx.vault(name)` path that populates `_amendmentVaults`\n // throws if the registry is null. The defensive check here\n // is for TypeScript's narrowing.\n if (registry === null) continue\n const changesByCollection = registry.consumeChanges()\n const meta = registry.consumeMeta()\n if (changesByCollection.size === 0) continue\n\n const readOnlyVault = v._getReadOnlyFacade()\n if (readOnlyVault === null) continue\n\n // Build the invariant ctx once per vault — it's the same shape\n // every guard sees on the normal `check` path, just with a\n // synthetic `existing: null` (invariants get the full change\n // set in their first parameter; `existing` is a per-record\n // concept that doesn't apply here).\n const invariantsPassed: string[] = []\n for (const [collection, changes] of changesByCollection) {\n const guards = registry.guardsFor(collection).filter(g => g.amendment !== undefined)\n for (const guard of guards) {\n await GuardExecutor.runInvariant(guard, changes, {\n existing: null,\n vault: readOnlyVault,\n userId: v.userId,\n role: v.role,\n })\n }\n if (guards.length > 0) invariantsPassed.push(collection)\n }\n\n // Append the audit ledger entry. Silent no-op when the\n // history strategy isn't configured — the records still\n // committed, only the multi-record summary is unavailable.\n const ledger = v._getLedgerOrNull()\n if (ledger) {\n const role = v.role as 'admin' | 'owner'\n const amendment: NonNullable<LedgerEntry['amendment']> = {\n reason: options!.reason,\n role,\n changes: meta,\n invariantsPassed,\n }\n await ledger.append({\n op: 'amendment',\n collection: '',\n id: '',\n version: 0,\n actor: v.userId,\n // No payload to hash — the per-record entries already\n // captured `payloadHash` at their own append time. We use\n // a sha256 of the canonical reason string so the field is\n // populated with something deterministic and non-empty.\n payloadHash: '',\n amendment,\n })\n }\n void vaultName\n }\n } catch (err) {\n await revertExecuted(ctx._executed, store, db)\n throw err instanceof InvariantError ? err : new InvariantError(\n err instanceof Error ? err.message : `invariant violated: ${String(err)}`,\n )\n }\n }\n\n // ─── Commit-time changeset invariant phase (#342) ───────────\n // Runs for BOTH ordinary and amendment transactions (placed after the\n // amendment phase so an amendment commit is still subject to these\n // set-level constraints). Assemble the changeset from the executed\n // staged ops, deduped to the LAST write per (vault, coll, id) while\n // preserving write order, then group `GuardChange` by collection\n // (scope) and run each matching invariant. A throw mirrors the\n // amendment-phase failure mode exactly: revert every executed op and\n // re-throw as `InvariantError`.\n if (invariants.length > 0) {\n // Dedup ctx._ops to the last write per key, preserving first-seen\n // (write) order so the changeset is stable and order-meaningful.\n const lastOp = new Map<string, StagedOp>()\n const order: string[] = []\n for (const op of ctx._ops) {\n const key = keyOf(op)\n if (!lastOp.has(key)) order.push(key)\n lastOp.set(key, op)\n }\n\n // Group {before, after} pairs by collection name (the invariant\n // scope). `before` is the plaintext prior captured in Phase 1 (null\n // for inserts / unwatched — only watched scopes were captured, and\n // every grouped key belongs to a watched scope). `after` is the\n // written record, null for a delete.\n const changesByScope = new Map<string, GuardChange<unknown>[]>()\n // Parallel map: scope → the vault name of its (last-seen) op, used to\n // build the per-invariant read-only ctx (facade + userId + role).\n const scopeVault = new Map<string, string>()\n for (const key of order) {\n const op = lastOp.get(key)!\n if (!watchedScopes.has(op.collectionName)) continue\n const before = plainBefore.get(key) ?? null\n const after = op.type === 'delete' ? null : (op.record ?? null)\n const change = { before, after } as GuardChange<unknown>\n const arr = changesByScope.get(op.collectionName)\n if (arr) arr.push(change)\n else changesByScope.set(op.collectionName, [change])\n\n // Stash the vault name alongside so we can build a per-vault ctx.\n // (All ops in a scope group could span vaults; we resolve the\n // vault per change below via a parallel map keyed the same way.)\n scopeVault.set(op.collectionName, op.vaultName)\n }\n\n try {\n for (const inv of invariants) {\n const changes = changesByScope.get(inv.scope)\n if (changes === undefined || changes.length === 0) continue\n const vaultName = scopeVault.get(inv.scope)!\n const v = db.vault(vaultName)\n // Prefer the real read-only facade so the invariant can read\n // sibling collections; fall back to a minimal read-only stub.\n const facade: ReadOnlyVaultFacade =\n v._getReadOnlyFacade() ?? {\n collection<R = unknown>(name: string) {\n const c = v.collection<R>(name)\n return {\n get: (id: string) => c.get(id),\n list: () => c.list(),\n query: () => c.query(),\n }\n },\n }\n const ctxForInv: GuardContext<unknown> = {\n existing: null,\n vault: facade,\n userId: v.userId,\n role: v.role,\n }\n await inv.check(changes, ctxForInv)\n }\n } catch (err) {\n await revertExecuted(ctx._executed, store, db)\n throw err instanceof InvariantError ? err : new InvariantError(\n err instanceof Error ? err.message : `invariant violated: ${String(err)}`,\n )\n }\n }\n\n return bodyResult\n}\n\n/**\n * Phase 3 helper — restore captured prior envelopes via the raw store\n * to avoid re-firing Collection-level side effects (we don't want a\n * cascade of change events undoing themselves). The ledger is left\n * as-is: each committed op appended an entry; the revert is\n * deliberately NOT recorded as a compensating entry because the\n * caller-facing contract is \"atomic or not at all,\" not \"every write\n * visible in the audit trail.\" Auditors who need the intermediate\n * state can still reconstruct it by walking the ledger through the\n * failed-tx timestamp.\n *\n * @internal — shared between `runTransaction` and\n * `Collection.putManyAtomic`. Both register source ops + nested\n * derivation side-effect ops onto `_executed`; this helper unwinds the\n * combined list in reverse on rollback.\n */\nexport async function revertExecuted(\n executed: ReadonlyArray<ExecutedOp>,\n store: Noydb['_store'],\n db?: Noydb,\n): Promise<void> {\n for (const { op, priorEnvelope } of executed.slice().reverse()) {\n try {\n if (priorEnvelope) {\n await store.put(op.vaultName, op.collectionName, op.id, priorEnvelope)\n } else {\n await store.delete(op.vaultName, op.collectionName, op.id)\n }\n // Sync the Collection-layer cache with what we just wrote at\n // the raw store. Without this, eager-mode `get` would still\n // return the rolled-back record from its in-memory map. The\n // Collection's `_invalidateCacheEntry` is a no-op when the\n // collection hasn't yet been hydrated.\n if (db) {\n const coll = db.vault(op.vaultName).collection(op.collectionName)\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n await (coll as any)._invalidateCacheEntry(op.id)\n }\n } catch {\n // swallow — best-effort. Surfacing the revert error would mask\n // the original one that triggered the rollback.\n }\n }\n}\n\nfunction keyOf(op: StagedOp): string {\n return `${op.vaultName}\\x00${op.collectionName}\\x00${op.id}`\n}\n"],"mappings":"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","names":[]}
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  ensureCollectionDEK
3
- } from "./chunk-A5ZOOZFB.js";
3
+ } from "./chunk-6H2ZUNR7.js";
4
4
  import {
5
5
  NOYDB_FORMAT_VERSION
6
- } from "./chunk-GC4V7RU7.js";
6
+ } from "./chunk-F3BPIPLS.js";
7
7
  import {
8
8
  decrypt,
9
9
  encrypt
@@ -76,4 +76,4 @@ export {
76
76
  listCredentials,
77
77
  credentialStatus
78
78
  };
79
- //# sourceMappingURL=chunk-NSCVNK5K.js.map
79
+ //# sourceMappingURL=chunk-SCJPI4Z5.js.map
@@ -22,9 +22,9 @@ function withMaterializedView(spec) {
22
22
  throw new ValidationError("withMaterializedView: query must be a function returning a Query<T>");
23
23
  }
24
24
  if (spec.unionSources) {
25
- if (spec.unionSources.length < 2) {
25
+ if (spec.unionSources.length < 1) {
26
26
  throw new MaterializedViewConfigError(
27
- "unionSources requires at least 2 source collections"
27
+ "unionSources requires at least 1 source collection"
28
28
  );
29
29
  }
30
30
  const seen = /* @__PURE__ */ new Set();
@@ -56,6 +56,16 @@ function withMaterializedView(spec) {
56
56
  `withMaterializedView "${spec.name}": UNION strategy with aggregate requires groupBy \u2014 use groupBy to declare the bucketing keys, or remove aggregate for a pure dedup MV`
57
57
  );
58
58
  }
59
+ if (spec.moneyFields && !spec.aggregate) {
60
+ throw new MaterializedViewConfigError(
61
+ `withMaterializedView "${spec.name}": moneyFields requires aggregate \u2014 moneyFields rewrites sum/min/max reducers over money output fields, so it is meaningless without an aggregate spec`
62
+ );
63
+ }
64
+ if (spec.unionSources.some((s) => s.join && s.join.length > 0) && (!spec.sources || spec.sources.length === 0)) {
65
+ throw new MaterializedViewConfigError(
66
+ `withMaterializedView "${spec.name}": a unionSources arm declares join(s) but no \`sources\` are listed \u2014 declare sources: [...] with the right-side (join-target) collection names so writes to them trigger MV refresh`
67
+ );
68
+ }
59
69
  if (spec.predicates) {
60
70
  throw new MaterializedViewConfigError(
61
71
  `withMaterializedView "${spec.name}": predicates are not supported on UNION strategies \u2014 UNION mode does not use a Query<T> chain, so .wherePredicate() cannot fire. Use the query() form, or open an issue if per-arm predicates are needed`
@@ -79,4 +89,4 @@ function withMaterializedView(spec) {
79
89
  export {
80
90
  withMaterializedView
81
91
  };
82
- //# sourceMappingURL=chunk-ZWTNWAO4.js.map
92
+ //# sourceMappingURL=chunk-TKIY625R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/materialized-views/with-materialized-view.ts"],"sourcesContent":["import { MaterializedViewConfigError, ValidationError } from '../errors.js'\nimport type { MaterializedViewStrategy, MaterializedViewStrategyHandle } from './types.js'\n\n/**\n * Register a materialized view: a declared query whose result is\n * persisted as a queryable collection and kept fresh as sources\n * change. Writes go through the standard `Collection.put` pipeline;\n * refresh-driven deletes route through `Collection._internalDelete` so\n * user `onDelete` guards on the output collection aren't tripped by\n * housekeeping.\n *\n * Two registration modes:\n * - **single-source** — declare `query: (db) => Query<TRow>`; the\n * dependency analyzer derives source collections from the plan.\n * - **UNION** — declare `unionSources: [{ collection, map }, ...]`\n * plus optional `groupBy` + `aggregate`; the executor reads each\n * arm, maps to the unified row shape, concatenates, then groups\n * and aggregates.\n *\n * The two modes are mutually exclusive — exactly one of `query` /\n * `unionSources` must be set at registration time.\n *\n * See docs/superpowers/specs/2026-05-20-dim14-mv-v2-design.md (single-source v2)\n * and docs/superpowers/specs/2026-05-21-dim14-mv-multikey-and-union.md (UNION).\n */\nexport function withMaterializedView<TRow extends Record<string, unknown>>(\n spec: MaterializedViewStrategy<TRow>,\n): MaterializedViewStrategyHandle {\n if (!spec.name || spec.name.length === 0) {\n throw new ValidationError('withMaterializedView: name is required')\n }\n // Mutual exclusion: query and unionSources cannot coexist.\n if (spec.query && spec.unionSources) {\n throw new MaterializedViewConfigError(\n 'query and unionSources are mutually exclusive — pick one',\n )\n }\n // Strategy must declare one of the two.\n if (!spec.query && !spec.unionSources) {\n throw new MaterializedViewConfigError(\n 'strategy must declare either query or unionSources',\n )\n }\n if (spec.query !== undefined && typeof spec.query !== 'function') {\n throw new ValidationError('withMaterializedView: query must be a function returning a Query<T>')\n }\n // UNION-form invariants.\n if (spec.unionSources) {\n // A single arm is a deliberate shape (#331): map→group→aggregate over\n // ONE collection with a COMPUTED bucket key (e.g. month sliced from a\n // date field) — something the query form's stored-field groupBy cannot\n // express. The executor and dependency analyzer are arm-count-agnostic.\n if (spec.unionSources.length < 1) {\n throw new MaterializedViewConfigError(\n 'unionSources requires at least 1 source collection',\n )\n }\n const seen = new Set<string>()\n for (const s of spec.unionSources) {\n if (typeof s?.collection !== 'string' || s.collection.length === 0) {\n throw new MaterializedViewConfigError(\n 'each unionSources entry must declare a non-empty `collection` string',\n )\n }\n if (typeof s.map !== 'function') {\n throw new MaterializedViewConfigError(\n `unionSources entry for \"${s.collection}\" is missing a \\`map\\` function`,\n )\n }\n if (seen.has(s.collection)) {\n throw new MaterializedViewConfigError(\n `unionSources must reference distinct collections (duplicate: \"${s.collection}\")`,\n )\n }\n seen.add(s.collection)\n }\n if (Array.isArray(spec.groupBy) && spec.groupBy.length === 0) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": groupBy must not be an empty array — omit it or provide at least one field name`,\n )\n }\n if (spec.aggregate && !spec.groupBy) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": UNION strategy with aggregate requires groupBy — `\n + `use groupBy to declare the bucketing keys, or remove aggregate for a pure dedup MV`,\n )\n }\n // `moneyFields` only has meaning when there's an aggregate to\n // money-rewrite — it keys reducer outputs, so declaring it without\n // `aggregate` is a no-op config mistake.\n if (spec.moneyFields && !spec.aggregate) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": moneyFields requires aggregate — `\n + `moneyFields rewrites sum/min/max reducers over money output fields, `\n + `so it is meaningless without an aggregate spec`,\n )\n }\n // Per-arm joins resolve right-side collections that the union\n // dependency set (built from arm `collection`s alone) does NOT\n // include. The consumer must list those right-side collections in\n // `sources` so writes to them trigger MV refresh.\n if (spec.unionSources.some(s => s.join && s.join.length > 0) && (!spec.sources || spec.sources.length === 0)) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": a unionSources arm declares join(s) but `\n + `no \\`sources\\` are listed — declare sources: [...] with the right-side `\n + `(join-target) collection names so writes to them trigger MV refresh`,\n )\n }\n if (spec.predicates) {\n throw new MaterializedViewConfigError(\n `withMaterializedView \"${spec.name}\": predicates are not supported on UNION strategies — `\n + `UNION mode does not use a Query<T> chain, so .wherePredicate() cannot fire. `\n + `Use the query() form, or open an issue if per-arm predicates are needed`,\n )\n }\n }\n if (typeof spec.rowKey !== 'function') {\n throw new ValidationError('withMaterializedView: rowKey is required (no default; see spec § Type surface)')\n }\n if (spec.refresh !== 'eager' && spec.refresh !== 'lazy' && spec.refresh !== 'manual') {\n throw new ValidationError(\n `withMaterializedView: refresh must be 'eager' | 'lazy' | 'manual', got \"${String(spec.refresh)}\"`,\n )\n }\n return {\n __noydb_strategy: 'materialized-view',\n spec,\n }\n}\n"],"mappings":";;;;;;AAyBO,SAAS,qBACd,MACgC;AAChC,MAAI,CAAC,KAAK,QAAQ,KAAK,KAAK,WAAW,GAAG;AACxC,UAAM,IAAI,gBAAgB,wCAAwC;AAAA,EACpE;AAEA,MAAI,KAAK,SAAS,KAAK,cAAc;AACnC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,KAAK,SAAS,CAAC,KAAK,cAAc;AACrC,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,MAAI,KAAK,UAAU,UAAa,OAAO,KAAK,UAAU,YAAY;AAChE,UAAM,IAAI,gBAAgB,qEAAqE;AAAA,EACjG;AAEA,MAAI,KAAK,cAAc;AAKrB,QAAI,KAAK,aAAa,SAAS,GAAG;AAChC,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,UAAM,OAAO,oBAAI,IAAY;AAC7B,eAAW,KAAK,KAAK,cAAc;AACjC,UAAI,OAAO,GAAG,eAAe,YAAY,EAAE,WAAW,WAAW,GAAG;AAClE,cAAM,IAAI;AAAA,UACR;AAAA,QACF;AAAA,MACF;AACA,UAAI,OAAO,EAAE,QAAQ,YAAY;AAC/B,cAAM,IAAI;AAAA,UACR,2BAA2B,EAAE,UAAU;AAAA,QACzC;AAAA,MACF;AACA,UAAI,KAAK,IAAI,EAAE,UAAU,GAAG;AAC1B,cAAM,IAAI;AAAA,UACR,iEAAiE,EAAE,UAAU;AAAA,QAC/E;AAAA,MACF;AACA,WAAK,IAAI,EAAE,UAAU;AAAA,IACvB;AACA,QAAI,MAAM,QAAQ,KAAK,OAAO,KAAK,KAAK,QAAQ,WAAW,GAAG;AAC5D,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MACpC;AAAA,IACF;AACA,QAAI,KAAK,aAAa,CAAC,KAAK,SAAS;AACnC,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAEpC;AAAA,IACF;AAIA,QAAI,KAAK,eAAe,CAAC,KAAK,WAAW;AACvC,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AAKA,QAAI,KAAK,aAAa,KAAK,OAAK,EAAE,QAAQ,EAAE,KAAK,SAAS,CAAC,MAAM,CAAC,KAAK,WAAW,KAAK,QAAQ,WAAW,IAAI;AAC5G,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AACA,QAAI,KAAK,YAAY;AACnB,YAAM,IAAI;AAAA,QACR,yBAAyB,KAAK,IAAI;AAAA,MAGpC;AAAA,IACF;AAAA,EACF;AACA,MAAI,OAAO,KAAK,WAAW,YAAY;AACrC,UAAM,IAAI,gBAAgB,mFAAgF;AAAA,EAC5G;AACA,MAAI,KAAK,YAAY,WAAW,KAAK,YAAY,UAAU,KAAK,YAAY,UAAU;AACpF,UAAM,IAAI;AAAA,MACR,2EAA2E,OAAO,KAAK,OAAO,CAAC;AAAA,IACjG;AAAA,EACF;AACA,SAAO;AAAA,IACL,kBAAkB;AAAA,IAClB;AAAA,EACF;AACF;","names":[]}
@@ -33,37 +33,34 @@ var OverlayedCollection = class {
33
33
  /** Get the merged row by id. */
34
34
  async get(id) {
35
35
  const overlayRow = await this.overlayCollection.get(id);
36
- if (overlayRow !== null && this.shadowPredicateApplies(overlayRow)) {
37
- return overlayRow;
38
- }
39
36
  const baseRow = await this.baseCollection.get(id);
40
- if (baseRow !== null) return baseRow;
41
- return null;
37
+ return this.mergeRows(overlayRow, baseRow);
42
38
  }
43
39
  /** List union of base + overlay ids, applying the merge per row. */
44
40
  async list() {
45
41
  const baseRows = await this.baseCollection.list();
46
42
  const overlayRows = await this.overlayCollection.list();
47
- const merged = /* @__PURE__ */ new Map();
48
43
  const idOf = (row) => {
49
44
  if (this.baseRowKey) return this.baseRowKey(row);
50
45
  const idField = row.id;
51
46
  return typeof idField === "string" ? idField : "";
52
47
  };
48
+ const baseById = /* @__PURE__ */ new Map();
49
+ const overlayById = /* @__PURE__ */ new Map();
53
50
  for (const row of baseRows) {
54
51
  const id = idOf(row);
55
- if (id) merged.set(id, row);
52
+ if (id) baseById.set(id, row);
56
53
  }
57
54
  for (const row of overlayRows) {
58
55
  const id = idOf(row);
59
- if (!id) continue;
60
- if (this.shadowPredicateApplies(row)) {
61
- merged.set(id, row);
62
- } else if (!merged.has(id)) {
63
- continue;
64
- }
56
+ if (id) overlayById.set(id, row);
65
57
  }
66
- return [...merged.values()];
58
+ const out = [];
59
+ for (const id of /* @__PURE__ */ new Set([...baseById.keys(), ...overlayById.keys()])) {
60
+ const merged = this.mergeRows(overlayById.get(id) ?? null, baseById.get(id) ?? null);
61
+ if (merged !== null) out.push(merged);
62
+ }
63
+ return out;
67
64
  }
68
65
  /**
69
66
  * Write to the overlay. Two forms:
@@ -103,9 +100,42 @@ var OverlayedCollection = class {
103
100
  async delete(id) {
104
101
  await this.overlayCollection.delete(id);
105
102
  }
106
- /** True when `overlay[shadowField] === shadowValue`. */
107
- shadowPredicateApplies(row) {
108
- return row[this.spec.shadowField] === this.spec.shadowValue;
103
+ /**
104
+ * Merge a single id's overlay + base rows into the visible row.
105
+ *
106
+ * Priority (first match wins):
107
+ * 1. Binary shadow win — overlay present AND
108
+ * `overlay[shadowField] === shadowValue` → return the overlay row
109
+ * entirely. This stays FIRST so the original binary behaviour is
110
+ * unchanged whether or not `mergeMode` is configured.
111
+ * 2. Field-level merge — overlay present, `mergeMode` configured,
112
+ * and a rule whose `whenStatus` equals `overlay[shadowField]`.
113
+ * The matched rule pulls its `overlayFields` (those present on
114
+ * the overlay row) on top of the base row. With no base row, the
115
+ * overlay row is returned as-is.
116
+ * 3. Fallback — return the base row (possibly `null`). An
117
+ * overlay-only row that qualifies under neither (1) nor (2) and
118
+ * has no base is therefore NOT surfaced.
119
+ */
120
+ mergeRows(overlayRow, baseRow) {
121
+ const shadowField = this.spec.shadowField;
122
+ if (overlayRow !== null && overlayRow[shadowField] === this.spec.shadowValue) {
123
+ return overlayRow;
124
+ }
125
+ if (overlayRow !== null && this.spec.mergeMode) {
126
+ const status = overlayRow[shadowField];
127
+ const rule = this.spec.mergeMode.rules.find((r) => r.whenStatus === status);
128
+ if (rule) {
129
+ if (baseRow === null) return overlayRow;
130
+ const overlaySrc = overlayRow;
131
+ const picked = {};
132
+ for (const field of rule.overlayFields) {
133
+ if (field in overlaySrc) picked[field] = overlaySrc[field];
134
+ }
135
+ return { ...baseRow, ...picked };
136
+ }
137
+ }
138
+ return baseRow;
109
139
  }
110
140
  // ─── Throw-stubs for the unimplemented Collection<T> surface ───────
111
141
  //
@@ -176,4 +206,4 @@ var OverlayedCollection = class {
176
206
  export {
177
207
  OverlayedCollection
178
208
  };
179
- //# sourceMappingURL=chunk-OHVFWCJP.js.map
209
+ //# sourceMappingURL=chunk-VLMPU56Q.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/overlay-views/virtual-collection.ts"],"sourcesContent":["import { OverlayIdMismatchError } from '../errors.js'\nimport type { Collection } from '../collection.js'\nimport type { OverlayedViewStrategy } from './types.js'\n\n/**\n * Virtual-collection proxy returned by `vault.collection(overlayName)`\n * when `overlayName` is a registered `withOverlayedView`.\n *\n * Implements the core `Collection<T>`-shaped read/write surface with\n * merge-on-read semantics:\n * - `get(id)`: overlay row wins iff `overlay[shadowField] === shadowValue`;\n * when `spec.mergeMode` is set, an intermediate status may instead pull\n * a declared subset of fields from the overlay over the base (#348)\n * - `list()` / `.query()`: union of ids, per-id merge applied\n * - `put(record)` / `put(id, record)`: routes to overlay; id derived\n * via the base MV's `rowKey` (validated on the two-arg form)\n * - `delete(id)`: removes the overlay row only; base stays\n *\n * Reactive APIs (`live`, `subscribe`, `query().live()`) are out of\n * scope for this release and surface as \"not yet implemented\" — wired in a\n * future sub-issue.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport class OverlayedCollection<T extends Record<string, unknown> = any> {\n constructor(\n private readonly spec: OverlayedViewStrategy,\n private readonly baseCollection: Collection<T>,\n private readonly overlayCollection: Collection<T>,\n private readonly baseRowKey: ((row: Record<string, unknown>) => string) | undefined,\n ) {}\n\n /**\n * Convenience accessors for advanced callers that need to bypass the\n * virtual layer (bulk imports, direct overlay queries). Mirrors the\n * spec's \"direct writes to the underlying overlay collection skip\n * the validation\" escape hatch.\n */\n readonly overlay = {\n rowKey: (row: Record<string, unknown>): string => {\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\": base \"${this.spec.base}\" is not an MV — ` +\n `cannot auto-derive id from the row. Use \\`put(id, record)\\` instead.`,\n )\n }\n return this.baseRowKey(row)\n },\n }\n\n /** Get the merged row by id. */\n async get(id: string): Promise<T | null> {\n const overlayRow = await this.overlayCollection.get(id)\n const baseRow = await this.baseCollection.get(id)\n return this.mergeRows(overlayRow, baseRow)\n }\n\n /** List union of base + overlay ids, applying the merge per row. */\n async list(): Promise<T[]> {\n const baseRows = await this.baseCollection.list()\n const overlayRows = await this.overlayCollection.list()\n const idOf = (row: T): string => {\n // Best-effort: use baseRowKey if available, else assume the row\n // has a `.id` field (common pattern). The spec requires every\n // base MV to declare `rowKey`, so the first branch is the\n // canonical path.\n if (this.baseRowKey) return this.baseRowKey(row as Record<string, unknown>)\n const idField = (row as Record<string, unknown>).id\n return typeof idField === 'string' ? idField : ''\n }\n // Key base + overlay rows by id, then union the id sets and run the\n // same per-id merge `get()` uses. Nulls (overlay-only rows that\n // don't qualify and have no base) are filtered out.\n const baseById = new Map<string, T>()\n const overlayById = new Map<string, T>()\n for (const row of baseRows) {\n const id = idOf(row)\n if (id) baseById.set(id, row)\n }\n for (const row of overlayRows) {\n const id = idOf(row)\n if (id) overlayById.set(id, row)\n }\n const out: T[] = []\n for (const id of new Set([...baseById.keys(), ...overlayById.keys()])) {\n const merged = this.mergeRows(overlayById.get(id) ?? null, baseById.get(id) ?? null)\n if (merged !== null) out.push(merged)\n }\n return out\n }\n\n /**\n * Write to the overlay. Two forms:\n * - `put(record)`: id is derived via the base MV's `rowKey(record)`.\n * Throws if the base isn't an MV.\n * - `put(id, record)`: validates `id === rowKey(record)`; throws\n * `OverlayIdMismatchError` on mismatch.\n */\n async put(idOrRecord: string | T, maybeRecord?: T): Promise<void> {\n let id: string\n let record: T\n if (maybeRecord === undefined) {\n // Single-arg form: put(record). Derive id via base rowKey.\n record = idOrRecord as T\n if (!this.baseRowKey) {\n throw new Error(\n `Overlay \"${this.spec.name}\".put(record): base \"${this.spec.base}\" is not an MV. ` +\n `Use put(id, record) explicitly.`,\n )\n }\n id = this.baseRowKey(record as Record<string, unknown>)\n } else {\n // Two-arg form: put(id, record). Validate against rowKey.\n id = idOrRecord as string\n record = maybeRecord\n if (this.baseRowKey) {\n const expected = this.baseRowKey(record as Record<string, unknown>)\n if (id !== expected) {\n throw new OverlayIdMismatchError(id, expected)\n }\n }\n }\n await this.overlayCollection.put(id, record)\n }\n\n /**\n * Remove the overlay row only. Idempotent (no-op on absent).\n * The base row is untouched — if a base row exists for `id`,\n * subsequent reads return it.\n */\n async delete(id: string): Promise<void> {\n await this.overlayCollection.delete(id)\n }\n\n /**\n * Merge a single id's overlay + base rows into the visible row.\n *\n * Priority (first match wins):\n * 1. Binary shadow win — overlay present AND\n * `overlay[shadowField] === shadowValue` → return the overlay row\n * entirely. This stays FIRST so the original binary behaviour is\n * unchanged whether or not `mergeMode` is configured.\n * 2. Field-level merge — overlay present, `mergeMode` configured,\n * and a rule whose `whenStatus` equals `overlay[shadowField]`.\n * The matched rule pulls its `overlayFields` (those present on\n * the overlay row) on top of the base row. With no base row, the\n * overlay row is returned as-is.\n * 3. Fallback — return the base row (possibly `null`). An\n * overlay-only row that qualifies under neither (1) nor (2) and\n * has no base is therefore NOT surfaced.\n */\n private mergeRows(overlayRow: T | null, baseRow: T | null): T | null {\n const shadowField = this.spec.shadowField\n if (\n overlayRow !== null &&\n (overlayRow as Record<string, unknown>)[shadowField] === this.spec.shadowValue\n ) {\n return overlayRow\n }\n if (overlayRow !== null && this.spec.mergeMode) {\n const status = (overlayRow as Record<string, unknown>)[shadowField]\n const rule = this.spec.mergeMode.rules.find((r) => r.whenStatus === status)\n if (rule) {\n if (baseRow === null) return overlayRow\n const overlaySrc = overlayRow as Record<string, unknown>\n const picked: Record<string, unknown> = {}\n for (const field of rule.overlayFields) {\n if (field in overlaySrc) picked[field] = overlaySrc[field]\n }\n return { ...(baseRow as Record<string, unknown>), ...picked } as T\n }\n }\n return baseRow\n }\n\n // ─── Throw-stubs for the unimplemented Collection<T> surface ───────\n //\n // `Vault.collection(name)` widens the return type to `Collection<T>`\n // for the overlay intercept, but `OverlayedCollection` doesn't\n // implement the full surface. These stubs catch the common\n // reactive / chainable APIs with a clear \"not yet implemented\"\n // error pointing at the relevant issue — so consumers don't hit a\n // cryptic `undefined is not a function` runtime crash.\n //\n // Throw-stubs so consumers get actionable errors rather than cryptic crashes.\n\n /** @throws — chainable Query<T> over a virtual collection is deferred. */\n query(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".query() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` + filter for now, or read from the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections directly. ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — change-stream subscription over a virtual collection is deferred. */\n subscribe(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".subscribe() is not yet implemented for overlay views (#154). ` +\n `Subscribe to the underlying \\`${this.spec.base}\\` / \\`${this.spec.overlay}\\` collections individually for now. ` +\n `Merged change-stream lands in a future MV sub-issue.`,\n )\n }\n\n /** @throws — live query over a virtual collection is deferred. */\n live(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".live() is not yet implemented for overlay views (#154). ` +\n `Reactive APIs land in a future MV sub-issue.`,\n )\n }\n\n /** @throws — async iteration over a virtual collection is deferred. */\n scan(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".scan() is not yet implemented for overlay views (#154). ` +\n `Use \\`list()\\` for now (no row-count ceiling at niwat scale), or scan the underlying collections directly.`,\n )\n }\n\n /** @throws — lazy-mode query is not applicable to virtual collections. */\n lazyQuery(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".lazyQuery() is not supported. ` +\n `Virtual collections always materialize through base + overlay reads — lazy-mode indexed lookups don't apply.`,\n )\n }\n\n /** @throws — bulk-atomic put is deferred to a future MV sub-issue. */\n putManyAtomic(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".putManyAtomic() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.put(record)\\` calls for now, or write to \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — bulk delete is deferred to a future MV sub-issue. */\n deleteMany(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".deleteMany() is not yet implemented for overlay views (#154). ` +\n `Use sequential \\`.delete(id)\\` calls for now, or operate on \\`${this.spec.overlay}\\` directly.`,\n )\n }\n\n /** @throws — `.first()` over a virtual collection is deferred. */\n first(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".first() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list())[0]\\` for now.`,\n )\n }\n\n /** @throws — `.count()` over a virtual collection is deferred. */\n count(): never {\n throw new Error(\n `OverlayedCollection \"${this.spec.name}\".count() is not yet implemented for overlay views (#154). ` +\n `Use \\`(await list()).length\\` for now.`,\n )\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NOYDB_FORMAT_VERSION
3
- } from "./chunk-GC4V7RU7.js";
3
+ } from "./chunk-F3BPIPLS.js";
4
4
  import {
5
5
  encrypt
6
6
  } from "./chunk-YULZKK4F.js";
@@ -258,4 +258,4 @@ export {
258
258
  BLOB_EVICTION_AUDIT_COLLECTION,
259
259
  runCompaction
260
260
  };
261
- //# sourceMappingURL=chunk-6AJBSQU4.js.map
261
+ //# sourceMappingURL=chunk-XL35NSEN.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  readPath
3
- } from "./chunk-TV3YZ35S.js";
3
+ } from "./chunk-CJORTUJ2.js";
4
4
 
5
5
  // src/indexing/eager-indexes.ts
6
6
  var CollectionIndexes = class {
@@ -129,4 +129,4 @@ function removeFromIndex(idx, id, record) {
129
129
  export {
130
130
  CollectionIndexes
131
131
  };
132
- //# sourceMappingURL=chunk-WIBHRONM.js.map
132
+ //# sourceMappingURL=chunk-XWH4MXIU.js.map
@@ -1,10 +1,10 @@
1
- import { a8 as ConsentStrategy } from '../types-V5R2-pd4.cjs';
2
- export { a9 as CONSENT_AUDIT_COLLECTION, aa as ConsentAuditEntry, ab as ConsentAuditFilter, ac as ConsentContext, ad as ConsentOp, ae as loadConsentEntries, af as writeConsentEntry } from '../types-V5R2-pd4.cjs';
3
- import '../lazy-builder-Ci5_YG73.cjs';
4
- import '../predicate-Bt5ft-9c.cjs';
5
- import '../strategy-CrS7PnbE.cjs';
1
+ import { a8 as ConsentStrategy } from '../types-Bze6vkwm.cjs';
2
+ export { a9 as CONSENT_AUDIT_COLLECTION, aa as ConsentAuditEntry, ab as ConsentAuditFilter, ac as ConsentContext, ad as ConsentOp, ae as loadConsentEntries, af as writeConsentEntry } from '../types-Bze6vkwm.cjs';
3
+ import '../lazy-builder-eYZzLEL1.cjs';
4
+ import '../predicate-BmhBSPCH.cjs';
5
+ import '../strategy-BtW8fAjz.cjs';
6
6
  import '../strategy-BSxFXGzb.cjs';
7
- import '../index-CUVOMtgg.cjs';
7
+ import '../index-C8Bk3-VF.cjs';
8
8
  import '@noy-db/attestation';
9
9
 
10
10
  /**
@@ -1,10 +1,10 @@
1
- import { a8 as ConsentStrategy } from '../types-BFHQUjdy.js';
2
- export { a9 as CONSENT_AUDIT_COLLECTION, aa as ConsentAuditEntry, ab as ConsentAuditFilter, ac as ConsentContext, ad as ConsentOp, ae as loadConsentEntries, af as writeConsentEntry } from '../types-BFHQUjdy.js';
3
- import '../lazy-builder-D5GU14TS.js';
4
- import '../predicate-Bt5ft-9c.js';
5
- import '../strategy-CrS7PnbE.js';
1
+ import { a8 as ConsentStrategy } from '../types-DrmBTscX.js';
2
+ export { a9 as CONSENT_AUDIT_COLLECTION, aa as ConsentAuditEntry, ab as ConsentAuditFilter, ac as ConsentContext, ad as ConsentOp, ae as loadConsentEntries, af as writeConsentEntry } from '../types-DrmBTscX.js';
3
+ import '../lazy-builder-ChSqcF5t.js';
4
+ import '../predicate-BmhBSPCH.js';
5
+ import '../strategy-BtW8fAjz.js';
6
6
  import '../strategy-BSxFXGzb.js';
7
- import '../index-Cqzp4tt9.js';
7
+ import '../index-nP99bXLg.js';
8
8
  import '@noy-db/attestation';
9
9
 
10
10
  /**
@@ -123,6 +123,18 @@ function withDerivation(spec) {
123
123
  if (typeof spec.derive !== "function") {
124
124
  throw new ValidationError("withDerivation: derive must be a function");
125
125
  }
126
+ if (spec.sources !== void 0) {
127
+ for (const extra of spec.sources) {
128
+ if (typeof extra !== "string" || extra.length === 0) {
129
+ throw new ValidationError("withDerivation: each entry in sources[] must be a non-empty string");
130
+ }
131
+ if (extra === spec.source) {
132
+ throw new ValidationError(
133
+ `withDerivation: sources[] must not contain the primary source "${spec.source}"`
134
+ );
135
+ }
136
+ }
137
+ }
126
138
  const lifecycleMode = typeof spec.lifecycle === "string" ? spec.lifecycle : spec.lifecycle.mode;
127
139
  for (const [outputKey, outputSpec] of Object.entries(spec.outputs)) {
128
140
  if (outputSpec.shape === "array") {
@@ -153,11 +165,15 @@ function withDerivation(spec) {
153
165
  }
154
166
 
155
167
  // src/derivations/strategy-hash.ts
156
- async function computeStrategyHash(source, outputKeys, derive) {
168
+ async function computeStrategyHash(source, outputKeys, derive, sources) {
157
169
  const canonical = JSON.stringify({
158
170
  source,
159
171
  outputs: [...outputKeys].sort(),
160
- derive: derive.toString()
172
+ derive: derive.toString(),
173
+ // Declared sibling sources (#344) — adding/removing a trigger
174
+ // collection invalidates cached derived records. Omitted when empty
175
+ // so strategies without siblings keep their existing hash.
176
+ ...sources?.length ? { sources: [...sources].sort() } : {}
161
177
  });
162
178
  const bytes = new TextEncoder().encode(canonical);
163
179
  const digest = await crypto.subtle.digest("SHA-256", bytes);
@@ -171,11 +187,16 @@ var DerivationRegistry = class {
171
187
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
172
188
  async register(spec) {
173
189
  const outputKeys = Object.keys(spec.outputs);
174
- const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive);
190
+ const strategyHash = await computeStrategyHash(spec.source, outputKeys, spec.derive, spec.sources);
175
191
  const reg = { spec, strategyHash };
176
192
  const fromSource = this._bySource.get(spec.source);
177
193
  if (fromSource) fromSource.push(reg);
178
194
  else this._bySource.set(spec.source, [reg]);
195
+ for (const extra of spec.sources ?? []) {
196
+ const fromExtra = this._bySource.get(extra);
197
+ if (fromExtra) fromExtra.push(reg);
198
+ else this._bySource.set(extra, [reg]);
199
+ }
179
200
  for (const key of outputKeys) {
180
201
  const output = spec.outputs[key];
181
202
  if (!output) continue;