@reventlessdev/reventless-core 3.0.0-alpha.252 → 3.0.0-alpha.253

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,14 @@
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.253 (2026-09-01)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-core
9
+
10
+
11
+
12
+
13
+
6
14
  # 3.0.0-alpha.252 (2026-09-01)
7
15
 
8
16
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.252",
3
+ "version": "3.0.0-alpha.253",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
29
29
  "sury": "11.0.0-rc.2",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.8",
32
- "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
- "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
36
32
  "@reventlessdev/rescript-node": "2.0.0-alpha.8",
33
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
34
+ "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
37
36
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.8",
37
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.154",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.153",
40
39
  "@reventlessdev/reventless-interop": "3.0.0-alpha.34",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.125"
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.126",
41
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
45
45
  "sury-ppx": "11.0.0-rc.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.75"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.76"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "12.3.0"
@@ -117,7 +117,7 @@ let checkRetiredValue = (~entityName: string, stateSchema: S.t<unknown>): retire
117
117
  ->Option.map(f => ` (that is "${f}")`)
118
118
  ->Option.getOr(
119
119
  " (it declares none)",
120
- )}. A retirement state no command's @transition can name loses the command filtering the state form exists for.`,
120
+ )}. A retirement state no command's declared edge can name loses the command filtering the state form exists for.`,
121
121
  )
122
122
  }
123
123
  let declared = switch stateSchema {
@@ -179,8 +179,9 @@ let reportRetiredStates = (
179
179
  }
180
180
  }
181
181
 
182
- // The states a record's lifecycle field can hold — the field `@transition` is
183
- // written in terms of. `None`: no lifecycle; `Some([])`: one with no cases.
182
+ // The states a record's lifecycle field can hold — the field a command's
183
+ // declared edge is written in terms of. `None`: no lifecycle; `Some([])`: one
184
+ // with no cases.
184
185
  let lifecycleStatesFromStateSchema = (
185
186
  ~entityName: string,
186
187
  stateSchema: S.t<unknown>,
@@ -373,18 +374,17 @@ let reportTranslationTables = (
373
374
  }
374
375
  }
375
376
 
376
- // A command's declared edge names states belonging to another component's enum.
377
- // From `@transition` the PPX only ever sees names, so a misspelling compiles clean
378
- // and produces a command legal in a state no row is in. Both sides are in hand
379
- // here. A state the linked views do not declare raises (this runs at assembly,
377
+ // A command's declared edge names states belonging to another component's enum,
378
+ // and nothing forces a spec to pick the one its linked view declares. Both sides
379
+ // are in hand here. A state the linked views do not declare raises (this runs at assembly,
380
380
  // never in a Lambda, so that is a failed deploy); no resolvable view only warns.
381
381
  // Checked against the UNION of the linked views: a slice feeding two is not
382
382
  // claiming which one.
383
383
  //
384
- // A `commandTransition` switch comes through the same `commandDef` fields and is
385
- // checked the same way. The compiler has already resolved its constructors, so
386
- // what survives to here is naming the wrong enum rather than misspelling one —
387
- // which this catches for the same reason and with the same message.
384
+ // The compiler has already resolved the switch's constructors, so what survives
385
+ // to here is a component naming the wrong lifecycle enum rather than misspelling
386
+ // a state — the half of the check `lifecycleState` cannot make, since nothing
387
+ // forces a spec to pick its linked view's enum.
388
388
  let checkDeclaredTransitions = (
389
389
  ~pluginName: string,
390
390
  ~writables: array<Reventless.Plugin.writableDef>,
@@ -714,9 +714,8 @@ let toCommandDef = (
714
714
  // component-level shortcut would gate `AddProduct` and `PlaceOrder` alike.
715
715
  ~commandAuthorization: unknown => Reventless.Authorization.permission,
716
716
  // The spec's `command => Transition.t<_>`, evaluated per variant the same way
717
- // `commandAuthorization` is. It wins over the `@transition` metadata where it
718
- // declares an edge, and stands aside where it says `Unrestricted` — which is
719
- // what the PPX injects for a spec that never wrote the switch.
717
+ // `commandAuthorization` is, and the only declaration of an edge there is —
718
+ // the PPX injects `Unrestricted` for a spec that writes no switch.
720
719
  //
721
720
  // Read at `t<string>` because this is the erasure boundary: the spec declares
722
721
  // its edges over the linked view's own lifecycle enum, whose arms are
@@ -740,34 +739,15 @@ let toCommandDef = (
740
739
  Reventless.DcbTag.isVariantPayloadBearing(parentSchema, variantName)
741
740
  ? {"TAG": variantName}->Obj.magic
742
741
  : variantName->Obj.magic
743
- // The spec's own switch, which is the only form that can speak for a
744
- // constructor the host did not declare: `@transition` lowers to a dict on
745
- // the parent union, and a variant spread splices members, so a spliced
746
- // command reaches the metadata below carrying nothing.
747
- let declared = commandTransition(syntheticCommand)
748
- // An edge is ONE declaration, so the two fields are chosen together. Taking
749
- // them separately would let `Guards(…)` — which positively claims the
750
- // command moves the row nowhere — inherit a target from an annotation it
751
- // was written to replace.
752
- //
753
- // The `@transition` metadata is per-variant but attached by the PPX to the
754
- // *parent* schema as one dict. It is consulted only where the switch
755
- // declares no edge at all: a spec that never wrote one answers
756
- // `Unrestricted` for every command, which is how the annotation stays in
757
- // charge for the hosts that still use it.
742
+ // The spec's own switch, which is exhaustive — so it also speaks for a
743
+ // constructor the host did not declare but spliced from a trait.
758
744
  //
745
+ // An edge is ONE declaration, so the two fields are read off it together.
759
746
  // `targetState: None` ⇒ AutoUI's board resolver falls back to its name-stem
760
747
  // heuristic.
761
- let (allowedStates, targetState) = switch declared {
762
- | Unrestricted => (
763
- ApiAllowedStatesHelpers.getAllowedStates(parentSchema, ~variantName),
764
- ApiTargetStateHelpers.getTargetState(parentSchema, ~variantName),
765
- )
766
- | _ => (
767
- Reventless.Transition.allowedStates(declared),
768
- Reventless.Transition.targetState(declared),
769
- )
770
- }
748
+ let declared = commandTransition(syntheticCommand)
749
+ let allowedStates = Reventless.Transition.allowedStates(declared)
750
+ let targetState = Reventless.Transition.targetState(declared)
771
751
  // API-exposed iff the whole command isn't @noApi and this variant
772
752
  // isn't in its @noApi-variants set — mirrors the API-generation filter
773
753
  // (Plugin_Helpers / PluginBaseFragment). Drives the event-graph API badge.
@@ -25,8 +25,6 @@ import * as StateAnnotations$Reventless from "@reventlessdev/reventless-spec/src
25
25
  import * as ApiNoApiHelpers$ReventlessCore from "../../components/Api/ApiNoApiHelpers.res.mjs";
26
26
  import * as SuryToJsonSchema$ReventlessCore from "../../components/Api/SuryToJsonSchema.res.mjs";
27
27
  import * as Capability_Inference$ReventlessCore from "./Capability_Inference.res.mjs";
28
- import * as ApiTargetStateHelpers$ReventlessCore from "../../components/Api/ApiTargetStateHelpers.res.mjs";
29
- import * as ApiAllowedStatesHelpers$ReventlessCore from "../../components/Api/ApiAllowedStatesHelpers.res.mjs";
30
28
  import * as GraphQL_FragmentGenerator$ReventlessCore from "../../components/Api/GraphQL_FragmentGenerator.res.mjs";
31
29
 
32
30
  let log = Logger$ReventlessCore.fromEnv();
@@ -122,7 +120,7 @@ function checkRetiredValue(entityName, stateSchema) {
122
120
  let named = values.join(", ");
123
121
  let lifecycle = lifecycleFieldFromStateSchema(entityName, stateSchema);
124
122
  if (Primitive_object.notequal(lifecycle, field)) {
125
- log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's @transition can name loses the command filtering the state form exists for.`);
123
+ log.warn("Plugin_Structure", undefined, entityName + `: @retired(` + named + `) is on "` + field + `", which is not this record's lifecycle field` + Stdlib_Option.getOr(Stdlib_Option.map(lifecycle, f => ` (that is "` + f + `")`), " (it declares none)") + `. A retirement state no command's declared edge can name loses the command filtering the state form exists for.`);
126
124
  }
127
125
  let declared;
128
126
  declared = stateSchema.type === "object" ? Stdlib_Option.getOr(Stdlib_Option.map(stateSchema.properties[field], schema => {
@@ -602,14 +600,8 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
602
600
  TAG: variantName
603
601
  }) : variantName;
604
602
  let declared = commandTransition(syntheticCommand);
605
- let match$1;
606
- match$1 = typeof declared !== "object" ? [
607
- ApiAllowedStatesHelpers$ReventlessCore.getAllowedStates(parentSchema, variantName),
608
- ApiTargetStateHelpers$ReventlessCore.getTargetState(parentSchema, variantName)
609
- ] : [
610
- Transition$Reventless.allowedStates(declared),
611
- Transition$Reventless.targetState(declared)
612
- ];
603
+ let allowedStates = Transition$Reventless.allowedStates(declared);
604
+ let targetState = Transition$Reventless.targetState(declared);
613
605
  let apiExposed = false;
614
606
  if (!ApiNoApiHelpers$ReventlessCore.isNoApi(parentSchema)) {
615
607
  let excluded = ApiNoApiHelpers$ReventlessCore.getExcludedVariants(parentSchema);
@@ -626,8 +618,8 @@ function toCommandDef(isAggregate, mutationFieldFor, parentSchema, commandAuthor
626
618
  aggregateIdField: match[1],
627
619
  mutationField: mutationField,
628
620
  references: references,
629
- allowedStates: match$1[0],
630
- targetState: match$1[1],
621
+ allowedStates: allowedStates,
622
+ targetState: targetState,
631
623
  apiExposed: apiExposed,
632
624
  requiredAccess: requiredAccess,
633
625
  ownerField: Owner$Reventless.fieldNamesOfProperties(properties)[0]
@@ -2,8 +2,9 @@
2
2
 
3
3
  open Reventless.Plugin
4
4
 
5
- // Alias, not `open`: the PPX strips `@transition` before the typechecker, so the
6
- // state constructors never reach it and an `open` would warn as unused.
5
+ // Alias, not `open`: the lifecycle states are named qualified in
6
+ // `commandTransition` below, and an `open` would pull the whole view's namespace
7
+ // in for four constructors.
7
8
  module Plugins = PluginsReadModelSpec
8
9
 
9
10
  // The Plugin aggregate is keyed by plugin **name** (not name@version). One
@@ -15,10 +16,7 @@ module Plugins = PluginsReadModelSpec
15
16
  // more same-shaped ones, so the `pluginDefinition` pair must lead (DZakh/sury#392).
16
17
  @schema
17
18
  type command =
18
- // The handshake brings the version's row into being, so it runs from no state
19
- // — `decide` accepts it against an unknown version and against every status
20
- // but `Connected`, where it is a definition refresh rather than a move.
21
- | @noApi @transition(() => Plugins.Connected) Connect(pluginDefinition)
19
+ | @noApi Connect(pluginDefinition)
22
20
  // Records a protocol-version incompatibility without changing connection state.
23
21
  | @noApi ReportIncompatibility(pluginDefinition)
24
22
  | @noApi Heartbeat(version)
@@ -26,28 +24,20 @@ type command =
26
24
  // re-run for an already-connected version so its definition is refreshed on the
27
25
  // row — unlike Heartbeat, which no-ops a connected keep-alive.
28
26
  | @noApi Redetect(version)
29
- // A heartbeat timing out is the only way a row reaches `Disconnected`, and
30
- // `decide` moves the row for exactly one status — anything else is a stray
31
- // disconnect it tolerates without emitting.
32
- | @noApi @transition(([Plugins.Connected]) => Plugins.Disconnected) Disconnect(version)
27
+ | @noApi Disconnect(version)
33
28
  // Admin lifecycle commands — API-exposed (auto-derived admin mutations,
34
29
  // Cognito group-gated). `version` selects which known version to act on.
35
- // The `@transition` edges must agree with `decide` below; `Platform_Admin_Structure`
36
- // reads them off this schema rather than restating them.
37
30
  //
38
31
  // Inline records, unlike the `@noApi` commands above, and the difference is the
39
32
  // API. A positional payload publishes its argument as `_0`, which names nothing:
40
33
  // a caller cannot match it against a field it holds, and a coercion failure says
41
34
  // `Variable '_0'` rather than what was missing. These three are the ones a person
42
35
  // calls, so their argument carries its own name.
43
- | @transition(([Plugins.Inactive, Plugins.Retired]) => Plugins.Connected)
44
- Activate({version: version})
45
- | @transition(([Plugins.Connected, Plugins.Disconnected]) => Plugins.Inactive)
46
- Deactivate({version: version})
36
+ | Activate({version: version})
37
+ | Deactivate({version: version})
47
38
  // Manual admin retirement of a specific version (archive). Distinct from the
48
39
  // automatic `Superseded` transition (which is decided, not commanded).
49
- | @transition(([Plugins.Connected, Plugins.Disconnected, Plugins.Inactive]) => Plugins.Retired)
50
- Retire({version: version})
40
+ | Retire({version: version})
51
41
 
52
42
  // Carries both the superseded and the superseding version's full definition —
53
43
  // a deterministic trigger for version-to-version schema/data migrations
@@ -91,3 +81,28 @@ type error =
91
81
  | AlreadyConnected
92
82
  | IsDisconnected
93
83
  | IsInactive
84
+
85
+ // The edges must agree with `decide`; `Platform_Admin_Structure` reads them off
86
+ // the assembled structure rather than restating them.
87
+ type lifecycleState = Plugins.status
88
+
89
+ let commandTransition = (command: command): Reventless.Transition.t<lifecycleState> => {
90
+ open Reventless.Transition
91
+ switch command {
92
+ // The handshake brings the version's row into being, so it runs from no state
93
+ // — `decide` accepts it against an unknown version and against every status
94
+ // but `Connected`, where it is a definition refresh rather than a move.
95
+ | Connect(_) => Creates(Plugins.Connected)
96
+ // Neither records a move: an incompatibility is noted beside the row, and a
97
+ // heartbeat or a re-detect refreshes one that is already where it belongs.
98
+ | ReportIncompatibility(_) | Heartbeat(_) | Redetect(_) => Unrestricted
99
+ // A heartbeat timing out is the only way a row reaches `Disconnected`, and
100
+ // `decide` moves the row for exactly one status — anything else is a stray
101
+ // disconnect it tolerates without emitting.
102
+ | Disconnect(_) => Moves([Plugins.Connected], Plugins.Disconnected)
103
+ | Activate(_) => Moves([Plugins.Inactive, Plugins.Retired], Plugins.Connected)
104
+ | Deactivate(_) => Moves([Plugins.Connected, Plugins.Disconnected], Plugins.Inactive)
105
+ | Retire(_) =>
106
+ Moves([Plugins.Connected, Plugins.Disconnected, Plugins.Inactive], Plugins.Retired)
107
+ }
108
+ }
@@ -92,6 +92,52 @@ let errorSchema = Sury.union([
92
92
  Sury.literal("IsInactive")
93
93
  ]);
94
94
 
95
+ function commandTransition(command) {
96
+ switch (command.TAG) {
97
+ case "Connect" :
98
+ return {
99
+ TAG: "Creates",
100
+ _0: "Connected"
101
+ };
102
+ case "Disconnect" :
103
+ return {
104
+ TAG: "Moves",
105
+ _0: ["Connected"],
106
+ _1: "Disconnected"
107
+ };
108
+ case "Activate" :
109
+ return {
110
+ TAG: "Moves",
111
+ _0: [
112
+ "Inactive",
113
+ "Retired"
114
+ ],
115
+ _1: "Connected"
116
+ };
117
+ case "Deactivate" :
118
+ return {
119
+ TAG: "Moves",
120
+ _0: [
121
+ "Connected",
122
+ "Disconnected"
123
+ ],
124
+ _1: "Inactive"
125
+ };
126
+ case "Retire" :
127
+ return {
128
+ TAG: "Moves",
129
+ _0: [
130
+ "Connected",
131
+ "Disconnected",
132
+ "Inactive"
133
+ ],
134
+ _1: "Retired"
135
+ };
136
+ default:
137
+ return "Unrestricted";
138
+ }
139
+ }
140
+
95
141
  let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, [
96
142
  "Connect",
97
143
  "ReportIncompatibility",
@@ -100,66 +146,10 @@ let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, [
100
146
  "Disconnect"
101
147
  ]);
102
148
 
103
- let commandSchema$2 = Api$ReventlessInfra.markAllowedStates(commandSchema$1, [
104
- [
105
- "Disconnect",
106
- ["Connected"]
107
- ],
108
- [
109
- "Activate",
110
- [
111
- "Inactive",
112
- "Retired"
113
- ]
114
- ],
115
- [
116
- "Deactivate",
117
- [
118
- "Connected",
119
- "Disconnected"
120
- ]
121
- ],
122
- [
123
- "Retire",
124
- [
125
- "Connected",
126
- "Disconnected",
127
- "Inactive"
128
- ]
129
- ]
130
- ]);
131
-
132
- let commandSchema$3 = Api$ReventlessInfra.markTargetState(commandSchema$2, [
133
- [
134
- "Connect",
135
- "Connected"
136
- ],
137
- [
138
- "Disconnect",
139
- "Disconnected"
140
- ],
141
- [
142
- "Activate",
143
- "Connected"
144
- ],
145
- [
146
- "Deactivate",
147
- "Inactive"
148
- ],
149
- [
150
- "Retire",
151
- "Retired"
152
- ]
153
- ]);
154
-
155
149
  function commandAuthorization(param) {
156
150
  return "AllowAuthenticated";
157
151
  }
158
152
 
159
- function commandTransition(param) {
160
- return "Unrestricted";
161
- }
162
-
163
153
  let traits = [];
164
154
 
165
155
  let name = "Plugin";
@@ -177,10 +167,10 @@ export {
177
167
  versionSupersededDataSchema,
178
168
  eventSchema,
179
169
  errorSchema,
180
- commandSchema$3 as commandSchema,
170
+ commandTransition,
171
+ commandSchema$1 as commandSchema,
181
172
  moduleUrl,
182
173
  commandAuthorization,
183
- commandTransition,
184
174
  traits,
185
175
  }
186
176
  /* commandSchema Not a pure module */
@@ -304,11 +304,11 @@ describe("lifecycle topology", () => {
304
304
  })
305
305
 
306
306
  describe("commandTransition — the edge as a switch", () => {
307
- // The seam a variant spread needs. `@transition` lowers to a dict on the
308
- // parent union keyed by variant name, and a spread splices members, so a
309
- // command a host did not declare cannot be annotated at all. A switch over the
310
- // command reaches every constructor, spliced or not, and the compiler makes
311
- // it exhaustive.
307
+ // The seam a variant spread needs. The removed attribute lowered to a dict on
308
+ // the parent union keyed by variant name, and a spread splices members, so a
309
+ // command a host did not declare could not be annotated at all. A switch over
310
+ // the command reaches every constructor, spliced or not, and the compiler
311
+ // makes it exhaustive.
312
312
  let structure = Plugin_Structure.make(
313
313
  ~name="TransitionSwitchPlugin",
314
314
  ~stateChangeSlices=[module(PsTransitionSwitchSlice)],
@@ -325,21 +325,15 @@ describe("commandTransition — the edge as a switch", () => {
325
325
  })
326
326
 
327
327
  testSync("a guard declares its from-set and no target", () => {
328
- // The same positive claim the one-sided `@transition` makes: legal in these
329
- // states, and it moves the row nowhere.
328
+ // A positive claim rather than an omission: legal in these states, and it
329
+ // moves the row nowhere. Distinguishable on the wire from both fields being
330
+ // None, and consumers are expected to honour it.
330
331
  expect((
331
332
  byName("Rebook")->Option.flatMap(c => c.allowedStates),
332
333
  byName("Rebook")->Option.flatMap(c => c.targetState),
333
334
  ))->toEqual((Some(["Booked"]), None))
334
335
  })
335
336
 
336
- testSync("the switch wins over an @transition on the same command", () => {
337
- // `Rebook` carries both, and they disagree on purpose: the attribute says
338
- // Draft. Two sources of truth need a stated winner, and it is the one the
339
- // compiler checked.
340
- expect(byName("Rebook")->Option.flatMap(c => c.allowedStates))->toEqual(Some(["Booked"]))
341
- })
342
-
343
337
  testSync("Unrestricted declares nothing, leaving the command unconstrained", () => {
344
338
  // Distinct from a from-set naming every state, which says the same thing to
345
339
  // a menu and something different to a reader.
@@ -384,12 +378,14 @@ describe("traitDeclarations — a graft leaves a trace", () => {
384
378
  })
385
379
  })
386
380
 
387
- describe("@transition cross-check", () => {
381
+ describe("declared-edge cross-check", () => {
388
382
  // The exit criterion: a state no linked view declares stops the build, and the
389
383
  // message says which command, which state, and what it was checked against.
390
384
  // `DispatchShipment` produces `ShipmentDispatched`, which the `Shipments` view
391
385
  // consumes — so the two are linked, the view's lifecycle is in hand, and
392
- // "Dispatchd" has somewhere to be wrong.
386
+ // "Dispatchd" has somewhere to be wrong. It compiles because the switch names
387
+ // the slice's OWN enum: the typechecker cannot see that the wrong lifecycle
388
+ // was picked, and this is the check that can.
393
389
  let buildBad = () =>
394
390
  try {
395
391
  Plugin_Structure.make(
@@ -504,11 +500,10 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
504
500
  ))->toEqual((Some(true), Some(false)))
505
501
  })
506
502
 
507
- testSync("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
508
- // End-to-end: the reventless-ppx @transition annotation → markTargetState
509
- // metadata → ApiTargetStateHelpers.getTargetState → commandDef. The
510
- // un-annotated CancelShipment carries None (resolver falls back to
511
- // name-stem).
503
+ testSync("ShipOrder: the switch's target flows through to commandDef.targetState", () => {
504
+ // End-to-end: the spec's `commandTransition`, evaluated per constructor
505
+ // against a synthetic command → commandDef. `CancelShipment` answers
506
+ // `Unrestricted` and carries None (resolver falls back to name-stem).
512
507
  let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
513
508
  let byName = name => shipOrder.commands->Array.find(c => c.name == name)
514
509
  expect((
@@ -517,10 +512,10 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
517
512
  ))->toEqual((Some("Shipped"), None))
518
513
  })
519
514
 
520
- testSync("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
521
- // The other half of the same annotation, which the removed pair spelled
522
- // separately: one attribute now fills both fields, so a command that
523
- // declares a target cannot end up without the states it may run from.
515
+ testSync("ShipOrder: the switch's from-set flows through to commandDef.allowedStates", () => {
516
+ // The other half of the same declaration. An edge is one value, so a
517
+ // command that declares a target cannot end up without the states it may
518
+ // run from — the two fields are read off it together.
524
519
  let shipOrder = structure.stateChangeSlices->Array.getUnsafe(1)
525
520
  let byName = name => shipOrder.commands->Array.find(c => c.name == name)
526
521
  expect((
@@ -602,9 +602,6 @@ globalThis.describe("commandTransition — the edge as a switch", () => {
602
602
  undefined
603
603
  ]);
604
604
  });
605
- globalThis.test("the switch wins over an @transition on the same command", () => {
606
- globalThis.expect(Stdlib_Option.flatMap(byName("Rebook"), c => c.allowedStates)).toEqual(["Booked"]);
607
- });
608
605
  globalThis.test("Unrestricted declares nothing, leaving the command unconstrained", () => {
609
606
  globalThis.expect([
610
607
  Stdlib_Option.flatMap(byName("Abandon"), c => c.allowedStates),
@@ -646,7 +643,7 @@ globalThis.describe("traitDeclarations — a graft leaves a trace", () => {
646
643
  });
647
644
  });
648
645
 
649
- globalThis.describe("@transition cross-check", () => {
646
+ globalThis.describe("declared-edge cross-check", () => {
650
647
  let buildBad = () => {
651
648
  try {
652
649
  Plugin_Structure$ReventlessCore.make("TransitionPlugin", undefined, undefined, [PsShipmentsViewSlice], [PsDispatchShipmentSlice], undefined, undefined, undefined, undefined, undefined, undefined);
@@ -759,7 +756,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
759
756
  false
760
757
  ]);
761
758
  });
762
- globalThis.test("ShipOrder: @transition's target flows through the PPX to commandDef.targetState", () => {
759
+ globalThis.test("ShipOrder: the switch's target flows through to commandDef.targetState", () => {
763
760
  let shipOrder = structure.stateChangeSlices[1];
764
761
  let byName = name => shipOrder.commands.find(c => c.name === name);
765
762
  globalThis.expect([
@@ -770,7 +767,7 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
770
767
  undefined
771
768
  ]);
772
769
  });
773
- globalThis.test("ShipOrder: @transition's from-set flows through to commandDef.allowedStates", () => {
770
+ globalThis.test("ShipOrder: the switch's from-set flows through to commandDef.allowedStates", () => {
774
771
  let shipOrder = structure.stateChangeSlices[1];
775
772
  let byName = name => shipOrder.commands.find(c => c.name === name);
776
773
  globalThis.expect([
@@ -1,15 +1,16 @@
1
- // Test fixture spec for the @transition cross-check — the failing case.
1
+ // Test fixture spec for the declared-edge cross-check — the failing case.
2
2
  //
3
- // `Dispatchd` is a deliberate typo. It compiles: the PPX strips the attribute
4
- // before the typechecker sees it, so a misspelled state is a well-formed string
5
- // all the way down. That is exactly the hole the structure-side check closes,
6
- // and a fixture that could not compile would not demonstrate it.
3
+ // The switch names constructors of THIS file's own enum, and `Dispatchd` is a
4
+ // real case of it, so the compiler is satisfied. The linked `Shipments` view
5
+ // declares `Booked | Dispatched`, and nothing forces a spec to pick its linked
6
+ // view's enum — which is the hole the structure-side check closes, and the half
7
+ // the typechecker cannot reach.
7
8
 
8
9
  @@reventless.spec("DispatchShipment")
9
10
 
10
11
  type shipmentStatus =
11
12
  | Booked
12
- | Dispatched
13
+ | Dispatchd
13
14
 
14
15
  @schema
15
16
  type consumedEvent = ShipmentBooked({shipmentId: string})
@@ -20,7 +21,7 @@ type state = bool
20
21
  let initialState = false
21
22
 
22
23
  @schema
23
- type command = @transition(([Booked]) => Dispatchd) DispatchShipment({shipmentId: string})
24
+ type command = DispatchShipment({shipmentId: string})
24
25
 
25
26
  @schema
26
27
  type error = ShipmentNotFound
@@ -29,3 +30,12 @@ type error = ShipmentNotFound
29
30
  type event = ShipmentDispatched({shipmentId: string})
30
31
 
31
32
  let decide = (_state, _command): result<array<event>, error> => Ok([])
33
+
34
+ type lifecycleState = shipmentStatus
35
+
36
+ let commandTransition = (command: command): Reventless.Transition.t<lifecycleState> => {
37
+ open Reventless.Transition
38
+ switch command {
39
+ | DispatchShipment(_) => Moves([Booked], Dispatchd)
40
+ }
41
+ }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Sury from "sury";
4
4
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
- import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
6
5
 
7
6
  let consumedEventSchema = Sury.$schema(s => ({
8
7
  TAG: "ShipmentBooked",
@@ -32,24 +31,18 @@ function decide(_state, _command) {
32
31
  };
33
32
  }
34
33
 
35
- let commandSchema$1 = Api$ReventlessInfra.markAllowedStates(commandSchema, [[
36
- "DispatchShipment",
37
- ["Booked"]
38
- ]]);
39
-
40
- let commandSchema$2 = Api$ReventlessInfra.markTargetState(commandSchema$1, [[
41
- "DispatchShipment",
42
- "Dispatchd"
43
- ]]);
34
+ function commandTransition(command) {
35
+ return {
36
+ TAG: "Moves",
37
+ _0: ["Booked"],
38
+ _1: "Dispatchd"
39
+ };
40
+ }
44
41
 
45
42
  function commandAuthorization(param) {
46
43
  return "AllowAuthenticated";
47
44
  }
48
45
 
49
- function commandTransition(param) {
50
- return "Unrestricted";
51
- }
52
-
53
46
  let traits = [];
54
47
 
55
48
  let name = "DispatchShipment";
@@ -68,14 +61,14 @@ export {
68
61
  consumedEventSchema,
69
62
  evolve,
70
63
  initialState,
64
+ commandSchema,
71
65
  errorSchema,
72
66
  eventSchema,
73
67
  decide,
74
- commandSchema$2 as commandSchema,
68
+ commandTransition,
75
69
  moduleUrl,
76
70
  commandAuthorization,
77
71
  readConsistency,
78
- commandTransition,
79
72
  traits,
80
73
  }
81
74
  /* consumedEventSchema Not a pure module */
@@ -13,27 +13,21 @@ type consumedEvent = OrderPlaced | OrderShipped | OrderCancelled
13
13
 
14
14
  let evolve = (_state, _event) => true
15
15
 
16
- // CancelShipment is payload-less — it compiles to a bare string literal, so it
17
- // exercises toCommandDef's payload-less branch (still surfaced in `commands`).
18
- // It is also `@noApi` (internal/admin only): a two-variant slice with exactly
19
- // one API-exposed command, which is the case that used to leak the exposed
20
- // command's slice mutation field onto the non-exposed variant.
21
- // The lifecycle this slice's commands move a shipment through. The PPX strips
22
- // @transition before the typechecker sees it, so this enum is not what makes the
23
- // reference below valid — the structure-side check against the linked view is.
24
- // It is here because a state is a constructor, and naming one in a string is a
25
- // reference nothing can check at any layer.
16
+ // The lifecycle this slice's commands move a shipment through. Declared here
17
+ // rather than on a linked view because this fixture's plugin has none: the
18
+ // structure-side check warns instead of failing when a component's views declare
19
+ // no lifecycle, and that is the path this exercises.
26
20
  type shipmentStatus = Placed | Shipped | Cancelled
27
21
 
28
22
  // CancelShipment is payload-less — it compiles to a bare string literal, so it
29
23
  // exercises toCommandDef's payload-less branch (still surfaced in `commands`).
30
24
  // It is also `@noApi` (internal/admin only): a two-variant slice with exactly
31
25
  // one API-exposed command, which is the case that used to leak the exposed
32
- // command's slice mutation field onto the non-exposed variant. It carries no
33
- // @transition, which is the un-annotated case the resolver still guesses for.
26
+ // command's slice mutation field onto the non-exposed variant. It declares no
27
+ // edge, which is the case the board resolver still guesses for.
34
28
  @schema
35
29
  type command =
36
- | @transition(([Placed]) => Shipped) ShipOrder({orderId: string})
30
+ | ShipOrder({orderId: string})
37
31
  | @noApi CancelShipment
38
32
 
39
33
  // Two variants, one payload-less and one carrying a field — the same two branches
@@ -47,3 +41,13 @@ type error = OrderNotFound | NotShippable({reason: string})
47
41
  type event = OrderShipped({orderId: string}) | ShipmentVoided
48
42
 
49
43
  let decide = (_state, _command): result<array<event>, error> => Ok([])
44
+
45
+ type lifecycleState = shipmentStatus
46
+
47
+ let commandTransition = (command: command): Reventless.Transition.t<lifecycleState> => {
48
+ open Reventless.Transition
49
+ switch command {
50
+ | ShipOrder(_) => Moves([Placed], Shipped)
51
+ | CancelShipment => Unrestricted
52
+ }
53
+ }
@@ -45,26 +45,24 @@ function decide(_state, _command) {
45
45
  };
46
46
  }
47
47
 
48
- let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, ["CancelShipment"]);
49
-
50
- let commandSchema$2 = Api$ReventlessInfra.markAllowedStates(commandSchema$1, [[
51
- "ShipOrder",
52
- ["Placed"]
53
- ]]);
48
+ function commandTransition(command) {
49
+ if (typeof command !== "object") {
50
+ return "Unrestricted";
51
+ } else {
52
+ return {
53
+ TAG: "Moves",
54
+ _0: ["Placed"],
55
+ _1: "Shipped"
56
+ };
57
+ }
58
+ }
54
59
 
55
- let commandSchema$3 = Api$ReventlessInfra.markTargetState(commandSchema$2, [[
56
- "ShipOrder",
57
- "Shipped"
58
- ]]);
60
+ let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, ["CancelShipment"]);
59
61
 
60
62
  function commandAuthorization(param) {
61
63
  return "AllowAuthenticated";
62
64
  }
63
65
 
64
- function commandTransition(param) {
65
- return "Unrestricted";
66
- }
67
-
68
66
  let traits = [];
69
67
 
70
68
  let name = "ShipOrder";
@@ -86,11 +84,11 @@ export {
86
84
  errorSchema,
87
85
  eventSchema,
88
86
  decide,
89
- commandSchema$3 as commandSchema,
87
+ commandTransition,
88
+ commandSchema$1 as commandSchema,
90
89
  moduleUrl,
91
90
  commandAuthorization,
92
91
  readConsistency,
93
- commandTransition,
94
92
  traits,
95
93
  }
96
94
  /* consumedEventSchema Not a pure module */
@@ -1,18 +1,13 @@
1
- // Test fixture spec for `commandTransition` — the edge declared as a switch
2
- // rather than as an attribute.
1
+ // Test fixture spec for `commandTransition` — the three shapes of a declared
2
+ // edge, in one command type:
3
3
  //
4
- // Three cases in one command type:
5
- //
6
- // `Book` declares only through the switch, which is the case
7
- // `@transition` cannot serve for a spliced constructor.
8
- // `Rebook` carries BOTH. The switch says `Booked`, the attribute says
9
- // `Draft`; the switch is what must reach the commandDef, because
10
- // two sources of truth need a stated winner.
11
- // `Abandon` declares nothing in either, and must stay unconstrained.
4
+ // `Book` moves the row: a from-set and a target.
5
+ // `Rebook` guards only: a from-set and no target, which is the positive
6
+ // claim that the command moves the row nowhere.
7
+ // `Abandon` declares nothing, and must stay unconstrained.
12
8
  //
13
9
  // The states go in as constructors of this file's own enum, so the compiler
14
- // resolves them. `@transition` on `Rebook` cannot — it is stripped before the
15
- // typechecker runs, which is why `PsDispatchShipment` can carry a typo at all.
10
+ // resolves them.
16
11
 
17
12
  @@reventless.spec("TransitionSwitch")
18
13
 
@@ -29,7 +24,7 @@ let initialState = false
29
24
  @schema
30
25
  type command =
31
26
  | Book({bookingId: string})
32
- | @transition(([Draft]) => Draft) Rebook({bookingId: string})
27
+ | Rebook({bookingId: string})
33
28
  | Abandon({bookingId: string})
34
29
 
35
30
  @schema
@@ -2,7 +2,6 @@
2
2
 
3
3
  import * as Sury from "sury";
4
4
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
- import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
6
5
 
7
6
  let consumedEventSchema = Sury.$schema(s => ({
8
7
  TAG: "BookingDrafted",
@@ -72,16 +71,6 @@ let traits = [{
72
71
  posture: "SelfContained"
73
72
  }];
74
73
 
75
- let commandSchema$1 = Api$ReventlessInfra.markAllowedStates(commandSchema, [[
76
- "Rebook",
77
- ["Draft"]
78
- ]]);
79
-
80
- let commandSchema$2 = Api$ReventlessInfra.markTargetState(commandSchema$1, [[
81
- "Rebook",
82
- "Draft"
83
- ]]);
84
-
85
74
  function commandAuthorization(param) {
86
75
  return "AllowAuthenticated";
87
76
  }
@@ -102,12 +91,12 @@ export {
102
91
  consumedEventSchema,
103
92
  evolve,
104
93
  initialState,
94
+ commandSchema,
105
95
  errorSchema,
106
96
  eventSchema,
107
97
  decide,
108
98
  commandTransition,
109
99
  traits,
110
- commandSchema$2 as commandSchema,
111
100
  moduleUrl,
112
101
  commandAuthorization,
113
102
  readConsistency,
@@ -1,4 +1,4 @@
1
- // Test fixture spec for the @transition cross-check.
1
+ // Test fixture spec for the declared-edge cross-check.
2
2
  // A view that declares a lifecycle, so a command's declared states have
3
3
  // something to be checked against. `PsOrdersView` deliberately declares none —
4
4
  // the two together cover both the checked and the unvalidated path.
@@ -1,18 +0,0 @@
1
- // Read per-variant `allowedStates` metadata that the reventless-ppx
2
- // an `@transition` from-set attaches to a command schema. Consumed by
3
- // `Plugin_Structure.toCommandDef` when building the `commandDef` records that
4
- // land in `Platform_ComponentDefinitions` so AutoUI can filter the per-row command
5
- // menu by the row's status.
6
-
7
- open ReventlessInfra.Api
8
-
9
- /** Returns the allowed-state list for a given command-variant name, or None
10
- when the variant declares no `@transition` (the command shows on every row). */
11
- let getAllowedStates = (
12
- commandSchema: S.t<unknown>,
13
- ~variantName: string,
14
- ): option<array<string>> =>
15
- switch commandSchema->S.Metadata.get(~id=allowedStatesId) {
16
- | None => None
17
- | Some(entries) => entries->Dict.get(variantName)
18
- }
@@ -1,16 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
- import * as Sury from "sury";
4
- import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
5
-
6
- function getAllowedStates(commandSchema, variantName) {
7
- let entries = Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.allowedStatesId);
8
- if (entries !== undefined) {
9
- return entries[variantName];
10
- }
11
- }
12
-
13
- export {
14
- getAllowedStates,
15
- }
16
- /* sury Not a pure module */
@@ -1,21 +0,0 @@
1
- // Read per-variant `targetState` metadata that the reventless-ppx
2
- // an `@transition` target attaches to a command schema. Consumed by
3
- // `Plugin_Structure.toCommandDef` when building the `commandDef` records that
4
- // land in `Platform_ComponentDefinitions`, so AutoUI's board drag resolver can
5
- // move a row by a *declared* transition instead of a name-stem guess. Mirrors
6
- // `ApiAllowedStatesHelpers.getAllowedStates`.
7
-
8
- open ReventlessInfra.Api
9
-
10
- /** Returns the declared target state for a given command-variant name, or None
11
- when the variant declares no target — either because it carries no
12
- `@transition` at all, or because it declares a from-set only, which is a
13
- command saying it does not move the row. */
14
- let getTargetState = (
15
- commandSchema: S.t<unknown>,
16
- ~variantName: string,
17
- ): option<string> =>
18
- switch commandSchema->S.Metadata.get(~id=targetStateId) {
19
- | None => None
20
- | Some(entries) => entries->Dict.get(variantName)
21
- }
@@ -1,16 +0,0 @@
1
- // Generated by ReScript, PLEASE EDIT WITH CARE
2
-
3
- import * as Sury from "sury";
4
- import * as Api$ReventlessInfra from "@reventlessdev/reventless-infra/src/components/Api.res.mjs";
5
-
6
- function getTargetState(commandSchema, variantName) {
7
- let entries = Sury.$Metadata_get(commandSchema, Api$ReventlessInfra.targetStateId);
8
- if (entries !== undefined) {
9
- return entries[variantName];
10
- }
11
- }
12
-
13
- export {
14
- getTargetState,
15
- }
16
- /* sury Not a pure module */