@reventlessdev/reventless-spec 3.0.0-alpha.170 → 3.0.0-alpha.172

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,37 @@
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.172 (2026-10-21)
7
+
8
+ * feat!: a rejected command says whether the model, the access check or the platform said no ([6a8a6b0](https://github.com/ReventlessDev/reventless-core/commit/6a8a6b0f2f31e766ae26a944c23b0904e51a5865))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a type error variant with one of those names fails to
13
+ compile; rename it for what the model means, such as AlreadyBooked. On
14
+ AWS, rebuild every plugin against this release before a client selects
15
+ cause: a plugin built earlier answers it with a non-null error.
16
+
17
+
18
+
19
+ # 3.0.0-alpha.171 (2026-10-21)
20
+
21
+ * feat!: a state view's row key is module Id ([08c9726](https://github.com/ReventlessDev/reventless-core/commit/08c972659a3d689a39fd5e2f2026597155e92210))
22
+ * feat!: a plugin structure's component definitions are read-side and write-side defs ([ea2ab69](https://github.com/ReventlessDev/reventless-core/commit/ea2ab69e928ba16d7e640e4c66ab0e9ba034fe5c))
23
+
24
+ ### BREAKING CHANGES
25
+
26
+ * a state view spec declares its row key as module Id. A
27
+ hand-written module Key in a spec under StateView/ or StateViewStream/
28
+ fails the build with "A state view's row key is `module Id`, not
29
+ `module Key`".
30
+ * Plugin.queryableDef, Plugin.writableDef, their schemas,
31
+ isPublicQueryable and encodeQueryableDef no longer exist; a reader names
32
+ readSideDef, writeSideDef, isPublicReadSide and encodeReadSideDef. There
33
+ are no aliases, so the compiler lists every reader.
34
+
35
+
36
+
6
37
  # 3.0.0-alpha.170 (2026-10-21)
7
38
 
8
39
  * feat!: the write-side logic module is called the decision, and a codemod moves an app over ([cb573ff](https://github.com/ReventlessDev/reventless-core/commit/cb573ff5b928c7d7f3e75988dfaa65066f8c95eb))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.170",
3
+ "version": "3.0.0-alpha.172",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -9,11 +9,18 @@ type CommandPending {
9
9
  }
10
10
 
11
11
  type CommandRejected {
12
+ cause: CommandRejectionCause!
12
13
  errorCode: String!
13
14
  errorDetail: String
14
15
  msgId: ID!
15
16
  }
16
17
 
18
+ enum CommandRejectionCause {
19
+ ACCESS
20
+ DOMAIN
21
+ INFRASTRUCTURE
22
+ }
23
+
17
24
  union CommandResult = CommandAccepted | CommandPending | CommandRejected
18
25
 
19
26
  type Mutation {
@@ -0,0 +1,54 @@
1
+ /**
2
+ The codes the platform itself puts on a rejected command, as opposed to the
3
+ error names a spec declares.
4
+
5
+ A spec may not name an error like one of these: the PPX refuses it, so a
6
+ client reading only `errorCode` can still tell a platform code from the
7
+ model's. The PPX holds a copy of `platformErrorCodes` (it cannot read this
8
+ file); a spec test keeps the two equal.
9
+ */
10
+ type platformErrorCode =
11
+ /** The caller may not act: an `@authorize` rule or an ownership check refused. */
12
+ | Forbidden
13
+ /** Contention the retries could not absorb, or a handler that failed outright. */
14
+ | Conflict
15
+ /** The event log refused the append. */
16
+ | AppendFailed
17
+ /** The plugin is disconnected; retry later. */
18
+ | PluginUnavailable
19
+ /** An administrator deactivated the plugin. */
20
+ | PluginInactive
21
+ /** The plugin version was retired. */
22
+ | PluginRetired
23
+ /** An inbound translation's commands could not be published. */
24
+ | PublishFailed
25
+ /** An inbound translation refused its input. */
26
+ | TranslationFailed
27
+
28
+ let code = (c: platformErrorCode): string =>
29
+ switch c {
30
+ | Forbidden => "Forbidden"
31
+ | Conflict => "Conflict"
32
+ | AppendFailed => "AppendFailed"
33
+ | PluginUnavailable => "PluginUnavailable"
34
+ | PluginInactive => "PluginInactive"
35
+ | PluginRetired => "PluginRetired"
36
+ | PublishFailed => "PublishFailed"
37
+ | TranslationFailed => "TranslationFailed"
38
+ }
39
+
40
+ /** Every constructor, in declaration order. */
41
+ let all = [
42
+ Forbidden,
43
+ Conflict,
44
+ AppendFailed,
45
+ PluginUnavailable,
46
+ PluginInactive,
47
+ PluginRetired,
48
+ PublishFailed,
49
+ TranslationFailed,
50
+ ]
51
+
52
+ let platformErrorCodes = all->Array.map(code)
53
+
54
+ let isPlatformErrorCode = (name: string): bool => platformErrorCodes->Array.includes(name)
@@ -0,0 +1,48 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+
4
+ function code(c) {
5
+ switch (c) {
6
+ case "Forbidden" :
7
+ return "Forbidden";
8
+ case "Conflict" :
9
+ return "Conflict";
10
+ case "AppendFailed" :
11
+ return "AppendFailed";
12
+ case "PluginUnavailable" :
13
+ return "PluginUnavailable";
14
+ case "PluginInactive" :
15
+ return "PluginInactive";
16
+ case "PluginRetired" :
17
+ return "PluginRetired";
18
+ case "PublishFailed" :
19
+ return "PublishFailed";
20
+ case "TranslationFailed" :
21
+ return "TranslationFailed";
22
+ }
23
+ }
24
+
25
+ let all = [
26
+ "Forbidden",
27
+ "Conflict",
28
+ "AppendFailed",
29
+ "PluginUnavailable",
30
+ "PluginInactive",
31
+ "PluginRetired",
32
+ "PublishFailed",
33
+ "TranslationFailed"
34
+ ];
35
+
36
+ let platformErrorCodes = all.map(code);
37
+
38
+ function isPlatformErrorCode(name) {
39
+ return platformErrorCodes.includes(name);
40
+ }
41
+
42
+ export {
43
+ code,
44
+ all,
45
+ platformErrorCodes,
46
+ isPlatformErrorCode,
47
+ }
48
+ /* platformErrorCodes Not a pure module */
@@ -173,7 +173,7 @@ type commandDef = {
173
173
  means the command does not move the row. */
174
174
  targetState: option<string>,
175
175
  /** Where the from-set came from, ranking an inherited edge against an authored
176
- one the way `queryableDef.labelFieldSource` does for its field:
176
+ one the way `readSideDef.labelFieldSource` does for its field:
177
177
 
178
178
  - `"derived"` — the component's own scenarios.
179
179
  - `"declared"` — its `commandTransition` switch.
@@ -204,7 +204,7 @@ type commandDef = {
204
204
  }
205
205
 
206
206
  @schema
207
- type queryableDef = {
207
+ type readSideDef = {
208
208
  name: string,
209
209
  queryField: string,
210
210
  schema: string,
@@ -276,7 +276,7 @@ type errorDef = {
276
276
  }
277
277
 
278
278
  @schema
279
- type writableDef = {
279
+ type writeSideDef = {
280
280
  name: string,
281
281
  commands: array<commandDef>,
282
282
  producedEventTypes: array<string>,
@@ -289,7 +289,7 @@ type writableDef = {
289
289
  required list still lacks the key — both admin resolvers heal absent ones to
290
290
  `[]` on read, and a new one must be added there too. */
291
291
  errors: array<errorDef>,
292
- /** Chapter grouping band — see `queryableDef.chapter`. */
292
+ /** Chapter grouping band — see `readSideDef.chapter`. */
293
293
  chapter: option<string>,
294
294
  }
295
295
 
@@ -299,7 +299,7 @@ type automationSliceDef = {
299
299
  consumedEventTypes: array<string>,
300
300
  producedCommandTypes: array<string>,
301
301
  targetName: string,
302
- /** Chapter grouping band — see `queryableDef.chapter`. */
302
+ /** Chapter grouping band — see `readSideDef.chapter`. */
303
303
  chapter: option<string>,
304
304
  /** The declared `schedule`, as the JSON of `Schedule.timeSchema`; `None` for an
305
305
  automation started by events only, and in a structure from before the field. */
@@ -314,7 +314,7 @@ type outboundTranslationSliceDef = {
314
314
  targetName: option<string>,
315
315
  // Foreign system this slice publishes to — drives the external box (Event Graph).
316
316
  externalSystem: option<string>,
317
- /** Chapter grouping band — see `queryableDef.chapter`. */
317
+ /** Chapter grouping band — see `readSideDef.chapter`. */
318
318
  chapter: option<string>,
319
319
  /** The topics this slice subscribes to, as `Spec.sourceNames` declares them —
320
320
  an Aggregate's `Spec.name` or a DCB source name. `Some([])` is the declared
@@ -333,7 +333,7 @@ type inboundTranslationSliceDef = {
333
333
  targetName: string,
334
334
  // Foreign system this slice receives from — drives the external box (Event Graph).
335
335
  externalSystem: option<string>,
336
- /** Chapter grouping band — see `queryableDef.chapter`. */
336
+ /** Chapter grouping band — see `readSideDef.chapter`. */
337
337
  chapter: option<string>,
338
338
  /** The roles any of its commands' rules name; see `commandDef`. */
339
339
  requiredRoles?: array<string>,
@@ -393,7 +393,7 @@ type extensionDef = {
393
393
  An extension point owned by a plugin, from the producer side.
394
394
 
395
395
  `sourceEventTypes` are the `Delegate`'s events feeding the published protocol,
396
- plugin-qualified to match `writableDef.producedEventTypes`. `commandTypes` is the
396
+ plugin-qualified to match `writeSideDef.producedEventTypes`. `commandTypes` is the
397
397
  EP's inbound protocol — None (read as []) for a `command = unit` EP, which routes
398
398
  nothing. Definitions persisted before a field was added must be re-emitted.
399
399
  */
@@ -474,10 +474,10 @@ is fabricated and warned about, so it is the one shape to avoid. Regression suit
474
474
  */
475
475
  @schema
476
476
  type pluginStructure = {
477
- readModels: array<queryableDef>,
478
- stateViewSlices: array<queryableDef>,
479
- stateChangeSlices: array<writableDef>,
480
- aggregates: array<writableDef>,
477
+ readModels: array<readSideDef>,
478
+ stateViewSlices: array<readSideDef>,
479
+ stateChangeSlices: array<writeSideDef>,
480
+ aggregates: array<writeSideDef>,
481
481
  automationSlices: array<automationSliceDef>,
482
482
  outboundTranslationSlices: array<outboundTranslationSliceDef>,
483
483
  inboundTranslationSlices: array<inboundTranslationSliceDef>,
@@ -106,7 +106,7 @@ let commandDefSchema = Sury.$schema(s => ({
106
106
  ownerField: s.m(Sury.$option(Sury.string))
107
107
  }));
108
108
 
109
- let queryableDefSchema = Sury.$schema(s => ({
109
+ let readSideDefSchema = Sury.$schema(s => ({
110
110
  name: s.m(Sury.string),
111
111
  queryField: s.m(Sury.string),
112
112
  schema: s.m(Sury.string),
@@ -141,7 +141,7 @@ let errorDefSchema = Sury.$schema(s => ({
141
141
  references: s.m(Sury.array(fieldReferenceSchema))
142
142
  }));
143
143
 
144
- let writableDefSchema = Sury.$schema(s => ({
144
+ let writeSideDefSchema = Sury.$schema(s => ({
145
145
  name: s.m(Sury.string),
146
146
  commands: s.m(Sury.array(commandDefSchema)),
147
147
  producedEventTypes: s.m(Sury.array(Sury.string)),
@@ -239,10 +239,10 @@ let traitDeclarationSchema = Sury.$schema(s => ({
239
239
  }));
240
240
 
241
241
  let pluginStructureSchema = Sury.$schema(s => ({
242
- readModels: s.m(Sury.array(queryableDefSchema)),
243
- stateViewSlices: s.m(Sury.array(queryableDefSchema)),
244
- stateChangeSlices: s.m(Sury.array(writableDefSchema)),
245
- aggregates: s.m(Sury.array(writableDefSchema)),
242
+ readModels: s.m(Sury.array(readSideDefSchema)),
243
+ stateViewSlices: s.m(Sury.array(readSideDefSchema)),
244
+ stateChangeSlices: s.m(Sury.array(writeSideDefSchema)),
245
+ aggregates: s.m(Sury.array(writeSideDefSchema)),
246
246
  automationSlices: s.m(Sury.array(automationSliceDefSchema)),
247
247
  outboundTranslationSlices: s.m(Sury.array(outboundTranslationSliceDefSchema)),
248
248
  inboundTranslationSlices: s.m(Sury.array(inboundTranslationSliceDefSchema)),
@@ -290,10 +290,10 @@ export {
290
290
  commandLevelSchema,
291
291
  fieldReferenceSchema,
292
292
  commandDefSchema,
293
- queryableDefSchema,
293
+ readSideDefSchema,
294
294
  eventDefSchema,
295
295
  errorDefSchema,
296
- writableDefSchema,
296
+ writeSideDefSchema,
297
297
  automationSliceDefSchema,
298
298
  outboundTranslationSliceDefSchema,
299
299
  inboundTranslationSliceDefSchema,
@@ -161,7 +161,7 @@ type stateAnnotationSpec = {
161
161
  command's declared edge is written in terms of, a board draws its columns
162
162
  from and a state diagram renders. `Some(name)` when one such annotation
163
163
  exists; the PPX errors on duplicate `@lifecycle` annotations within the same
164
- record. Codegen consumes this to populate `queryableDef.lifecycleField`, and
164
+ record. Codegen consumes this to populate `readSideDef.lifecycleField`, and
165
165
  falls back to a field literally named `"lifecycle"` whose shape is an enum
166
166
  when this annotation is absent — so a record whose field can honestly be
167
167
  called `lifecycle` declares one without ceremony.
@@ -53,11 +53,11 @@ module type Spec = {
53
53
 
54
54
  /**
55
55
  What a row is keyed by. `Id.StringPure` (a plain `string`) unless the view
56
- declares its identity — `module Key = OrderId` — in which case a projection
56
+ declares its identity — `module Id = OrderId` — in which case a projection
57
57
  that keys a row by another entity's id does not compile. Auto-injected by
58
58
  `@@reventless.spec` in a `StateView/` folder.
59
59
  */
60
- module Key: Id.T
60
+ module Id: Id.T
61
61
 
62
62
  /**
63
63
  Events this view slice projects. Only needs the fields required for the projection —
@@ -125,7 +125,7 @@ module type Projection = {
125
125
  Receives the event wrapped in a `consumed` envelope (event + `meta` +
126
126
  `recordedAt`); only events declared in `Spec.consumedEvent` — no wildcard needed.
127
127
  */
128
- let project: consumed<Spec.consumedEvent> => array<Projection.action<Spec.Key.t, Spec.state>>
128
+ let project: consumed<Spec.consumedEvent> => array<Projection.action<Spec.Id.t, Spec.state>>
129
129
 
130
130
  /** File URL of this Projection module (`import.meta.url`). */
131
131
  let moduleUrl: string
@@ -595,7 +595,8 @@ let lifecycleMapFor = (
595
595
 
596
596
  // ── Rule 2 and 3: the relation a command's own scenarios describe ───────────
597
597
 
598
- type outcome = Emitted | NoChange | Refused
598
+ // `Rejected`: the decision rejected the command, the sidecar's `"error"`.
599
+ type outcome = Emitted | NoChange | Rejected
599
600
 
600
601
  type observation = {
601
602
  title: string,
@@ -646,7 +647,7 @@ let outcomeOf = (s: scenario): outcome =>
646
647
  // `thenNoEvent`, and the empty `then` an older sidecar wrote for the same
647
648
  // thing. Accepted, and nothing happened.
648
649
  | "noEvent" | "" => NoChange
649
- | "error" => Refused
650
+ | "error" => Rejected
650
651
  // A `then` this harvest has no reading for — a side effect, a published
651
652
  // command. Not an effect on this row either way.
652
653
  | _ => NoChange
@@ -739,7 +740,7 @@ let deriveCommands = (
739
740
  mine->Array.filterMap(o => o.outcome == Emitted || o.from == noRow ? None : Some(o.from)),
740
741
  ),
741
742
  refusedStates: sortedUnique(
742
- mine->Array.filterMap(o => o.outcome == Refused && o.from != noRow ? Some(o.from) : None),
743
+ mine->Array.filterMap(o => o.outcome == Rejected && o.from != noRow ? Some(o.from) : None),
743
744
  ),
744
745
  targets: sortedUnique(
745
746
  effective->Array.filterMap(o => o.to == o.from || o.to == noRow ? None : Some(o.to)),
@@ -938,7 +939,7 @@ let compare = (
938
939
  // exercised in and **refused**: that is the switch and the behavior
939
940
  // disagreeing about the same row.
940
941
  //
941
- // 🚨 **Refused, not merely inert.** `Unrestricted` claims the command is
942
+ // 🚨 **Rejected, not merely inert.** `Unrestricted` claims the command is
942
943
  // never refused — it does not claim the command always emits. A state where a
943
944
  // scenario shows it accepted and silent AGREES with that: returning `Ok([])`
944
945
  // for a command that would change nothing is this codebase's idempotency
@@ -470,7 +470,7 @@ function outcomeOf(s) {
470
470
  let match = s.thenKind;
471
471
  switch (match) {
472
472
  case "error" :
473
- return "Refused";
473
+ return "Rejected";
474
474
  case "event" :
475
475
  return "Emitted";
476
476
  default:
@@ -561,7 +561,7 @@ function deriveCommands(component, observations, labelled) {
561
561
  }
562
562
  })),
563
563
  refusedStates: sortedUnique(Stdlib_Array.filterMap(mine, o => {
564
- if (o.outcome === "Refused" && o.from !== noRow) {
564
+ if (o.outcome === "Rejected" && o.from !== noRow) {
565
565
  return o.from;
566
566
  }
567
567
  })),
@@ -21,18 +21,18 @@ let needsOf = (structure: Plugin.pluginStructure, ~plugin: string): array<need>
21
21
  let need = (item, role) => {plugin, item, role, group: Role.groupOf(role)}
22
22
  let rolesOf = (item, roles: option<array<string>>) =>
23
23
  roles->Option.getOr([])->Array.map(role => need(item, Role.make(role)))
24
- let writable = (defs: array<Plugin.writableDef>) =>
24
+ let writeSide = (defs: array<Plugin.writeSideDef>) =>
25
25
  defs->Array.flatMap(w =>
26
26
  w.commands->Array.flatMap(c => rolesOf(`${w.name}.${c.name}`, c.requiredRoles))
27
27
  )
28
- let queryable = (defs: array<Plugin.queryableDef>) =>
28
+ let readSide = (defs: array<Plugin.readSideDef>) =>
29
29
  defs->Array.flatMap(q => rolesOf(q.name, q.requiredRoles))
30
30
  Array.flat([
31
- writable(structure.aggregates),
32
- writable(structure.stateChangeSlices),
31
+ writeSide(structure.aggregates),
32
+ writeSide(structure.stateChangeSlices),
33
33
  structure.inboundTranslationSlices->Array.flatMap(s => rolesOf(s.name, s.requiredRoles)),
34
- queryable(structure.readModels),
35
- queryable(structure.stateViewSlices),
34
+ readSide(structure.readModels),
35
+ readSide(structure.stateViewSlices),
36
36
  ])
37
37
  }
38
38
 
@@ -13,14 +13,14 @@ function needsOf(structure, plugin) {
13
13
  role: role,
14
14
  group: Role$Reventless.groupOf(role)
15
15
  }));
16
- let writable = defs => defs.flatMap(w => w.commands.flatMap(c => rolesOf(w.name + `.` + c.name, c.requiredRoles)));
17
- let queryable = defs => defs.flatMap(q => rolesOf(q.name, q.requiredRoles));
16
+ let writeSide = defs => defs.flatMap(w => w.commands.flatMap(c => rolesOf(w.name + `.` + c.name, c.requiredRoles)));
17
+ let readSide = defs => defs.flatMap(q => rolesOf(q.name, q.requiredRoles));
18
18
  return [
19
- writable(structure.aggregates),
20
- writable(structure.stateChangeSlices),
19
+ writeSide(structure.aggregates),
20
+ writeSide(structure.stateChangeSlices),
21
21
  structure.inboundTranslationSlices.flatMap(s => rolesOf(s.name, s.requiredRoles)),
22
- queryable(structure.readModels),
23
- queryable(structure.stateViewSlices)
22
+ readSide(structure.readModels),
23
+ readSide(structure.stateViewSlices)
24
24
  ].flat();
25
25
  }
26
26