@reventlessdev/reventless-spec 3.0.0-alpha.62 → 3.0.0-alpha.64

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 CHANGED
@@ -3,6 +3,21 @@
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.64 (2026-07-03)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-spec
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.63 (2026-07-02)
15
+
16
+ ### Bug Fixes
17
+
18
+ * **spec,interop,layer-builder:** generator/protocol/build failure modes (plan A8,A9) ([66d7a54](https://github.com/ReventlessDev/reventless-core/commit/66d7a54e3a0afdbfe3ea2975f517d1d64d52c180))
19
+
20
+
6
21
  # 3.0.0-alpha.62 (2026-06-29)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,18 +1,28 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.62",
3
+ "version": "3.0.0-alpha.64",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
7
7
  "generate-plugin": "./run-generator.mjs"
8
8
  },
9
+ "jest": {
10
+ "testMatch": [
11
+ "<rootDir>/tests/**/*Test.res.mjs"
12
+ ],
13
+ "moduleFileExtensions": [
14
+ "js",
15
+ "mjs"
16
+ ]
17
+ },
9
18
  "dependencies": {
10
19
  "jsonschema2graphql": "1.1.1",
11
20
  "sury": "11.0.0-alpha.4",
12
21
  "sury-ppx": "11.0.0-alpha.2"
13
22
  },
14
23
  "devDependencies": {
15
- "rescript": "^12.3.0"
24
+ "rescript": "^12.3.0",
25
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.5"
16
26
  },
17
27
  "peerDependencies": {
18
28
  "rescript": "^12.3.0"
@@ -28,6 +38,8 @@
28
38
  "scripts": {
29
39
  "build": "rescript build",
30
40
  "start": "rescript start",
31
- "clean": "rescript clean"
41
+ "clean": "rescript clean",
42
+ "test": "NODE_OPTIONS='--experimental-vm-modules' jest",
43
+ "dev": "NODE_OPTIONS='--experimental-vm-modules' jest --watchAll"
32
44
  }
33
45
  }
package/rescript.json CHANGED
@@ -15,10 +15,15 @@
15
15
  {
16
16
  "dir": "src",
17
17
  "subdirs": true
18
+ },
19
+ {
20
+ "dir": "tests",
21
+ "subdirs": true
18
22
  }
19
23
  ],
20
24
  "dependencies": [
21
- "sury"
25
+ "sury",
26
+ "@reventlessdev/rescript-jest"
22
27
  ],
23
28
  "compiler-flags": [],
24
29
  "suffix": ".res.mjs"
@@ -0,0 +1,99 @@
1
+ // Single source of truth for the Reventless component-kind vocabulary: the kinds
2
+ // themselves, every accepted folder spelling (singular / plural / short form),
3
+ // the canonical folder name, and the body-file suffixes. The plugin generator
4
+ // (`generator/Discovery`) and the external tooling that classifies files by
5
+ // folder (`reventless-gwt`'s `ComponentMeta`) both derive from this, so the two
6
+ // can't drift — previously the generator accepted plural/short folder names that
7
+ // the gwt discovery silently didn't recognise.
8
+
9
+ type t =
10
+ | StateChangeSlice
11
+ | StateViewSlice
12
+ | StateViewSliceStream
13
+ | AutomationSlice
14
+ | InboundTranslationSlice
15
+ | OutboundTranslationSlice
16
+ | Aggregate
17
+ | ReadModel
18
+ | ReadModelStream
19
+ | Task
20
+ | ExtensionPoint
21
+ | Extension
22
+
23
+ let all = [
24
+ StateChangeSlice,
25
+ StateViewSlice,
26
+ StateViewSliceStream,
27
+ AutomationSlice,
28
+ InboundTranslationSlice,
29
+ OutboundTranslationSlice,
30
+ Aggregate,
31
+ ReadModel,
32
+ ReadModelStream,
33
+ Task,
34
+ ExtensionPoint,
35
+ Extension,
36
+ ]
37
+
38
+ // The canonical (singular) folder name for a kind — the spelling the naming
39
+ // conventions and the graph `kind` field use.
40
+ let folderName = (t: t): string =>
41
+ switch t {
42
+ | StateChangeSlice => "StateChangeSlice"
43
+ | StateViewSlice => "StateViewSlice"
44
+ | StateViewSliceStream => "StateViewSliceStream"
45
+ | AutomationSlice => "AutomationSlice"
46
+ | InboundTranslationSlice => "InboundTranslationSlice"
47
+ | OutboundTranslationSlice => "OutboundTranslationSlice"
48
+ | Aggregate => "Aggregate"
49
+ | ReadModel => "ReadModel"
50
+ | ReadModelStream => "ReadModelStream"
51
+ | Task => "Task"
52
+ | ExtensionPoint => "ExtensionPoint"
53
+ | Extension => "Extension"
54
+ }
55
+
56
+ // Every folder spelling that denotes a kind → the kind. Tolerates the singular,
57
+ // plural, and (for slices) short forms the generator has always accepted.
58
+ let folderToKind = (folder: string): option<t> =>
59
+ switch folder {
60
+ | "StateChange" | "StateChanges" | "StateChangeSlice" | "StateChangeSlices" =>
61
+ Some(StateChangeSlice)
62
+ | "StateView" | "StateViews" | "StateViewSlice" | "StateViewSlices" => Some(StateViewSlice)
63
+ | "StateViewSliceStream" | "StateViewSliceStreams" => Some(StateViewSliceStream)
64
+ | "Automation" | "Automations" | "AutomationSlice" | "AutomationSlices" => Some(AutomationSlice)
65
+ | "InboundTranslation"
66
+ | "InboundTranslations"
67
+ | "InboundTranslationSlice"
68
+ | "InboundTranslationSlices" =>
69
+ Some(InboundTranslationSlice)
70
+ | "OutboundTranslation"
71
+ | "OutboundTranslations"
72
+ | "OutboundTranslationSlice"
73
+ | "OutboundTranslationSlices" =>
74
+ Some(OutboundTranslationSlice)
75
+ | "Aggregate" | "Aggregates" => Some(Aggregate)
76
+ | "ReadModel" | "ReadModels" => Some(ReadModel)
77
+ | "ReadModelStream" | "ReadModelStreams" => Some(ReadModelStream)
78
+ | "Task" | "Tasks" => Some(Task)
79
+ | "ExtensionPoint" | "ExtensionPoints" => Some(ExtensionPoint)
80
+ | "Extension" | "Extensions" => Some(Extension)
81
+ | _ => None
82
+ }
83
+
84
+ let isKindFolder = (folder: string): bool => folderToKind(folder)->Option.isSome
85
+
86
+ // Body-file suffixes stripped to recover a component's spec stem (longest first,
87
+ // so `_ExtensionPointMapping` is removed before `_ExtensionPoint`). Spec and body
88
+ // files in one folder collapse to the same component name.
89
+ let bodySuffixes = [
90
+ "_ExtensionPointMapping",
91
+ "_ExtensionPoint",
92
+ "_Extension",
93
+ "_Projections",
94
+ "_Projection",
95
+ "_Mappings",
96
+ "_Behavior",
97
+ "_Automation",
98
+ "_Translation",
99
+ ]
@@ -0,0 +1,125 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+
5
+ let all = [
6
+ "StateChangeSlice",
7
+ "StateViewSlice",
8
+ "StateViewSliceStream",
9
+ "AutomationSlice",
10
+ "InboundTranslationSlice",
11
+ "OutboundTranslationSlice",
12
+ "Aggregate",
13
+ "ReadModel",
14
+ "ReadModelStream",
15
+ "Task",
16
+ "ExtensionPoint",
17
+ "Extension"
18
+ ];
19
+
20
+ function folderName(t) {
21
+ switch (t) {
22
+ case "StateChangeSlice" :
23
+ return "StateChangeSlice";
24
+ case "StateViewSlice" :
25
+ return "StateViewSlice";
26
+ case "StateViewSliceStream" :
27
+ return "StateViewSliceStream";
28
+ case "AutomationSlice" :
29
+ return "AutomationSlice";
30
+ case "InboundTranslationSlice" :
31
+ return "InboundTranslationSlice";
32
+ case "OutboundTranslationSlice" :
33
+ return "OutboundTranslationSlice";
34
+ case "Aggregate" :
35
+ return "Aggregate";
36
+ case "ReadModel" :
37
+ return "ReadModel";
38
+ case "ReadModelStream" :
39
+ return "ReadModelStream";
40
+ case "Task" :
41
+ return "Task";
42
+ case "ExtensionPoint" :
43
+ return "ExtensionPoint";
44
+ case "Extension" :
45
+ return "Extension";
46
+ }
47
+ }
48
+
49
+ function folderToKind(folder) {
50
+ switch (folder) {
51
+ case "Aggregate" :
52
+ case "Aggregates" :
53
+ return "Aggregate";
54
+ case "Automation" :
55
+ case "AutomationSlice" :
56
+ case "AutomationSlices" :
57
+ case "Automations" :
58
+ return "AutomationSlice";
59
+ case "ExtensionPoint" :
60
+ case "ExtensionPoints" :
61
+ return "ExtensionPoint";
62
+ case "Extension" :
63
+ case "Extensions" :
64
+ return "Extension";
65
+ case "InboundTranslation" :
66
+ case "InboundTranslationSlice" :
67
+ case "InboundTranslationSlices" :
68
+ case "InboundTranslations" :
69
+ return "InboundTranslationSlice";
70
+ case "OutboundTranslation" :
71
+ case "OutboundTranslationSlice" :
72
+ case "OutboundTranslationSlices" :
73
+ case "OutboundTranslations" :
74
+ return "OutboundTranslationSlice";
75
+ case "ReadModelStream" :
76
+ case "ReadModelStreams" :
77
+ return "ReadModelStream";
78
+ case "ReadModel" :
79
+ case "ReadModels" :
80
+ return "ReadModel";
81
+ case "StateChange" :
82
+ case "StateChangeSlice" :
83
+ case "StateChangeSlices" :
84
+ case "StateChanges" :
85
+ return "StateChangeSlice";
86
+ case "StateViewSliceStream" :
87
+ case "StateViewSliceStreams" :
88
+ return "StateViewSliceStream";
89
+ case "StateView" :
90
+ case "StateViewSlice" :
91
+ case "StateViewSlices" :
92
+ case "StateViews" :
93
+ return "StateViewSlice";
94
+ case "Task" :
95
+ case "Tasks" :
96
+ return "Task";
97
+ default:
98
+ return;
99
+ }
100
+ }
101
+
102
+ function isKindFolder(folder) {
103
+ return Stdlib_Option.isSome(folderToKind(folder));
104
+ }
105
+
106
+ let bodySuffixes = [
107
+ "_ExtensionPointMapping",
108
+ "_ExtensionPoint",
109
+ "_Extension",
110
+ "_Projections",
111
+ "_Projection",
112
+ "_Mappings",
113
+ "_Behavior",
114
+ "_Automation",
115
+ "_Translation"
116
+ ];
117
+
118
+ export {
119
+ all,
120
+ folderName,
121
+ folderToKind,
122
+ isKindFolder,
123
+ bodySuffixes,
124
+ }
125
+ /* No side effect */
@@ -18,16 +18,7 @@ let buildVariantLookup = (schema: S.t<unknown>): dict<variantKind> => {
18
18
  anyOf->Array.forEach(variantSchema =>
19
19
  switch variantSchema {
20
20
  | Object({items, properties}) =>
21
- let tagName =
22
- items
23
- ->Array.find(item => item.location == "TAG")
24
- ->Option.flatMap(item =>
25
- switch item.schema {
26
- | String({const}) => Some(const)
27
- | _ => None
28
- }
29
- )
30
- switch tagName {
21
+ switch DcbTag.variantTagName(items) {
31
22
  | Some(name) =>
32
23
  let kind = if properties->Dict.keysToArray->Array.length == 0 {
33
24
  PayloadLess
@@ -45,16 +36,7 @@ let buildVariantLookup = (schema: S.t<unknown>): dict<variantKind> => {
45
36
  )
46
37
  | Object({items}) =>
47
38
  // Single-variant schema with fields
48
- let tagName =
49
- items
50
- ->Array.find(item => item.location == "TAG")
51
- ->Option.flatMap(item =>
52
- switch item.schema {
53
- | String({const}) => Some(const)
54
- | _ => None
55
- }
56
- )
57
- switch tagName {
39
+ switch DcbTag.variantTagName(items) {
58
40
  | Some(name) => lookup->Dict.set(name, WithFields)
59
41
  | None => ()
60
42
  }
@@ -120,7 +102,15 @@ let makeDecoder = (schema: S.t<'event>): makeDecoderResult<'event> => {
120
102
  try {
121
103
  Some(JSON.Object(jsonDict)->S.parseJsonOrThrow(schema))
122
104
  } catch {
123
- | _ => None
105
+ | _ =>
106
+ // A parse failure here means a stored event no longer matches the
107
+ // current schema (drift). Dropping it silently hid real data loss;
108
+ // surface it as a warning so it's diagnosable.
109
+ Console.warn(
110
+ "DcbDecode: dropped event `" ++
111
+ eventType ++ "` — payload does not match the current schema (drift?)",
112
+ )
113
+ None
124
114
  }
125
115
  }
126
116
 
@@ -1,8 +1,8 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as S from "sury/src/S.res.mjs";
4
- import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
4
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as DcbTag$Reventless from "./DcbTag.res.mjs";
6
6
 
7
7
  function buildVariantLookup(schema) {
8
8
  let lookup = {};
@@ -14,18 +14,9 @@ function buildVariantLookup(schema) {
14
14
  }
15
15
  break;
16
16
  case "object" :
17
- let tagName = Stdlib_Option.flatMap(schema.items.find(item => item.location === "TAG"), item => {
18
- let match = item.schema;
19
- if (match.type !== "string") {
20
- return;
21
- }
22
- let $$const = match.const;
23
- if ($$const !== undefined) {
24
- return $$const;
25
- }
26
- });
27
- if (tagName !== undefined) {
28
- lookup[tagName] = "WithFields";
17
+ let name$1 = DcbTag$Reventless.variantTagName(schema.items);
18
+ if (name$1 !== undefined) {
19
+ lookup[name$1] = "WithFields";
29
20
  }
30
21
  break;
31
22
  case "union" :
@@ -40,21 +31,12 @@ function buildVariantLookup(schema) {
40
31
  return;
41
32
  }
42
33
  case "object" :
43
- let tagName = Stdlib_Option.flatMap(variantSchema.items.find(item => item.location === "TAG"), item => {
44
- let match = item.schema;
45
- if (match.type !== "string") {
46
- return;
47
- }
48
- let $$const = match.const;
49
- if ($$const !== undefined) {
50
- return $$const;
51
- }
52
- });
53
- if (tagName === undefined) {
34
+ let name$1 = DcbTag$Reventless.variantTagName(variantSchema.items);
35
+ if (name$1 === undefined) {
54
36
  return;
55
37
  }
56
38
  let kind = Object.keys(variantSchema.properties).length === 0 ? "PayloadLess" : "WithFields";
57
- lookup[tagName] = kind;
39
+ lookup[name$1] = kind;
58
40
  return;
59
41
  default:
60
42
  return;
@@ -100,6 +82,7 @@ function makeDecoder(schema) {
100
82
  try {
101
83
  return Primitive_option.some(S.parseJsonOrThrow(jsonDict, schema));
102
84
  } catch (exn) {
85
+ console.warn("DcbDecode: dropped event `" + eventType + "` — payload does not match the current schema (drift?)");
103
86
  return;
104
87
  }
105
88
  };
@@ -331,6 +331,22 @@ let extractTagsFromProperties = (properties: dict<S.t<unknown>>, jsonDict: dict<
331
331
  }
332
332
  )
333
333
 
334
+ // Extract the discriminating TAG constructor name from a variant Object schema's
335
+ // `items`. sury compiles a record-payload variant `Foo({...})` to an Object whose
336
+ // `items` carry one entry at location "TAG" holding a `String` const = "Foo".
337
+ // Returns None for a non-tagged object (no TAG item, or a non-const schema).
338
+ // Single source for the ~dozen previously-inlined identical extractions across
339
+ // this file, DcbDecode and DcbValidation.
340
+ let variantTagName = (items: array<S.item>): option<string> =>
341
+ items
342
+ ->Array.find(item => item.location == "TAG")
343
+ ->Option.flatMap(item =>
344
+ switch item.schema {
345
+ | String({const}) => Some(const)
346
+ | _ => None
347
+ }
348
+ )
349
+
334
350
  /**
335
351
  Extracts DCB tags from an event JSON value using the event's sury schema.
336
352
 
@@ -354,15 +370,7 @@ let extractTagsFromJson = (schema: S.t<unknown>, json: JSON.t): array<tag> =>
354
370
  } else {
355
371
  switch variantSchema {
356
372
  | Object({items, properties}) =>
357
- let variantTag = items
358
- ->Array.find(item => item.location == "TAG")
359
- ->Option.flatMap(item =>
360
- switch item.schema {
361
- | String({const}) => Some(const)
362
- | _ => None
363
- }
364
- )
365
- if variantTag == jsonTag {
373
+ if variantTagName(items) == jsonTag {
366
374
  extractTagsFromProperties(properties, jsonDict)
367
375
  } else {
368
376
  []
@@ -417,15 +425,7 @@ let extractVariantNames = (schema: S.t<'a>): array<string> => {
417
425
  | Union({anyOf}) =>
418
426
  anyOf->Array.filterMap(variantSchema =>
419
427
  switch variantSchema {
420
- | Object({items}) =>
421
- items
422
- ->Array.find(item => item.location == "TAG")
423
- ->Option.flatMap(item =>
424
- switch item.schema {
425
- | String({const}) => Some(const)
426
- | _ => None
427
- }
428
- )
428
+ | Object({items}) => variantTagName(items)
429
429
  // Payload-less variants (sury-compiled `S.literal("Name")` strings) are
430
430
  // intentionally excluded here: DCB event-type lookups can't WHERE-clause
431
431
  // on bare-string events, so Plugin_Structure.consumedEventTypes and
@@ -437,16 +437,7 @@ let extractVariantNames = (schema: S.t<'a>): array<string> => {
437
437
  | _ => None
438
438
  }
439
439
  )
440
- | Object({items}) =>
441
- items
442
- ->Array.find(item => item.location == "TAG")
443
- ->Option.flatMap(item =>
444
- switch item.schema {
445
- | String({const}) => Some([const])
446
- | _ => None
447
- }
448
- )
449
- ->Option.getOr([])
440
+ | Object({items}) => variantTagName(items)->Option.mapOr([], t => [t])
450
441
  | _ => []
451
442
  }
452
443
  }
@@ -466,29 +457,12 @@ let extractAllVariantNames = (schema: S.t<'a>): array<string> => {
466
457
  | Union({anyOf}) =>
467
458
  anyOf->Array.filterMap(variantSchema =>
468
459
  switch variantSchema {
469
- | Object({items}) =>
470
- items
471
- ->Array.find(item => item.location == "TAG")
472
- ->Option.flatMap(item =>
473
- switch item.schema {
474
- | String({const}) => Some(const)
475
- | _ => None
476
- }
477
- )
460
+ | Object({items}) => variantTagName(items)
478
461
  | String({const}) => Some(const)
479
462
  | _ => None
480
463
  }
481
464
  )
482
- | Object({items}) =>
483
- items
484
- ->Array.find(item => item.location == "TAG")
485
- ->Option.flatMap(item =>
486
- switch item.schema {
487
- | String({const}) => Some([const])
488
- | _ => None
489
- }
490
- )
491
- ->Option.getOr([])
465
+ | Object({items}) => variantTagName(items)->Option.mapOr([], t => [t])
492
466
  | String({const: ?Some(name)}) => [name]
493
467
  | _ => []
494
468
  }
@@ -514,29 +488,11 @@ let isVariantPayloadBearing = (schema: S.t<'a>, name: string): bool => {
514
488
  | Union({anyOf}) =>
515
489
  anyOf->Array.some(variantSchema =>
516
490
  switch variantSchema {
517
- | Object({items}) =>
518
- items
519
- ->Array.find(item => item.location == "TAG")
520
- ->Option.flatMap(item =>
521
- switch item.schema {
522
- | String({const}) => Some(const == name)
523
- | _ => None
524
- }
525
- )
526
- ->Option.getOr(false)
491
+ | Object({items}) => variantTagName(items)->Option.mapOr(false, t => t == name)
527
492
  | _ => false
528
493
  }
529
494
  )
530
- | Object({items}) =>
531
- items
532
- ->Array.find(item => item.location == "TAG")
533
- ->Option.flatMap(item =>
534
- switch item.schema {
535
- | String({const}) => Some(const == name)
536
- | _ => None
537
- }
538
- )
539
- ->Option.getOr(false)
495
+ | Object({items}) => variantTagName(items)->Option.mapOr(false, t => t == name)
540
496
  | _ => false
541
497
  }
542
498
  }
@@ -602,15 +558,7 @@ let extractTagsFromJsonExpanded = (schema: S.t<unknown>, json: JSON.t): array<ta
602
558
  } else {
603
559
  switch variantSchema {
604
560
  | Object({items, properties}) =>
605
- let variantTag = items
606
- ->Array.find(item => item.location == "TAG")
607
- ->Option.flatMap(item =>
608
- switch item.schema {
609
- | String({const}) => Some(const)
610
- | _ => None
611
- }
612
- )
613
- if variantTag == jsonTag {
561
+ if variantTagName(items) == jsonTag {
614
562
  extractTagsFromPropertiesExpanded(properties, jsonDict)
615
563
  } else {
616
564
  []
@@ -703,15 +651,9 @@ let extractTagKeysByEventType = (schema: S.t<'a>): dict<array<string>> => {
703
651
  let addVariant = (variantSchema: S.t<unknown>) =>
704
652
  switch variantSchema {
705
653
  | Object({items, properties}) =>
706
- items
707
- ->Array.find(item => item.location == "TAG")
708
- ->Option.flatMap(item =>
709
- switch item.schema {
710
- | String({const}) => Some(const)
711
- | _ => None
712
- }
654
+ variantTagName(items)->Option.forEach(eventType =>
655
+ result->Dict.set(eventType, tagKeysOfProperties(properties))
713
656
  )
714
- ->Option.forEach(eventType => result->Dict.set(eventType, tagKeysOfProperties(properties)))
715
657
  | _ => ()
716
658
  }
717
659
  switch schema->toUnknownSchema {
@@ -957,15 +899,10 @@ let eventShapesOfSchema = (schema: S.t<'a>): array<DcbScopeInference.eventShape>
957
899
  let ofVariant = (variantSchema: S.t<unknown>): option<DcbScopeInference.eventShape> =>
958
900
  switch variantSchema {
959
901
  | Object({items, properties}) =>
960
- items
961
- ->Array.find(item => item.location == "TAG")
962
- ->Option.flatMap(item =>
963
- switch item.schema {
964
- | String({const}) => Some(const)
965
- | _ => None
966
- }
967
- )
968
- ->Option.map(eventType => {DcbScopeInference.eventType, idFields: idFieldsOfProperties(properties)})
902
+ variantTagName(items)->Option.map(eventType => {
903
+ DcbScopeInference.eventType,
904
+ idFields: idFieldsOfProperties(properties),
905
+ })
969
906
  | String({const}) => Some({DcbScopeInference.eventType: const, idFields: []})
970
907
  | _ => None
971
908
  }
@@ -1091,17 +1028,7 @@ let findMultiTagVariantNames = (schema: S.t<unknown>): array<string> => {
1091
1028
  ->Array.filter(((_, fieldSchema)) => isTagged(fieldSchema))
1092
1029
  ->Array.length
1093
1030
  if tagCount > 1 {
1094
- Some(
1095
- items
1096
- ->Array.find(item => item.location == "TAG")
1097
- ->Option.flatMap(item =>
1098
- switch item.schema {
1099
- | String({const}) => Some(const)
1100
- | _ => None
1101
- }
1102
- )
1103
- ->Option.getOr("(unknown)"),
1104
- )
1031
+ Some(variantTagName(items)->Option.getOr("(unknown)"))
1105
1032
  } else {
1106
1033
  None
1107
1034
  }