@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
@@ -27,7 +27,14 @@ module PsPlaceOrderSlice: ReventlessInfra.StateChangeSlice.T = {
27
27
  }
28
28
  let isAsync = false
29
29
  type component = scsComponent
30
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
30
+ let make = (
31
+ ~dcbEventLog as _,
32
+ ~publishJsons as _,
33
+ ~tagKeysByEventType as _=?,
34
+ ~crossPartitionTagKeys as _=?,
35
+ ~runtime as _=?,
36
+ ~opts as _=?,
37
+ ): component => Obj.magic(0)
31
38
  }
32
39
  module PsShipOrderSlice: ReventlessInfra.StateChangeSlice.T = {
33
40
  module Spec = PsShipOrder
@@ -40,7 +47,14 @@ module PsShipOrderSlice: ReventlessInfra.StateChangeSlice.T = {
40
47
  }
41
48
  let isAsync = false
42
49
  type component = scsComponent
43
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
50
+ let make = (
51
+ ~dcbEventLog as _,
52
+ ~publishJsons as _,
53
+ ~tagKeysByEventType as _=?,
54
+ ~crossPartitionTagKeys as _=?,
55
+ ~runtime as _=?,
56
+ ~opts as _=?,
57
+ ): component => Obj.magic(0)
44
58
  }
45
59
  module PsOrdersViewSlice: ReventlessInfra.StateViewSlice.T = {
46
60
  module Spec = PsOrdersView
@@ -99,7 +113,14 @@ module PsAttachInvoiceSlice: ReventlessInfra.StateChangeSlice.T = {
99
113
  }
100
114
  let isAsync = false
101
115
  type component = scsComponent
102
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
116
+ let make = (
117
+ ~dcbEventLog as _,
118
+ ~publishJsons as _,
119
+ ~tagKeysByEventType as _=?,
120
+ ~crossPartitionTagKeys as _=?,
121
+ ~runtime as _=?,
122
+ ~opts as _=?,
123
+ ): component => Obj.magic(0)
103
124
  }
104
125
 
105
126
  module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -113,7 +134,14 @@ module PsUploadAvatarSlice: ReventlessInfra.StateChangeSlice.T = {
113
134
  }
114
135
  let isAsync = false
115
136
  type component = scsComponent
116
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
137
+ let make = (
138
+ ~dcbEventLog as _,
139
+ ~publishJsons as _,
140
+ ~tagKeysByEventType as _=?,
141
+ ~crossPartitionTagKeys as _=?,
142
+ ~runtime as _=?,
143
+ ~opts as _=?,
144
+ ): component => Obj.magic(0)
117
145
  }
118
146
 
119
147
  module PsUploadImagesSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -127,7 +155,14 @@ module PsUploadImagesSlice: ReventlessInfra.StateChangeSlice.T = {
127
155
  }
128
156
  let isAsync = false
129
157
  type component = scsComponent
130
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
158
+ let make = (
159
+ ~dcbEventLog as _,
160
+ ~publishJsons as _,
161
+ ~tagKeysByEventType as _=?,
162
+ ~crossPartitionTagKeys as _=?,
163
+ ~runtime as _=?,
164
+ ~opts as _=?,
165
+ ): component => Obj.magic(0)
131
166
  }
132
167
 
133
168
  module PsTypoStoreSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -141,7 +176,14 @@ module PsTypoStoreSlice: ReventlessInfra.StateChangeSlice.T = {
141
176
  }
142
177
  let isAsync = false
143
178
  type component = scsComponent
144
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
179
+ let make = (
180
+ ~dcbEventLog as _,
181
+ ~publishJsons as _,
182
+ ~tagKeysByEventType as _=?,
183
+ ~crossPartitionTagKeys as _=?,
184
+ ~runtime as _=?,
185
+ ~opts as _=?,
186
+ ): component => Obj.magic(0)
145
187
  }
146
188
 
147
189
  module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -155,7 +197,14 @@ module PsChangePhotoSlice: ReventlessInfra.StateChangeSlice.T = {
155
197
  }
156
198
  let isAsync = false
157
199
  type component = scsComponent
158
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
200
+ let make = (
201
+ ~dcbEventLog as _,
202
+ ~publishJsons as _,
203
+ ~tagKeysByEventType as _=?,
204
+ ~crossPartitionTagKeys as _=?,
205
+ ~runtime as _=?,
206
+ ~opts as _=?,
207
+ ): component => Obj.magic(0)
159
208
  }
160
209
 
161
210
  module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -169,7 +218,14 @@ module PsReserveStockSlice: ReventlessInfra.StateChangeSlice.T = {
169
218
  }
170
219
  let isAsync = false
171
220
  type component = scsComponent
172
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
221
+ let make = (
222
+ ~dcbEventLog as _,
223
+ ~publishJsons as _,
224
+ ~tagKeysByEventType as _=?,
225
+ ~crossPartitionTagKeys as _=?,
226
+ ~runtime as _=?,
227
+ ~opts as _=?,
228
+ ): component => Obj.magic(0)
173
229
  }
174
230
 
175
231
  module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
@@ -183,7 +239,14 @@ module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
183
239
  }
184
240
  let isAsync = false
185
241
  type component = scsComponent
186
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
242
+ let make = (
243
+ ~dcbEventLog as _,
244
+ ~publishJsons as _,
245
+ ~tagKeysByEventType as _=?,
246
+ ~crossPartitionTagKeys as _=?,
247
+ ~runtime as _=?,
248
+ ~opts as _=?,
249
+ ): component => Obj.magic(0)
187
250
  }
188
251
  module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
189
252
  module Spec = PsTransitionSwitch
@@ -196,7 +259,14 @@ module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
196
259
  }
197
260
  let isAsync = false
198
261
  type component = scsComponent
199
- let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
262
+ let make = (
263
+ ~dcbEventLog as _,
264
+ ~publishJsons as _,
265
+ ~tagKeysByEventType as _=?,
266
+ ~crossPartitionTagKeys as _=?,
267
+ ~runtime as _=?,
268
+ ~opts as _=?,
269
+ ): component => Obj.magic(0)
200
270
  }
201
271
  module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
202
272
  module Spec = PsShipmentsView
@@ -241,7 +311,7 @@ describe("lifecycle topology", () => {
241
311
  references: [],
242
312
  allowedStates,
243
313
  targetState,
244
- allowedStatesSource: ?allowedStates->Option.map(_ => "declared"),
314
+ allowedStatesSource: ?(allowedStates->Option.map(_ => "declared")),
245
315
  apiExposed: None,
246
316
  requiredAccess: None,
247
317
  ownerField: None,
@@ -251,7 +321,9 @@ describe("lifecycle topology", () => {
251
321
  // `Cancelled` is in the enum and nothing declares an edge into it.
252
322
  let findings = Plugin_Structure.lifecycleTopologyFindings(
253
323
  ~writables=[
254
- writableWith([command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ())]),
324
+ writableWith([
325
+ command(~name="Ship", ~allowedStates=["Placed"], ~targetState="Shipped", ()),
326
+ ]),
255
327
  ],
256
328
  ~lifecycleStatesByView=ordersStates,
257
329
  )
@@ -360,7 +432,7 @@ describe("the harvested model against the switch", () => {
360
432
  ): Reventless.Plugin.derivedEdge => {
361
433
  component: "TransitionSwitch",
362
434
  command,
363
- level: ?level,
435
+ ?level,
364
436
  allowedStates,
365
437
  targets,
366
438
  }
@@ -406,7 +478,10 @@ describe("the harvested model against the switch", () => {
406
478
  // cannot express. The harvest reports that as a contradiction; here the
407
479
  // choice is between the declaration and picking one of the two arbitrarily.
408
480
  expect(
409
- edgeOf([edge(~command="Book", ~allowedStates=["Booked"], ~targets=["Booked", "Abandoned"])], "Book"),
481
+ edgeOf(
482
+ [edge(~command="Book", ~allowedStates=["Booked"], ~targets=["Booked", "Abandoned"])],
483
+ "Book",
484
+ ),
410
485
  )->toEqual(Some((Some(["Booked"]), Some("Booked"), Some("derived"))))
411
486
  })
412
487
 
@@ -560,231 +635,334 @@ describe("declared-edge cross-check", () => {
560
635
 
561
636
  describe("Plugin_Structure.make — Phase 2 graph fields", () => {
562
637
  describe("stateChangeSlices", () => {
563
- testSync("produces two SCS entries in declaration order", () => {
564
- expect(structure.stateChangeSlices->Array.length)->toBe(2)
565
- })
566
-
567
- testSync("PlaceOrder: producedEventTypes contains OrderPlaced (qualified)", () => {
568
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
569
- expect(placeOrder.producedEventTypes)->toEqual(["TestPlugin.OrderPlaced"])
570
- })
571
-
572
- testSync("PlaceOrder: consumedEventTypes contains CatalogProductSynced qualified (payload-less variants excluded)", () => {
573
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
574
- expect(placeOrder.consumedEventTypes)->toEqual(["TestPlugin.CatalogProductSynced"])
575
- })
576
-
577
- testSync("PlaceOrder: linkedViews contains Orders (consumes its OrderPlaced)", () => {
578
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
579
- expect(placeOrder.linkedViews)->toEqual(["Orders"])
580
- })
581
-
582
- testSync("PlaceOrder: consistencyRead is AvailableProducts (only SVS consuming CatalogProductSynced)", () => {
583
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
584
- expect(placeOrder.consistencyRead)->toEqual(Some("AvailableProducts"))
585
- })
586
-
587
- testSync("PlaceOrder command: level Collection (creation command), aggregateIdField orderId for UUID injection", () => {
588
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
589
- let cmd = placeOrder.commands->Array.getUnsafe(0)
590
- expect((cmd.level, cmd.aggregateIdField))->toEqual((
591
- Reventless.Plugin.Collection,
592
- Some("orderId"),
593
- ))
594
- })
595
-
596
- testSync("ShipOrder: producedEventTypes contains OrderShipped (qualified)", () => {
597
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
598
- expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
599
- })
600
-
601
- testSync("ShipOrder: consumedEventTypes is empty (all consumed events are payload-less literals)", () => {
602
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
603
- expect(shipOrder.consumedEventTypes)->toEqual([])
604
- })
605
-
606
- testSync("ShipOrder: linkedViews contains Orders (consumes its OrderShipped)", () => {
607
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
608
- expect(shipOrder.linkedViews)->toEqual(["Orders"])
609
- })
610
-
611
- testSync("ShipOrder: consistencyRead is None (empty consumedEventTypes cannot overlap any SVS)", () => {
612
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
613
- expect(shipOrder.consistencyRead)->toEqual(None)
614
- })
615
-
616
- testSync("ShipOrder command: level Instance, aggregateIdField orderId", () => {
617
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
618
- let cmd = shipOrder.commands->Array.getUnsafe(0)
619
- expect((cmd.level, cmd.aggregateIdField))->toEqual((
620
- Reventless.Plugin.Instance,
621
- Some("orderId"),
622
- ))
623
- })
624
-
625
- testSync("ShipOrder: payload-less command CancelShipment is surfaced in commands", () => {
626
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
627
- expect(shipOrder.commands->Array.map(c => c.name))->toEqual(["ShipOrder", "CancelShipment"])
628
- })
629
-
630
- testSync("ShipOrder: the @noApi CancelShipment is tagged apiExposed=false; ShipOrder stays exposed", () => {
631
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
632
- let byName = name => shipOrder.commands->Array.find(c => c.name == name)
633
- expect((
634
- byName("ShipOrder")->Option.flatMap(c => c.apiExposed),
635
- byName("CancelShipment")->Option.flatMap(c => c.apiExposed),
636
- ))->toEqual((Some(true), Some(false)))
637
- })
638
-
639
- testSync("ShipOrder: the switch's target flows through to commandDef.targetState", () => {
640
- // End-to-end: the spec's `commandTransition`, evaluated per constructor
641
- // against a synthetic command → commandDef. `CancelShipment` answers
642
- // `Unrestricted` and carries None (resolver falls back to name-stem).
643
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
644
- let byName = name => shipOrder.commands->Array.find(c => c.name == name)
645
- expect((
646
- byName("ShipOrder")->Option.flatMap(c => c.targetState),
647
- byName("CancelShipment")->Option.flatMap(c => c.targetState),
648
- ))->toEqual((Some("Shipped"), None))
649
- })
650
-
651
- testSync("ShipOrder: the switch's from-set flows through to commandDef.allowedStates", () => {
652
- // The other half of the same declaration. An edge is one value, so a
653
- // command that declares a target cannot end up without the states it may
654
- // run from — the two fields are read off it together.
655
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
656
- let byName = name => shipOrder.commands->Array.find(c => c.name == name)
657
- expect((
658
- byName("ShipOrder")->Option.flatMap(c => c.allowedStates),
659
- byName("CancelShipment")->Option.flatMap(c => c.allowedStates),
660
- ))->toEqual((Some(["Placed"]), None))
661
- })
662
-
663
- testSync("ShipOrder: the @noApi variant carries no callable mutation field (no sibling leak)", () => {
664
- // Regression: for a single-exposed-command slice, `mutationFieldFor`
665
- // resolves every variant — including the @noApi one — to the slice's one
666
- // mutation field. The non-exposed variant must not carry that sibling
667
- // field; it emits an empty sentinel instead, while the exposed command
668
- // keeps a real, non-empty field.
669
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
670
- let byName = name => shipOrder.commands->Array.find(c => c.name == name)
671
- let shipField = byName("ShipOrder")->Option.map(c => c.mutationField)->Option.getOr("")
672
- let cancelField = byName("CancelShipment")->Option.map(c => c.mutationField)->Option.getOr("x")
673
- expect((shipField->String.length > 0, cancelField))->toEqual((true, ""))
674
- })
638
+ testSync(
639
+ "produces two SCS entries in declaration order",
640
+ () => {
641
+ expect(structure.stateChangeSlices->Array.length)->toBe(2)
642
+ },
643
+ )
644
+
645
+ testSync(
646
+ "PlaceOrder: producedEventTypes contains OrderPlaced (qualified)",
647
+ () => {
648
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
649
+ expect(placeOrder.producedEventTypes)->toEqual(["TestPlugin.OrderPlaced"])
650
+ },
651
+ )
652
+
653
+ testSync(
654
+ "PlaceOrder: consumedEventTypes contains CatalogProductSynced qualified (payload-less variants excluded)",
655
+ () => {
656
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
657
+ expect(placeOrder.consumedEventTypes)->toEqual(["TestPlugin.CatalogProductSynced"])
658
+ },
659
+ )
660
+
661
+ testSync(
662
+ "PlaceOrder: linkedViews contains Orders (consumes its OrderPlaced)",
663
+ () => {
664
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
665
+ expect(placeOrder.linkedViews)->toEqual(["Orders"])
666
+ },
667
+ )
668
+
669
+ testSync(
670
+ "PlaceOrder: consistencyRead is AvailableProducts (only SVS consuming CatalogProductSynced)",
671
+ () => {
672
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
673
+ expect(placeOrder.consistencyRead)->toEqual(Some("AvailableProducts"))
674
+ },
675
+ )
676
+
677
+ testSync(
678
+ "PlaceOrder command: level Collection (creation command), aggregateIdField orderId for UUID injection",
679
+ () => {
680
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
681
+ let cmd = placeOrder.commands->Array.getUnsafe(0)
682
+ expect((cmd.level, cmd.aggregateIdField))->toEqual((
683
+ Reventless.Plugin.Collection,
684
+ Some("orderId"),
685
+ ))
686
+ },
687
+ )
688
+
689
+ testSync(
690
+ "ShipOrder: producedEventTypes contains OrderShipped (qualified)",
691
+ () => {
692
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
693
+ expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
694
+ },
695
+ )
696
+
697
+ testSync(
698
+ "ShipOrder: consumedEventTypes is empty (all consumed events are payload-less literals)",
699
+ () => {
700
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
701
+ expect(shipOrder.consumedEventTypes)->toEqual([])
702
+ },
703
+ )
704
+
705
+ testSync(
706
+ "ShipOrder: linkedViews contains Orders (consumes its OrderShipped)",
707
+ () => {
708
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
709
+ expect(shipOrder.linkedViews)->toEqual(["Orders"])
710
+ },
711
+ )
712
+
713
+ testSync(
714
+ "ShipOrder: consistencyRead is None (empty consumedEventTypes cannot overlap any SVS)",
715
+ () => {
716
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
717
+ expect(shipOrder.consistencyRead)->toEqual(None)
718
+ },
719
+ )
720
+
721
+ testSync(
722
+ "ShipOrder command: level Instance, aggregateIdField orderId",
723
+ () => {
724
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
725
+ let cmd = shipOrder.commands->Array.getUnsafe(0)
726
+ expect((cmd.level, cmd.aggregateIdField))->toEqual((
727
+ Reventless.Plugin.Instance,
728
+ Some("orderId"),
729
+ ))
730
+ },
731
+ )
732
+
733
+ testSync(
734
+ "ShipOrder: payload-less command CancelShipment is surfaced in commands",
735
+ () => {
736
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
737
+ expect(shipOrder.commands->Array.map(c => c.name))->toEqual(["ShipOrder", "CancelShipment"])
738
+ },
739
+ )
740
+
741
+ testSync(
742
+ "ShipOrder: the @noApi CancelShipment is tagged apiExposed=false; ShipOrder stays exposed",
743
+ () => {
744
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
745
+ let byName = name => shipOrder.commands->Array.find(c => c.name == name)
746
+ expect((
747
+ byName("ShipOrder")->Option.flatMap(c => c.apiExposed),
748
+ byName("CancelShipment")->Option.flatMap(c => c.apiExposed),
749
+ ))->toEqual((Some(true), Some(false)))
750
+ },
751
+ )
752
+
753
+ testSync(
754
+ "ShipOrder: the switch's target flows through to commandDef.targetState",
755
+ () => {
756
+ // End-to-end: the spec's `commandTransition`, evaluated per constructor
757
+ // against a synthetic command → commandDef. `CancelShipment` answers
758
+ // `Unrestricted` and carries None (resolver falls back to name-stem).
759
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
760
+ let byName = name => shipOrder.commands->Array.find(c => c.name == name)
761
+ expect((
762
+ byName("ShipOrder")->Option.flatMap(c => c.targetState),
763
+ byName("CancelShipment")->Option.flatMap(c => c.targetState),
764
+ ))->toEqual((Some("Shipped"), None))
765
+ },
766
+ )
767
+
768
+ testSync(
769
+ "ShipOrder: the switch's from-set flows through to commandDef.allowedStates",
770
+ () => {
771
+ // The other half of the same declaration. An edge is one value, so a
772
+ // command that declares a target cannot end up without the states it may
773
+ // run from — the two fields are read off it together.
774
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
775
+ let byName = name => shipOrder.commands->Array.find(c => c.name == name)
776
+ expect((
777
+ byName("ShipOrder")->Option.flatMap(c => c.allowedStates),
778
+ byName("CancelShipment")->Option.flatMap(c => c.allowedStates),
779
+ ))->toEqual((Some(["Placed"]), None))
780
+ },
781
+ )
782
+
783
+ testSync(
784
+ "ShipOrder: the @noApi variant carries no callable mutation field (no sibling leak)",
785
+ () => {
786
+ // Regression: for a single-exposed-command slice, `mutationFieldFor`
787
+ // resolves every variant — including the @noApi one — to the slice's one
788
+ // mutation field. The non-exposed variant must not carry that sibling
789
+ // field; it emits an empty sentinel instead, while the exposed command
790
+ // keeps a real, non-empty field.
791
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
792
+ let byName = name => shipOrder.commands->Array.find(c => c.name == name)
793
+ let shipField = byName("ShipOrder")->Option.map(c => c.mutationField)->Option.getOr("")
794
+ let cancelField =
795
+ byName("CancelShipment")->Option.map(c => c.mutationField)->Option.getOr("x")
796
+ expect((shipField->String.length > 0, cancelField))->toEqual((true, ""))
797
+ },
798
+ )
675
799
 
676
800
  // A consumer that builds its own mutation document declares one variable
677
801
  // per argument, and JSON Schema does not carry the GraphQL type those
678
802
  // variables need — `orderId` reads as a plain string there while the server
679
803
  // declares `ID!`. So the rendered type rides along on the property.
680
- testSync("ShipOrder: the command schema publishes each argument's GraphQL type", () => {
681
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
682
- let ship = shipOrder.commands->Array.find(c => c.name == "ShipOrder")->Option.getOrThrow
683
- expect(ship.schema->String.includes(`"x-reventless-graphql-type":"ID!"`))->toBe(true)
684
- })
804
+ testSync(
805
+ "ShipOrder: the command schema publishes each argument's GraphQL type",
806
+ () => {
807
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
808
+ let ship = shipOrder.commands->Array.find(c => c.name == "ShipOrder")->Option.getOrThrow
809
+ expect(ship.schema->String.includes(`"x-reventless-graphql-type":"ID!"`))->toBe(true)
810
+ },
811
+ )
685
812
 
686
813
  // The names are composed from the mutation field, and a `@noApi` variant
687
814
  // has none — publishing a type derived from the empty sentinel would name
688
815
  // types no schema declares.
689
- testSync("CancelShipment: a @noApi variant publishes no argument types", () => {
690
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
691
- let cancel =
692
- shipOrder.commands->Array.find(c => c.name == "CancelShipment")->Option.getOrThrow
693
- expect(cancel.schema->String.includes("x-reventless-graphql-type"))->toBe(false)
694
- })
695
-
696
- testSync("ShipOrder: payload-less event ShipmentVoided is surfaced in events", () => {
697
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
698
- expect(shipOrder.events->Array.map(e => e.name))->toEqual(["OrderShipped", "ShipmentVoided"])
699
- })
700
-
701
- testSync("ShipOrder: producedEventTypes still excludes the payload-less ShipmentVoided (DCB filter intact)", () => {
702
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
703
- expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
704
- })
816
+ testSync(
817
+ "CancelShipment: a @noApi variant publishes no argument types",
818
+ () => {
819
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
820
+ let cancel =
821
+ shipOrder.commands->Array.find(c => c.name == "CancelShipment")->Option.getOrThrow
822
+ expect(cancel.schema->String.includes("x-reventless-graphql-type"))->toBe(false)
823
+ },
824
+ )
825
+
826
+ testSync(
827
+ "ShipOrder: payload-less event ShipmentVoided is surfaced in events",
828
+ () => {
829
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
830
+ expect(shipOrder.events->Array.map(e => e.name))->toEqual([
831
+ "OrderShipped",
832
+ "ShipmentVoided",
833
+ ])
834
+ },
835
+ )
836
+
837
+ testSync(
838
+ "ShipOrder: producedEventTypes still excludes the payload-less ShipmentVoided (DCB filter intact)",
839
+ () => {
840
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
841
+ expect(shipOrder.producedEventTypes)->toEqual(["TestPlugin.OrderShipped"])
842
+ },
843
+ )
705
844
 
706
845
  // A slice's declared errors are the third family of its contract, beside the
707
846
  // commands it accepts and the events it emits — what a caller has to handle
708
847
  // when a decision is refused.
709
- testSync("PlaceOrder: the single payload-less error variant is surfaced", () => {
710
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
711
- expect(placeOrder.errors->Array.map(e => e.name))->toEqual(["AlreadyPlaced"])
712
- })
713
-
714
- testSync("ShipOrder: both error variants surface, in declaration order", () => {
715
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
716
- expect(shipOrder.errors->Array.map(e => e.name))->toEqual(["OrderNotFound", "NotShippable"])
717
- })
718
-
719
- testSync("ShipOrder: a payload-carrying error carries its field schema", () => {
720
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
721
- let notShippable =
722
- shipOrder.errors->Array.find(e => e.name == "NotShippable")->Option.getOrThrow
723
- expect(notShippable.schema->String.includes("\"reason\""))->toBe(true)
724
- })
725
-
726
- testSync("ShipOrder: a payload-less error carries no fields and no references", () => {
727
- let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
728
- let notFound = shipOrder.errors->Array.find(e => e.name == "OrderNotFound")->Option.getOrThrow
729
- expect((notFound.schema->String.includes("\"reason\""), notFound.references->Array.length))
730
- ->toEqual((false, 0))
731
- })
848
+ testSync(
849
+ "PlaceOrder: the single payload-less error variant is surfaced",
850
+ () => {
851
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
852
+ expect(placeOrder.errors->Array.map(e => e.name))->toEqual(["AlreadyPlaced"])
853
+ },
854
+ )
855
+
856
+ testSync(
857
+ "ShipOrder: both error variants surface, in declaration order",
858
+ () => {
859
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
860
+ expect(shipOrder.errors->Array.map(e => e.name))->toEqual(["OrderNotFound", "NotShippable"])
861
+ },
862
+ )
863
+
864
+ testSync(
865
+ "ShipOrder: a payload-carrying error carries its field schema",
866
+ () => {
867
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
868
+ let notShippable =
869
+ shipOrder.errors->Array.find(e => e.name == "NotShippable")->Option.getOrThrow
870
+ expect(notShippable.schema->String.includes("\"reason\""))->toBe(true)
871
+ },
872
+ )
873
+
874
+ testSync(
875
+ "ShipOrder: a payload-less error carries no fields and no references",
876
+ () => {
877
+ let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
878
+ let notFound =
879
+ shipOrder.errors->Array.find(e => e.name == "OrderNotFound")->Option.getOrThrow
880
+ expect((
881
+ notFound.schema->String.includes("\"reason\""),
882
+ notFound.references->Array.length,
883
+ ))->toEqual((false, 0))
884
+ },
885
+ )
732
886
  })
733
887
 
734
888
  describe("stateViewSlices", () => {
735
- testSync("produces four SVS entries in declaration order", () => {
736
- expect(structure.stateViewSlices->Array.length)->toBe(4)
737
- })
738
-
739
- testSync("OrdersView: consumedEventTypes contains the three order events (qualified)", () => {
740
- let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
741
- expect(ordersView.consumedEventTypes)->toEqual([
742
- "TestPlugin.OrderPlaced",
743
- "TestPlugin.OrderShipped",
744
- "TestPlugin.OrderCancelled",
745
- ])
746
- })
747
-
748
- testSync("OrdersView: linkedWriteSide contains both SCS producing order events", () => {
749
- let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
750
- expect(ordersView.linkedWriteSide)->toEqual(["PlaceOrder", "ShipOrder"])
751
- })
752
-
753
- testSync("AvailableProductsView: consumedEventTypes contains catalog events (qualified)", () => {
754
- let apv = structure.stateViewSlices->Array.getUnsafe(1)
755
- expect(apv.consumedEventTypes)->toEqual([
756
- "TestPlugin.CatalogProductSynced",
757
- "TestPlugin.CatalogProductPriceChanged",
758
- ])
759
- })
760
-
761
- testSync("AvailableProductsView: linkedWriteSide is empty (no SCS produces these events)", () => {
762
- let apv = structure.stateViewSlices->Array.getUnsafe(1)
763
- expect(apv.linkedWriteSide)->toEqual([])
764
- })
889
+ testSync(
890
+ "produces four SVS entries in declaration order",
891
+ () => {
892
+ expect(structure.stateViewSlices->Array.length)->toBe(4)
893
+ },
894
+ )
895
+
896
+ testSync(
897
+ "OrdersView: consumedEventTypes contains the three order events (qualified)",
898
+ () => {
899
+ let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
900
+ expect(ordersView.consumedEventTypes)->toEqual([
901
+ "TestPlugin.OrderPlaced",
902
+ "TestPlugin.OrderShipped",
903
+ "TestPlugin.OrderCancelled",
904
+ ])
905
+ },
906
+ )
907
+
908
+ testSync(
909
+ "OrdersView: linkedWriteSide contains both SCS producing order events",
910
+ () => {
911
+ let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
912
+ expect(ordersView.linkedWriteSide)->toEqual(["PlaceOrder", "ShipOrder"])
913
+ },
914
+ )
915
+
916
+ testSync(
917
+ "AvailableProductsView: consumedEventTypes contains catalog events (qualified)",
918
+ () => {
919
+ let apv = structure.stateViewSlices->Array.getUnsafe(1)
920
+ expect(apv.consumedEventTypes)->toEqual([
921
+ "TestPlugin.CatalogProductSynced",
922
+ "TestPlugin.CatalogProductPriceChanged",
923
+ ])
924
+ },
925
+ )
926
+
927
+ testSync(
928
+ "AvailableProductsView: linkedWriteSide is empty (no SCS produces these events)",
929
+ () => {
930
+ let apv = structure.stateViewSlices->Array.getUnsafe(1)
931
+ expect(apv.linkedWriteSide)->toEqual([])
932
+ },
933
+ )
765
934
  })
766
935
 
767
936
  describe("labelField / searchableFields", () => {
768
- testSync("OrdersView: no @displayName → first non-`*Id` string field wins (customerName)", () => {
769
- let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
770
- expect((ordersView.labelField, ordersView.searchableFields))->toEqual((
771
- "customerName",
772
- ["customerName"],
773
- ))
774
- })
775
-
776
- testSync("AvailableProductsView: no @displayName → first non-`*Id` string field wins (name)", () => {
777
- let apv = structure.stateViewSlices->Array.getUnsafe(1)
778
- expect((apv.labelField, apv.searchableFields))->toEqual(("name", ["name"]))
779
- })
780
-
781
- testSync("Customers: composite @displayName → labelField=displayName, searchableFields=raw source fields in declaration order", () => {
782
- let customers = structure.stateViewSlices->Array.getUnsafe(2)
783
- expect((customers.labelField, customers.searchableFields))->toEqual((
784
- "displayName",
785
- ["firstName", "lastName"],
786
- ))
787
- })
937
+ testSync(
938
+ "OrdersView: no @displayName → first non-`*Id` string field wins (customerName)",
939
+ () => {
940
+ let ordersView = structure.stateViewSlices->Array.getUnsafe(0)
941
+ expect((ordersView.labelField, ordersView.searchableFields))->toEqual((
942
+ "customerName",
943
+ ["customerName"],
944
+ ))
945
+ },
946
+ )
947
+
948
+ testSync(
949
+ "AvailableProductsView: no @displayName → first non-`*Id` string field wins (name)",
950
+ () => {
951
+ let apv = structure.stateViewSlices->Array.getUnsafe(1)
952
+ expect((apv.labelField, apv.searchableFields))->toEqual(("name", ["name"]))
953
+ },
954
+ )
955
+
956
+ testSync(
957
+ "Customers: composite @displayName → labelField=displayName, searchableFields=raw source fields in declaration order",
958
+ () => {
959
+ let customers = structure.stateViewSlices->Array.getUnsafe(2)
960
+ expect((customers.labelField, customers.searchableFields))->toEqual((
961
+ "displayName",
962
+ ["firstName", "lastName"],
963
+ ))
964
+ },
965
+ )
788
966
  })
789
967
 
790
968
  // What a field *is* comes from `SchemaType`, the IR every other schema
@@ -798,149 +976,204 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
798
976
  (r.field, r.searchableFields)
799
977
  }
800
978
 
801
- testSync("a DateTime is not a name — a state whose only string is one falls to id", () => {
802
- // The shipped `Orders` case: `placedAt` was added so date views have a
803
- // field to key off, and a positional rule let it rename the entity.
804
- let schema = S.schema(s =>
805
- {
806
- "orderId": s.matches(Reventless.Reference.to_("Order")),
807
- "placedAt": s.matches(Reventless.DateTime.schema),
808
- "shippedAt": s.matches(Reventless.DateTime.schema),
809
- }
810
- )
811
- expect(labelOf(schema))->toEqual(("id", []))
812
- })
979
+ testSync(
980
+ "a DateTime is not a name — a state whose only string is one falls to id",
981
+ () => {
982
+ // The shipped `Orders` case: `placedAt` was added so date views have a
983
+ // field to key off, and a positional rule let it rename the entity.
984
+ let schema = S.schema(
985
+ s =>
986
+ {
987
+ "orderId": s.matches(Reventless.Reference.to_("Order")),
988
+ "placedAt": s.matches(Reventless.DateTime.schema),
989
+ "shippedAt": s.matches(Reventless.DateTime.schema),
990
+ },
991
+ )
992
+ expect(labelOf(schema))->toEqual(("id", []))
993
+ },
994
+ )
813
995
 
814
- testSync("a DateTime is skipped, not fatal — a later plain string still wins", () => {
815
- let schema = S.schema(s =>
816
- {
817
- "placedAt": s.matches(Reventless.DateTime.schema),
818
- "reference": s.matches(S.string),
819
- }
820
- )
821
- expect(labelOf(schema))->toEqual(("reference", ["reference"]))
822
- })
996
+ testSync(
997
+ "a DateTime is skipped, not fatal — a later plain string still wins",
998
+ () => {
999
+ let schema = S.schema(
1000
+ s =>
1001
+ {
1002
+ "placedAt": s.matches(Reventless.DateTime.schema),
1003
+ "reference": s.matches(S.string),
1004
+ },
1005
+ )
1006
+ expect(labelOf(schema))->toEqual(("reference", ["reference"]))
1007
+ },
1008
+ )
823
1009
 
824
- testSync("an optional string is still the entity's name", () => {
825
- let schema = S.schema(s =>
826
- {
827
- "nickname": s.matches(S.option(S.string)),
828
- }
829
- )
830
- expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
831
- })
1010
+ testSync(
1011
+ "an optional string is still the entity's name",
1012
+ () => {
1013
+ let schema = S.schema(
1014
+ s =>
1015
+ {
1016
+ "nickname": s.matches(S.option(S.string)),
1017
+ },
1018
+ )
1019
+ expect(labelOf(schema))->toEqual(("nickname", ["nickname"]))
1020
+ },
1021
+ )
832
1022
 
833
- testSync("a reference is not a name even when its field is not called `*Id`", () => {
834
- let schema = S.schema(s =>
835
- {
836
- "customer": s.matches(Reventless.Reference.to_("Customer")),
837
- "note": s.matches(S.string),
838
- }
839
- )
840
- expect(labelOf(schema))->toEqual(("note", ["note"]))
841
- })
1023
+ testSync(
1024
+ "a reference is not a name even when its field is not called `*Id`",
1025
+ () => {
1026
+ let schema = S.schema(
1027
+ s =>
1028
+ {
1029
+ "customer": s.matches(Reventless.Reference.to_("Customer")),
1030
+ "note": s.matches(S.string),
1031
+ },
1032
+ )
1033
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
1034
+ },
1035
+ )
842
1036
 
843
- testSync("a DCB-tagged string is not a name", () => {
844
- let schema = S.schema(s =>
845
- {
846
- "partition": s.matches(Reventless.DcbTag.string),
847
- "note": s.matches(S.string),
848
- }
849
- )
850
- expect(labelOf(schema))->toEqual(("note", ["note"]))
851
- })
1037
+ testSync(
1038
+ "a DCB-tagged string is not a name",
1039
+ () => {
1040
+ let schema = S.schema(
1041
+ s =>
1042
+ {
1043
+ "partition": s.matches(Reventless.DcbTag.string),
1044
+ "note": s.matches(S.string),
1045
+ },
1046
+ )
1047
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
1048
+ },
1049
+ )
852
1050
 
853
- testSync("the `*Id` test is case-insensitive — `productID` is a reference", () => {
854
- let schema = S.schema(s =>
855
- {
856
- "productID": s.matches(S.string),
857
- "note": s.matches(S.string),
858
- }
859
- )
860
- expect(labelOf(schema))->toEqual(("note", ["note"]))
861
- })
1051
+ testSync(
1052
+ "the `*Id` test is case-insensitive — `productID` is a reference",
1053
+ () => {
1054
+ let schema = S.schema(
1055
+ s =>
1056
+ {
1057
+ "productID": s.matches(S.string),
1058
+ "note": s.matches(S.string),
1059
+ },
1060
+ )
1061
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
1062
+ },
1063
+ )
862
1064
 
863
- testSync("a semantic-carrying string is a value, not a name", () => {
864
- // `imageUrl` names a stored object. A bucket key read as a product's name
865
- // is what the previous rule did whenever no plain string preceded it.
866
- let schema = S.schema(s =>
867
- {
868
- "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
869
- "note": s.matches(S.string),
870
- }
871
- )
872
- expect(labelOf(schema))->toEqual(("note", ["note"]))
873
- })
1065
+ testSync(
1066
+ "a semantic-carrying string is a value, not a name",
1067
+ () => {
1068
+ // `imageUrl` names a stored object. A bucket key read as a product's name
1069
+ // is what the previous rule did whenever no plain string preceded it.
1070
+ let schema = S.schema(
1071
+ s =>
1072
+ {
1073
+ "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
1074
+ "note": s.matches(S.string),
1075
+ },
1076
+ )
1077
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
1078
+ },
1079
+ )
874
1080
 
875
- testSync("a state whose only string is a semantic falls to id", () => {
876
- let schema = S.schema(s =>
877
- {
878
- "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
879
- }
880
- )
881
- expect(labelOf(schema))->toEqual(("id", []))
882
- })
1081
+ testSync(
1082
+ "a state whose only string is a semantic falls to id",
1083
+ () => {
1084
+ let schema = S.schema(
1085
+ s =>
1086
+ {
1087
+ "imageUrl": s.matches(Reventless.StorageRef.forStore(~store="productImages")),
1088
+ },
1089
+ )
1090
+ expect(labelOf(schema))->toEqual(("id", []))
1091
+ },
1092
+ )
883
1093
 
884
- testSync("`id` itself is still excluded by name", () => {
885
- // `SchemaType` reads a name that short as an ordinary string, so the
886
- // exclusion cannot come from the IR.
887
- let schema = S.schema(s =>
888
- {
889
- "id": s.matches(S.string),
890
- "note": s.matches(S.string),
891
- }
892
- )
893
- expect(labelOf(schema))->toEqual(("note", ["note"]))
894
- })
1094
+ testSync(
1095
+ "`id` itself is still excluded by name",
1096
+ () => {
1097
+ // `SchemaType` reads a name that short as an ordinary string, so the
1098
+ // exclusion cannot come from the IR.
1099
+ let schema = S.schema(
1100
+ s =>
1101
+ {
1102
+ "id": s.matches(S.string),
1103
+ "note": s.matches(S.string),
1104
+ },
1105
+ )
1106
+ expect(labelOf(schema))->toEqual(("note", ["note"]))
1107
+ },
1108
+ )
895
1109
  })
896
1110
 
897
1111
  describe("labelField — the conventional-name rung", () => {
898
1112
  let labelOf = schema => {
899
- let r = Plugin_Structure.labelFieldsFromStateSchema(~entityName="Test", schema->S.castToUnknown)
1113
+ let r = Plugin_Structure.labelFieldsFromStateSchema(
1114
+ ~entityName="Test",
1115
+ schema->S.castToUnknown,
1116
+ )
900
1117
  (r.field, r.searchableFields)
901
1118
  }
902
1119
 
903
- testSync("a field named `name` beats an earlier string", () => {
904
- let schema = S.schema(s =>
905
- {
906
- "sku": s.matches(S.string),
907
- "name": s.matches(S.string),
908
- }
909
- )
910
- expect(labelOf(schema))->toEqual(("name", ["name"]))
911
- })
1120
+ testSync(
1121
+ "a field named `name` beats an earlier string",
1122
+ () => {
1123
+ let schema = S.schema(
1124
+ s =>
1125
+ {
1126
+ "sku": s.matches(S.string),
1127
+ "name": s.matches(S.string),
1128
+ },
1129
+ )
1130
+ expect(labelOf(schema))->toEqual(("name", ["name"]))
1131
+ },
1132
+ )
912
1133
 
913
- testSync("`title` counts too, case-insensitively", () => {
914
- let schema = S.schema(s =>
915
- {
916
- "body": s.matches(S.string),
917
- "Title": s.matches(S.string),
918
- }
919
- )
920
- expect(labelOf(schema))->toEqual(("Title", ["Title"]))
921
- })
1134
+ testSync(
1135
+ "`title` counts too, case-insensitively",
1136
+ () => {
1137
+ let schema = S.schema(
1138
+ s =>
1139
+ {
1140
+ "body": s.matches(S.string),
1141
+ "Title": s.matches(S.string),
1142
+ },
1143
+ )
1144
+ expect(labelOf(schema))->toEqual(("Title", ["Title"]))
1145
+ },
1146
+ )
922
1147
 
923
- testSync("the match is exact — `customerName` is a customer's name, not this record's", () => {
924
- // The shipped `OrdersView` fixture depends on this: it has exactly one
925
- // eligible field and must be picked by position, not by convention.
926
- let schema = S.schema(s =>
927
- {
928
- "customerName": s.matches(S.string),
929
- "note": s.matches(S.string),
930
- }
931
- )
932
- expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
933
- })
1148
+ testSync(
1149
+ "the match is exact — `customerName` is a customer's name, not this record's",
1150
+ () => {
1151
+ // The shipped `OrdersView` fixture depends on this: it has exactly one
1152
+ // eligible field and must be picked by position, not by convention.
1153
+ let schema = S.schema(
1154
+ s =>
1155
+ {
1156
+ "customerName": s.matches(S.string),
1157
+ "note": s.matches(S.string),
1158
+ },
1159
+ )
1160
+ expect(labelOf(schema))->toEqual(("customerName", ["customerName"]))
1161
+ },
1162
+ )
934
1163
 
935
- testSync("an optional `name` still outranks an earlier string", () => {
936
- let schema = S.schema(s =>
937
- {
938
- "sku": s.matches(S.string),
939
- "name": s.matches(S.option(S.string)),
940
- }
941
- )
942
- expect(labelOf(schema))->toEqual(("name", ["name"]))
943
- })
1164
+ testSync(
1165
+ "an optional `name` still outranks an earlier string",
1166
+ () => {
1167
+ let schema = S.schema(
1168
+ s =>
1169
+ {
1170
+ "sku": s.matches(S.string),
1171
+ "name": s.matches(S.option(S.string)),
1172
+ },
1173
+ )
1174
+ expect(labelOf(schema))->toEqual(("name", ["name"]))
1175
+ },
1176
+ )
944
1177
  })
945
1178
 
946
1179
  // The rung is published because the four are not equally believable: a
@@ -953,54 +1186,72 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
953
1186
  schema->S.castToUnknown,
954
1187
  ).source->Plugin_Structure.labelFieldSourceToString
955
1188
 
956
- testSync("a @displayName spec is a declaration", () => {
957
- let customers = structure.stateViewSlices->Array.getUnsafe(2)
958
- expect((customers.labelField, customers.labelFieldSource))->toEqual((
959
- "displayName",
960
- Some("annotation"),
961
- ))
962
- })
1189
+ testSync(
1190
+ "a @displayName spec is a declaration",
1191
+ () => {
1192
+ let customers = structure.stateViewSlices->Array.getUnsafe(2)
1193
+ expect((customers.labelField, customers.labelFieldSource))->toEqual((
1194
+ "displayName",
1195
+ Some("annotation"),
1196
+ ))
1197
+ },
1198
+ )
963
1199
 
964
- testSync("a field named `name` is a guess the consumer can also make", () => {
965
- let schema = S.schema(s =>
966
- {
967
- "sku": s.matches(S.string),
968
- "name": s.matches(S.string),
969
- }
970
- )
971
- expect(sourceOf(schema))->toEqual("convention")
972
- })
973
-
974
- testSync("declaration order is a guess only this side can make", () => {
975
- // `customerName` is eligible and unconventional, so it wins by position —
976
- // and a consumer told only "customerName" could not tell that from a
977
- // declaration.
978
- let schema = S.schema(s =>
979
- {
980
- "customerName": s.matches(S.string),
981
- "note": s.matches(S.string),
982
- }
983
- )
984
- expect(sourceOf(schema))->toEqual("position")
985
- })
1200
+ testSync(
1201
+ "a field named `name` is a guess the consumer can also make",
1202
+ () => {
1203
+ let schema = S.schema(
1204
+ s =>
1205
+ {
1206
+ "sku": s.matches(S.string),
1207
+ "name": s.matches(S.string),
1208
+ },
1209
+ )
1210
+ expect(sourceOf(schema))->toEqual("convention")
1211
+ },
1212
+ )
986
1213
 
987
- testSync("no candidate at all is the state saying it has no human field", () => {
988
- let schema = S.schema(s =>
989
- {
990
- "orderId": s.matches(Reventless.Reference.to_("Order")),
991
- "placedAt": s.matches(Reventless.DateTime.schema),
992
- }
993
- )
994
- expect(sourceOf(schema))->toEqual("fallback")
995
- })
1214
+ testSync(
1215
+ "declaration order is a guess only this side can make",
1216
+ () => {
1217
+ // `customerName` is eligible and unconventional, so it wins by position —
1218
+ // and a consumer told only "customerName" could not tell that from a
1219
+ // declaration.
1220
+ let schema = S.schema(
1221
+ s =>
1222
+ {
1223
+ "customerName": s.matches(S.string),
1224
+ "note": s.matches(S.string),
1225
+ },
1226
+ )
1227
+ expect(sourceOf(schema))->toEqual("position")
1228
+ },
1229
+ )
996
1230
 
997
- testSync("every built def states one", () => {
998
- let stated =
999
- Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(q =>
1000
- q.labelFieldSource->Option.isSome
1231
+ testSync(
1232
+ "no candidate at all is the state saying it has no human field",
1233
+ () => {
1234
+ let schema = S.schema(
1235
+ s =>
1236
+ {
1237
+ "orderId": s.matches(Reventless.Reference.to_("Order")),
1238
+ "placedAt": s.matches(Reventless.DateTime.schema),
1239
+ },
1001
1240
  )
1002
- expect(stated)->toEqual(true)
1003
- })
1241
+ expect(sourceOf(schema))->toEqual("fallback")
1242
+ },
1243
+ )
1244
+
1245
+ testSync(
1246
+ "every built def states one",
1247
+ () => {
1248
+ let stated =
1249
+ Array.concat(structure.readModels, structure.stateViewSlices)->Array.every(
1250
+ q => q.labelFieldSource->Option.isSome,
1251
+ )
1252
+ expect(stated)->toEqual(true)
1253
+ },
1254
+ )
1004
1255
  })
1005
1256
 
1006
1257
  // The singular is published because it is not derivable from the plural without
@@ -1016,25 +1267,33 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1016
1267
 
1017
1268
  // "strip a trailing `s`" would publish `TestPlugin_Categorie`, a field the
1018
1269
  // generated SDL does not contain.
1019
- testSync("an `-ies` plural publishes the `y` singular, not a truncation", () => {
1020
- expect((categories.queryField, categories.singleQueryField))->toEqual((
1021
- "TestPlugin_Categories",
1022
- Some("TestPlugin_Category"),
1023
- ))
1024
- })
1025
-
1026
- testSync("every built def states the name Api_Naming returned", () => {
1027
- let stated = structure.stateViewSlices->Array.every(q =>
1028
- q.singleQueryField ==
1029
- Some(
1030
- Api_Naming.queryFieldNamesForStateView(
1031
- ~plugin="TestPlugin",
1032
- ~viewName=q.name,
1033
- ).singleFieldName,
1270
+ testSync(
1271
+ "an `-ies` plural publishes the `y` singular, not a truncation",
1272
+ () => {
1273
+ expect((categories.queryField, categories.singleQueryField))->toEqual((
1274
+ "TestPlugin_Categories",
1275
+ Some("TestPlugin_Category"),
1276
+ ))
1277
+ },
1278
+ )
1279
+
1280
+ testSync(
1281
+ "every built def states the name Api_Naming returned",
1282
+ () => {
1283
+ let stated =
1284
+ structure.stateViewSlices->Array.every(
1285
+ q =>
1286
+ q.singleQueryField ==
1287
+ Some(
1288
+ Api_Naming.queryFieldNamesForStateView(
1289
+ ~plugin="TestPlugin",
1290
+ ~viewName=q.name,
1291
+ ).singleFieldName,
1292
+ ),
1034
1293
  )
1035
- )
1036
- expect(stated)->toEqual(true)
1037
- })
1294
+ expect(stated)->toEqual(true)
1295
+ },
1296
+ )
1038
1297
  })
1039
1298
 
1040
1299
  // Which field identifies a row, and whether the author said so or this side
@@ -1050,37 +1309,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1050
1309
 
1051
1310
  // Orders holds `orderId` + `customerName`; the component name matches the
1052
1311
  // former, so the rung is the one a client could also reach on its own.
1053
- testSync("a name-matching `*Id` field is a guess the consumer can also make", () =>
1054
- expect(keyOf(structure.stateViewSlices->Array.getUnsafe(0)))->toEqual((
1055
- Some("orderId"),
1056
- Some("convention"),
1057
- ))
1312
+ testSync(
1313
+ "a name-matching `*Id` field is a guess the consumer can also make",
1314
+ () =>
1315
+ expect(keyOf(structure.stateViewSlices->Array.getUnsafe(0)))->toEqual((
1316
+ Some("orderId"),
1317
+ Some("convention"),
1318
+ )),
1058
1319
  )
1059
1320
 
1060
1321
  // AvailableProducts: the name would ask for `availableProductId`, which is not
1061
1322
  // a field — `productId` wins because it is the only candidate there is.
1062
- testSync("a sole `*Id` field is a fact only this side can see", () =>
1063
- expect(keyOf(structure.stateViewSlices->Array.getUnsafe(1)))->toEqual((
1064
- Some("productId"),
1065
- Some("sole"),
1066
- ))
1323
+ testSync(
1324
+ "a sole `*Id` field is a fact only this side can see",
1325
+ () =>
1326
+ expect(keyOf(structure.stateViewSlices->Array.getUnsafe(1)))->toEqual((
1327
+ Some("productId"),
1328
+ Some("sole"),
1329
+ )),
1067
1330
  )
1068
1331
 
1069
- testSync("an `-ies` component name singularises to its key", () =>
1070
- expect(keyOf(withCategories.stateViewSlices->Array.getUnsafe(0)))->toEqual((
1071
- Some("categoryId"),
1072
- Some("convention"),
1073
- ))
1332
+ testSync(
1333
+ "an `-ies` component name singularises to its key",
1334
+ () =>
1335
+ expect(keyOf(withCategories.stateViewSlices->Array.getUnsafe(0)))->toEqual((
1336
+ Some("categoryId"),
1337
+ Some("convention"),
1338
+ )),
1074
1339
  )
1075
1340
 
1076
1341
  // AnnotatedView declares `@id itemId` beside an `@index` field `ownerId`, and
1077
1342
  // its name matches neither — inference alone would decline here, so reporting
1078
1343
  // `itemId` proves the declaration produced it.
1079
- testSync("a declared @id is used where inference would decline", () =>
1080
- expect(keyOf(structure.stateViewSlices->Array.getUnsafe(3)))->toEqual((
1081
- Some("itemId"),
1082
- Some("annotation"),
1083
- ))
1344
+ testSync(
1345
+ "a declared @id is used where inference would decline",
1346
+ () =>
1347
+ expect(keyOf(structure.stateViewSlices->Array.getUnsafe(3)))->toEqual((
1348
+ Some("itemId"),
1349
+ Some("annotation"),
1350
+ )),
1084
1351
  )
1085
1352
  })
1086
1353
 
@@ -1089,7 +1356,9 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1089
1356
  Plugin_Structure.lifecycleFieldFromStateSchema(schema->S.castToUnknown)
1090
1357
 
1091
1358
  let withLifecycle = (schema, ~field) =>
1092
- schema->S.castToUnknown->S.Metadata.set(
1359
+ schema
1360
+ ->S.castToUnknown
1361
+ ->S.Metadata.set(
1093
1362
  ~id=Reventless.StateAnnotations.stateAnnotationsId,
1094
1363
  {
1095
1364
  ids: [],
@@ -1114,63 +1383,85 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1114
1383
  },
1115
1384
  )
1116
1385
 
1117
- testSync("a `lifecycle` field holding a closed set of values is the lifecycle field", () => {
1118
- let schema = S.schema(s =>
1119
- {
1120
- "lifecycle": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
1121
- }
1122
- )
1123
- expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
1124
- })
1386
+ testSync(
1387
+ "a `lifecycle` field holding a closed set of values is the lifecycle field",
1388
+ () => {
1389
+ let schema = S.schema(
1390
+ s =>
1391
+ {
1392
+ "lifecycle": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
1393
+ },
1394
+ )
1395
+ expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
1396
+ },
1397
+ )
1125
1398
 
1126
- testSync("an optional one counts too", () => {
1127
- let schema = S.schema(s =>
1128
- {
1129
- "lifecycle": s.matches(S.option(S.union([S.literal("Placed"), S.literal("Shipped")]))),
1130
- }
1131
- )
1132
- expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
1133
- })
1399
+ testSync(
1400
+ "an optional one counts too",
1401
+ () => {
1402
+ let schema = S.schema(
1403
+ s =>
1404
+ {
1405
+ "lifecycle": s.matches(
1406
+ S.option(S.union([S.literal("Placed"), S.literal("Shipped")])),
1407
+ ),
1408
+ },
1409
+ )
1410
+ expect(lifecycleOf(schema))->toEqual(Some("lifecycle"))
1411
+ },
1412
+ )
1134
1413
 
1135
- testSync("free text named `lifecycle` is not a lifecycle", () => {
1136
- // `allowedStates` filtering needs states to compare against; a string
1137
- // field named `lifecycle` gives a command menu nothing to match.
1138
- let schema = S.schema(s => {"lifecycle": s.matches(S.string)})
1139
- expect(lifecycleOf(schema))->toEqual(None)
1140
- })
1414
+ testSync(
1415
+ "free text named `lifecycle` is not a lifecycle",
1416
+ () => {
1417
+ // `allowedStates` filtering needs states to compare against; a string
1418
+ // field named `lifecycle` gives a command menu nothing to match.
1419
+ let schema = S.schema(s => {"lifecycle": s.matches(S.string)})
1420
+ expect(lifecycleOf(schema))->toEqual(None)
1421
+ },
1422
+ )
1141
1423
 
1142
1424
  // The one behaviour the rename deliberately changes, so it is asserted rather
1143
1425
  // than assumed. `status` is a promiscuous name — geocoding progress, todo-queue
1144
1426
  // progress, translation audit outcome — and a convention keyed on it guessed
1145
1427
  // often. A record whose lifecycle really does live in a field called `status`
1146
1428
  // says so with `@lifecycle`.
1147
- testSync("an unannotated field named `status` resolves to None", () => {
1148
- let schema = S.schema(s =>
1149
- {
1150
- "status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
1151
- }
1152
- )
1153
- expect(lifecycleOf(schema))->toEqual(None)
1154
- })
1429
+ testSync(
1430
+ "an unannotated field named `status` resolves to None",
1431
+ () => {
1432
+ let schema = S.schema(
1433
+ s =>
1434
+ {
1435
+ "status": s.matches(S.union([S.literal("Placed"), S.literal("Shipped")])),
1436
+ },
1437
+ )
1438
+ expect(lifecycleOf(schema))->toEqual(None)
1439
+ },
1440
+ )
1155
1441
 
1156
- testSync("the annotation names a field the convention would never reach", () => {
1157
- let schema = S.schema(s =>
1158
- {
1159
- "locationStatus": s.matches(S.union([S.literal("Pending"), S.literal("Located")])),
1160
- }
1161
- )
1162
- expect(lifecycleOf(schema->withLifecycle(~field="locationStatus")))->toEqual(
1163
- Some("locationStatus"),
1164
- )
1165
- })
1442
+ testSync(
1443
+ "the annotation names a field the convention would never reach",
1444
+ () => {
1445
+ let schema = S.schema(
1446
+ s =>
1447
+ {
1448
+ "locationStatus": s.matches(S.union([S.literal("Pending"), S.literal("Located")])),
1449
+ },
1450
+ )
1451
+ expect(lifecycleOf(schema->withLifecycle(~field="locationStatus")))->toEqual(
1452
+ Some("locationStatus"),
1453
+ )
1454
+ },
1455
+ )
1166
1456
  })
1167
1457
 
1168
1458
  describe("retiredField — annotation and nothing else", () => {
1169
- let retiredOf = schema =>
1170
- Plugin_Structure.retiredFieldFromStateSchema(schema->S.castToUnknown)
1459
+ let retiredOf = schema => Plugin_Structure.retiredFieldFromStateSchema(schema->S.castToUnknown)
1171
1460
 
1172
1461
  let withRetired = (schema, ~field) =>
1173
- schema->S.castToUnknown->S.Metadata.set(
1462
+ schema
1463
+ ->S.castToUnknown
1464
+ ->S.Metadata.set(
1174
1465
  ~id=Reventless.StateAnnotations.stateAnnotationsId,
1175
1466
  {
1176
1467
  ids: [],
@@ -1191,84 +1482,108 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1191
1482
  groupBy: None,
1192
1483
  visibility: None,
1193
1484
  live: None,
1194
- retired: Some({field, label: "", showWhenFalse: false, values: None, namedWhenRetired: false}),
1485
+ retired: Some({
1486
+ field,
1487
+ label: "",
1488
+ showWhenFalse: false,
1489
+ values: None,
1490
+ namedWhenRetired: false,
1491
+ }),
1195
1492
  },
1196
1493
  )
1197
1494
 
1198
1495
  // The state form. The value is published beside the field so a client holding
1199
1496
  // the def holds the whole predicate — two places deriving one comparison is
1200
1497
  // how they come to disagree about which rows a caller may see.
1201
- testSync("publishes the retirement state beside the field", () => {
1202
- let withState = schema =>
1203
- schema->S.castToUnknown->S.Metadata.set(
1204
- ~id=Reventless.StateAnnotations.stateAnnotationsId,
1205
- {
1206
- ids: [],
1207
- compositeIds: [],
1208
- subIds: [],
1209
- compositeSubIds: [],
1210
- indexes: [],
1211
- hidden: [],
1212
- summary: [],
1213
- drillTargets: [],
1214
- drillTargetKeys: [],
1215
- collapsed: [],
1216
- scan: [],
1217
- scanSort: [],
1218
- semantic: [],
1219
- metric: [],
1220
- lifecycle: Some("accountStatus"),
1221
- groupBy: None,
1222
- visibility: None,
1223
- live: None,
1224
- retired: Some({
1225
- field: "accountStatus",
1226
- label: "",
1227
- showWhenFalse: false,
1228
- values: Some(["Deactivated", "Closed"]),
1229
- namedWhenRetired: false,
1230
- }),
1231
- },
1232
- )
1233
- let schema = S.schema(s => {"accountStatus": s.matches(S.string)})->withState
1234
- expect((
1235
- Plugin_Structure.retiredFieldFromStateSchema(schema),
1236
- Plugin_Structure.retiredValuesFromStateSchema(schema),
1237
- ))->toEqual((Some("accountStatus"), Some(["Deactivated", "Closed"])))
1238
- })
1498
+ testSync(
1499
+ "publishes the retirement state beside the field",
1500
+ () => {
1501
+ let withState = schema =>
1502
+ schema
1503
+ ->S.castToUnknown
1504
+ ->S.Metadata.set(
1505
+ ~id=Reventless.StateAnnotations.stateAnnotationsId,
1506
+ {
1507
+ ids: [],
1508
+ compositeIds: [],
1509
+ subIds: [],
1510
+ compositeSubIds: [],
1511
+ indexes: [],
1512
+ hidden: [],
1513
+ summary: [],
1514
+ drillTargets: [],
1515
+ drillTargetKeys: [],
1516
+ collapsed: [],
1517
+ scan: [],
1518
+ scanSort: [],
1519
+ semantic: [],
1520
+ metric: [],
1521
+ lifecycle: Some("accountStatus"),
1522
+ groupBy: None,
1523
+ visibility: None,
1524
+ live: None,
1525
+ retired: Some({
1526
+ field: "accountStatus",
1527
+ label: "",
1528
+ showWhenFalse: false,
1529
+ values: Some(["Deactivated", "Closed"]),
1530
+ namedWhenRetired: false,
1531
+ }),
1532
+ },
1533
+ )
1534
+ let schema = S.schema(s => {"accountStatus": s.matches(S.string)})->withState
1535
+ expect((
1536
+ Plugin_Structure.retiredFieldFromStateSchema(schema),
1537
+ Plugin_Structure.retiredValuesFromStateSchema(schema),
1538
+ ))->toEqual((Some("accountStatus"), Some(["Deactivated", "Closed"])))
1539
+ },
1540
+ )
1239
1541
 
1240
1542
  // Absent is what says "boolean form", so it has to stay tellable from a state
1241
1543
  // form — including one naming nothing, which is `Some([])`.
1242
- testSync("publishes no states on the boolean form", () => {
1243
- let schema = S.schema(s => {"archived": s.matches(S.bool)})
1244
- expect(
1245
- Plugin_Structure.retiredValuesFromStateSchema(schema->withRetired(~field="archived")),
1246
- )->toEqual(None)
1247
- })
1248
-
1249
- testSync("names the annotated field", () => {
1250
- let schema = S.schema(s => {"archived": s.matches(S.bool)})
1251
- expect(retiredOf(schema->withRetired(~field="archived")))->toEqual(Some("archived"))
1252
- })
1544
+ testSync(
1545
+ "publishes no states on the boolean form",
1546
+ () => {
1547
+ let schema = S.schema(s => {"archived": s.matches(S.bool)})
1548
+ expect(
1549
+ Plugin_Structure.retiredValuesFromStateSchema(schema->withRetired(~field="archived")),
1550
+ )->toEqual(None)
1551
+ },
1552
+ )
1553
+
1554
+ testSync(
1555
+ "names the annotated field",
1556
+ () => {
1557
+ let schema = S.schema(s => {"archived": s.matches(S.bool)})
1558
+ expect(retiredOf(schema->withRetired(~field="archived")))->toEqual(Some("archived"))
1559
+ },
1560
+ )
1253
1561
 
1254
1562
  // The whole reason this has no convention rung. `lifecycleField` may still fall
1255
1563
  // back to a field literally named `lifecycle` because guessing wrong there makes
1256
1564
  // a command menu filter oddly; guessing wrong here makes rows disappear for every caller who is not
1257
1565
  // elevated, so an unannotated boolean stays as visible as it was.
1258
- testSync("declines a conventionally-named boolean nobody annotated", () => {
1259
- let schema = S.schema(s =>
1260
- {
1261
- "archived": s.matches(S.bool),
1262
- "deactivated": s.matches(S.bool),
1263
- }
1264
- )
1265
- expect(retiredOf(schema))->toEqual(None)
1266
- })
1566
+ testSync(
1567
+ "declines a conventionally-named boolean nobody annotated",
1568
+ () => {
1569
+ let schema = S.schema(
1570
+ s =>
1571
+ {
1572
+ "archived": s.matches(S.bool),
1573
+ "deactivated": s.matches(S.bool),
1574
+ },
1575
+ )
1576
+ expect(retiredOf(schema))->toEqual(None)
1577
+ },
1578
+ )
1267
1579
 
1268
- testSync("declines a schema carrying no annotation spec at all", () => {
1269
- let schema = S.schema(s => {"id": s.matches(S.string)})
1270
- expect(retiredOf(schema))->toEqual(None)
1271
- })
1580
+ testSync(
1581
+ "declines a schema carrying no annotation spec at all",
1582
+ () => {
1583
+ let schema = S.schema(s => {"id": s.matches(S.string)})
1584
+ expect(retiredOf(schema))->toEqual(None)
1585
+ },
1586
+ )
1272
1587
  })
1273
1588
 
1274
1589
  // A misspelled retirement state used to be fatal or merely logged depending on
@@ -1278,7 +1593,9 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1278
1593
  // a build, so this rung was raised to match the other.
1279
1594
  describe("@retired state names are checked against the field's own enum", () => {
1280
1595
  let withRetiredStates = (schema, ~field, ~lifecycle, ~values) =>
1281
- schema->S.castToUnknown->S.Metadata.set(
1596
+ schema
1597
+ ->S.castToUnknown
1598
+ ->S.Metadata.set(
1282
1599
  ~id=Reventless.StateAnnotations.stateAnnotationsId,
1283
1600
  {
1284
1601
  ids: [],
@@ -1304,73 +1621,93 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1304
1621
  )
1305
1622
 
1306
1623
  let shelf = values =>
1307
- S.schema(s =>
1308
- {
1309
- "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1310
- }
1624
+ S.schema(
1625
+ s =>
1626
+ {
1627
+ "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1628
+ },
1311
1629
  )->withRetiredStates(~field="shelf", ~lifecycle=Some("shelf"), ~values=Some(values))
1312
1630
 
1313
1631
  let check = schema => Plugin_Structure.checkRetiredValue(~entityName="Products", schema)
1314
1632
 
1315
- testSync("a declared state passes", () => {
1316
- expect(check(shelf(["Archived"])))->toEqual(Plugin_Structure.Checked([]))
1317
- })
1633
+ testSync(
1634
+ "a declared state passes",
1635
+ () => {
1636
+ expect(check(shelf(["Archived"])))->toEqual(Plugin_Structure.Checked([]))
1637
+ },
1638
+ )
1318
1639
 
1319
1640
  // Reported per state rather than as a set: one wrong entry among several
1320
1641
  // still narrows something, so the symptom is a subset of rows leaking.
1321
- testSync("each undeclared state is reported on its own", () => {
1322
- switch check(shelf(["Archived", "Discontinud", "Withdrawn"])) {
1323
- | Checked(failures) =>
1324
- expect(failures->Array.length)->toBe(2)
1325
- expect(failures->Array.some(f => f->String.includes("Discontinud")))->toBe(true)
1326
- expect(failures->Array.some(f => f->String.includes("Withdrawn")))->toBe(true)
1327
- // Naming what the field does declare is what turns the failure into a fix.
1328
- expect(failures->Array.every(f => f->String.includes("Listed, Archived")))->toBe(true)
1329
- | other => expect(other)->toEqual(Plugin_Structure.Checked([]))
1330
- }
1331
- })
1642
+ testSync(
1643
+ "each undeclared state is reported on its own",
1644
+ () => {
1645
+ switch check(shelf(["Archived", "Discontinud", "Withdrawn"])) {
1646
+ | Checked(failures) =>
1647
+ expect(failures->Array.length)->toBe(2)
1648
+ expect(failures->Array.some(f => f->String.includes("Discontinud")))->toBe(true)
1649
+ expect(failures->Array.some(f => f->String.includes("Withdrawn")))->toBe(true)
1650
+ // Naming what the field does declare is what turns the failure into a fix.
1651
+ expect(failures->Array.every(f => f->String.includes("Listed, Archived")))->toBe(true)
1652
+ | other => expect(other)->toEqual(Plugin_Structure.Checked([]))
1653
+ }
1654
+ },
1655
+ )
1332
1656
 
1333
1657
  // The condition that came with promoting this: a fatal rule that is silent
1334
1658
  // when it cannot run looks exactly like one that passed.
1335
- testSync("a field with no cases to compare against says so instead of passing", () => {
1336
- let schema =
1337
- S.schema(s => {"shelf": s.matches(S.string)})->withRetiredStates(
1338
- ~field="shelf",
1339
- ~lifecycle=Some("shelf"),
1340
- ~values=Some(["Archived"]),
1341
- )
1342
- switch check(schema) {
1343
- | Unchecked(why) => expect(why->String.includes("no cases"))->toBe(true)
1344
- | other => expect(other)->toEqual(Plugin_Structure.Unchecked(""))
1345
- }
1346
- })
1659
+ testSync(
1660
+ "a field with no cases to compare against says so instead of passing",
1661
+ () => {
1662
+ let schema =
1663
+ S.schema(s => {"shelf": s.matches(S.string)})->withRetiredStates(
1664
+ ~field="shelf",
1665
+ ~lifecycle=Some("shelf"),
1666
+ ~values=Some(["Archived"]),
1667
+ )
1668
+ switch check(schema) {
1669
+ | Unchecked(why) => expect(why->String.includes("no cases"))->toBe(true)
1670
+ | other => expect(other)->toEqual(Plugin_Structure.Unchecked(""))
1671
+ }
1672
+ },
1673
+ )
1347
1674
 
1348
1675
  // The boolean form names no state, so there is nothing to compare — which is
1349
1676
  // not the same as having failed to compare it.
1350
- testSync("the boolean form is not a skip", () => {
1351
- let schema =
1352
- S.schema(s => {"archived": s.matches(S.bool)})->withRetiredStates(
1353
- ~field="archived",
1354
- ~lifecycle=None,
1355
- ~values=None,
1677
+ testSync(
1678
+ "the boolean form is not a skip",
1679
+ () => {
1680
+ let schema =
1681
+ S.schema(s => {"archived": s.matches(S.bool)})->withRetiredStates(
1682
+ ~field="archived",
1683
+ ~lifecycle=None,
1684
+ ~values=None,
1685
+ )
1686
+ expect(Plugin_Structure.checkRetiredValue(~entityName="Products", schema))->toEqual(
1687
+ Plugin_Structure.NotDeclared,
1356
1688
  )
1357
- expect(Plugin_Structure.checkRetiredValue(~entityName="Products", schema))->toEqual(
1358
- Plugin_Structure.NotDeclared,
1359
- )
1360
- })
1689
+ },
1690
+ )
1361
1691
 
1362
1692
  // The second rule stays a warning: retirement still works, and which field
1363
1693
  // carries it is a modelling judgement rather than a name that is wrong.
1364
- testSync("a state on a non-lifecycle field is not a failure", () => {
1365
- let schema =
1366
- S.schema(s =>
1367
- {
1368
- "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1369
- "lifecycle": s.matches(S.union([S.literal("Draft"), S.literal("Live")])),
1370
- }
1371
- )->withRetiredStates(~field="shelf", ~lifecycle=Some("lifecycle"), ~values=Some(["Archived"]))
1372
- expect(check(schema))->toEqual(Plugin_Structure.Checked([]))
1373
- })
1694
+ testSync(
1695
+ "a state on a non-lifecycle field is not a failure",
1696
+ () => {
1697
+ let schema = S.schema(
1698
+ s =>
1699
+ {
1700
+ "shelf": s.matches(S.union([S.literal("Listed"), S.literal("Archived")])),
1701
+ "lifecycle": s.matches(S.union([S.literal("Draft"), S.literal("Live")])),
1702
+ },
1703
+ )->withRetiredStates(
1704
+ ~field="shelf",
1705
+ ~lifecycle=Some("lifecycle"),
1706
+ ~values=Some(["Archived"]),
1707
+ )
1708
+ expect(check(schema))->toEqual(Plugin_Structure.Checked([]))
1709
+ },
1710
+ )
1374
1711
  })
1375
1712
 
1376
1713
  // Shared by the read-side and write-side schema cases below, which ask the
@@ -1395,107 +1732,165 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1395
1732
  // annotated state types. Plugin_Structure now uses SuryToJsonSchema.deriveObjectSchema
1396
1733
  // (annotation-aware) instead of S.toJSONSchema (metadata-blind).
1397
1734
  describe("queryableDef.schema propagates x-reventless-* annotations", () => {
1398
- let parseSchema = (svs: Reventless.Plugin.queryableDef): JSON.t =>
1399
- svs.schema->JSON.parseOrThrow
1735
+ let parseSchema = (svs: Reventless.Plugin.queryableDef): JSON.t => svs.schema->JSON.parseOrThrow
1400
1736
 
1401
1737
  let annotatedSchema = structure.stateViewSlices->Array.getUnsafe(3)->parseSchema
1402
1738
 
1403
- testSync("itemId carries x-reventless-id", () => {
1404
- expect(
1405
- annotatedSchema
1406
- ->getPropertyOf("itemId")
1407
- ->Option.flatMap(s => getProperty(s, "x-reventless-id"))
1408
- ->Option.flatMap(JSON.Decode.bool),
1409
- )->toBe(Some(true))
1410
- })
1411
-
1412
- testSync("version carries x-reventless-subId", () => {
1413
- expect(
1414
- annotatedSchema
1415
- ->getPropertyOf("version")
1416
- ->Option.flatMap(s => getProperty(s, "x-reventless-subId"))
1417
- ->Option.flatMap(JSON.Decode.bool),
1418
- )->toBe(Some(true))
1419
- })
1420
-
1421
- testSync("ownerId carries x-reventless-index = byOwner", () => {
1422
- expect(
1423
- annotatedSchema
1424
- ->getPropertyOf("ownerId")
1425
- ->Option.flatMap(s => getProperty(s, "x-reventless-index"))
1426
- ->Option.flatMap(JSON.Decode.string),
1427
- )->toBe(Some("byOwner"))
1428
- })
1429
-
1430
- testSync("@semantic(\"currency\") flows through the PPX to x-reventless-semantic", () => {
1431
- expect(
1432
- annotatedSchema
1433
- ->getPropertyOf("total")
1434
- ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1435
- ->Option.flatMap(JSON.Decode.string),
1436
- )->toBe(Some("currency"))
1437
- })
1438
-
1439
- testSync("@sensitive flows through the PPX to x-reventless-sensitive", () => {
1440
- expect(
1441
- annotatedSchema
1442
- ->getPropertyOf("contact")
1443
- ->Option.flatMap(s => getProperty(s, "x-reventless-sensitive"))
1444
- ->Option.flatMap(JSON.Decode.bool),
1445
- )->toBe(Some(true))
1446
- })
1739
+ testSync(
1740
+ "itemId carries x-reventless-id",
1741
+ () => {
1742
+ expect(
1743
+ annotatedSchema
1744
+ ->getPropertyOf("itemId")
1745
+ ->Option.flatMap(s => getProperty(s, "x-reventless-id"))
1746
+ ->Option.flatMap(JSON.Decode.bool),
1747
+ )->toBe(Some(true))
1748
+ },
1749
+ )
1750
+
1751
+ testSync(
1752
+ "version carries x-reventless-subId",
1753
+ () => {
1754
+ expect(
1755
+ annotatedSchema
1756
+ ->getPropertyOf("version")
1757
+ ->Option.flatMap(s => getProperty(s, "x-reventless-subId"))
1758
+ ->Option.flatMap(JSON.Decode.bool),
1759
+ )->toBe(Some(true))
1760
+ },
1761
+ )
1762
+
1763
+ testSync(
1764
+ "ownerId carries x-reventless-index = byOwner",
1765
+ () => {
1766
+ expect(
1767
+ annotatedSchema
1768
+ ->getPropertyOf("ownerId")
1769
+ ->Option.flatMap(s => getProperty(s, "x-reventless-index"))
1770
+ ->Option.flatMap(JSON.Decode.string),
1771
+ )->toBe(Some("byOwner"))
1772
+ },
1773
+ )
1774
+
1775
+ testSync(
1776
+ "@semantic(\"currency\") flows through the PPX to x-reventless-semantic",
1777
+ () => {
1778
+ expect(
1779
+ annotatedSchema
1780
+ ->getPropertyOf("total")
1781
+ ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1782
+ ->Option.flatMap(JSON.Decode.string),
1783
+ )->toBe(Some("currency"))
1784
+ },
1785
+ )
1786
+
1787
+ testSync(
1788
+ "@sensitive flows through the PPX to x-reventless-sensitive",
1789
+ () => {
1790
+ expect(
1791
+ annotatedSchema
1792
+ ->getPropertyOf("contact")
1793
+ ->Option.flatMap(s => getProperty(s, "x-reventless-sensitive"))
1794
+ ->Option.flatMap(JSON.Decode.bool),
1795
+ )->toBe(Some(true))
1796
+ },
1797
+ )
1447
1798
 
1448
1799
  // The case the marker exists for. A renderer composes from an event payload,
1449
1800
  // and the annotation path `deriveObjectSchema` reads for state is
1450
1801
  // state-only — so a marker that lived there would be absent exactly where
1451
1802
  // the value is about to be interpolated into a message.
1452
- testSync("@sensitive reaches an event variant's payload, not only state", () =>
1453
- expect(
1454
- Reventless.Sensitive.variantFieldNames(
1455
- PsAnnotatedView.consumedEventSchema->S.castToUnknown,
1456
- ~variant="ItemRecorded",
1457
- ),
1458
- )->toEqual(["contact"])
1459
- )
1460
-
1461
- testSync("@metric flows through the PPX to x-reventless-metric {aggregate,label}", () => {
1462
- let metricObj =
1463
- annotatedSchema
1464
- ->getPropertyOf("total")
1465
- ->Option.flatMap(s => getProperty(s, "x-reventless-metric"))
1466
- expect((
1467
- metricObj->Option.flatMap(m => getProperty(m, "aggregate"))->Option.flatMap(JSON.Decode.string),
1468
- metricObj->Option.flatMap(m => getProperty(m, "label"))->Option.flatMap(JSON.Decode.string),
1469
- ))->toEqual((Some("sum"), Some("Revenue")))
1470
- })
1803
+ testSync(
1804
+ "@sensitive reaches an event variant's payload, not only state",
1805
+ () =>
1806
+ expect(
1807
+ Reventless.Sensitive.variantFieldNames(
1808
+ PsAnnotatedView.consumedEventSchema->S.castToUnknown,
1809
+ ~variant="ItemRecorded",
1810
+ ),
1811
+ )->toEqual(["contact"]),
1812
+ )
1471
1813
 
1472
- testSync("unannotated field 'name' has no x-reventless-* keys", () => {
1473
- let nameField = annotatedSchema->getPropertyOf("name")
1474
- expect((
1475
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
1476
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-subId")),
1477
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-index")),
1478
- ))->toEqual((None, None, None))
1479
- })
1480
-
1481
- testSync("unannotated state-view slice (Orders) emits no x-reventless-* keys", () => {
1482
- let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1483
- let orderIdField = ordersSchema->getPropertyOf("orderId")
1484
- expect(
1485
- orderIdField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
1486
- )->toBe(None)
1487
- })
1488
-
1489
- testSync("@live(false) flows through the PPX to top-level x-reventless-live", () => {
1490
- expect(
1491
- getProperty(annotatedSchema, "x-reventless-live")->Option.flatMap(JSON.Decode.bool),
1492
- )->toBe(Some(false))
1493
- })
1494
-
1495
- testSync("unannotated state-view slice (Orders) has no top-level x-reventless-live", () => {
1496
- let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1497
- expect(getProperty(ordersSchema, "x-reventless-live"))->toBe(None)
1498
- })
1814
+ // JSON Schema's own keyword rather than an `x-reventless-` one, so a client
1815
+ // that knows nothing about Reventless reads it.
1816
+ testSync(
1817
+ "@default flows through the PPX to JSON Schema's `default`",
1818
+ () => {
1819
+ expect(
1820
+ annotatedSchema
1821
+ ->getPropertyOf("quantity")
1822
+ ->Option.flatMap(s => getProperty(s, "default")),
1823
+ )->toEqual(Some(JSON.Encode.int(1)))
1824
+ },
1825
+ )
1826
+
1827
+ testSync(
1828
+ "@default leaves the field's own shape alone",
1829
+ () => {
1830
+ expect(
1831
+ annotatedSchema
1832
+ ->getPropertyOf("quantity")
1833
+ ->Option.flatMap(s => getProperty(s, "type"))
1834
+ ->Option.flatMap(JSON.Decode.string),
1835
+ )->toBe(Some("integer"))
1836
+ },
1837
+ )
1838
+
1839
+ testSync(
1840
+ "@metric flows through the PPX to x-reventless-metric {aggregate,label}",
1841
+ () => {
1842
+ let metricObj =
1843
+ annotatedSchema
1844
+ ->getPropertyOf("total")
1845
+ ->Option.flatMap(s => getProperty(s, "x-reventless-metric"))
1846
+ expect((
1847
+ metricObj
1848
+ ->Option.flatMap(m => getProperty(m, "aggregate"))
1849
+ ->Option.flatMap(JSON.Decode.string),
1850
+ metricObj
1851
+ ->Option.flatMap(m => getProperty(m, "label"))
1852
+ ->Option.flatMap(JSON.Decode.string),
1853
+ ))->toEqual((Some("sum"), Some("Revenue")))
1854
+ },
1855
+ )
1856
+
1857
+ testSync(
1858
+ "unannotated field 'name' has no x-reventless-* keys",
1859
+ () => {
1860
+ let nameField = annotatedSchema->getPropertyOf("name")
1861
+ expect((
1862
+ nameField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
1863
+ nameField->Option.flatMap(s => getProperty(s, "x-reventless-subId")),
1864
+ nameField->Option.flatMap(s => getProperty(s, "x-reventless-index")),
1865
+ ))->toEqual((None, None, None))
1866
+ },
1867
+ )
1868
+
1869
+ testSync(
1870
+ "unannotated state-view slice (Orders) emits no x-reventless-* keys",
1871
+ () => {
1872
+ let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1873
+ let orderIdField = ordersSchema->getPropertyOf("orderId")
1874
+ expect(orderIdField->Option.flatMap(s => getProperty(s, "x-reventless-id")))->toBe(None)
1875
+ },
1876
+ )
1877
+
1878
+ testSync(
1879
+ "@live(false) flows through the PPX to top-level x-reventless-live",
1880
+ () => {
1881
+ expect(
1882
+ getProperty(annotatedSchema, "x-reventless-live")->Option.flatMap(JSON.Decode.bool),
1883
+ )->toBe(Some(false))
1884
+ },
1885
+ )
1886
+
1887
+ testSync(
1888
+ "unannotated state-view slice (Orders) has no top-level x-reventless-live",
1889
+ () => {
1890
+ let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1891
+ expect(getProperty(ordersSchema, "x-reventless-live"))->toBe(None)
1892
+ },
1893
+ )
1499
1894
  })
1500
1895
 
1501
1896
  // The generator captures each component's chapter (source-folder grouping band)
@@ -1511,25 +1906,37 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1511
1906
  ~componentChapters=Dict.fromArray([("PlaceOrder", "Order"), ("Orders", "Order")]),
1512
1907
  )
1513
1908
 
1514
- testSync("a chaptered state-change slice carries Some(chapter)", () => {
1515
- let placeOrder = chaptered.stateChangeSlices->Array.getUnsafe(0)
1516
- expect(placeOrder.chapter)->toEqual(Some("Order"))
1517
- })
1909
+ testSync(
1910
+ "a chaptered state-change slice carries Some(chapter)",
1911
+ () => {
1912
+ let placeOrder = chaptered.stateChangeSlices->Array.getUnsafe(0)
1913
+ expect(placeOrder.chapter)->toEqual(Some("Order"))
1914
+ },
1915
+ )
1518
1916
 
1519
- testSync("a chaptered state-view slice carries Some(chapter)", () => {
1520
- let orders = chaptered.stateViewSlices->Array.getUnsafe(0)
1521
- expect(orders.chapter)->toEqual(Some("Order"))
1522
- })
1917
+ testSync(
1918
+ "a chaptered state-view slice carries Some(chapter)",
1919
+ () => {
1920
+ let orders = chaptered.stateViewSlices->Array.getUnsafe(0)
1921
+ expect(orders.chapter)->toEqual(Some("Order"))
1922
+ },
1923
+ )
1523
1924
 
1524
- testSync("a component absent from the map carries None", () => {
1525
- let shipOrder = chaptered.stateChangeSlices->Array.getUnsafe(1)
1526
- expect(shipOrder.chapter)->toEqual(None)
1527
- })
1925
+ testSync(
1926
+ "a component absent from the map carries None",
1927
+ () => {
1928
+ let shipOrder = chaptered.stateChangeSlices->Array.getUnsafe(1)
1929
+ expect(shipOrder.chapter)->toEqual(None)
1930
+ },
1931
+ )
1528
1932
 
1529
- testSync("no ~componentChapters → every def is chapterless (None)", () => {
1530
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
1531
- expect(placeOrder.chapter)->toEqual(None)
1532
- })
1933
+ testSync(
1934
+ "no ~componentChapters → every def is chapterless (None)",
1935
+ () => {
1936
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
1937
+ expect(placeOrder.chapter)->toEqual(None)
1938
+ },
1939
+ )
1533
1940
  })
1534
1941
 
1535
1942
  // A field typed as a storage ref states that the deployment needs that store
@@ -1542,120 +1949,152 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1542
1949
  )
1543
1950
  let stores = withStores.requiredStores->Option.getOr([])
1544
1951
 
1545
- testSync("an unqualified store resolves against the declaring plugin", () =>
1546
- expect(stores->Array.includes("TestPlugin.documents"))->toBe(true)
1952
+ testSync(
1953
+ "an unqualified store resolves against the declaring plugin",
1954
+ () => expect(stores->Array.includes("TestPlugin.documents"))->toBe(true),
1547
1955
  )
1548
1956
 
1549
- testSync("a qualified store keeps its own plugin", () =>
1550
- expect(stores->Array.includes("branding.logos"))->toBe(true)
1957
+ testSync(
1958
+ "a qualified store keeps its own plugin",
1959
+ () => expect(stores->Array.includes("branding.logos"))->toBe(true),
1551
1960
  )
1552
1961
 
1553
1962
  // `documents` is declared on both a command field and an event field.
1554
- testSync("a store declared by several fields is collected once", () =>
1555
- expect(stores->Array.filter(s => s == "TestPlugin.documents")->Array.length)->toBe(1)
1963
+ testSync(
1964
+ "a store declared by several fields is collected once",
1965
+ () => expect(stores->Array.filter(s => s == "TestPlugin.documents")->Array.length)->toBe(1),
1556
1966
  )
1557
1967
 
1558
- testSync("only the declared stores are collected", () =>
1559
- expect(stores->Array.length)->toBe(2)
1560
- )
1968
+ testSync("only the declared stores are collected", () => expect(stores->Array.length)->toBe(2))
1561
1969
 
1562
- testSync("a plugin declaring no stores collects none — not None", () =>
1563
- expect(structure.requiredStores)->toEqual(Some([]))
1970
+ testSync(
1971
+ "a plugin declaring no stores collects none — not None",
1972
+ () => expect(structure.requiredStores)->toEqual(Some([])),
1564
1973
  )
1565
1974
 
1566
- testSync("each requirement keeps its declaring (component, field) sites", () => {
1567
- // `documents` is declared on a command field AND an event field of the
1568
- // same component under the same field name — one declaration site, so
1569
- // the identical triples collapse to one entry.
1570
- // `annotation` is the source text, not the key: the own-plugin store is
1571
- // written bare and the foreign one qualified, which is exactly the
1572
- // distinction no downstream comparison can recover.
1573
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1574
- {
1575
- store: "TestPlugin.documents",
1576
- component: "AttachInvoice",
1577
- field: "documentUrl",
1578
- annotation: Some("documents"),
1579
- },
1580
- {
1581
- store: "branding.logos",
1582
- component: "AttachInvoice",
1583
- field: "logoUrl",
1584
- annotation: Some("branding.logos"),
1585
- },
1586
- ]
1587
- expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
1588
- })
1975
+ testSync(
1976
+ "each requirement keeps its declaring (component, field) sites",
1977
+ () => {
1978
+ // `documents` is declared on a command field AND an event field of the
1979
+ // same component under the same field name — one declaration site, so
1980
+ // the identical triples collapse to one entry.
1981
+ // `annotation` is the source text, not the key: the own-plugin store is
1982
+ // written bare and the foreign one qualified, which is exactly the
1983
+ // distinction no downstream comparison can recover.
1984
+ let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1985
+ {
1986
+ store: "TestPlugin.documents",
1987
+ component: "AttachInvoice",
1988
+ field: "documentUrl",
1989
+ annotation: Some("documents"),
1990
+ },
1991
+ {
1992
+ store: "branding.logos",
1993
+ component: "AttachInvoice",
1994
+ field: "logoUrl",
1995
+ annotation: Some("branding.logos"),
1996
+ },
1997
+ ]
1998
+ expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
1999
+ },
2000
+ )
1589
2001
 
1590
2002
  // The manifest is a rendering of the structure. Its key set must be
1591
2003
  // byte-identical to what the deployed plugin reports as `requiredStores`,
1592
2004
  // because the deploy-time coverage assertion compares against exactly that
1593
2005
  // — this equality is the whole contract of `capabilities.json`.
1594
- describe("capability manifest", () => {
1595
- let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
1596
-
1597
- testSync("keys are byte-identical to requiredStores", () =>
1598
- expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
1599
- withStores.requiredStores->Option.getOr([]),
2006
+ describe(
2007
+ "capability manifest",
2008
+ () => {
2009
+ let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
2010
+
2011
+ testSync(
2012
+ "keys are byte-identical to requiredStores",
2013
+ () =>
2014
+ expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
2015
+ withStores.requiredStores->Option.getOr([]),
2016
+ ),
1600
2017
  )
1601
- )
1602
-
1603
- testSync("each entry carries its provenance", () => {
1604
- let entry =
1605
- manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
1606
- let expected: array<Reventless.CapabilityManifest.provenance> = [
1607
- {component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
1608
- ]
1609
- expect(entry.declaredBy)->toEqual(expected)
1610
- })
1611
-
1612
- testSync("a structure declaring nothing yields an empty list, not a missing manifest", () =>
1613
- expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
1614
- )
1615
-
1616
- // A capability a slice declares rides the same manifest as the stores, so
1617
- // the platform generator reads one file. It carries no `field`: nothing
1618
- // annotated it, and naming one would be a fiction.
1619
- describe("declared capabilities", () => {
1620
- let withCapability = {
1621
- ...structure,
1622
- requiredCapabilities: Some([
1623
- ({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
1624
- {capability: "Geocoding", component: "GeocodeDropOff"},
1625
- ]),
1626
- traitDeclarations: None,
1627
- }
1628
- let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
1629
2018
 
1630
- testSync("two components needing one capability yield one entry", () =>
1631
- expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"])
2019
+ testSync(
2020
+ "each entry carries its provenance",
2021
+ () => {
2022
+ let entry =
2023
+ manifest.capabilities
2024
+ ->Array.find(c => c.key == "TestPlugin.documents")
2025
+ ->Option.getOrThrow
2026
+ let expected: array<Reventless.CapabilityManifest.provenance> = [
2027
+ {component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
2028
+ ]
2029
+ expect(entry.declaredBy)->toEqual(expected)
2030
+ },
1632
2031
  )
1633
2032
 
1634
- testSync("both declaring components are kept, with no field", () =>
1635
- expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
1636
- ({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
1637
- {component: "GeocodeDropOff"},
1638
- ])
2033
+ testSync(
2034
+ "a structure declaring nothing yields an empty list, not a missing manifest",
2035
+ () =>
2036
+ expect(
2037
+ Reventless.CapabilityManifest.fromStructure(structure).capabilities,
2038
+ )->toEqual([]),
1639
2039
  )
1640
2040
 
1641
- // The generator downstream renders a real `Platform.capability` arm, and
1642
- // a name this build cannot map has no arm to render — so it is dropped
1643
- // here rather than passed through as an unrenderable key.
1644
- testSync("a capability this build does not know is dropped", () =>
1645
- expect(
1646
- Reventless.CapabilityManifest.fromStructure({
2041
+ // A capability a slice declares rides the same manifest as the stores, so
2042
+ // the platform generator reads one file. It carries no `field`: nothing
2043
+ // annotated it, and naming one would be a fiction.
2044
+ describe(
2045
+ "declared capabilities",
2046
+ () => {
2047
+ let withCapability = {
1647
2048
  ...structure,
1648
- requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
2049
+ requiredCapabilities: Some([
2050
+ (
2051
+ {
2052
+ capability: "Geocoding",
2053
+ component: "GeocodeAddress",
2054
+ }: Reventless.Plugin.requiredCapabilityDeclaration
2055
+ ),
2056
+ {capability: "Geocoding", component: "GeocodeDropOff"},
2057
+ ]),
1649
2058
  traitDeclarations: None,
1650
- }).capabilities,
1651
- )->toEqual([])
2059
+ }
2060
+ let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
2061
+
2062
+ testSync(
2063
+ "two components needing one capability yield one entry",
2064
+ () => expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"]),
2065
+ )
2066
+
2067
+ testSync(
2068
+ "both declaring components are kept, with no field",
2069
+ () =>
2070
+ expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
2071
+ ({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
2072
+ {component: "GeocodeDropOff"},
2073
+ ]),
2074
+ )
2075
+
2076
+ // The generator downstream renders a real `Platform.capability` arm, and
2077
+ // a name this build cannot map has no arm to render — so it is dropped
2078
+ // here rather than passed through as an unrenderable key.
2079
+ testSync(
2080
+ "a capability this build does not know is dropped",
2081
+ () =>
2082
+ expect(
2083
+ Reventless.CapabilityManifest.fromStructure({
2084
+ ...structure,
2085
+ requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
2086
+ traitDeclarations: None,
2087
+ }).capabilities,
2088
+ )->toEqual([]),
2089
+ )
2090
+ },
1652
2091
  )
1653
- })
1654
2092
 
1655
- testSync("rendering is deterministic and newline-terminated", () => {
1656
- let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
1657
- expect(rendered)->toBe(
1658
- `{
2093
+ testSync(
2094
+ "rendering is deterministic and newline-terminated",
2095
+ () => {
2096
+ let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
2097
+ expect(rendered)->toBe(`{
1659
2098
  "capabilities": [
1660
2099
  {
1661
2100
  "kind": "ObjectStore",
@@ -1681,10 +2120,11 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1681
2120
  }
1682
2121
  ]
1683
2122
  }
1684
- `,
2123
+ `)
2124
+ },
1685
2125
  )
1686
- })
1687
- })
2126
+ },
2127
+ )
1688
2128
  })
1689
2129
 
1690
2130
  // A field typed uploadable declares its store by being named. The collection
@@ -1698,23 +2138,27 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1698
2138
  )
1699
2139
  let stores = withUploads.requiredStores->Option.getOr([])
1700
2140
 
1701
- testSync("the derived store is the field name, pluralised", () =>
1702
- expect(stores->Array.includes("TestPlugin.productImages"))->toBe(true)
2141
+ testSync(
2142
+ "the derived store is the field name, pluralised",
2143
+ () => expect(stores->Array.includes("TestPlugin.productImages"))->toBe(true),
1703
2144
  )
1704
2145
 
1705
- testSync("an optional field still declares its store", () =>
1706
- expect(stores->Array.includes("TestPlugin.categoryImages"))->toBe(true)
2146
+ testSync(
2147
+ "an optional field still declares its store",
2148
+ () => expect(stores->Array.includes("TestPlugin.categoryImages"))->toBe(true),
1707
2149
  )
1708
2150
 
1709
2151
  // The array marker lives on the element type. Reading the field schema
1710
2152
  // directly answers `None`, which is the same silence as never declaring —
1711
2153
  // so this asserts collection, not just that the fixture compiled.
1712
- testSync("an array field declares through its element", () =>
1713
- expect(stores->Array.includes("TestPlugin.datasheets"))->toBe(true)
2154
+ testSync(
2155
+ "an array field declares through its element",
2156
+ () => expect(stores->Array.includes("TestPlugin.datasheets"))->toBe(true),
1714
2157
  )
1715
2158
 
1716
- testSync("an explicit @storageRef overrides the derived name", () =>
1717
- expect(stores->Array.includes("branding.logos"))->toBe(true)
2159
+ testSync(
2160
+ "an explicit @storageRef overrides the derived name",
2161
+ () => expect(stores->Array.includes("branding.logos"))->toBe(true),
1718
2162
  )
1719
2163
 
1720
2164
  testSync("nothing else is collected", () => expect(stores->Array.length)->toBe(4))
@@ -1722,39 +2166,43 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1722
2166
  // The event field derives the same store as the command field of the same
1723
2167
  // name, so the two collapse to one declaration site — the behaviour the
1724
2168
  // annotation form already had, reached without an annotation.
1725
- testSync("each requirement keeps its declaring sites", () => {
1726
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1727
- {
1728
- store: "TestPlugin.categoryImages",
1729
- component: "UploadImages",
1730
- field: "categoryImage",
1731
- annotation: Some("categoryImages"),
1732
- },
1733
- {
1734
- store: "TestPlugin.datasheets",
1735
- component: "UploadImages",
1736
- field: "datasheets",
1737
- annotation: Some("datasheets"),
1738
- },
1739
- {
1740
- store: "TestPlugin.productImages",
1741
- component: "UploadImages",
1742
- field: "productImage",
1743
- annotation: Some("productImages"),
1744
- },
1745
- {
1746
- store: "branding.logos",
1747
- component: "UploadImages",
1748
- field: "logo",
1749
- annotation: Some("branding.logos"),
1750
- },
1751
- ]
1752
- expect(withUploads.requiredStoreDeclarations)->toEqual(Some(expected))
1753
- })
2169
+ testSync(
2170
+ "each requirement keeps its declaring sites",
2171
+ () => {
2172
+ let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
2173
+ {
2174
+ store: "TestPlugin.categoryImages",
2175
+ component: "UploadImages",
2176
+ field: "categoryImage",
2177
+ annotation: Some("categoryImages"),
2178
+ },
2179
+ {
2180
+ store: "TestPlugin.datasheets",
2181
+ component: "UploadImages",
2182
+ field: "datasheets",
2183
+ annotation: Some("datasheets"),
2184
+ },
2185
+ {
2186
+ store: "TestPlugin.productImages",
2187
+ component: "UploadImages",
2188
+ field: "productImage",
2189
+ annotation: Some("productImages"),
2190
+ },
2191
+ {
2192
+ store: "branding.logos",
2193
+ component: "UploadImages",
2194
+ field: "logo",
2195
+ annotation: Some("branding.logos"),
2196
+ },
2197
+ ]
2198
+ expect(withUploads.requiredStoreDeclarations)->toEqual(Some(expected))
2199
+ },
2200
+ )
1754
2201
 
1755
2202
  let commandJson =
1756
- ((withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)).schema
1757
- ->JSON.parseOrThrow
2203
+ (
2204
+ (withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
2205
+ ).schema->JSON.parseOrThrow
1758
2206
 
1759
2207
  let semanticOf = field =>
1760
2208
  commandJson
@@ -1764,24 +2212,28 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1764
2212
 
1765
2213
  // The whole point of the family: one declaration states the content type
1766
2214
  // too, so the UI stops learning "this is an image" from the field's name.
1767
- testSync("the wire carries the content half, not just the store", () =>
1768
- expect(semanticOf("productImage"))->toBe(Some("uploadableImage"))
2215
+ testSync(
2216
+ "the wire carries the content half, not just the store",
2217
+ () => expect(semanticOf("productImage"))->toBe(Some("uploadableImage")),
1769
2218
  )
1770
2219
 
1771
2220
  // The override replaces the *store*, not the type's semantic: a field
1772
2221
  // pointing at another plugin's store is still an image.
1773
- testSync("an overridden field keeps its uploadable semantic", () =>
1774
- expect(semanticOf("logo"))->toBe(Some("uploadableImage"))
2222
+ testSync(
2223
+ "an overridden field keeps its uploadable semantic",
2224
+ () => expect(semanticOf("logo"))->toBe(Some("uploadableImage")),
1775
2225
  )
1776
2226
 
1777
- testSync("the non-image arm carries its own id, on the element", () =>
1778
- expect(
1779
- commandJson
1780
- ->getPropertyOf("datasheets")
1781
- ->Option.flatMap(s => getProperty(s, "items"))
1782
- ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1783
- ->Option.flatMap(JSON.Decode.string),
1784
- )->toBe(Some("uploadableFile"))
2227
+ testSync(
2228
+ "the non-image arm carries its own id, on the element",
2229
+ () =>
2230
+ expect(
2231
+ commandJson
2232
+ ->getPropertyOf("datasheets")
2233
+ ->Option.flatMap(s => getProperty(s, "items"))
2234
+ ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
2235
+ ->Option.flatMap(JSON.Decode.string),
2236
+ )->toBe(Some("uploadableFile")),
1785
2237
  )
1786
2238
  })
1787
2239
 
@@ -1800,49 +2252,58 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1800
2252
  | _ => "(not a JS error)"
1801
2253
  }
1802
2254
 
1803
- testSync("two stores one transposition apart fail the build", () =>
1804
- expect(failureText() != "")->toBe(true)
2255
+ testSync(
2256
+ "two stores one transposition apart fail the build",
2257
+ () => expect(failureText() != "")->toBe(true),
1805
2258
  )
1806
2259
 
1807
- testSync("the failure names both stores and both fields", () => {
1808
- let text = failureText()
1809
- expect([
1810
- text->String.includes("TestPlugin.productImages"),
1811
- text->String.includes("TestPlugin.productImgaes"),
1812
- text->String.includes("TypoStore.productImage"),
1813
- text->String.includes("@storageRef"),
1814
- ])->toEqual([true, true, true, true])
1815
- })
2260
+ testSync(
2261
+ "the failure names both stores and both fields",
2262
+ () => {
2263
+ let text = failureText()
2264
+ expect([
2265
+ text->String.includes("TestPlugin.productImages"),
2266
+ text->String.includes("TestPlugin.productImgaes"),
2267
+ text->String.includes("TypoStore.productImage"),
2268
+ text->String.includes("@storageRef"),
2269
+ ])->toEqual([true, true, true, true])
2270
+ },
2271
+ )
1816
2272
 
1817
2273
  // Every legitimate pair in the other fixtures has to stay legitimate — a
1818
2274
  // check that fails on real designs is worse than the typo it catches.
1819
- testSync("the four stores of the uploadable fixture do not collide", () =>
1820
- expect(
1821
- Capability_Inference.collisions(
1822
- Plugin_Structure.make(
1823
- ~name="TestPlugin",
1824
- ~stateChangeSlices=[module(PsUploadImagesSlice)],
1825
- ).requiredStoreDeclarations->Option.getOr([]),
1826
- ),
1827
- )->toEqual([])
1828
- )
1829
-
1830
- testSync("one edit, transpositions included; two edits not", () =>
1831
- expect(
1832
- [
1833
- ("productImages", "productImgaes"),
1834
- ("productImages", "productImage"),
1835
- ("logos", "logo"),
1836
- ("documents", "logos"),
1837
- ("productImages", "productImgeas"),
1838
- ]->Array.map(((a, b)) => Capability_Inference.withinOneEdit(a, b)),
1839
- )->toEqual([true, true, true, false, false])
2275
+ testSync(
2276
+ "the four stores of the uploadable fixture do not collide",
2277
+ () =>
2278
+ expect(
2279
+ Capability_Inference.collisions(
2280
+ Plugin_Structure.make(
2281
+ ~name="TestPlugin",
2282
+ ~stateChangeSlices=[module(PsUploadImagesSlice)],
2283
+ ).requiredStoreDeclarations->Option.getOr([]),
2284
+ ),
2285
+ )->toEqual([]),
2286
+ )
2287
+
2288
+ testSync(
2289
+ "one edit, transpositions included; two edits not",
2290
+ () =>
2291
+ expect(
2292
+ [
2293
+ ("productImages", "productImgaes"),
2294
+ ("productImages", "productImage"),
2295
+ ("logos", "logo"),
2296
+ ("documents", "logos"),
2297
+ ("productImages", "productImgeas"),
2298
+ ]->Array.map(((a, b)) => Capability_Inference.withinOneEdit(a, b)),
2299
+ )->toEqual([true, true, true, false, false]),
1840
2300
  )
1841
2301
 
1842
2302
  // Reached only through an explicit override: two *derived* names cannot
1843
2303
  // differ by number, because each is pluralised through its singular stem.
1844
- testSync("a singular/plural pair collides even at two edits apart", () =>
1845
- expect(Capability_Inference.sameStem("productImage", "productImages"))->toBe(true)
2304
+ testSync(
2305
+ "a singular/plural pair collides even at two edits apart",
2306
+ () => expect(Capability_Inference.sameStem("productImage", "productImages"))->toBe(true),
1846
2307
  )
1847
2308
  })
1848
2309
 
@@ -1858,62 +2319,75 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1858
2319
  ~stateChangeSlices=[module(PsUploadAvatarSlice)],
1859
2320
  )
1860
2321
 
1861
- testSync("an optional storage ref still requires its store", () =>
1862
- expect(withOptional.requiredStores)->toEqual(Some(["TestPlugin.avatars"]))
2322
+ testSync(
2323
+ "an optional storage ref still requires its store",
2324
+ () => expect(withOptional.requiredStores)->toEqual(Some(["TestPlugin.avatars"])),
1863
2325
  )
1864
2326
 
1865
2327
  // Declared on the command and on the event under one field name — the same
1866
2328
  // collapse the required case makes, so optionality does not change arity.
1867
- testSync("and still carries one declaring site", () => {
1868
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1869
- {
1870
- store: "TestPlugin.avatars",
1871
- component: "UploadAvatar",
1872
- field: "avatarUrl",
1873
- annotation: Some("avatars"),
1874
- },
1875
- ]
1876
- expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
1877
- })
2329
+ testSync(
2330
+ "and still carries one declaring site",
2331
+ () => {
2332
+ let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
2333
+ {
2334
+ store: "TestPlugin.avatars",
2335
+ component: "UploadAvatar",
2336
+ field: "avatarUrl",
2337
+ annotation: Some("avatars"),
2338
+ },
2339
+ ]
2340
+ expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
2341
+ },
2342
+ )
1878
2343
 
1879
- testSync("an optional reference is still collected", () => {
1880
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1881
- let expected: array<Reventless.Plugin.fieldReference> = [
1882
- {fieldName: "referredBy", entity: "Customers", plugin: None},
1883
- ]
1884
- expect(cmd.references)->toEqual(expected)
1885
- })
2344
+ testSync(
2345
+ "an optional reference is still collected",
2346
+ () => {
2347
+ let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
2348
+ let expected: array<Reventless.Plugin.fieldReference> = [
2349
+ {fieldName: "referredBy", entity: "Customers", plugin: None},
2350
+ ]
2351
+ expect(cmd.references)->toEqual(expected)
2352
+ },
2353
+ )
1886
2354
 
1887
2355
  // What `requiredStores` knows, the wire has to carry. A reader deciding
1888
2356
  // which command fills a storage-ref field, or which store's endpoint an
1889
2357
  // upload goes to, has only the command schema to go on.
1890
- testSync("the command schema carries the field's storage-ref marker", () => {
1891
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1892
- let target =
1893
- cmd.schema
1894
- ->JSON.parseOrThrow
1895
- ->getPropertyOf("avatarUrl")
1896
- ->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target"))
1897
- ->Option.flatMap(t => getProperty(t, "store"))
1898
- ->Option.flatMap(JSON.Decode.string)
1899
- expect(target)->toBe(Some("avatars"))
1900
- })
2358
+ testSync(
2359
+ "the command schema carries the field's storage-ref marker",
2360
+ () => {
2361
+ let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
2362
+ let target =
2363
+ cmd.schema
2364
+ ->JSON.parseOrThrow
2365
+ ->getPropertyOf("avatarUrl")
2366
+ ->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target"))
2367
+ ->Option.flatMap(t => getProperty(t, "store"))
2368
+ ->Option.flatMap(JSON.Decode.string)
2369
+ expect(target)->toBe(Some("avatars"))
2370
+ },
2371
+ )
1901
2372
 
1902
2373
  // A form submits what a schema says is required. `avatarUrl?` is optional in
1903
2374
  // the spec, so listing it would make the picture mandatory in every UI that
1904
2375
  // renders the command — the failure this pairs with, since carrying the
1905
2376
  // marker is worth nothing if the field cannot be left empty.
1906
- testSync("an optional command argument is not required", () => {
1907
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1908
- let required =
1909
- cmd.schema
1910
- ->JSON.parseOrThrow
1911
- ->getProperty("required")
1912
- ->Option.flatMap(JSON.Decode.array)
1913
- ->Option.getOr([])
1914
- ->Array.filterMap(JSON.Decode.string)
1915
- expect(required)->toEqual(["customerId"])
1916
- })
2377
+ testSync(
2378
+ "an optional command argument is not required",
2379
+ () => {
2380
+ let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
2381
+ let required =
2382
+ cmd.schema
2383
+ ->JSON.parseOrThrow
2384
+ ->getProperty("required")
2385
+ ->Option.flatMap(JSON.Decode.array)
2386
+ ->Option.getOr([])
2387
+ ->Array.filterMap(JSON.Decode.string)
2388
+ expect(required)->toEqual(["customerId"])
2389
+ },
2390
+ )
1917
2391
  })
1918
2392
 
1919
2393
  // Declarations are authoritative; the name heuristics are a lint. A
@@ -1930,52 +2404,66 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1930
2404
  PsChangePhoto.eventSchema->S.castToUnknown,
1931
2405
  )
1932
2406
 
1933
- testSync("a heuristic-only match produces a warning", () => {
1934
- let expected: array<Capability_Inference.warning> = [
1935
- {component: "ChangePhoto", field: "photoUrl"},
1936
- ]
1937
- expect(commandWarnings)->toEqual(expected)
1938
- })
1939
-
1940
- testSync("a declared field with a heuristic name does not warn", () => {
1941
- // The event's `thumbnail` matches the name heuristic but carries
1942
- // @storageRef — only the undeclared `photoUrl` warns.
1943
- let expected: array<Capability_Inference.warning> = [
1944
- {component: "ChangePhoto", field: "photoUrl"},
1945
- ]
1946
- expect(eventWarnings)->toEqual(expected)
1947
- })
2407
+ testSync(
2408
+ "a heuristic-only match produces a warning",
2409
+ () => {
2410
+ let expected: array<Capability_Inference.warning> = [
2411
+ {component: "ChangePhoto", field: "photoUrl"},
2412
+ ]
2413
+ expect(commandWarnings)->toEqual(expected)
2414
+ },
2415
+ )
1948
2416
 
1949
- testSync("a heuristic-only match reaches no manifest entry", () => {
1950
- let withHeuristicField = Plugin_Structure.make(
1951
- ~name="TestPlugin",
1952
- ~stateChangeSlices=[module(PsChangePhotoSlice)],
1953
- )
1954
- // Only the *declared* `thumbnail` store is collected; `photoUrl` — the
1955
- // heuristic-only match — provisions nothing.
1956
- expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
1957
- let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
1958
- expect(
1959
- manifest.capabilities->Array.every(entry =>
1960
- entry.declaredBy->Array.every(site => site.field != Some("photoUrl"))
1961
- ),
1962
- )->toBe(true)
1963
- })
1964
-
1965
- testSync("the warning names the field and the settling annotation", () => {
1966
- let text =
1967
- commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
1968
- expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
1969
- expect(text->String.includes("@storageRef"))->toBe(true)
1970
- })
1971
-
1972
- testSync("suffix and exact name rules match; ordinary names do not", () => {
1973
- expect(
1974
- ["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
1975
- Capability_Inference.nameMatches,
1976
- ),
1977
- )->toEqual([true, true, true, false, false])
1978
- })
2417
+ testSync(
2418
+ "a declared field with a heuristic name does not warn",
2419
+ () => {
2420
+ // The event's `thumbnail` matches the name heuristic but carries
2421
+ // @storageRef — only the undeclared `photoUrl` warns.
2422
+ let expected: array<Capability_Inference.warning> = [
2423
+ {component: "ChangePhoto", field: "photoUrl"},
2424
+ ]
2425
+ expect(eventWarnings)->toEqual(expected)
2426
+ },
2427
+ )
2428
+
2429
+ testSync(
2430
+ "a heuristic-only match reaches no manifest entry",
2431
+ () => {
2432
+ let withHeuristicField = Plugin_Structure.make(
2433
+ ~name="TestPlugin",
2434
+ ~stateChangeSlices=[module(PsChangePhotoSlice)],
2435
+ )
2436
+ // Only the *declared* `thumbnail` store is collected; `photoUrl` — the
2437
+ // heuristic-only match — provisions nothing.
2438
+ expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
2439
+ let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
2440
+ expect(
2441
+ manifest.capabilities->Array.every(
2442
+ entry => entry.declaredBy->Array.every(site => site.field != Some("photoUrl")),
2443
+ ),
2444
+ )->toBe(true)
2445
+ },
2446
+ )
2447
+
2448
+ testSync(
2449
+ "the warning names the field and the settling annotation",
2450
+ () => {
2451
+ let text = commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
2452
+ expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
2453
+ expect(text->String.includes("@storageRef"))->toBe(true)
2454
+ },
2455
+ )
2456
+
2457
+ testSync(
2458
+ "suffix and exact name rules match; ordinary names do not",
2459
+ () => {
2460
+ expect(
2461
+ ["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
2462
+ Capability_Inference.nameMatches,
2463
+ ),
2464
+ )->toEqual([true, true, true, false, false])
2465
+ },
2466
+ )
1979
2467
  })
1980
2468
 
1981
2469
  // A plural reference is declared exactly like a singular one, and the ppx puts
@@ -1997,57 +2485,72 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1997
2485
  // scalar one is here so a fix that reaches them by breaking it cannot pass.
1998
2486
  // The nested one rides alongside, named by its path rather than by the field
1999
2487
  // that holds it.
2000
- testSync("a command collects its array refs alongside its scalar one", () => {
2001
- let cmd = slice.commands->Array.getUnsafe(0)
2002
- expect(cmd.references->referencesOf)->toEqual([
2003
- ("customerId", "Customers"),
2004
- ("productIds", "AvailableProducts"),
2005
- ("warehouseIds", "Warehouses"),
2006
- ("lineItems[].productId", "AvailableProducts"),
2007
- ])
2008
- })
2488
+ testSync(
2489
+ "a command collects its array refs alongside its scalar one",
2490
+ () => {
2491
+ let cmd = slice.commands->Array.getUnsafe(0)
2492
+ expect(cmd.references->referencesOf)->toEqual([
2493
+ ("customerId", "Customers"),
2494
+ ("productIds", "AvailableProducts"),
2495
+ ("warehouseIds", "Warehouses"),
2496
+ ("lineItems[].productId", "AvailableProducts"),
2497
+ ])
2498
+ },
2499
+ )
2009
2500
 
2010
2501
  // A reference declared on a field of a record the command holds belongs to
2011
2502
  // *that* field, so it is named by its path: `[]` for "each element of", `.`
2012
2503
  // for "property of". Attributing it to `lineItems` would name a field that
2013
2504
  // declares nothing, and dropping it would leave the consuming side free to
2014
2505
  // guess an entity from the name.
2015
- testSync("a reference on a nested record's field is named by its path", () => {
2016
- let cmd = slice.commands->Array.getUnsafe(0)
2017
- expect(
2018
- cmd.references->Array.find(({fieldName}) => fieldName == "lineItems[].productId"),
2019
- )->toEqual(
2020
- Some({
2021
- Reventless.Plugin.fieldName: "lineItems[].productId",
2022
- entity: "AvailableProducts",
2023
- plugin: None,
2024
- }),
2025
- )
2026
- })
2506
+ testSync(
2507
+ "a reference on a nested record's field is named by its path",
2508
+ () => {
2509
+ let cmd = slice.commands->Array.getUnsafe(0)
2510
+ expect(
2511
+ cmd.references->Array.find(({fieldName}) => fieldName == "lineItems[].productId"),
2512
+ )->toEqual(
2513
+ Some({
2514
+ Reventless.Plugin.fieldName: "lineItems[].productId",
2515
+ entity: "AvailableProducts",
2516
+ plugin: None,
2517
+ }),
2518
+ )
2519
+ },
2520
+ )
2027
2521
 
2028
2522
  // The other half of the path contract: a flat field keeps its bare name, so
2029
2523
  // nothing reading today's paths has to learn the new form to go on working.
2030
- testSync("a flat reference in the same variant keeps its bare name", () => {
2031
- let cmd = slice.commands->Array.getUnsafe(0)
2032
- expect(cmd.references->Array.some(({fieldName}) => fieldName == "customerId"))->toBe(true)
2033
- })
2524
+ testSync(
2525
+ "a flat reference in the same variant keeps its bare name",
2526
+ () => {
2527
+ let cmd = slice.commands->Array.getUnsafe(0)
2528
+ expect(cmd.references->Array.some(({fieldName}) => fieldName == "customerId"))->toBe(true)
2529
+ },
2530
+ )
2034
2531
 
2035
2532
  // Optionality is a statement about presence. An optional array puts the
2036
2533
  // element two wrappers down, which is the shape most likely to be missed by
2037
2534
  // an unwrap that only handles one.
2038
- testSync("an optional array ref is collected under the field's own name", () => {
2039
- let cmd = slice.commands->Array.getUnsafe(0)
2040
- expect(
2041
- cmd.references->Array.find(({fieldName}) => fieldName == "warehouseIds"),
2042
- )->toEqual(Some({Reventless.Plugin.fieldName: "warehouseIds", entity: "Warehouses", plugin: None}))
2043
- })
2535
+ testSync(
2536
+ "an optional array ref is collected under the field's own name",
2537
+ () => {
2538
+ let cmd = slice.commands->Array.getUnsafe(0)
2539
+ expect(cmd.references->Array.find(({fieldName}) => fieldName == "warehouseIds"))->toEqual(
2540
+ Some({Reventless.Plugin.fieldName: "warehouseIds", entity: "Warehouses", plugin: None}),
2541
+ )
2542
+ },
2543
+ )
2044
2544
 
2045
2545
  // The event walk is a separate function over the same field dicts. Left to
2046
2546
  // itself it would be fixed by accident or not at all.
2047
- testSync("an event collects its array ref too", () => {
2048
- let evt = slice.events->Array.getUnsafe(0)
2049
- expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
2050
- })
2547
+ testSync(
2548
+ "an event collects its array ref too",
2549
+ () => {
2550
+ let evt = slice.events->Array.getUnsafe(0)
2551
+ expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
2552
+ },
2553
+ )
2051
2554
 
2052
2555
  // A client assembling this mutation declares a variable per argument, and
2053
2556
  // for `lineItems` it needs the *element* input type by name. The rendered
@@ -2057,28 +2560,34 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
2057
2560
  //
2058
2561
  // The name is composed from the mutation field by a server-side convention,
2059
2562
  // so nothing downstream of the JSON Schema could reconstruct it.
2060
- testSync("a nested input object publishes the type name the SDL declares", () => {
2061
- let cmd = slice.commands->Array.getUnsafe(0)
2062
- let itemName =
2063
- cmd.schema
2064
- ->JSON.parseOrThrow
2065
- ->getPropertyOf("lineItems")
2066
- ->Option.flatMap(s => getProperty(s, "items"))
2067
- ->Option.flatMap(s => getProperty(s, "x-reventless-graphql-input"))
2068
- ->Option.flatMap(JSON.Decode.string)
2069
- expect(itemName)->toEqual(Some(cmd.mutationField ++ "LineItems"))
2070
- })
2563
+ testSync(
2564
+ "a nested input object publishes the type name the SDL declares",
2565
+ () => {
2566
+ let cmd = slice.commands->Array.getUnsafe(0)
2567
+ let itemName =
2568
+ cmd.schema
2569
+ ->JSON.parseOrThrow
2570
+ ->getPropertyOf("lineItems")
2571
+ ->Option.flatMap(s => getProperty(s, "items"))
2572
+ ->Option.flatMap(s => getProperty(s, "x-reventless-graphql-input"))
2573
+ ->Option.flatMap(JSON.Decode.string)
2574
+ expect(itemName)->toEqual(Some(cmd.mutationField ++ "LineItems"))
2575
+ },
2576
+ )
2071
2577
 
2072
2578
  // The declared entity, not the one `productIds` reads like. Before the
2073
2579
  // element unwrap this field resolved by heuristic to `Products`.
2074
- testSync("the declared entity wins over the one the field name suggests", () => {
2075
- let cmd = slice.commands->Array.getUnsafe(0)
2076
- expect(
2077
- cmd.references
2078
- ->Array.find(({fieldName}) => fieldName == "productIds")
2079
- ->Option.map(({entity}) => entity),
2080
- )->toEqual(Some("AvailableProducts"))
2081
- })
2580
+ testSync(
2581
+ "the declared entity wins over the one the field name suggests",
2582
+ () => {
2583
+ let cmd = slice.commands->Array.getUnsafe(0)
2584
+ expect(
2585
+ cmd.references
2586
+ ->Array.find(({fieldName}) => fieldName == "productIds")
2587
+ ->Option.map(({entity}) => entity),
2588
+ )->toEqual(Some("AvailableProducts"))
2589
+ },
2590
+ )
2082
2591
  })
2083
2592
  })
2084
2593
 
@@ -2099,30 +2608,29 @@ describe("the port's translation table", () => {
2099
2608
 
2100
2609
  testSync("accepts a many-to-one declaration", () =>
2101
2610
  expect(
2102
- failures(~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])])->Array.length,
2611
+ failures(
2612
+ ~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])],
2613
+ )->Array.length,
2103
2614
  )->toBe(0)
2104
2615
  )
2105
2616
 
2106
2617
  testSync("rejects a published name the extension point does not declare", () =>
2107
- expect(
2108
- failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length,
2109
- )->toBe(1)
2618
+ expect(failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length)->toBe(1)
2110
2619
  )
2111
2620
 
2112
2621
  testSync("rejects a source name the delegate does not declare", () =>
2113
- expect(
2114
- failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length,
2115
- )->toBe(1)
2622
+ expect(failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length)->toBe(1)
2116
2623
  )
2117
-
2118
2624
  })
2119
2625
 
2120
2626
  describe("the translation table against the probe", () => {
2121
2627
  let declared = [
2122
- ({
2123
- ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
2124
- fromEventTypes: ["Archived", "Discontinued"],
2125
- }: ReventlessInfra.ExtensionPointMapping.publishedEvent),
2628
+ (
2629
+ {
2630
+ ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
2631
+ fromEventTypes: ["Archived", "Discontinued"],
2632
+ }: ReventlessInfra.ExtensionPointMapping.publishedEvent
2633
+ ),
2126
2634
  ]
2127
2635
  let drift = (~observed, ~followed) =>
2128
2636
  Plugin_Structure.translationTableDrift(~label="EP", ~declared, ~observed, ~followed)
@@ -2137,10 +2645,7 @@ describe("the translation table against the probe", () => {
2137
2645
 
2138
2646
  // The probe watched the arm publish it, so the declaration is stale.
2139
2647
  testSync("fails on an edge the probe saw and the declaration omits", () => {
2140
- let (failures, _) = drift(
2141
- ~observed=[("Archived", "ProductRelisted")],
2142
- ~followed=["Archived"],
2143
- )
2648
+ let (failures, _) = drift(~observed=[("Archived", "ProductRelisted")], ~followed=["Archived"])
2144
2649
  expect(failures->Array.length)->toBe(1)
2145
2650
  })
2146
2651
 
@@ -2185,9 +2690,7 @@ describe("the subscriber's half of the table", () => {
2185
2690
  )
2186
2691
 
2187
2692
  testSync("rejects a command neither side declares", () =>
2188
- expect(
2189
- failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length,
2190
- )->toBe(1)
2693
+ expect(failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length)->toBe(1)
2191
2694
  )
2192
2695
  })
2193
2696