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

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.147 (2026-09-25)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **spec,core:** a partition tag its chapter backs overrides inference ([4244a4a](https://github.com/ReventlessDev/reventless-core/commit/4244a4af9e7aad4378d9b6276ddeb44878e125b2))
11
+
12
+
6
13
  # 3.0.0-alpha.146 (2026-09-24)
7
14
 
8
15
  ### 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.147",
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,14 @@ 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 every foreign
501
+ event the slice reads by that key is written by a slice partitioned by it. Rule 1
502
+ cannot tell an own key read back off a sibling's events from a reference
503
+ (`ShipOrder` writing `{orderId, carrierBookingId}` and reading `OrderPlaced` has
504
+ `AddProduct`'s shape exactly), and the hint is how the author says which. The
505
+ chapter is what separates the two: every event under `Order/` carries `orderId`,
506
+ while a `@partitionTag categoryId` on `AddProduct` disagrees with `Product/`.
496
507
 
497
508
  A hint that names a key the slice's events do not carry is not checked here:
498
509
  inference ignores it, and the boundary derivation reports the slice instead.
@@ -502,6 +513,40 @@ let validatePartitionHintsVsInference = (
502
513
  ): scopeInferenceIssues => {
503
514
  let contradictions: array<validationError> = []
504
515
  let redundancies: array<validationError> = []
516
+ let overrides: array<validationError> = []
517
+ let chapterKeys = DcbScopeInference.chapterKeys(shapes)
518
+ let producersOf = eventType =>
519
+ shapes->Array.filter(p => p.produced->Array.some(e => e.eventType == eventType))
520
+ // The chapter backs the hint, and what the slice reads by that key is its own
521
+ // entity: each foreign arm carrying it comes from a slice partitioned by it.
522
+ let backedByChapter = (s: DcbScopeInference.sliceShape, hint, partitionBySlice) => {
523
+ let chapterAgrees =
524
+ s.chapter
525
+ ->Option.flatMap(c => chapterKeys->Dict.get(c))
526
+ ->Option.mapOr(false, keys => keys->Array.includes(hint))
527
+ let own = s.produced->Array.map(e => e.eventType)
528
+ let readsOwnEntity =
529
+ s.consumed
530
+ ->Array.filter(e =>
531
+ !(own->Array.includes(e.eventType)) &&
532
+ e->DcbScopeInference.keysOfEvent->Array.includes(hint)
533
+ )
534
+ ->Array.every(e =>
535
+ switch producersOf(e.eventType) {
536
+ | [] => false
537
+ | producers =>
538
+ producers->Array.every(p => partitionBySlice->Dict.get(p.sliceName) == Some(hint))
539
+ }
540
+ )
541
+ chapterAgrees && readsOwnEntity
542
+ }
543
+ let overridden = (s: DcbScopeInference.sliceShape, hint, inference) =>
544
+ overrides->Array.push({
545
+ sliceName: s.sliceName,
546
+ message: `@partitionTag ${hint} decides the partition against inference (${inference}): every event in chapter ${s.chapter->Option.getOr(
547
+ "",
548
+ )} carries ${hint}, and what the slice reads by it is written by slices partitioned by ${hint}.`,
549
+ })
505
550
  shapes->Array.forEach(s =>
506
551
  switch s.partitionHint {
507
552
  | Some(hint) if DcbScopeInference.producedKeys(s)->Array.includes(hint) =>
@@ -546,6 +591,8 @@ let validatePartitionHintsVsInference = (
546
591
  sliceName: s.sliceName,
547
592
  message: `@partitionTag ${hint} is what inference derives without it — the annotation is redundant and can be removed.`,
548
593
  })
594
+ | Some(inferred) if backedByChapter(s, hint, unaided.partitionBySlice) =>
595
+ overridden(s, hint, `it derives ${inferred}`)
549
596
  | Some(inferred) =>
550
597
  contradictions->Array.push({
551
598
  sliceName: s.sliceName,
@@ -553,7 +600,11 @@ let validatePartitionHintsVsInference = (
553
600
  })
554
601
  | None =>
555
602
  let candidates = unaided.candidatesBySlice->Dict.get(s.sliceName)->Option.getOr([])
556
- if !(candidates->Array.includes(hint)) {
603
+ if candidates->Array.includes(hint) {
604
+ ()
605
+ } else if backedByChapter(s, hint, unaided.partitionBySlice) {
606
+ overridden(s, hint, `candidates: ${candidates->Array.join(", ")}`)
607
+ } else {
557
608
  contradictions->Array.push({
558
609
  sliceName: s.sliceName,
559
610
  message: `@partitionTag names ${hint}, which this slice only reads as a reference to another entity (candidates: ${candidates->Array.join(
@@ -566,7 +617,7 @@ let validatePartitionHintsVsInference = (
566
617
  | _ => ()
567
618
  }
568
619
  )
569
- {contradictions, redundancies}
620
+ {contradictions, redundancies, overrides}
570
621
  }
571
622
 
572
623
  /**
@@ -424,13 +424,46 @@ 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, partitionBySlice) => {
439
+ let chapterAgrees = Stdlib_Option.mapOr(Stdlib_Option.flatMap(s.chapter, c => chapterKeys[c]), false, keys => keys.includes(hint));
440
+ let own = s.produced.map(e => e.eventType);
441
+ let readsOwnEntity = s.consumed.filter(e => {
442
+ if (own.includes(e.eventType)) {
443
+ return false;
444
+ } else {
445
+ return DcbScopeInference$Reventless.keysOfEvent(e).includes(hint);
446
+ }
447
+ }).every(e => {
448
+ let producers = producersOf(e.eventType);
449
+ if (producers.length !== 0) {
450
+ return producers.every(p => Primitive_object.equal(partitionBySlice[p.sliceName], hint));
451
+ } else {
452
+ return false;
453
+ }
454
+ });
455
+ if (chapterAgrees) {
456
+ return readsOwnEntity;
457
+ } else {
458
+ return false;
459
+ }
460
+ };
461
+ let overridden = (s, hint, inference) => {
462
+ overrides.push({
463
+ sliceName: s.sliceName,
464
+ 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 + `.`
465
+ });
466
+ };
434
467
  shapes.forEach(s => {
435
468
  let hint = s.partitionHint;
436
469
  if (hint === undefined) {
@@ -470,16 +503,23 @@ function validatePartitionHintsVsInference(shapes) {
470
503
  sliceName: s.sliceName,
471
504
  message: `@partitionTag ` + hint + ` is what inference derives without it — the annotation is redundant and can be removed.`
472
505
  });
506
+ return;
507
+ } else if (backedByChapter(s, hint, unaided.partitionBySlice)) {
508
+ return overridden(s, hint, `it derives ` + inferred);
473
509
  } else {
474
510
  contradictions.push({
475
511
  sliceName: s.sliceName,
476
512
  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
513
  });
514
+ return;
478
515
  }
479
- return;
480
516
  }
481
517
  let candidates = Stdlib_Option.getOr(unaided.candidatesBySlice[s.sliceName], []);
482
- if (!candidates.includes(hint)) {
518
+ if (candidates.includes(hint)) {
519
+ return;
520
+ } else if (backedByChapter(s, hint, unaided.partitionBySlice)) {
521
+ return overridden(s, hint, `candidates: ` + candidates.join(", "));
522
+ } else {
483
523
  contradictions.push({
484
524
  sliceName: s.sliceName,
485
525
  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 +529,8 @@ function validatePartitionHintsVsInference(shapes) {
489
529
  });
490
530
  return {
491
531
  contradictions: contradictions,
492
- redundancies: redundancies
532
+ redundancies: redundancies,
533
+ overrides: overrides
493
534
  };
494
535
  }
495
536