@reventlessdev/reventless-spec 3.0.0-alpha.170 → 3.0.0-alpha.171
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 +18 -0
- package/package.json +1 -1
- package/src/components/Plugin.res +12 -12
- package/src/components/Plugin.res.mjs +8 -8
- package/src/components/StateAnnotations.res +1 -1
- package/src/components/StateViewSlice.res +3 -3
- package/src/lifecycle/CheckLifecycleModel.res +5 -4
- package/src/lifecycle/CheckLifecycleModel.res.mjs +2 -2
- package/src/types/RoleCoverage.res +6 -6
- package/src/types/RoleCoverage.res.mjs +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,24 @@
|
|
|
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.171 (2026-10-21)
|
|
7
|
+
|
|
8
|
+
* feat!: a state view's row key is module Id ([08c9726](https://github.com/ReventlessDev/reventless-core/commit/08c972659a3d689a39fd5e2f2026597155e92210))
|
|
9
|
+
* feat!: a plugin structure's component definitions are read-side and write-side defs ([ea2ab69](https://github.com/ReventlessDev/reventless-core/commit/ea2ab69e928ba16d7e640e4c66ab0e9ba034fe5c))
|
|
10
|
+
|
|
11
|
+
### BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* a state view spec declares its row key as module Id. A
|
|
14
|
+
hand-written module Key in a spec under StateView/ or StateViewStream/
|
|
15
|
+
fails the build with "A state view's row key is `module Id`, not
|
|
16
|
+
`module Key`".
|
|
17
|
+
* Plugin.queryableDef, Plugin.writableDef, their schemas,
|
|
18
|
+
isPublicQueryable and encodeQueryableDef no longer exist; a reader names
|
|
19
|
+
readSideDef, writeSideDef, isPublicReadSide and encodeReadSideDef. There
|
|
20
|
+
are no aliases, so the compiler lists every reader.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
6
24
|
# 3.0.0-alpha.170 (2026-10-21)
|
|
7
25
|
|
|
8
26
|
* 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
|
@@ -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 `
|
|
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
|
|
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
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
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<
|
|
478
|
-
stateViewSlices: array<
|
|
479
|
-
stateChangeSlices: array<
|
|
480
|
-
aggregates: array<
|
|
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
|
|
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
|
|
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(
|
|
243
|
-
stateViewSlices: s.m(Sury.array(
|
|
244
|
-
stateChangeSlices: s.m(Sury.array(
|
|
245
|
-
aggregates: s.m(Sury.array(
|
|
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
|
-
|
|
293
|
+
readSideDefSchema,
|
|
294
294
|
eventDefSchema,
|
|
295
295
|
errorDefSchema,
|
|
296
|
-
|
|
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 `
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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" =>
|
|
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 ==
|
|
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
|
-
// 🚨 **
|
|
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 "
|
|
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 === "
|
|
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
|
|
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
|
|
28
|
+
let readSide = (defs: array<Plugin.readSideDef>) =>
|
|
29
29
|
defs->Array.flatMap(q => rolesOf(q.name, q.requiredRoles))
|
|
30
30
|
Array.flat([
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
writeSide(structure.aggregates),
|
|
32
|
+
writeSide(structure.stateChangeSlices),
|
|
33
33
|
structure.inboundTranslationSlices->Array.flatMap(s => rolesOf(s.name, s.requiredRoles)),
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
17
|
-
let
|
|
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
|
-
|
|
20
|
-
|
|
19
|
+
writeSide(structure.aggregates),
|
|
20
|
+
writeSide(structure.stateChangeSlices),
|
|
21
21
|
structure.inboundTranslationSlices.flatMap(s => rolesOf(s.name, s.requiredRoles)),
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
readSide(structure.readModels),
|
|
23
|
+
readSide(structure.stateViewSlices)
|
|
24
24
|
].flat();
|
|
25
25
|
}
|
|
26
26
|
|