@reventlessdev/reventless-core 3.0.0-alpha.260 → 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 (203) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/package.json +9 -9
  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 +38 -51
  33. package/src/components/Api/GraphQL_SchemaInspector.res +5 -3
  34. package/src/components/Api/GraphQL_Stitcher.res +9 -10
  35. package/src/components/Api/MCP_SchemaGenerator.res +8 -12
  36. package/src/components/Api/OwnerScopeDiagnostics.res +1 -2
  37. package/src/components/Api/QueryDbListQuery.res +13 -6
  38. package/src/components/Api/SchemaType.res +28 -23
  39. package/src/components/Api/SuryToJsonSchema.res +9 -10
  40. package/src/components/AutomationSlice/AutomationSlice.res +4 -4
  41. package/src/components/AutomationSlice/AutomationSlice_Builder.res +15 -14
  42. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -3
  43. package/src/components/Builder_Helpers.res +12 -23
  44. package/src/components/CommandGenerator/CommandGenerator.res +6 -1
  45. package/src/components/CommandGenerator/CommandGenerator_Builder.res +29 -21
  46. package/src/components/CommandGenerator/CommandGenerator_Callback.res +17 -21
  47. package/src/components/CommandTopic/CommandTopic.res +3 -3
  48. package/src/components/CommandTopic/CommandTopic_Builder.res +10 -10
  49. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -4
  50. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -2
  51. package/src/components/Counter/Counter_Builder.res +1 -6
  52. package/src/components/Counter/Counter_Callback.res +3 -3
  53. package/src/components/Dcb/Dcb_Builder.res +970 -956
  54. package/src/components/DcbEventLog/DcbEventLog.res +4 -4
  55. package/src/components/DcbEventLog/DcbEventLog_Builder.res +0 -1
  56. package/src/components/DcbEventLog/DcbEventLog_Operations.res +17 -18
  57. package/src/components/EventCollector/EventCollector_Builder.res +2 -1
  58. package/src/components/EventLog/EventLog.res +5 -1
  59. package/src/components/EventLog/EventLog_Operations.res +12 -8
  60. package/src/components/EventMapper/EventMapper.res +8 -10
  61. package/src/components/EventMapper/EventMapper_Builder.res +4 -1
  62. package/src/components/EventMapper/EventMapper_Callback.res +46 -31
  63. package/src/components/EventTopic/EventTopic.res +3 -3
  64. package/src/components/EventTopic/EventTopic_Builder.res +1 -1
  65. package/src/components/Extension/Extension.res +4 -1
  66. package/src/components/Extension/Extension_Operations.res +11 -3
  67. package/src/components/ExtensionPoint/ExtensionPoint.res +3 -3
  68. package/src/components/ExtensionPoint/ExtensionPoint_Builder.res +9 -10
  69. package/src/components/ExtensionPoint/ExtensionPoint_Callback.res +2 -6
  70. package/src/components/ExtensionPoint/ExtensionPoint_Operations.res +13 -5
  71. package/src/components/Heartbeat/Heartbeat_Callback.res +3 -3
  72. package/src/components/InboundTranslationSlice/InboundTranslationSlice.res +4 -4
  73. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Builder.res +7 -10
  74. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice.res +4 -4
  75. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Builder.res +15 -14
  76. package/src/components/QueryDb/QueryDb_Adapter.res +1 -3
  77. package/src/components/QueryDb/QueryDb_Operations.res +10 -10
  78. package/src/components/ReadModel/ReadModel.res +3 -3
  79. package/src/components/ReadModel/ReadModel_Builder.res +1 -2
  80. package/src/components/ReadModel/ReadModel_Callback.res +2 -1
  81. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +5 -2
  82. package/src/components/StateChangeSlice/StateChangeSlice.res +3 -3
  83. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +23 -4
  84. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +22 -6
  85. package/src/components/StateTopic.res +1 -3
  86. package/src/components/StateViewSlice/StateViewSlice.res +4 -4
  87. package/src/components/StateViewSlice/StateViewSlice_Builder.res +79 -68
  88. package/src/components/StateViewSlice/StateViewSlice_Callback.res +3 -1
  89. package/src/components/Task/Task.res +3 -3
  90. package/src/components/Task/Task_Adapter.res +4 -1
  91. package/src/components/Task/Task_Builder.res +24 -21
  92. package/src/plugin/api/PluginBaseFragment.res +9 -3
  93. package/src/plugin/component/Capability_Inference.res +8 -10
  94. package/src/plugin/component/Plugin.res +6 -6
  95. package/src/plugin/component/Plugin_Builder.res +54 -50
  96. package/src/plugin/component/Plugin_Callback.res +10 -6
  97. package/src/plugin/component/Plugin_Helpers.res +264 -230
  98. package/src/plugin/component/Plugin_Structure.res +370 -324
  99. package/src/plugin/component/Plugin_SubscriptionSchema.res +8 -6
  100. package/src/plugin/component/SchemaWalker.res +11 -12
  101. package/src/plugin/connect/PluginConnectExtension_Mapping.res +45 -50
  102. package/src/plugin/lifecycle/PluginBehavior.res +3 -4
  103. package/src/plugin/lifecycle/PluginSpec.res +1 -2
  104. package/src/plugin/lifecycle/PluginsProjection.res +16 -5
  105. package/src/plugin/lifecycle/PluginsReadModelSpec.res +0 -2
  106. package/src/util/CommandPublisher.res +9 -2
  107. package/src/util/CsvStream.res +3 -3
  108. package/src/util/Interstack.res +7 -15
  109. package/src/util/Interstack.resi +3 -3
  110. package/src/util/LogFormat.res +5 -9
  111. package/src/util/Logger.res +1 -2
  112. package/src/util/Lru.res +2 -1
  113. package/src/util/Lru.resi +2 -3
  114. package/src/util/ScheduleOps.res +24 -27
  115. package/src/util/StorePrefixCollision.res +6 -9
  116. package/src/util/Util_Promise.res +4 -1
  117. package/src/util/Util_Promise.res.mjs +1 -1
  118. package/tests/IdentityTest.res +166 -113
  119. package/tests/ResourceAttributionTest.res +28 -25
  120. package/tests/StorePrefixCollisionTest.res +2 -1
  121. package/tests/TaskBucketNamingTest.res +3 -2
  122. package/tests/adapter/EventLogProvisioningTest.res +51 -48
  123. package/tests/adapter/MonitoringTest.res +81 -38
  124. package/tests/adapter/QueryInterceptionTest.res +2 -2
  125. package/tests/adapter/RefusalVocabularyTest.res +31 -23
  126. package/tests/adapter/RuntimeExtensionHookReachTest.res +9 -8
  127. package/tests/adapter/RuntimeExtensionTest.res +5 -3
  128. package/tests/admin/AdminApiSchemaDriftTest.res +22 -16
  129. package/tests/admin/Platform_Admin_StructureTest.res +10 -11
  130. package/tests/admin/Platform_BakedManifestTest.res +31 -35
  131. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +18 -15
  132. package/tests/admin/Platform_PluginStructuresApiTest.res +5 -3
  133. package/tests/admin/Platform_UIFragmentsApiTest.res +6 -3
  134. package/tests/aggregate/AggregateCacheTest.res +10 -12
  135. package/tests/aggregate/AggregateCallbackTest.res +75 -46
  136. package/tests/aggregate/AggregateCausationTest.res +19 -20
  137. package/tests/aggregate/AggregateConflictTest.res +41 -19
  138. package/tests/aggregate/AggregateFixtures.res +15 -5
  139. package/tests/aggregate/AggregateRejectionTest.res +97 -80
  140. package/tests/aggregate/AggregateSnapshotTest.res +44 -16
  141. package/tests/api/Api_IdsTest.res +10 -4
  142. package/tests/api/BrandedMarkerCompositionTest.res +48 -36
  143. package/tests/api/GeolocationSemanticTest.res +3 -3
  144. package/tests/api/GraphQL_FragmentGeneratorTest.res +66 -54
  145. package/tests/api/OwnerScopeDiagnosticsTest.res +31 -27
  146. package/tests/api/StorageRefFieldsTest.res +30 -18
  147. package/tests/api/SuryToJsonSchemaTest.res +1315 -1014
  148. package/tests/api/TaggedUnionSchemaTest.res +17 -24
  149. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +235 -179
  150. package/tests/commandgenerator/CommandGeneratorFixtures.res +8 -13
  151. package/tests/commandgenerator/OwnerStampingTest.res +91 -76
  152. package/tests/commandtopic/CommandOutcomeHookTest.res +198 -155
  153. package/tests/commandtopic/CommandTopicCallbackFixtures.res +5 -4
  154. package/tests/commandtopic/CommandTopicCallbackTest.res +61 -43
  155. package/tests/commandtopic/CommandTopicHelpersRejectionTest.res +70 -55
  156. package/tests/counter/CounterCallbackTest.res +40 -34
  157. package/tests/counter/CounterFixtures.res +4 -4
  158. package/tests/dcb/DcbDecodeTest.res +115 -84
  159. package/tests/dcb/DcbEventLogOperationsTest.res +194 -143
  160. package/tests/dcb/DcbEventLogSchemaTest.res +13 -13
  161. package/tests/dcb/DcbFixtures.res +4 -5
  162. package/tests/dcb/DcbScopeInferenceTest.res +173 -114
  163. package/tests/dcb/DcbStateChangeSliceTest.res +391 -262
  164. package/tests/dcb/DcbTagTest.res +22 -11
  165. package/tests/dcb/DcbValidationTest.res +288 -218
  166. package/tests/eventlog/EventLogFixtures.res +4 -4
  167. package/tests/eventlog/EventLogOperationsTest.res +102 -67
  168. package/tests/eventlog/EventLogRetryTest.res +104 -65
  169. package/tests/eventmapper/EventMapperCallbackTest.res +118 -83
  170. package/tests/eventmapper/EventMapperFixtures.res +15 -23
  171. package/tests/eventmapper/EventMapperTest.res +54 -38
  172. package/tests/eventtopic/EventTopicFixtures.res +3 -4
  173. package/tests/eventtopic/EventTopicOperationsTest.res +41 -29
  174. package/tests/extensionpoint/ExtensionPointCallbackTest.res +58 -45
  175. package/tests/extensionpoint/ExtensionPointFixtures.res +16 -20
  176. package/tests/extensionpoint/ExtensionPointOperationsTest.res +70 -49
  177. package/tests/extensionpoint/PluginDisconnectScheduleTest.res +2 -1
  178. package/tests/logger/LogFormatTest.res +104 -82
  179. package/tests/logger/LoggerLazyTest.res +21 -12
  180. package/tests/message/MessageTest.res +96 -93
  181. package/tests/message/MetaEnvelopeTest.res +26 -30
  182. package/tests/plugin/HeartbeatDisconnectGraceTest.res +31 -25
  183. package/tests/plugin/ManifestVisibilityTest.res +20 -12
  184. package/tests/plugin/PluginDefinitionScalars.res +1 -2
  185. package/tests/plugin/PluginLifecycleCorpusTest.res +15 -12
  186. package/tests/plugin/PluginSpecExperiment.res +0 -1
  187. package/tests/plugin/PluginStructureTest.res +1529 -1045
  188. package/tests/plugin/Plugin_EventQuerySchemaTest.res +21 -10
  189. package/tests/plugin/SchemaWalkerTest.res +116 -70
  190. package/tests/plugin/StateChange/PsReserveStock.res +4 -4
  191. package/tests/plugin/StateView/PsAnnotatedView.res +4 -4
  192. package/tests/plugin/StateView/PsCustomersView.res +4 -4
  193. package/tests/plugin/StateView/PsShipmentsView.res +3 -2
  194. package/tests/projection/LifecycleTrailTest.res +1 -4
  195. package/tests/projection/ProjectionOptimizeTest.res +13 -9
  196. package/tests/querydb/QueryDbFixtures.res +0 -1
  197. package/tests/querydb/QueryDbOperationsTest.res +99 -64
  198. package/tests/querydb/TaggedUnionStampTest.res +10 -8
  199. package/tests/sideeffecthandler/SideEffectHandlerCallbackTest.res +50 -35
  200. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +4 -10
  201. package/tests/util/CommandPublisherTest.res +0 -1
  202. package/tests/util/CsvStreamTest.res +3 -3
  203. 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,126 +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"])
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"]),
1459
1812
  )
1460
1813
 
1461
1814
  // JSON Schema's own keyword rather than an `x-reventless-` one, so a client
1462
1815
  // that knows nothing about Reventless reads it.
1463
- testSync("@default flows through the PPX to JSON Schema's `default`", () => {
1464
- expect(
1465
- annotatedSchema
1466
- ->getPropertyOf("quantity")
1467
- ->Option.flatMap(s => getProperty(s, "default")),
1468
- )->toEqual(Some(JSON.Encode.int(1)))
1469
- })
1470
-
1471
- testSync("@default leaves the field's own shape alone", () => {
1472
- expect(
1473
- annotatedSchema
1474
- ->getPropertyOf("quantity")
1475
- ->Option.flatMap(s => getProperty(s, "type"))
1476
- ->Option.flatMap(JSON.Decode.string),
1477
- )->toBe(Some("integer"))
1478
- })
1479
-
1480
- testSync("@metric flows through the PPX to x-reventless-metric {aggregate,label}", () => {
1481
- let metricObj =
1482
- annotatedSchema
1483
- ->getPropertyOf("total")
1484
- ->Option.flatMap(s => getProperty(s, "x-reventless-metric"))
1485
- expect((
1486
- metricObj->Option.flatMap(m => getProperty(m, "aggregate"))->Option.flatMap(JSON.Decode.string),
1487
- metricObj->Option.flatMap(m => getProperty(m, "label"))->Option.flatMap(JSON.Decode.string),
1488
- ))->toEqual((Some("sum"), Some("Revenue")))
1489
- })
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
+ )
1490
1826
 
1491
- testSync("unannotated field 'name' has no x-reventless-* keys", () => {
1492
- let nameField = annotatedSchema->getPropertyOf("name")
1493
- expect((
1494
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
1495
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-subId")),
1496
- nameField->Option.flatMap(s => getProperty(s, "x-reventless-index")),
1497
- ))->toEqual((None, None, None))
1498
- })
1499
-
1500
- testSync("unannotated state-view slice (Orders) emits no x-reventless-* keys", () => {
1501
- let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1502
- let orderIdField = ordersSchema->getPropertyOf("orderId")
1503
- expect(
1504
- orderIdField->Option.flatMap(s => getProperty(s, "x-reventless-id")),
1505
- )->toBe(None)
1506
- })
1507
-
1508
- testSync("@live(false) flows through the PPX to top-level x-reventless-live", () => {
1509
- expect(
1510
- getProperty(annotatedSchema, "x-reventless-live")->Option.flatMap(JSON.Decode.bool),
1511
- )->toBe(Some(false))
1512
- })
1513
-
1514
- testSync("unannotated state-view slice (Orders) has no top-level x-reventless-live", () => {
1515
- let ordersSchema = structure.stateViewSlices->Array.getUnsafe(0)->parseSchema
1516
- expect(getProperty(ordersSchema, "x-reventless-live"))->toBe(None)
1517
- })
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
+ )
1518
1894
  })
1519
1895
 
1520
1896
  // The generator captures each component's chapter (source-folder grouping band)
@@ -1530,25 +1906,37 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1530
1906
  ~componentChapters=Dict.fromArray([("PlaceOrder", "Order"), ("Orders", "Order")]),
1531
1907
  )
1532
1908
 
1533
- testSync("a chaptered state-change slice carries Some(chapter)", () => {
1534
- let placeOrder = chaptered.stateChangeSlices->Array.getUnsafe(0)
1535
- expect(placeOrder.chapter)->toEqual(Some("Order"))
1536
- })
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
+ )
1537
1916
 
1538
- testSync("a chaptered state-view slice carries Some(chapter)", () => {
1539
- let orders = chaptered.stateViewSlices->Array.getUnsafe(0)
1540
- expect(orders.chapter)->toEqual(Some("Order"))
1541
- })
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
+ )
1542
1924
 
1543
- testSync("a component absent from the map carries None", () => {
1544
- let shipOrder = chaptered.stateChangeSlices->Array.getUnsafe(1)
1545
- expect(shipOrder.chapter)->toEqual(None)
1546
- })
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
+ )
1547
1932
 
1548
- testSync("no ~componentChapters → every def is chapterless (None)", () => {
1549
- let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
1550
- expect(placeOrder.chapter)->toEqual(None)
1551
- })
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
+ )
1552
1940
  })
1553
1941
 
1554
1942
  // A field typed as a storage ref states that the deployment needs that store
@@ -1561,120 +1949,152 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1561
1949
  )
1562
1950
  let stores = withStores.requiredStores->Option.getOr([])
1563
1951
 
1564
- testSync("an unqualified store resolves against the declaring plugin", () =>
1565
- 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),
1566
1955
  )
1567
1956
 
1568
- testSync("a qualified store keeps its own plugin", () =>
1569
- 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),
1570
1960
  )
1571
1961
 
1572
1962
  // `documents` is declared on both a command field and an event field.
1573
- testSync("a store declared by several fields is collected once", () =>
1574
- 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),
1575
1966
  )
1576
1967
 
1577
- testSync("only the declared stores are collected", () =>
1578
- expect(stores->Array.length)->toBe(2)
1579
- )
1968
+ testSync("only the declared stores are collected", () => expect(stores->Array.length)->toBe(2))
1580
1969
 
1581
- testSync("a plugin declaring no stores collects none — not None", () =>
1582
- expect(structure.requiredStores)->toEqual(Some([]))
1970
+ testSync(
1971
+ "a plugin declaring no stores collects none — not None",
1972
+ () => expect(structure.requiredStores)->toEqual(Some([])),
1583
1973
  )
1584
1974
 
1585
- testSync("each requirement keeps its declaring (component, field) sites", () => {
1586
- // `documents` is declared on a command field AND an event field of the
1587
- // same component under the same field name — one declaration site, so
1588
- // the identical triples collapse to one entry.
1589
- // `annotation` is the source text, not the key: the own-plugin store is
1590
- // written bare and the foreign one qualified, which is exactly the
1591
- // distinction no downstream comparison can recover.
1592
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1593
- {
1594
- store: "TestPlugin.documents",
1595
- component: "AttachInvoice",
1596
- field: "documentUrl",
1597
- annotation: Some("documents"),
1598
- },
1599
- {
1600
- store: "branding.logos",
1601
- component: "AttachInvoice",
1602
- field: "logoUrl",
1603
- annotation: Some("branding.logos"),
1604
- },
1605
- ]
1606
- expect(withStores.requiredStoreDeclarations)->toEqual(Some(expected))
1607
- })
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
+ )
1608
2001
 
1609
2002
  // The manifest is a rendering of the structure. Its key set must be
1610
2003
  // byte-identical to what the deployed plugin reports as `requiredStores`,
1611
2004
  // because the deploy-time coverage assertion compares against exactly that
1612
2005
  // — this equality is the whole contract of `capabilities.json`.
1613
- describe("capability manifest", () => {
1614
- let manifest = Reventless.CapabilityManifest.fromStructure(withStores)
1615
-
1616
- testSync("keys are byte-identical to requiredStores", () =>
1617
- expect(manifest.capabilities->Array.map(c => c.key))->toEqual(
1618
- 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
+ ),
1619
2017
  )
1620
- )
1621
-
1622
- testSync("each entry carries its provenance", () => {
1623
- let entry =
1624
- manifest.capabilities->Array.find(c => c.key == "TestPlugin.documents")->Option.getOrThrow
1625
- let expected: array<Reventless.CapabilityManifest.provenance> = [
1626
- {component: "AttachInvoice", field: "documentUrl", annotation: "documents"},
1627
- ]
1628
- expect(entry.declaredBy)->toEqual(expected)
1629
- })
1630
-
1631
- testSync("a structure declaring nothing yields an empty list, not a missing manifest", () =>
1632
- expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
1633
- )
1634
-
1635
- // A capability a slice declares rides the same manifest as the stores, so
1636
- // the platform generator reads one file. It carries no `field`: nothing
1637
- // annotated it, and naming one would be a fiction.
1638
- describe("declared capabilities", () => {
1639
- let withCapability = {
1640
- ...structure,
1641
- requiredCapabilities: Some([
1642
- ({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
1643
- {capability: "Geocoding", component: "GeocodeDropOff"},
1644
- ]),
1645
- traitDeclarations: None,
1646
- }
1647
- let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
1648
2018
 
1649
- testSync("two components needing one capability yield one entry", () =>
1650
- 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
+ },
1651
2031
  )
1652
2032
 
1653
- testSync("both declaring components are kept, with no field", () =>
1654
- expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
1655
- ({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
1656
- {component: "GeocodeDropOff"},
1657
- ])
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([]),
1658
2039
  )
1659
2040
 
1660
- // The generator downstream renders a real `Platform.capability` arm, and
1661
- // a name this build cannot map has no arm to render — so it is dropped
1662
- // here rather than passed through as an unrenderable key.
1663
- testSync("a capability this build does not know is dropped", () =>
1664
- expect(
1665
- 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 = {
1666
2048
  ...structure,
1667
- 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
+ ]),
1668
2058
  traitDeclarations: None,
1669
- }).capabilities,
1670
- )->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
+ },
1671
2091
  )
1672
- })
1673
2092
 
1674
- testSync("rendering is deterministic and newline-terminated", () => {
1675
- let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
1676
- expect(rendered)->toBe(
1677
- `{
2093
+ testSync(
2094
+ "rendering is deterministic and newline-terminated",
2095
+ () => {
2096
+ let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
2097
+ expect(rendered)->toBe(`{
1678
2098
  "capabilities": [
1679
2099
  {
1680
2100
  "kind": "ObjectStore",
@@ -1700,10 +2120,11 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1700
2120
  }
1701
2121
  ]
1702
2122
  }
1703
- `,
2123
+ `)
2124
+ },
1704
2125
  )
1705
- })
1706
- })
2126
+ },
2127
+ )
1707
2128
  })
1708
2129
 
1709
2130
  // A field typed uploadable declares its store by being named. The collection
@@ -1717,23 +2138,27 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1717
2138
  )
1718
2139
  let stores = withUploads.requiredStores->Option.getOr([])
1719
2140
 
1720
- testSync("the derived store is the field name, pluralised", () =>
1721
- 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),
1722
2144
  )
1723
2145
 
1724
- testSync("an optional field still declares its store", () =>
1725
- 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),
1726
2149
  )
1727
2150
 
1728
2151
  // The array marker lives on the element type. Reading the field schema
1729
2152
  // directly answers `None`, which is the same silence as never declaring —
1730
2153
  // so this asserts collection, not just that the fixture compiled.
1731
- testSync("an array field declares through its element", () =>
1732
- 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),
1733
2157
  )
1734
2158
 
1735
- testSync("an explicit @storageRef overrides the derived name", () =>
1736
- 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),
1737
2162
  )
1738
2163
 
1739
2164
  testSync("nothing else is collected", () => expect(stores->Array.length)->toBe(4))
@@ -1741,39 +2166,43 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1741
2166
  // The event field derives the same store as the command field of the same
1742
2167
  // name, so the two collapse to one declaration site — the behaviour the
1743
2168
  // annotation form already had, reached without an annotation.
1744
- testSync("each requirement keeps its declaring sites", () => {
1745
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1746
- {
1747
- store: "TestPlugin.categoryImages",
1748
- component: "UploadImages",
1749
- field: "categoryImage",
1750
- annotation: Some("categoryImages"),
1751
- },
1752
- {
1753
- store: "TestPlugin.datasheets",
1754
- component: "UploadImages",
1755
- field: "datasheets",
1756
- annotation: Some("datasheets"),
1757
- },
1758
- {
1759
- store: "TestPlugin.productImages",
1760
- component: "UploadImages",
1761
- field: "productImage",
1762
- annotation: Some("productImages"),
1763
- },
1764
- {
1765
- store: "branding.logos",
1766
- component: "UploadImages",
1767
- field: "logo",
1768
- annotation: Some("branding.logos"),
1769
- },
1770
- ]
1771
- expect(withUploads.requiredStoreDeclarations)->toEqual(Some(expected))
1772
- })
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
+ )
1773
2201
 
1774
2202
  let commandJson =
1775
- ((withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)).schema
1776
- ->JSON.parseOrThrow
2203
+ (
2204
+ (withUploads.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
2205
+ ).schema->JSON.parseOrThrow
1777
2206
 
1778
2207
  let semanticOf = field =>
1779
2208
  commandJson
@@ -1783,24 +2212,28 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1783
2212
 
1784
2213
  // The whole point of the family: one declaration states the content type
1785
2214
  // too, so the UI stops learning "this is an image" from the field's name.
1786
- testSync("the wire carries the content half, not just the store", () =>
1787
- 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")),
1788
2218
  )
1789
2219
 
1790
2220
  // The override replaces the *store*, not the type's semantic: a field
1791
2221
  // pointing at another plugin's store is still an image.
1792
- testSync("an overridden field keeps its uploadable semantic", () =>
1793
- expect(semanticOf("logo"))->toBe(Some("uploadableImage"))
2222
+ testSync(
2223
+ "an overridden field keeps its uploadable semantic",
2224
+ () => expect(semanticOf("logo"))->toBe(Some("uploadableImage")),
1794
2225
  )
1795
2226
 
1796
- testSync("the non-image arm carries its own id, on the element", () =>
1797
- expect(
1798
- commandJson
1799
- ->getPropertyOf("datasheets")
1800
- ->Option.flatMap(s => getProperty(s, "items"))
1801
- ->Option.flatMap(s => getProperty(s, "x-reventless-semantic"))
1802
- ->Option.flatMap(JSON.Decode.string),
1803
- )->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")),
1804
2237
  )
1805
2238
  })
1806
2239
 
@@ -1819,49 +2252,58 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1819
2252
  | _ => "(not a JS error)"
1820
2253
  }
1821
2254
 
1822
- testSync("two stores one transposition apart fail the build", () =>
1823
- expect(failureText() != "")->toBe(true)
2255
+ testSync(
2256
+ "two stores one transposition apart fail the build",
2257
+ () => expect(failureText() != "")->toBe(true),
1824
2258
  )
1825
2259
 
1826
- testSync("the failure names both stores and both fields", () => {
1827
- let text = failureText()
1828
- expect([
1829
- text->String.includes("TestPlugin.productImages"),
1830
- text->String.includes("TestPlugin.productImgaes"),
1831
- text->String.includes("TypoStore.productImage"),
1832
- text->String.includes("@storageRef"),
1833
- ])->toEqual([true, true, true, true])
1834
- })
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
+ )
1835
2272
 
1836
2273
  // Every legitimate pair in the other fixtures has to stay legitimate — a
1837
2274
  // check that fails on real designs is worse than the typo it catches.
1838
- testSync("the four stores of the uploadable fixture do not collide", () =>
1839
- expect(
1840
- Capability_Inference.collisions(
1841
- Plugin_Structure.make(
1842
- ~name="TestPlugin",
1843
- ~stateChangeSlices=[module(PsUploadImagesSlice)],
1844
- ).requiredStoreDeclarations->Option.getOr([]),
1845
- ),
1846
- )->toEqual([])
1847
- )
1848
-
1849
- testSync("one edit, transpositions included; two edits not", () =>
1850
- expect(
1851
- [
1852
- ("productImages", "productImgaes"),
1853
- ("productImages", "productImage"),
1854
- ("logos", "logo"),
1855
- ("documents", "logos"),
1856
- ("productImages", "productImgeas"),
1857
- ]->Array.map(((a, b)) => Capability_Inference.withinOneEdit(a, b)),
1858
- )->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]),
1859
2300
  )
1860
2301
 
1861
2302
  // Reached only through an explicit override: two *derived* names cannot
1862
2303
  // differ by number, because each is pluralised through its singular stem.
1863
- testSync("a singular/plural pair collides even at two edits apart", () =>
1864
- 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),
1865
2307
  )
1866
2308
  })
1867
2309
 
@@ -1877,62 +2319,75 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1877
2319
  ~stateChangeSlices=[module(PsUploadAvatarSlice)],
1878
2320
  )
1879
2321
 
1880
- testSync("an optional storage ref still requires its store", () =>
1881
- 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"])),
1882
2325
  )
1883
2326
 
1884
2327
  // Declared on the command and on the event under one field name — the same
1885
2328
  // collapse the required case makes, so optionality does not change arity.
1886
- testSync("and still carries one declaring site", () => {
1887
- let expected: array<Reventless.Plugin.requiredStoreDeclaration> = [
1888
- {
1889
- store: "TestPlugin.avatars",
1890
- component: "UploadAvatar",
1891
- field: "avatarUrl",
1892
- annotation: Some("avatars"),
1893
- },
1894
- ]
1895
- expect(withOptional.requiredStoreDeclarations)->toEqual(Some(expected))
1896
- })
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
+ )
1897
2343
 
1898
- testSync("an optional reference is still collected", () => {
1899
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1900
- let expected: array<Reventless.Plugin.fieldReference> = [
1901
- {fieldName: "referredBy", entity: "Customers", plugin: None},
1902
- ]
1903
- expect(cmd.references)->toEqual(expected)
1904
- })
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
+ )
1905
2354
 
1906
2355
  // What `requiredStores` knows, the wire has to carry. A reader deciding
1907
2356
  // which command fills a storage-ref field, or which store's endpoint an
1908
2357
  // upload goes to, has only the command schema to go on.
1909
- testSync("the command schema carries the field's storage-ref marker", () => {
1910
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1911
- let target =
1912
- cmd.schema
1913
- ->JSON.parseOrThrow
1914
- ->getPropertyOf("avatarUrl")
1915
- ->Option.flatMap(s => getProperty(s, "x-reventless-semantic-target"))
1916
- ->Option.flatMap(t => getProperty(t, "store"))
1917
- ->Option.flatMap(JSON.Decode.string)
1918
- expect(target)->toBe(Some("avatars"))
1919
- })
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
+ )
1920
2372
 
1921
2373
  // A form submits what a schema says is required. `avatarUrl?` is optional in
1922
2374
  // the spec, so listing it would make the picture mandatory in every UI that
1923
2375
  // renders the command — the failure this pairs with, since carrying the
1924
2376
  // marker is worth nothing if the field cannot be left empty.
1925
- testSync("an optional command argument is not required", () => {
1926
- let cmd = (withOptional.stateChangeSlices->Array.getUnsafe(0)).commands->Array.getUnsafe(0)
1927
- let required =
1928
- cmd.schema
1929
- ->JSON.parseOrThrow
1930
- ->getProperty("required")
1931
- ->Option.flatMap(JSON.Decode.array)
1932
- ->Option.getOr([])
1933
- ->Array.filterMap(JSON.Decode.string)
1934
- expect(required)->toEqual(["customerId"])
1935
- })
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
+ )
1936
2391
  })
1937
2392
 
1938
2393
  // Declarations are authoritative; the name heuristics are a lint. A
@@ -1949,52 +2404,66 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1949
2404
  PsChangePhoto.eventSchema->S.castToUnknown,
1950
2405
  )
1951
2406
 
1952
- testSync("a heuristic-only match produces a warning", () => {
1953
- let expected: array<Capability_Inference.warning> = [
1954
- {component: "ChangePhoto", field: "photoUrl"},
1955
- ]
1956
- expect(commandWarnings)->toEqual(expected)
1957
- })
1958
-
1959
- testSync("a declared field with a heuristic name does not warn", () => {
1960
- // The event's `thumbnail` matches the name heuristic but carries
1961
- // @storageRef — only the undeclared `photoUrl` warns.
1962
- let expected: array<Capability_Inference.warning> = [
1963
- {component: "ChangePhoto", field: "photoUrl"},
1964
- ]
1965
- expect(eventWarnings)->toEqual(expected)
1966
- })
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
+ )
1967
2416
 
1968
- testSync("a heuristic-only match reaches no manifest entry", () => {
1969
- let withHeuristicField = Plugin_Structure.make(
1970
- ~name="TestPlugin",
1971
- ~stateChangeSlices=[module(PsChangePhotoSlice)],
1972
- )
1973
- // Only the *declared* `thumbnail` store is collected; `photoUrl` — the
1974
- // heuristic-only match — provisions nothing.
1975
- expect(withHeuristicField.requiredStores)->toEqual(Some(["TestPlugin.productPhotos"]))
1976
- let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
1977
- expect(
1978
- manifest.capabilities->Array.every(entry =>
1979
- entry.declaredBy->Array.every(site => site.field != Some("photoUrl"))
1980
- ),
1981
- )->toBe(true)
1982
- })
1983
-
1984
- testSync("the warning names the field and the settling annotation", () => {
1985
- let text =
1986
- commandWarnings->Array.getUnsafe(0)->Capability_Inference.message
1987
- expect(text->String.includes("ChangePhoto.photoUrl"))->toBe(true)
1988
- expect(text->String.includes("@storageRef"))->toBe(true)
1989
- })
1990
-
1991
- testSync("suffix and exact name rules match; ordinary names do not", () => {
1992
- expect(
1993
- ["invoiceFileRef", "upload", "thumbnail", "customerName", "documentUrl"]->Array.map(
1994
- Capability_Inference.nameMatches,
1995
- ),
1996
- )->toEqual([true, true, true, false, false])
1997
- })
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
+ )
1998
2467
  })
1999
2468
 
2000
2469
  // A plural reference is declared exactly like a singular one, and the ppx puts
@@ -2016,57 +2485,72 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
2016
2485
  // scalar one is here so a fix that reaches them by breaking it cannot pass.
2017
2486
  // The nested one rides alongside, named by its path rather than by the field
2018
2487
  // that holds it.
2019
- testSync("a command collects its array refs alongside its scalar one", () => {
2020
- let cmd = slice.commands->Array.getUnsafe(0)
2021
- expect(cmd.references->referencesOf)->toEqual([
2022
- ("customerId", "Customers"),
2023
- ("productIds", "AvailableProducts"),
2024
- ("warehouseIds", "Warehouses"),
2025
- ("lineItems[].productId", "AvailableProducts"),
2026
- ])
2027
- })
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
+ )
2028
2500
 
2029
2501
  // A reference declared on a field of a record the command holds belongs to
2030
2502
  // *that* field, so it is named by its path: `[]` for "each element of", `.`
2031
2503
  // for "property of". Attributing it to `lineItems` would name a field that
2032
2504
  // declares nothing, and dropping it would leave the consuming side free to
2033
2505
  // guess an entity from the name.
2034
- testSync("a reference on a nested record's field is named by its path", () => {
2035
- let cmd = slice.commands->Array.getUnsafe(0)
2036
- expect(
2037
- cmd.references->Array.find(({fieldName}) => fieldName == "lineItems[].productId"),
2038
- )->toEqual(
2039
- Some({
2040
- Reventless.Plugin.fieldName: "lineItems[].productId",
2041
- entity: "AvailableProducts",
2042
- plugin: None,
2043
- }),
2044
- )
2045
- })
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
+ )
2046
2521
 
2047
2522
  // The other half of the path contract: a flat field keeps its bare name, so
2048
2523
  // nothing reading today's paths has to learn the new form to go on working.
2049
- testSync("a flat reference in the same variant keeps its bare name", () => {
2050
- let cmd = slice.commands->Array.getUnsafe(0)
2051
- expect(cmd.references->Array.some(({fieldName}) => fieldName == "customerId"))->toBe(true)
2052
- })
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
+ )
2053
2531
 
2054
2532
  // Optionality is a statement about presence. An optional array puts the
2055
2533
  // element two wrappers down, which is the shape most likely to be missed by
2056
2534
  // an unwrap that only handles one.
2057
- testSync("an optional array ref is collected under the field's own name", () => {
2058
- let cmd = slice.commands->Array.getUnsafe(0)
2059
- expect(
2060
- cmd.references->Array.find(({fieldName}) => fieldName == "warehouseIds"),
2061
- )->toEqual(Some({Reventless.Plugin.fieldName: "warehouseIds", entity: "Warehouses", plugin: None}))
2062
- })
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
+ )
2063
2544
 
2064
2545
  // The event walk is a separate function over the same field dicts. Left to
2065
2546
  // itself it would be fixed by accident or not at all.
2066
- testSync("an event collects its array ref too", () => {
2067
- let evt = slice.events->Array.getUnsafe(0)
2068
- expect(evt.references->referencesOf)->toEqual([("productIds", "AvailableProducts")])
2069
- })
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
+ )
2070
2554
 
2071
2555
  // A client assembling this mutation declares a variable per argument, and
2072
2556
  // for `lineItems` it needs the *element* input type by name. The rendered
@@ -2076,28 +2560,34 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
2076
2560
  //
2077
2561
  // The name is composed from the mutation field by a server-side convention,
2078
2562
  // so nothing downstream of the JSON Schema could reconstruct it.
2079
- testSync("a nested input object publishes the type name the SDL declares", () => {
2080
- let cmd = slice.commands->Array.getUnsafe(0)
2081
- let itemName =
2082
- cmd.schema
2083
- ->JSON.parseOrThrow
2084
- ->getPropertyOf("lineItems")
2085
- ->Option.flatMap(s => getProperty(s, "items"))
2086
- ->Option.flatMap(s => getProperty(s, "x-reventless-graphql-input"))
2087
- ->Option.flatMap(JSON.Decode.string)
2088
- expect(itemName)->toEqual(Some(cmd.mutationField ++ "LineItems"))
2089
- })
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
+ )
2090
2577
 
2091
2578
  // The declared entity, not the one `productIds` reads like. Before the
2092
2579
  // element unwrap this field resolved by heuristic to `Products`.
2093
- testSync("the declared entity wins over the one the field name suggests", () => {
2094
- let cmd = slice.commands->Array.getUnsafe(0)
2095
- expect(
2096
- cmd.references
2097
- ->Array.find(({fieldName}) => fieldName == "productIds")
2098
- ->Option.map(({entity}) => entity),
2099
- )->toEqual(Some("AvailableProducts"))
2100
- })
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
+ )
2101
2591
  })
2102
2592
  })
2103
2593
 
@@ -2118,30 +2608,29 @@ describe("the port's translation table", () => {
2118
2608
 
2119
2609
  testSync("accepts a many-to-one declaration", () =>
2120
2610
  expect(
2121
- failures(~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])])->Array.length,
2611
+ failures(
2612
+ ~declared=[published("ProductWithdrawn", ["Archived", "Discontinued"])],
2613
+ )->Array.length,
2122
2614
  )->toBe(0)
2123
2615
  )
2124
2616
 
2125
2617
  testSync("rejects a published name the extension point does not declare", () =>
2126
- expect(
2127
- failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length,
2128
- )->toBe(1)
2618
+ expect(failures(~declared=[published("ProductRetired", ["Archived"])])->Array.length)->toBe(1)
2129
2619
  )
2130
2620
 
2131
2621
  testSync("rejects a source name the delegate does not declare", () =>
2132
- expect(
2133
- failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length,
2134
- )->toBe(1)
2622
+ expect(failures(~declared=[published("ProductWithdrawn", ["Archivd"])])->Array.length)->toBe(1)
2135
2623
  )
2136
-
2137
2624
  })
2138
2625
 
2139
2626
  describe("the translation table against the probe", () => {
2140
2627
  let declared = [
2141
- ({
2142
- ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
2143
- fromEventTypes: ["Archived", "Discontinued"],
2144
- }: ReventlessInfra.ExtensionPointMapping.publishedEvent),
2628
+ (
2629
+ {
2630
+ ReventlessInfra.ExtensionPointMapping.name: "ProductWithdrawn",
2631
+ fromEventTypes: ["Archived", "Discontinued"],
2632
+ }: ReventlessInfra.ExtensionPointMapping.publishedEvent
2633
+ ),
2145
2634
  ]
2146
2635
  let drift = (~observed, ~followed) =>
2147
2636
  Plugin_Structure.translationTableDrift(~label="EP", ~declared, ~observed, ~followed)
@@ -2156,10 +2645,7 @@ describe("the translation table against the probe", () => {
2156
2645
 
2157
2646
  // The probe watched the arm publish it, so the declaration is stale.
2158
2647
  testSync("fails on an edge the probe saw and the declaration omits", () => {
2159
- let (failures, _) = drift(
2160
- ~observed=[("Archived", "ProductRelisted")],
2161
- ~followed=["Archived"],
2162
- )
2648
+ let (failures, _) = drift(~observed=[("Archived", "ProductRelisted")], ~followed=["Archived"])
2163
2649
  expect(failures->Array.length)->toBe(1)
2164
2650
  })
2165
2651
 
@@ -2204,9 +2690,7 @@ describe("the subscriber's half of the table", () => {
2204
2690
  )
2205
2691
 
2206
2692
  testSync("rejects a command neither side declares", () =>
2207
- expect(
2208
- failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length,
2209
- )->toBe(1)
2693
+ expect(failures([handled("ProductBecameAvailable", ["SyncNewProdct"])])->Array.length)->toBe(1)
2210
2694
  )
2211
2695
  })
2212
2696