@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
|
@@ -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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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 (
|
|
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
|
|