@reventlessdev/reventless-spec 3.0.0-alpha.144 → 3.0.0-alpha.145

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,13 @@
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.145 (2026-09-24)
7
+
8
+ ### Features
9
+
10
+ * **dcb:** a command reference no decision reads by stays out of the query ([3ae149a](https://github.com/ReventlessDev/reventless-core/commit/3ae149ae91ebab63413be57df5827c5fd4390160))
11
+
12
+
6
13
  # 3.0.0-alpha.144 (2026-09-23)
7
14
 
8
15
  * feat(node)!: every command-line tool reads its arguments through parseArgs ([2e41979](https://github.com/ReventlessDev/reventless-core/commit/2e41979b73646c46356e197e17eb4a3da0154c73))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.144",
3
+ "version": "3.0.0-alpha.145",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -10,7 +10,7 @@ a tag-metadata flag. That is precisely what we are replacing — so both the run
10
10
  VS Code tooling (building shapes from parsed `.res` source) can feed the same
11
11
  `infer`. See `docs/plans/done/dcb-tag-scope-inference.md` § "Phase 1 design".
12
12
 
13
- The three rules (over the representation):
13
+ The four rules (over the representation):
14
14
 
15
15
  1. **Owner / partition.** A slice's partition key is the key its *own* emitted
16
16
  events are identified by — computed as `producedKeys(S)` minus the keys `S`
@@ -33,6 +33,12 @@ The three rules (over the representation):
33
33
  producing slice's own partition key. Foreign reference keys (e.g. `categoryId`
34
34
  on `ProductAdded`) are payload ⇒ not indexed ⇒ the sibling-leak GSI write never
35
35
  happens.
36
+
37
+ 4. **Command scope.** A scalar key on a *command* is a query tag iff it is the
38
+ slice's partition or a key the slice reads off a foreign event (rule 2's
39
+ candidates). `ShipOrder({orderId, carrierId})` names the carrier as information:
40
+ AND-ing it into the clause would hide every order event that does not carry it,
41
+ so it is payload. See `commandPayloadKeys`.
36
42
  */
37
43
  /** A field the slice graph treats as an entity identity.
38
44
 
@@ -55,6 +61,9 @@ type idField = {
55
61
  byTag?: bool,
56
62
  /** The tag key, when the field's type says it (an identity) rather than its name. */
57
63
  key?: string,
64
+ /** The author asked for the tag (`@dcbTag`, `@partitionTag`, `@crossPartition`,
65
+ `@compositePartitionTag`), so a command keeps it in its query (rule 4). */
66
+ declared?: bool,
58
67
  }
59
68
 
60
69
  /** One variant arm: its constructor name and the `*Id` fields it carries. */
@@ -397,6 +406,35 @@ let crossPartitionForSlice = (s: sliceShape): array<string> => {
397
406
  foreign->Array.filter(k => Some(k) != partition && scalar->Array.includes(k))
398
407
  }
399
408
 
409
+ /**
410
+ Rule 4: the scalar command keys that stay out of the slice's decision query.
411
+
412
+ A key stays in when it is the partition, when the slice reads it off a foreign
413
+ event, when the boundary reads it across partitions, when the author declared it
414
+ on the command or on one of the slice's own arms (an M:N capacity read's
415
+ `@crossPartition` sits on the event), or when the command also carries it as a
416
+ list. Anything else only names another thing, and the fence is the partition.
417
+ */
418
+ let commandPayloadKeys = (s: sliceShape, ~partition: string, ~crossPartition: array<string>): array<
419
+ string,
420
+ > => {
421
+ let foreign = foreignConsumedKeys(s)
422
+ let declared = (f: idField) => f.declared->Option.getOr(false)
423
+ let pinned =
424
+ s.command
425
+ ->Array.filter(f => f.isList || declared(f))
426
+ ->Array.concat(
427
+ Array.concat(s.consumed, s.produced)->Array.flatMap(e => e.idFields->Array.filter(declared)),
428
+ )
429
+ ->Array.map(tagKeyOf)
430
+ commandScalarKeys(s)->Array.filter(k =>
431
+ k != partition &&
432
+ !(foreign->Array.includes(k)) &&
433
+ !(crossPartition->Array.includes(k)) &&
434
+ !(pinned->Array.includes(k))
435
+ )
436
+ }
437
+
400
438
  let infer = (slices: array<sliceShape>): derived => {
401
439
  let {partitionBySlice, ambiguities} = resolvePartitions(slices)
402
440
 
@@ -258,6 +258,25 @@ function crossPartitionForSlice(s) {
258
258
  });
259
259
  }
260
260
 
261
+ function commandPayloadKeys(s, partition, crossPartition) {
262
+ let foreign = foreignConsumedKeys(s);
263
+ let declared = f => Stdlib_Option.getOr(f.declared, false);
264
+ let pinned = s.command.filter(f => {
265
+ if (f.isList) {
266
+ return true;
267
+ } else {
268
+ return Stdlib_Option.getOr(f.declared, false);
269
+ }
270
+ }).concat(s.consumed.concat(s.produced).flatMap(e => e.idFields.filter(declared))).map(tagKeyOf);
271
+ return commandScalarKeys(s).filter(k => {
272
+ if (k !== partition && !foreign.includes(k) && !crossPartition.includes(k)) {
273
+ return !pinned.includes(k);
274
+ } else {
275
+ return false;
276
+ }
277
+ });
278
+ }
279
+
261
280
  function infer(slices) {
262
281
  let match = resolvePartitions(slices);
263
282
  let partitionBySlice = match.partitionBySlice;
@@ -332,6 +351,7 @@ export {
332
351
  resolvePartitions,
333
352
  partitionBlockers,
334
353
  crossPartitionForSlice,
354
+ commandPayloadKeys,
335
355
  infer,
336
356
  }
337
357
  /* No side effect */
@@ -126,6 +126,17 @@ let dcbTagKeyOverrideId: S.Metadata.Id.t<string> = S.Metadata.Id.make(
126
126
  ~name="tagKeyOverride",
127
127
  )
128
128
 
129
+ /**
130
+ Internal sury metadata ID marking a tag the author wrote (`@dcbTag`) rather than
131
+ one the naming convention or the field's identity implied. A declared command tag
132
+ always stays in the decision query; an implied one the slice does not decide by
133
+ is payload (`DcbScopeInference.commandPayloadKeys`).
134
+ */
135
+ let dcbDeclaredTagId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(
136
+ ~namespace="dcb",
137
+ ~name="declaredTag",
138
+ )
139
+
129
140
  /**
130
141
  Type-preserving forms of the markers below, for a field whose schema is not a
131
142
  bare `S.string` — an identity (`OrderId.schema->DcbTag.mark`) keeps its type and
@@ -143,6 +154,12 @@ let markPartition = (schema: S.t<'a>): S.t<'a> =>
143
154
  let markCrossPartition = (schema: S.t<'a>): S.t<'a> =>
144
155
  schema->mark->S.Metadata.set(~id=dcbCrossPartitionId, true)
145
156
 
157
+ let markDeclared = (schema: S.t<'a>): S.t<'a> =>
158
+ schema->mark->S.Metadata.set(~id=dcbDeclaredTagId, true)
159
+
160
+ let markDeclaredForKey = (schema: S.t<'a>, ~key: string): S.t<'a> =>
161
+ schema->markForKey(~key)->S.Metadata.set(~id=dcbDeclaredTagId, true)
162
+
146
163
  /**
147
164
  A sury string schema annotated as a DCB tag field.
148
165
 
@@ -190,6 +207,11 @@ annotations carrying a string payload.
190
207
  */
191
208
  let stringForKey = (~key: string): S.t<string> => S.string->markForKey(~key)
192
209
 
210
+ /** What `@dcbTag` emits: `string` / `stringForKey`, marked as written by the author. */
211
+ let declared: S.t<string> = S.string->markDeclared
212
+
213
+ let declaredForKey = (~key: string): S.t<string> => S.string->markDeclaredForKey(~key)
214
+
193
215
  /**
194
216
  A sury int schema annotated as a DCB tag field.
195
217
  Use with `@s.matches(DcbTag.int)` on integer fields that should be extracted as tags.
@@ -359,6 +381,14 @@ let isPartitionTag = (fieldSchema: S.t<unknown>) =>
359
381
  let isCrossPartitionTag = (fieldSchema: S.t<unknown>) =>
360
382
  S.Metadata.get(fieldSchema, ~id=dcbCrossPartitionId)->Option.isSome
361
383
 
384
+ /** Returns `true` if the author asked for the tag: `@dcbTag`, `@partitionTag`,
385
+ `@crossPartition` or `@compositePartitionTag`. */
386
+ let isDeclaredTag = (fieldSchema: S.t<unknown>) =>
387
+ S.Metadata.get(fieldSchema, ~id=dcbDeclaredTagId)->Option.isSome ||
388
+ isPartitionTag(fieldSchema) ||
389
+ isCrossPartitionTag(fieldSchema) ||
390
+ S.Metadata.get(fieldSchema, ~id=dcbCompositePartitionMemberId)->Option.isSome
391
+
362
392
  /**
363
393
  Returns `true` if the schema is an array whose item schema is a cross-partition
364
394
  DCB tag (`array<@s.matches(DcbTag.crossPartition) string>`).
@@ -926,6 +956,10 @@ pairing matches nothing, so results are unchanged. A type that carries the tag
926
956
  as a *secondary* tag is retained (a legitimate cross-partition read), and a type
927
957
  absent from the map is kept (cannot be proven vacuous).
928
958
 
959
+ `~payloadTagKeys` (`commandPayloadTagKeys`) are command keys the slice does not
960
+ decide by; they are left out of every clause, so they narrow neither the read nor
961
+ the append condition.
962
+
929
963
  @example
930
964
  ```rescript
931
965
  // Single-entity command → single AND clause
@@ -953,20 +987,23 @@ let buildQueryFromCommand = (
953
987
  ~value: 'a,
954
988
  ~tagKeysByEventType: dict<array<string>>=Dict.make(),
955
989
  ~crossPartitionTagKeys: array<string>=[],
990
+ ~payloadTagKeys: array<string>=[],
956
991
  ): query => {
957
992
  let typesForTags = clauseTags =>
958
993
  narrowEventTypesForTags(eventTypes, clauseTags, tagKeysByEventType)
994
+ let decidedBy = (tags: array<tag>) =>
995
+ tags->Array.filter(tag => !(payloadTagKeys->Array.includes(tag.key)))
959
996
  if hasTaggedArrayFields(schema) {
960
997
  // Array fields already fan out per element into single-tag clauses, so a
961
998
  // cross-partition array tag is already its own clause (the adapter routes it
962
999
  // by `crossPartitionTagKeys`).
963
- let tags = extractTagsExpanded(schema, value)
1000
+ let tags = extractTagsExpanded(schema, value)->decidedBy
964
1001
  tags->Array.map(tag => {
965
1002
  let clauseTags = [{key: tag.key, value: tag.value}]
966
1003
  {eventTypes: typesForTags(clauseTags), tags: clauseTags}
967
1004
  })
968
1005
  } else {
969
- let tags = extractTags(schema, value)
1006
+ let tags = extractTags(schema, value)->decidedBy
970
1007
  // When any command tag is cross-partition, fan every scalar tag out into its
971
1008
  // own single-tag clause instead of AND-ing them into one composite
972
1009
  // (exact-pair) clause. An M:N command (`SubscribeStudent({courseId, studentId})`)
@@ -1108,11 +1145,18 @@ let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInfer
1108
1145
  } else {
1109
1146
  None
1110
1147
  }
1148
+ let isDeclared = (fieldSchema: S.t<unknown>, isList) => {
1149
+ let valueSchema = switch (isList, fieldSchema) {
1150
+ | (true, Array({additionalItems: Schema(item)})) => item
1151
+ | _ => fieldSchema
1152
+ }
1153
+ valueSchema->Semantic.unwrapOptional->Option.getOr(valueSchema)->isDeclaredTag
1154
+ }
1111
1155
  let identity = (name, fieldSchema, isList) =>
1112
1156
  switch typedKey(fieldSchema, isList) {
1113
1157
  | Some(key) => Some({DcbScopeInference.name, isList, key})
1114
1158
  | None => untypedIdentity(name, fieldSchema, isList)
1115
- }
1159
+ }->Option.map(f => isDeclared(fieldSchema, isList) ? {...f, declared: true} : f)
1116
1160
  properties
1117
1161
  ->Dict.toArray
1118
1162
  ->Array.flatMap(((name, fieldSchema)) => {
@@ -1337,6 +1381,27 @@ let deriveEffectiveScope = (slices: array<sliceSchemas>): effectiveScope => {
1337
1381
  }
1338
1382
  }
1339
1383
 
1384
+ /**
1385
+ The command tag keys a slice's decision query leaves out (rule 4 of
1386
+ `DcbScopeInference`), given the slice's partition within its boundary and the
1387
+ boundary's cross-partition keys. Empty unless the partition is a single key: a
1388
+ composite boundary keeps every tag it has.
1389
+ */
1390
+ let commandPayloadTagKeys = (
1391
+ shape: DcbScopeInference.sliceShape,
1392
+ ~partitionTag: option<derivedPartitionTag>,
1393
+ ~crossPartitionTagKeys: array<string>,
1394
+ ): array<string> =>
1395
+ switch partitionTag {
1396
+ | Some(Simple({key})) =>
1397
+ DcbScopeInference.commandPayloadKeys(
1398
+ shape,
1399
+ ~partition=key,
1400
+ ~crossPartition=crossPartitionTagKeys,
1401
+ )
1402
+ | _ => []
1403
+ }
1404
+
1340
1405
  // --- Composite partition key helpers ---
1341
1406
 
1342
1407
  type compositePartitionFieldInfo = {name: string, position: int, sep: string}
@@ -32,6 +32,8 @@ let dcbCompositePartitionMemberId = Sury.$Metadata_Id_make("dcb", "compositePart
32
32
 
33
33
  let dcbTagKeyOverrideId = Sury.$Metadata_Id_make("dcb", "tagKeyOverride");
34
34
 
35
+ let dcbDeclaredTagId = Sury.$Metadata_Id_make("dcb", "declaredTag");
36
+
35
37
  function mark(schema) {
36
38
  return Sury.$Metadata_set(schema, dcbTagId, true);
37
39
  }
@@ -48,12 +50,26 @@ function markCrossPartition(schema) {
48
50
  return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbCrossPartitionId, true);
49
51
  }
50
52
 
53
+ function markDeclared(schema) {
54
+ return Sury.$Metadata_set(Sury.$Metadata_set(schema, dcbTagId, true), dcbDeclaredTagId, true);
55
+ }
56
+
57
+ function markDeclaredForKey(schema, key) {
58
+ return Sury.$Metadata_set(markForKey(schema, key), dcbDeclaredTagId, true);
59
+ }
60
+
51
61
  let string = Sury.$Metadata_set(Sury.string, dcbTagId, true);
52
62
 
53
63
  function stringForKey(key) {
54
64
  return markForKey(Sury.string, key);
55
65
  }
56
66
 
67
+ let declared = markDeclared(Sury.string);
68
+
69
+ function declaredForKey(key) {
70
+ return markDeclaredForKey(Sury.string, key);
71
+ }
72
+
57
73
  let int = Sury.$Metadata_set(Sury.int32, dcbTagId, true);
58
74
 
59
75
  let partition = markPartition(Sury.string);
@@ -172,6 +188,14 @@ function isCrossPartitionTag(fieldSchema) {
172
188
  return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCrossPartitionId));
173
189
  }
174
190
 
191
+ function isDeclaredTag(fieldSchema) {
192
+ if (Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbDeclaredTagId)) || Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbPartitionTagId)) || Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCrossPartitionId))) {
193
+ return true;
194
+ } else {
195
+ return Stdlib_Option.isSome(Sury.$Metadata_get(fieldSchema, dcbCompositePartitionMemberId));
196
+ }
197
+ }
198
+
175
199
  function isCrossPartitionTaggedArray(fieldSchema) {
176
200
  if (fieldSchema.type !== "array") {
177
201
  return false;
@@ -610,11 +634,13 @@ function narrowEventTypesForTags(eventTypes, tags, tagKeysByEventType) {
610
634
  });
611
635
  }
612
636
 
613
- function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt) {
637
+ function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, payloadTagKeysOpt) {
614
638
  let tagKeysByEventType = tagKeysByEventTypeOpt !== undefined ? tagKeysByEventTypeOpt : ({});
615
639
  let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
640
+ let payloadTagKeys = payloadTagKeysOpt !== undefined ? payloadTagKeysOpt : [];
641
+ let decidedBy = tags => tags.filter(tag => !payloadTagKeys.includes(tag.key));
616
642
  if (hasTaggedArrayFields(schema)) {
617
- let tags = extractTagsExpanded(schema, value);
643
+ let tags = decidedBy(extractTagsExpanded(schema, value));
618
644
  return tags.map(tag => {
619
645
  let clauseTags = [{
620
646
  key: tag.key,
@@ -626,7 +652,7 @@ function buildQueryFromCommand(eventTypes, schema, value, tagKeysByEventTypeOpt,
626
652
  };
627
653
  });
628
654
  }
629
- let tags$1 = extractTags(schema, value);
655
+ let tags$1 = decidedBy(extractTags(schema, value));
630
656
  let hasCrossPartition = tags$1.some(tag => crossPartitionTagKeys.includes(tag.key));
631
657
  if (hasCrossPartition && tags$1.length > 1) {
632
658
  return tags$1.map(tag => {
@@ -735,15 +761,8 @@ function idFieldsOfProperties(properties) {
735
761
  }
736
762
  return Stdlib_Option.map(Semantic$Reventless.identityKey(valueSchema), param => resolveTagKey("", valueSchema));
737
763
  };
738
- let identity = (name, fieldSchema, isList) => {
739
- let key = typedKey(fieldSchema, isList);
740
- if (key !== undefined) {
741
- return {
742
- name: name,
743
- isList: isList,
744
- key: key
745
- };
746
- } else if (isIdName(name)) {
764
+ let untypedIdentity = (name, fieldSchema, isList) => {
765
+ if (isIdName(name)) {
747
766
  return {
748
767
  name: name,
749
768
  isList: isList
@@ -758,6 +777,31 @@ function idFieldsOfProperties(properties) {
758
777
  return;
759
778
  }
760
779
  };
780
+ let isDeclared = (fieldSchema, isList) => {
781
+ let valueSchema;
782
+ if (isList && fieldSchema.type === "array") {
783
+ let item = fieldSchema.additionalItems;
784
+ valueSchema = item === "strip" || item === "strict" ? fieldSchema : item;
785
+ } else {
786
+ valueSchema = fieldSchema;
787
+ }
788
+ return isDeclaredTag(Stdlib_Option.getOr(Semantic$Reventless.unwrapOptional(valueSchema), valueSchema));
789
+ };
790
+ let identity = (name, fieldSchema, isList) => {
791
+ let key = typedKey(fieldSchema, isList);
792
+ return Stdlib_Option.map(key !== undefined ? ({
793
+ name: name,
794
+ isList: isList,
795
+ key: key
796
+ }) : untypedIdentity(name, fieldSchema, isList), f => {
797
+ if (!isDeclared(fieldSchema, isList)) {
798
+ return f;
799
+ }
800
+ let newrecord = {...f};
801
+ newrecord.declared = true;
802
+ return newrecord;
803
+ });
804
+ };
761
805
  return Object.entries(properties).flatMap(param => {
762
806
  let fieldSchema = param[1];
763
807
  let isList;
@@ -895,6 +939,19 @@ function deriveEffectiveScope(slices) {
895
939
  };
896
940
  }
897
941
 
942
+ function commandPayloadTagKeys(shape, partitionTag, crossPartitionTagKeys) {
943
+ if (partitionTag === undefined) {
944
+ return [];
945
+ }
946
+ switch (partitionTag.TAG) {
947
+ case "Simple" :
948
+ return DcbScopeInference$Reventless.commandPayloadKeys(shape, partitionTag._0.key, crossPartitionTagKeys);
949
+ case "Composite" :
950
+ case "ByEventType" :
951
+ return [];
952
+ }
953
+ }
954
+
898
955
  function extractCompositePartitionFieldsFromProperties(properties) {
899
956
  return Stdlib_Array.filterMap(Object.entries(properties), param => {
900
957
  let meta = Sury.$Metadata_get(param[1], dcbCompositePartitionMemberId);
@@ -1084,12 +1141,17 @@ export {
1084
1141
  dcbCrossPartitionId,
1085
1142
  dcbCompositePartitionMemberId,
1086
1143
  dcbTagKeyOverrideId,
1144
+ dcbDeclaredTagId,
1087
1145
  mark,
1088
1146
  markForKey,
1089
1147
  markPartition,
1090
1148
  markCrossPartition,
1149
+ markDeclared,
1150
+ markDeclaredForKey,
1091
1151
  string,
1092
1152
  stringForKey,
1153
+ declared,
1154
+ declaredForKey,
1093
1155
  int,
1094
1156
  partition,
1095
1157
  crossPartition,
@@ -1105,6 +1167,7 @@ export {
1105
1167
  hasNestedTags,
1106
1168
  isPartitionTag,
1107
1169
  isCrossPartitionTag,
1170
+ isDeclaredTag,
1108
1171
  isCrossPartitionTaggedArray,
1109
1172
  resolveTagKey,
1110
1173
  resolveArrayTagKey,
@@ -1140,6 +1203,7 @@ export {
1140
1203
  sliceShapeFromSchemas,
1141
1204
  sliceShape,
1142
1205
  deriveEffectiveScope,
1206
+ commandPayloadTagKeys,
1143
1207
  extractCompositePartitionFieldsFromProperties,
1144
1208
  extractCompositePartitionFields,
1145
1209
  getCompositePartitionKeyValue,
@@ -170,20 +170,24 @@ exact-match trap. Returns one `validationError` (warning) per offending
170
170
 
171
171
  @param slices `(sliceName, commandSchema, consumedEventSchema)` per consumer.
172
172
  @param producedTagKeys event type → its produced tag-key set (from the event-log schema).
173
+ @param payloadTagKeysBySlice slice → the command keys its query leaves out
174
+ (`DcbTag.commandPayloadTagKeys`), which take no part in a composite read.
173
175
  */
174
176
  let validateCompositeReads = (
175
177
  ~slices: array<(string, S.t<unknown>, S.t<unknown>)>,
176
178
  ~producedTagKeys: dict<array<string>>,
179
+ ~payloadTagKeysBySlice: dict<array<string>>=Dict.make(),
177
180
  ): array<validationError> => {
178
181
  let warnings: array<validationError> = []
179
182
  slices->Array.forEach(((sliceName, commandSchema, consumedSchema)) =>
180
183
  if !DcbTag.hasTaggedArrayFields(commandSchema) {
181
184
  let commandTagKeysByVariant = DcbTag.extractTagKeysByEventType(commandSchema)
182
185
  let consumedTypes = DcbTag.extractVariantNames(consumedSchema)
186
+ let payload = payloadTagKeysBySlice->Dict.get(sliceName)->Option.getOr([])
183
187
  commandTagKeysByVariant
184
188
  ->Dict.valuesToArray
185
189
  ->Array.forEach(cmdKeys => {
186
- let querySet = dedupeKeys(cmdKeys)
190
+ let querySet = dedupeKeys(cmdKeys)->Array.filter(k => !(payload->Array.includes(k)))
187
191
  if querySet->Array.length >= 2 {
188
192
  consumedTypes->Array.forEach(
189
193
  consumedType =>
@@ -564,3 +568,40 @@ let validatePartitionHintsVsInference = (
564
568
  )
565
569
  {contradictions, redundancies}
566
570
  }
571
+
572
+ /**
573
+ Finds `@noDcbTag` on command fields whose key the slice's query leaves out anyway
574
+ (rule 4 of `DcbScopeInference`). Such a reference is payload without the
575
+ annotation, so the annotation can go.
576
+
577
+ The PPX consumes the attribute, so the caller reads the suppressed fields off the
578
+ source. A field the shape does not know (a name that is no identity) is skipped.
579
+
580
+ @param suppressedBySlice slice → the command fields written `@noDcbTag`.
581
+ @param payloadTagKeysBySlice slice → `DcbTag.commandPayloadTagKeys`.
582
+ */
583
+ let validateCommandTagSuppressions = (
584
+ ~shapes: array<DcbScopeInference.sliceShape>,
585
+ ~suppressedBySlice: dict<array<string>>,
586
+ ~payloadTagKeysBySlice: dict<array<string>>,
587
+ ): array<validationError> =>
588
+ shapes->Array.flatMap(s => {
589
+ let payload = payloadTagKeysBySlice->Dict.get(s.sliceName)->Option.getOr([])
590
+ suppressedBySlice
591
+ ->Dict.get(s.sliceName)
592
+ ->Option.getOr([])
593
+ ->Array.filterMap(field =>
594
+ s.command
595
+ ->Array.find(f => f.name == field)
596
+ ->Option.map(DcbScopeInference.tagKeyOf)
597
+ ->Option.flatMap(
598
+ key =>
599
+ payload->Array.includes(key)
600
+ ? Some({
601
+ sliceName: s.sliceName,
602
+ message: `@noDcbTag ${field} is what inference derives without it — ${s.sliceName} decides nothing by ${key}, so it stays out of the query anyway. The annotation is redundant and can be removed.`,
603
+ })
604
+ : None,
605
+ )
606
+ )
607
+ })
@@ -191,7 +191,8 @@ function dedupeKeys(keys) {
191
191
  });
192
192
  }
193
193
 
194
- function validateCompositeReads(slices, producedTagKeys) {
194
+ function validateCompositeReads(slices, producedTagKeys, payloadTagKeysBySliceOpt) {
195
+ let payloadTagKeysBySlice = payloadTagKeysBySliceOpt !== undefined ? payloadTagKeysBySliceOpt : ({});
195
196
  let warnings = [];
196
197
  slices.forEach(param => {
197
198
  let commandSchema = param[1];
@@ -201,8 +202,9 @@ function validateCompositeReads(slices, producedTagKeys) {
201
202
  }
202
203
  let commandTagKeysByVariant = DcbTag$Reventless.extractTagKeysByEventType(commandSchema);
203
204
  let consumedTypes = DcbTag$Reventless.extractVariantNames(param[2]);
205
+ let payload = Stdlib_Option.getOr(payloadTagKeysBySlice[sliceName], []);
204
206
  Object.values(commandTagKeysByVariant).forEach(cmdKeys => {
205
- let querySet = dedupeKeys(cmdKeys);
207
+ let querySet = dedupeKeys(cmdKeys).filter(k => !payload.includes(k));
206
208
  if (querySet.length >= 2) {
207
209
  consumedTypes.forEach(consumedType => {
208
210
  let producedKeys = producedTagKeys[consumedType];
@@ -491,6 +493,20 @@ function validatePartitionHintsVsInference(shapes) {
491
493
  };
492
494
  }
493
495
 
496
+ function validateCommandTagSuppressions(shapes, suppressedBySlice, payloadTagKeysBySlice) {
497
+ return shapes.flatMap(s => {
498
+ let payload = Stdlib_Option.getOr(payloadTagKeysBySlice[s.sliceName], []);
499
+ return Stdlib_Array.filterMap(Stdlib_Option.getOr(suppressedBySlice[s.sliceName], []), field => Stdlib_Option.flatMap(Stdlib_Option.map(s.command.find(f => f.name === field), DcbScopeInference$Reventless.tagKeyOf), key => {
500
+ if (payload.includes(key)) {
501
+ return {
502
+ sliceName: s.sliceName,
503
+ message: `@noDcbTag ` + field + ` is what inference derives without it — ` + s.sliceName + ` decides nothing by ` + key + `, so it stays out of the query anyway. The annotation is redundant and can be removed.`
504
+ };
505
+ }
506
+ }));
507
+ });
508
+ }
509
+
494
510
  export {
495
511
  extractVariantInfo,
496
512
  extractAllVariants,
@@ -505,5 +521,6 @@ export {
505
521
  validateCrossPartitionScope,
506
522
  validateScopeVsInference,
507
523
  validatePartitionHintsVsInference,
524
+ validateCommandTagSuppressions,
508
525
  }
509
526
  /* DcbTag-Reventless Not a pure module */