@reventlessdev/reventless-core 3.0.0-alpha.251 → 3.0.0-alpha.253

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 (73) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +8 -8
  3. package/src/admin/Platform_Admin_Structure.res +4 -1
  4. package/src/admin/Platform_Admin_Structure.res.mjs +4 -2
  5. package/src/admin/Platform_BakedManifest.res.mjs +3 -1
  6. package/src/admin/Platform_PluginStructuresApi.res +43 -7
  7. package/src/admin/Platform_PluginStructuresApi.res.mjs +47 -1
  8. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +8 -0
  9. package/src/components/Api/ApiNoApiHelpers.res +37 -3
  10. package/src/components/Api/ApiNoApiHelpers.res.mjs +29 -2
  11. package/src/components/AutomationSlice/AutomationSlice_Callback.res +4 -4
  12. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +3 -3
  13. package/src/plugin/api/PluginBaseFragment.res +3 -3
  14. package/src/plugin/component/Plugin_Structure.res +158 -25
  15. package/src/plugin/component/Plugin_Structure.res.mjs +82 -18
  16. package/src/plugin/connect/PluginConnectExtension_Mapping.res.mjs +3 -1
  17. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +3 -1
  18. package/src/plugin/connect/PluginExtensionPoint_UiFragment.res.mjs +3 -1
  19. package/src/plugin/lifecycle/PluginBehavior.res +11 -11
  20. package/src/plugin/lifecycle/PluginBehavior.res.mjs +11 -11
  21. package/src/plugin/lifecycle/PluginSpec.res +39 -16
  22. package/src/plugin/lifecycle/PluginSpec.res.mjs +54 -56
  23. package/tests/admin/AdminApiSchemaDriftTest.res +27 -1
  24. package/tests/admin/AdminApiSchemaDriftTest.res.mjs +22 -2
  25. package/tests/admin/Platform_Admin_StructureTest.res +8 -3
  26. package/tests/admin/Platform_Admin_StructureTest.res.mjs +8 -2
  27. package/tests/admin/Platform_BakedManifestTest.res +2 -0
  28. package/tests/admin/Platform_BakedManifestTest.res.mjs +3 -1
  29. package/tests/admin/Platform_ComponentDefinitionsApiTest.res +4 -0
  30. package/tests/admin/Platform_ComponentDefinitionsApiTest.res.mjs +21 -7
  31. package/tests/admin/Platform_PluginStructuresApiTest.res +16 -0
  32. package/tests/admin/Platform_PluginStructuresApiTest.res.mjs +17 -3
  33. package/tests/aggregate/AggregateCacheTest.res.mjs +15 -3
  34. package/tests/aggregate/AggregateConflictTest.res.mjs +15 -3
  35. package/tests/aggregate/AggregateFixtures.res.mjs +15 -3
  36. package/tests/aggregate/AggregateSnapshotTest.res.mjs +21 -5
  37. package/tests/api/ApiNoApiTest.res +46 -0
  38. package/tests/api/ApiNoApiTest.res.mjs +49 -1
  39. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +27 -3
  40. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +12 -2
  41. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +2 -0
  42. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +12 -2
  43. package/tests/dcb/DcbFixtures.res.mjs +8 -0
  44. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +18 -0
  45. package/tests/eventmapper/EventMapperFixtures.res.mjs +15 -3
  46. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +9 -1
  47. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -1
  48. package/tests/message/MessageTest.res +2 -0
  49. package/tests/message/MessageTest.res.mjs +3 -1
  50. package/tests/plugin/PluginStructureAccessTest.res.mjs +2 -0
  51. package/tests/plugin/PluginStructureTest.res +191 -12
  52. package/tests/plugin/PluginStructureTest.res.mjs +246 -6
  53. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +8 -0
  54. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -0
  55. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res +17 -7
  56. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +12 -11
  57. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
  58. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
  59. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
  60. package/tests/plugin/StateChangeSlice/PsShipOrder.res +17 -13
  61. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +17 -11
  62. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +54 -0
  63. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +104 -0
  64. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
  65. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
  66. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
  67. package/tests/plugin/StateViewSlice/PsShipmentsView.res +1 -1
  68. package/tests/plugin/pluginDefinitionRequiredScalars.txt +18 -0
  69. package/tests/util/CommandPublisherTest.res.mjs +9 -1
  70. package/src/components/Api/ApiAllowedStatesHelpers.res +0 -18
  71. package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +0 -16
  72. package/src/components/Api/ApiTargetStateHelpers.res +0 -21
  73. package/src/components/Api/ApiTargetStateHelpers.res.mjs +0 -16
@@ -185,6 +185,19 @@ module PsDispatchShipmentSlice: ReventlessInfra.StateChangeSlice.T = {
185
185
  type component = scsComponent
186
186
  let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
187
187
  }
188
+ module PsTransitionSwitchSlice: ReventlessInfra.StateChangeSlice.T = {
189
+ module Spec = PsTransitionSwitch
190
+ module Behavior = {
191
+ type state = PsTransitionSwitch.state
192
+ let initialState = PsTransitionSwitch.initialState
193
+ let evolve = PsTransitionSwitch.evolve
194
+ let decide = PsTransitionSwitch.decide
195
+ let moduleUrl = PsTransitionSwitch.moduleUrl
196
+ }
197
+ let isAsync = false
198
+ type component = scsComponent
199
+ let make = (~dcbEventLog as _, ~publishJsons as _, ~tagKeysByEventType as _=?, ~crossPartitionTagKeys as _=?, ~runtime as _=?, ~opts as _=?): component => Obj.magic(0)
200
+ }
188
201
  module PsShipmentsViewSlice: ReventlessInfra.StateViewSlice.T = {
189
202
  module Spec = PsShipmentsView
190
203
  module Projection = {
@@ -290,12 +303,89 @@ describe("lifecycle topology", () => {
290
303
  })
291
304
  })
292
305
 
293
- describe("@transition cross-check", () => {
306
+ describe("commandTransition — the edge as a switch", () => {
307
+ // The seam a variant spread needs. The removed attribute lowered to a dict on
308
+ // the parent union keyed by variant name, and a spread splices members, so a
309
+ // command a host did not declare could not be annotated at all. A switch over
310
+ // the command reaches every constructor, spliced or not, and the compiler
311
+ // makes it exhaustive.
312
+ let structure = Plugin_Structure.make(
313
+ ~name="TransitionSwitchPlugin",
314
+ ~stateChangeSlices=[module(PsTransitionSwitchSlice)],
315
+ ~stateViewSlices=[],
316
+ )
317
+ let slice = structure.stateChangeSlices->Array.getUnsafe(0)
318
+ let byName = name => slice.commands->Array.find(c => c.name == name)
319
+
320
+ testSync("the switch fills allowedStates and targetState", () => {
321
+ expect((
322
+ byName("Book")->Option.flatMap(c => c.allowedStates),
323
+ byName("Book")->Option.flatMap(c => c.targetState),
324
+ ))->toEqual((Some(["Draft"]), Some("Booked")))
325
+ })
326
+
327
+ testSync("a guard declares its from-set and no target", () => {
328
+ // A positive claim rather than an omission: legal in these states, and it
329
+ // moves the row nowhere. Distinguishable on the wire from both fields being
330
+ // None, and consumers are expected to honour it.
331
+ expect((
332
+ byName("Rebook")->Option.flatMap(c => c.allowedStates),
333
+ byName("Rebook")->Option.flatMap(c => c.targetState),
334
+ ))->toEqual((Some(["Booked"]), None))
335
+ })
336
+
337
+ testSync("Unrestricted declares nothing, leaving the command unconstrained", () => {
338
+ // Distinct from a from-set naming every state, which says the same thing to
339
+ // a menu and something different to a reader.
340
+ expect((
341
+ byName("Abandon")->Option.flatMap(c => c.allowedStates),
342
+ byName("Abandon")->Option.flatMap(c => c.targetState),
343
+ ))->toEqual((None, None))
344
+ })
345
+ })
346
+
347
+ describe("traitDeclarations — a graft leaves a trace", () => {
348
+ // The one signal a graft leaves that survives into a deployed plugin. Every
349
+ // other one — the package dependency, the variant spread, the rules alias, the
350
+ // conformance binding — is source-side and invisible to a running estate.
351
+ let structure = Plugin_Structure.make(
352
+ ~name="TraitPlugin",
353
+ ~stateChangeSlices=[module(PsTransitionSwitchSlice), module(PsShipOrderSlice)],
354
+ ~stateViewSlices=[],
355
+ )
356
+
357
+ testSync("a declaring component's trait reaches the structure", () => {
358
+ expect(
359
+ structure.traitDeclarations->Option.getOr([])->Array.map(d => (d.trait, d.component)),
360
+ )->toEqual([("@reventlessdev/trait-booking", "TransitionSwitch")])
361
+ })
362
+
363
+ testSync("the component name is filled in by the structure, not declared", () => {
364
+ // The trait exports its identity and posture; it cannot know which component a
365
+ // host grafted it onto, and a host writing that name down would be the one
366
+ // hand-typed string this mechanism exists to avoid.
367
+ expect(
368
+ structure.traitDeclarations
369
+ ->Option.getOr([])
370
+ ->Array.map(d => (d.version, d.posture, d.component)),
371
+ )->toEqual([("9.9.9", "SelfContained", "TransitionSwitch")])
372
+ })
373
+
374
+ testSync("a component that is nobody's graft contributes nothing", () => {
375
+ // `PsShipOrder` declares no traits, and the injected default is empty — so the
376
+ // absence of a graft is silence rather than a row.
377
+ expect(structure.traitDeclarations->Option.getOr([])->Array.length)->toBe(1)
378
+ })
379
+ })
380
+
381
+ describe("declared-edge cross-check", () => {
294
382
  // The exit criterion: a state no linked view declares stops the build, and the
295
383
  // message says which command, which state, and what it was checked against.
296
384
  // `DispatchShipment` produces `ShipmentDispatched`, which the `Shipments` view
297
385
  // consumes — so the two are linked, the view's lifecycle is in hand, and
298
- // "Dispatchd" has somewhere to be wrong.
386
+ // "Dispatchd" has somewhere to be wrong. It compiles because the switch names
387
+ // the slice's OWN enum: the typechecker cannot see that the wrong lifecycle
388
+ // was picked, and this is the check that can.
299
389
  let buildBad = () =>
300
390
  try {
301
391
  Plugin_Structure.make(
@@ -410,11 +500,10 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
410
500
  ))->toEqual((Some(true), Some(false)))
411
501
  })
412
502
 
413
- testSync("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
414
- // End-to-end: the reventless-ppx @transition annotation → markTargetState
415
- // metadata → ApiTargetStateHelpers.getTargetState → commandDef. The
416
- // un-annotated CancelShipment carries None (resolver falls back to
417
- // name-stem).
503
+ testSync("ShipOrder: the switch's target flows through to commandDef.targetState", () => {
504
+ // End-to-end: the spec's `commandTransition`, evaluated per constructor
505
+ // against a synthetic command → commandDef. `CancelShipment` answers
506
+ // `Unrestricted` and carries None (resolver falls back to name-stem).
418
507
  let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
419
508
  let byName = name => shipOrder.commands->Array.find(c => c.name == name)
420
509
  expect((
@@ -423,10 +512,10 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
423
512
  ))->toEqual((Some("Shipped"), None))
424
513
  })
425
514
 
426
- testSync("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
427
- // The other half of the same annotation, which the removed pair spelled
428
- // separately: one attribute now fills both fields, so a command that
429
- // declares a target cannot end up without the states it may run from.
515
+ testSync("ShipOrder: the switch's from-set flows through to commandDef.allowedStates", () => {
516
+ // The other half of the same declaration. An edge is one value, so a
517
+ // command that declares a target cannot end up without the states it may
518
+ // run from — the two fields are read off it together.
430
519
  let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
431
520
  let byName = name => shipOrder.commands->Array.find(c => c.name == name)
432
521
  expect((
@@ -1366,6 +1455,45 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1366
1455
  expect(Reventless.CapabilityManifest.fromStructure(structure).capabilities)->toEqual([])
1367
1456
  )
1368
1457
 
1458
+ // A capability a slice declares rides the same manifest as the stores, so
1459
+ // the platform generator reads one file. It carries no `field`: nothing
1460
+ // annotated it, and naming one would be a fiction.
1461
+ describe("declared capabilities", () => {
1462
+ let withCapability = {
1463
+ ...structure,
1464
+ requiredCapabilities: Some([
1465
+ ({capability: "Geocoding", component: "GeocodeAddress"}: Reventless.Plugin.requiredCapabilityDeclaration),
1466
+ {capability: "Geocoding", component: "GeocodeDropOff"},
1467
+ ]),
1468
+ traitDeclarations: None,
1469
+ }
1470
+ let entries = Reventless.CapabilityManifest.fromStructure(withCapability).capabilities
1471
+
1472
+ testSync("two components needing one capability yield one entry", () =>
1473
+ expect(entries->Array.map(e => e.key))->toEqual(["Geocoding"])
1474
+ )
1475
+
1476
+ testSync("both declaring components are kept, with no field", () =>
1477
+ expect((entries->Array.getUnsafe(0)).declaredBy)->toEqual([
1478
+ ({component: "GeocodeAddress"}: Reventless.CapabilityManifest.provenance),
1479
+ {component: "GeocodeDropOff"},
1480
+ ])
1481
+ )
1482
+
1483
+ // The generator downstream renders a real `Platform.capability` arm, and
1484
+ // a name this build cannot map has no arm to render — so it is dropped
1485
+ // here rather than passed through as an unrenderable key.
1486
+ testSync("a capability this build does not know is dropped", () =>
1487
+ expect(
1488
+ Reventless.CapabilityManifest.fromStructure({
1489
+ ...structure,
1490
+ requiredCapabilities: Some([{capability: "Telepathy", component: "Guess"}]),
1491
+ traitDeclarations: None,
1492
+ }).capabilities,
1493
+ )->toEqual([])
1494
+ )
1495
+ })
1496
+
1369
1497
  testSync("rendering is deterministic and newline-terminated", () => {
1370
1498
  let rendered = Reventless.CapabilityManifest.renderForStructure(withStores)
1371
1499
  expect(rendered)->toBe(
@@ -1671,7 +1799,7 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1671
1799
  let manifest = Reventless.CapabilityManifest.fromStructure(withHeuristicField)
1672
1800
  expect(
1673
1801
  manifest.capabilities->Array.every(entry =>
1674
- entry.declaredBy->Array.every(site => site.field != "photoUrl")
1802
+ entry.declaredBy->Array.every(site => site.field != Some("photoUrl"))
1675
1803
  ),
1676
1804
  )->toBe(true)
1677
1805
  })
@@ -1856,3 +1984,54 @@ describe("the subscriber's half of the table", () => {
1856
1984
  )->toBe(1)
1857
1985
  )
1858
1986
  })
1987
+
1988
+ // ── One command name, one handler ────────────────────────────────────────────
1989
+ //
1990
+ // A DCB plugin routes a command by its bare type name, so two slices declaring
1991
+ // one name have one handler between them. The runtime notices only when it goes
1992
+ // to stamp owner fields — by which point the deploy has succeeded and the losing
1993
+ // slice is silently unreachable — so the structure refuses it instead.
1994
+
1995
+ describe("duplicate DCB command names", () => {
1996
+ // The same slice twice is the smallest possible collision, and it needs no
1997
+ // fixture that exists only to collide.
1998
+ testSync("two slices declaring one command are refused", () =>
1999
+ expect(
2000
+ switch Plugin_Structure.make(
2001
+ ~name="TestPlugin",
2002
+ ~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
2003
+ ) {
2004
+ | _ => false
2005
+ | exception _ => true
2006
+ },
2007
+ )->toBe(true)
2008
+ )
2009
+
2010
+ testSync("the refusal names the command and both slices", () =>
2011
+ switch Plugin_Structure.make(
2012
+ ~name="TestPlugin",
2013
+ ~stateChangeSlices=[module(PsShipOrderSlice), module(PsShipOrderSlice)],
2014
+ ) {
2015
+ | _ => expect("no refusal")->toBe("a refusal naming the command")
2016
+ | exception JsExn(e) =>
2017
+ let message = e->JsExn.message->Option.getOr("")
2018
+ expect((
2019
+ message->String.includes("ShipOrder"),
2020
+ message->String.includes("routes a command by its bare name"),
2021
+ ))->toEqual((true, true))
2022
+ }
2023
+ )
2024
+
2025
+ // Distinct names are the ordinary case and must stay silent.
2026
+ testSync("distinct command names are fine", () =>
2027
+ expect(
2028
+ switch Plugin_Structure.make(
2029
+ ~name="TestPlugin",
2030
+ ~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
2031
+ ) {
2032
+ | _ => true
2033
+ | exception _ => false
2034
+ },
2035
+ )->toBe(true)
2036
+ )
2037
+ })
@@ -30,6 +30,7 @@ import * as PsShipmentsView$ReventlessCore from "./StateViewSlice/PsShipmentsVie
30
30
  import * as Plugin_Structure$ReventlessCore from "../../src/plugin/component/Plugin_Structure.res.mjs";
31
31
  import * as PsCategoriesView$ReventlessCore from "./StateViewSlice/PsCategoriesView.res.mjs";
32
32
  import * as PsDispatchShipment$ReventlessCore from "./StateChangeSlice/PsDispatchShipment.res.mjs";
33
+ import * as PsTransitionSwitch$ReventlessCore from "./StateChangeSlice/PsTransitionSwitch.res.mjs";
33
34
  import * as Capability_Inference$ReventlessCore from "../../src/plugin/component/Capability_Inference.res.mjs";
34
35
  import * as PsAvailableProductsView$ReventlessCore from "./StateViewSlice/PsAvailableProductsView.res.mjs";
35
36
 
@@ -53,6 +54,8 @@ let PsPlaceOrderSlice_Spec = {
53
54
  eventSchema: PsPlaceOrder$ReventlessCore.eventSchema,
54
55
  commandSchema: PsPlaceOrder$ReventlessCore.commandSchema,
55
56
  commandAuthorization: PsPlaceOrder$ReventlessCore.commandAuthorization,
57
+ commandTransition: PsPlaceOrder$ReventlessCore.commandTransition,
58
+ traits: PsPlaceOrder$ReventlessCore.traits,
56
59
  readConsistency: PsPlaceOrder$ReventlessCore.readConsistency
57
60
  };
58
61
 
@@ -83,6 +86,8 @@ let PsShipOrderSlice_Spec = {
83
86
  eventSchema: PsShipOrder$ReventlessCore.eventSchema,
84
87
  commandSchema: PsShipOrder$ReventlessCore.commandSchema,
85
88
  commandAuthorization: PsShipOrder$ReventlessCore.commandAuthorization,
89
+ commandTransition: PsShipOrder$ReventlessCore.commandTransition,
90
+ traits: PsShipOrder$ReventlessCore.traits,
86
91
  readConsistency: PsShipOrder$ReventlessCore.readConsistency
87
92
  };
88
93
 
@@ -243,6 +248,8 @@ let PsAttachInvoiceSlice_Spec = {
243
248
  eventSchema: PsAttachInvoice$ReventlessCore.eventSchema,
244
249
  commandSchema: PsAttachInvoice$ReventlessCore.commandSchema,
245
250
  commandAuthorization: PsAttachInvoice$ReventlessCore.commandAuthorization,
251
+ commandTransition: PsAttachInvoice$ReventlessCore.commandTransition,
252
+ traits: PsAttachInvoice$ReventlessCore.traits,
246
253
  readConsistency: PsAttachInvoice$ReventlessCore.readConsistency
247
254
  };
248
255
 
@@ -273,6 +280,8 @@ let PsUploadAvatarSlice_Spec = {
273
280
  eventSchema: PsUploadAvatar$ReventlessCore.eventSchema,
274
281
  commandSchema: PsUploadAvatar$ReventlessCore.commandSchema,
275
282
  commandAuthorization: PsUploadAvatar$ReventlessCore.commandAuthorization,
283
+ commandTransition: PsUploadAvatar$ReventlessCore.commandTransition,
284
+ traits: PsUploadAvatar$ReventlessCore.traits,
276
285
  readConsistency: PsUploadAvatar$ReventlessCore.readConsistency
277
286
  };
278
287
 
@@ -303,6 +312,8 @@ let PsUploadImagesSlice_Spec = {
303
312
  eventSchema: PsUploadImages$ReventlessCore.eventSchema,
304
313
  commandSchema: PsUploadImages$ReventlessCore.commandSchema,
305
314
  commandAuthorization: PsUploadImages$ReventlessCore.commandAuthorization,
315
+ commandTransition: PsUploadImages$ReventlessCore.commandTransition,
316
+ traits: PsUploadImages$ReventlessCore.traits,
306
317
  readConsistency: PsUploadImages$ReventlessCore.readConsistency
307
318
  };
308
319
 
@@ -333,6 +344,8 @@ let PsTypoStoreSlice_Spec = {
333
344
  eventSchema: PsTypoStore$ReventlessCore.eventSchema,
334
345
  commandSchema: PsTypoStore$ReventlessCore.commandSchema,
335
346
  commandAuthorization: PsTypoStore$ReventlessCore.commandAuthorization,
347
+ commandTransition: PsTypoStore$ReventlessCore.commandTransition,
348
+ traits: PsTypoStore$ReventlessCore.traits,
336
349
  readConsistency: PsTypoStore$ReventlessCore.readConsistency
337
350
  };
338
351
 
@@ -363,6 +376,8 @@ let PsChangePhotoSlice_Spec = {
363
376
  eventSchema: PsChangePhoto$ReventlessCore.eventSchema,
364
377
  commandSchema: PsChangePhoto$ReventlessCore.commandSchema,
365
378
  commandAuthorization: PsChangePhoto$ReventlessCore.commandAuthorization,
379
+ commandTransition: PsChangePhoto$ReventlessCore.commandTransition,
380
+ traits: PsChangePhoto$ReventlessCore.traits,
366
381
  readConsistency: PsChangePhoto$ReventlessCore.readConsistency
367
382
  };
368
383
 
@@ -393,6 +408,8 @@ let PsReserveStockSlice_Spec = {
393
408
  eventSchema: PsReserveStock$ReventlessCore.eventSchema,
394
409
  commandSchema: PsReserveStock$ReventlessCore.commandSchema,
395
410
  commandAuthorization: PsReserveStock$ReventlessCore.commandAuthorization,
411
+ commandTransition: PsReserveStock$ReventlessCore.commandTransition,
412
+ traits: PsReserveStock$ReventlessCore.traits,
396
413
  readConsistency: PsReserveStock$ReventlessCore.readConsistency
397
414
  };
398
415
 
@@ -423,6 +440,8 @@ let PsDispatchShipmentSlice_Spec = {
423
440
  eventSchema: PsDispatchShipment$ReventlessCore.eventSchema,
424
441
  commandSchema: PsDispatchShipment$ReventlessCore.commandSchema,
425
442
  commandAuthorization: PsDispatchShipment$ReventlessCore.commandAuthorization,
443
+ commandTransition: PsDispatchShipment$ReventlessCore.commandTransition,
444
+ traits: PsDispatchShipment$ReventlessCore.traits,
426
445
  readConsistency: PsDispatchShipment$ReventlessCore.readConsistency
427
446
  };
428
447
 
@@ -433,12 +452,44 @@ let PsDispatchShipmentSlice = {
433
452
  make: make$13
434
453
  };
435
454
 
455
+ let Behavior$9 = {
456
+ initialState: false,
457
+ evolve: PsTransitionSwitch$ReventlessCore.evolve,
458
+ decide: PsTransitionSwitch$ReventlessCore.decide,
459
+ moduleUrl: PsTransitionSwitch$ReventlessCore.moduleUrl
460
+ };
461
+
462
+ function make$14(param, param$1, param$2, param$3, param$4, param$5) {
463
+ return 0;
464
+ }
465
+
466
+ let PsTransitionSwitchSlice_Spec = {
467
+ name: PsTransitionSwitch$ReventlessCore.name,
468
+ moduleUrl: PsTransitionSwitch$ReventlessCore.moduleUrl,
469
+ Id: Id$Reventless.$$String,
470
+ consumedEventSchema: PsTransitionSwitch$ReventlessCore.consumedEventSchema,
471
+ errorSchema: PsTransitionSwitch$ReventlessCore.errorSchema,
472
+ eventSchema: PsTransitionSwitch$ReventlessCore.eventSchema,
473
+ commandSchema: PsTransitionSwitch$ReventlessCore.commandSchema,
474
+ commandAuthorization: PsTransitionSwitch$ReventlessCore.commandAuthorization,
475
+ commandTransition: PsTransitionSwitch$ReventlessCore.commandTransition,
476
+ traits: PsTransitionSwitch$ReventlessCore.traits,
477
+ readConsistency: PsTransitionSwitch$ReventlessCore.readConsistency
478
+ };
479
+
480
+ let PsTransitionSwitchSlice = {
481
+ Spec: PsTransitionSwitchSlice_Spec,
482
+ Behavior: Behavior$9,
483
+ isAsync: false,
484
+ make: make$14
485
+ };
486
+
436
487
  let Projection$5 = {
437
488
  project: PsShipmentsView$ReventlessCore.project,
438
489
  moduleUrl: PsShipmentsView$ReventlessCore.moduleUrl
439
490
  };
440
491
 
441
- function make$14(param, param$1, param$2) {
492
+ function make$15(param, param$1, param$2) {
442
493
  return 0;
443
494
  }
444
495
 
@@ -456,7 +507,7 @@ let PsShipmentsViewSlice_Spec = {
456
507
  let PsShipmentsViewSlice = {
457
508
  Spec: PsShipmentsViewSlice_Spec,
458
509
  Projection: Projection$5,
459
- make: make$14
510
+ make: make$15
460
511
  };
461
512
 
462
513
  let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [
@@ -529,7 +580,70 @@ globalThis.describe("lifecycle topology", () => {
529
580
  });
530
581
  });
531
582
 
532
- globalThis.describe("@transition cross-check", () => {
583
+ globalThis.describe("commandTransition — the edge as a switch", () => {
584
+ let structure = Plugin_Structure$ReventlessCore.make("TransitionSwitchPlugin", undefined, undefined, [], [PsTransitionSwitchSlice], undefined, undefined, undefined, undefined, undefined, undefined);
585
+ let slice = structure.stateChangeSlices[0];
586
+ let byName = name => slice.commands.find(c => c.name === name);
587
+ globalThis.test("the switch fills allowedStates and targetState", () => {
588
+ globalThis.expect([
589
+ Stdlib_Option.flatMap(byName("Book"), c => c.allowedStates),
590
+ Stdlib_Option.flatMap(byName("Book"), c => c.targetState)
591
+ ]).toEqual([
592
+ ["Draft"],
593
+ "Booked"
594
+ ]);
595
+ });
596
+ globalThis.test("a guard declares its from-set and no target", () => {
597
+ globalThis.expect([
598
+ Stdlib_Option.flatMap(byName("Rebook"), c => c.allowedStates),
599
+ Stdlib_Option.flatMap(byName("Rebook"), c => c.targetState)
600
+ ]).toEqual([
601
+ ["Booked"],
602
+ undefined
603
+ ]);
604
+ });
605
+ globalThis.test("Unrestricted declares nothing, leaving the command unconstrained", () => {
606
+ globalThis.expect([
607
+ Stdlib_Option.flatMap(byName("Abandon"), c => c.allowedStates),
608
+ Stdlib_Option.flatMap(byName("Abandon"), c => c.targetState)
609
+ ]).toEqual([
610
+ undefined,
611
+ undefined
612
+ ]);
613
+ });
614
+ });
615
+
616
+ globalThis.describe("traitDeclarations — a graft leaves a trace", () => {
617
+ let structure = Plugin_Structure$ReventlessCore.make("TraitPlugin", undefined, undefined, [], [
618
+ PsTransitionSwitchSlice,
619
+ PsShipOrderSlice
620
+ ], undefined, undefined, undefined, undefined, undefined, undefined);
621
+ globalThis.test("a declaring component's trait reaches the structure", () => {
622
+ globalThis.expect(Stdlib_Option.getOr(structure.traitDeclarations, []).map(d => [
623
+ d.trait,
624
+ d.component
625
+ ])).toEqual([[
626
+ "@reventlessdev/trait-booking",
627
+ "TransitionSwitch"
628
+ ]]);
629
+ });
630
+ globalThis.test("the component name is filled in by the structure, not declared", () => {
631
+ globalThis.expect(Stdlib_Option.getOr(structure.traitDeclarations, []).map(d => [
632
+ d.version,
633
+ d.posture,
634
+ d.component
635
+ ])).toEqual([[
636
+ "9.9.9",
637
+ "SelfContained",
638
+ "TransitionSwitch"
639
+ ]]);
640
+ });
641
+ globalThis.test("a component that is nobody's graft contributes nothing", () => {
642
+ globalThis.expect(Stdlib_Option.getOr(structure.traitDeclarations, []).length).toBe(1);
643
+ });
644
+ });
645
+
646
+ globalThis.describe("declared-edge cross-check", () => {
533
647
  let buildBad = () => {
534
648
  try {
535
649
  Plugin_Structure$ReventlessCore.make("TransitionPlugin", undefined, undefined, [PsShipmentsViewSlice], [PsDispatchShipmentSlice], undefined, undefined, undefined, undefined, undefined, undefined);
@@ -642,7 +756,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
642
756
  false
643
757
  ]);
644
758
  });
645
- globalThis.test("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
759
+ globalThis.test("ShipOrder: the switch's target flows through to commandDef.targetState", () => {
646
760
  let shipOrder = structure.stateChangeSlices[1];
647
761
  let byName = name => shipOrder.commands.find(c => c.name === name);
648
762
  globalThis.expect([
@@ -653,7 +767,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
653
767
  undefined
654
768
  ]);
655
769
  });
656
- globalThis.test("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
770
+ globalThis.test("ShipOrder: the switch's from-set flows through to commandDef.allowedStates", () => {
657
771
  let shipOrder = structure.stateChangeSlices[1];
658
772
  let byName = name => shipOrder.commands.find(c => c.name === name);
659
773
  globalThis.expect([
@@ -1448,6 +1562,78 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1448
1562
  globalThis.test("a structure declaring nothing yields an empty list, not a missing manifest", () => {
1449
1563
  globalThis.expect(CapabilityManifest$Reventless.fromStructure(structure).capabilities).toEqual([]);
1450
1564
  });
1565
+ globalThis.describe("declared capabilities", () => {
1566
+ let withCapability_readModels = structure.readModels;
1567
+ let withCapability_stateViewSlices = structure.stateViewSlices;
1568
+ let withCapability_stateChangeSlices = structure.stateChangeSlices;
1569
+ let withCapability_aggregates = structure.aggregates;
1570
+ let withCapability_automationSlices = structure.automationSlices;
1571
+ let withCapability_outboundTranslationSlices = structure.outboundTranslationSlices;
1572
+ let withCapability_inboundTranslationSlices = structure.inboundTranslationSlices;
1573
+ let withCapability_extensions = structure.extensions;
1574
+ let withCapability_extensionPoints = structure.extensionPoints;
1575
+ let withCapability_requiredStores = structure.requiredStores;
1576
+ let withCapability_requiredStoreDeclarations = structure.requiredStoreDeclarations;
1577
+ let withCapability_requiredCapabilities = [
1578
+ {
1579
+ capability: "Geocoding",
1580
+ component: "GeocodeAddress"
1581
+ },
1582
+ {
1583
+ capability: "Geocoding",
1584
+ component: "GeocodeDropOff"
1585
+ }
1586
+ ];
1587
+ let withCapability = {
1588
+ readModels: withCapability_readModels,
1589
+ stateViewSlices: withCapability_stateViewSlices,
1590
+ stateChangeSlices: withCapability_stateChangeSlices,
1591
+ aggregates: withCapability_aggregates,
1592
+ automationSlices: withCapability_automationSlices,
1593
+ outboundTranslationSlices: withCapability_outboundTranslationSlices,
1594
+ inboundTranslationSlices: withCapability_inboundTranslationSlices,
1595
+ extensions: withCapability_extensions,
1596
+ extensionPoints: withCapability_extensionPoints,
1597
+ requiredStores: withCapability_requiredStores,
1598
+ requiredStoreDeclarations: withCapability_requiredStoreDeclarations,
1599
+ requiredCapabilities: withCapability_requiredCapabilities,
1600
+ traitDeclarations: undefined
1601
+ };
1602
+ let entries = CapabilityManifest$Reventless.fromStructure(withCapability).capabilities;
1603
+ globalThis.test("two components needing one capability yield one entry", () => {
1604
+ globalThis.expect(entries.map(e => e.key)).toEqual(["Geocoding"]);
1605
+ });
1606
+ globalThis.test("both declaring components are kept, with no field", () => {
1607
+ globalThis.expect(entries[0].declaredBy).toEqual([
1608
+ {
1609
+ component: "GeocodeAddress"
1610
+ },
1611
+ {
1612
+ component: "GeocodeDropOff"
1613
+ }
1614
+ ]);
1615
+ });
1616
+ globalThis.test("a capability this build does not know is dropped", () => {
1617
+ globalThis.expect(CapabilityManifest$Reventless.fromStructure({
1618
+ readModels: structure.readModels,
1619
+ stateViewSlices: structure.stateViewSlices,
1620
+ stateChangeSlices: structure.stateChangeSlices,
1621
+ aggregates: structure.aggregates,
1622
+ automationSlices: structure.automationSlices,
1623
+ outboundTranslationSlices: structure.outboundTranslationSlices,
1624
+ inboundTranslationSlices: structure.inboundTranslationSlices,
1625
+ extensions: structure.extensions,
1626
+ extensionPoints: structure.extensionPoints,
1627
+ requiredStores: structure.requiredStores,
1628
+ requiredStoreDeclarations: structure.requiredStoreDeclarations,
1629
+ requiredCapabilities: [{
1630
+ capability: "Telepathy",
1631
+ component: "Guess"
1632
+ }],
1633
+ traitDeclarations: undefined
1634
+ }).capabilities).toEqual([]);
1635
+ });
1636
+ });
1451
1637
  globalThis.test("rendering is deterministic and newline-terminated", () => {
1452
1638
  let rendered = CapabilityManifest$Reventless.renderForStructure(withStores);
1453
1639
  globalThis.expect(rendered).toBe(`{
@@ -1662,7 +1848,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
1662
1848
  let withHeuristicField = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [PsChangePhotoSlice], undefined, undefined, undefined, undefined, undefined, undefined);
1663
1849
  globalThis.expect(withHeuristicField.requiredStores).toEqual(["TestPlugin.productPhotos"]);
1664
1850
  let manifest = CapabilityManifest$Reventless.fromStructure(withHeuristicField);
1665
- globalThis.expect(manifest.capabilities.every(entry => entry.declaredBy.every(site => site.field !== "photoUrl"))).toBe(true);
1851
+ globalThis.expect(manifest.capabilities.every(entry => entry.declaredBy.every(site => Primitive_object.notequal(site.field, "photoUrl")))).toBe(true);
1666
1852
  });
1667
1853
  globalThis.test("the warning names the field and the settling annotation", () => {
1668
1854
  let text = Capability_Inference$ReventlessCore.message(commandWarnings[0]);
@@ -1858,6 +2044,59 @@ globalThis.describe("the subscriber's half of the table", () => {
1858
2044
  });
1859
2045
  });
1860
2046
 
2047
+ globalThis.describe("duplicate DCB command names", () => {
2048
+ globalThis.test("two slices declaring one command are refused", () => {
2049
+ let tmp;
2050
+ try {
2051
+ Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
2052
+ PsShipOrderSlice,
2053
+ PsShipOrderSlice
2054
+ ], undefined, undefined, undefined, undefined, undefined, undefined);
2055
+ tmp = false;
2056
+ } catch (exn) {
2057
+ tmp = true;
2058
+ }
2059
+ globalThis.expect(tmp).toBe(true);
2060
+ });
2061
+ globalThis.test("the refusal names the command and both slices", () => {
2062
+ let val;
2063
+ try {
2064
+ val = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
2065
+ PsShipOrderSlice,
2066
+ PsShipOrderSlice
2067
+ ], undefined, undefined, undefined, undefined, undefined, undefined);
2068
+ } catch (raw_e) {
2069
+ let e = Primitive_exceptions.internalToException(raw_e);
2070
+ if (e.RE_EXN_ID === "JsExn") {
2071
+ let message = Stdlib_Option.getOr(Stdlib_JsExn.message(e._1), "");
2072
+ globalThis.expect([
2073
+ message.includes("ShipOrder"),
2074
+ message.includes("routes a command by its bare name")
2075
+ ]).toEqual([
2076
+ true,
2077
+ true
2078
+ ]);
2079
+ return;
2080
+ }
2081
+ throw e;
2082
+ }
2083
+ globalThis.expect("no refusal").toBe("a refusal naming the command");
2084
+ });
2085
+ globalThis.test("distinct command names are fine", () => {
2086
+ let tmp;
2087
+ try {
2088
+ Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, undefined, [
2089
+ PsPlaceOrderSlice,
2090
+ PsShipOrderSlice
2091
+ ], undefined, undefined, undefined, undefined, undefined, undefined);
2092
+ tmp = true;
2093
+ } catch (exn) {
2094
+ tmp = false;
2095
+ }
2096
+ globalThis.expect(tmp).toBe(true);
2097
+ });
2098
+ });
2099
+
1861
2100
  export {
1862
2101
  PsPlaceOrderSlice,
1863
2102
  PsShipOrderSlice,
@@ -1873,6 +2112,7 @@ export {
1873
2112
  PsChangePhotoSlice,
1874
2113
  PsReserveStockSlice,
1875
2114
  PsDispatchShipmentSlice,
2115
+ PsTransitionSwitchSlice,
1876
2116
  PsShipmentsViewSlice,
1877
2117
  structure,
1878
2118
  }
@@ -36,6 +36,12 @@ function commandAuthorization(param) {
36
36
  return "AllowAuthenticated";
37
37
  }
38
38
 
39
+ function commandTransition(param) {
40
+ return "Unrestricted";
41
+ }
42
+
43
+ let traits = [];
44
+
39
45
  let name = "AttachInvoice";
40
46
 
41
47
  let Id;
@@ -59,5 +65,7 @@ export {
59
65
  moduleUrl,
60
66
  commandAuthorization,
61
67
  readConsistency,
68
+ commandTransition,
69
+ traits,
62
70
  }
63
71
  /* consumedEventSchema Not a pure module */
@@ -37,6 +37,12 @@ function commandAuthorization(param) {
37
37
  return "AllowAuthenticated";
38
38
  }
39
39
 
40
+ function commandTransition(param) {
41
+ return "Unrestricted";
42
+ }
43
+
44
+ let traits = [];
45
+
40
46
  let name = "ChangePhoto";
41
47
 
42
48
  let Id;
@@ -60,5 +66,7 @@ export {
60
66
  moduleUrl,
61
67
  commandAuthorization,
62
68
  readConsistency,
69
+ commandTransition,
70
+ traits,
63
71
  }
64
72
  /* consumedEventSchema Not a pure module */