@reventlessdev/reventless-spec 3.0.0-alpha.145 → 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,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.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
|
+
|
|
13
|
+
# 3.0.0-alpha.146 (2026-09-24)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **dcb:** a command reference no decision reads by stays out of the query ([3ae149a](https://github.com/ReventlessDev/reventless-core/commit/3ae149ae91ebab63413be57df5827c5fd4390160))
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
# 3.0.0-alpha.145 (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,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
|
|
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 (
|
|
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
|
|