@reventlessdev/reventless-core 3.0.0-alpha.259 → 3.0.0-alpha.261

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 (209) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +11 -11
  3. package/src/Behavior.res +1 -4
  4. package/src/Message.res +17 -14
  5. package/src/Projection.res +51 -25
  6. package/src/RequestContext.res +8 -9
  7. package/src/ResourceAttribution.res +28 -14
  8. package/src/adapter/Adapter.res +48 -4
  9. package/src/adapter/EventLogProvisioning/EventLogProvisioning.res +5 -5
  10. package/src/adapter/Messaging/Messaging_Log_Backend.res +10 -7
  11. package/src/adapter/Messaging/Messaging_Log_Backend.res.mjs +2 -4
  12. package/src/adapter/Monitoring/Monitoring.res +16 -17
  13. package/src/adapter/QueryInterception/QueryInterception.res +0 -1
  14. package/src/adapter/Runtime/AggregateRuntime_Builder_Common.res +12 -5
  15. package/src/adapter/Runtime/EventCollectorRuntime_Builder_Single.res +11 -7
  16. package/src/adapter/Runtime/PluginRuntime_Builder_Micro.res +1 -7
  17. package/src/adapter/Runtime/PluginRuntime_Builder_Single.res +1 -7
  18. package/src/adapter/RuntimeExtension/RuntimeExtension.res +0 -1
  19. package/src/admin/Platform_Admin.res +17 -6
  20. package/src/admin/Platform_AdminApi.res +14 -9
  21. package/src/admin/Platform_Admin_Callback.res +1 -1
  22. package/src/admin/Platform_BakedManifest.res +34 -34
  23. package/src/admin/Platform_ComponentDefinitionsApi.res +9 -21
  24. package/src/admin/Platform_PluginStructuresApi.res +15 -12
  25. package/src/admin/UiFragmentRegistry/StateChange/UiFragmentRegistry.res +1 -1
  26. package/src/components/Aggregate/Aggregate.res +13 -12
  27. package/src/components/Aggregate/Aggregate_Builder.res +22 -21
  28. package/src/components/Aggregate/Aggregate_Callback.res +91 -86
  29. package/src/components/Api/ApiNoApiHelpers.res +1 -3
  30. package/src/components/Api/Api_Ids.res +0 -1
  31. package/src/components/Api/Api_Naming.res +41 -18
  32. package/src/components/Api/GraphQL_FragmentGenerator.res +42 -52
  33. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +11 -4
  34. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  35. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  36. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  37. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  38. package/src/components/Api/QueryDbListQuery.res +13 -6
  39. package/src/components/Api/SchemaType.res +40 -20
  40. package/src/components/Api/SchemaType.res.mjs +16 -0
  41. package/src/components/Api/SuryToJsonSchema.res +18 -10
  42. package/src/components/Api/SuryToJsonSchema.res.mjs +135 -121
  43. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  44. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  45. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  46. package/src/components/Builder_Helpers.res +12 -23
  47. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  48. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  49. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  50. package/src/components/CommandTopic/CommandTopic.res +3 -3
  51. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  52. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  53. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  54. package/src/components/Counter/Counter_Builder.res +1 -6
  55. package/src/components/Counter/Counter_Callback.res +3 -3
  56. package/src/components/Dcb/Dcb_Builder.res +970 -956
  57. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  58. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  59. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  60. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  61. package/src/components/EventLog/EventLog.res +5 -1
  62. package/src/components/EventLog/EventLog_Operations.res +12 -8
  63. package/src/components/EventMapper/EventMapper.res +8 -10
  64. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  65. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  66. package/src/components/EventTopic/EventTopic.res +3 -3
  67. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  68. package/src/components/Extension/Extension.res +4 -1
  69. package/src/components/Extension/Extension_Operations.res +11 -3
  70. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  71. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  72. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  73. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  74. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  75. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  76. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  77. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  78. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  79. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  80. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  81. package/src/components/ReadModel/ReadModel.res +3 -3
  82. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  83. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  84. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  85. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  86. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  87. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  88. package/src/components/StateTopic.res +1 -3
  89. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  90. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  91. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  92. package/src/components/Task/Task.res +3 -3
  93. package/src/components/Task/Task_Adapter.res +4 -1
  94. package/src/components/Task/Task_Builder.res +24 -21
  95. package/src/plugin/api/PluginBaseFragment.res +9 -3
  96. package/src/plugin/component/Capability_Inference.res +8 -10
  97. package/src/plugin/component/Plugin.res +6 -6
  98. package/src/plugin/component/Plugin_Builder.res +54 -50
  99. package/src/plugin/component/Plugin_Callback.res +10 -6
  100. package/src/plugin/component/Plugin_Helpers.res +264 -230
  101. package/src/plugin/component/Plugin_Structure.res +370 -324
  102. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  103. package/src/plugin/component/SchemaWalker.res +11 -12
  104. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  105. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  106. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  107. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  108. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  109. package/src/util/CommandPublisher.res +9 -2
  110. package/src/util/CsvStream.res +3 -3
  111. package/src/util/Interstack.res +7 -15
  112. package/src/util/Interstack.resi +3 -3
  113. package/src/util/LogFormat.res +5 -9
  114. package/src/util/Logger.res +1 -2
  115. package/src/util/Lru.res +2 -1
  116. package/src/util/Lru.resi +2 -3
  117. package/src/util/ScheduleOps.res +24 -27
  118. package/src/util/StorePrefixCollision.res +6 -9
  119. package/src/util/Util_Promise.res +4 -1
  120. package/src/util/Util_Promise.res.mjs +1 -1
  121. package/tests/IdentityTest.res +166 -113
  122. package/tests/ResourceAttributionTest.res +28 -25
  123. package/tests/StorePrefixCollisionTest.res +2 -1
  124. package/tests/TaskBucketNamingTest.res +3 -2
  125. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  126. package/tests/adapter/MonitoringTest.res +81 -38
  127. package/tests/adapter/QueryInterceptionTest.res +2 -2
  128. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  129. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  130. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  131. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  132. package/tests/admin/Platform_Admin_StructureTest.res +10 -11
  133. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  134. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  135. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  136. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  137. package/tests/aggregate/AggregateCacheTest.res +10 -12
  138. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  139. package/tests/aggregate/AggregateCausationTest.res +19 -20
  140. package/tests/aggregate/AggregateConflictTest.res +41 -19
  141. package/tests/aggregate/AggregateFixtures.res +15 -5
  142. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  143. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  144. package/tests/api/Api_IdsTest.res +10 -4
  145. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  146. package/tests/api/GeolocationSemanticTest.res +3 -3
  147. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  148. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  149. package/tests/api/StorageRefFieldsTest.res +30 -18
  150. package/tests/api/SuryToJsonSchemaTest.res +1338 -981
  151. package/tests/api/SuryToJsonSchemaTest.res.mjs +31 -0
  152. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  153. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  154. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  155. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  156. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  157. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  158. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  159. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  160. package/tests/counter/CounterCallbackTest.res +40 -34
  161. package/tests/counter/CounterFixtures.res +4 -4
  162. package/tests/dcb/DcbDecodeTest.res +115 -84
  163. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  164. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  165. package/tests/dcb/DcbFixtures.res +4 -5
  166. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  167. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  168. package/tests/dcb/DcbTagTest.res +22 -11
  169. package/tests/dcb/DcbValidationTest.res +288 -218
  170. package/tests/eventlog/EventLogFixtures.res +4 -4
  171. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  172. package/tests/eventlog/EventLogRetryTest.res +104 -65
  173. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  174. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  175. package/tests/eventmapper/EventMapperTest.res +54 -38
  176. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  177. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  178. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  179. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  180. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  181. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  182. package/tests/logger/LogFormatTest.res +104 -82
  183. package/tests/logger/LoggerLazyTest.res +21 -12
  184. package/tests/message/MessageTest.res +96 -93
  185. package/tests/message/MetaEnvelopeTest.res +26 -30
  186. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  187. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  188. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  189. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  190. package/tests/plugin/PluginSpecExperiment.res +0 -1
  191. package/tests/plugin/PluginStructureTest.res +1533 -1030
  192. package/tests/plugin/PluginStructureTest.res.mjs +6 -0
  193. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  194. package/tests/plugin/SchemaWalkerTest.res +116 -70
  195. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  196. package/tests/plugin/StateView/PsAnnotatedView.res +6 -4
  197. package/tests/plugin/StateView/PsAnnotatedView.res.mjs +7 -3
  198. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  199. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  200. package/tests/projection/LifecycleTrailTest.res +1 -4
  201. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  202. package/tests/querydb/QueryDbFixtures.res +0 -1
  203. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  204. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  205. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  206. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  207. package/tests/util/CommandPublisherTest.res +0 -1
  208. package/tests/util/CsvStreamTest.res +3 -3
  209. package/tests/util/LruTest.res +84 -56
@@ -6,7 +6,13 @@ module DSI = Reventless.DcbScopeInference
6
6
  let scal = (name): DSI.idField => {name, isList: false}
7
7
  let arr = (name): DSI.idField => {name, isList: true}
8
8
  let ev = (eventType, idFields): DSI.eventShape => {eventType, idFields}
9
- let slice = (sliceName, ~command=[], ~consumed=[], ~produced=[], ~partitionHint=?): DSI.sliceShape => {
9
+ let slice = (
10
+ sliceName,
11
+ ~command=[],
12
+ ~consumed=[],
13
+ ~produced=[],
14
+ ~partitionHint=?,
15
+ ): DSI.sliceShape => {
10
16
  sliceName,
11
17
  command,
12
18
  consumed,
@@ -19,7 +25,11 @@ let slice = (sliceName, ~command=[], ~consumed=[], ~produced=[], ~partitionHint=
19
25
  let addProduct = slice(
20
26
  "AddProduct",
21
27
  ~command=[scal("productId"), scal("categoryId")],
22
- ~consumed=[ev("ProductAdded", [scal("productId")]), ev("CategoryAdded", [scal("categoryId")]), ev("CategoryArchived", [scal("categoryId")])],
28
+ ~consumed=[
29
+ ev("ProductAdded", [scal("productId")]),
30
+ ev("CategoryAdded", [scal("categoryId")]),
31
+ ev("CategoryArchived", [scal("categoryId")]),
32
+ ],
23
33
  ~produced=[ev("ProductAdded", [scal("productId"), scal("categoryId")])],
24
34
  )
25
35
  let addCategory = slice(
@@ -44,47 +54,57 @@ let catalog = [addProduct, addCategory, renameCategory, archiveCategory]
44
54
 
45
55
  describe("DcbScopeInference:", () => {
46
56
  describe("tagKeyOf", () => {
47
- testSync("scalar *Id uses the field name", () =>
48
- expect(DSI.tagKeyOf(scal("categoryId")))->toBe("categoryId")
57
+ testSync(
58
+ "scalar *Id uses the field name",
59
+ () => expect(DSI.tagKeyOf(scal("categoryId")))->toBe("categoryId"),
49
60
  )
50
- testSync("plural *Ids strips the trailing s", () =>
51
- expect(DSI.tagKeyOf(arr("productIds")))->toBe("productId")
61
+ testSync(
62
+ "plural *Ids strips the trailing s",
63
+ () => expect(DSI.tagKeyOf(arr("productIds")))->toBe("productId"),
52
64
  )
53
65
  })
54
66
 
55
67
  describe("hybrid catalog (no annotations)", () => {
56
68
  let d = DSI.infer(catalog)
57
69
 
58
- testSync("derives categoryId as the only cross-partition key — without any @crossPartition", () =>
59
- expect(d.crossPartitionTagKeys)->toEqual(["categoryId"])
70
+ testSync(
71
+ "derives categoryId as the only cross-partition key — without any @crossPartition",
72
+ () => expect(d.crossPartitionTagKeys)->toEqual(["categoryId"]),
60
73
  )
61
74
 
62
- testSync("AddProduct partition = productId (foreign categoryId eliminated)", () =>
63
- expect(d.partitionBySlice->Dict.get("AddProduct"))->toEqual(Some("productId"))
75
+ testSync(
76
+ "AddProduct partition = productId (foreign categoryId eliminated)",
77
+ () => expect(d.partitionBySlice->Dict.get("AddProduct"))->toEqual(Some("productId")),
64
78
  )
65
79
 
66
- testSync("Category slices partition = categoryId", () => {
67
- expect(d.partitionBySlice->Dict.get("AddCategory"))->toEqual(Some("categoryId"))
68
- expect(d.partitionBySlice->Dict.get("RenameCategory"))->toEqual(Some("categoryId"))
69
- expect(d.partitionBySlice->Dict.get("ArchiveCategory"))->toEqual(Some("categoryId"))
70
- })
71
-
72
- testSync("ProductAdded indexes productId only — categoryId is payload (no sibling leak)", () =>
73
- expect(d.tagKeysByEventType->Dict.get("ProductAdded"))->toEqual(Some(["productId"]))
80
+ testSync(
81
+ "Category slices partition = categoryId",
82
+ () => {
83
+ expect(d.partitionBySlice->Dict.get("AddCategory"))->toEqual(Some("categoryId"))
84
+ expect(d.partitionBySlice->Dict.get("RenameCategory"))->toEqual(Some("categoryId"))
85
+ expect(d.partitionBySlice->Dict.get("ArchiveCategory"))->toEqual(Some("categoryId"))
86
+ },
74
87
  )
75
88
 
76
- testSync("CategoryAdded indexes its own categoryId", () =>
77
- expect(d.tagKeysByEventType->Dict.get("CategoryAdded"))->toEqual(Some(["categoryId"]))
89
+ testSync(
90
+ "ProductAdded indexes productId only — categoryId is payload (no sibling leak)",
91
+ () => expect(d.tagKeysByEventType->Dict.get("ProductAdded"))->toEqual(Some(["productId"])),
78
92
  )
79
93
 
80
- testSync("no ambiguities for a well-formed boundary", () =>
81
- expect(d.ambiguities)->toEqual([])
94
+ testSync(
95
+ "CategoryAdded indexes its own categoryId",
96
+ () => expect(d.tagKeysByEventType->Dict.get("CategoryAdded"))->toEqual(Some(["categoryId"])),
82
97
  )
83
98
 
84
- testSync("ownerByKey points categoryId at a Category slice, productId at Product", () => {
85
- expect(d.ownerByKey->Dict.get("productId"))->toEqual(Some("AddProduct"))
86
- expect(d.ownerByKey->Dict.get("categoryId")->Option.isSome)->toBe(true)
87
- })
99
+ testSync("no ambiguities for a well-formed boundary", () => expect(d.ambiguities)->toEqual([]))
100
+
101
+ testSync(
102
+ "ownerByKey points categoryId at a Category slice, productId at Product",
103
+ () => {
104
+ expect(d.ownerByKey->Dict.get("productId"))->toEqual(Some("AddProduct"))
105
+ expect(d.ownerByKey->Dict.get("categoryId")->Option.isSome)->toBe(true)
106
+ },
107
+ )
88
108
  })
89
109
 
90
110
  describe("own composite read + @partitionTag hint (RecordProductDemand shape)", () => {
@@ -94,22 +114,34 @@ describe("DcbScopeInference:", () => {
94
114
  let recordDemand = slice(
95
115
  "RecordProductDemand",
96
116
  ~partitionHint="productId",
97
- ~consumed=[ev("ProductDemandRecorded", [scal("orderId")]), ev("ProductDemandRevoked", [scal("orderId")])],
98
- ~produced=[ev("ProductDemandRecorded", [scal("productId"), scal("orderId")]), ev("ProductDemandRevoked", [scal("productId"), scal("orderId")])],
117
+ ~consumed=[
118
+ ev("ProductDemandRecorded", [scal("orderId")]),
119
+ ev("ProductDemandRevoked", [scal("orderId")]),
120
+ ],
121
+ ~produced=[
122
+ ev("ProductDemandRecorded", [scal("productId"), scal("orderId")]),
123
+ ev("ProductDemandRevoked", [scal("productId"), scal("orderId")]),
124
+ ],
99
125
  )
100
126
  let d = DSI.infer([recordDemand])
101
127
 
102
- testSync("the @partitionTag hint resolves the partition (no ambiguity)", () => {
103
- expect(d.partitionBySlice->Dict.get("RecordProductDemand"))->toEqual(Some("productId"))
104
- expect(d.ambiguities)->toEqual([])
105
- })
106
- testSync("orderId stays indexed — it is an own-stream read key, not a foreign ref", () =>
107
- expect(d.tagKeysByEventType->Dict.get("ProductDemandRecorded"))->toEqual(
108
- Some(["orderId", "productId"]),
109
- )
128
+ testSync(
129
+ "the @partitionTag hint resolves the partition (no ambiguity)",
130
+ () => {
131
+ expect(d.partitionBySlice->Dict.get("RecordProductDemand"))->toEqual(Some("productId"))
132
+ expect(d.ambiguities)->toEqual([])
133
+ },
110
134
  )
111
- testSync("orderId is not cross-partition (no Order entity owns it in this boundary)", () =>
112
- expect(d.crossPartitionTagKeys)->toEqual([])
135
+ testSync(
136
+ "orderId stays indexed — it is an own-stream read key, not a foreign ref",
137
+ () =>
138
+ expect(d.tagKeysByEventType->Dict.get("ProductDemandRecorded"))->toEqual(
139
+ Some(["orderId", "productId"]),
140
+ ),
141
+ )
142
+ testSync(
143
+ "orderId is not cross-partition (no Order entity owns it in this boundary)",
144
+ () => expect(d.crossPartitionTagKeys)->toEqual([]),
113
145
  )
114
146
  })
115
147
 
@@ -122,7 +154,10 @@ describe("DcbScopeInference:", () => {
122
154
  let placeOrder = slice(
123
155
  "PlaceOrder",
124
156
  ~command=[scal("orderId"), arr("productIds")],
125
- ~consumed=[ev("OrderPlaced", [scal("orderId")]), ev("CatalogProductSynced", [scal("productId")])],
157
+ ~consumed=[
158
+ ev("OrderPlaced", [scal("orderId")]),
159
+ ev("CatalogProductSynced", [scal("productId")]),
160
+ ],
126
161
  ~produced=[ev("OrderPlaced", [scal("orderId"), arr("productIds")])],
127
162
  )
128
163
  let productEntity = slice(
@@ -133,14 +168,17 @@ describe("DcbScopeInference:", () => {
133
168
  )
134
169
  let d = DSI.infer([placeOrder, productEntity])
135
170
 
136
- testSync("an array-only foreign read is NOT cross-partition (auto-fans, partition-scoped)", () =>
137
- expect(d.crossPartitionTagKeys)->toEqual([])
171
+ testSync(
172
+ "an array-only foreign read is NOT cross-partition (auto-fans, partition-scoped)",
173
+ () => expect(d.crossPartitionTagKeys)->toEqual([]),
138
174
  )
139
- testSync("PlaceOrder partition = orderId", () =>
140
- expect(d.partitionBySlice->Dict.get("PlaceOrder"))->toEqual(Some("orderId"))
175
+ testSync(
176
+ "PlaceOrder partition = orderId",
177
+ () => expect(d.partitionBySlice->Dict.get("PlaceOrder"))->toEqual(Some("orderId")),
141
178
  )
142
- testSync("OrderPlaced indexes orderId only (productIds is a foreign payload ref)", () =>
143
- expect(d.tagKeysByEventType->Dict.get("OrderPlaced"))->toEqual(Some(["orderId"]))
179
+ testSync(
180
+ "OrderPlaced indexes orderId only (productIds is a foreign payload ref)",
181
+ () => expect(d.tagKeysByEventType->Dict.get("OrderPlaced"))->toEqual(Some(["orderId"])),
144
182
  )
145
183
  })
146
184
 
@@ -151,71 +189,86 @@ describe("DcbScopeInference:", () => {
151
189
  describe("schema adapter ⇄ inference parity", () => {
152
190
  let shapes = Reventless.DcbTag.eventShapesOfSchema(DcbFixtures.subscriptionEventSchema)
153
191
 
154
- testSync("eventShapesOfSchema extracts *Id fields by name (tag flags ignored)", () => {
155
- expect(shapes->Array.length)->toBe(1)
156
- let arm = shapes->Array.getUnsafe(0)
157
- expect(arm.eventType)->toBe("StudentSubscribed")
158
- let names = arm.idFields->Array.map(f => f.name)->Array.toSorted(String.compare)
159
- expect(names)->toEqual(["courseId", "studentId"])
160
- })
161
-
162
- testSync("inference reproduces extractCrossPartitionTagKeys ⇒ [\"studentId\"]", () => {
163
- // A Student entity owns studentId; Subscribe (built from the real fixture
164
- // schema) reads it from the foreign StudentRegistered event.
165
- let studentEntity = slice(
166
- "RegisterStudent",
167
- ~produced=[ev("StudentRegistered", [scal("studentId")])],
168
- )
169
- let subscribe: DSI.sliceShape = {
170
- sliceName: "SubscribeStudent",
171
- // The real command carries studentId as a SCALAR — so the foreign read is
172
- // fanned cross-partition (a scalar reference, unlike PlaceOrder's array).
173
- command: [scal("courseId"), scal("studentId")],
174
- consumed: [ev("StudentRegistered", [scal("studentId")])],
175
- produced: shapes, // StudentSubscribed({courseId, studentId}) from the schema adapter
176
- partitionHint: None,
177
- }
178
- let d = DSI.infer([studentEntity, subscribe])
179
- expect(d.crossPartitionTagKeys)->toEqual(
180
- Reventless.DcbTag.extractCrossPartitionTagKeys(DcbFixtures.subscriptionEventSchema),
181
- )
182
- expect(d.partitionBySlice->Dict.get("SubscribeStudent"))->toEqual(Some("courseId"))
183
- })
192
+ testSync(
193
+ "eventShapesOfSchema extracts *Id fields by name (tag flags ignored)",
194
+ () => {
195
+ expect(shapes->Array.length)->toBe(1)
196
+ let arm = shapes->Array.getUnsafe(0)
197
+ expect(arm.eventType)->toBe("StudentSubscribed")
198
+ let names = arm.idFields->Array.map(f => f.name)->Array.toSorted(String.compare)
199
+ expect(names)->toEqual(["courseId", "studentId"])
200
+ },
201
+ )
202
+
203
+ testSync(
204
+ "inference reproduces extractCrossPartitionTagKeys ⇒ [\"studentId\"]",
205
+ () => {
206
+ // A Student entity owns studentId; Subscribe (built from the real fixture
207
+ // schema) reads it from the foreign StudentRegistered event.
208
+ let studentEntity = slice(
209
+ "RegisterStudent",
210
+ ~produced=[ev("StudentRegistered", [scal("studentId")])],
211
+ )
212
+ let subscribe: DSI.sliceShape = {
213
+ sliceName: "SubscribeStudent",
214
+ // The real command carries studentId as a SCALAR — so the foreign read is
215
+ // fanned cross-partition (a scalar reference, unlike PlaceOrder's array).
216
+ command: [scal("courseId"), scal("studentId")],
217
+ consumed: [ev("StudentRegistered", [scal("studentId")])],
218
+ produced: shapes, // StudentSubscribed({courseId, studentId}) from the schema adapter
219
+ partitionHint: None,
220
+ }
221
+ let d = DSI.infer([studentEntity, subscribe])
222
+ expect(d.crossPartitionTagKeys)->toEqual(
223
+ Reventless.DcbTag.extractCrossPartitionTagKeys(DcbFixtures.subscriptionEventSchema),
224
+ )
225
+ expect(d.partitionBySlice->Dict.get("SubscribeStudent"))->toEqual(Some("courseId"))
226
+ },
227
+ )
184
228
  })
185
229
 
186
230
  describe("validateScopeVsInference (annotation ⇄ inference)", () => {
187
231
  let d = DSI.infer(catalog)
188
232
 
189
- testSync("flags @crossPartition on a slice's OWN partition as a contradiction", () => {
190
- // AddCategory's partition is categoryId; marking it @crossPartition is wrong.
191
- let issues = Reventless.DcbValidation.validateScopeVsInference(
192
- ~annotations=[("AddCategory", ["categoryId"])],
193
- ~inferred=d,
194
- )
195
- expect(issues.contradictions->Array.length)->toBe(1)
196
- expect((issues.contradictions->Array.getUnsafe(0)).sliceName)->toBe("AddCategory")
197
- expect(issues.redundancies)->toEqual([])
198
- })
199
-
200
- testSync("flags @crossPartition that inference already derives as redundant", () => {
201
- // AddProduct really does read categoryId cross-partition — the annotation is
202
- // just no longer needed.
203
- let issues = Reventless.DcbValidation.validateScopeVsInference(
204
- ~annotations=[("AddProduct", ["categoryId"])],
205
- ~inferred=d,
206
- )
207
- expect(issues.contradictions)->toEqual([])
208
- expect(issues.redundancies->Array.length)->toBe(1)
209
- })
210
-
211
- testSync("no annotations ⇒ no issues (the migrated catalog)", () => {
212
- let issues = Reventless.DcbValidation.validateScopeVsInference(
213
- ~annotations=catalog->Array.map(s => (s.sliceName, [])),
214
- ~inferred=d,
215
- )
216
- expect(issues.contradictions)->toEqual([])
217
- expect(issues.redundancies)->toEqual([])
218
- })
233
+ testSync(
234
+ "flags @crossPartition on a slice's OWN partition as a contradiction",
235
+ () => {
236
+ // AddCategory's partition is categoryId; marking it @crossPartition is wrong.
237
+ let issues = Reventless.DcbValidation.validateScopeVsInference(
238
+ ~annotations=[("AddCategory", ["categoryId"])],
239
+ ~inferred=d,
240
+ )
241
+ expect(issues.contradictions->Array.length)->toBe(1)
242
+ expect((issues.contradictions->Array.getUnsafe(0)).sliceName)->toBe("AddCategory")
243
+ expect(issues.redundancies)->toEqual([])
244
+ },
245
+ )
246
+
247
+ testSync(
248
+ "flags @crossPartition that inference already derives as redundant",
249
+ () => {
250
+ // AddProduct really does read categoryId cross-partition — the annotation is
251
+ // just no longer needed.
252
+ let issues = Reventless.DcbValidation.validateScopeVsInference(
253
+ ~annotations=[("AddProduct", ["categoryId"])],
254
+ ~inferred=d,
255
+ )
256
+ expect(issues.contradictions)->toEqual([])
257
+ expect(issues.redundancies->Array.length)->toBe(1)
258
+ },
259
+ )
260
+
261
+ testSync(
262
+ "no annotations ⇒ no issues (the migrated catalog)",
263
+ () => {
264
+ let issues = Reventless.DcbValidation.validateScopeVsInference(
265
+ ~annotations=catalog->Array.map(s => (s.sliceName, [])),
266
+ ~inferred=d,
267
+ )
268
+ expect(issues.contradictions)->toEqual([])
269
+ expect(issues.redundancies)->toEqual([])
270
+ },
271
+ )
219
272
  })
220
273
 
221
274
  describe("ambiguity surfacing", () => {
@@ -229,14 +282,20 @@ describe("DcbScopeInference:", () => {
229
282
  )
230
283
  let d = DSI.infer([aOwner, bOwner, join])
231
284
 
232
- testSync("a pure-join slice with no own key is reported ambiguous", () => {
233
- let names = d.ambiguities->Array.map(((n, _)) => n)
234
- expect(names->Array.includes("Join"))->toBe(true)
235
- })
236
- testSync("the well-formed owners are not ambiguous", () => {
237
- let names = d.ambiguities->Array.map(((n, _)) => n)
238
- expect(names->Array.includes("A"))->toBe(false)
239
- expect(names->Array.includes("B"))->toBe(false)
240
- })
285
+ testSync(
286
+ "a pure-join slice with no own key is reported ambiguous",
287
+ () => {
288
+ let names = d.ambiguities->Array.map(((n, _)) => n)
289
+ expect(names->Array.includes("Join"))->toBe(true)
290
+ },
291
+ )
292
+ testSync(
293
+ "the well-formed owners are not ambiguous",
294
+ () => {
295
+ let names = d.ambiguities->Array.map(((n, _)) => n)
296
+ expect(names->Array.includes("A"))->toBe(false)
297
+ expect(names->Array.includes("B"))->toBe(false)
298
+ },
299
+ )
241
300
  })
242
301
  })