@reventlessdev/reventless-spec 3.0.0-alpha.61
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 +625 -0
- package/LICENSE +202 -0
- package/README.md +114 -0
- package/package.json +34 -0
- package/rescript.json +25 -0
- package/run-generator.mjs +2 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +168 -0
- package/src/LogPrefix.res.mjs +148 -0
- package/src/PackageVersion.res +73 -0
- package/src/PackageVersion.res.mjs +81 -0
- package/src/components/Aggregate.res +64 -0
- package/src/components/Aggregate.res.mjs +2 -0
- package/src/components/AutomationSlice.res +279 -0
- package/src/components/AutomationSlice.res.mjs +30 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +128 -0
- package/src/components/DcbDecode.res.mjs +117 -0
- package/src/components/DcbTag.res +1256 -0
- package/src/components/DcbTag.res.mjs +869 -0
- package/src/components/DcbValidation.res +362 -0
- package/src/components/DcbValidation.res.mjs +360 -0
- package/src/components/DisplayName.res +40 -0
- package/src/components/DisplayName.res.mjs +26 -0
- package/src/components/ExtensionPoint.res +27 -0
- package/src/components/ExtensionPoint.res.mjs +2 -0
- package/src/components/InboundTranslationSlice.res +79 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +108 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +373 -0
- package/src/components/Plugin.res.mjs +249 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +51 -0
- package/src/components/Reference.res.mjs +37 -0
- package/src/components/StateAnnotations.res +65 -0
- package/src/components/StateAnnotations.res.mjs +15 -0
- package/src/components/StateChangeSlice.res +131 -0
- package/src/components/StateChangeSlice.res.mjs +2 -0
- package/src/components/StateViewSlice.res +99 -0
- package/src/components/StateViewSlice.res.mjs +2 -0
- package/src/components/Task.res +62 -0
- package/src/components/Task.res.mjs +2 -0
- package/src/generator/Codegen.res +703 -0
- package/src/generator/Codegen.res.mjs +485 -0
- package/src/generator/Config.res +85 -0
- package/src/generator/Config.res.mjs +78 -0
- package/src/generator/Discovery.res +204 -0
- package/src/generator/Discovery.res.mjs +213 -0
- package/src/generator/Generator_Node.res +49 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +386 -0
- package/src/generator/Pairing.res.mjs +353 -0
- package/src/generator/PluginGenerator.res +53 -0
- package/src/generator/PluginGenerator.res.mjs +70 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +78 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/EventMapping.res +100 -0
- package/src/types/EventMapping.res.mjs +15 -0
- package/src/types/Handler.res +30 -0
- package/src/types/Handler.res.mjs +2 -0
- package/src/types/Id.res +75 -0
- package/src/types/Id.res.mjs +37 -0
- package/src/types/Identity.res +46 -0
- package/src/types/Identity.res.mjs +51 -0
- package/src/types/Message.res +197 -0
- package/src/types/Message.res.mjs +108 -0
- package/src/types/Projection.res +220 -0
- package/src/types/Projection.res.mjs +44 -0
- package/src/types/QueryEngine.res +123 -0
- package/src/types/QueryEngine.res.mjs +12 -0
- package/src/types/ReadConsistency.res +38 -0
- package/src/types/ReadConsistency.res.mjs +29 -0
- package/src/types/Schedule.res +65 -0
- package/src/types/Schedule.res.mjs +68 -0
- package/src/types/SideEffect.res +45 -0
- package/src/types/SideEffect.res.mjs +2 -0
- package/src/types/StoredEvent.res +46 -0
- package/src/types/StoredEvent.res.mjs +32 -0
- package/src/types/Visibility.res +24 -0
- package/src/types/Visibility.res.mjs +25 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
type validationError = {
|
|
2
|
+
sliceName: string,
|
|
3
|
+
message: string,
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
type variantInfo = {
|
|
7
|
+
tagName: string,
|
|
8
|
+
fields: dict<S.t<unknown>>,
|
|
9
|
+
taggedFields: array<string>,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Extract variant info (TAG name, fields, tagged fields) from a single variant schema
|
|
13
|
+
let extractVariantInfo = (variantSchema: S.t<unknown>): option<variantInfo> =>
|
|
14
|
+
switch variantSchema {
|
|
15
|
+
| Object({items, properties}) =>
|
|
16
|
+
let tagName =
|
|
17
|
+
items
|
|
18
|
+
->Array.find(item => item.location == "TAG")
|
|
19
|
+
->Option.flatMap(item =>
|
|
20
|
+
switch item.schema {
|
|
21
|
+
| String({const}) => Some(const)
|
|
22
|
+
| _ => None
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
tagName->Option.map(tagName => {
|
|
26
|
+
let taggedFields =
|
|
27
|
+
properties
|
|
28
|
+
->Dict.toArray
|
|
29
|
+
->Array.filterMap(((fieldName, fieldSchema)) =>
|
|
30
|
+
if DcbTag.isTagged(fieldSchema) {
|
|
31
|
+
Some(fieldName)
|
|
32
|
+
} else {
|
|
33
|
+
None
|
|
34
|
+
}
|
|
35
|
+
)
|
|
36
|
+
{tagName, fields: properties, taggedFields}
|
|
37
|
+
})
|
|
38
|
+
| _ => None
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Extract all variant infos from a schema (handles Union and single Object)
|
|
42
|
+
let extractAllVariants = (schema: S.t<unknown>): array<variantInfo> =>
|
|
43
|
+
switch schema {
|
|
44
|
+
| Union({anyOf}) => anyOf->Array.filterMap(extractVariantInfo)
|
|
45
|
+
| Object(_) =>
|
|
46
|
+
switch extractVariantInfo(schema) {
|
|
47
|
+
| Some(info) => [info]
|
|
48
|
+
| None => []
|
|
49
|
+
}
|
|
50
|
+
| String({const: ?Some(name)}) =>
|
|
51
|
+
// Single payload-less variant — sury compiles `type t = Foo` to String({const: "Foo"})
|
|
52
|
+
[{tagName: name, fields: Dict.make(), taggedFields: []}]
|
|
53
|
+
| _ => []
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Check if a variant has no payload fields (only TAG)
|
|
57
|
+
let isPayloadLess = (info: variantInfo): bool => info.fields->Dict.toArray->Array.length == 0
|
|
58
|
+
|
|
59
|
+
// Compare two sury schema types for structural compatibility
|
|
60
|
+
let rec schemasAreCompatible = (a: S.t<unknown>, b: S.t<unknown>): bool =>
|
|
61
|
+
switch (a, b) {
|
|
62
|
+
| (String(_), String(_)) => true
|
|
63
|
+
| (Number(_), Number(_)) => true
|
|
64
|
+
| (Boolean(_), Boolean(_)) => true
|
|
65
|
+
| (BigInt(_), BigInt(_)) => true
|
|
66
|
+
| (Array({additionalItems: Schema(aItem)}), Array({additionalItems: Schema(bItem)})) =>
|
|
67
|
+
schemasAreCompatible(aItem, bItem)
|
|
68
|
+
| (Union(_), Union(_)) => true
|
|
69
|
+
| (Object(_), Object(_)) => true
|
|
70
|
+
| _ => false
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Get a human-readable type name for a schema
|
|
74
|
+
let schemaTypeName = (schema: S.t<unknown>): string =>
|
|
75
|
+
switch schema {
|
|
76
|
+
| String(_) => "string"
|
|
77
|
+
| Number(_) => "float"
|
|
78
|
+
| Boolean(_) => "bool"
|
|
79
|
+
| BigInt(_) => "bigint"
|
|
80
|
+
| Array(_) => "array"
|
|
81
|
+
| Object(_) => "object"
|
|
82
|
+
| Union(_) => "union"
|
|
83
|
+
| Never(_) => "never"
|
|
84
|
+
| Unknown(_) => "unknown"
|
|
85
|
+
| Ref(_) => "ref"
|
|
86
|
+
| _ => "unknown"
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type producerEntry = {
|
|
90
|
+
sliceName: string,
|
|
91
|
+
variant: variantInfo,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let dedupeKeys = (keys: array<string>): array<string> => {
|
|
95
|
+
let seen = Set.make()
|
|
96
|
+
keys->Array.filter(k =>
|
|
97
|
+
if seen->Set.has(k) {
|
|
98
|
+
false
|
|
99
|
+
} else {
|
|
100
|
+
seen->Set.add(k)
|
|
101
|
+
true
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
Warns when a slice issues a **composite** (multi-tag) decision read for an event
|
|
108
|
+
type whose *produced* tag set is a strict superset of the query's tags.
|
|
109
|
+
|
|
110
|
+
The `tag_composite` GSI key is built from **all** of an event's tags, so a
|
|
111
|
+
composite query matches only events tagged *exactly* its query set. An event that
|
|
112
|
+
also carries an extra tag is therefore silently missed by the composite read,
|
|
113
|
+
while its per-tag fences still move (Issue 5). This commonly bites when a tag is
|
|
114
|
+
later added to a multi-tag event without updating the reading slice.
|
|
115
|
+
|
|
116
|
+
Only all-scalar multi-tag commands build composite reads: a command carrying a
|
|
117
|
+
tagged array reads via per-element OR (single-tag) clauses, which cannot hit the
|
|
118
|
+
exact-match trap. Returns one `validationError` (warning) per offending
|
|
119
|
+
(slice, consumed type) pair.
|
|
120
|
+
|
|
121
|
+
@param slices `(sliceName, commandSchema, consumedEventSchema)` per consumer.
|
|
122
|
+
@param producedTagKeys event type → its produced tag-key set (from the event-log schema).
|
|
123
|
+
*/
|
|
124
|
+
let validateCompositeReads = (
|
|
125
|
+
~slices: array<(string, S.t<unknown>, S.t<unknown>)>,
|
|
126
|
+
~producedTagKeys: dict<array<string>>,
|
|
127
|
+
): array<validationError> => {
|
|
128
|
+
let warnings: array<validationError> = []
|
|
129
|
+
slices->Array.forEach(((sliceName, commandSchema, consumedSchema)) =>
|
|
130
|
+
if !DcbTag.hasTaggedArrayFields(commandSchema) {
|
|
131
|
+
let commandTagKeysByVariant = DcbTag.extractTagKeysByEventType(commandSchema)
|
|
132
|
+
let consumedTypes = DcbTag.extractVariantNames(consumedSchema)
|
|
133
|
+
commandTagKeysByVariant
|
|
134
|
+
->Dict.valuesToArray
|
|
135
|
+
->Array.forEach(cmdKeys => {
|
|
136
|
+
let querySet = dedupeKeys(cmdKeys)
|
|
137
|
+
if querySet->Array.length >= 2 {
|
|
138
|
+
consumedTypes->Array.forEach(consumedType =>
|
|
139
|
+
switch producedTagKeys->Dict.get(consumedType) {
|
|
140
|
+
| None => ()
|
|
141
|
+
| Some(producedKeys) =>
|
|
142
|
+
let producedSet = dedupeKeys(producedKeys)
|
|
143
|
+
let isSuperset = querySet->Array.every(k => producedSet->Array.includes(k))
|
|
144
|
+
if isSuperset && producedSet->Array.length > querySet->Array.length {
|
|
145
|
+
let extra = producedSet->Array.filter(k => !(querySet->Array.includes(k)))
|
|
146
|
+
let _ = warnings->Array.push({
|
|
147
|
+
sliceName,
|
|
148
|
+
message: `composite read on tags [${querySet->Array.join(
|
|
149
|
+
", ",
|
|
150
|
+
)}] will silently miss '${consumedType}', which also carries [${extra->Array.join(
|
|
151
|
+
", ",
|
|
152
|
+
)}] — the tag_composite key includes ALL of an event's tags, so a composite query matches only events tagged exactly [${querySet->Array.join(
|
|
153
|
+
", ",
|
|
154
|
+
)}]. Align the event's tag set with the query, or read '${consumedType}' via a single-tag clause.`,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
warnings
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let validateProducedAndConsumed = (
|
|
167
|
+
~produced: array<(string, S.t<unknown>)>,
|
|
168
|
+
~consumed: array<(string, S.t<unknown>)>,
|
|
169
|
+
): result<unit, array<validationError>> => {
|
|
170
|
+
let errors: array<validationError> = []
|
|
171
|
+
|
|
172
|
+
// Build a map of TAG name -> array of (sliceName, variantInfo) for produced events
|
|
173
|
+
let producerMap: dict<array<producerEntry>> = Dict.make()
|
|
174
|
+
produced->Array.forEach(((sliceName, schema)) => {
|
|
175
|
+
let variants = extractAllVariants(schema)
|
|
176
|
+
variants->Array.forEach(variant => {
|
|
177
|
+
let existing = producerMap->Dict.get(variant.tagName)->Option.getOr([])
|
|
178
|
+
producerMap->Dict.set(variant.tagName, existing->Array.concat([{sliceName, variant}]))
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
|
|
182
|
+
// Rule 1: Payload equivalence across producers
|
|
183
|
+
// All producers of the same TAG must have identical fields, types, and tag annotations
|
|
184
|
+
producerMap->Dict.toArray->Array.forEach(((tagName, entries)) => {
|
|
185
|
+
if entries->Array.length > 1 {
|
|
186
|
+
let first = entries->Array.getUnsafe(0)
|
|
187
|
+
let firstFields = first.variant.fields->Dict.keysToArray->Array.toSorted(String.compare)
|
|
188
|
+
let firstTagged = first.variant.taggedFields->Array.toSorted(String.compare)
|
|
189
|
+
|
|
190
|
+
entries->Array.forEachWithIndex((entry, idx) => {
|
|
191
|
+
if idx > 0 {
|
|
192
|
+
let entryFields = entry.variant.fields->Dict.keysToArray->Array.toSorted(String.compare)
|
|
193
|
+
let entryTagged = entry.variant.taggedFields->Array.toSorted(String.compare)
|
|
194
|
+
|
|
195
|
+
// Check field names match
|
|
196
|
+
if firstFields->Array.length != entryFields->Array.length ||
|
|
197
|
+
firstFields->Array.some(f => !(entryFields->Array.includes(f))) {
|
|
198
|
+
let missingInEntry =
|
|
199
|
+
firstFields->Array.filter(f => !(entryFields->Array.includes(f)))
|
|
200
|
+
let extraInEntry =
|
|
201
|
+
entryFields->Array.filter(f => !(firstFields->Array.includes(f)))
|
|
202
|
+
let parts = []
|
|
203
|
+
if missingInEntry->Array.length > 0 {
|
|
204
|
+
let _ =
|
|
205
|
+
parts->Array.push(
|
|
206
|
+
`'${first.sliceName}' has fields [${missingInEntry->Array.join(", ")}] which '${entry.sliceName}' is missing`,
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
if extraInEntry->Array.length > 0 {
|
|
210
|
+
let _ =
|
|
211
|
+
parts->Array.push(
|
|
212
|
+
`'${entry.sliceName}' has fields [${extraInEntry->Array.join(", ")}] which '${first.sliceName}' is missing`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
let _ =
|
|
216
|
+
errors->Array.push({
|
|
217
|
+
sliceName: entry.sliceName,
|
|
218
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but with different fields: ${parts->Array.join("; ")}`,
|
|
219
|
+
})
|
|
220
|
+
} else {
|
|
221
|
+
// Fields match by name — check types are compatible
|
|
222
|
+
firstFields->Array.forEach(fieldName => {
|
|
223
|
+
let firstSchema = first.variant.fields->Dict.getUnsafe(fieldName)
|
|
224
|
+
let entrySchema = entry.variant.fields->Dict.getUnsafe(fieldName)
|
|
225
|
+
if !schemasAreCompatible(firstSchema, entrySchema) {
|
|
226
|
+
let _ =
|
|
227
|
+
errors->Array.push({
|
|
228
|
+
sliceName: entry.sliceName,
|
|
229
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but field '${fieldName}' has type '${schemaTypeName(firstSchema)}' in '${first.sliceName}' and '${schemaTypeName(entrySchema)}' in '${entry.sliceName}'`,
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check tag annotations match
|
|
236
|
+
if firstTagged->Array.length != entryTagged->Array.length ||
|
|
237
|
+
firstTagged->Array.some(f => !(entryTagged->Array.includes(f))) {
|
|
238
|
+
let untaggedInEntry =
|
|
239
|
+
firstTagged->Array.filter(f => !(entryTagged->Array.includes(f)))
|
|
240
|
+
let untaggedInFirst =
|
|
241
|
+
entryTagged->Array.filter(f => !(firstTagged->Array.includes(f)))
|
|
242
|
+
let parts = []
|
|
243
|
+
if untaggedInEntry->Array.length > 0 {
|
|
244
|
+
let _ =
|
|
245
|
+
parts->Array.push(
|
|
246
|
+
`[${untaggedInEntry->Array.join(", ")}] tagged in '${first.sliceName}' but not in '${entry.sliceName}'`,
|
|
247
|
+
)
|
|
248
|
+
}
|
|
249
|
+
if untaggedInFirst->Array.length > 0 {
|
|
250
|
+
let _ =
|
|
251
|
+
parts->Array.push(
|
|
252
|
+
`[${untaggedInFirst->Array.join(", ")}] tagged in '${entry.sliceName}' but not in '${first.sliceName}'`,
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
let _ =
|
|
256
|
+
errors->Array.push({
|
|
257
|
+
sliceName: entry.sliceName,
|
|
258
|
+
message: `Producers '${first.sliceName}' and '${entry.sliceName}' both produce '${tagName}' but tag annotations differ: ${parts->Array.join("; ")}`,
|
|
259
|
+
})
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
// Rule 2: Every consumed event has a producer
|
|
267
|
+
// Rule 3: Consumed fields exist in produced shape (payload-less always valid)
|
|
268
|
+
// Rule 4: Type compatibility
|
|
269
|
+
consumed->Array.forEach(((sliceName, schema)) => {
|
|
270
|
+
let variants = extractAllVariants(schema)
|
|
271
|
+
variants->Array.forEach(variant => {
|
|
272
|
+
switch producerMap->Dict.get(variant.tagName) {
|
|
273
|
+
| None =>
|
|
274
|
+
let _ =
|
|
275
|
+
errors->Array.push({
|
|
276
|
+
sliceName,
|
|
277
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}' but no slice produces it`,
|
|
278
|
+
})
|
|
279
|
+
| Some(producers) =>
|
|
280
|
+
// Use first producer as the authoritative shape (Rule 1 ensures they're all identical)
|
|
281
|
+
let producer = producers->Array.getUnsafe(0)
|
|
282
|
+
|
|
283
|
+
// Skip field checks for payload-less consumed variants
|
|
284
|
+
if !isPayloadLess(variant) {
|
|
285
|
+
variant.fields->Dict.toArray->Array.forEach(((fieldName, consumedFieldSchema)) => {
|
|
286
|
+
switch producer.variant.fields->Dict.get(fieldName) {
|
|
287
|
+
| None =>
|
|
288
|
+
let _ =
|
|
289
|
+
errors->Array.push({
|
|
290
|
+
sliceName,
|
|
291
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' (${schemaTypeName(consumedFieldSchema)}) but produced '${variant.tagName}' has no field '${fieldName}'`,
|
|
292
|
+
})
|
|
293
|
+
| Some(producedFieldSchema) =>
|
|
294
|
+
if !schemasAreCompatible(consumedFieldSchema, producedFieldSchema) {
|
|
295
|
+
let _ =
|
|
296
|
+
errors->Array.push({
|
|
297
|
+
sliceName,
|
|
298
|
+
message: `Slice '${sliceName}' consumes '${variant.tagName}.${fieldName}' as ${schemaTypeName(consumedFieldSchema)} but producer declares it as ${schemaTypeName(producedFieldSchema)}`,
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
if errors->Array.length > 0 {
|
|
309
|
+
Error(errors)
|
|
310
|
+
} else {
|
|
311
|
+
Ok()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
Validates that DCB tag *scope* (`PartitionScoped` default vs `@crossPartition`)
|
|
317
|
+
agrees across every produced event type that carries a given tag key.
|
|
318
|
+
|
|
319
|
+
The scope is a property of the tag *key*: read routing and — decisively — the
|
|
320
|
+
consistency fence are driven by one global per-key flag (the fence is bumped by
|
|
321
|
+
*writers*, so a key cannot be cross-partition for one producer and
|
|
322
|
+
partition-scoped for another without making the fence ambiguous). This returns
|
|
323
|
+
one warning per producer that carries a key some *other* producer declared
|
|
324
|
+
`@crossPartition` but this one left partition-scoped.
|
|
325
|
+
|
|
326
|
+
@param producers `(sliceName, producedEventSchema)` per producing slice.
|
|
327
|
+
*/
|
|
328
|
+
let validateCrossPartitionScope = (
|
|
329
|
+
~producers: array<(string, S.t<unknown>)>,
|
|
330
|
+
): array<validationError> => {
|
|
331
|
+
let perProducer = producers->Array.map(((name, schema)) => {
|
|
332
|
+
let cpSet = Set.make()
|
|
333
|
+
DcbTag.extractCrossPartitionTagKeys(schema)->Array.forEach(k => cpSet->Set.add(k))
|
|
334
|
+
let carried = Set.make()
|
|
335
|
+
DcbTag.extractTagKeysByEventType(schema)
|
|
336
|
+
->Dict.valuesToArray
|
|
337
|
+
->Array.forEach(ks => ks->Array.forEach(k => carried->Set.add(k)))
|
|
338
|
+
(name, cpSet, carried)
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
let globalCp = Set.make()
|
|
342
|
+
perProducer->Array.forEach(((_, cpSet, _)) =>
|
|
343
|
+
cpSet->Set.values->Array.fromIterator->Array.forEach(k => globalCp->Set.add(k))
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
let warnings: array<validationError> = []
|
|
347
|
+
perProducer->Array.forEach(((name, cpSet, carried)) =>
|
|
348
|
+
carried
|
|
349
|
+
->Set.values
|
|
350
|
+
->Array.fromIterator
|
|
351
|
+
->Array.toSorted((a, b) => String.compare(a, b))
|
|
352
|
+
->Array.forEach(k =>
|
|
353
|
+
if globalCp->Set.has(k) && !(cpSet->Set.has(k)) {
|
|
354
|
+
let _ = warnings->Array.push({
|
|
355
|
+
sliceName: name,
|
|
356
|
+
message: `tag '${k}' is declared @crossPartition by another producer but is partition-scoped here — DCB tag scope must agree across every event type that carries the tag (read-scope and fence-scope follow one global per-key flag). Add @crossPartition to '${k}' here, or remove it from the other producer.`,
|
|
357
|
+
})
|
|
358
|
+
}
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
warnings
|
|
362
|
+
}
|