@reventlessdev/reventless-gwt 1.0.0-alpha.208 → 1.0.0-alpha.210

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,28 @@
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
+ # 1.0.0-alpha.210 (2026-09-16)
7
+
8
+ * feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
9
+ ### Features
10
+
11
+ * **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
12
+
13
+ ### BREAKING CHANGES
14
+
15
+ * DcbTag.derivePartitionTag is removed; a boundary whose
16
+ partition cannot be inferred no longer deploys.
17
+
18
+
19
+
20
+ # 1.0.0-alpha.209 (2026-09-11)
21
+
22
+ **Note:** Version bump only for package @reventlessdev/reventless-gwt
23
+
24
+
25
+
26
+
27
+
6
28
  # 1.0.0-alpha.208 (2026-09-09)
7
29
 
8
30
  **Note:** Version bump only for package @reventlessdev/reventless-gwt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.208",
3
+ "version": "1.0.0-alpha.210",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -14,12 +14,12 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "sury": "11.0.0-rc.2",
17
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
18
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
19
- "@reventlessdev/rescript-node": "2.0.0-alpha.11",
20
- "@reventlessdev/reventless-core": "3.0.0-alpha.261",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.162",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.134"
18
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
19
+ "@reventlessdev/reventless-core": "3.0.0-alpha.263",
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.164",
21
+ "@reventlessdev/rescript-node": "2.0.0-alpha.12",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.136"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.0",
@@ -195,7 +195,7 @@ function Make(Spec) {
195
195
  contents: undefined
196
196
  };
197
197
  let consumedEventTypes = DcbDecode$Reventless.makeDecoder(Spec.consumedEventSchema).eventTypes;
198
- let scopeShape = DcbTag$Reventless.sliceShapeFromSchemas("", Spec.commandSchema, Spec.consumedEventSchema, Spec.eventSchema);
198
+ let scopeShape = DcbTag$Reventless.sliceShapeFromSchemas("", Spec.commandSchema, Spec.consumedEventSchema, Spec.eventSchema, undefined);
199
199
  let set = new Set();
200
200
  DcbTag$Reventless.extractCrossPartitionTagKeys(Spec.eventSchema).forEach(k => {
201
201
  set.add(k);
package/src/Flow_GWT.res CHANGED
@@ -239,10 +239,27 @@ let thenBoundaryScopeResolves = async (
239
239
  ) => {
240
240
  let s = await flowP
241
241
  let scope = Reventless.DcbTag.deriveEffectiveScope(slices)
242
- // An ambiguity that costs nothing is not a failure: a boundary whose fallback
243
- // carries every derived key reads exactly as the derivation would. What fails
244
- // is losing a key, because that is the case where the runtime answers wrongly.
245
- let outcome = if scope.droppedCrossPartitionTagKeys->Array.length > 0 {
242
+ // Deploy and boot derive the storage partition from the same slices and refuse
243
+ // the boundary when it fails, so the test fails with their reasons.
244
+ let partitionReasons = {
245
+ let hints = Reventless.DcbValidation.validatePartitionHintsVsInference(
246
+ ~shapes=slices->Array.map(Reventless.DcbTag.sliceShape),
247
+ )
248
+ let contradictions = hints.contradictions->Array.map(e => `${e.sliceName}: ${e.message}`)
249
+ switch Reventless.DcbTag.deriveBoundaryPartition(slices) {
250
+ | _ => contradictions
251
+ | exception JsExn(err) =>
252
+ contradictions->Array.concat([
253
+ JsExn.message(err)->Option.getOr("partition derivation failed"),
254
+ ])
255
+ }
256
+ }
257
+ // Beyond that, an ambiguity that costs nothing is not a failure: a boundary whose
258
+ // fallback carries every derived key reads exactly as the derivation would. What
259
+ // fails is losing a key, because that is the case where the runtime answers wrongly.
260
+ let outcome = if partitionReasons->Array.length > 0 {
261
+ Outcome.fail(PartitionUnresolved({boundary: name, reasons: partitionReasons}))
262
+ } else if scope.droppedCrossPartitionTagKeys->Array.length > 0 {
246
263
  Outcome.fail(
247
264
  ScopeDegraded({
248
265
  boundary: name,
@@ -1,15 +1,18 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
+ import * as Stdlib_JsExn from "@rescript/runtime/lib/es6/Stdlib_JsExn.js";
4
5
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
6
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
7
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
7
8
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
8
9
  import * as Message$Reventless from "@reventlessdev/reventless-spec/src/types/Message.res.mjs";
9
10
  import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
11
+ import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
10
12
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
11
13
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
12
14
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
15
+ import * as DcbValidation$Reventless from "@reventlessdev/reventless-spec/src/components/DcbValidation.res.mjs";
13
16
  import * as StubRuntime$ReventlessGwt from "./StubRuntime.res.mjs";
14
17
  import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
15
18
  import * as Projection_GWT$ReventlessGwt from "./Projection_GWT.res.mjs";
@@ -134,12 +137,32 @@ function test(name, timeout, body) {
134
137
  async function thenBoundaryScopeResolves(flowP, name, slices) {
135
138
  let s = await flowP;
136
139
  let scope = DcbTag$Reventless.deriveEffectiveScope(slices);
137
- let outcome = scope.droppedCrossPartitionTagKeys.length !== 0 ? Outcome$ReventlessGwt.fail({
138
- TAG: "ScopeDegraded",
140
+ let hints = DcbValidation$Reventless.validatePartitionHintsVsInference(slices.map(DcbTag$Reventless.sliceShape));
141
+ let contradictions = hints.contradictions.map(e => e.sliceName + `: ` + e.message);
142
+ let partitionReasons;
143
+ try {
144
+ DcbTag$Reventless.deriveBoundaryPartition(slices);
145
+ partitionReasons = contradictions;
146
+ } catch (raw_err) {
147
+ let err = Primitive_exceptions.internalToException(raw_err);
148
+ if (err.RE_EXN_ID === "JsExn") {
149
+ partitionReasons = contradictions.concat([Stdlib_Option.getOr(Stdlib_JsExn.message(err._1), "partition derivation failed")]);
150
+ } else {
151
+ throw err;
152
+ }
153
+ }
154
+ let outcome = partitionReasons.length !== 0 ? Outcome$ReventlessGwt.fail({
155
+ TAG: "PartitionUnresolved",
139
156
  boundary: name,
140
- dropped: scope.droppedCrossPartitionTagKeys,
141
- ambiguities: scope.ambiguities.map(param => param[0] + `: ` + param[1])
142
- }) : Outcome$ReventlessGwt.pass;
157
+ reasons: partitionReasons
158
+ }) : (
159
+ scope.droppedCrossPartitionTagKeys.length !== 0 ? Outcome$ReventlessGwt.fail({
160
+ TAG: "ScopeDegraded",
161
+ boundary: name,
162
+ dropped: scope.droppedCrossPartitionTagKeys,
163
+ ambiguities: scope.ambiguities.map(param => param[0] + `: ` + param[1])
164
+ }) : Outcome$ReventlessGwt.pass
165
+ );
143
166
  return recordOutcome(s, outcome);
144
167
  }
145
168
 
@@ -159,7 +182,7 @@ function CommandStep(Spec) {
159
182
  lastAggregateId: s.lastAggregateId
160
183
  };
161
184
  };
162
- let scopeShape = DcbTag$Reventless.sliceShapeFromSchemas("", Spec.commandSchema, Spec.consumedEventSchema, Spec.eventSchema);
185
+ let scopeShape = DcbTag$Reventless.sliceShapeFromSchemas("", Spec.commandSchema, Spec.consumedEventSchema, Spec.eventSchema, undefined);
163
186
  let set = new Set();
164
187
  DcbTag$Reventless.extractCrossPartitionTagKeys(Spec.eventSchema).forEach(k => {
165
188
  set.add(k);
package/src/Hint.res CHANGED
@@ -100,6 +100,11 @@ let forMismatch = (~slice="<slice>", m: Outcome.mismatch): t =>
100
100
  ", ",
101
101
  )}] were lost. A slice referencing another entity by one of these keys now decides against an empty history and rejects valid commands. Fix the named slice — usually a consumed arm declaring the id the slice is already partitioned by.`,
102
102
  }
103
+ | PartitionUnresolved(_) => {
104
+ locus: `${slice} (DCB boundary)`,
105
+ branch: None,
106
+ message: "A slice in this boundary has no derivable partition key, or a @partitionTag names a key inference contradicts. Deploy and boot refuse the boundary. Follow the named reason: usually a consumed arm declaring the slice's own id, or an annotation to add or move.",
107
+ }
103
108
  | Throw({error}) => {
104
109
  locus: `${slice}`,
105
110
  branch: None,
package/src/Hint.res.mjs CHANGED
@@ -95,6 +95,12 @@ function forMismatch(sliceOpt, m) {
95
95
  branch: undefined,
96
96
  message: `A slice in this boundary has no derivable partition key, so the derived DCB scope was discarded for all of them and the cross-partition reads of [` + m.dropped.join(", ") + `] were lost. A slice referencing another entity by one of these keys now decides against an empty history and rejects valid commands. Fix the named slice — usually a consumed arm declaring the id the slice is already partitioned by.`
97
97
  };
98
+ case "PartitionUnresolved" :
99
+ return {
100
+ locus: slice + ` (DCB boundary)`,
101
+ branch: undefined,
102
+ message: "A slice in this boundary has no derivable partition key, or a @partitionTag names a key inference contradicts. Deploy and boot refuse the boundary. Follow the named reason: usually a consumed arm declaring the slice's own id, or an annotation to add or move."
103
+ };
98
104
  case "Throw" :
99
105
  return {
100
106
  locus: slice,
@@ -76,6 +76,10 @@ let normalize = (m: Outcome.mismatch): normalized => {
76
76
  Expected(`${boundary}: cross-partition reads [${dropped->Array.join(", ")}] derived`),
77
77
  Actual(ambiguities->Array.join(" | ")),
78
78
  ]
79
+ | PartitionUnresolved({boundary, reasons}) => [
80
+ Expected(`${boundary}: every slice's partition key derived`),
81
+ Actual(reasons->Array.join(" | ")),
82
+ ]
79
83
  | Throw({error, stack}) => [Error(error), Stack(stack)]
80
84
  }
81
85
  {kind: Outcome.kindName(m), fields}
@@ -94,6 +94,18 @@ function normalize(m) {
94
94
  }
95
95
  ];
96
96
  break;
97
+ case "PartitionUnresolved" :
98
+ fields = [
99
+ {
100
+ TAG: "Expected",
101
+ _0: m.boundary + `: every slice's partition key derived`
102
+ },
103
+ {
104
+ TAG: "Actual",
105
+ _0: m.reasons.join(" | ")
106
+ }
107
+ ];
108
+ break;
97
109
  case "Throw" :
98
110
  fields = [
99
111
  {
package/src/Outcome.res CHANGED
@@ -29,6 +29,10 @@ type mismatch =
29
29
  // is exactly why it needs its own outcome: the defect is in the composition,
30
30
  // and nothing that tests one slice can see it.
31
31
  | ScopeDegraded({boundary: string, dropped: array<string>, ambiguities: array<string>})
32
+ // A DCB consistency boundary whose storage partition cannot be derived: a slice
33
+ // with no inferable key, or a `@partitionTag` inference contradicts. Deploy and
34
+ // boot refuse such a boundary, so its flow test fails with the same reasons.
35
+ | PartitionUnresolved({boundary: string, reasons: array<string>})
32
36
  | Throw({error: string, stack: string})
33
37
 
34
38
  type outcome = result<unit, mismatch>
@@ -48,6 +52,7 @@ let kindName = (m: mismatch) =>
48
52
  | QueryRowsMismatch(_) => "QueryRowsMismatch"
49
53
  | PublishedActionsMismatch(_) => "PublishedActionsMismatch"
50
54
  | ScopeDegraded(_) => "ScopeDegraded"
55
+ | PartitionUnresolved(_) => "PartitionUnresolved"
51
56
  | Throw(_) => "Throw"
52
57
  }
53
58
 
@@ -103,5 +108,7 @@ let format = (m: mismatch) =>
103
108
  `ScopeDegraded:\n boundary: ${boundary}\n dropped: ${dropped->Array.join(
104
109
  ", ",
105
110
  )}\n cause: ${ambiguities->Array.join(" | ")}`
111
+ | PartitionUnresolved({boundary, reasons}) =>
112
+ `PartitionUnresolved:\n boundary: ${boundary}\n cause: ${reasons->Array.join(" | ")}`
106
113
  | Throw({error, stack}) => `Throw: ${error}\n${stack}`
107
114
  }
@@ -31,6 +31,8 @@ function kindName(m) {
31
31
  return "PublishedActionsMismatch";
32
32
  case "ScopeDegraded" :
33
33
  return "ScopeDegraded";
34
+ case "PartitionUnresolved" :
35
+ return "PartitionUnresolved";
34
36
  case "Throw" :
35
37
  return "Throw";
36
38
  }
@@ -75,6 +77,8 @@ function format(m) {
75
77
  return `PublishedActionsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
76
78
  case "ScopeDegraded" :
77
79
  return `ScopeDegraded:\n boundary: ` + m.boundary + `\n dropped: ` + m.dropped.join(", ") + `\n cause: ` + m.ambiguities.join(" | ");
80
+ case "PartitionUnresolved" :
81
+ return `PartitionUnresolved:\n boundary: ` + m.boundary + `\n cause: ` + m.reasons.join(" | ");
78
82
  case "Throw" :
79
83
  return `Throw: ` + m.error + `\n` + m.stack;
80
84
  }