@reventlessdev/reventless-core 3.0.0-alpha.239 → 3.0.0-alpha.241

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 (172) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/package.json +11 -11
  3. package/src/Message.res +11 -11
  4. package/src/Message.res.mjs +15 -14
  5. package/src/Projection.res +2 -2
  6. package/src/Projection.res.mjs +5 -4
  7. package/src/adapter/Monitoring/Monitoring.res +24 -2
  8. package/src/adapter/Monitoring/Monitoring.res.mjs +3 -3
  9. package/src/admin/Platform_Admin.res +1 -16
  10. package/src/admin/Platform_Admin.res.mjs +2 -30
  11. package/src/admin/Platform_AdminApi.res.mjs +3 -3
  12. package/src/admin/Platform_Admin_Structure.res +58 -113
  13. package/src/admin/Platform_Admin_Structure.res.mjs +24 -139
  14. package/src/admin/UiFragmentRegistry/StateChangeSlice/UiFragmentRegistry.res.mjs +12 -12
  15. package/src/admin/UiFragmentRegistry/StateViewSlice/UiFragments.res.mjs +14 -14
  16. package/src/components/Aggregate/Aggregate_Callback.res +2 -2
  17. package/src/components/Aggregate/Aggregate_Callback.res.mjs +4 -4
  18. package/src/components/Api/ApiAllowedStatesHelpers.res.mjs +3 -3
  19. package/src/components/Api/ApiNoApiHelpers.res.mjs +5 -5
  20. package/src/components/Api/ApiTargetStateHelpers.res.mjs +3 -3
  21. package/src/components/Api/GraphQL_FragmentGenerator.res +18 -12
  22. package/src/components/Api/GraphQL_FragmentGenerator.res.mjs +7 -13
  23. package/src/components/Api/GraphQL_SchemaInspector.res +1 -1
  24. package/src/components/Api/GraphQL_SchemaInspector.res.mjs +1 -1
  25. package/src/components/Api/MCP_SchemaGenerator.res +1 -1
  26. package/src/components/Api/MCP_SchemaGenerator.res.mjs +1 -1
  27. package/src/components/Api/SchemaType.res +6 -10
  28. package/src/components/Api/SchemaType.res.mjs +4 -4
  29. package/src/components/Api/StorageRefFields.res +1 -1
  30. package/src/components/Api/StorageRefFields.res.mjs +1 -1
  31. package/src/components/Api/SuryToJsonSchema.res +11 -0
  32. package/src/components/Api/SuryToJsonSchema.res.mjs +2 -1
  33. package/src/components/AutomationSlice/AutomationSlice_Callback.res +3 -4
  34. package/src/components/AutomationSlice/AutomationSlice_Callback.res.mjs +22 -23
  35. package/src/components/CommandTopic/CommandTopic_Callback.res +1 -2
  36. package/src/components/CommandTopic/CommandTopic_Callback.res.mjs +3 -4
  37. package/src/components/CommandTopic/CommandTopic_Helpers.res +1 -1
  38. package/src/components/CommandTopic/CommandTopic_Helpers.res.mjs +3 -3
  39. package/src/components/Counter/Counter.res.mjs +2 -2
  40. package/src/components/Counter/Counter_Callback.res.mjs +4 -4
  41. package/src/components/Counter/Counter_Operations.res.mjs +4 -4
  42. package/src/components/Dcb/Dcb_Builder.res +1 -1
  43. package/src/components/Dcb/Dcb_Builder.res.mjs +8 -6
  44. package/src/components/DcbEventLog/DcbEventLog_Builder.res.mjs +4 -3
  45. package/src/components/EventCollector/EventCollector_Builder.res +1 -1
  46. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res +1 -2
  47. package/src/components/InboundTranslationSlice/InboundTranslationSlice_Callback.res.mjs +11 -12
  48. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res +3 -4
  49. package/src/components/OutboundTranslationSlice/OutboundTranslationSlice_Callback.res.mjs +24 -25
  50. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res +1 -1
  51. package/src/components/SideEffectHandler/SideEffectHandler_Callback.res.mjs +3 -3
  52. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +2 -2
  53. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +4 -4
  54. package/src/components/StateViewSlice/StateViewSlice_Builder.res +1 -1
  55. package/src/components/StateViewSlice/StateViewSlice_Builder.res.mjs +3 -3
  56. package/src/plugin/api/PluginBaseFragment.res +12 -29
  57. package/src/plugin/api/PluginBaseFragment.res.mjs +3 -22
  58. package/src/plugin/component/Capability_Inference.res +118 -1
  59. package/src/plugin/component/Capability_Inference.res.mjs +128 -1
  60. package/src/plugin/component/Plugin_Builder.res +2 -2
  61. package/src/plugin/component/Plugin_Builder.res.mjs +2 -1
  62. package/src/plugin/component/Plugin_Helpers.res +11 -11
  63. package/src/plugin/component/Plugin_Helpers.res.mjs +11 -10
  64. package/src/plugin/component/Plugin_Structure.res +349 -274
  65. package/src/plugin/component/Plugin_Structure.res.mjs +212 -175
  66. package/src/plugin/component/Plugin_SubscriptionSchema.res +1 -1
  67. package/src/plugin/component/Plugin_SubscriptionSchema.res.mjs +1 -1
  68. package/src/plugin/component/SchemaWalker.res +11 -11
  69. package/src/plugin/component/SchemaWalker.res.mjs +8 -9
  70. package/src/plugin/connect/PluginExtensionPoint_Plugin.res.mjs +15 -15
  71. package/src/plugin/lifecycle/PluginBehavior.res.mjs +55 -55
  72. package/src/plugin/lifecycle/PluginSpec.res +22 -7
  73. package/src/plugin/lifecycle/PluginSpec.res.mjs +91 -36
  74. package/src/plugin/lifecycle/PluginsReadModelSpec.res +36 -9
  75. package/src/plugin/lifecycle/PluginsReadModelSpec.res.mjs +33 -22
  76. package/src/util/Interstack.res +3 -3
  77. package/src/util/LogFormat.res +1 -2
  78. package/src/util/LogFormat.res.mjs +2 -2
  79. package/tests/IdentityTest.res +6 -7
  80. package/tests/IdentityTest.res.mjs +7 -9
  81. package/tests/adapter/MonitoringTest.res +30 -3
  82. package/tests/adapter/MonitoringTest.res.mjs +31 -12
  83. package/tests/adapter/RuntimeExtensionHookReachTest.res +0 -1
  84. package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +5 -7
  85. package/tests/admin/Platform_Admin_StructureTest.res +254 -0
  86. package/tests/admin/Platform_Admin_StructureTest.res.mjs +219 -0
  87. package/tests/aggregate/AggregateCacheTest.res +0 -1
  88. package/tests/aggregate/AggregateCacheTest.res.mjs +13 -15
  89. package/tests/aggregate/AggregateConflictTest.res +0 -1
  90. package/tests/aggregate/AggregateConflictTest.res.mjs +7 -9
  91. package/tests/aggregate/AggregateFixtures.res +0 -1
  92. package/tests/aggregate/AggregateFixtures.res.mjs +19 -21
  93. package/tests/aggregate/AggregateSnapshotTest.res +1 -2
  94. package/tests/aggregate/AggregateSnapshotTest.res.mjs +15 -16
  95. package/tests/api/ApiNoApiTest.res.mjs +17 -9
  96. package/tests/api/GraphQL_FragmentGeneratorTest.res +62 -1
  97. package/tests/api/GraphQL_FragmentGeneratorTest.res.mjs +102 -58
  98. package/tests/api/PsInternalFieldsReadModel.res +21 -0
  99. package/tests/api/PsInternalFieldsReadModel.res.mjs +74 -0
  100. package/tests/api/StorageRefFieldsTest.res.mjs +13 -13
  101. package/tests/api/SuryToJsonSchemaTest.res +43 -0
  102. package/tests/api/SuryToJsonSchemaTest.res.mjs +352 -849
  103. package/tests/commandgenerator/CommandGeneratorFixtures.res +0 -1
  104. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +19 -21
  105. package/tests/commandgenerator/OwnerStampingTest.res +0 -1
  106. package/tests/commandgenerator/OwnerStampingTest.res.mjs +9 -11
  107. package/tests/commandtopic/CommandTopicCallbackFixtures.res +0 -1
  108. package/tests/commandtopic/CommandTopicCallbackFixtures.res.mjs +7 -9
  109. package/tests/counter/CounterFixtures.res +0 -1
  110. package/tests/counter/CounterFixtures.res.mjs +1 -4
  111. package/tests/dcb/DcbDecodeTest.res.mjs +24 -24
  112. package/tests/dcb/DcbEventLogOperationsTest.res +2 -2
  113. package/tests/dcb/DcbEventLogOperationsTest.res.mjs +3 -3
  114. package/tests/dcb/DcbEventLogSchemaTest.res.mjs +7 -7
  115. package/tests/dcb/DcbFixtures.res +0 -1
  116. package/tests/dcb/DcbFixtures.res.mjs +87 -89
  117. package/tests/dcb/DcbTagTest.res.mjs +6 -6
  118. package/tests/dcb/DcbValidationTest.res.mjs +52 -52
  119. package/tests/eventlog/EventLogFixtures.res +0 -1
  120. package/tests/eventlog/EventLogFixtures.res.mjs +7 -9
  121. package/tests/eventmapper/EventMapperFixtures.res +0 -1
  122. package/tests/eventmapper/EventMapperFixtures.res.mjs +24 -26
  123. package/tests/eventtopic/EventTopicFixtures.res +0 -1
  124. package/tests/eventtopic/EventTopicFixtures.res.mjs +7 -9
  125. package/tests/extensionpoint/ExtensionPointFixtures.res +0 -1
  126. package/tests/extensionpoint/ExtensionPointFixtures.res.mjs +10 -12
  127. package/tests/extensionpoint/ExtensionPointOperationsTest.res +1 -2
  128. package/tests/extensionpoint/ExtensionPointOperationsTest.res.mjs +9 -10
  129. package/tests/logger/LogFormatTest.res +3 -2
  130. package/tests/logger/LogFormatTest.res.mjs +7 -9
  131. package/tests/message/MessageTest.res +2 -2
  132. package/tests/message/MessageTest.res.mjs +8 -7
  133. package/tests/message/MetaEnvelopeTest.res +5 -5
  134. package/tests/message/MetaEnvelopeTest.res.mjs +12 -11
  135. package/tests/plugin/ManifestVisibilityTest.res.mjs +7 -7
  136. package/tests/plugin/PluginDefinitionScalars.res +5 -3
  137. package/tests/plugin/PluginDefinitionScalars.res.mjs +2 -2
  138. package/tests/plugin/PluginLifecycleCorpusTest.res +4 -3
  139. package/tests/plugin/PluginLifecycleCorpusTest.res.mjs +3 -6
  140. package/tests/plugin/PluginSpecExperiment.res +0 -1
  141. package/tests/plugin/PluginSpecExperiment.res.mjs +12 -14
  142. package/tests/plugin/PluginStructureTest.res +188 -1
  143. package/tests/plugin/PluginStructureTest.res.mjs +293 -102
  144. package/tests/plugin/Plugin_EventQuerySchemaTest.res.mjs +3 -3
  145. package/tests/plugin/SchemaWalkerTest.res +0 -1
  146. package/tests/plugin/SchemaWalkerTest.res.mjs +17 -19
  147. package/tests/plugin/StateChangeSlice/PsAttachInvoice.res.mjs +5 -5
  148. package/tests/plugin/StateChangeSlice/PsChangePhoto.res.mjs +8 -8
  149. package/tests/plugin/StateChangeSlice/PsDispatchShipment.res.mjs +5 -5
  150. package/tests/plugin/StateChangeSlice/PsGatedCommands.res.mjs +9 -9
  151. package/tests/plugin/StateChangeSlice/PsPlaceOrder.res.mjs +5 -5
  152. package/tests/plugin/StateChangeSlice/PsReserveStock.res.mjs +8 -8
  153. package/tests/plugin/StateChangeSlice/PsShipOrder.res.mjs +15 -15
  154. package/tests/plugin/StateChangeSlice/PsTypoStore.res +31 -0
  155. package/tests/plugin/StateChangeSlice/PsTypoStore.res.mjs +62 -0
  156. package/tests/plugin/StateChangeSlice/PsUploadAvatar.res.mjs +8 -8
  157. package/tests/plugin/StateChangeSlice/PsUploadImages.res +40 -0
  158. package/tests/plugin/StateChangeSlice/PsUploadImages.res.mjs +66 -0
  159. package/tests/plugin/StateViewSlice/PsAnnotatedView.res.mjs +13 -12
  160. package/tests/plugin/StateViewSlice/PsAvailableProductsView.res.mjs +8 -8
  161. package/tests/plugin/StateViewSlice/PsCategoriesView.res.mjs +6 -6
  162. package/tests/plugin/StateViewSlice/PsCustomersView.res.mjs +10 -10
  163. package/tests/plugin/StateViewSlice/PsGatedView.res.mjs +6 -6
  164. package/tests/plugin/StateViewSlice/PsOrdersView.res.mjs +9 -9
  165. package/tests/plugin/StateViewSlice/PsShipmentsView.res.mjs +11 -10
  166. package/tests/plugin/pluginDefinitionRequiredScalars.txt +15 -0
  167. package/tests/querydb/QueryDbFixtures.res +0 -1
  168. package/tests/querydb/QueryDbFixtures.res.mjs +5 -7
  169. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res +0 -1
  170. package/tests/sideeffecthandler/SideEffectHandlerFixtures.res.mjs +4 -6
  171. package/tests/util/CommandPublisherTest.res +0 -1
  172. package/tests/util/CommandPublisherTest.res.mjs +4 -6
@@ -41,8 +41,8 @@ let rec isLifecycleShape = (t: SchemaType.schemaType): bool =>
41
41
  // and *exactly*: `customerName` holds a customer's name, not this record's.
42
42
  let conventionalLabelNames = ["name", "title", "label", "displayname"]
43
43
 
44
- let shapeOfItem = (~entityName: string, item: S.item): SchemaType.schemaType =>
45
- SchemaType.fromSury(~parentName=entityName, ~fieldName=item.location, item.schema)
44
+ let shapeOfField = (~entityName: string, ~name: string, schema: S.t<unknown>): SchemaType.schemaType =>
45
+ SchemaType.fromSury(~parentName=entityName, ~fieldName=name, schema)
46
46
 
47
47
  // Resolve the field that holds the entity's lifecycle, used to filter a per-row
48
48
  // command menu against each command's `allowedStates`. Resolution order:
@@ -69,12 +69,14 @@ let lifecycleFieldFromStateSchema = (
69
69
  | Some(_) as some => some
70
70
  | None =>
71
71
  switch stateSchema {
72
- | Object({items}) =>
73
- items
74
- ->Array.find(item =>
75
- item.location == "lifecycle" && isLifecycleShape(shapeOfItem(~entityName, item))
72
+ | Object({properties}) =>
73
+ properties
74
+ ->Dict.get("lifecycle")
75
+ ->Option.flatMap(schema =>
76
+ isLifecycleShape(shapeOfField(~entityName, ~name="lifecycle", schema))
77
+ ? Some("lifecycle")
78
+ : None
76
79
  )
77
- ->Option.map(item => item.location)
78
80
  | _ => None
79
81
  }
80
82
  }
@@ -171,11 +173,11 @@ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): retire
171
173
  )
172
174
  }
173
175
  let declared = switch stateSchema {
174
- | Object({items}) =>
175
- items
176
- ->Array.find(item => item.location == field)
177
- ->Option.map(item =>
178
- switch shapeOfItem(~entityName, item) {
176
+ | Object({properties}) =>
177
+ properties
178
+ ->Dict.get(field)
179
+ ->Option.map(schema =>
180
+ switch shapeOfField(~entityName, ~name=field, schema) {
179
181
  | Enum(_, values) => values
180
182
  | Nullable(Enum(_, values)) => values
181
183
  | _ => []
@@ -245,11 +247,11 @@ let lifecycleStatesFromStateSchema = (
245
247
  ): option<array<string>> =>
246
248
  lifecycleFieldFromStateSchema(~entityName, stateSchema)->Option.flatMap(field =>
247
249
  switch stateSchema {
248
- | Object({items}) =>
249
- items
250
- ->Array.find(item => item.location == field)
251
- ->Option.map(item =>
252
- switch shapeOfItem(~entityName, item) {
250
+ | Object({properties}) =>
251
+ properties
252
+ ->Dict.get(field)
253
+ ->Option.map(schema =>
254
+ switch shapeOfField(~entityName, ~name=field, schema) {
253
255
  | Enum(_, values) => values
254
256
  | Nullable(Enum(_, values)) => values
255
257
  | _ => []
@@ -377,27 +379,21 @@ let lifecycleTopologyFindings = (
377
379
  lifecycleStatesByView
378
380
  ->Dict.toArray
379
381
  ->Array.forEach(((view, states)) => {
380
- // Every edge any command declares into or out of this view's lifecycle.
381
- let edges = writables->Array.reduce([], (acc, w) =>
382
+ // Every state some command declares a transition INTO. The question this
383
+ // check asks is only about arrival, so it reads the target and never the
384
+ // from-set — which is what lets a creating command (`@transition(() => X)`,
385
+ // a target and no from-set, because it runs from no row) count towards
386
+ // reachability the same way an edge out of another state does.
387
+ let reachable = writables->Array.reduce([], (acc, w) =>
382
388
  w.linkedViews->Array.includes(view)
383
- ? Array.concat(
384
- acc,
385
- w.commands->Array.reduce([], (inner, cmd) =>
386
- switch (cmd.allowedStates, cmd.targetState) {
387
- | (Some(froms), Some(to)) =>
388
- Array.concat(inner, froms->Array.map(from => (from, to)))
389
- | _ => inner
390
- }
391
- ),
392
- )
389
+ ? Array.concat(acc, w.commands->Array.filterMap(cmd => cmd.targetState))
393
390
  : acc
394
391
  )
395
- if Array.length(edges) > 0 {
392
+ if Array.length(reachable) > 0 {
396
393
  // Rows start in the first declared state — the same convention the
397
394
  // lifecycle diagram uses — so nothing pointing at it is expected rather
398
395
  // than suspicious.
399
396
  let initial = states->Array.get(0)
400
- let reachable = edges->Array.map(((_, to)) => to)
401
397
 
402
398
  states->Array.forEach(state => {
403
399
  if !(reachable->Array.includes(state)) && Some(state) != initial {
@@ -523,26 +519,26 @@ let labelFieldsFromStateSchema = (
523
519
  | Some(spec) => {field: "displayName", searchableFields: spec.fields, source: Annotation}
524
520
  | None =>
525
521
  let candidates = switch stateSchema {
526
- | Object({items}) =>
527
- items->Array.filter(item =>
528
- item.location != "TAG" &&
529
- item.location != "id" &&
530
- isLabelShape(shapeOfItem(~entityName, item))
522
+ | Object({properties}) =>
523
+ properties
524
+ ->Dict.toArray
525
+ ->Array.filter(((name, schema)) =>
526
+ name != "TAG" && name != "id" && isLabelShape(shapeOfField(~entityName, ~name, schema))
531
527
  )
532
528
  | _ => []
533
529
  }
534
- let conventional = candidates->Array.find(item => {
535
- let lower = item.location->String.toLowerCase
530
+ let conventional = candidates->Array.find(((name, _)) => {
531
+ let lower = name->String.toLowerCase
536
532
  conventionalLabelNames->Array.some(n => n == lower)
537
533
  })
538
534
  let picked = switch conventional {
539
- | Some(item) => Some((item, Convention))
540
- | None => candidates->Array.get(0)->Option.map(item => (item, Position))
535
+ | Some((name, _)) => Some((name, Convention))
536
+ | None => candidates->Array.get(0)->Option.map(((name, _)) => (name, Position))
541
537
  }
542
538
  switch picked {
543
- | Some((item, source)) => {
544
- field: item.location,
545
- searchableFields: [item.location],
539
+ | Some((name, source)) => {
540
+ field: name,
541
+ searchableFields: [name],
546
542
  source,
547
543
  }
548
544
  | None =>
@@ -613,7 +609,7 @@ let toEventDef = (v: S.t<unknown>): option<Reventless.Plugin.eventDef> => {
613
609
 
614
610
  let extractEventDefs = (eventSchema: S.t<unknown>): array<Reventless.Plugin.eventDef> =>
615
611
  switch eventSchema {
616
- | Union({anyOf}) => anyOf->Array.filterMap(toEventDef)
612
+ | AnyOf({anyOf}) => anyOf->Array.filterMap(toEventDef)
617
613
  | _ => toEventDef(eventSchema)->Option.mapOr([], def => [def])
618
614
  }
619
615
 
@@ -627,6 +623,302 @@ let extractErrorDefs = (errorSchema: S.t<unknown>): array<Reventless.Plugin.erro
627
623
  {name, schema, references}: Reventless.Plugin.errorDef
628
624
  ))
629
625
 
626
+ // The command walk, module-level for the same reason as the event walk above:
627
+ // the synthetic Platform_Admin structure cannot reach `make` — it has no
628
+ // `module(Aggregate.T)` to hand it — and hand-writing a second copy of this walk
629
+ // is what let its command metadata drift from the SDL generated off the same
630
+ // schema.
631
+
632
+ // Aggregate commands that initialize a new aggregate instance are Collection-level
633
+ // (shown as table-top buttons); all others are Instance-level (shown per-row).
634
+ let isCreateCommandName = name =>
635
+ ["Add", "Create", "Register", "Open", "Initialize", "Submit", "Start", "Place"]->Array.some(p =>
636
+ name->String.startsWith(p)
637
+ )
638
+
639
+ let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknown>>) =>
640
+ if isAggregate {
641
+ if isCreateCommandName(variantName) {
642
+ (Reventless.Plugin.Collection, None)
643
+ } else {
644
+ (Reventless.Plugin.Instance, None)
645
+ }
646
+ } else {
647
+ let taggedFields =
648
+ properties
649
+ ->Dict.toArray
650
+ ->Array.filter(((fieldName, fieldSchema)) =>
651
+ fieldName != "TAG" &&
652
+ (Reventless.DcbTag.isTagged(fieldSchema) ||
653
+ Reventless.DcbTag.isTaggedArray(fieldSchema))
654
+ )
655
+ let taggedField =
656
+ taggedFields
657
+ ->Array.find(((_, fieldSchema)) => Reventless.DcbTag.isPartitionTag(fieldSchema))
658
+ ->Option.orElse(taggedFields->Array.get(0))
659
+ switch taggedField {
660
+ | Some((fieldName, _)) =>
661
+ if isCreateCommandName(variantName) {
662
+ // Creation command: Collection-level, but UUID is injected into the tagged ID field.
663
+ (Reventless.Plugin.Collection, Some(fieldName))
664
+ } else {
665
+ (Reventless.Plugin.Instance, Some(fieldName))
666
+ }
667
+ | None => (Reventless.Plugin.Collection, None)
668
+ }
669
+ }
670
+
671
+ // A rule the server enforces, expressed as the keys a client checks against
672
+ // `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
673
+ // its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
674
+ // an any-of and a client must read it that way.
675
+ //
676
+ // `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
677
+ // anyone holding a session already satisfies them — so they publish no keys
678
+ // rather than a key everyone holds.
679
+ //
680
+ // `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
681
+ // as locked-with-upsell in a tiered shell: a surface advertised as purchasable
682
+ // that no purchase unlocks. A component nobody may call belongs in no menu, and
683
+ // that is an omission for the enumerating side to make, not a key to invent here.
684
+ let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
685
+ switch rule {
686
+ | AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
687
+ | AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
688
+ }
689
+
690
+ // Write each mutation argument's rendered GraphQL type onto the property it
691
+ // belongs to, so a consumer assembling its own mutation document declares the
692
+ // variable the server actually expects instead of guessing `String!`.
693
+ //
694
+ // Mutates the freshly derived schema in place — `deriveObjectSchema` has just
695
+ // built it and nothing else holds it yet. A property with no matching
696
+ // argument is left alone rather than annotated with a guess.
697
+ let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
698
+ schema
699
+ ->JSON.Decode.object
700
+ ->Option.flatMap(o => o->Dict.get("properties"))
701
+ ->Option.flatMap(JSON.Decode.object)
702
+ ->Option.forEach(props =>
703
+ props
704
+ ->Dict.toArray
705
+ ->Array.forEach(((key, prop)) =>
706
+ switch (argTypes->Dict.get(key), prop->JSON.Decode.object) {
707
+ | (Some(gqlType), Some(p)) =>
708
+ p->Dict.set("x-reventless-graphql-type", JSON.Encode.string(gqlType))
709
+ | _ => ()
710
+ }
711
+ )
712
+ )
713
+ schema
714
+ }
715
+
716
+ let toCommandDef = (
717
+ ~isAggregate,
718
+ ~mutationFieldFor: string => string,
719
+ ~parentSchema: S.t<unknown>,
720
+ // The PPX-generated `command => permission`. Per VARIANT, not per component:
721
+ // one aggregate carries commands with very different audiences, and a
722
+ // component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
723
+ ~commandAuthorization: unknown => Reventless.Authorization.permission,
724
+ v: S.t<unknown>,
725
+ ): option<Reventless.Plugin.commandDef> => {
726
+ // Build a commandDef for one variant. `properties` is the variant's field dict —
727
+ // empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
728
+ // `S.literal("Archive")` string rather than an `{TAG, ...}` object.
729
+ let mkDef = (~variantName, ~properties) => {
730
+ let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
731
+ let references = extractReferences(properties)
732
+ // Per-variant `allowedStates` lives on the *parent* command schema
733
+ // (the PPX attaches a single dict<variantName, [|states|]> via
734
+ // markAllowedStates). Look it up by variant name; back-compat
735
+ // None when the variant lacks a @transition annotation.
736
+ let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
737
+ // The `@transition` target (the command's *to* status), read the same way
738
+ // as allowedStates. None ⇒ AutoUI's board resolver falls back to its
739
+ // name-stem heuristic.
740
+ let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
741
+ // API-exposed iff the whole command isn't @noApi and this variant
742
+ // isn't in its @noApi-variants set — mirrors the API-generation filter
743
+ // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
744
+ let apiExposed =
745
+ !ApiNoApiHelpers.isNoApi(parentSchema) &&
746
+ switch ApiNoApiHelpers.getExcludedVariants(parentSchema) {
747
+ | Some(excluded) => !(excluded->Set.has(variantName))
748
+ | None => true
749
+ }
750
+ // Evaluated against a synthetic value per constructor, the same shape the
751
+ // resolver builds at call time: a payload-bearing variant compiles to
752
+ // `{TAG, ...}`, a payload-less one to a bare string.
753
+ let syntheticCommand: unknown =
754
+ Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
755
+ ? {"TAG": variantName}->Obj.magic
756
+ : variantName->Obj.magic
757
+ let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
758
+ // See the note on the record's `mutationField` for why a non-exposed
759
+ // variant gets the empty sentinel. It has no callable field, and the
760
+ // argument type names are composed *from* that field name, so there is
761
+ // nothing to publish for it either.
762
+ let mutationField = apiExposed ? mutationFieldFor(variantName) : ""
763
+ let jsonSchema = v->SuryToJsonSchema.deriveObjectSchema
764
+ let annotatedSchema = if apiExposed {
765
+ GraphQL_FragmentGenerator.mutationArgTypes(~fieldName=mutationField, v)->Option.mapOr(
766
+ jsonSchema,
767
+ annotateArgTypes(jsonSchema, _),
768
+ )
769
+ } else {
770
+ jsonSchema
771
+ }
772
+ ({
773
+ Reventless.Plugin.name: variantName,
774
+ // The derived schema, not sury's raw one: `S.toJSONSchema` carries the
775
+ // shape and drops every `x-reventless-*` marker the PPX put on the
776
+ // fields, so a command's `@storageRef`/`@semantic`/`@ref` reached the
777
+ // wire on the read side and nowhere on the write side. A reader that
778
+ // matches a field against its setter — or picks the upload endpoint of
779
+ // the store a command argument declares — then has nothing to match on.
780
+ // `MCP_SchemaGenerator` already derives these same variant schemas.
781
+ //
782
+ // Carries `x-reventless-graphql-type` per property — see
783
+ // `annotateArgTypes`.
784
+ schema: annotatedSchema->JSON.stringify,
785
+ level,
786
+ aggregateIdField,
787
+ // A non-exposed (`@noApi`) variant has no callable mutation field. For a
788
+ // single-exposed-command slice, `mutationFieldFor` resolves *every*
789
+ // variant — including the `@noApi` one — to the slice's one mutation
790
+ // field, so emitting it here would hand the non-exposed variant a
791
+ // sibling's callable-looking field (e.g. `ReopenOrder` →
792
+ // `Ordering_CancelOrder`). Emit an empty sentinel instead; the variant
793
+ // stays listed with `apiExposed: false` for the event-graph badge, but
794
+ // no consumer can mistake it for a callable field. Exposed variants are
795
+ // byte-identical.
796
+ mutationField,
797
+ references,
798
+ allowedStates,
799
+ targetState,
800
+ apiExposed: Some(apiExposed),
801
+ requiredAccess,
802
+ // Resolved from this constructor's own properties, not the union's: two
803
+ // commands in one slice can disagree about whether they record an owner,
804
+ // and the write path stamps per constructor for the same reason.
805
+ ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
806
+ }: Reventless.Plugin.commandDef)
807
+ }
808
+ switch v {
809
+ | Object({properties}) =>
810
+ properties
811
+ ->Dict.get("TAG")
812
+ ->Option.flatMap(tagSchema =>
813
+ switch tagSchema {
814
+ | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
815
+ | _ => None
816
+ }
817
+ )
818
+ // Payload-less command variants (`| Archive`) compile to a bare string literal,
819
+ // not an `{TAG, ...}` object. They still get a generated mutation (API generation
820
+ // walks the schema via extractAllVariantNames), so surface them here too —
821
+ // otherwise the event graph hides a command the API actually exposes.
822
+ | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
823
+ | _ => None
824
+ }
825
+ }
826
+
827
+ let extractCommandDefs = (
828
+ ~isAggregate,
829
+ ~mutationFieldFor: string => string,
830
+ ~commandAuthorization: unknown => Reventless.Authorization.permission,
831
+ commandSchema: S.t<unknown>,
832
+ ): array<Reventless.Plugin.commandDef> =>
833
+ switch commandSchema {
834
+ | AnyOf({anyOf}) =>
835
+ anyOf->Array.filterMap(v =>
836
+ toCommandDef(
837
+ ~isAggregate,
838
+ ~mutationFieldFor,
839
+ ~parentSchema=commandSchema,
840
+ ~commandAuthorization,
841
+ v,
842
+ )
843
+ )
844
+ | _ =>
845
+ // Single-variant command types compile to a bare Object schema, not a Union.
846
+ toCommandDef(
847
+ ~isAggregate,
848
+ ~mutationFieldFor,
849
+ ~parentSchema=commandSchema,
850
+ ~commandAuthorization,
851
+ commandSchema,
852
+ )->Option.mapOr([], def => [def])
853
+ }
854
+
855
+ // Omitted for the default `Public` so a published def stays compact: absent and
856
+ // "public" are the same answer, and writing one would make every ordinary view
857
+ // carry a word that says nothing.
858
+ let visibilityTag = (v: Reventless.Visibility.t): option<string> =>
859
+ switch v {
860
+ | Public => None
861
+ | Internal => Some("Internal")
862
+ }
863
+
864
+ /**
865
+ A read model's published `queryableDef`, assembled from its **spec** rather than
866
+ from a built component.
867
+
868
+ `make` cannot serve the platform's own components: it takes
869
+ `module(ReadModel.T)` values, and the platform has no built module to hand
870
+ itself at structure-assembly time — the components it is describing are the ones
871
+ being assembled. That is a real constraint and this routes around it rather than
872
+ pretending it away, by taking the schemas the extractors actually need.
873
+
874
+ Everything here is the same helper `make` calls on the same schema, so the
875
+ platform's own view is described by the mechanism that describes everyone
876
+ else's. The alternative — a hand-written record — is what let the admin's
877
+ metadata drift from the SDL generated off the same spec, four times over.
878
+
879
+ `queryField` and `singleQueryField` come from `Api_Naming`, which is the only
880
+ place that decides how a name singularises. For the admin these are
881
+ byte-identical to the names it used to hand-write; `singularize` already handled
882
+ the plural-spec-name / singular-type shape that looked bespoke.
883
+ */
884
+ let queryableDefFromSpec = (
885
+ ~plugin: string,
886
+ ~name: string,
887
+ ~stateSchema: S.t<unknown>,
888
+ ~authorization: Reventless.Authorization.permission,
889
+ ~visibility: Reventless.Visibility.t=Public,
890
+ ~consumedEventTypes: array<string>=[],
891
+ ~linkedWriteSide: array<string>=[],
892
+ ~chapter: option<string>=?,
893
+ ): Reventless.Plugin.queryableDef => {
894
+ let qf = Api_Naming.queryFieldNamesForReadModel(~plugin, ~name)
895
+ let label = labelFieldsFromStateSchema(~entityName=name, stateSchema)
896
+ // The same call the capability deriver makes, so the published key and the key
897
+ // the generated filter/order-by is built from cannot disagree.
898
+ let keyField = GraphQL_FragmentGenerator.resolveKeyField(~entityName=name, stateSchema)
899
+ {
900
+ Reventless.Plugin.name: name,
901
+ queryField: qf.listFieldName,
902
+ schema: stateSchema->SuryToJsonSchema.deriveObjectSchema->JSON.stringify,
903
+ consumedEventTypes,
904
+ linkedWriteSide,
905
+ labelField: label.field,
906
+ searchableFields: label.searchableFields,
907
+ labelFieldSource: Some(labelFieldSourceToString(label.source)),
908
+ lifecycleField: lifecycleFieldFromStateSchema(~entityName=name, stateSchema),
909
+ ownerField: Reventless.Owner.fieldNames(stateSchema)->Array.get(0),
910
+ retiredField: retiredFieldFromStateSchema(stateSchema),
911
+ retiredValues: retiredValuesFromStateSchema(stateSchema),
912
+ namedWhenRetired: Some(namedWhenRetiredFromStateSchema(stateSchema)),
913
+ visibility: visibilityTag(visibility),
914
+ chapter,
915
+ singleQueryField: Some(qf.singleFieldName),
916
+ idField: keyField->Option.map(((f, _)) => f),
917
+ idFieldSource: keyField->Option.map(((_, rung)) => rung),
918
+ requiredAccess: accessKeysFor(authorization),
919
+ }
920
+ }
921
+
630
922
  let make = (
631
923
  type api role,
632
924
  ~name: string,
@@ -657,229 +949,6 @@ let make = (
657
949
  let commandVariantNames = schema => Reventless.DcbTag.extractAllVariantNames(schema)
658
950
  let qualify = (~prefix, names) => names->Array.map(n => prefix ++ "." ++ n)
659
951
 
660
- // Aggregate commands that initialize a new aggregate instance are Collection-level
661
- // (shown as table-top buttons); all others are Instance-level (shown per-row).
662
- let isCreateCommandName = name =>
663
- ["Add", "Create", "Register", "Open", "Initialize", "Submit", "Start", "Place"]->Array.some(p =>
664
- name->String.startsWith(p)
665
- )
666
-
667
- let commandLevelAndId = (~isAggregate, ~variantName, properties: dict<S.t<unknown>>) =>
668
- if isAggregate {
669
- if isCreateCommandName(variantName) {
670
- (Reventless.Plugin.Collection, None)
671
- } else {
672
- (Reventless.Plugin.Instance, None)
673
- }
674
- } else {
675
- let taggedFields =
676
- properties
677
- ->Dict.toArray
678
- ->Array.filter(((fieldName, fieldSchema)) =>
679
- fieldName != "TAG" &&
680
- (Reventless.DcbTag.isTagged(fieldSchema) ||
681
- Reventless.DcbTag.isTaggedArray(fieldSchema))
682
- )
683
- let taggedField =
684
- taggedFields
685
- ->Array.find(((_, fieldSchema)) => Reventless.DcbTag.isPartitionTag(fieldSchema))
686
- ->Option.orElse(taggedFields->Array.get(0))
687
- switch taggedField {
688
- | Some((fieldName, _)) =>
689
- if isCreateCommandName(variantName) {
690
- // Creation command: Collection-level, but UUID is injected into the tagged ID field.
691
- (Reventless.Plugin.Collection, Some(fieldName))
692
- } else {
693
- (Reventless.Plugin.Instance, Some(fieldName))
694
- }
695
- | None => (Reventless.Plugin.Collection, None)
696
- }
697
- }
698
-
699
- // A rule the server enforces, expressed as the keys a client checks against
700
- // `identity.groups ++ config.accessTiers`. `AllowGroups` is satisfied by ANY of
701
- // its groups (`Authorization.isAllowed` is `some`, not `every`), so the array is
702
- // an any-of and a client must read it that way.
703
- //
704
- // `AllowAuthenticated` / `AllowAnonymous` ask for nothing a client can check —
705
- // anyone holding a session already satisfies them — so they publish no keys
706
- // rather than a key everyone holds.
707
- //
708
- // `DenyAll` also publishes none, deliberately. An unsatisfiable key would render
709
- // as locked-with-upsell in a tiered shell: a surface advertised as purchasable
710
- // that no purchase unlocks. A component nobody may call belongs in no menu, and
711
- // that is an omission for the enumerating side to make, not a key to invent here.
712
- let accessKeysFor = (rule: Reventless.Authorization.permission): option<array<string>> =>
713
- switch rule {
714
- | AllowGroups(groups) if groups->Array.length > 0 => Some(groups)
715
- | AllowGroups(_) | AllowAuthenticated | AllowAnonymous | DenyAll => None
716
- }
717
-
718
- // Write each mutation argument's rendered GraphQL type onto the property it
719
- // belongs to, so a consumer assembling its own mutation document declares the
720
- // variable the server actually expects instead of guessing `String!`.
721
- //
722
- // Mutates the freshly derived schema in place — `deriveObjectSchema` has just
723
- // built it and nothing else holds it yet. A property with no matching
724
- // argument is left alone rather than annotated with a guess.
725
- let annotateArgTypes = (schema: JSON.t, argTypes: dict<string>): JSON.t => {
726
- schema
727
- ->JSON.Decode.object
728
- ->Option.flatMap(o => o->Dict.get("properties"))
729
- ->Option.flatMap(JSON.Decode.object)
730
- ->Option.forEach(props =>
731
- props
732
- ->Dict.toArray
733
- ->Array.forEach(((key, prop)) =>
734
- switch (argTypes->Dict.get(key), prop->JSON.Decode.object) {
735
- | (Some(gqlType), Some(p)) =>
736
- p->Dict.set("x-reventless-graphql-type", JSON.Encode.string(gqlType))
737
- | _ => ()
738
- }
739
- )
740
- )
741
- schema
742
- }
743
-
744
- let toCommandDef = (
745
- ~isAggregate,
746
- ~mutationFieldFor: string => string,
747
- ~parentSchema: S.t<unknown>,
748
- // The PPX-generated `command => permission`. Per VARIANT, not per component:
749
- // one aggregate carries commands with very different audiences, and a
750
- // component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
751
- ~commandAuthorization: unknown => Reventless.Authorization.permission,
752
- v: S.t<unknown>,
753
- ): option<Reventless.Plugin.commandDef> => {
754
- // Build a commandDef for one variant. `properties` is the variant's field dict —
755
- // empty for a payload-less variant (e.g. `| Archive`), which compiles to a bare
756
- // `S.literal("Archive")` string rather than an `{TAG, ...}` object.
757
- let mkDef = (~variantName, ~properties) => {
758
- let (level, aggregateIdField) = commandLevelAndId(~isAggregate, ~variantName, properties)
759
- let references = extractReferences(properties)
760
- // Per-variant `allowedStates` lives on the *parent* command schema
761
- // (the PPX attaches a single dict<variantName, [|states|]> via
762
- // markAllowedStates). Look it up by variant name; back-compat
763
- // None when the variant lacks a @transition annotation.
764
- let allowedStates = ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName)
765
- // The `@transition` target (the command's *to* status), read the same way
766
- // as allowedStates. None ⇒ AutoUI's board resolver falls back to its
767
- // name-stem heuristic.
768
- let targetState = ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName)
769
- // API-exposed iff the whole command isn't @noApi and this variant
770
- // isn't in its @noApi-variants set — mirrors the API-generation filter
771
- // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
772
- let apiExposed =
773
- !ApiNoApiHelpers.isNoApi(parentSchema) &&
774
- switch ApiNoApiHelpers.getExcludedVariants(parentSchema) {
775
- | Some(excluded) => !(excluded->Set.has(variantName))
776
- | None => true
777
- }
778
- // Evaluated against a synthetic value per constructor, the same shape the
779
- // resolver builds at call time: a payload-bearing variant compiles to
780
- // `{TAG, ...}`, a payload-less one to a bare string.
781
- let syntheticCommand: unknown =
782
- Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
783
- ? {"TAG": variantName}->Obj.magic
784
- : variantName->Obj.magic
785
- let requiredAccess = accessKeysFor(commandAuthorization(syntheticCommand))
786
- // See the note on the record's `mutationField` for why a non-exposed
787
- // variant gets the empty sentinel. It has no callable field, and the
788
- // argument type names are composed *from* that field name, so there is
789
- // nothing to publish for it either.
790
- let mutationField = apiExposed ? mutationFieldFor(variantName) : ""
791
- let jsonSchema = v->SuryToJsonSchema.deriveObjectSchema
792
- let annotatedSchema = if apiExposed {
793
- GraphQL_FragmentGenerator.mutationArgTypes(~fieldName=mutationField, v)->Option.mapOr(
794
- jsonSchema,
795
- annotateArgTypes(jsonSchema, _),
796
- )
797
- } else {
798
- jsonSchema
799
- }
800
- ({
801
- Reventless.Plugin.name: variantName,
802
- // The derived schema, not sury's raw one: `S.toJSONSchema` carries the
803
- // shape and drops every `x-reventless-*` marker the PPX put on the
804
- // fields, so a command's `@storageRef`/`@semantic`/`@ref` reached the
805
- // wire on the read side and nowhere on the write side. A reader that
806
- // matches a field against its setter — or picks the upload endpoint of
807
- // the store a command argument declares — then has nothing to match on.
808
- // `MCP_SchemaGenerator` already derives these same variant schemas.
809
- //
810
- // Carries `x-reventless-graphql-type` per property — see
811
- // `annotateArgTypes`.
812
- schema: annotatedSchema->JSON.stringify,
813
- level,
814
- aggregateIdField,
815
- // A non-exposed (`@noApi`) variant has no callable mutation field. For a
816
- // single-exposed-command slice, `mutationFieldFor` resolves *every*
817
- // variant — including the `@noApi` one — to the slice's one mutation
818
- // field, so emitting it here would hand the non-exposed variant a
819
- // sibling's callable-looking field (e.g. `ReopenOrder` →
820
- // `Ordering_CancelOrder`). Emit an empty sentinel instead; the variant
821
- // stays listed with `apiExposed: false` for the event-graph badge, but
822
- // no consumer can mistake it for a callable field. Exposed variants are
823
- // byte-identical.
824
- mutationField,
825
- references,
826
- allowedStates,
827
- targetState,
828
- apiExposed: Some(apiExposed),
829
- requiredAccess,
830
- // Resolved from this constructor's own properties, not the union's: two
831
- // commands in one slice can disagree about whether they record an owner,
832
- // and the write path stamps per constructor for the same reason.
833
- ownerField: Reventless.Owner.fieldNamesOfProperties(properties)->Array.get(0),
834
- }: Reventless.Plugin.commandDef)
835
- }
836
- switch v {
837
- | Object({properties}) =>
838
- properties
839
- ->Dict.get("TAG")
840
- ->Option.flatMap(tagSchema =>
841
- switch tagSchema {
842
- | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties))
843
- | _ => None
844
- }
845
- )
846
- // Payload-less command variants (`| Archive`) compile to a bare string literal,
847
- // not an `{TAG, ...}` object. They still get a generated mutation (API generation
848
- // walks the schema via extractAllVariantNames), so surface them here too —
849
- // otherwise the event graph hides a command the API actually exposes.
850
- | String({const: ?Some(variantName)}) => Some(mkDef(~variantName, ~properties=Dict.make()))
851
- | _ => None
852
- }
853
- }
854
-
855
- let extractCommandDefs = (
856
- ~isAggregate,
857
- ~mutationFieldFor: string => string,
858
- ~commandAuthorization: unknown => Reventless.Authorization.permission,
859
- commandSchema: S.t<unknown>,
860
- ): array<Reventless.Plugin.commandDef> =>
861
- switch commandSchema {
862
- | Union({anyOf}) =>
863
- anyOf->Array.filterMap(v =>
864
- toCommandDef(
865
- ~isAggregate,
866
- ~mutationFieldFor,
867
- ~parentSchema=commandSchema,
868
- ~commandAuthorization,
869
- v,
870
- )
871
- )
872
- | _ =>
873
- // Single-variant command types compile to a bare Object schema, not a Union.
874
- toCommandDef(
875
- ~isAggregate,
876
- ~mutationFieldFor,
877
- ~parentSchema=commandSchema,
878
- ~commandAuthorization,
879
- commandSchema,
880
- )->Option.mapOr([], def => [def])
881
- }
882
-
883
952
  // ── Per-component event type extraction ────────────────────────────────────
884
953
 
885
954
  let scsProduced =
@@ -1035,7 +1104,7 @@ let make = (
1035
1104
  | _ => []
1036
1105
  }
1037
1106
  switch schema {
1038
- | Union({anyOf}) => anyOf->Array.flatMap(fromVariant)
1107
+ | AnyOf({anyOf}) => anyOf->Array.flatMap(fromVariant)
1039
1108
  | other => fromVariant(other)
1040
1109
  }
1041
1110
  }
@@ -1107,6 +1176,18 @@ let make = (
1107
1176
  ->Belt.Set.String.fromArray
1108
1177
  ->Belt.Set.String.toArray
1109
1178
 
1179
+ // Two stores one edit apart are a typo that would provision twice. A hard
1180
+ // failure, unlike the heuristic lint below: nothing downstream can see the
1181
+ // mistake, and by deploy time both buckets exist.
1182
+ switch Capability_Inference.collisions(requiredStoreDeclarations) {
1183
+ | [] => ()
1184
+ | found =>
1185
+ JsError.throwWithMessage(
1186
+ `${name}: two declared object stores look like one store misspelled.\n` ++
1187
+ found->Array.map(Capability_Inference.collisionMessage)->Array.join("\n"),
1188
+ )
1189
+ }
1190
+
1110
1191
  // Heuristic-only matches — a field *named* like a stored-object ref with no
1111
1192
  // `@storageRef` — warn and provision nothing. Declaration outranks inference;
1112
1193
  // the warning names the annotation that would settle it.
@@ -1124,12 +1205,6 @@ let make = (
1124
1205
  // graph and dead-code analysis — read them so an Internal view still shows up there. The
1125
1206
  // deployed AutoUI's consumers (Platform_ComponentDefinitionsApi menu/pages) re-filter on
1126
1207
  // the tag so the live UI keeps hiding them — see Visibility.res, which documents this contract.
1127
- let visibilityTag = (v: Reventless.Visibility.t): option<string> =>
1128
- switch v {
1129
- | Public => None
1130
- | Internal => Some("Internal")
1131
- }
1132
-
1133
1208
  // View name -> the states its lifecycle field can hold, collected as the view
1134
1209
  // defs are built so the transition check below has both sides in one place.
1135
1210
  let lifecycleStatesByView: dict<array<string>> = Dict.make()