@reventlessdev/reventless-spec 3.0.0-alpha.123 → 3.0.0-alpha.125

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 (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +5 -3
  3. package/run-certify-trait.mjs +2 -0
  4. package/run-graft-trait.mjs +2 -0
  5. package/run-trait-manifest.mjs +2 -0
  6. package/schema/platform-api.graphql +41 -3
  7. package/src/components/Aggregate.res +22 -0
  8. package/src/components/AutomationSlice.res +29 -3
  9. package/src/components/CapabilityManifest.res +52 -24
  10. package/src/components/CapabilityManifest.res.mjs +35 -11
  11. package/src/components/InboundTranslationSlice.res +14 -0
  12. package/src/components/OutboundTranslationSlice.res +24 -0
  13. package/src/components/Plugin.res +202 -422
  14. package/src/components/Plugin.res.mjs +65 -3
  15. package/src/components/StateChangeSlice.res +22 -0
  16. package/src/components/TraitCertificate.res +105 -0
  17. package/src/components/TraitCertificate.res.mjs +65 -0
  18. package/src/components/TraitManifest.res +90 -0
  19. package/src/components/TraitManifest.res.mjs +48 -0
  20. package/src/generator/CertifyTrait.res +190 -0
  21. package/src/generator/CertifyTrait.res.mjs +154 -0
  22. package/src/generator/GraftTrait.res +230 -0
  23. package/src/generator/GraftTrait.res.mjs +193 -0
  24. package/src/generator/PlatformCodegen.res +44 -30
  25. package/src/generator/PlatformCodegen.res.mjs +36 -17
  26. package/src/generator/TraitManifestCli.res +138 -0
  27. package/src/generator/TraitManifestCli.res.mjs +105 -0
  28. package/src/semantic/Capabilities.res +19 -3
  29. package/src/semantic/Capabilities.res.mjs +18 -2
  30. package/src/semantic/CapabilityNeed.res +81 -0
  31. package/src/semantic/CapabilityNeed.res.mjs +46 -0
  32. package/src/semantic/Currency.res +519 -502
  33. package/src/semantic/Currency.res.mjs +7 -655
  34. package/src/semantic/Messaging.res +127 -0
  35. package/src/semantic/Messaging.res.mjs +57 -0
  36. package/src/semantic/Money.res +123 -21
  37. package/src/semantic/Money.res.mjs +49 -2
  38. package/src/types/Trait.res +62 -0
  39. package/src/types/Trait.res.mjs +18 -0
  40. package/src/types/Transition.res +71 -0
  41. package/src/types/Transition.res.mjs +36 -0
  42. package/scripts/generate-currency.mjs +0 -215
  43. package/scripts/iso-4217-list-one.xml +0 -1956
package/CHANGELOG.md CHANGED
@@ -3,6 +3,62 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.125 (2026-09-01)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **automation:** a mapping is handed the envelope's id ([9ee482c](https://github.com/ReventlessDev/reventless-core/commit/9ee482c5ad0aae09efd1259eeb7f39b781867b91))
11
+ * **capabilities:** a plugin's geocoding need was declared nowhere and failed silently ([67917dd](https://github.com/ReventlessDev/reventless-core/commit/67917dd504b43fa78b7c6a51644c9eae656b7f6b))
12
+ * feat(example)!: model product and category images as attachment sets ([6ae18d8](https://github.com/ReventlessDev/reventless-core/commit/6ae18d896215b448177ba0516e74bfda5f88d2db))
13
+ ### Features
14
+
15
+ * **capabilities:** a plugin can send a message without naming a provider ([b001a1e](https://github.com/ReventlessDev/reventless-core/commit/b001a1e9361a0f4d0affe228cb4c08c38a5a995e))
16
+ * **plugin:** the admin lifecycle commands name their argument ([ea552ec](https://github.com/ReventlessDev/reventless-core/commit/ea552ec64e5db0b3dc465c0d9a80cac89627f727))
17
+ * **spec:** a command declares its lifecycle edge as a value ([40eee9f](https://github.com/ReventlessDev/reventless-core/commit/40eee9f7723dc05e418be680528f01967d074da4))
18
+ * **spec:** a graft leaves a trace the deployed plugin can read ([c08ff6c](https://github.com/ReventlessDev/reventless-core/commit/c08ff6c0f6177d58603e7ae1e5cec392d9bac16a))
19
+ * **spec:** graft-trait, a CLI that runs a trait's emitter ([1b87399](https://github.com/ReventlessDev/reventless-core/commit/1b87399987033765d6a1ee8ea22ab7f02056e9c0))
20
+ * **traits:** a conformance run leaves something a machine can read ([cd9cb81](https://github.com/ReventlessDev/reventless-core/commit/cd9cb81aa643f8e30ccf072df458fdc136897746))
21
+ * **traits:** a listing reads a trait instead of being told about it ([8a23219](https://github.com/ReventlessDev/reventless-core/commit/8a23219c5a69011ef9310ebf8bfcbf9315a577ba))
22
+
23
+ ### BREAKING CHANGES
24
+
25
+ * ProductAdded/CategoryAdded lose their image field and
26
+ ChangeProductImage/ChangeCategoryImage are replaced; the alpha event log is
27
+ wiped on the next deploy.
28
+
29
+
30
+
31
+ # 3.0.0-alpha.124 (2026-08-27)
32
+
33
+ * feat(spec)!: reflect the command direction across a port, both halves ([f2fe258](https://github.com/ReventlessDev/reventless-core/commit/f2fe258d195b74f4a61488edee305665341020ea))
34
+ * feat(spec)!: read the port's translation table off the mapping's arms ([956348a](https://github.com/ReventlessDev/reventless-core/commit/956348a9fcb256cfff9db51809bdc27d73360e6c))
35
+ * feat(spec)!: keep the amount in minor units, and add times and allocate ([6ff6308](https://github.com/ReventlessDev/reventless-core/commit/6ff630875f9b126b2f87b980a4dbb56582f8aebe))
36
+ * feat(spec)!: hold a money amount in the currency's own units ([94d3bba](https://github.com/ReventlessDev/reventless-core/commit/94d3bbaf0c47a639c20de6847c4615070ad51b38))
37
+
38
+ ### BREAKING CHANGES
39
+
40
+ * `ExtensionPointMapping.Mapping` gains `acceptedCommands` and
41
+ `ExtensionMapping.Mapping` gains `issuedCommands`; both are derived, so a
42
+ mapping the ppx can read needs no source change, and one it cannot names itself
43
+ at compile time. Definitions persisted before the two new def fields must be
44
+ re-emitted before a consumer can read them as present — and until then they
45
+ decode as None, which means unknown, not "issues nothing".
46
+ * `ExtensionPointMapping.Mapping` requires `publishedEvents` and
47
+ `ExtensionMapping.Mapping` requires `handledEvents`; the ppx injects both for
48
+ mappings it can read, so app code must build against the matching ppx. Plugin
49
+ definitions persisted before the new fields must be re-emitted before a consumer
50
+ reads them as present.
51
+ * reverts the wire shape to whole minor units, so a log written
52
+ against the intervening commit reads a hundredfold low. Nothing released carried
53
+ the decimal form.
54
+ * stored amounts do not migrate. A log written before this holds
55
+ minor units and now decodes a hundredfold high, cleanly and without complaint —
56
+ a log that has to survive needs an upcaster written before this is deployed, and
57
+ a demo log needs discarding and reseeding. A stored currency outside the ten no
58
+ longer decodes; admit it in the generator or migrate the data.
59
+
60
+
61
+
6
62
  # 3.0.0-alpha.123 (2026-08-23)
7
63
 
8
64
  ### Features
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.123",
3
+ "version": "3.0.0-alpha.125",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
7
7
  "generate-plugin": "./run-generator.mjs",
8
- "generate-platform": "./run-platform-generator.mjs"
8
+ "generate-platform": "./run-platform-generator.mjs",
9
+ "graft-trait": "./run-graft-trait.mjs",
10
+ "certify-trait": "./run-certify-trait.mjs",
11
+ "trait-manifest": "./run-trait-manifest.mjs"
9
12
  },
10
13
  "jest": {
11
14
  "testMatch": [
@@ -42,7 +45,6 @@
42
45
  "build": "rescript build",
43
46
  "start": "rescript start",
44
47
  "clean": "rescript clean",
45
- "generate:currency": "node ./scripts/generate-currency.mjs",
46
48
  "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
47
49
  "dev": "NODE_OPTIONS='--experimental-vm-modules' jest --watchAll"
48
50
  }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./src/generator/CertifyTrait.res.mjs"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./src/generator/GraftTrait.res.mjs"
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "./src/generator/TraitManifestCli.res.mjs"
@@ -18,9 +18,9 @@ union CommandResult = CommandAccepted | CommandPending | CommandRejected
18
18
 
19
19
  type Mutation {
20
20
  Platform_PluginStatusChanged(pluginId: ID!, status: PluginStatus!): PluginStatusChangeEvent
21
- Platform_Plugin_Activate(_0: String!, id: ID!): CommandResult!
22
- Platform_Plugin_Deactivate(_0: String!, id: ID!): CommandResult!
23
- Platform_Plugin_Retire(_0: String!, id: ID!): CommandResult!
21
+ Platform_Plugin_Activate(id: ID!, version: String!): CommandResult!
22
+ Platform_Plugin_Deactivate(id: ID!, version: String!): CommandResult!
23
+ Platform_Plugin_Retire(id: ID!, version: String!): CommandResult!
24
24
  Platform_UIFragmentDeregistered(pluginId: ID!): UIFragmentChangeEvent
25
25
  Platform_UIFragmentRegistered(manifest: String, pluginId: ID!): UIFragmentChangeEvent
26
26
  Platform_UIFragmentUpdated(manifest: String, pluginId: ID!): UIFragmentChangeEvent
@@ -37,6 +37,11 @@ type PageInfo {
37
37
  startCursor: String
38
38
  }
39
39
 
40
+ type Platform_AcceptedCommandDef {
41
+ name: String!
42
+ toCommandTypes: [String!]!
43
+ }
44
+
40
45
  type Platform_AutomationSliceDef {
41
46
  chapter: String
42
47
  consumedEventTypes: [String!]!
@@ -88,13 +93,17 @@ type Platform_ExtensionDef {
88
93
  commandTypes: [String!]!
89
94
  delegateNames: [String!]!
90
95
  eventTypes: [String!]!
96
+ handledEvents: [Platform_HandledEventDef!]
97
+ issuedCommands: [Platform_IssuedCommandDef!]
91
98
  name: String!
92
99
  }
93
100
 
94
101
  type Platform_ExtensionPointDef {
102
+ acceptedCommands: [Platform_AcceptedCommandDef!]
95
103
  commandTypes: [String!]
96
104
  delegateNames: [String!]!
97
105
  name: String!
106
+ publishedEvents: [Platform_PublishedEventDef!]
98
107
  sourceEventTypes: [String!]!
99
108
  }
100
109
 
@@ -104,6 +113,11 @@ type Platform_FieldReference {
104
113
  plugin: String
105
114
  }
106
115
 
116
+ type Platform_HandledEventDef {
117
+ name: String!
118
+ toCommandTypes: [String!]!
119
+ }
120
+
107
121
  type Platform_InboundTranslationSliceDef {
108
122
  chapter: String
109
123
  commandTypes: [String!]!
@@ -112,6 +126,11 @@ type Platform_InboundTranslationSliceDef {
112
126
  targetName: String
113
127
  }
114
128
 
129
+ type Platform_IssuedCommandDef {
130
+ fromEventTypes: [String!]!
131
+ name: String!
132
+ }
133
+
115
134
  type Platform_OutboundTranslationSliceDef {
116
135
  chapter: String
117
136
  consumedEventTypes: [String!]!
@@ -206,10 +225,17 @@ type Platform_PluginStructureEntry {
206
225
  outboundTranslationSlices: [Platform_OutboundTranslationSliceDef!]!
207
226
  pluginId: String!
208
227
  readModels: [Platform_ReadSideDef!]!
228
+ requiredCapabilities: [Platform_RequiredCapabilityDeclaration!]
209
229
  requiredStoreDeclarations: [Platform_RequiredStoreDeclaration!]
210
230
  requiredStores: [String!]
211
231
  stateChangeSlices: [Platform_WriteSideDef!]!
212
232
  stateViewSlices: [Platform_ReadSideDef!]!
233
+ traitDeclarations: [Platform_TraitDeclaration!]
234
+ }
235
+
236
+ type Platform_PublishedEventDef {
237
+ fromEventTypes: [String!]!
238
+ name: String!
213
239
  }
214
240
 
215
241
  type Platform_ReadSideDef {
@@ -234,6 +260,11 @@ type Platform_ReadSideDef {
234
260
  visibility: String
235
261
  }
236
262
 
263
+ type Platform_RequiredCapabilityDeclaration {
264
+ capability: String!
265
+ component: String!
266
+ }
267
+
237
268
  type Platform_RequiredStoreDeclaration {
238
269
  annotation: String
239
270
  component: String!
@@ -241,6 +272,13 @@ type Platform_RequiredStoreDeclaration {
241
272
  store: String!
242
273
  }
243
274
 
275
+ type Platform_TraitDeclaration {
276
+ component: String!
277
+ posture: String!
278
+ trait: String!
279
+ version: String!
280
+ }
281
+
244
282
  type Platform_UIFragmentEntry {
245
283
  pages: [Platform_UIPage!]!
246
284
  panels: [Platform_UIPanel!]!
@@ -61,4 +61,26 @@ module type Spec = {
61
61
  `AllowAuthenticated`; override at the file/module level with
62
62
  `@@reventless.authorize(<rule>)`. */
63
63
  let commandAuthorization: command => Authorization.permission
64
+
65
+ /** The lifecycle enum this component's commands move a row through — the
66
+ linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
67
+ Auto-injected as `unit` alongside the default below; a host that declares
68
+ `commandTransition` declares this too, and the pair is what makes every
69
+ edge name one lifecycle. */
70
+ type lifecycleState
71
+
72
+ /** The lifecycle edge each command owns, read while the plugin structure is
73
+ assembled. Auto-injected as `_ => Unrestricted` by `@@reventless.spec`,
74
+ which leaves `@transition` in charge; a host that writes the switch by
75
+ hand takes charge instead, and gets an exhaustive one over typed states.
76
+ See `Transition`. */
77
+ let commandTransition: command => Transition.t<lifecycleState>
78
+
79
+ /** The domain traits grafted into this component, as values the trait packages
80
+ export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
81
+ by `@@reventless.spec`, so a component that is nobody's graft says so without
82
+ a line. A graft names its trait here and the structure records it, which is
83
+ the only way a deployed plugin can answer "where did this come from". See
84
+ `Trait`. */
85
+ let traits: array<Trait.t>
64
86
  }
@@ -83,6 +83,16 @@ module type Spec = {
83
83
  // validation step run before each command publish. `process` remains shared in
84
84
  // the slice's `Automation` module since it operates on `todoItem` regardless of
85
85
  // the originating source.
86
+ //
87
+ // `collect` is handed `~sourceId` alongside the event, so a mapping over an
88
+ // Aggregate source can key its todo item even though the payload does not repeat
89
+ // the id that addressed it — `Registered({email, address})` is the case this
90
+ // exists for. A DCB event usually names its own subject and can ignore it.
91
+ //
92
+ // An `OutboundTranslationSlice` remains the better shape for a relay that simply
93
+ // forwards: its item completes when the command is published rather than when an
94
+ // answering event arrives, so the target command is free to be idempotent instead
95
+ // of having to emit a fact whose only job is closing the row.
86
96
 
87
97
  /**
88
98
  Ambient deployment context plumbed to every mapping function.
@@ -125,7 +135,15 @@ module type Mapping = {
125
135
  type command
126
136
  let sourceEventSchema: S.t<sourceEvent>
127
137
  let sourceName: string
128
- let collect: (sourceEvent, context) => array<(string, todoItem)>
138
+ /**
139
+ `~sourceId` is the id of the entity the event was published for — the
140
+ envelope's `id`, not part of the event payload. A DCB event usually names its
141
+ own subject (`OrderPlaced({orderId, …})`) and can ignore this; an Aggregate's
142
+ event generally does not, because the aggregate id is what addressed it in the
143
+ first place. Without it a mapping over `Registered({email, address})` would
144
+ have no way to say which customer its todo item is for.
145
+ */
146
+ let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
129
147
  let resolve: sourceEvent => option<string>
130
148
  }
131
149
 
@@ -199,7 +217,15 @@ module type MappingImpl = {
199
217
  type sourceEvent
200
218
  type todoItem
201
219
  type command
202
- let collect: (sourceEvent, context) => array<(string, todoItem)>
220
+ /**
221
+ `~sourceId` is the id of the entity the event was published for — the
222
+ envelope's `id`, not part of the event payload. A DCB event usually names its
223
+ own subject (`OrderPlaced({orderId, …})`) and can ignore this; an Aggregate's
224
+ event generally does not, because the aggregate id is what addressed it in the
225
+ first place. Without it a mapping over `Registered({email, address})` would
226
+ have no way to say which customer its todo item is for.
227
+ */
228
+ let collect: (sourceEvent, ~sourceId: string, context) => array<(string, todoItem)>
203
229
  let resolve: sourceEvent => option<string>
204
230
  }
205
231
 
@@ -242,7 +268,7 @@ module FromOrderShipped = Reventless.AutomationSlice.Mapping.Make(
242
268
  OrderSpec, // Source: Aggregate spec module
243
269
  AutoFulfillmentSpec, // Target: this slice's spec
244
270
  {
245
- let collect = (event, _ctx) =>
271
+ let collect = (event, ~sourceId as _, _ctx) =>
246
272
  switch event {
247
273
  | OrderSpec.OrderShipped({orderId, productId}) =>
248
274
  [(orderId ++ ":" ++ productId, {AutoFulfillmentSpec.orderId, productId})]
@@ -2,29 +2,38 @@
2
2
  The per-plugin capability manifest — `capabilities.json`, written beside the
3
3
  generated `Plugin.res` by the plugin package's build.
4
4
 
5
- Each entry states one capability the plugin's fields declare they need, keyed
6
- by the store's qualified `{plugin}.{store}` identity, with the declaring
7
- `(component, field)` sites as provenance. The keys are taken verbatim from
8
- `pluginStructure.requiredStores` — the manifest is a rendering of the
9
- structure, never a second scan of the sources, so the two cannot spell one
10
- fact differently.
5
+ Each entry states one capability the plugin declares it needs, keyed by that
6
+ capability's identity — the qualified `{plugin}.{store}` for an object store, the
7
+ capability's own name for everything else — with the declaring sites as
8
+ provenance. The keys are taken verbatim from `pluginStructure`: the manifest is a
9
+ rendering of the structure, never a second scan of the sources, so the two cannot
10
+ spell one fact differently.
11
11
 
12
12
  The platform generator unions these files across a deployment's plugins and
13
13
  emits the platform's capability list from them.
14
14
  */
15
15
 
16
16
  @schema
17
- type kind = ObjectStore
17
+ type kind =
18
+ | ObjectStore
19
+ /** Address geocoding, reached through `Capabilities.geocode`. Declared by a
20
+ slice rather than by a field, so its entry carries no `field`. */
21
+ | Geocoding
22
+ /** Sending a message, reached through `Capabilities.messaging`. Slice-declared
23
+ like `Geocoding`, and for the same reason carries no `field`. */
24
+ | Messaging
18
25
 
19
- /** The declaration site: the component's spec name and the field carrying the
20
- `@storageRef` annotation, plus the store exactly as that field spells it.
26
+ /** The declaration site: the component's spec name, and — for a store — the field
27
+ carrying the `@storageRef` annotation plus the store exactly as that field
28
+ spells it.
21
29
 
22
- `annotation` is optional only to keep reading a manifest emitted before it
23
- existed: a plugin built earlier still parses, and a reader that cannot say
24
- what the source says omits the claim rather than inventing one. Every
25
- manifest emitted now carries it. */
30
+ `field` and `annotation` are optional. `annotation` because a manifest emitted
31
+ before it existed must still parse, and a reader that cannot say what the
32
+ source says omits the claim rather than inventing one; `field` because a
33
+ capability a slice declares has no declaring field, and naming one would be a
34
+ fiction. Every store entry emitted now carries both. */
26
35
  @schema
27
- type provenance = {component: string, field: string, annotation?: string}
36
+ type provenance = {component: string, field?: string, annotation?: string}
28
37
 
29
38
  @schema
30
39
  type entry = {
@@ -39,17 +48,18 @@ type t = {capabilities: array<entry>}
39
48
  /**
40
49
  Build the manifest from a plugin's structure.
41
50
 
42
- Keys iterate `requiredStores` itself, so a manifest's key set is byte-identical
43
- to what the deployed plugin reports at runtime — the contract the deploy-time
44
- coverage assertion checks against. A structure with no declarations yields an
45
- empty `capabilities` list, not an absent file: "declares nothing" is a
46
- statement, and the generator reading the manifests must be able to tell it
47
- apart from "was never built".
51
+ Store keys iterate `requiredStores` itself, so a manifest's key set is
52
+ byte-identical to what the deployed plugin reports at runtime — the contract the
53
+ deploy-time coverage assertion checks against. Capability entries follow, one per
54
+ distinct capability, ordered by name so the file does not churn. A structure with
55
+ no declarations yields an empty `capabilities` list, not an absent file:
56
+ "declares nothing" is a statement, and the generator reading the manifests must
57
+ be able to tell it apart from "was never built".
48
58
  */
49
59
  let fromStructure = (structure: Plugin.pluginStructure): t => {
50
60
  let declarations = structure.requiredStoreDeclarations->Option.getOr([])
51
- {
52
- capabilities: structure.requiredStores
61
+ let stores =
62
+ structure.requiredStores
53
63
  ->Option.getOr([])
54
64
  ->Array.map(key => {
55
65
  kind: ObjectStore,
@@ -59,8 +69,26 @@ let fromStructure = (structure: Plugin.pluginStructure): t => {
59
69
  ? Some({component: d.component, field: d.field, annotation: ?d.annotation})
60
70
  : None
61
71
  ),
62
- }),
63
- }
72
+ })
73
+ let needs = structure.requiredCapabilities->Option.getOr([])
74
+ let capabilityKeys =
75
+ needs->Array.map(d => d.capability)->Belt.Set.String.fromArray->Belt.Set.String.toArray
76
+ let capabilities = capabilityKeys->Array.filterMap(key =>
77
+ // An unrecognised capability is dropped rather than passed through: the
78
+ // generator downstream renders a real `Platform.capability` arm, and a name
79
+ // this build cannot map has no arm to render.
80
+ CapabilityNeed.fromString(key)->Option.map(need => {
81
+ kind: switch need {
82
+ | Geocoding => Geocoding
83
+ | Messaging => Messaging
84
+ },
85
+ key,
86
+ declaredBy: needs->Array.filterMap(d =>
87
+ d.capability == key ? Some({component: d.component}) : None
88
+ ),
89
+ })
90
+ )
91
+ {capabilities: Array.concat(stores, capabilities)}
64
92
  }
65
93
 
66
94
  /** Deterministic rendering: 2-space indent, trailing newline. Rebuilding with
@@ -3,13 +3,19 @@
3
3
  import * as Sury from "sury";
4
4
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
5
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
6
+ import * as Belt_SetString from "@rescript/runtime/lib/es6/Belt_SetString.js";
6
7
  import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
8
+ import * as CapabilityNeed$Reventless from "../semantic/CapabilityNeed.res.mjs";
7
9
 
8
- let kindSchema = Sury.literal("ObjectStore");
10
+ let kindSchema = Sury.union([
11
+ Sury.literal("ObjectStore"),
12
+ Sury.literal("Geocoding"),
13
+ Sury.literal("Messaging")
14
+ ]);
9
15
 
10
16
  let provenanceSchema = Sury.$schema(s => ({
11
17
  component: s.m(Sury.string),
12
- field: s.m(Sury.string),
18
+ field: s.m(Sury.$option(Sury.string)),
13
19
  annotation: s.m(Sury.$option(Sury.string))
14
20
  }));
15
21
 
@@ -25,20 +31,38 @@ let schema = Sury.$schema(s => ({
25
31
 
26
32
  function fromStructure(structure) {
27
33
  let declarations = Stdlib_Option.getOr(structure.requiredStoreDeclarations, []);
28
- return {
29
- capabilities: Stdlib_Option.getOr(structure.requiredStores, []).map(key => ({
30
- kind: "ObjectStore",
34
+ let stores = Stdlib_Option.getOr(structure.requiredStores, []).map(key => ({
35
+ kind: "ObjectStore",
36
+ key: key,
37
+ declaredBy: Stdlib_Array.filterMap(declarations, d => {
38
+ if (d.store === key) {
39
+ return {
40
+ component: d.component,
41
+ field: d.field,
42
+ annotation: d.annotation
43
+ };
44
+ }
45
+ })
46
+ }));
47
+ let needs = Stdlib_Option.getOr(structure.requiredCapabilities, []);
48
+ let capabilityKeys = Belt_SetString.toArray(Belt_SetString.fromArray(needs.map(d => d.capability)));
49
+ let capabilities = Stdlib_Array.filterMap(capabilityKeys, key => Stdlib_Option.map(CapabilityNeed$Reventless.fromString(key), need => {
50
+ let tmp;
51
+ tmp = need === "Geocoding" ? "Geocoding" : "Messaging";
52
+ return {
53
+ kind: tmp,
31
54
  key: key,
32
- declaredBy: Stdlib_Array.filterMap(declarations, d => {
33
- if (d.store === key) {
55
+ declaredBy: Stdlib_Array.filterMap(needs, d => {
56
+ if (d.capability === key) {
34
57
  return {
35
- component: d.component,
36
- field: d.field,
37
- annotation: d.annotation
58
+ component: d.component
38
59
  };
39
60
  }
40
61
  })
41
- }))
62
+ };
63
+ }));
64
+ return {
65
+ capabilities: stores.concat(capabilities)
42
66
  };
43
67
  }
44
68
 
@@ -62,6 +62,20 @@ module type Spec = {
62
62
  on structurally-detected inline spec modules — defaults to
63
63
  `AllowAuthenticated`. */
64
64
  let commandAuthorization: command => Authorization.permission
65
+
66
+ /** The lifecycle enum this component's commands move a row through — the
67
+ linked view's own, e.g. `type lifecycleState = Customers.accountStatus`.
68
+ Auto-injected as `unit` alongside the default below; a host that declares
69
+ `commandTransition` declares this too, and the pair is what makes every
70
+ edge name one lifecycle. */
71
+ type lifecycleState
72
+
73
+ /** The lifecycle edge each command owns, read while the plugin structure is
74
+ assembled. Auto-injected as `_ => Unrestricted` by `@@reventless.spec`,
75
+ which leaves `@transition` in charge; a host that writes the switch by
76
+ hand takes charge instead, and gets an exhaustive one over typed states.
77
+ See `Transition`. */
78
+ let commandTransition: command => Transition.t<lifecycleState>
65
79
  }
66
80
 
67
81
  /**
@@ -106,6 +106,30 @@ module type Spec = {
106
106
  in the Event Graph / Context Map.
107
107
  Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
108
108
  let externalSystem: option<string>
109
+
110
+ /**
111
+ The platform capabilities this slice's `translate` reaches for.
112
+
113
+ `[]` — the common case — means `translate` calls a service the framework does
114
+ not broker, and the deployment provisions nothing on its behalf. Naming a
115
+ capability makes the need a checked fact: it reaches `capabilities.json`, the
116
+ platform's generated capability list, and the deploy-time gate, which refuses a
117
+ plugin whose platform provisions none of it.
118
+
119
+ Declared rather than inferred because what `translate` reads off
120
+ `Capabilities.t` is only visible in its body, and provisioning infrastructure
121
+ from a guess at a function body is not a service. A trait exports the value for
122
+ its host to name, so grafting one cannot leave the need unstated.
123
+ */
124
+ let capabilityNeeds: array<CapabilityNeed.t>
125
+
126
+ /** The domain traits grafted into this component, as values the trait packages
127
+ export — `[TraitAttachments.Attachments.declaration]`. Auto-injected as `[]`
128
+ by `@@reventless.spec`, so a component that is nobody's graft says so without
129
+ a line. A graft names its trait here and the structure records it, which is
130
+ the only way a deployed plugin can answer "where did this come from". See
131
+ `Trait`. */
132
+ let traits: array<Trait.t>
109
133
  }
110
134
 
111
135
  /**