@reventlessdev/reventless-spec 3.0.0-alpha.100

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 (139) hide show
  1. package/CHANGELOG.md +931 -0
  2. package/LICENSE +202 -0
  3. package/README.md +109 -0
  4. package/package.json +49 -0
  5. package/rescript.json +32 -0
  6. package/run-generator.mjs +2 -0
  7. package/run-platform-generator.mjs +2 -0
  8. package/scripts/generate-currency.mjs +215 -0
  9. package/scripts/iso-4217-list-one.xml +1956 -0
  10. package/src/AnsiStyle.res +40 -0
  11. package/src/AnsiStyle.res.mjs +54 -0
  12. package/src/LogPrefix.res +192 -0
  13. package/src/LogPrefix.res.mjs +159 -0
  14. package/src/PackageVersion.res +67 -0
  15. package/src/PackageVersion.res.mjs +81 -0
  16. package/src/components/Aggregate.res +64 -0
  17. package/src/components/Aggregate.res.mjs +2 -0
  18. package/src/components/AutomationSlice.res +279 -0
  19. package/src/components/AutomationSlice.res.mjs +30 -0
  20. package/src/components/CapabilityManifest.res +74 -0
  21. package/src/components/CapabilityManifest.res.mjs +61 -0
  22. package/src/components/ComponentKind.res +99 -0
  23. package/src/components/ComponentKind.res.mjs +125 -0
  24. package/src/components/Counter.res +24 -0
  25. package/src/components/Counter.res.mjs +2 -0
  26. package/src/components/DcbDecode.res +118 -0
  27. package/src/components/DcbDecode.res.mjs +100 -0
  28. package/src/components/DcbScopeInference.res +244 -0
  29. package/src/components/DcbScopeInference.res.mjs +177 -0
  30. package/src/components/DcbTag.res +1335 -0
  31. package/src/components/DcbTag.res.mjs +898 -0
  32. package/src/components/DcbValidation.res +427 -0
  33. package/src/components/DcbValidation.res.mjs +423 -0
  34. package/src/components/DisplayName.res +40 -0
  35. package/src/components/DisplayName.res.mjs +26 -0
  36. package/src/components/ExtensionPoint.res +27 -0
  37. package/src/components/ExtensionPoint.res.mjs +2 -0
  38. package/src/components/InboundTranslationSlice.res +85 -0
  39. package/src/components/InboundTranslationSlice.res.mjs +2 -0
  40. package/src/components/OutboundTranslationSlice.res +153 -0
  41. package/src/components/OutboundTranslationSlice.res.mjs +2 -0
  42. package/src/components/Plugin.res +538 -0
  43. package/src/components/Plugin.res.mjs +264 -0
  44. package/src/components/PluginName.res +39 -0
  45. package/src/components/PluginName.res.mjs +45 -0
  46. package/src/components/ReadModel.res +199 -0
  47. package/src/components/ReadModel.res.mjs +18 -0
  48. package/src/components/Reference.res +55 -0
  49. package/src/components/Reference.res.mjs +50 -0
  50. package/src/components/Snapshot.res +26 -0
  51. package/src/components/Snapshot.res.mjs +2 -0
  52. package/src/components/StateAnnotations.res +97 -0
  53. package/src/components/StateAnnotations.res.mjs +15 -0
  54. package/src/components/StateChangeSlice.res +131 -0
  55. package/src/components/StateChangeSlice.res.mjs +2 -0
  56. package/src/components/StateViewSlice.res +123 -0
  57. package/src/components/StateViewSlice.res.mjs +2 -0
  58. package/src/components/Task.res +62 -0
  59. package/src/components/Task.res.mjs +2 -0
  60. package/src/generator/Codegen.res +842 -0
  61. package/src/generator/Codegen.res.mjs +565 -0
  62. package/src/generator/Config.res +106 -0
  63. package/src/generator/Config.res.mjs +69 -0
  64. package/src/generator/Discovery.res +230 -0
  65. package/src/generator/Discovery.res.mjs +198 -0
  66. package/src/generator/Generator_Node.res +14 -0
  67. package/src/generator/Generator_Node.res.mjs +18 -0
  68. package/src/generator/Pairing.res +460 -0
  69. package/src/generator/Pairing.res.mjs +415 -0
  70. package/src/generator/PlatformCodegen.res +207 -0
  71. package/src/generator/PlatformCodegen.res.mjs +154 -0
  72. package/src/generator/PlatformGenerator.res +126 -0
  73. package/src/generator/PlatformGenerator.res.mjs +114 -0
  74. package/src/generator/PlatformManifests.res +203 -0
  75. package/src/generator/PlatformManifests.res.mjs +212 -0
  76. package/src/generator/PluginGenerator.res +57 -0
  77. package/src/generator/PluginGenerator.res.mjs +73 -0
  78. package/src/semantic/Bytes.res +54 -0
  79. package/src/semantic/Bytes.res.mjs +38 -0
  80. package/src/semantic/Capabilities.res +43 -0
  81. package/src/semantic/Capabilities.res.mjs +17 -0
  82. package/src/semantic/Color.res +51 -0
  83. package/src/semantic/Color.res.mjs +29 -0
  84. package/src/semantic/Currency.res +598 -0
  85. package/src/semantic/Currency.res.mjs +743 -0
  86. package/src/semantic/DateRange.res +148 -0
  87. package/src/semantic/DateRange.res.mjs +74 -0
  88. package/src/semantic/Duration.res +53 -0
  89. package/src/semantic/Duration.res.mjs +26 -0
  90. package/src/semantic/Email.res +51 -0
  91. package/src/semantic/Email.res.mjs +31 -0
  92. package/src/semantic/GeoPoint.res +226 -0
  93. package/src/semantic/GeoPoint.res.mjs +190 -0
  94. package/src/semantic/Geocoding.res +127 -0
  95. package/src/semantic/Geocoding.res.mjs +36 -0
  96. package/src/semantic/Money.res +196 -0
  97. package/src/semantic/Money.res.mjs +138 -0
  98. package/src/semantic/Offload.res +294 -0
  99. package/src/semantic/Offload.res.mjs +191 -0
  100. package/src/semantic/Percent.res +53 -0
  101. package/src/semantic/Percent.res.mjs +33 -0
  102. package/src/semantic/Phone.res +55 -0
  103. package/src/semantic/Phone.res.mjs +29 -0
  104. package/src/semantic/Semantic.res +162 -0
  105. package/src/semantic/Semantic.res.mjs +95 -0
  106. package/src/semantic/StorageRef.res +164 -0
  107. package/src/semantic/StorageRef.res.mjs +111 -0
  108. package/src/semantic/Url.res +66 -0
  109. package/src/semantic/Url.res.mjs +48 -0
  110. package/src/types/Authorization.res +23 -0
  111. package/src/types/Authorization.res.mjs +33 -0
  112. package/src/types/Behavior.res +86 -0
  113. package/src/types/Behavior.res.mjs +2 -0
  114. package/src/types/DateTime.res +29 -0
  115. package/src/types/DateTime.res.mjs +16 -0
  116. package/src/types/EventMapping.res +100 -0
  117. package/src/types/EventMapping.res.mjs +15 -0
  118. package/src/types/Handler.res +30 -0
  119. package/src/types/Handler.res.mjs +2 -0
  120. package/src/types/Id.res +75 -0
  121. package/src/types/Id.res.mjs +37 -0
  122. package/src/types/Identity.res +46 -0
  123. package/src/types/Identity.res.mjs +51 -0
  124. package/src/types/Message.res +326 -0
  125. package/src/types/Message.res.mjs +186 -0
  126. package/src/types/Projection.res +220 -0
  127. package/src/types/Projection.res.mjs +44 -0
  128. package/src/types/QueryEngine.res +123 -0
  129. package/src/types/QueryEngine.res.mjs +12 -0
  130. package/src/types/ReadConsistency.res +38 -0
  131. package/src/types/ReadConsistency.res.mjs +29 -0
  132. package/src/types/Schedule.res +65 -0
  133. package/src/types/Schedule.res.mjs +68 -0
  134. package/src/types/SideEffect.res +45 -0
  135. package/src/types/SideEffect.res.mjs +2 -0
  136. package/src/types/StoredEvent.res +46 -0
  137. package/src/types/StoredEvent.res.mjs +32 -0
  138. package/src/types/Visibility.res +24 -0
  139. package/src/types/Visibility.res.mjs +25 -0
@@ -0,0 +1,538 @@
1
+ /** The logical name of a plugin (serializable as JSON). */
2
+ @schema
3
+ type name = string
4
+
5
+ /** The semantic version string of a plugin release. */
6
+ @schema
7
+ type version = string
8
+
9
+ /**
10
+ Classifies the business role of a plugin, carried on `pluginDefinition` so the
11
+ plugin-lifecycle read model can segregate infrastructure/commercial/marketplace
12
+ plugins from domain plugins in the admin Plugins view. Absent (on definitions
13
+ persisted before this field existed) is read as `Domain`.
14
+
15
+ Canonical here in `reventless-spec` because `pluginDefinition` (a `@schema` type
16
+ nested in the lifecycle Message union) needs the sury schema; `ReventlessCore.Plugin_BuiltHook`
17
+ re-exports this same type for its deploy-time metadata registry.
18
+ */
19
+ @schema
20
+ type pluginKind =
21
+ | Domain
22
+ | PlatformInfrastructure
23
+ | Commercial
24
+ | Marketplace
25
+
26
+ /**
27
+ Describes an extension point exported by a plugin.
28
+ Included in the plugin's `pluginDefinition` for use by the gateway / host.
29
+ */
30
+ @schema
31
+ type extensionPointDefinition = {
32
+ name: string,
33
+ commandTopic: string,
34
+ eventTopic: string,
35
+ }
36
+
37
+ /**
38
+ Describes an extension imported by a plugin (i.e. a connection to a host plugin's
39
+ extension point).
40
+ Included in the plugin's `pluginDefinition` for use by the host.
41
+ */
42
+ @schema
43
+ type extensionDefinition = {
44
+ name: string,
45
+ extensionPointName: string,
46
+ /**
47
+ DCB EventLog source names this extension consumes. Convention: each entry is
48
+ the `name` field of a peer plugin's `dcbEventLogDefinition` (i.e. `${peer}DcbEventLog`).
49
+ The admin uses these to provision cross-plugin SNS subscriptions from peer DCB
50
+ EventTopics → this plugin's EventCollector. `[]` for extensions that only consume
51
+ ExtensionPoint EventTopics.
52
+ */
53
+ dcbSources: array<string>,
54
+ }
55
+
56
+ /**
57
+ Describes a DCB EventLog exposed by a plugin.
58
+ Included in the plugin's `pluginDefinition` so the admin can provision SNS
59
+ subscriptions from this plugin's DCB EventTopic to any peer plugin whose
60
+ extension references the DCB log by name.
61
+ */
62
+ @schema
63
+ type dcbEventLogDefinition = {
64
+ /** Service name carried in event meta — convention: `${plugin.name}DcbEventLog`. */
65
+ name: string,
66
+ /** SNS topic ARN for the DCB EventLog's EventTopic. */
67
+ eventTopicArn: string,
68
+ }
69
+
70
+ /**
71
+ Protocol version declaration for a single extension point connection.
72
+
73
+ Carried in the `ConnectPlugin` handshake so the host can validate schema
74
+ compatibility before accepting the extension. Use `[]` when version
75
+ negotiation is not needed.
76
+ */
77
+ // Protocol version declaration for a single extension point connection.
78
+ // Carried in the ConnectPlugin handshake so the host can validate compatibility.
79
+ @schema
80
+ type extensionProtocol = {
81
+ extensionPointName: string,
82
+ /** SemVer of the command schema the extension was compiled against. */
83
+ commandVersion: string,
84
+ /** SemVer of the event schema the extension was compiled against. */
85
+ eventVersion: string,
86
+ }
87
+
88
+ /**
89
+ A GraphQL schema fragment contributed by a plugin.
90
+ Encoded as JSON for transport; protocol identifies the schema format (e.g. "graphql").
91
+ */
92
+ @schema
93
+ type apiSchemaFragment = {encoded: string, protocol: string}
94
+
95
+ /**
96
+ The API a plugin's GraphQL fields are stitched into: the Domain API (the default —
97
+ application plugins) or the Platform API (platform-level plugins such as an inspector,
98
+ which contribute fields alongside the admin base). Serializes as the bare string
99
+ "Domain" / "Platform". Consumed by the schema-fragment registry to maintain one
100
+ cumulative schema per API.
101
+ */
102
+ @schema
103
+ type apiTarget = Domain | Platform
104
+
105
+ // Sury's nullableAsOption creates T | undefined | null which fails jsonableValidation
106
+ // inside union variant payloads. js_nullable creates T | null (no undefined) which is
107
+ // JSON-safe and passes jsonableValidation in all contexts.
108
+ @module("sury/src/Sury.res.mjs") external _jsNullable: (S.t<'a>, unit) => S.t<option<'a>> = "js_nullable"
109
+ let apiSchemaFragmentOffloadSchema = Offload.optionSchema(~store="pluginApiFragments", apiSchemaFragmentSchema)
110
+ let dcbEventLogOptionSchema = _jsNullable(dcbEventLogDefinitionSchema, ())
111
+ // js_nullable creates T | null which passes sury's jsonableValidation inside union variant payloads.
112
+ let stringOptionSchema = _jsNullable(S.string, ())
113
+ let stringArrayOptionSchema = _jsNullable(S.array(S.string), ())
114
+ let boolOptionSchema = _jsNullable(S.bool, ())
115
+
116
+ // ── UI fragment manifest types ────────────────────────────────────────────────
117
+
118
+ @schema
119
+ type panelManifestEntry = {
120
+ fragmentId: string,
121
+ title: string,
122
+ description: string,
123
+ positions: array<string>,
124
+ requiredAccess: @s.matches(stringOptionSchema) option<string>,
125
+ }
126
+
127
+ @schema
128
+ type menuEntry = {
129
+ label: string,
130
+ icon: @s.matches(stringOptionSchema) option<string>,
131
+ group: @s.matches(stringOptionSchema) option<string>,
132
+ sortOrder: int,
133
+ }
134
+
135
+ @schema
136
+ type pageManifestEntry = {
137
+ fragmentId: string,
138
+ title: string,
139
+ menuEntry: menuEntry,
140
+ requiredAccess: @s.matches(stringOptionSchema) option<string>,
141
+ }
142
+
143
+ @schema
144
+ type uiFragmentManifest = {
145
+ remoteEntryUrl: string,
146
+ panels: array<panelManifestEntry>,
147
+ pages: array<pageManifestEntry>,
148
+ }
149
+
150
+ let uiFragmentManifestOptionSchema = _jsNullable(uiFragmentManifestSchema, ())
151
+
152
+ // ── Plugin structure types (component metadata for Auto UI and event graph) ──
153
+
154
+ @schema
155
+ type commandLevel = Collection | Instance
156
+
157
+ @schema
158
+ type fieldReference = {
159
+ fieldName: string,
160
+ entity: string,
161
+ plugin: @s.matches(stringOptionSchema) option<string>,
162
+ }
163
+
164
+ @schema
165
+ type commandDef = {
166
+ name: string,
167
+ schema: string,
168
+ level: commandLevel,
169
+ aggregateIdField: @s.matches(stringOptionSchema) option<string>,
170
+ mutationField: string,
171
+ references: array<fieldReference>,
172
+ /**
173
+ Status values under which this command is meaningful. `None` means the command
174
+ is always available (back-compat default). `Some([…])` lets AutoUI hide the
175
+ command on rows whose status field is not in the set — see `queryableDef.statusField`
176
+ for how the row's status is located. `Some([])` is the defensive "never show" form.
177
+ */
178
+ allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
179
+ /**
180
+ The single status value this command's handler writes — the command's *to*
181
+ state, sibling of `allowedStates`' *from* set. Source: the
182
+ `@targetState("Shipped")` command-variant annotation. `None` (absent
183
+ annotation) is the back-compat default: AutoUI's board resolver then falls
184
+ back to its name-stem heuristic. `Some("Shipped")` lets the resolver move a
185
+ row by a declared transition instead of a guess. js_nullable for JSON safety,
186
+ same as `allowedStates`.
187
+ */
188
+ targetState: @s.matches(stringOptionSchema) option<string>,
189
+ /**
190
+ Whether this command variant is exposed in the generated API (a non-`@noApi`
191
+ variant of a non-`@noApi` command). Dev tooling badges API-exposed commands in
192
+ the event graph. js_nullable (T | null) so it stays JSON-safe inside the
193
+ persisted/lifecycle payloads; absent on defs written before this field existed
194
+ (read as None) — those stores must be reset. See [[sury-optional-field-absent-vs-null]].
195
+ */
196
+ apiExposed: @s.matches(boolOptionSchema) option<bool>,
197
+ }
198
+
199
+ @schema
200
+ type queryableDef = {
201
+ name: string,
202
+ queryField: string,
203
+ schema: string,
204
+ consumedEventTypes: array<string>,
205
+ linkedWriteSide: array<string>,
206
+ /**
207
+ The field on this entity that carries the human-readable label.
208
+ When the entity's state schema declares one or more `@displayName` annotations,
209
+ this resolves to `"displayName"` (the projected column). Otherwise it falls back
210
+ to the first non-`id` string property, or `"id"` as a last resort.
211
+ */
212
+ labelField: string,
213
+ /**
214
+ Fields appropriate for label-oriented text search.
215
+ Mirrors `labelField` when the entity uses the fallback or single-field label.
216
+ For composite `@displayName` annotations, lists the *raw* underlying source
217
+ fields (so clients with substring indexes can target them directly).
218
+ */
219
+ searchableFields: array<string>,
220
+ /**
221
+ Which rung of the `labelField` ladder produced it, so a consumer with a name
222
+ rule of its own can tell a declaration from a guess before ranking the two:
223
+
224
+ - `"annotation"` — a `@displayName` spec. The author said which field names the
225
+ record; nothing a client infers locally outranks it.
226
+ - `"convention"` — a field literally named `name`/`title`/`label`/`displayName`.
227
+ A guess, and the one guess a client can independently arrive at.
228
+ - `"position"` — the first candidate in declaration order. A guess, and a fact
229
+ only this side knows; a client's own conventional-name rule is the better
230
+ answer where the two differ.
231
+ - `"fallback"` — no candidate at all, so `labelField` is `"id"`. The state
232
+ saying it has no human-readable field.
233
+
234
+ `None` means not stated — defs persisted before this field existed, and
235
+ hand-rolled defs that decline to say. Distinct from `Some("fallback")`, which
236
+ is this side stating that it looked. js_nullable for the same JSON-safety
237
+ reason as `statusField`.
238
+ */
239
+ labelFieldSource: @s.matches(stringOptionSchema) option<string>,
240
+ /**
241
+ Name of the state field whose value identifies the row's lifecycle status, used
242
+ by AutoUI together with `commandDef.allowedStates` to filter the per-row command
243
+ menu. Resolution order (codegen): (1) field annotated `@status`; (2) a field
244
+ literally named `"status"`; (3) `None`. Spec authors that hand-roll a
245
+ `queryableDef` set this explicitly.
246
+ */
247
+ statusField: @s.matches(stringOptionSchema) option<string>,
248
+ /**
249
+ Component visibility hint (`@@reventless.visibility`). `Some("Internal")` marks a
250
+ ReadModel / StateViewSlice that the deployed AutoUI hides from its menu, drill-down
251
+ pages, web event graph and cross-plugin edges. `None` (absent) means Public. Internal
252
+ components are still CARRIED in `pluginStructure` (tagged here) so developer tools — the
253
+ `reventless-gwt` / VSCode domain graph and dead-code analysis — can see them, per
254
+ Visibility.res. Optional for back-compat: definitions persisted before this field
255
+ existed decode as `None` (Public).
256
+ */
257
+ visibility: @s.matches(stringOptionSchema) option<string>,
258
+ /**
259
+ Intra-plugin grouping band (the "chapter") this component belongs to, captured at
260
+ build time from its source folder by the plugin generator: the first path segment
261
+ under the plugin's `src/` that is not a recognised kind-folder
262
+ (`src/<Chapter>/…/<Component>.res` → `Some("<Chapter>")`; a component directly under a
263
+ kind-folder → `None`). Lets a consumer that renders the event graph from the
264
+ *deployed* plugin structure group components into chapter sub-containers identically
265
+ to the authoring tooling, with no workspace/disk access — the renderer already
266
+ supports the bands (`DomainGraphD2 ~chapters`); only this datum was missing on the
267
+ deployed side. `None` (absent) renders flat. js_nullable (T | null) keeps it JSON-safe
268
+ inside the lifecycle Message union; always written (None → null), so defs persisted
269
+ before this field existed must be reset/re-emitted. See [[deployed-chapter-grouping]].
270
+ */
271
+ chapter: @s.matches(stringOptionSchema) option<string>,
272
+ }
273
+
274
+ /**
275
+ One emitted event of a write side, with its field schema. Mirrors `commandDef`
276
+ but for the past-tense facts a write side produces: `name` is the event variant
277
+ name (e.g. `OrderPlaced`), `schema` is the JSON Schema of that variant's payload
278
+ (same serialization as `commandDef.schema`: `SuryToJsonSchema.deriveObjectSchema`,
279
+ so field-level `x-reventless-*` extensions are carried and the variant's `TAG`
280
+ discriminator is not — the constructor name is already `name`), `references` its
281
+ cross-entity field links.
282
+ Carried so developer tools (the `reventless-dev` / VSCode domain graph) can show
283
+ event field rows — AutoUI ignores it. */
284
+ @schema
285
+ type eventDef = {
286
+ name: string,
287
+ schema: string,
288
+ references: array<fieldReference>,
289
+ }
290
+
291
+ @schema
292
+ type writableDef = {
293
+ name: string,
294
+ commands: array<commandDef>,
295
+ producedEventTypes: array<string>,
296
+ consumedEventTypes: array<string>,
297
+ linkedViews: array<string>,
298
+ consistencyRead: @s.matches(stringOptionSchema) option<string>,
299
+ /** Emitted-event field schemas (Phase 6.3). Required like the other write-side
300
+ arrays; `[]` when there are none. The structure is re-derived on every build/
301
+ deploy, so no persisted-data back-compat shim is needed. */
302
+ events: array<eventDef>,
303
+ /** Chapter grouping band — see `queryableDef.chapter`. */
304
+ chapter: @s.matches(stringOptionSchema) option<string>,
305
+ }
306
+
307
+ @schema
308
+ type automationSliceDef = {
309
+ name: string,
310
+ consumedEventTypes: array<string>,
311
+ producedCommandTypes: array<string>,
312
+ targetName: string,
313
+ /** Chapter grouping band — see `queryableDef.chapter`. */
314
+ chapter: @s.matches(stringOptionSchema) option<string>,
315
+ }
316
+
317
+ @schema
318
+ type outboundTranslationSliceDef = {
319
+ name: string,
320
+ consumedEventTypes: array<string>,
321
+ inboundCommandTypes: array<string>,
322
+ targetName: @s.matches(stringOptionSchema) option<string>,
323
+ // Foreign system this slice publishes to — drives the external box (Event Graph).
324
+ externalSystem: @s.matches(stringOptionSchema) option<string>,
325
+ /** Chapter grouping band — see `queryableDef.chapter`. */
326
+ chapter: @s.matches(stringOptionSchema) option<string>,
327
+ }
328
+
329
+ @schema
330
+ type inboundTranslationSliceDef = {
331
+ name: string,
332
+ commandTypes: array<string>,
333
+ targetName: string,
334
+ // Foreign system this slice receives from — drives the external box (Event Graph).
335
+ externalSystem: @s.matches(stringOptionSchema) option<string>,
336
+ /** Chapter grouping band — see `queryableDef.chapter`. */
337
+ chapter: @s.matches(stringOptionSchema) option<string>,
338
+ }
339
+
340
+ @schema
341
+ type extensionDef = {
342
+ name: string,
343
+ delegateNames: array<string>,
344
+ eventTypes: array<string>,
345
+ commandTypes: array<string>,
346
+ }
347
+
348
+ /**
349
+ Describes an extension point owned by a plugin, from the *producer* side.
350
+
351
+ `sourceEventTypes` are the owner-plugin internal events (the `Delegate`'s events)
352
+ that feed this extension point's published protocol — plugin-qualified to match
353
+ `writableDef.producedEventTypes`, so the event graph can link a producing
354
+ write-side to the extension point it ultimately feeds. `delegateNames` are the
355
+ connected targets (one per `ExtensionPointMapping`).
356
+
357
+ `commandTypes` are the EP's *inbound* command protocol (the variants of its
358
+ `command` type). It is empty (None, read as []) when the EP declares
359
+ `command = unit` — a notification-only, events-out boundary that accepts nothing
360
+ inward. The event graph uses this to decide whether the EP routes any command (an
361
+ empty list means no `routesTo` edge: there is nothing for the EP to route).
362
+
363
+ Optional (None for a `command = unit` EP). Uses the `js_nullable` pattern (T | null).
364
+ A sury field cannot be BOTH absent-tolerant on decode AND JSON-encodable (proven:
365
+ S.option = `T|undefined`, nullableAsOption = `T|undefined|null` both decode an absent
366
+ key but fail jsonableValidation; js_nullable = `T|null` is the only JSON-safe form but
367
+ rejects an absent key). This def is nested in the JSON-encoded lifecycle Message union
368
+ (Connect/Heartbeat), so jsonability wins → js_nullable. It always writes the field
369
+ (None → null), so it is present-required on decode; a plugin definition persisted
370
+ before this field existed must be reset/re-emitted. Read with `->Option.getOr([])`.
371
+ */
372
+ @schema
373
+ type extensionPointDef = {
374
+ name: string,
375
+ delegateNames: array<string>,
376
+ sourceEventTypes: array<string>,
377
+ commandTypes: @s.matches(stringArrayOptionSchema) option<array<string>>,
378
+ }
379
+
380
+ // js_nullable creates `array | null` (not `| undefined`), which passes sury's
381
+ // jsonableValidation inside the pluginStructure union variant payload.
382
+ let extensionPointDefArrayOptionSchema = _jsNullable(S.array(extensionPointDefSchema), ())
383
+
384
+ /**
385
+ One field's store requirement, with its provenance.
386
+
387
+ `store` is the same qualified `{plugin}.{store}` string `requiredStores`
388
+ carries; `component` and `field` name the declaration site. The site matters
389
+ because a requirement only ever changes by editing a field — when a rename
390
+ removes a store from the manifest, the diff has to say which field caused it.
391
+
392
+ `annotation` is the store exactly as the field spells it — bare for a store
393
+ the declaring plugin owns, qualified for a foreign one. It is recorded rather
394
+ than reconstructed: only here is the owning plugin unambiguous, so anything
395
+ downstream would have to infer it by comparing a registered plugin name with
396
+ whatever name a deploy manifest happened to use, and those were never required
397
+ to match.
398
+
399
+ Optional for the same reason `CapabilityManifest.provenance` and
400
+ `PlatformCodegen.provenance` — the two places this value travels onward to —
401
+ already declare it optional: an event stored before the field existed cannot
402
+ say what the source said, and a reader that cannot say omits the claim rather
403
+ than inventing one. Every definition emitted now carries it.
404
+
405
+ That is not a stylistic preference. It was first added here as a required
406
+ `string` while events written without it were already stored, and since the
407
+ lifecycle aggregate replays its own log before every decision, those events
408
+ stopped decoding and the plugin's registration froze for two days. `None` is
409
+ also the honest value: `""` would assert the author wrote an empty annotation.
410
+ See the schema-evolution note on `pluginStructure` below.
411
+ */
412
+ @schema
413
+ type requiredStoreDeclaration = {
414
+ store: string,
415
+ component: string,
416
+ field: string,
417
+ annotation: @s.matches(stringOptionSchema) option<string>,
418
+ }
419
+
420
+ let requiredStoreDeclarationArrayOptionSchema = _jsNullable(
421
+ S.array(requiredStoreDeclarationSchema),
422
+ (),
423
+ )
424
+
425
+ /**
426
+ Adding a field here? It has to be a shape a stale event can be healed into.
427
+
428
+ Everything reachable from `pluginDefinition` is persisted in the Plugin lifecycle
429
+ aggregate's event log, and that aggregate replays its own log before every
430
+ decision. Events already written do not have your new field, so if decoding one
431
+ of them throws, the aggregate cannot process ANY command for that plugin — it
432
+ stops answering the deploy handshake and its registration silently freezes at
433
+ whatever version connected last.
434
+
435
+ `Message.parseJsonTolerant` heals a stale event on read, but only for shapes it
436
+ can supply a value for: a `T | null` union (→ `None`), an array (→ `[]`), a
437
+ mandatory enum (→ first variant), a nested object (→ recursively filled), and a
438
+ scalar (→ `""` / `0` / `false`, logged as a warning because it is a fabricated
439
+ value, not a derived one).
440
+
441
+ So: **prefer `js_nullable` for anything genuinely optional**, and expect a scalar
442
+ addition to show up as a warning in the logs of every deployment that still holds
443
+ older events. A field that can be absent should say so in its type rather than
444
+ lean on the healer.
445
+
446
+ The regression suite for this is `PluginLifecycleCorpusTest` in reventless-core,
447
+ which decodes frozen payloads captured off a deployed log. If it goes red naming
448
+ your field, re-shape the field — do not re-cut the fixtures. Background:
449
+ `docs/analysis/plugin-definition-schema-evolution-wedge.md`.
450
+ */
451
+ @schema
452
+ type pluginStructure = {
453
+ readModels: array<queryableDef>,
454
+ stateViewSlices: array<queryableDef>,
455
+ stateChangeSlices: array<writableDef>,
456
+ aggregates: array<writableDef>,
457
+ automationSlices: array<automationSliceDef>,
458
+ outboundTranslationSlices: array<outboundTranslationSliceDef>,
459
+ inboundTranslationSlices: array<inboundTranslationSliceDef>,
460
+ extensions: array<extensionDef>,
461
+ // Extension points owned by this plugin (producer side). Optional so plugin
462
+ // definitions persisted before this field existed still decode (absent → None,
463
+ // read as []). js_nullable keeps it JSON-safe inside union variant payloads.
464
+ extensionPoints: @s.matches(extensionPointDefArrayOptionSchema)
465
+ option<array<extensionPointDef>>,
466
+ /**
467
+ The object stores this plugin's fields declare they need, deduplicated and
468
+ fully qualified as `{plugin}.{store}`.
469
+
470
+ A field typed as a storage ref states a *requirement*: the deployment needs
471
+ that store to exist. Collecting the requirement here is what lets it be read
472
+ without re-walking every component's schema — the same reason
473
+ `producedEventTypes` is carried rather than recomputed.
474
+
475
+ Qualified even for the common same-plugin case, so one entry has one shape
476
+ and the string is directly the store's identity. Optional and js_nullable for
477
+ the same reason as `extensionPoints`: definitions persisted before this field
478
+ existed still decode (absent → None, read as []).
479
+ */
480
+ requiredStores: @s.matches(stringArrayOptionSchema) option<array<string>>,
481
+ /**
482
+ Provenance for `requiredStores`: one entry per declaring `(component, field)`
483
+ site, with `store` matching the qualified key above. `requiredStores` is
484
+ derived from this list, so the two cannot disagree. Optional and js_nullable
485
+ for the same reason as `extensionPoints`.
486
+ */
487
+ requiredStoreDeclarations: @s.matches(requiredStoreDeclarationArrayOptionSchema)
488
+ option<array<requiredStoreDeclaration>>,
489
+ }
490
+
491
+ let pluginStructureOffloadSchema = Offload.optionSchema(~store="pluginStructures", pluginStructureSchema)
492
+
493
+ /**
494
+ The self-description of a deployed plugin, persisted in the plugin's event store.
495
+
496
+ Used by the gateway to discover extension points, extensions, and protocol versions.
497
+ The `eventCollector` field is mutable so it can be set after the heartbeat lambda
498
+ registers its own ARN.
499
+ */
500
+ @schema
501
+ type pluginDefinition = {
502
+ id: string,
503
+ name: name,
504
+ version: version,
505
+ extensionPoints: array<extensionPointDefinition>,
506
+ extensions: array<extensionDefinition>,
507
+ mutable eventCollector: string,
508
+ // Protocol version declarations for each extension point this plugin connects to.
509
+ // Use [] when the plugin does not need version negotiation.
510
+ extensionProtocols: array<extensionProtocol>,
511
+ // GraphQL schema fragment contributed by this plugin (optional, set at build time).
512
+ // Offloadable: a large SDL fragment is content-addressed to the pluginApiFragments
513
+ // store by the client and carried by reference; a small one stays Inline. optionSchema
514
+ // wraps the untagged codec in js_nullable (T | null, not T | undefined | null) so it
515
+ // passes jsonableValidation inside the lifecycle Message union, and marks the store.
516
+ apiSchemaFragment: @s.matches(apiSchemaFragmentOffloadSchema) option<Offload.payload<apiSchemaFragment>>,
517
+ // API target for schema routing in split-API mode.
518
+ // None/"Domain" → fragment goes to the DomainApi (default).
519
+ // Some("Platform") → fragment goes to the PlatformApi; excluded from DomainApi runtime schema.
520
+ // Uses @s.matches(stringOptionSchema) — js_nullable creates string | null (not string | undefined),
521
+ // which passes sury's jsonableValidation inside union variant payloads.
522
+ apiTarget: @s.matches(stringOptionSchema) option<string>,
523
+ // Component graph metadata — populated by makePluginDefinition; absent for older protocol versions.
524
+ // Offloadable: the large structure is content-addressed to the pluginStructures store by
525
+ // the client and carried by reference; a small one stays Inline (see apiSchemaFragment).
526
+ structure: @s.matches(pluginStructureOffloadSchema) option<Offload.payload<pluginStructure>>,
527
+ // DCB EventLog definition for plugins that bundle a DcbEventLog component.
528
+ // Carries the EventTopic ARN so the admin can provision cross-plugin SNS
529
+ // subscriptions from this plugin's DCB topic → peer EventCollectors.
530
+ // None for plugins without a DCB EventLog.
531
+ dcbEventLog: @s.matches(dcbEventLogOptionSchema) option<dcbEventLogDefinition>,
532
+ // Business role of this plugin. Mandatory: `Domain` is the default kind, resolved once
533
+ // at the deploy-metadata → definition boundary (Plugin_Builder). PlatformInfrastructure
534
+ // plugins are segregated out of the admin Plugins list. Payload-less variant → serialises
535
+ // as a bare JSON string, so it is JSON-safe inside the lifecycle Message union without js_nullable.
536
+ kind: pluginKind,
537
+ }
538
+