@reventlessdev/reventless-spec 3.0.0-alpha.146 → 3.0.0-alpha.148

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,20 @@
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.148 (2026-09-25)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **spec:** a chapter that carries both keys backs neither ([cbd823e](https://github.com/ReventlessDev/reventless-core/commit/cbd823e4f87ddd5810debd929bb536b2003d0e4c))
11
+
12
+
13
+ # 3.0.0-alpha.147 (2026-09-25)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **spec,core:** a partition tag its chapter backs overrides inference ([4244a4a](https://github.com/ReventlessDev/reventless-core/commit/4244a4af9e7aad4378d9b6276ddeb44878e125b2))
18
+
19
+
6
20
  # 3.0.0-alpha.146 (2026-09-24)
7
21
 
8
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.146",
3
+ "version": "3.0.0-alpha.148",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -211,33 +211,43 @@ Applying the give-back everywhere would be wrong: `AddProduct` reads
211
211
  `CategoryAdded({categoryId})` from a slice partitioned by `categoryId`, which
212
212
  would hand `categoryId` back and leave it two candidates.
213
213
  */
214
- let resolvePartitions = (slices: array<sliceShape>): partitionResolution => {
215
- let producersByEventType: dict<array<sliceShape>> = Dict.make()
216
- slices->Array.forEach(s =>
217
- s.produced->Array.forEach(e => {
218
- let prev = producersByEventType->Dict.get(e.eventType)->Option.getOr([])
219
- if !(prev->Array.some(p => p.sliceName == s.sliceName)) {
220
- producersByEventType->Dict.set(e.eventType, prev->Array.concat([s]))
221
- }
222
- })
223
- )
224
- // chapter -> the keys every id-carrying event written in that chapter carries.
225
- let chapterKeys = Dict.make()
214
+ /**
215
+ Chapter -> the keys every id-carrying event written in that chapter carries: what the
216
+ chapter says its slices are about. Rule 1's tie-break reads it, and so does the check
217
+ that decides whether a `@partitionTag` inference disagrees with is the author's
218
+ mistake or inference's (`DcbValidation.validatePartitionHintsVsInference`).
219
+ */
220
+ let chapterKeys = (slices: array<sliceShape>): dict<array<string>> => {
221
+ let byChapter = Dict.make()
226
222
  slices->Array.forEach(s =>
227
223
  s.chapter->Option.forEach(chapter => {
228
224
  let events = s.produced->Array.filter(e => e.idFields->Array.length > 0)
229
225
  events->Array.forEach(
230
226
  e => {
231
227
  let keys = e->keysOfEvent
232
- let shared = switch chapterKeys->Dict.get(chapter) {
228
+ let shared = switch byChapter->Dict.get(chapter) {
233
229
  | Some(prev) => prev->Array.filter(k => keys->Array.includes(k))
234
230
  | None => dedupSorted(keys)
235
231
  }
236
- chapterKeys->Dict.set(chapter, shared)
232
+ byChapter->Dict.set(chapter, shared)
237
233
  },
238
234
  )
239
235
  })
240
236
  )
237
+ byChapter
238
+ }
239
+
240
+ let resolvePartitions = (slices: array<sliceShape>): partitionResolution => {
241
+ let producersByEventType: dict<array<sliceShape>> = Dict.make()
242
+ slices->Array.forEach(s =>
243
+ s.produced->Array.forEach(e => {
244
+ let prev = producersByEventType->Dict.get(e.eventType)->Option.getOr([])
245
+ if !(prev->Array.some(p => p.sliceName == s.sliceName)) {
246
+ producersByEventType->Dict.set(e.eventType, prev->Array.concat([s]))
247
+ }
248
+ })
249
+ )
250
+ let chapterKeys = chapterKeys(slices)
241
251
  let chapterKeysOf = (s: sliceShape) => s.chapter->Option.flatMap(c => chapterKeys->Dict.get(c))
242
252
  let byChapter = (s: sliceShape, candidates) =>
243
253
  switch s->chapterKeysOf {
@@ -58,6 +58,20 @@ function sameAssignment(a, b) {
58
58
  return Primitive_object.equal(entries(a), entries(b));
59
59
  }
60
60
 
61
+ function chapterKeys(slices) {
62
+ let byChapter = {};
63
+ slices.forEach(s => Stdlib_Option.forEach(s.chapter, chapter => {
64
+ let events = s.produced.filter(e => e.idFields.length !== 0);
65
+ events.forEach(e => {
66
+ let keys = e.idFields.map(tagKeyOf);
67
+ let prev = byChapter[chapter];
68
+ let shared = prev !== undefined ? prev.filter(k => keys.includes(k)) : dedupSorted(keys);
69
+ byChapter[chapter] = shared;
70
+ });
71
+ }));
72
+ return byChapter;
73
+ }
74
+
61
75
  function resolvePartitions(slices) {
62
76
  let producersByEventType = {};
63
77
  slices.forEach(s => {
@@ -69,17 +83,8 @@ function resolvePartitions(slices) {
69
83
  }
70
84
  });
71
85
  });
72
- let chapterKeys = {};
73
- slices.forEach(s => Stdlib_Option.forEach(s.chapter, chapter => {
74
- let events = s.produced.filter(e => e.idFields.length !== 0);
75
- events.forEach(e => {
76
- let keys = e.idFields.map(tagKeyOf);
77
- let prev = chapterKeys[chapter];
78
- let shared = prev !== undefined ? prev.filter(k => keys.includes(k)) : dedupSorted(keys);
79
- chapterKeys[chapter] = shared;
80
- });
81
- }));
82
- let chapterKeysOf = s => Stdlib_Option.flatMap(s.chapter, c => chapterKeys[c]);
86
+ let chapterKeys$1 = chapterKeys(slices);
87
+ let chapterKeysOf = s => Stdlib_Option.flatMap(s.chapter, c => chapterKeys$1[c]);
83
88
  let byChapter = (s, candidates) => {
84
89
  let keys = chapterKeysOf(s);
85
90
  if (keys === undefined) {
@@ -348,6 +353,7 @@ export {
348
353
  commandScalarKeys,
349
354
  foreignConsumedKeys,
350
355
  sameAssignment,
356
+ chapterKeys,
351
357
  resolvePartitions,
352
358
  partitionBlockers,
353
359
  crossPartitionForSlice,
@@ -440,12 +440,15 @@ let validateCrossPartitionScope = (~producers: array<(string, S.t<unknown>)>): a
440
440
  warnings
441
441
  }
442
442
 
443
- /** Two-bucket result of validating annotations against the inferred scope. */
443
+ /** Result of validating annotations against the inferred scope. */
444
444
  type scopeInferenceIssues = {
445
445
  /** Annotations that conflict with inference — likely bugs (warn/error). */
446
446
  contradictions: array<validationError>,
447
447
  /** Annotations inference already derives — safe to delete (info). */
448
448
  redundancies: array<validationError>,
449
+ /** Annotations that decide against inference where the slice's chapter backs them —
450
+ kept, and reported so the decision stays visible (info). */
451
+ overrides: array<validationError>,
449
452
  }
450
453
 
451
454
  /**
@@ -483,7 +486,7 @@ let validateScopeVsInference = (
483
486
  }
484
487
  )
485
488
  )
486
- {contradictions, redundancies}
489
+ {contradictions, redundancies, overrides: []}
487
490
  }
488
491
 
489
492
  /**
@@ -493,6 +496,15 @@ Checks each `@partitionTag` against what inference derives with that hint remove
493
496
  hint names a key the slice only reads as a reference to another entity. Storage,
494
497
  fence and read scope would all follow the wrong key.
495
498
  - **Redundant** — inference reaches the same key unaided; the annotation can go.
499
+ - **Override** — inference disagrees, but the slice's chapter backs the hint: every
500
+ id-carrying event written in the chapter carries the hinted key and not every one
501
+ carries the key inference chose, and every foreign
502
+ event the slice reads by that key is written by a slice partitioned by it. Rule 1
503
+ cannot tell an own key read back off a sibling's events from a reference
504
+ (`ShipOrder` writing `{orderId, carrierBookingId}` and reading `OrderPlaced` has
505
+ `AddProduct`'s shape exactly), and the hint is how the author says which. The
506
+ chapter is what separates the two: every event under `Order/` carries `orderId`,
507
+ while a `@partitionTag categoryId` on `AddProduct` disagrees with `Product/`.
496
508
 
497
509
  A hint that names a key the slice's events do not carry is not checked here:
498
510
  inference ignores it, and the boundary derivation reports the slice instead.
@@ -502,6 +514,45 @@ let validatePartitionHintsVsInference = (
502
514
  ): scopeInferenceIssues => {
503
515
  let contradictions: array<validationError> = []
504
516
  let redundancies: array<validationError> = []
517
+ let overrides: array<validationError> = []
518
+ let chapterKeys = DcbScopeInference.chapterKeys(shapes)
519
+ let producersOf = eventType =>
520
+ shapes->Array.filter(p => p.produced->Array.some(e => e.eventType == eventType))
521
+ // The chapter backs the hint, and what the slice reads by that key is its own
522
+ // entity: each foreign arm carrying it comes from a slice partitioned by it.
523
+ // Backing means telling the keys apart: a chapter whose events all carry both the
524
+ // hinted key and inference's (one slice, one event with both ids) says nothing
525
+ // about which one the slice is about, so it backs neither.
526
+ let backedByChapter = (s: DcbScopeInference.sliceShape, hint, inferred, partitionBySlice) => {
527
+ let chapterAgrees =
528
+ s.chapter
529
+ ->Option.flatMap(c => chapterKeys->Dict.get(c))
530
+ ->Option.mapOr(false, keys =>
531
+ keys->Array.includes(hint) && !(inferred->Array.some(k => keys->Array.includes(k)))
532
+ )
533
+ let own = s.produced->Array.map(e => e.eventType)
534
+ let readsOwnEntity =
535
+ s.consumed
536
+ ->Array.filter(e =>
537
+ !(own->Array.includes(e.eventType)) &&
538
+ e->DcbScopeInference.keysOfEvent->Array.includes(hint)
539
+ )
540
+ ->Array.every(e =>
541
+ switch producersOf(e.eventType) {
542
+ | [] => false
543
+ | producers =>
544
+ producers->Array.every(p => partitionBySlice->Dict.get(p.sliceName) == Some(hint))
545
+ }
546
+ )
547
+ chapterAgrees && readsOwnEntity
548
+ }
549
+ let overridden = (s: DcbScopeInference.sliceShape, hint, inference) =>
550
+ overrides->Array.push({
551
+ sliceName: s.sliceName,
552
+ message: `@partitionTag ${hint} decides the partition against inference (${inference}): every event in chapter ${s.chapter->Option.getOr(
553
+ "",
554
+ )} carries ${hint}, and what the slice reads by it is written by slices partitioned by ${hint}.`,
555
+ })
505
556
  shapes->Array.forEach(s =>
506
557
  switch s.partitionHint {
507
558
  | Some(hint) if DcbScopeInference.producedKeys(s)->Array.includes(hint) =>
@@ -546,6 +597,8 @@ let validatePartitionHintsVsInference = (
546
597
  sliceName: s.sliceName,
547
598
  message: `@partitionTag ${hint} is what inference derives without it — the annotation is redundant and can be removed.`,
548
599
  })
600
+ | Some(inferred) if backedByChapter(s, hint, [inferred], unaided.partitionBySlice) =>
601
+ overridden(s, hint, `it derives ${inferred}`)
549
602
  | Some(inferred) =>
550
603
  contradictions->Array.push({
551
604
  sliceName: s.sliceName,
@@ -553,7 +606,11 @@ let validatePartitionHintsVsInference = (
553
606
  })
554
607
  | None =>
555
608
  let candidates = unaided.candidatesBySlice->Dict.get(s.sliceName)->Option.getOr([])
556
- if !(candidates->Array.includes(hint)) {
609
+ if candidates->Array.includes(hint) {
610
+ ()
611
+ } else if backedByChapter(s, hint, candidates, unaided.partitionBySlice) {
612
+ overridden(s, hint, `candidates: ${candidates->Array.join(", ")}`)
613
+ } else {
557
614
  contradictions->Array.push({
558
615
  sliceName: s.sliceName,
559
616
  message: `@partitionTag names ${hint}, which this slice only reads as a reference to another entity (candidates: ${candidates->Array.join(
@@ -566,7 +623,7 @@ let validatePartitionHintsVsInference = (
566
623
  | _ => ()
567
624
  }
568
625
  )
569
- {contradictions, redundancies}
626
+ {contradictions, redundancies, overrides}
570
627
  }
571
628
 
572
629
  /**
@@ -424,13 +424,52 @@ function validateScopeVsInference(annotations, inferred) {
424
424
  });
425
425
  return {
426
426
  contradictions: contradictions,
427
- redundancies: redundancies
427
+ redundancies: redundancies,
428
+ overrides: []
428
429
  };
429
430
  }
430
431
 
431
432
  function validatePartitionHintsVsInference(shapes) {
432
433
  let contradictions = [];
433
434
  let redundancies = [];
435
+ let overrides = [];
436
+ let chapterKeys = DcbScopeInference$Reventless.chapterKeys(shapes);
437
+ let producersOf = eventType => shapes.filter(p => p.produced.some(e => e.eventType === eventType));
438
+ let backedByChapter = (s, hint, inferred, partitionBySlice) => {
439
+ let chapterAgrees = Stdlib_Option.mapOr(Stdlib_Option.flatMap(s.chapter, c => chapterKeys[c]), false, keys => {
440
+ if (keys.includes(hint)) {
441
+ return !inferred.some(k => keys.includes(k));
442
+ } else {
443
+ return false;
444
+ }
445
+ });
446
+ let own = s.produced.map(e => e.eventType);
447
+ let readsOwnEntity = s.consumed.filter(e => {
448
+ if (own.includes(e.eventType)) {
449
+ return false;
450
+ } else {
451
+ return DcbScopeInference$Reventless.keysOfEvent(e).includes(hint);
452
+ }
453
+ }).every(e => {
454
+ let producers = producersOf(e.eventType);
455
+ if (producers.length !== 0) {
456
+ return producers.every(p => Primitive_object.equal(partitionBySlice[p.sliceName], hint));
457
+ } else {
458
+ return false;
459
+ }
460
+ });
461
+ if (chapterAgrees) {
462
+ return readsOwnEntity;
463
+ } else {
464
+ return false;
465
+ }
466
+ };
467
+ let overridden = (s, hint, inference) => {
468
+ overrides.push({
469
+ sliceName: s.sliceName,
470
+ message: `@partitionTag ` + hint + ` decides the partition against inference (` + inference + `): every event in chapter ` + Stdlib_Option.getOr(s.chapter, "") + ` carries ` + hint + `, and what the slice reads by it is written by slices partitioned by ` + hint + `.`
471
+ });
472
+ };
434
473
  shapes.forEach(s => {
435
474
  let hint = s.partitionHint;
436
475
  if (hint === undefined) {
@@ -470,16 +509,23 @@ function validatePartitionHintsVsInference(shapes) {
470
509
  sliceName: s.sliceName,
471
510
  message: `@partitionTag ` + hint + ` is what inference derives without it — the annotation is redundant and can be removed.`
472
511
  });
512
+ return;
513
+ } else if (backedByChapter(s, hint, [inferred], unaided.partitionBySlice)) {
514
+ return overridden(s, hint, `it derives ` + inferred);
473
515
  } else {
474
516
  contradictions.push({
475
517
  sliceName: s.sliceName,
476
518
  message: `@partitionTag names ` + hint + `, but inference derives ` + inferred + ` from the slice graph — ` + hint + ` is read from another entity. Remove the annotation, or move it to ` + inferred + `.`
477
519
  });
520
+ return;
478
521
  }
479
- return;
480
522
  }
481
523
  let candidates = Stdlib_Option.getOr(unaided.candidatesBySlice[s.sliceName], []);
482
- if (!candidates.includes(hint)) {
524
+ if (candidates.includes(hint)) {
525
+ return;
526
+ } else if (backedByChapter(s, hint, candidates, unaided.partitionBySlice)) {
527
+ return overridden(s, hint, `candidates: ` + candidates.join(", "));
528
+ } else {
483
529
  contradictions.push({
484
530
  sliceName: s.sliceName,
485
531
  message: `@partitionTag names ` + hint + `, which this slice only reads as a reference to another entity (candidates: ` + candidates.join(", ") + `). Move the annotation to the slice's own key.`
@@ -489,7 +535,8 @@ function validatePartitionHintsVsInference(shapes) {
489
535
  });
490
536
  return {
491
537
  contradictions: contradictions,
492
- redundancies: redundancies
538
+ redundancies: redundancies,
539
+ overrides: overrides
493
540
  };
494
541
  }
495
542