@reventlessdev/reventless-gwt 1.0.0-alpha.199 → 1.0.0-alpha.201

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,21 @@
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.201 (2026-09-02)
7
+
8
+ ### Features
9
+
10
+ * **dcb:** a boundary that cannot derive its scope says so ([db79969](https://github.com/ReventlessDev/reventless-core/commit/db79969df36bd90607425f6a22b4624227cfc4a0))
11
+
12
+
13
+ # 1.0.0-alpha.200 (2026-09-01)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/reventless-gwt
16
+
17
+
18
+
19
+
20
+
6
21
  # 1.0.0-alpha.199 (2026-09-01)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.199",
3
+ "version": "1.0.0-alpha.201",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -15,16 +15,16 @@
15
15
  "dependencies": {
16
16
  "sury": "11.0.0-rc.2",
17
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
18
- "@reventlessdev/rescript-node": "2.0.0-alpha.8",
19
18
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
20
- "@reventlessdev/reventless-core": "3.0.0-alpha.252",
21
- "@reventlessdev/reventless-infra": "3.0.0-alpha.153",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.125"
19
+ "@reventlessdev/rescript-node": "2.0.0-alpha.8",
20
+ "@reventlessdev/reventless-core": "3.0.0-alpha.254",
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.155",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.127"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.0",
26
26
  "sury-ppx": "11.0.0-rc.2",
27
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.75"
27
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.76"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "rescript": "12.3.0"
package/src/Flow_GWT.res CHANGED
@@ -218,6 +218,45 @@ let test = (name, ~timeout=?, body: unit => flow) =>
218
218
  s.outcome
219
219
  })
220
220
 
221
+ // -- Boundary scope ----------------------------------------------------------
222
+
223
+ // Assert that a DCB consistency boundary derives its tag scope.
224
+ //
225
+ // Every other step here tests slices; this one tests the set of them, and it is
226
+ // in the flow DSL because a flow is the only test kind that already reasons
227
+ // about more than one slice at a time. The rest of the harness derives scope
228
+ // per slice (`DcbScopeInference.crossPartitionForSlice`), which cannot fail the
229
+ // way a boundary fails: a single slice always resolves, so the all-or-nothing
230
+ // fallback in `deriveEffectiveScope` — one unresolvable slice discarding the
231
+ // derived scope for every slice beside it — has no per-slice symptom at all. The
232
+ // slices keep passing their own tests and the platform rejects valid commands.
233
+ //
234
+ // Takes the generated `<Plugin>.Plugin.dcbSliceSchemas`, so it asserts over the
235
+ // boundary as deployed rather than over the steps a given flow happens to name.
236
+ let thenBoundaryScopeResolves = async (
237
+ flowP: flow,
238
+ ~name: string,
239
+ slices: array<Reventless.DcbTag.sliceSchemas>,
240
+ ) => {
241
+ let s = await flowP
242
+ let scope = Reventless.DcbTag.deriveEffectiveScope(slices)
243
+ // An ambiguity that costs nothing is not a failure: a boundary whose fallback
244
+ // carries every derived key reads exactly as the derivation would. What fails
245
+ // is losing a key, because that is the case where the runtime answers wrongly.
246
+ let outcome = if scope.droppedCrossPartitionTagKeys->Array.length > 0 {
247
+ Outcome.fail(
248
+ ScopeDegraded({
249
+ boundary: name,
250
+ dropped: scope.droppedCrossPartitionTagKeys,
251
+ ambiguities: scope.ambiguities->Array.map(((slice, reason)) => `${slice}: ${reason}`),
252
+ }),
253
+ )
254
+ } else {
255
+ Outcome.pass
256
+ }
257
+ s->recordOutcome(outcome)
258
+ }
259
+
221
260
  // -- CommandStep: a StateChangeSlice / Aggregate decider ---------------------
222
261
 
223
262
  module CommandStep = (
@@ -131,6 +131,18 @@ function test(name, timeout, body) {
131
131
  JestBind$ReventlessGwt.testPromise("Flow", name, timeout, async () => (await body()).outcome);
132
132
  }
133
133
 
134
+ async function thenBoundaryScopeResolves(flowP, name, slices) {
135
+ let s = await flowP;
136
+ let scope = DcbTag$Reventless.deriveEffectiveScope(slices);
137
+ let outcome = scope.droppedCrossPartitionTagKeys.length !== 0 ? Outcome$ReventlessGwt.fail({
138
+ TAG: "ScopeDegraded",
139
+ boundary: name,
140
+ dropped: scope.droppedCrossPartitionTagKeys,
141
+ ambiguities: scope.ambiguities.map(param => param[0] + `: ` + param[1])
142
+ }) : Outcome$ReventlessGwt.pass;
143
+ return recordOutcome(s, outcome);
144
+ }
145
+
134
146
  function CommandStep(Spec) {
135
147
  return Behavior => {
136
148
  let consumedDecoder = DcbDecode$Reventless.makeDecoder(Spec.consumedEventSchema);
@@ -589,6 +601,7 @@ export {
589
601
  decodeAggregateMatching,
590
602
  describe,
591
603
  test,
604
+ thenBoundaryScopeResolves,
592
605
  CommandStep,
593
606
  AggregateCommandStep,
594
607
  AutomationStep,
package/src/Hint.res CHANGED
@@ -93,6 +93,13 @@ let forMismatch = (~slice="<slice>", m: Outcome.mismatch): t =>
93
93
  branch: None,
94
94
  message: "The ExtensionPoint mapping / Extension delegate published a different set of events or commands than expected. Check the variant match arms and the one-to-many fan-out.",
95
95
  }
96
+ | ScopeDegraded({dropped}) => {
97
+ locus: `${slice} (DCB boundary)`,
98
+ branch: None,
99
+ 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 [${dropped->Array.join(
100
+ ", ",
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
+ }
96
103
  | Throw({error}) => {
97
104
  locus: `${slice}`,
98
105
  branch: None,
package/src/Hint.res.mjs CHANGED
@@ -89,6 +89,12 @@ function forMismatch(sliceOpt, m) {
89
89
  branch: undefined,
90
90
  message: "The ExtensionPoint mapping / Extension delegate published a different set of events or commands than expected. Check the variant match arms and the one-to-many fan-out."
91
91
  };
92
+ case "ScopeDegraded" :
93
+ return {
94
+ locus: slice + ` (DCB boundary)`,
95
+ branch: undefined,
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
+ };
92
98
  case "Throw" :
93
99
  return {
94
100
  locus: slice,
@@ -66,6 +66,10 @@ let normalize = (m: Outcome.mismatch): normalized => {
66
66
  Expected(renderMany(expected)),
67
67
  Actual(renderMany(actual)),
68
68
  ]
69
+ | ScopeDegraded({boundary, dropped, ambiguities}) => [
70
+ Expected(`${boundary}: cross-partition reads [${dropped->Array.join(", ")}] derived`),
71
+ Actual(ambiguities->Array.join(" | ")),
72
+ ]
69
73
  | Throw({error, stack}) => [Error(error), Stack(stack)]
70
74
  }
71
75
  {kind: Outcome.kindName(m), fields}
@@ -82,6 +82,18 @@ function normalize(m) {
82
82
  }
83
83
  ];
84
84
  break;
85
+ case "ScopeDegraded" :
86
+ fields = [
87
+ {
88
+ TAG: "Expected",
89
+ _0: m.boundary + `: cross-partition reads [` + m.dropped.join(", ") + `] derived`
90
+ },
91
+ {
92
+ TAG: "Actual",
93
+ _0: m.ambiguities.join(" | ")
94
+ }
95
+ ];
96
+ break;
85
97
  case "Throw" :
86
98
  fields = [
87
99
  {
package/src/Outcome.res CHANGED
@@ -26,6 +26,13 @@ type mismatch =
26
26
  // one-to-many fan-out (one inbound message → N published actions). Used by
27
27
  // `Delegate_GWT` and the cross-plugin `Flow_GWT` boundary steps.
28
28
  | PublishedActionsMismatch({expected: array<JSON.t>, actual: array<JSON.t>})
29
+ // A DCB consistency boundary could not derive its tag scope, so the runtime
30
+ // falls back to `@crossPartition` annotations and loses the cross-partition
31
+ // reference reads named in `dropped`. Not a mismatch between an expectation
32
+ // and a value — every slice in it still behaves correctly in isolation, which
33
+ // is exactly why it needs its own outcome: the defect is in the composition,
34
+ // and nothing that tests one slice can see it.
35
+ | ScopeDegraded({boundary: string, dropped: array<string>, ambiguities: array<string>})
29
36
  | Throw({error: string, stack: string})
30
37
 
31
38
  type outcome = result<unit, mismatch>
@@ -44,6 +51,7 @@ let kindName = (m: mismatch) =>
44
51
  | TranslateError(_) => "TranslateError"
45
52
  | QueryRowsMismatch(_) => "QueryRowsMismatch"
46
53
  | PublishedActionsMismatch(_) => "PublishedActionsMismatch"
54
+ | ScopeDegraded(_) => "ScopeDegraded"
47
55
  | Throw(_) => "Throw"
48
56
  }
49
57
 
@@ -93,5 +101,7 @@ let format = (m: mismatch) =>
93
101
  `PublishedActionsMismatch:\n expected: ${stringifyJsonArray(expected)}\n actual: ${stringifyJsonArray(
94
102
  actual,
95
103
  )}`
104
+ | ScopeDegraded({boundary, dropped, ambiguities}) =>
105
+ `ScopeDegraded:\n boundary: ${boundary}\n dropped: ${dropped->Array.join(", ")}\n cause: ${ambiguities->Array.join(" | ")}`
96
106
  | Throw({error, stack}) => `Throw: ${error}\n${stack}`
97
107
  }
@@ -29,6 +29,8 @@ function kindName(m) {
29
29
  return "QueryRowsMismatch";
30
30
  case "PublishedActionsMismatch" :
31
31
  return "PublishedActionsMismatch";
32
+ case "ScopeDegraded" :
33
+ return "ScopeDegraded";
32
34
  case "Throw" :
33
35
  return "Throw";
34
36
  }
@@ -71,6 +73,8 @@ function format(m) {
71
73
  return `QueryRowsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
72
74
  case "PublishedActionsMismatch" :
73
75
  return `PublishedActionsMismatch:\n expected: ` + stringifyJsonArray(m.expected) + `\n actual: ` + stringifyJsonArray(m.actual);
76
+ case "ScopeDegraded" :
77
+ return `ScopeDegraded:\n boundary: ` + m.boundary + `\n dropped: ` + m.dropped.join(", ") + `\n cause: ` + m.ambiguities.join(" | ");
74
78
  case "Throw" :
75
79
  return `Throw: ` + m.error + `\n` + m.stack;
76
80
  }