@reventlessdev/reventless-core 3.0.0-alpha.274 → 3.0.0-alpha.276

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.276 (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.275 (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.274 (2026-09-24)
7
21
 
8
22
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.274",
3
+ "version": "3.0.0-alpha.276",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -30,20 +30,20 @@
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
32
32
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.15",
33
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
34
- "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
35
33
  "@reventlessdev/rescript-node": "2.0.0-alpha.15",
34
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
35
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.15",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.174",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.146",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.176",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.148",
41
41
  "@reventlessdev/reventless-interop": "3.0.0-alpha.38"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.1",
45
45
  "sury-ppx": "11.0.0",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.92"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.94"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.1"
@@ -237,11 +237,14 @@ module Make = (
237
237
 
238
238
  // A `@partitionTag` inference contradicts would steer storage, fence and read
239
239
  // scope to a key the slice only references, so it stops the build. One that
240
- // inference agrees with is noise, and says so.
240
+ // inference agrees with is noise, and says so. One its chapter backs against
241
+ // inference is the author deciding what inference cannot see, and is kept.
241
242
  let hintIssues = Reventless.DcbValidation.validatePartitionHintsVsInference(
242
243
  ~shapes=inferenceShapes,
243
244
  )
244
- hintIssues.redundancies->Array.forEach(e =>
245
+ hintIssues.redundancies
246
+ ->Array.concat(hintIssues.overrides)
247
+ ->Array.forEach(e =>
245
248
  log.info(~comp="Dcb_Builder", `DCB partition (${e.sliceName}): ${e.message}`)
246
249
  )
247
250
  if hintIssues.contradictions->Array.length > 0 {
@@ -178,7 +178,7 @@ function Make(DcbEventLogStorage) {
178
178
  }));
179
179
  let inferenceShapes = sliceSchemas.map(DcbTag$Reventless.sliceShape);
180
180
  let hintIssues = DcbValidation$Reventless.validatePartitionHintsVsInference(inferenceShapes);
181
- hintIssues.redundancies.forEach(e => log.info("Dcb_Builder", undefined, `DCB partition (` + e.sliceName + `): ` + e.message));
181
+ hintIssues.redundancies.concat(hintIssues.overrides).forEach(e => log.info("Dcb_Builder", undefined, `DCB partition (` + e.sliceName + `): ` + e.message));
182
182
  if (hintIssues.contradictions.length !== 0) {
183
183
  let detail = hintIssues.contradictions.map(e => e.sliceName + `: ` + e.message).join(" | ");
184
184
  log.error("Dcb_Builder", undefined, `DCB partition contradiction (` + name + `): ` + detail);
@@ -503,6 +503,105 @@ describe("DcbScopeInference:", () => {
503
503
  expect(issues.redundancies)->toEqual([])
504
504
  },
505
505
  )
506
+
507
+ // `ShipOrder` records the carrier's booking, which nothing else in the boundary
508
+ // writes or reads. Rule 1 subtracts `orderId` (read off `OrderPlaced`) and is
509
+ // left with the booking: `AddProduct`'s shape exactly, with the other answer.
510
+ let placeOrder = slice(
511
+ "PlaceOrder",
512
+ ~chapter="Order",
513
+ ~produced=[ev("OrderPlaced", [scal("orderId")])],
514
+ )
515
+ let shipOrder = slice(
516
+ "ShipOrder",
517
+ ~chapter="Order",
518
+ ~partitionHint="orderId",
519
+ ~command=[scal("orderId"), scal("carrierBookingId")],
520
+ ~consumed=[ev("OrderPlaced", [scal("orderId")]), ev("OrderShipped", [scal("orderId")])],
521
+ ~produced=[ev("OrderShipped", [scal("orderId"), scal("carrierBookingId")])],
522
+ )
523
+
524
+ testSync(
525
+ "a hint its chapter backs overrides inference instead of contradicting it",
526
+ () => {
527
+ let issues = check([placeOrder, shipOrder])
528
+ expect(issues.contradictions)->toEqual([])
529
+ expect(issues.redundancies)->toEqual([])
530
+ expect(issues.overrides->names)->toEqual(["ShipOrder"])
531
+ // and the hint is what the boundary is partitioned by
532
+ expect(
533
+ DSI.resolvePartitions([placeOrder, shipOrder]).partitionBySlice->Dict.get("ShipOrder"),
534
+ )->toEqual(Some("orderId"))
535
+ },
536
+ )
537
+
538
+ testSync(
539
+ "without a chapter the same hint is still a contradiction",
540
+ () => {
541
+ let noChapter = (s: DSI.sliceShape) => {...s, chapter: ?None}
542
+ let issues = check([noChapter(placeOrder), noChapter(shipOrder)])
543
+ expect(issues.contradictions->names)->toEqual(["ShipOrder"])
544
+ expect(issues.overrides)->toEqual([])
545
+ },
546
+ )
547
+
548
+ // Every producer is present, so the arms it reads by `categoryId` all come from
549
+ // category slices: only the chapter can refuse the hint.
550
+ let categories = [
551
+ {...addCategory, chapter: "Category"},
552
+ {...renameCategory, chapter: "Category"},
553
+ {...archiveCategory, chapter: "Category"},
554
+ ]
555
+
556
+ testSync(
557
+ "a hint its chapter disagrees with is still a contradiction",
558
+ () => {
559
+ // `RenameProduct` writes only `productId`, so `Product/` is about products.
560
+ let renameProduct = slice(
561
+ "RenameProduct",
562
+ ~chapter="Product",
563
+ ~consumed=[ev("ProductAdded", [scal("productId")])],
564
+ ~produced=[ev("ProductRenamed", [scal("productId")])],
565
+ )
566
+ let inProduct = {...hinted(addProduct, "categoryId"), chapter: "Product"}
567
+ let issues = check([inProduct, renameProduct, ...categories])
568
+ expect(issues.contradictions->names)->toEqual(["AddProduct"])
569
+ expect(issues.overrides)->toEqual([])
570
+ },
571
+ )
572
+
573
+ testSync(
574
+ "a chapter that carries both keys everywhere backs neither",
575
+ () => {
576
+ // One slice, one event with both ids: the chapter cannot say which one it is about.
577
+ let inProduct = {...hinted(addProduct, "categoryId"), chapter: "Product"}
578
+ let issues = check([inProduct, ...categories])
579
+ expect(issues.contradictions->names)->toEqual(["AddProduct"])
580
+ expect(issues.overrides)->toEqual([])
581
+ },
582
+ )
583
+
584
+ testSync(
585
+ "a chapter-backed hint read off another entity's events is still a contradiction",
586
+ () => {
587
+ // `WarehouseAssigned` carries the order but belongs to the warehouse.
588
+ let assign = slice(
589
+ "AssignWarehouse",
590
+ ~chapter="Warehouse",
591
+ ~partitionHint="warehouseId",
592
+ ~produced=[ev("WarehouseAssigned", [scal("warehouseId"), scal("orderId")])],
593
+ )
594
+ let ship = {
595
+ ...shipOrder,
596
+ consumed: shipOrder.consumed->Array.concat([
597
+ ev("WarehouseAssigned", [scal("warehouseId"), scal("orderId")]),
598
+ ]),
599
+ }
600
+ let issues = check([placeOrder, ship, assign])
601
+ expect(issues.overrides->names)->toEqual([])
602
+ expect(issues.contradictions->names)->toEqual(["ShipOrder"])
603
+ },
604
+ )
506
605
  })
507
606
 
508
607
  describe("ambiguity surfacing", () => {
@@ -1,5 +1,6 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
+ import * as Belt_Array from "@rescript/runtime/lib/es6/Belt_Array.js";
3
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
5
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
5
6
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
@@ -814,6 +815,156 @@ globalThis.describe("DcbScopeInference:", () => {
814
815
  globalThis.expect(issues.contradictions).toEqual([]);
815
816
  globalThis.expect(issues.redundancies).toEqual([]);
816
817
  });
818
+ let placeOrder = slice("PlaceOrder", undefined, undefined, [{
819
+ eventType: "OrderPlaced",
820
+ idFields: [{
821
+ name: "orderId",
822
+ isList: false
823
+ }]
824
+ }], undefined, "Order");
825
+ let shipOrder = slice("ShipOrder", [
826
+ {
827
+ name: "orderId",
828
+ isList: false
829
+ },
830
+ {
831
+ name: "carrierBookingId",
832
+ isList: false
833
+ }
834
+ ], [
835
+ {
836
+ eventType: "OrderPlaced",
837
+ idFields: [{
838
+ name: "orderId",
839
+ isList: false
840
+ }]
841
+ },
842
+ {
843
+ eventType: "OrderShipped",
844
+ idFields: [{
845
+ name: "orderId",
846
+ isList: false
847
+ }]
848
+ }
849
+ ], [{
850
+ eventType: "OrderShipped",
851
+ idFields: [
852
+ {
853
+ name: "orderId",
854
+ isList: false
855
+ },
856
+ {
857
+ name: "carrierBookingId",
858
+ isList: false
859
+ }
860
+ ]
861
+ }], "orderId", "Order");
862
+ globalThis.test("a hint its chapter backs overrides inference instead of contradicting it", () => {
863
+ let issues = DcbValidation$Reventless.validatePartitionHintsVsInference([
864
+ placeOrder,
865
+ shipOrder
866
+ ]);
867
+ globalThis.expect(issues.contradictions).toEqual([]);
868
+ globalThis.expect(issues.redundancies).toEqual([]);
869
+ globalThis.expect(issues.overrides.map(e => e.sliceName)).toEqual(["ShipOrder"]);
870
+ globalThis.expect(DcbScopeInference$Reventless.resolvePartitions([
871
+ placeOrder,
872
+ shipOrder
873
+ ]).partitionBySlice["ShipOrder"]).toEqual("orderId");
874
+ });
875
+ globalThis.test("without a chapter the same hint is still a contradiction", () => {
876
+ let noChapter = s => {
877
+ let newrecord = {...s};
878
+ newrecord.chapter = undefined;
879
+ return newrecord;
880
+ };
881
+ let issues = DcbValidation$Reventless.validatePartitionHintsVsInference([
882
+ noChapter(placeOrder),
883
+ noChapter(shipOrder)
884
+ ]);
885
+ globalThis.expect(issues.contradictions.map(e => e.sliceName)).toEqual(["ShipOrder"]);
886
+ globalThis.expect(issues.overrides).toEqual([]);
887
+ });
888
+ let newrecord = {...addCategory};
889
+ let newrecord$1 = {...renameCategory};
890
+ let newrecord$2 = {...archiveCategory};
891
+ let categories = [
892
+ (newrecord.chapter = "Category", newrecord),
893
+ (newrecord$1.chapter = "Category", newrecord$1),
894
+ (newrecord$2.chapter = "Category", newrecord$2)
895
+ ];
896
+ globalThis.test("a hint its chapter disagrees with is still a contradiction", () => {
897
+ let renameProduct = slice("RenameProduct", undefined, [{
898
+ eventType: "ProductAdded",
899
+ idFields: [{
900
+ name: "productId",
901
+ isList: false
902
+ }]
903
+ }], [{
904
+ eventType: "ProductRenamed",
905
+ idFields: [{
906
+ name: "productId",
907
+ isList: false
908
+ }]
909
+ }], undefined, "Product");
910
+ let newrecord = {...hinted(addProduct, "categoryId")};
911
+ newrecord.chapter = "Product";
912
+ let issues = DcbValidation$Reventless.validatePartitionHintsVsInference(Belt_Array.concatMany([
913
+ [
914
+ newrecord,
915
+ renameProduct
916
+ ],
917
+ categories
918
+ ]));
919
+ globalThis.expect(issues.contradictions.map(e => e.sliceName)).toEqual(["AddProduct"]);
920
+ globalThis.expect(issues.overrides).toEqual([]);
921
+ });
922
+ globalThis.test("a chapter that carries both keys everywhere backs neither", () => {
923
+ let newrecord = {...hinted(addProduct, "categoryId")};
924
+ newrecord.chapter = "Product";
925
+ let issues = DcbValidation$Reventless.validatePartitionHintsVsInference(Belt_Array.concatMany([
926
+ [newrecord],
927
+ categories
928
+ ]));
929
+ globalThis.expect(issues.contradictions.map(e => e.sliceName)).toEqual(["AddProduct"]);
930
+ globalThis.expect(issues.overrides).toEqual([]);
931
+ });
932
+ globalThis.test("a chapter-backed hint read off another entity's events is still a contradiction", () => {
933
+ let assign = slice("AssignWarehouse", undefined, undefined, [{
934
+ eventType: "WarehouseAssigned",
935
+ idFields: [
936
+ {
937
+ name: "warehouseId",
938
+ isList: false
939
+ },
940
+ {
941
+ name: "orderId",
942
+ isList: false
943
+ }
944
+ ]
945
+ }], "warehouseId", "Warehouse");
946
+ let newrecord = {...shipOrder};
947
+ newrecord.consumed = shipOrder.consumed.concat([{
948
+ eventType: "WarehouseAssigned",
949
+ idFields: [
950
+ {
951
+ name: "warehouseId",
952
+ isList: false
953
+ },
954
+ {
955
+ name: "orderId",
956
+ isList: false
957
+ }
958
+ ]
959
+ }]);
960
+ let issues = DcbValidation$Reventless.validatePartitionHintsVsInference([
961
+ placeOrder,
962
+ newrecord,
963
+ assign
964
+ ]);
965
+ globalThis.expect(issues.overrides.map(e => e.sliceName)).toEqual([]);
966
+ globalThis.expect(issues.contradictions.map(e => e.sliceName)).toEqual(["ShipOrder"]);
967
+ });
817
968
  });
818
969
  globalThis.describe("ambiguity surfacing", () => {
819
970
  let aOwner = slice("A", undefined, undefined, [{