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

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 (66) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/package.json +7 -7
  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 +175 -22
  15. package/src/plugin/component/Plugin_Structure.res.mjs +89 -17
  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 +11 -3
  22. package/src/plugin/lifecycle/PluginSpec.res.mjs +11 -3
  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 +185 -1
  52. package/tests/plugin/PluginStructureTest.res.mjs +246 -3
  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.mjs +8 -0
  56. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +8 -0
  57. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +8 -0
  58. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -0
  59. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +8 -0
  60. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res +59 -0
  61. package/tests/plugin/StateChangeSlice/PsTransitionSwitch.res.mjs +115 -0
  62. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +8 -0
  63. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -0
  64. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +8 -0
  65. package/tests/plugin/pluginDefinitionRequiredScalars.txt +18 -0
  66. package/tests/util/CommandPublisherTest.res.mjs +9 -1
@@ -373,12 +373,18 @@ let reportTranslationTables = (
373
373
  }
374
374
  }
375
375
 
376
- // `@transition` names states belonging to another component's enum, which the PPX
377
- // only ever sees as names — so a misspelling compiles clean and produces a command
378
- // legal in a state no row is in. Both sides are in hand here. A state the linked
379
- // views do not declare raises (this runs at assembly, never in a Lambda, so that is
380
- // a failed deploy); no resolvable view only warns. Checked against the UNION of the
381
- // linked views: a slice feeding two is not claiming which one.
376
+ // A command's declared edge names states belonging to another component's enum.
377
+ // From `@transition` the PPX only ever sees names, so a misspelling compiles clean
378
+ // and produces a command legal in a state no row is in. Both sides are in hand
379
+ // here. A state the linked views do not declare raises (this runs at assembly,
380
+ // never in a Lambda, so that is a failed deploy); no resolvable view only warns.
381
+ // Checked against the UNION of the linked views: a slice feeding two is not
382
+ // claiming which one.
383
+ //
384
+ // A `commandTransition` switch comes through the same `commandDef` fields and is
385
+ // checked the same way. The compiler has already resolved its constructors, so
386
+ // what survives to here is naming the wrong enum rather than misspelling one —
387
+ // which this catches for the same reason and with the same message.
382
388
  let checkDeclaredTransitions = (
383
389
  ~pluginName: string,
384
390
  ~writables: array<Reventless.Plugin.writableDef>,
@@ -412,7 +418,7 @@ let checkDeclaredTransitions = (
412
418
  ->Array.forEach(state =>
413
419
  failures
414
420
  ->Array.push(
415
- `${w.name}.${cmd.name}: @transition names "${state}", which none of its ` ++
421
+ `${w.name}.${cmd.name} declares state "${state}", which none of its ` ++
416
422
  `linked views declare — ${w.linkedViews->Array.join(
417
423
  ", ",
418
424
  )} know ${known->Array.join(", ")}.`,
@@ -430,14 +436,14 @@ let checkDeclaredTransitions = (
430
436
  if unvalidated.contents > 0 {
431
437
  log.warn(
432
438
  ~comp="Plugin_Structure",
433
- `${pluginName}: ${unvalidated.contents->Int.toString} command(s) declare a @transition ` ++
439
+ `${pluginName}: ${unvalidated.contents->Int.toString} command(s) declare a transition ` ++
434
440
  `but no linked view declares a lifecycle to check it against.`,
435
441
  )
436
442
  }
437
443
 
438
444
  if Array.length(failures) > 0 {
439
445
  JsError.throwWithMessage(
440
- `${pluginName}: @transition names states that do not exist.\n` ++
446
+ `${pluginName}: a declared transition names states that do not exist.\n` ++
441
447
  failures->Array.join("\n"),
442
448
  )
443
449
  }
@@ -707,6 +713,18 @@ let toCommandDef = (
707
713
  // one aggregate carries commands with very different audiences, and a
708
714
  // component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
709
715
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
716
+ // The spec's `command => Transition.t<_>`, evaluated per variant the same way
717
+ // `commandAuthorization` is. It wins over the `@transition` metadata where it
718
+ // declares an edge, and stands aside where it says `Unrestricted` — which is
719
+ // what the PPX injects for a spec that never wrote the switch.
720
+ //
721
+ // Read at `t<string>` because this is the erasure boundary: the spec declares
722
+ // its edges over the linked view's own lifecycle enum, whose arms are
723
+ // payload-less and therefore ARE their own names at run time. The one cast
724
+ // that says so is the same `->Obj.magic` every spec member arrives through
725
+ // (see the call sites below), so `Transition` itself asserts nothing about
726
+ // representation and stays parameterised all the way down.
727
+ ~commandTransition: unknown => Reventless.Transition.t<string>,
710
728
  v: S.t<unknown>,
711
729
  ): option<Reventless.Plugin.commandDef> => {
712
730
  // Build a commandDef for one variant. `properties` is the variant's field dict —
@@ -715,12 +733,41 @@ let toCommandDef = (
715
733
  let mkDef = (~variantName, ~properties) => {
716
734
  let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
717
735
  let references = extractReferences(properties)
718
- // Per-variant, but attached by the PPX to the *parent* schema as one dict.
719
- let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
720
- // The `@transition` target (the command's *to* status), read the same way
721
- // as allowedStates. None ⇒ AutoUI's board resolver falls back to its
722
- // name-stem heuristic.
723
- let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
736
+ // Evaluated against a synthetic value per constructor, the same shape the
737
+ // resolver builds at call time: a payload-bearing variant compiles to
738
+ // `{TAG, ...}`, a payload-less one to a bare string.
739
+ let syntheticCommand: unknown =
740
+ Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
741
+ ? {"TAG": variantName}->Obj.magic
742
+ : variantName->Obj.magic
743
+ // The spec's own switch, which is the only form that can speak for a
744
+ // constructor the host did not declare: `@transition` lowers to a dict on
745
+ // the parent union, and a variant spread splices members, so a spliced
746
+ // command reaches the metadata below carrying nothing.
747
+ let declared = commandTransition(syntheticCommand)
748
+ // An edge is ONE declaration, so the two fields are chosen together. Taking
749
+ // them separately would let `Guards(…)` — which positively claims the
750
+ // command moves the row nowhere — inherit a target from an annotation it
751
+ // was written to replace.
752
+ //
753
+ // The `@transition` metadata is per-variant but attached by the PPX to the
754
+ // *parent* schema as one dict. It is consulted only where the switch
755
+ // declares no edge at all: a spec that never wrote one answers
756
+ // `Unrestricted` for every command, which is how the annotation stays in
757
+ // charge for the hosts that still use it.
758
+ //
759
+ // `targetState: None` ⇒ AutoUI's board resolver falls back to its name-stem
760
+ // heuristic.
761
+ let (allowedStates, targetState) = switch declared {
762
+ | Unrestricted => (
763
+ ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName),
764
+ ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName),
765
+ )
766
+ | _ => (
767
+ Reventless.Transition.allowedStates(declared),
768
+ Reventless.Transition.targetState(declared),
769
+ )
770
+ }
724
771
  // API-exposed iff the whole command isn't @noApi and this variant
725
772
  // isn't in its @noApi-variants set — mirrors the API-generation filter
726
773
  // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
@@ -730,13 +777,6 @@ let toCommandDef = (
730
777
  | Some(excluded) => !(excluded->Set.has(variantName))
731
778
  | None => true
732
779
  }
733
- // Evaluated against a synthetic value per constructor, the same shape the
734
- // resolver builds at call time: a payload-bearing variant compiles to
735
- // `{TAG, ...}`, a payload-less one to a bare string.
736
- let syntheticCommand: unknown =
737
- Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
738
- ? {"TAG": variantName}->Obj.magic
739
- : variantName->Obj.magic
740
780
  let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
741
781
  // See the note on the record's `mutationField` for why a non-exposed
742
782
  // variant gets the empty sentinel. It has no callable field, and the
@@ -796,6 +836,7 @@ let extractCommandDefs = (
796
836
  ~isAggregate,
797
837
  ~mutationFieldFor: string => string,
798
838
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
839
+ ~commandTransition: unknown => Reventless.Transition.t<string>,
799
840
  commandSchema: S.t<unknown>,
800
841
  ): array<Reventless.Plugin.commandDef> =>
801
842
  switch commandSchema {
@@ -806,6 +847,7 @@ let extractCommandDefs = (
806
847
  ~mutationFieldFor,
807
848
  ~parentSchema=commandSchema,
808
849
  ~commandAuthorization,
850
+ ~commandTransition,
809
851
  v,
810
852
  )
811
853
  )
@@ -816,6 +858,7 @@ let extractCommandDefs = (
816
858
  ~mutationFieldFor,
817
859
  ~parentSchema=commandSchema,
818
860
  ~commandAuthorization,
861
+ ~commandTransition,
819
862
  commandSchema,
820
863
  )->Option.mapOr([], def => [def])
821
864
  }
@@ -1061,6 +1104,49 @@ let make = (
1061
1104
  }
1062
1105
  }
1063
1106
 
1107
+ // ── One command name, one handler ──────────────────────────────────────────
1108
+ //
1109
+ // A DCB plugin routes a command by its bare type name: the runtime builds
1110
+ // `handlersByType[typeName]` across every StateChangeSlice in the plugin and
1111
+ // dispatches on the incoming command's TAG alone. Two slices declaring one name
1112
+ // therefore have one handler between them — the second registration overwrites
1113
+ // the first, and the plugin deploys.
1114
+ //
1115
+ // Refused here rather than logged there. The runtime notices only when it goes
1116
+ // to stamp owner fields, by which point the deploy has succeeded and the losing
1117
+ // slice is silently unreachable; a name is a compile-time fact and this is the
1118
+ // last place that can see all of them at once.
1119
+ //
1120
+ // Aggregates are exempt and are not checked: their commands are addressed per
1121
+ // component channel, so two aggregates sharing a command name is ordinary.
1122
+ let dcbCommandOwners: dict<array<string>> = Dict.make()
1123
+ stateChangeSlices->Array.forEach((module(SCS: ReventlessInfra.StateChangeSlice.T)) =>
1124
+ Reventless.DcbTag.extractAllVariantNames(
1125
+ SCS.Spec.commandSchema->S.castToUnknown,
1126
+ )->Array.forEach(cmd => {
1127
+ let owners = dcbCommandOwners->Dict.get(cmd)->Option.getOr([])
1128
+ dcbCommandOwners->Dict.set(cmd, Array.concat(owners, [SCS.Spec.name]))
1129
+ })
1130
+ )
1131
+ let clashes =
1132
+ dcbCommandOwners
1133
+ ->Dict.toArray
1134
+ ->Array.filter(((_, owners)) => owners->Array.length > 1)
1135
+ ->Array.toSorted((((a, _)), ((b, _))) => String.compare(a, b))
1136
+ if clashes->Array.length > 0 {
1137
+ JsError.throwWithMessage(
1138
+ `${name}: ` ++
1139
+ clashes
1140
+ ->Array.map((((cmd, owners))) =>
1141
+ `${owners->Array.join(" and ")} both declare the command "${cmd}"`
1142
+ )
1143
+ ->Array.join("; ") ++
1144
+ `.\n A DCB plugin routes a command by its bare name, so one of these would never run. ` ++
1145
+ `Qualify them — the shipped traits' emitters prefix every name with the host they are ` ++
1146
+ `grafted onto, for exactly this reason.`,
1147
+ )
1148
+ }
1149
+
1064
1150
  // One list feeds both the store collection and the lint below, so a field is
1065
1151
  // read exactly once by either.
1066
1152
  let storeDeclarationSites: array<(string, S.t<unknown>)> =
@@ -1118,6 +1204,69 @@ let make = (
1118
1204
  )
1119
1205
  }
1120
1206
 
1207
+ // Capabilities a slice declares its `translate` reaches for. Sorted and
1208
+ // deduplicated for the same reason the store declarations are: the manifest is
1209
+ // committed, so it must not churn on a re-build. Only outbound translation
1210
+ // slices are handed `Capabilities.t`, so only they can declare.
1211
+ let requiredCapabilities =
1212
+ outboundTranslationSlices
1213
+ ->Array.flatMap((module(OTS: ReventlessInfra.OutboundTranslationSlice.T)) =>
1214
+ OTS.Spec.capabilityNeeds->Array.map(need => (
1215
+ {
1216
+ Reventless.Plugin.capability: Reventless.CapabilityNeed.toString(need),
1217
+ component: OTS.Spec.name,
1218
+ }: Reventless.Plugin.requiredCapabilityDeclaration
1219
+ ))
1220
+ )
1221
+ ->Array.toSorted((a, b) =>
1222
+ a.capability == b.capability
1223
+ ? String.compare(a.component, b.component)
1224
+ : String.compare(a.capability, b.capability)
1225
+ )
1226
+ ->Array.reduce([], (acc, d) =>
1227
+ switch acc->Array.last {
1228
+ | Some(prev) if prev == d => acc
1229
+ | _ => acc->Array.concat([d])
1230
+ }
1231
+ )
1232
+
1233
+ // The traits grafted into this plugin, one entry per declaring component. The
1234
+ // component name is added here rather than declared: a trait cannot know which
1235
+ // component a host grafted it onto, and a host writing it down would be the one
1236
+ // hand-typed string this whole mechanism exists to avoid. Sorted and
1237
+ // deduplicated for the same reason the two declarations above are.
1238
+ let traitDeclarations = {
1239
+ let entry = (~component, decl: Reventless.Trait.t) => (
1240
+ {
1241
+ Reventless.Plugin.trait: decl.trait,
1242
+ version: decl.version,
1243
+ posture: Reventless.Trait.postureToString(decl.posture),
1244
+ component,
1245
+ }: Reventless.Plugin.traitDeclaration
1246
+ )
1247
+ [
1248
+ aggregates->Array.flatMap((module(A: ReventlessInfra.Aggregate.T with type api = api)) =>
1249
+ A.Spec.traits->Array.map(entry(~component=A.Spec.name, ...))
1250
+ ),
1251
+ stateChangeSlices->Array.flatMap((module(SCS: ReventlessInfra.StateChangeSlice.T)) =>
1252
+ SCS.Spec.traits->Array.map(entry(~component=SCS.Spec.name, ...))
1253
+ ),
1254
+ outboundTranslationSlices->Array.flatMap((
1255
+ module(OTS: ReventlessInfra.OutboundTranslationSlice.T),
1256
+ ) => OTS.Spec.traits->Array.map(entry(~component=OTS.Spec.name, ...))),
1257
+ ]
1258
+ ->Array.flat
1259
+ ->Array.toSorted((a, b) =>
1260
+ a.trait == b.trait ? String.compare(a.component, b.component) : String.compare(a.trait, b.trait)
1261
+ )
1262
+ ->Array.reduce([], (acc, d) =>
1263
+ switch acc->Array.last {
1264
+ | Some(prev) if prev == d => acc
1265
+ | _ => acc->Array.concat([d])
1266
+ }
1267
+ )
1268
+ }
1269
+
1121
1270
  // Heuristic-only matches — a field *named* like a stored-object ref with no
1122
1271
  // `@storageRef` — warn and provision nothing. Declaration outranks inference;
1123
1272
  // the warning names the annotation that would settle it.
@@ -1252,6 +1401,7 @@ let make = (
1252
1401
  ~variant=variantName,
1253
1402
  ),
1254
1403
  ~commandAuthorization=SCS.Spec.commandAuthorization->Obj.magic,
1404
+ ~commandTransition=SCS.Spec.commandTransition->Obj.magic,
1255
1405
  SCS.Spec.commandSchema->S.castToUnknown,
1256
1406
  ),
1257
1407
  producedEventTypes: produced,
@@ -1276,6 +1426,7 @@ let make = (
1276
1426
  ~isAggregate=true,
1277
1427
  ~mutationFieldFor=variantName => Api_Naming.aggregateMutationField(~plugin=name, ~aggregate=A.Spec.name, ~command=variantName),
1278
1428
  ~commandAuthorization=A.Spec.commandAuthorization->Obj.magic,
1429
+ ~commandTransition=A.Spec.commandTransition->Obj.magic,
1279
1430
  A.Spec.commandSchema->S.castToUnknown,
1280
1431
  ),
1281
1432
  producedEventTypes: produced,
@@ -1747,5 +1898,7 @@ let make = (
1747
1898
  extensionPoints: Some(extensionPointDefs),
1748
1899
  requiredStores: Some(requiredStores),
1749
1900
  requiredStoreDeclarations: Some(requiredStoreDeclarations),
1901
+ traitDeclarations: Some(traitDeclarations),
1902
+ requiredCapabilities: Some(requiredCapabilities),
1750
1903
  }
1751
1904
  }
@@ -9,14 +9,17 @@ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
9
9
  import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
10
10
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
11
11
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
12
+ import * as Trait$Reventless from "@reventlessdev/reventless-spec/src/types/Trait.res.mjs";
12
13
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
13
14
  import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
14
15
  import * as Reference$Reventless from "@reventlessdev/reventless-spec/src/components/Reference.res.mjs";
15
16
  import * as Util_Sury$Reventless from "@reventlessdev/reventless-spec/src/util/Util_Sury.res.mjs";
16
17
  import * as Logger$ReventlessCore from "../../util/Logger.res.mjs";
17
18
  import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
19
+ import * as Transition$Reventless from "@reventlessdev/reventless-spec/src/types/Transition.res.mjs";
18
20
  import * as DisplayName$Reventless from "@reventlessdev/reventless-spec/src/components/DisplayName.res.mjs";
19
21
  import * as Api_Naming$ReventlessCore from "../../components/Api/Api_Naming.res.mjs";
22
+ import * as CapabilityNeed$Reventless from "@reventlessdev/reventless-spec/src/semantic/CapabilityNeed.res.mjs";
20
23
  import * as SchemaType$ReventlessCore from "../../components/Api/SchemaType.res.mjs";
21
24
  import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src/components/StateAnnotations.res.mjs";
22
25
  import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
@@ -343,16 +346,16 @@ function checkDeclaredTransitions(pluginName, writables, lifecycleStatesByView)
343
346
  unvalidated.contents = unvalidated.contents + 1 | 0;
344
347
  } else {
345
348
  declared.filter(state => !known.includes(state)).forEach(state => {
346
- failures.push(w.name + `.` + cmd.name + `: @transition names "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
349
+ failures.push(w.name + `.` + cmd.name + ` declares state "` + state + `", which none of its ` + (`linked views declare — ` + w.linkedViews.join(", ") + ` know ` + known.join(", ") + `.`));
347
350
  });
348
351
  }
349
352
  });
350
353
  });
351
354
  if (unvalidated.contents > 0) {
352
- log.warn("Plugin_Structure", undefined, pluginName + `: ` + unvalidated.contents.toString() + ` command(s) declare a @transition but no linked view declares a lifecycle to check it against.`);
355
+ log.warn("Plugin_Structure", undefined, pluginName + `: ` + unvalidated.contents.toString() + ` command(s) declare a transition but no linked view declares a lifecycle to check it against.`);
353
356
  }
354
357
  if (failures.length !== 0) {
355
- return Stdlib_JsError.throwWithMessage(pluginName + `: @transition names states that do not exist.\n` + failures.join("\n"));
358
+ return Stdlib_JsError.throwWithMessage(pluginName + `: a declared transition names states that do not exist.\n` + failures.join("\n"));
356
359
  }
357
360
  }
358
361
 
@@ -591,20 +594,27 @@ function annotateArgTypes(schema, argTypes) {
591
594
  return schema;
592
595
  }
593
596
 
594
- function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, v) {
597
+ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthorization, commandTransition, v) {
595
598
  let mkDef = (variantName, properties) => {
596
599
  let match = commandLevelAndId(isAggregate, variantName, properties);
597
600
  let references = extractReferences(properties);
598
- let allowedStates = ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName);
599
- let targetState = ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName);
601
+ let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
602
+ TAG: variantName
603
+ }) : variantName;
604
+ let declared = commandTransition(syntheticCommand);
605
+ let match$1;
606
+ match$1 = typeof declared !== "object" ? [
607
+ ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName),
608
+ ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName)
609
+ ] : [
610
+ Transition$Reventless.allowedStates(declared),
611
+ Transition$Reventless.targetState(declared)
612
+ ];
600
613
  let apiExposed = false;
601
614
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(parentSchema)) {
602
615
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
603
616
  apiExposed = excluded !== undefined ? !excluded.has(variantName) : true;
604
617
  }
605
- let syntheticCommand = DcbTag$Reventless.isVariantPayloadBearing(parentSchema, variantName) ? ({
606
- TAG: variantName
607
- }) : variantName;
608
618
  let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand));
609
619
  let mutationField = apiExposed ? mutationFieldFor(variantName) : "";
610
620
  let jsonSchema = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(v);
@@ -616,8 +626,8 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
616
626
  aggregateIdField: match[1],
617
627
  mutationField: mutationField,
618
628
  references: references,
619
- allowedStates: allowedStates,
620
- targetState: targetState,
629
+ allowedStates: match$1[0],
630
+ targetState: match$1[1],
621
631
  apiExposed: apiExposed,
622
632
  requiredAccess: requiredAccess,
623
633
  ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
@@ -647,11 +657,11 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
647
657
  }
648
658
  }
649
659
 
650
- function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandSchema) {
660
+ function extractCommandDefs(isAggregate, mutationFieldFor, commandAuthorization, commandTransition, commandSchema) {
651
661
  if (commandSchema.type === "anyOf") {
652
- return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, v));
662
+ return Stdlib_Array.filterMap(commandSchema.anyOf, v => toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, v));
653
663
  } else {
654
- return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandSchema), [], def => [def]);
664
+ return Stdlib_Option.mapOr(toCommandDef(isAggregate, mutationFieldFor, commandSchema, commandAuthorization, commandTransition, commandSchema), [], def => [def]);
655
665
  }
656
666
  }
657
667
 
@@ -791,6 +801,17 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
791
801
  return Primitive_string.compare(a.field, b.field);
792
802
  }
793
803
  };
804
+ let dcbCommandOwners = {};
805
+ stateChangeSlices.forEach(SCS => {
806
+ DcbTag$Reventless.extractAllVariantNames(SCS.Spec.commandSchema).forEach(cmd => {
807
+ let owners = Stdlib_Option.getOr(dcbCommandOwners[cmd], []);
808
+ dcbCommandOwners[cmd] = owners.concat([SCS.Spec.name]);
809
+ });
810
+ });
811
+ let clashes = Object.entries(dcbCommandOwners).filter(param => param[1].length > 1).toSorted((param, param$1) => Primitive_string.compare(param[0], param$1[0]));
812
+ if (clashes.length !== 0) {
813
+ Stdlib_JsError.throwWithMessage(name + `: ` + clashes.map(param => param[1].join(" and ") + ` both declare the command "` + param[0] + `"`).join("; ") + `.\n A DCB plugin routes a command by its bare name, so one of these would never run. Qualify them — the shipped traits' emitters prefix every name with the host they are grafted onto, for exactly this reason.`);
814
+ }
794
815
  let storeDeclarationSites = [
795
816
  aggregates.flatMap(A => [
796
817
  [
@@ -874,6 +895,55 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
874
895
  if (found.length !== 0) {
875
896
  Stdlib_JsError.throwWithMessage(name + `: two declared object stores look like one store misspelled.\n` + found.map(Capability_Inference$ReventlessCore.collisionMessage).join("\n"));
876
897
  }
898
+ let requiredCapabilities = Stdlib_Array.reduce(outboundTranslationSlices.flatMap(OTS => OTS.Spec.capabilityNeeds.map(need => ({
899
+ capability: CapabilityNeed$Reventless.toString(need),
900
+ component: OTS.Spec.name
901
+ }))).toSorted((a, b) => {
902
+ if (a.capability === b.capability) {
903
+ return Primitive_string.compare(a.component, b.component);
904
+ } else {
905
+ return Primitive_string.compare(a.capability, b.capability);
906
+ }
907
+ }), [], (acc, d) => {
908
+ let prev = Stdlib_Array.last(acc);
909
+ if (prev !== undefined) {
910
+ if (Primitive_object.equal(prev, d)) {
911
+ return acc;
912
+ } else {
913
+ return acc.concat([d]);
914
+ }
915
+ } else {
916
+ return acc.concat([d]);
917
+ }
918
+ });
919
+ let entry = (component, decl) => ({
920
+ trait: decl.trait,
921
+ version: decl.version,
922
+ posture: Trait$Reventless.postureToString(decl.posture),
923
+ component: component
924
+ });
925
+ let traitDeclarations = Stdlib_Array.reduce([
926
+ aggregates.flatMap(A => A.Spec.traits.map(extra => entry(A.Spec.name, extra))),
927
+ stateChangeSlices.flatMap(SCS => SCS.Spec.traits.map(extra => entry(SCS.Spec.name, extra))),
928
+ outboundTranslationSlices.flatMap(OTS => OTS.Spec.traits.map(extra => entry(OTS.Spec.name, extra)))
929
+ ].flat().toSorted((a, b) => {
930
+ if (a.trait === b.trait) {
931
+ return Primitive_string.compare(a.component, b.component);
932
+ } else {
933
+ return Primitive_string.compare(a.trait, b.trait);
934
+ }
935
+ }), [], (acc, d) => {
936
+ let prev = Stdlib_Array.last(acc);
937
+ if (prev !== undefined) {
938
+ if (Primitive_object.equal(prev, d)) {
939
+ return acc;
940
+ } else {
941
+ return acc.concat([d]);
942
+ }
943
+ } else {
944
+ return acc.concat([d]);
945
+ }
946
+ });
877
947
  storeDeclarationSites.forEach(param => {
878
948
  Capability_Inference$ReventlessCore.scanSchema(param[0], param[1]).forEach(w => log.warn("Plugin_Structure", undefined, Capability_Inference$ReventlessCore.message(w)));
879
949
  });
@@ -968,7 +1038,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
968
1038
  let consumed = match$1[1];
969
1039
  return {
970
1040
  name: SCS.Spec.name,
971
- commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandSchema),
1041
+ commands: extractCommandDefs(false, variantName => Api_Naming$ReventlessCore.sliceMutationFieldFor(name, SCS.Spec.name, SCS.Spec.commandSchema, variantName), SCS.Spec.commandAuthorization, SCS.Spec.commandTransition, SCS.Spec.commandSchema),
972
1042
  producedEventTypes: produced,
973
1043
  consumedEventTypes: consumed,
974
1044
  linkedViews: linkedSvsFor(produced),
@@ -983,7 +1053,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
983
1053
  let produced = match[1];
984
1054
  return {
985
1055
  name: A.Spec.name,
986
- commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandSchema),
1056
+ commands: extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.aggregateMutationField(name, A.Spec.name, variantName), A.Spec.commandAuthorization, A.Spec.commandTransition, A.Spec.commandSchema),
987
1057
  producedEventTypes: produced,
988
1058
  consumedEventTypes: [],
989
1059
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
@@ -1268,7 +1338,9 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
1268
1338
  extensions: extensionDefs,
1269
1339
  extensionPoints: extensionPointDefs,
1270
1340
  requiredStores: requiredStores,
1271
- requiredStoreDeclarations: requiredStoreDeclarations
1341
+ requiredStoreDeclarations: requiredStoreDeclarations,
1342
+ requiredCapabilities: requiredCapabilities,
1343
+ traitDeclarations: traitDeclarations
1272
1344
  };
1273
1345
  }
1274
1346
 
@@ -75,7 +75,9 @@ function Make(Spec) {
75
75
  errorSchema: ExtensionMapping$ReventlessInfra.NoDelegate.errorSchema,
76
76
  commandSchema: ExtensionMapping$ReventlessInfra.NoDelegate.commandSchema,
77
77
  moduleUrl: ExtensionMapping$ReventlessInfra.NoDelegate.moduleUrl,
78
- commandAuthorization: ExtensionMapping$ReventlessInfra.NoDelegate.commandAuthorization
78
+ commandAuthorization: ExtensionMapping$ReventlessInfra.NoDelegate.commandAuthorization,
79
+ commandTransition: ExtensionMapping$ReventlessInfra.NoDelegate.commandTransition,
80
+ traits: ExtensionMapping$ReventlessInfra.NoDelegate.traits
79
81
  },
80
82
  moduleUrl: PluginExtensionPointSpec$ReventlessInfra.moduleUrl,
81
83
  delegateModuleUrl: delegateModuleUrl,
@@ -405,7 +405,9 @@ function Make(Spec) {
405
405
  errorSchema: PluginSpec$ReventlessCore.errorSchema,
406
406
  commandSchema: PluginSpec$ReventlessCore.commandSchema,
407
407
  moduleUrl: PluginSpec$ReventlessCore.moduleUrl,
408
- commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization
408
+ commandAuthorization: PluginSpec$ReventlessCore.commandAuthorization,
409
+ commandTransition: PluginSpec$ReventlessCore.commandTransition,
410
+ traits: PluginSpec$ReventlessCore.traits
409
411
  },
410
412
  moduleUrl: PluginExtensionPointSpec$ReventlessInfra.moduleUrl,
411
413
  mapIncomingCommand: mapIncomingCommand,
@@ -72,7 +72,9 @@ let Mapping = ExtensionPointMapping$ReventlessInfra.Make({
72
72
  errorSchema: UiFragmentRegistry$ReventlessCore.errorSchema,
73
73
  commandSchema: UiFragmentRegistry$ReventlessCore.commandSchema,
74
74
  moduleUrl: UiFragmentRegistry$ReventlessCore.moduleUrl,
75
- commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization
75
+ commandAuthorization: UiFragmentRegistry$ReventlessCore.commandAuthorization,
76
+ commandTransition: UiFragmentRegistry$ReventlessCore.commandTransition,
77
+ traits: UiFragmentRegistry$ReventlessCore.traits
76
78
  },
77
79
  moduleUrl: moduleUrl,
78
80
  mapIncomingCommand: mapIncomingCommand,
@@ -159,19 +159,19 @@ let decide = (state, command) =>
159
159
  Ok(Array.concat([VersionDisconnected(definition)], promoteEvents(state, v)))
160
160
  | _ => Ok([]) // not connected / unknown → tolerate stray disconnect
161
161
  }
162
- | Activate(v) =>
163
- switch state.known->Dict.get(v) {
162
+ | Activate({version}) =>
163
+ switch state.known->Dict.get(version) {
164
164
  | Some({status: Inactive | Retired, definition}) =>
165
165
  let base = [VersionActivated(definition)]
166
- let supersede = if currentAfterConnect(state.known, v) == v {
166
+ let supersede = if currentAfterConnect(state.known, version) == version {
167
167
  switch state.current {
168
- | Some(c) if c != v =>
168
+ | Some(c) if c != version =>
169
169
  switch state.known->Dict.get(c) {
170
170
  | Some({definition: defC}) => [
171
171
  VersionSuperseded({
172
172
  supersededVersion: c,
173
173
  supersededDefinition: defC,
174
- newVersion: v,
174
+ newVersion: version,
175
175
  newDefinition: definition,
176
176
  }),
177
177
  ]
@@ -187,18 +187,18 @@ let decide = (state, command) =>
187
187
  | Some({status: Disconnected}) => Error(IsDisconnected)
188
188
  | None => Error(UnknownVersion)
189
189
  }
190
- | Deactivate(v) =>
191
- switch state.known->Dict.get(v) {
190
+ | Deactivate({version}) =>
191
+ switch state.known->Dict.get(version) {
192
192
  | Some({status: Connected | Disconnected, definition}) =>
193
- Ok(Array.concat([VersionDeactivated(definition)], promoteEvents(state, v)))
193
+ Ok(Array.concat([VersionDeactivated(definition)], promoteEvents(state, version)))
194
194
  | Some({status: Inactive | Retired}) => Ok([]) // idempotent / archived no-op
195
195
  | None => Error(UnknownVersion)
196
196
  }
197
- | Retire(v) =>
198
- switch state.known->Dict.get(v) {
197
+ | Retire({version}) =>
198
+ switch state.known->Dict.get(version) {
199
199
  | Some({status: Retired}) => Ok([]) // idempotent
200
200
  | Some({definition}) =>
201
- Ok(Array.concat([VersionRetired(definition)], promoteEvents(state, v)))
201
+ Ok(Array.concat([VersionRetired(definition)], promoteEvents(state, version)))
202
202
  | None => Error(UnknownVersion)
203
203
  }
204
204
  | ReportIncompatibility(def) => Ok([IncompatiblePluginDetected(def)])
@@ -232,8 +232,8 @@ function decide(state, command) {
232
232
  };
233
233
  }
234
234
  case "Activate" :
235
- let v$4 = command._0;
236
- let match$4 = state.known[v$4];
235
+ let version = command.version;
236
+ let match$4 = state.known[version];
237
237
  if (match$4 === undefined) {
238
238
  return {
239
239
  TAG: "Error",
@@ -261,17 +261,17 @@ function decide(state, command) {
261
261
  _0: definition
262
262
  }];
263
263
  let supersede;
264
- if (currentAfterConnect(state.known, v$4) === v$4) {
264
+ if (currentAfterConnect(state.known, version) === version) {
265
265
  let c = state.current;
266
266
  if (c !== undefined) {
267
- if (c !== v$4) {
267
+ if (c !== version) {
268
268
  let match$5 = state.known[c];
269
269
  supersede = match$5 !== undefined ? [{
270
270
  TAG: "VersionSuperseded",
271
271
  _0: {
272
272
  supersededVersion: c,
273
273
  supersededDefinition: match$5.definition,
274
- newVersion: v$4,
274
+ newVersion: version,
275
275
  newDefinition: definition
276
276
  }
277
277
  }] : [];
@@ -290,8 +290,8 @@ function decide(state, command) {
290
290
  };
291
291
  break;
292
292
  case "Deactivate" :
293
- let v$5 = command._0;
294
- let match$6 = state.known[v$5];
293
+ let version$1 = command.version;
294
+ let match$6 = state.known[version$1];
295
295
  if (match$6 === undefined) {
296
296
  return {
297
297
  TAG: "Error",
@@ -314,12 +314,12 @@ function decide(state, command) {
314
314
  _0: [{
315
315
  TAG: "VersionDeactivated",
316
316
  _0: match$6.definition
317
- }].concat(promoteEvents(state, v$5))
317
+ }].concat(promoteEvents(state, version$1))
318
318
  };
319
319
  break;
320
320
  case "Retire" :
321
- let v$6 = command._0;
322
- let match$7 = state.known[v$6];
321
+ let version$2 = command.version;
322
+ let match$7 = state.known[version$2];
323
323
  if (match$7 !== undefined) {
324
324
  if (match$7.status === "Retired") {
325
325
  return {
@@ -332,7 +332,7 @@ function decide(state, command) {
332
332
  _0: [{
333
333
  TAG: "VersionRetired",
334
334
  _0: match$7.definition
335
- }].concat(promoteEvents(state, v$6))
335
+ }].concat(promoteEvents(state, version$2))
336
336
  };
337
337
  }
338
338
  } else {