@reventlessdev/reventless-spec 3.0.0-alpha.132 → 3.0.0-alpha.134
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.
- package/CHANGELOG.md +14 -0
- package/package.json +2 -2
- package/src/AnsiStyle.res +0 -1
- package/src/LogPrefix.res +4 -13
- package/src/components/AutomationSlice.res +1 -5
- package/src/components/CapabilityManifest.res +2 -3
- package/src/components/DcbDecode.res +6 -2
- package/src/components/DcbScopeInference.res +16 -12
- package/src/components/DcbTag.res +74 -46
- package/src/components/DcbValidation.res +114 -89
- package/src/components/DisplayName.res +4 -2
- package/src/components/ExtensionPoint.res +0 -1
- package/src/components/FieldDefault.res +52 -0
- package/src/components/FieldDefault.res.mjs +50 -0
- package/src/components/InboundTranslationSlice.res +0 -2
- package/src/components/OutboundTranslationSlice.res +10 -8
- package/src/components/Owner.res +4 -4
- package/src/components/Plugin.res +10 -4
- package/src/components/ReadModel.res +0 -1
- package/src/components/Reference.res +2 -8
- package/src/components/Sensitive.res +4 -4
- package/src/components/StateAnnotations.res +4 -2
- package/src/components/StateChangeSlice.res +0 -2
- package/src/components/StateViewSlice.res +0 -2
- package/src/components/TaggedUnion.res +1 -2
- package/src/components/Task.res +0 -1
- package/src/components/TraitCertificate.res +2 -2
- package/src/components/TraitManifest.res +1 -1
- package/src/generator/CertifyTrait.res +2 -4
- package/src/generator/Codegen.res +77 -68
- package/src/generator/Discovery.res +29 -12
- package/src/generator/GraftTrait.res +17 -9
- package/src/generator/Pairing.res +39 -13
- package/src/generator/PlatformCodegen.res +13 -11
- package/src/generator/PlatformManifests.res +1 -3
- package/src/generator/PluginGenerator.res +0 -1
- package/src/generator/TraitManifestCli.res +7 -7
- package/src/lifecycle/CheckLifecycleModel.res +173 -145
- package/src/lifecycle/CheckLifecycleModel.res.mjs +17 -2
- package/src/semantic/Bytes.res +0 -1
- package/src/semantic/CalendarDate.res +2 -2
- package/src/semantic/Capabilities.res +4 -6
- package/src/semantic/Capabilities.res.mjs +9 -11
- package/src/semantic/CapabilityNeed.res +4 -5
- package/src/semantic/CaptionedImage.res +0 -1
- package/src/semantic/Color.res +0 -1
- package/src/semantic/Currency.res +189 -170
- package/src/semantic/DateRange.res +3 -4
- package/src/semantic/DateTime.res +5 -5
- package/src/semantic/Duration.res +2 -2
- package/src/semantic/Email.res +0 -1
- package/src/semantic/FileRef.res +2 -2
- package/src/semantic/GeoPoint.res +17 -21
- package/src/semantic/Geocoding.res +8 -9
- package/src/semantic/Geolocation.res +6 -9
- package/src/semantic/ImageRef.res +2 -2
- package/src/semantic/MemberRef.res +0 -1
- package/src/semantic/Messaging.res +141 -21
- package/src/semantic/Messaging.res.mjs +51 -8
- package/src/semantic/Money.res +23 -20
- package/src/semantic/Offload.res +6 -4
- package/src/semantic/Percent.res +3 -3
- package/src/semantic/Phone.res +0 -1
- package/src/semantic/RowImage.res +4 -3
- package/src/semantic/Semantic.res +8 -11
- package/src/semantic/StorageRef.res +17 -17
- package/src/semantic/Template.res +5 -7
- package/src/semantic/UploadableFile.res +0 -1
- package/src/semantic/UploadableImage.res +0 -1
- package/src/semantic/Url.res +3 -3
- package/src/types/Authorization.res +1 -2
- package/src/types/Handler.res +0 -1
- package/src/types/Identity.res +1 -3
- package/src/types/Lifecycle.res +7 -6
- package/src/types/Message.res +6 -4
- package/src/types/OwnerScope.res +1 -3
- package/src/types/Projection.res +9 -2
- package/src/types/StoredEvent.res +2 -4
- package/src/types/Transition.res +8 -4
- package/src/util/Util_Sury.res +1 -4
|
@@ -73,12 +73,12 @@ let rec schemasAreCompatible = (a: S.t<unknown>, b: S.t<unknown>): bool =>
|
|
|
73
73
|
let at = av->Array.map(v => v.tagName)->Array.toSorted(String.compare)
|
|
74
74
|
let bt = bv->Array.map(v => v.tagName)->Array.toSorted(String.compare)
|
|
75
75
|
at == bt &&
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
at->Array.every(tag =>
|
|
77
|
+
switch (av->Array.find(v => v.tagName == tag), bv->Array.find(v => v.tagName == tag)) {
|
|
78
|
+
| (Some(x), Some(y)) => propsCompatible(x.fields, y.fields)
|
|
79
|
+
| _ => false
|
|
80
|
+
}
|
|
81
|
+
)
|
|
82
82
|
| _ => false
|
|
83
83
|
}
|
|
84
84
|
// Two property maps are compatible when they carry the same field names and each
|
|
@@ -87,12 +87,12 @@ and propsCompatible = (aProps: dict<S.t<unknown>>, bProps: dict<S.t<unknown>>):
|
|
|
87
87
|
let aKeys = aProps->Dict.keysToArray->Array.toSorted(String.compare)
|
|
88
88
|
let bKeys = bProps->Dict.keysToArray->Array.toSorted(String.compare)
|
|
89
89
|
aKeys == bKeys &&
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
aKeys->Array.every(k =>
|
|
91
|
+
switch (aProps->Dict.get(k), bProps->Dict.get(k)) {
|
|
92
|
+
| (Some(av), Some(bv)) => schemasAreCompatible(av, bv)
|
|
93
|
+
| _ => false
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// Get a human-readable type name for a schema
|
|
@@ -185,26 +185,27 @@ let validateCompositeReads = (
|
|
|
185
185
|
->Array.forEach(cmdKeys => {
|
|
186
186
|
let querySet = dedupeKeys(cmdKeys)
|
|
187
187
|
if querySet->Array.length >= 2 {
|
|
188
|
-
consumedTypes->Array.forEach(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
188
|
+
consumedTypes->Array.forEach(
|
|
189
|
+
consumedType =>
|
|
190
|
+
switch producedTagKeys->Dict.get(consumedType) {
|
|
191
|
+
| None => ()
|
|
192
|
+
| Some(producedKeys) =>
|
|
193
|
+
let producedSet = dedupeKeys(producedKeys)
|
|
194
|
+
let isSuperset = querySet->Array.every(k => producedSet->Array.includes(k))
|
|
195
|
+
if isSuperset && producedSet->Array.length > querySet->Array.length {
|
|
196
|
+
let extra = producedSet->Array.filter(k => !(querySet->Array.includes(k)))
|
|
197
|
+
let _ = warnings->Array.push({
|
|
198
|
+
sliceName,
|
|
199
|
+
message: `composite read on tags [${querySet->Array.join(
|
|
200
|
+
", ",
|
|
201
|
+
)}] will silently miss '${consumedType}', which also carries [${extra->Array.join(
|
|
202
|
+
", ",
|
|
203
|
+
)}] — the tag_composite key includes ALL of an event's tags, so a composite query matches only events tagged exactly [${querySet->Array.join(
|
|
204
|
+
", ",
|
|
205
|
+
)}]. Align the event's tag set with the query, or read '${consumedType}' via a single-tag clause.`,
|
|
206
|
+
})
|
|
207
|
+
}
|
|
208
|
+
},
|
|
208
209
|
)
|
|
209
210
|
}
|
|
210
211
|
})
|
|
@@ -231,7 +232,9 @@ let validateProducedAndConsumed = (
|
|
|
231
232
|
|
|
232
233
|
// Rule 1: Payload equivalence across producers
|
|
233
234
|
// All producers of the same TAG must have identical fields, types, and tag annotations
|
|
234
|
-
producerMap
|
|
235
|
+
producerMap
|
|
236
|
+
->Dict.toArray
|
|
237
|
+
->Array.forEach(((tagName, entries)) => {
|
|
235
238
|
if entries->Array.length > 1 {
|
|
236
239
|
let first = entries->Array.getUnsafe(0)
|
|
237
240
|
let firstFields = first.variant.fields->Dict.keysToArray->Array.toSorted(String.compare)
|
|
@@ -243,70 +246,85 @@ let validateProducedAndConsumed = (
|
|
|
243
246
|
let entryTagged = entry.variant.taggedFields->Array.toSorted(String.compare)
|
|
244
247
|
|
|
245
248
|
// Check field names match
|
|
246
|
-
if
|
|
247
|
-
firstFields->Array.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
let
|
|
251
|
-
|
|
249
|
+
if (
|
|
250
|
+
firstFields->Array.length != entryFields->Array.length ||
|
|
251
|
+
firstFields->Array.some(f => !(entryFields->Array.includes(f)))
|
|
252
|
+
) {
|
|
253
|
+
let missingInEntry = firstFields->Array.filter(f => !(entryFields->Array.includes(f)))
|
|
254
|
+
let extraInEntry = entryFields->Array.filter(f => !(firstFields->Array.includes(f)))
|
|
252
255
|
let parts = []
|
|
253
256
|
if missingInEntry->Array.length > 0 {
|
|
254
257
|
let _ =
|
|
255
258
|
parts->Array.push(
|
|
256
|
-
`'${first.sliceName}' has fields [${missingInEntry->Array.join(
|
|
259
|
+
`'${first.sliceName}' has fields [${missingInEntry->Array.join(
|
|
260
|
+
", ",
|
|
261
|
+
)}] which '${entry.sliceName}' is missing`,
|
|
257
262
|
)
|
|
258
263
|
}
|
|
259
264
|
if extraInEntry->Array.length > 0 {
|
|
260
265
|
let _ =
|
|
261
266
|
parts->Array.push(
|
|
262
|
-
`'${entry.sliceName}' has fields [${extraInEntry->Array.join(
|
|
267
|
+
`'${entry.sliceName}' has fields [${extraInEntry->Array.join(
|
|
268
|
+
", ",
|
|
269
|
+
)}] which '${first.sliceName}' is missing`,
|
|
263
270
|
)
|
|
264
271
|
}
|
|
265
|
-
let _ =
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
272
|
+
let _ = errors->Array.push({
|
|
273
|
+
sliceName: entry.sliceName,
|
|
274
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but with different fields: ${parts->Array.join(
|
|
275
|
+
"; ",
|
|
276
|
+
)}`,
|
|
277
|
+
})
|
|
270
278
|
} else {
|
|
271
279
|
// Fields match by name — check types are compatible
|
|
272
|
-
firstFields->Array.forEach(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
errors->Array.push({
|
|
280
|
+
firstFields->Array.forEach(
|
|
281
|
+
fieldName => {
|
|
282
|
+
let firstSchema = first.variant.fields->Dict.getUnsafe(fieldName)
|
|
283
|
+
let entrySchema = entry.variant.fields->Dict.getUnsafe(fieldName)
|
|
284
|
+
if !schemasAreCompatible(firstSchema, entrySchema) {
|
|
285
|
+
let _ = errors->Array.push({
|
|
278
286
|
sliceName: entry.sliceName,
|
|
279
|
-
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but field '${fieldName}' has type '${schemaTypeName(
|
|
287
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but field '${fieldName}' has type '${schemaTypeName(
|
|
288
|
+
firstSchema,
|
|
289
|
+
)}' in '${first.sliceName}' and '${schemaTypeName(
|
|
290
|
+
entrySchema,
|
|
291
|
+
)}' in '${entry.sliceName}'`,
|
|
280
292
|
})
|
|
281
|
-
|
|
282
|
-
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
)
|
|
283
296
|
}
|
|
284
297
|
|
|
285
298
|
// Check tag annotations match
|
|
286
|
-
if
|
|
287
|
-
firstTagged->Array.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
let
|
|
291
|
-
|
|
299
|
+
if (
|
|
300
|
+
firstTagged->Array.length != entryTagged->Array.length ||
|
|
301
|
+
firstTagged->Array.some(f => !(entryTagged->Array.includes(f)))
|
|
302
|
+
) {
|
|
303
|
+
let untaggedInEntry = firstTagged->Array.filter(f => !(entryTagged->Array.includes(f)))
|
|
304
|
+
let untaggedInFirst = entryTagged->Array.filter(f => !(firstTagged->Array.includes(f)))
|
|
292
305
|
let parts = []
|
|
293
306
|
if untaggedInEntry->Array.length > 0 {
|
|
294
307
|
let _ =
|
|
295
308
|
parts->Array.push(
|
|
296
|
-
`[${untaggedInEntry->Array.join(
|
|
309
|
+
`[${untaggedInEntry->Array.join(
|
|
310
|
+
", ",
|
|
311
|
+
)}] tagged in '${first.sliceName}' but not in '${entry.sliceName}'`,
|
|
297
312
|
)
|
|
298
313
|
}
|
|
299
314
|
if untaggedInFirst->Array.length > 0 {
|
|
300
315
|
let _ =
|
|
301
316
|
parts->Array.push(
|
|
302
|
-
`[${untaggedInFirst->Array.join(
|
|
317
|
+
`[${untaggedInFirst->Array.join(
|
|
318
|
+
", ",
|
|
319
|
+
)}] tagged in '${entry.sliceName}' but not in '${first.sliceName}'`,
|
|
303
320
|
)
|
|
304
321
|
}
|
|
305
|
-
let _ =
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
322
|
+
let _ = errors->Array.push({
|
|
323
|
+
sliceName: entry.sliceName,
|
|
324
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but tag annotations differ: ${parts->Array.join(
|
|
325
|
+
"; ",
|
|
326
|
+
)}`,
|
|
327
|
+
})
|
|
310
328
|
}
|
|
311
329
|
}
|
|
312
330
|
})
|
|
@@ -321,35 +339,42 @@ let validateProducedAndConsumed = (
|
|
|
321
339
|
variants->Array.forEach(variant => {
|
|
322
340
|
switch producerMap->Dict.get(variant.tagName) {
|
|
323
341
|
| None =>
|
|
324
|
-
let _ =
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
})
|
|
342
|
+
let _ = errors->Array.push({
|
|
343
|
+
sliceName,
|
|
344
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}' but no slice produces it`,
|
|
345
|
+
})
|
|
329
346
|
| Some(producers) =>
|
|
330
347
|
// Use first producer as the authoritative shape (Rule 1 ensures they're all identical)
|
|
331
348
|
let producer = producers->Array.getUnsafe(0)
|
|
332
349
|
|
|
333
350
|
// Skip field checks for payload-less consumed variants
|
|
334
351
|
if !isPayloadLess(variant) {
|
|
335
|
-
variant.fields
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
352
|
+
variant.fields
|
|
353
|
+
->Dict.toArray
|
|
354
|
+
->Array.forEach(
|
|
355
|
+
((fieldName, consumedFieldSchema)) => {
|
|
356
|
+
switch producer.variant.fields->Dict.get(fieldName) {
|
|
357
|
+
| None =>
|
|
358
|
+
let _ = errors->Array.push({
|
|
340
359
|
sliceName,
|
|
341
|
-
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' (${schemaTypeName(
|
|
360
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' (${schemaTypeName(
|
|
361
|
+
consumedFieldSchema,
|
|
362
|
+
)}) but produced '${variant.tagName}' has no field '${fieldName}'`,
|
|
342
363
|
})
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
errors->Array.push({
|
|
364
|
+
| Some(producedFieldSchema) =>
|
|
365
|
+
if !schemasAreCompatible(consumedFieldSchema, producedFieldSchema) {
|
|
366
|
+
let _ = errors->Array.push({
|
|
347
367
|
sliceName,
|
|
348
|
-
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' as ${describeSchema(
|
|
368
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' as ${describeSchema(
|
|
369
|
+
consumedFieldSchema,
|
|
370
|
+
)} but producer '${producer.sliceName}' declares it as ${describeSchema(
|
|
371
|
+
producedFieldSchema,
|
|
372
|
+
)}`,
|
|
349
373
|
})
|
|
374
|
+
}
|
|
350
375
|
}
|
|
351
|
-
}
|
|
352
|
-
|
|
376
|
+
},
|
|
377
|
+
)
|
|
353
378
|
}
|
|
354
379
|
}
|
|
355
380
|
})
|
|
@@ -375,9 +400,9 @@ one warning per producer that carries a key some *other* producer declared
|
|
|
375
400
|
|
|
376
401
|
@param producers `(sliceName, producedEventSchema)` per producing slice.
|
|
377
402
|
*/
|
|
378
|
-
let validateCrossPartitionScope = (
|
|
379
|
-
|
|
380
|
-
|
|
403
|
+
let validateCrossPartitionScope = (~producers: array<(string, S.t<unknown>)>): array<
|
|
404
|
+
validationError,
|
|
405
|
+
> => {
|
|
381
406
|
let perProducer = producers->Array.map(((name, schema)) => {
|
|
382
407
|
let cpSet = Set.make()
|
|
383
408
|
DcbTag.extractCrossPartitionTagKeys(schema)->Array.forEach(k => cpSet->Set.add(k))
|
|
@@ -14,8 +14,10 @@ type displayNameSpec = {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/** Sury metadata ID used to attach a `displayNameSpec` to a state schema. */
|
|
17
|
-
let displayNameId: S.Metadata.Id.t<displayNameSpec> =
|
|
18
|
-
|
|
17
|
+
let displayNameId: S.Metadata.Id.t<displayNameSpec> = S.Metadata.Id.make(
|
|
18
|
+
~namespace="reventless",
|
|
19
|
+
~name="displayName",
|
|
20
|
+
)
|
|
19
21
|
|
|
20
22
|
/** Returns the spec attached to a state schema, if any. */
|
|
21
23
|
let getSpec = (schema: S.t<unknown>): option<displayNameSpec> =>
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The value a field opens with where nobody has said otherwise, published as JSON
|
|
3
|
+
Schema's own `default` keyword.
|
|
4
|
+
|
|
5
|
+
A statement about the domain: an order line is for one of the thing unless the
|
|
6
|
+
shopper says two. A consumer that finds no default opens the field blank rather
|
|
7
|
+
than inventing one.
|
|
8
|
+
|
|
9
|
+
Write `@default(1)` — see `DefaultInference`, which desugars to the constructors
|
|
10
|
+
below. The marker rides on the field's own schema rather than in a record-level
|
|
11
|
+
spec keyed by field name, which is what makes it work at any depth: a record
|
|
12
|
+
nested inside a command carries no such spec, and its fields are the ones a
|
|
13
|
+
repeating group opens blank.
|
|
14
|
+
|
|
15
|
+
@example
|
|
16
|
+
```rescript
|
|
17
|
+
@schema
|
|
18
|
+
type lineItem = {
|
|
19
|
+
@ref("AvailableProducts") productId: string,
|
|
20
|
+
@default(1) quantity: int,
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
*/
|
|
24
|
+
let defaultId: S.Metadata.Id.t<JSON.t> = S.Metadata.Id.make(
|
|
25
|
+
~namespace="reventless",
|
|
26
|
+
~name="default",
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
/** Layers a default onto whatever the field already resolved to. Wraps rather
|
|
30
|
+
than replaces, for the reason `Sensitive.mark` does: a field carries at most
|
|
31
|
+
one `@s.matches`. */
|
|
32
|
+
let mark = (schema: S.t<'a>, value: JSON.t): S.t<'a> => schema->S.Metadata.set(~id=defaultId, value)
|
|
33
|
+
|
|
34
|
+
let int = (schema: S.t<int>, value: int): S.t<int> => schema->mark(JSON.Encode.int(value))
|
|
35
|
+
|
|
36
|
+
let float = (schema: S.t<float>, value: float): S.t<float> => schema->mark(JSON.Encode.float(value))
|
|
37
|
+
|
|
38
|
+
let string = (schema: S.t<string>, value: string): S.t<string> =>
|
|
39
|
+
schema->mark(JSON.Encode.string(value))
|
|
40
|
+
|
|
41
|
+
let bool = (schema: S.t<bool>, value: bool): S.t<bool> => schema->mark(JSON.Encode.bool(value))
|
|
42
|
+
|
|
43
|
+
/** The default a field declares. An optional field keeps its marker inside the
|
|
44
|
+
wrapper sury builds for `f?: t`, so the outer schema is read first and a
|
|
45
|
+
marker on the wrapper wins — the precedence `Semantic.getFrom` follows. */
|
|
46
|
+
let rec getFrom = (schema: S.t<unknown>): option<JSON.t> =>
|
|
47
|
+
switch S.Metadata.get(schema, ~id=defaultId) {
|
|
48
|
+
| Some(_) as found => found
|
|
49
|
+
| None => schema->Semantic.unwrapOptional->Option.flatMap(getFrom)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let get = (fieldSchema: S.t<'a>): option<JSON.t> => fieldSchema->S.castToUnknown->getFrom
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
6
|
+
|
|
7
|
+
let defaultId = Sury.$Metadata_Id_make("reventless", "default");
|
|
8
|
+
|
|
9
|
+
function mark(schema, value) {
|
|
10
|
+
return Sury.$Metadata_set(schema, defaultId, value);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function int(schema, value) {
|
|
14
|
+
return Sury.$Metadata_set(schema, defaultId, value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function float(schema, value) {
|
|
18
|
+
return Sury.$Metadata_set(schema, defaultId, value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function string(schema, value) {
|
|
22
|
+
return Sury.$Metadata_set(schema, defaultId, value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function bool(schema, value) {
|
|
26
|
+
return Sury.$Metadata_set(schema, defaultId, value);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getFrom(schema) {
|
|
30
|
+
let found = Sury.$Metadata_get(schema, defaultId);
|
|
31
|
+
if (found !== undefined) {
|
|
32
|
+
return found;
|
|
33
|
+
} else {
|
|
34
|
+
return Stdlib_Option.flatMap(Semantic$Reventless.unwrapOptional(schema), getFrom);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let get = getFrom;
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
defaultId,
|
|
42
|
+
mark,
|
|
43
|
+
int,
|
|
44
|
+
float,
|
|
45
|
+
string,
|
|
46
|
+
bool,
|
|
47
|
+
getFrom,
|
|
48
|
+
get,
|
|
49
|
+
}
|
|
50
|
+
/* defaultId Not a pure module */
|
|
@@ -31,7 +31,6 @@ let translate = input => switch input.status {
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
*/
|
|
34
|
-
|
|
35
34
|
/**
|
|
36
35
|
The lean Spec for an InboundTranslationSlice — types, identity, schemas.
|
|
37
36
|
*/
|
|
@@ -96,4 +95,3 @@ module type Translation = {
|
|
|
96
95
|
/** File URL of this Translation module (`import.meta.url`). */
|
|
97
96
|
let moduleUrl: string
|
|
98
97
|
}
|
|
99
|
-
|
|
@@ -44,7 +44,6 @@ let maxRetries = 3
|
|
|
44
44
|
let heartbeatInterval = 60
|
|
45
45
|
```
|
|
46
46
|
*/
|
|
47
|
-
|
|
48
47
|
/**
|
|
49
48
|
The lean Spec for an OutboundTranslationSlice — types, identity, schemas, sweep config.
|
|
50
49
|
*/
|
|
@@ -167,9 +166,11 @@ module type Translation = {
|
|
|
167
166
|
the framework knows nothing about still reaches it directly and simply ignores
|
|
168
167
|
this argument.
|
|
169
168
|
*/
|
|
170
|
-
let translate: (
|
|
171
|
-
|
|
172
|
-
|
|
169
|
+
let translate: (
|
|
170
|
+
string,
|
|
171
|
+
Spec.outboundItem,
|
|
172
|
+
~capabilities: Capabilities.t,
|
|
173
|
+
) => promise<result<option<(string, Spec.inboundCommand)>, string>>
|
|
173
174
|
|
|
174
175
|
/**
|
|
175
176
|
The retry budget is spent: this item will never be attempted again.
|
|
@@ -186,11 +187,12 @@ module type Translation = {
|
|
|
186
187
|
The row is marked `Abandoned` either way; this decides only whether anything
|
|
187
188
|
downstream hears about it.
|
|
188
189
|
*/
|
|
189
|
-
let onExhausted: (
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
let onExhausted: (
|
|
191
|
+
string,
|
|
192
|
+
Spec.outboundItem,
|
|
193
|
+
~lastError: option<string>,
|
|
194
|
+
) => option<(string, Spec.inboundCommand)>
|
|
192
195
|
|
|
193
196
|
/** File URL of this Translation module (`import.meta.url`). */
|
|
194
197
|
let moduleUrl: string
|
|
195
198
|
}
|
|
196
|
-
|
package/src/components/Owner.res
CHANGED
|
@@ -78,10 +78,10 @@ the view on the wrong value.
|
|
|
78
78
|
*/
|
|
79
79
|
let isFieldOwner = (schema: S.t<unknown>): bool =>
|
|
80
80
|
isOwner(schema) ||
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
81
|
+
switch schema->Semantic.unwrapOptional {
|
|
82
|
+
| Some(inner) => isOwner(inner)
|
|
83
|
+
| None => false
|
|
84
|
+
}
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
The owner fields declared on an object schema, in declaration order.
|
|
@@ -79,7 +79,10 @@ type apiTarget = Domain | Platform
|
|
|
79
79
|
// nothing to annotate. The `T | null` these fields used to carry worked around a sury
|
|
80
80
|
// bug — undefined failing jsonableValidation inside a union variant payload — fixed in
|
|
81
81
|
// 11.0.0-alpha.11. `Offload` below is not an optional wrapper but an either-or codec.
|
|
82
|
-
let apiSchemaFragmentOffloadSchema = Offload.optionSchema(
|
|
82
|
+
let apiSchemaFragmentOffloadSchema = Offload.optionSchema(
|
|
83
|
+
~store="pluginApiFragments",
|
|
84
|
+
apiSchemaFragmentSchema,
|
|
85
|
+
)
|
|
83
86
|
|
|
84
87
|
// ── UI fragment manifest types ────────────────────────────────────────────────
|
|
85
88
|
|
|
@@ -489,7 +492,10 @@ type pluginStructure = {
|
|
|
489
492
|
traitDeclarations: option<array<traitDeclaration>>,
|
|
490
493
|
}
|
|
491
494
|
|
|
492
|
-
let pluginStructureOffloadSchema = Offload.optionSchema(
|
|
495
|
+
let pluginStructureOffloadSchema = Offload.optionSchema(
|
|
496
|
+
~store="pluginStructures",
|
|
497
|
+
pluginStructureSchema,
|
|
498
|
+
)
|
|
493
499
|
|
|
494
500
|
/**
|
|
495
501
|
The self-description of a deployed plugin, persisted in the plugin's event store.
|
|
@@ -510,7 +516,8 @@ type pluginDefinition = {
|
|
|
510
516
|
extensionProtocols: array<extensionProtocol>,
|
|
511
517
|
// Offloadable: a large SDL fragment is content-addressed to the pluginApiFragments
|
|
512
518
|
// store and carried by reference; a small one stays Inline.
|
|
513
|
-
apiSchemaFragment: @s.matches(apiSchemaFragmentOffloadSchema)
|
|
519
|
+
apiSchemaFragment: @s.matches(apiSchemaFragmentOffloadSchema)
|
|
520
|
+
option<Offload.payload<apiSchemaFragment>>,
|
|
514
521
|
// Schema routing in split-API mode: None/"Domain" → DomainApi, Some("Platform") →
|
|
515
522
|
// PlatformApi (and excluded from the DomainApi runtime schema).
|
|
516
523
|
apiTarget: option<string>,
|
|
@@ -524,4 +531,3 @@ type pluginDefinition = {
|
|
|
524
531
|
// variant → a bare JSON string.
|
|
525
532
|
kind: pluginKind,
|
|
526
533
|
}
|
|
527
|
-
|
|
@@ -99,10 +99,7 @@ Only one record deep, matching [`DcbTag.nestedRecordProperties`], which is the
|
|
|
99
99
|
walk shared with the tag extraction so a marker is found in the same places by
|
|
100
100
|
both.
|
|
101
101
|
*/
|
|
102
|
-
let collectFieldTargets = (
|
|
103
|
-
fieldName: string,
|
|
104
|
-
schema: S.t<unknown>,
|
|
105
|
-
): array<(string, target)> =>
|
|
102
|
+
let collectFieldTargets = (fieldName: string, schema: S.t<unknown>): array<(string, target)> =>
|
|
106
103
|
switch getFieldTarget(schema) {
|
|
107
104
|
| Some(target) => [(fieldName, target)]
|
|
108
105
|
| None =>
|
|
@@ -115,10 +112,7 @@ let collectFieldTargets = (
|
|
|
115
112
|
->Array.filterMap(((propName, propSchema)) =>
|
|
116
113
|
propName === "TAG"
|
|
117
114
|
? None
|
|
118
|
-
: getFieldTarget(propSchema)->Option.map(target => (
|
|
119
|
-
`${prefix}.${propName}`,
|
|
120
|
-
target,
|
|
121
|
-
))
|
|
115
|
+
: getFieldTarget(propSchema)->Option.map(target => (`${prefix}.${propName}`, target))
|
|
122
116
|
)
|
|
123
117
|
}
|
|
124
118
|
}
|
|
@@ -83,10 +83,10 @@ the enclosing one would withhold a whole record because one leaf is private.
|
|
|
83
83
|
*/
|
|
84
84
|
let isFieldSensitive = (schema: S.t<unknown>): bool =>
|
|
85
85
|
isSensitive(schema) ||
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
switch schema->Semantic.unwrapOptional {
|
|
87
|
+
| Some(inner) => isSensitive(inner)
|
|
88
|
+
| None => false
|
|
89
|
+
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
92
|
The sensitive fields declared on an object schema, in declaration order.
|
|
@@ -211,8 +211,10 @@ type stateAnnotationSpec = {
|
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
/** Sury metadata ID used to attach a `stateAnnotationSpec` to a state schema. */
|
|
214
|
-
let stateAnnotationsId: S.Metadata.Id.t<stateAnnotationSpec> =
|
|
215
|
-
|
|
214
|
+
let stateAnnotationsId: S.Metadata.Id.t<stateAnnotationSpec> = S.Metadata.Id.make(
|
|
215
|
+
~namespace="reventless",
|
|
216
|
+
~name="stateAnnotations",
|
|
217
|
+
)
|
|
216
218
|
|
|
217
219
|
/** Returns the spec attached to a state schema, if any. */
|
|
218
220
|
let getSpec = (schema: S.t<unknown>): option<stateAnnotationSpec> =>
|
|
@@ -41,7 +41,6 @@ let decide = (state, command) => switch command {
|
|
|
41
41
|
}
|
|
42
42
|
```
|
|
43
43
|
*/
|
|
44
|
-
|
|
45
44
|
/**
|
|
46
45
|
The lean Spec for a StateChangeSlice — types, identity, schemas. State and
|
|
47
46
|
state-evolution functions live in the sibling `Behavior` module type.
|
|
@@ -150,4 +149,3 @@ module type Behavior = {
|
|
|
150
149
|
/** File URL of this Behavior module (`import.meta.url`). */
|
|
151
150
|
let moduleUrl: string
|
|
152
151
|
}
|
|
153
|
-
|
|
@@ -34,7 +34,6 @@ let project = ({event}) => switch event {
|
|
|
34
34
|
}
|
|
35
35
|
```
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
37
|
/**
|
|
39
38
|
The lean Spec for a StateViewSlice — types, identity, schemas, infra config.
|
|
40
39
|
Per D2, `state` lives here (not in `Projection`) because the projected state
|
|
@@ -120,4 +119,3 @@ module type Projection = {
|
|
|
120
119
|
/** File URL of this Projection module (`import.meta.url`). */
|
|
121
120
|
let moduleUrl: string
|
|
122
121
|
}
|
|
123
|
-
|
|
@@ -113,8 +113,7 @@ let armsOf = (schema: S.t<unknown>): option<array<arm>> =>
|
|
|
113
113
|
| Some(String({const: ?Some(tag)})) =>
|
|
114
114
|
let fields = properties->Dict.toArray->Array.filter(((name, _)) => name !== "TAG")
|
|
115
115
|
if (
|
|
116
|
-
fields->Array.length == 0 ||
|
|
117
|
-
fields->Array.some(((name, _)) => isPositionalName(name))
|
|
116
|
+
fields->Array.length == 0 || fields->Array.some(((name, _)) => isPositionalName(name))
|
|
118
117
|
) {
|
|
119
118
|
None
|
|
120
119
|
} else {
|
package/src/components/Task.res
CHANGED
|
@@ -26,7 +26,6 @@ The framework does not own the runner. A consumer runs their own suite, their ow
|
|
|
26
26
|
way, and hands the report here — so this module is a pure transformation with no
|
|
27
27
|
opinion about Jest, CI, or where files live. `fromReport` is the whole of it.
|
|
28
28
|
*/
|
|
29
|
-
|
|
30
29
|
/** One assertion, and whether it held. Named, not numbered: a count that changed
|
|
31
30
|
tells a reader nothing, and a name that disappeared tells them everything. */
|
|
32
31
|
@schema
|
|
@@ -101,5 +100,6 @@ let render = (certificate: t): string =>
|
|
|
101
100
|
console reader and a machine reader agree without either restating the rule. */
|
|
102
101
|
let summarize = (certificate: t): string =>
|
|
103
102
|
`${certificate.trait}@${certificate.traitVersion} → ${certificate.host}: ` ++
|
|
104
|
-
`${certificate.passed->Int.toString}/${
|
|
103
|
+
`${certificate.passed->Int.toString}/${certificate.assertions
|
|
104
|
+
->Array.length
|
|
105
105
|
->Int.toString} assertions, ` ++ (certificate->verified ? "verified" : "NOT verified")
|