@reventlessdev/reventless-spec 3.0.0-alpha.128 → 3.0.0-alpha.130

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.
@@ -73,13 +73,13 @@ type apiSchemaFragment = {encoded: string, protocol: string}
73
73
  @schema
74
74
  type apiTarget = Domain | Platform
75
75
 
76
- // js_nullable (T | null) is the only optional that passes jsonableValidation inside
77
- // union variant payloads; nullableAsOption adds `undefined` and fails it.
76
+ // Every optional below carries sury's default `option` encoding: the key is omitted,
77
+ // never written as `null`. That is the shape the rest of the repo already uses
78
+ // (`Message.meta`, `storedEvent.tags`), so there is one optional form on the wire and
79
+ // nothing to annotate. The `T | null` these fields used to carry worked around a sury
80
+ // bug — undefined failing jsonableValidation inside a union variant payload — fixed in
81
+ // 11.0.0-alpha.11. `Offload` below is not an optional wrapper but an either-or codec.
78
82
  let apiSchemaFragmentOffloadSchema = Offload.optionSchema(~store="pluginApiFragments", apiSchemaFragmentSchema)
79
- let dcbEventLogOptionSchema = dcbEventLogDefinitionSchema->S.nullAsOption
80
- let stringOptionSchema = S.string->S.nullAsOption
81
- let stringArrayOptionSchema = S.array(S.string)->S.nullAsOption
82
- let boolOptionSchema = S.bool->S.nullAsOption
83
83
 
84
84
  // ── UI fragment manifest types ────────────────────────────────────────────────
85
85
 
@@ -89,14 +89,14 @@ type panelManifestEntry = {
89
89
  title: string,
90
90
  description: string,
91
91
  positions: array<string>,
92
- requiredAccess: @s.matches(stringOptionSchema) option<string>,
92
+ requiredAccess: option<string>,
93
93
  }
94
94
 
95
95
  @schema
96
96
  type menuEntry = {
97
97
  label: string,
98
- icon: @s.matches(stringOptionSchema) option<string>,
99
- group: @s.matches(stringOptionSchema) option<string>,
98
+ icon: option<string>,
99
+ group: option<string>,
100
100
  sortOrder: int,
101
101
  }
102
102
 
@@ -105,7 +105,7 @@ type pageManifestEntry = {
105
105
  fragmentId: string,
106
106
  title: string,
107
107
  menuEntry: menuEntry,
108
- requiredAccess: @s.matches(stringOptionSchema) option<string>,
108
+ requiredAccess: option<string>,
109
109
  }
110
110
 
111
111
  @schema
@@ -115,18 +115,44 @@ type uiFragmentManifest = {
115
115
  pages: array<pageManifestEntry>,
116
116
  }
117
117
 
118
- let uiFragmentManifestOptionSchema = uiFragmentManifestSchema->S.nullAsOption
118
+ let uiFragmentManifestOptionSchema = S.option(uiFragmentManifestSchema)
119
119
 
120
120
  // ── Plugin structure types (component metadata for Auto UI and event graph) ──
121
121
 
122
122
  @schema
123
123
  type commandLevel = Collection | Instance
124
124
 
125
+ /** One command's lifecycle edge as the component's own scenarios describe it:
126
+ where a scenario shows the command taking effect, where those scenarios land,
127
+ and whether it brings a row into existence or acts on one.
128
+
129
+ Harvested from the GWT corpus by `check:lifecycle` and committed beside the
130
+ plugin as `src/LifecycleModel.res`, so structure assembly reads a value rather
131
+ than the test tree — which is not published with a plugin package.
132
+
133
+ No `@schema`: nothing serialises this. It is read once, while the structure is
134
+ assembled, and what leaves is the `commandDef` fields it resolved. */
135
+ type derivedEdge = {
136
+ /** The writable's `Spec.name`, which is what the structure knows it by. */
137
+ component: string,
138
+ command: string,
139
+ /** Absent where the corpus could not label a history — the honest answer when
140
+ the linked views declare no lifecycle field, and the reason the name-stem
141
+ guess is still there to fall back to. */
142
+ level?: commandLevel,
143
+ /** States a scenario shows the command taking effect from. Empty means the
144
+ corpus said nothing, NOT that the command belongs nowhere. */
145
+ allowedStates: array<string>,
146
+ /** States those scenarios land in. More than one is an edge a single
147
+ `targetState` cannot carry, so it is left to the declaration. */
148
+ targets: array<string>,
149
+ }
150
+
125
151
  @schema
126
152
  type fieldReference = {
127
153
  fieldName: string,
128
154
  entity: string,
129
- plugin: @s.matches(stringOptionSchema) option<string>,
155
+ plugin: option<string>,
130
156
  }
131
157
 
132
158
  @schema
@@ -134,23 +160,40 @@ type commandDef = {
134
160
  name: string,
135
161
  schema: string,
136
162
  level: commandLevel,
137
- aggregateIdField: @s.matches(stringOptionSchema) option<string>,
163
+ aggregateIdField: option<string>,
138
164
  mutationField: string,
139
165
  references: array<fieldReference>,
140
- /** The declared *from* set — lifecycle states this command is meaningful in.
141
- `None` means always available; `Some([])` means never show. */
142
- allowedStates: @s.matches(stringArrayOptionSchema) option<array<string>>,
143
- /** The declared *to* state this command's handler writes. `None` with a
144
- from-set present means the command does not move the row. */
145
- targetState: @s.matches(stringOptionSchema) option<string>,
166
+ /** The *from* set — lifecycle states this command is meaningful in. `None`
167
+ means always available; `Some([])` means never show. */
168
+ allowedStates: option<array<string>>,
169
+ /** The *to* state this command's handler writes. `None` with a from-set present
170
+ means the command does not move the row. */
171
+ targetState: option<string>,
172
+ /** Where the from-set came from, ranking an inherited edge against an authored
173
+ one the way `queryableDef.labelFieldSource` does for its field:
174
+
175
+ - `"derived"` — the component's own scenarios.
176
+ - `"declared"` — its `commandTransition` switch.
177
+ - `"unrestricted"` — the switch declares the command legal in *every*
178
+ state, so there is no from-set to publish and `allowedStates` is absent.
179
+
180
+ Absent means the command names no states to come from and nothing said that
181
+ was deliberate — a spec that wrote no switch, or one whose command creates
182
+ the row. So a consumer drawing a state machine can tell an edge left
183
+ unconstrained on purpose from one that simply has no from-set.
184
+
185
+ It speaks for the from-set only. The two halves of an edge are reported
186
+ separately because they fail separately: a corpus routinely shows a command
187
+ taking effect without ever showing where it lands. */
188
+ allowedStatesSource?: string,
146
189
  /** Whether the variant is exposed in the generated API (non-`@noApi`). */
147
- apiExposed: @s.matches(boolOptionSchema) option<bool>,
190
+ apiExposed: option<bool>,
148
191
  /** Access keys — any one of them — a caller needs to be *offered* this command.
149
192
  A hint derived from the server's rule, never the refusal itself. */
150
- requiredAccess: @s.matches(stringArrayOptionSchema) option<array<string>>,
193
+ requiredAccess: option<array<string>>,
151
194
  /** The `@owner` command field the server stamps with the caller's identity; a
152
195
  client omits it from a form, since whatever it collects is discarded. */
153
- ownerField: @s.matches(stringOptionSchema) option<string>,
196
+ ownerField: option<string>,
154
197
  }
155
198
 
156
199
  @schema
@@ -168,41 +211,41 @@ type queryableDef = {
168
211
  searchableFields: array<string>,
169
212
  /** Which rung produced `labelField`, so a consumer can rank it against its own
170
213
  rule: `"annotation"` | `"convention"` | `"position"` | `"fallback"`. */
171
- labelFieldSource: @s.matches(stringOptionSchema) option<string>,
214
+ labelFieldSource: option<string>,
172
215
  /** The state field holding the row's lifecycle, paired with
173
216
  `commandDef.allowedStates`. From `@lifecycle`, else an enum named `lifecycle`. */
174
- lifecycleField: @s.matches(stringOptionSchema) option<string>,
217
+ lifecycleField: option<string>,
175
218
  /** The `@owner` state field. Reads of this view are narrowed server-side to a
176
219
  non-elevated caller's own rows. */
177
- ownerField: @s.matches(stringOptionSchema) option<string>,
220
+ ownerField: option<string>,
178
221
  /** The `@retired` state field withdrawing a row from ordinary reads. From the
179
222
  annotation only — no fallback by name, since guessing hides data. */
180
- retiredField: @s.matches(stringOptionSchema) option<string>,
223
+ retiredField: option<string>,
181
224
  /** The states a row is retired *in* (state form of `@retired`); `None` is the
182
225
  boolean form, where the excluded value is always `true`. */
183
- retiredValues: @s.matches(stringArrayOptionSchema) option<array<string>>,
226
+ retiredValues: option<array<string>>,
184
227
  /** Whether the view publishes the by-ids reference door that names a retired row
185
228
  to any caller holding a pointer (`@namedWhenRetired`). Never true without
186
229
  `retiredField`. */
187
- namedWhenRetired: @s.matches(boolOptionSchema) option<bool>,
230
+ namedWhenRetired: option<bool>,
188
231
  /** `@@reventless.visibility`. `Some("Internal")` hides the component from AutoUI;
189
232
  it is still carried here for developer tooling. `None` means Public. */
190
- visibility: @s.matches(stringOptionSchema) option<string>,
233
+ visibility: option<string>,
191
234
  /** Intra-plugin grouping band, the first non-kind path segment under `src/`.
192
235
  `None` renders flat. */
193
- chapter: @s.matches(stringOptionSchema) option<string>,
236
+ chapter: option<string>,
194
237
  /** The singular counterpart of `queryField` (`Plugin_Order`), also the prefix of
195
238
  the generated input types. Not derivable without `Api_Naming.singularize`. */
196
- singleQueryField: @s.matches(stringOptionSchema) option<string>,
239
+ singleQueryField: option<string>,
197
240
  /** The state field identifying a row, as opposed to a reference to another entity.
198
241
  `None` means unresolved — no key-derived filter or sort until `@id` is declared. */
199
- idField: @s.matches(stringOptionSchema) option<string>,
242
+ idField: option<string>,
200
243
  /** Which rung produced `idField`, as `labelFieldSource` does: `"annotation"` |
201
244
  `"convention"` | `"sole"`. */
202
- idFieldSource: @s.matches(stringOptionSchema) option<string>,
245
+ idFieldSource: option<string>,
203
246
  /** Access keys — any one of them — a caller needs to be *offered* this view. A
204
247
  denied read comes back empty rather than erroring, hence the hint. */
205
- requiredAccess: @s.matches(stringArrayOptionSchema) option<array<string>>,
248
+ requiredAccess: option<array<string>>,
206
249
  }
207
250
 
208
251
  /** One emitted event of a write side. `name` is the variant name, `schema` its
@@ -230,7 +273,7 @@ type writableDef = {
230
273
  producedEventTypes: array<string>,
231
274
  consumedEventTypes: array<string>,
232
275
  linkedViews: array<string>,
233
- consistencyRead: @s.matches(stringOptionSchema) option<string>,
276
+ consistencyRead: option<string>,
234
277
  /** Emitted-event field schemas; `[]` when there are none. */
235
278
  events: array<eventDef>,
236
279
  /** Declared-error field schemas. Required, but a persisted structure predating a
@@ -238,7 +281,7 @@ type writableDef = {
238
281
  `[]` on read, and a new one must be added there too. */
239
282
  errors: array<errorDef>,
240
283
  /** Chapter grouping band — see `queryableDef.chapter`. */
241
- chapter: @s.matches(stringOptionSchema) option<string>,
284
+ chapter: option<string>,
242
285
  }
243
286
 
244
287
  @schema
@@ -248,7 +291,7 @@ type automationSliceDef = {
248
291
  producedCommandTypes: array<string>,
249
292
  targetName: string,
250
293
  /** Chapter grouping band — see `queryableDef.chapter`. */
251
- chapter: @s.matches(stringOptionSchema) option<string>,
294
+ chapter: option<string>,
252
295
  }
253
296
 
254
297
  @schema
@@ -256,11 +299,11 @@ type outboundTranslationSliceDef = {
256
299
  name: string,
257
300
  consumedEventTypes: array<string>,
258
301
  inboundCommandTypes: array<string>,
259
- targetName: @s.matches(stringOptionSchema) option<string>,
302
+ targetName: option<string>,
260
303
  // Foreign system this slice publishes to — drives the external box (Event Graph).
261
- externalSystem: @s.matches(stringOptionSchema) option<string>,
304
+ externalSystem: option<string>,
262
305
  /** Chapter grouping band — see `queryableDef.chapter`. */
263
- chapter: @s.matches(stringOptionSchema) option<string>,
306
+ chapter: option<string>,
264
307
  /** The topics this slice subscribes to, as `Spec.sourceNames` declares them —
265
308
  an Aggregate's `Spec.name` or a DCB source name. `Some([])` is the declared
266
309
  default and means this plugin's own DCB log; `None` is an older structure
@@ -268,7 +311,7 @@ type outboundTranslationSliceDef = {
268
311
  which names event types and not where they came from — two topics carrying
269
312
  an event of the same name are indistinguishable there. Optional so an older
270
313
  reader ignores it. */
271
- consumedSources: @s.matches(stringArrayOptionSchema) option<array<string>>,
314
+ consumedSources: option<array<string>>,
272
315
  }
273
316
 
274
317
  @schema
@@ -277,9 +320,9 @@ type inboundTranslationSliceDef = {
277
320
  commandTypes: array<string>,
278
321
  targetName: string,
279
322
  // Foreign system this slice receives from — drives the external box (Event Graph).
280
- externalSystem: @s.matches(stringOptionSchema) option<string>,
323
+ externalSystem: option<string>,
281
324
  /** Chapter grouping band — see `queryableDef.chapter`. */
282
- chapter: @s.matches(stringOptionSchema) option<string>,
325
+ chapter: option<string>,
283
326
  }
284
327
 
285
328
  /** A published event of an extension point and the internal events producing it.
@@ -291,8 +334,6 @@ type publishedEventDef = {
291
334
  fromEventTypes: array<string>,
292
335
  }
293
336
 
294
- let publishedEventDefArrayOptionSchema = S.array(publishedEventDefSchema)->S.nullAsOption
295
-
296
337
  /** The command direction's producer half: a command an extension point takes and
297
338
  the delegate commands it routes to. `name` is EP-qualified, `toCommandTypes`
298
339
  plugin-qualified. */
@@ -302,8 +343,6 @@ type acceptedCommandDef = {
302
343
  toCommandTypes: array<string>,
303
344
  }
304
345
 
305
- let acceptedCommandDefArrayOptionSchema = S.array(acceptedCommandDefSchema)->S.nullAsOption
306
-
307
346
  /** The subscriber's half: a published event and the commands it routes to. A
308
347
  delegate command is plugin-qualified, one sent back to the EP is EP-qualified. */
309
348
  @schema
@@ -312,8 +351,6 @@ type handledEventDef = {
312
351
  toCommandTypes: array<string>,
313
352
  }
314
353
 
315
- let handledEventDefArrayOptionSchema = S.array(handledEventDefSchema)->S.nullAsOption
316
-
317
354
  /** The command direction's subscriber half: a command sent back to the port and
318
355
  the internal events producing it. `name` is EP-qualified, `fromEventTypes`
319
356
  plugin-qualified. */
@@ -323,21 +360,19 @@ type issuedCommandDef = {
323
360
  fromEventTypes: array<string>,
324
361
  }
325
362
 
326
- let issuedCommandDefArrayOptionSchema = S.array(issuedCommandDefSchema)->S.nullAsOption
327
-
328
363
  @schema
329
364
  type extensionDef = {
330
365
  name: string,
331
366
  delegateNames: array<string>,
332
367
  eventTypes: array<string>,
333
368
  commandTypes: array<string>,
334
- /** Which published event routes to which commands. js_nullable like
369
+ /** Which published event routes to which commands. Optional like
335
370
  `extensionPointDef.commandTypes`; re-emit definitions persisted before it. */
336
- handledEvents: @s.matches(handledEventDefArrayOptionSchema) option<array<handledEventDef>>,
371
+ handledEvents: option<array<handledEventDef>>,
337
372
  /** Which internal event sends which command back to the port. `None` means a
338
373
  definition persisted before the field, NOT an extension that issues nothing —
339
374
  a reader joining the two halves must keep them apart. */
340
- issuedCommands: @s.matches(issuedCommandDefArrayOptionSchema) option<array<issuedCommandDef>>,
375
+ issuedCommands: option<array<issuedCommandDef>>,
341
376
  }
342
377
 
343
378
  /**
@@ -346,28 +381,21 @@ An extension point owned by a plugin, from the producer side.
346
381
  `sourceEventTypes` are the `Delegate`'s events feeding the published protocol,
347
382
  plugin-qualified to match `writableDef.producedEventTypes`. `commandTypes` is the
348
383
  EP's inbound protocol — None (read as []) for a `command = unit` EP, which routes
349
- nothing. js_nullable is the only JSON-safe optional here (this def is nested in the
350
- lifecycle Message union); definitions persisted before a field must be re-emitted.
384
+ nothing. Definitions persisted before a field was added must be re-emitted.
351
385
  */
352
386
  @schema
353
387
  type extensionPointDef = {
354
388
  name: string,
355
389
  delegateNames: array<string>,
356
390
  sourceEventTypes: array<string>,
357
- commandTypes: @s.matches(stringArrayOptionSchema) option<array<string>>,
391
+ commandTypes: option<array<string>>,
358
392
  /** Which internal event becomes which published event. */
359
- publishedEvents: @s.matches(publishedEventDefArrayOptionSchema)
360
- option<array<publishedEventDef>>,
393
+ publishedEvents: option<array<publishedEventDef>>,
361
394
  /** Which arriving command becomes which delegate command. `None` means a
362
395
  definition persisted before the field, NOT a port that accepts nothing. */
363
- acceptedCommands: @s.matches(acceptedCommandDefArrayOptionSchema)
364
- option<array<acceptedCommandDef>>,
396
+ acceptedCommands: option<array<acceptedCommandDef>>,
365
397
  }
366
398
 
367
- // js_nullable creates `array | null` (not `| undefined`), which passes sury's
368
- // jsonableValidation inside the pluginStructure union variant payload.
369
- let extensionPointDefArrayOptionSchema = S.array(extensionPointDefSchema)->S.nullAsOption
370
-
371
399
  /**
372
400
  One field's store requirement, with its provenance.
373
401
 
@@ -382,12 +410,9 @@ type requiredStoreDeclaration = {
382
410
  store: string,
383
411
  component: string,
384
412
  field: string,
385
- annotation: @s.matches(stringOptionSchema) option<string>,
413
+ annotation: option<string>,
386
414
  }
387
415
 
388
- let requiredStoreDeclarationArrayOptionSchema =
389
- S.array(requiredStoreDeclarationSchema)->S.nullAsOption
390
-
391
416
  /**
392
417
  One component's capability requirement, with its provenance.
393
418
 
@@ -400,9 +425,6 @@ expressible as an annotation on one, which is why the need is declared.
400
425
  @schema
401
426
  type requiredCapabilityDeclaration = {capability: string, component: string}
402
427
 
403
- let requiredCapabilityDeclarationArrayOptionSchema =
404
- S.array(requiredCapabilityDeclarationSchema)->S.nullAsOption
405
-
406
428
  /**
407
429
  One graft's provenance: which trait, at which version, on which component.
408
430
 
@@ -427,14 +449,13 @@ type traitDeclaration = {
427
449
  component: string,
428
450
  }
429
451
 
430
- let traitDeclarationArrayOptionSchema = S.array(traitDeclarationSchema)->S.nullAsOption
431
-
432
452
  /**
433
453
  Adding a field here? It must be a shape a stale event can be healed into — the
434
454
  lifecycle aggregate replays its own log before every decision, so one event that
435
- fails to decode freezes that plugin's registration. `Message.parseJsonTolerant`
436
- heals `T | null`, arrays, enums and nested objects; a bare scalar is fabricated
437
- and warned about. Prefer `js_nullable`. Regression suite:
455
+ fails to decode freezes that plugin's registration. Make it `option`, which
456
+ `Message.parseJsonTolerant` heals to `None` whatever the inner type is. A required
457
+ array heals to `[]` and a required enum to its first variant; a required *scalar*
458
+ is fabricated and warned about, so it is the one shape to avoid. Regression suite:
438
459
  `PluginLifecycleCorpusTest` — if it goes red, re-shape the field, not the fixtures.
439
460
  */
440
461
  @schema
@@ -449,27 +470,23 @@ type pluginStructure = {
449
470
  extensions: array<extensionDef>,
450
471
  // Extension points owned by this plugin (producer side). Optional so older
451
472
  // definitions still decode (absent → None, read as []).
452
- extensionPoints: @s.matches(extensionPointDefArrayOptionSchema)
453
- option<array<extensionPointDef>>,
473
+ extensionPoints: option<array<extensionPointDef>>,
454
474
  /** The object stores this plugin's fields declare they need, deduplicated and
455
475
  qualified as `{plugin}.{store}` even for the same-plugin case. */
456
- requiredStores: @s.matches(stringArrayOptionSchema) option<array<string>>,
476
+ requiredStores: option<array<string>>,
457
477
  /** Provenance for `requiredStores`: one entry per declaring `(component, field)`.
458
478
  `requiredStores` is derived from it, so the two cannot disagree. */
459
- requiredStoreDeclarations: @s.matches(requiredStoreDeclarationArrayOptionSchema)
460
- option<array<requiredStoreDeclaration>>,
479
+ requiredStoreDeclarations: option<array<requiredStoreDeclaration>>,
461
480
  /** The platform capabilities this plugin's components declare they need, one
462
481
  entry per declaring component. Object stores are not here — a store need is
463
482
  a field's, and travels as `requiredStores`. Absent → None, read as []. */
464
- requiredCapabilities: @s.matches(requiredCapabilityDeclarationArrayOptionSchema)
465
- option<array<requiredCapabilityDeclaration>>,
483
+ requiredCapabilities: option<array<requiredCapabilityDeclaration>>,
466
484
  /** The domain traits grafted into this plugin, one entry per declaring
467
485
  component. Absent → None, read as []. The only signal a graft leaves that
468
486
  survives into a deployed plugin — every other one (the dependency, the
469
487
  variant spread, the rules alias, the conformance binding) is source-side.
470
488
  A claim about origin, never about behaviour: see `Trait`. */
471
- traitDeclarations: @s.matches(traitDeclarationArrayOptionSchema)
472
- option<array<traitDeclaration>>,
489
+ traitDeclarations: option<array<traitDeclaration>>,
473
490
  }
474
491
 
475
492
  let pluginStructureOffloadSchema = Offload.optionSchema(~store="pluginStructures", pluginStructureSchema)
@@ -496,15 +513,15 @@ type pluginDefinition = {
496
513
  apiSchemaFragment: @s.matches(apiSchemaFragmentOffloadSchema) option<Offload.payload<apiSchemaFragment>>,
497
514
  // Schema routing in split-API mode: None/"Domain" → DomainApi, Some("Platform") →
498
515
  // PlatformApi (and excluded from the DomainApi runtime schema).
499
- apiTarget: @s.matches(stringOptionSchema) option<string>,
516
+ apiTarget: option<string>,
500
517
  // Component graph metadata, offloadable like apiSchemaFragment. Absent for older
501
518
  // protocol versions.
502
519
  structure: @s.matches(pluginStructureOffloadSchema) option<Offload.payload<pluginStructure>>,
503
520
  // EventTopic ARN of a bundled DcbEventLog, so the admin can subscribe peer
504
521
  // EventCollectors to it. None for plugins without one.
505
- dcbEventLog: @s.matches(dcbEventLogOptionSchema) option<dcbEventLogDefinition>,
522
+ dcbEventLog: option<dcbEventLogDefinition>,
506
523
  // Mandatory; `Domain` is resolved as the default in Plugin_Builder. Payload-less
507
- // variant → a bare JSON string, so JSON-safe without js_nullable.
524
+ // variant → a bare JSON string.
508
525
  kind: pluginKind,
509
526
  }
510
527