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

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.
Files changed (39) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +17 -3
  3. package/package.json +5 -4
  4. package/run-rename-to-decision.mjs +3 -0
  5. package/src/AnsiStyle.res +3 -3
  6. package/src/CliArgs.res +1 -1
  7. package/src/LogPrefix.res +1 -1
  8. package/src/codemod/RenameToDecision.res +207 -0
  9. package/src/codemod/RenameToDecision.res.mjs +334 -0
  10. package/src/components/Aggregate.res +1 -1
  11. package/src/components/CapabilityManifest.res +1 -1
  12. package/src/components/ComponentKind.res +2 -2
  13. package/src/components/ComponentKind.res.mjs +1 -1
  14. package/src/components/DcbScopeInference.res +2 -2
  15. package/src/components/DcbTag.res +5 -5
  16. package/src/components/OutboundTranslationSlice.res +2 -2
  17. package/src/components/Plugin.res +2 -2
  18. package/src/components/Sensitive.res +1 -1
  19. package/src/components/Snapshot.res +5 -5
  20. package/src/components/StateChangeSlice.res +6 -5
  21. package/src/components/TaggedUnion.res +1 -1
  22. package/src/generator/Codegen.res +5 -5
  23. package/src/generator/Codegen.res.mjs +1 -1
  24. package/src/generator/Discovery.res +3 -3
  25. package/src/generator/Pairing.res +47 -18
  26. package/src/generator/Pairing.res.mjs +41 -10
  27. package/src/lifecycle/CheckLifecycleModel.res +3 -3
  28. package/src/semantic/Capabilities.res +2 -2
  29. package/src/semantic/CapabilityNeed.res +3 -3
  30. package/src/semantic/Color.res +8 -8
  31. package/src/semantic/IdentityProvider.res +1 -1
  32. package/src/semantic/Messaging.res +2 -2
  33. package/src/types/{Behavior.res → Decision.res} +10 -6
  34. package/src/types/Message.res +1 -1
  35. package/src/types/OwnerScope.res +5 -5
  36. package/src/types/ShellEnvironment.res +39 -0
  37. package/src/types/ShellEnvironment.res.mjs +39 -0
  38. package/src/types/Trait.res +1 -1
  39. /package/src/types/{Behavior.res.mjs → Decision.res.mjs} +0 -0
@@ -2,7 +2,7 @@
2
2
  Module type for an aggregate's identity and schema specification.
3
3
 
4
4
  Every aggregate defines a `Spec` module satisfying this type. The `Spec` is
5
- used by `Platform.Aggregate.Make` and by `Behavior.T` to fix the aggregate's
5
+ used by `Platform.Aggregate.Make` and by `Decision.T` to fix the aggregate's
6
6
  command, event, and error types.
7
7
 
8
8
  @example
@@ -76,7 +76,7 @@ let fromStructure = (structure: Plugin.pluginStructure): t => {
76
76
  let capabilityKeys =
77
77
  needs->Array.map(d => d.capability)->Belt.Set.String.fromArray->Belt.Set.String.toArray
78
78
  let capabilities = capabilityKeys->Array.filterMap(key =>
79
- // An unrecognised capability is dropped rather than passed through: the
79
+ // An unrecognized capability is dropped rather than passed through: the
80
80
  // generator downstream renders a real `Platform.capability` arm, and a name
81
81
  // this build cannot map has no arm to render.
82
82
  CapabilityNeed.fromString(key)->Option.map(need => {
@@ -4,7 +4,7 @@
4
4
  // (`generator/Discovery`) and the external tooling that classifies files by
5
5
  // folder (`reventless-gwt`'s `ComponentMeta`) both derive from this, so the two
6
6
  // can't drift — previously the generator accepted plural/short folder names that
7
- // the gwt discovery silently didn't recognise.
7
+ // the gwt discovery silently didn't recognize.
8
8
 
9
9
  type t =
10
10
  | StateChangeSlice
@@ -99,7 +99,7 @@ let bodySuffixes = [
99
99
  "_Projections",
100
100
  "_Projection",
101
101
  "_Mappings",
102
- "_Behavior",
102
+ "_Decision",
103
103
  "_Automation",
104
104
  "_Translation",
105
105
  ]
@@ -112,7 +112,7 @@ let bodySuffixes = [
112
112
  "_Projections",
113
113
  "_Projection",
114
114
  "_Mappings",
115
- "_Behavior",
115
+ "_Decision",
116
116
  "_Automation",
117
117
  "_Translation"
118
118
  ];
@@ -46,10 +46,10 @@ The four rules (over the representation):
46
46
  and this module stays schema-agnostic by taking the name alone.
47
47
 
48
48
  `byTag` marks the exception: an identity the *name* does not declare, which an
49
- adapter recognised from an explicit `@partitionTag`. A domain's own identifier
49
+ adapter recognized from an explicit `@partitionTag`. A domain's own identifier
50
50
  is often not suffixed — `sku`, `isbn`, `vin` — and without this the annotation
51
51
  naming one would be extracted as a hint and then dropped, because `seedOf`
52
- only honours a hint already among the produced keys. So the escape hatch would
52
+ only honors a hint already among the produced keys. So the escape hatch would
53
53
  have worked for every field except the ones that need it.
54
54
 
55
55
  It is carried rather than folded in because removing the annotation removes
@@ -100,7 +100,7 @@ let dcbPartitionTagId: S.Metadata.Id.t<bool> = S.Metadata.Id.make(
100
100
  Internal sury metadata ID marking a tag field as *cross-partition* — readable
101
101
  across every partition that carries it (a secondary-tag read), not just the
102
102
  partition it is the partition key of. Opt-in; absence = the default
103
- partition-scoped behaviour. The scope is a property of the tag *key* and must
103
+ partition-scoped behavior. The scope is a property of the tag *key* and must
104
104
  agree across every event type that carries it (the fence-scope half depends on
105
105
  it). See `docs/analysis/dcb-consistency-check-issues.md` Issue 13.
106
106
  */
@@ -497,7 +497,7 @@ tag the catalog's own events write. A key derived from `lineItems` would name a
497
497
  tag no producer has ever written, and the decision read would come back empty —
498
498
  the identical symptom to extracting no tags at all, from a different cause. An
499
499
  author who wants another key still writes `DcbTag.stringForKey` on the nested
500
- field, which `resolveTagKey` honours here as it does at the top level.
500
+ field, which `resolveTagKey` honors here as it does at the top level.
501
501
  */
502
502
  let nestedRecordTags = (fieldSchema: S.t<unknown>, jsonValue: JSON.t): array<tag> =>
503
503
  switch nestedRecordProperties(fieldSchema) {
@@ -655,7 +655,7 @@ let extractVariantNames = (schema: S.t<'a>): array<string> => {
655
655
  // intentionally excluded here: DCB event-type lookups can't WHERE-clause
656
656
  // on bare-string events, so Plugin_Structure.consumedEventTypes and
657
657
  // related graph fields would otherwise claim cross-component edges the
658
- // runtime can't honour. Callers that need every constructor (e.g.
658
+ // runtime can't honor. Callers that need every constructor (e.g.
659
659
  // mapping-mode `acceptedTags` filters that match a JSON envelope's
660
660
  // `event` TAG against the Delegate's full constructor set) should call
661
661
  // [`extractAllVariantNames`] instead.
@@ -729,7 +729,7 @@ let isVariantPayloadBearing = (schema: S.t<'a>, name: string): bool => {
729
729
  Extracts tags from a flat JSON object, expanding array values into per-element tags.
730
730
 
731
731
  For scalar tagged fields, behaves identically to `extractTagsFromProperties`.
732
- For array tagged fields, produces one tag per element. Tag keys honour an optional
732
+ For array tagged fields, produces one tag per element. Tag keys honor an optional
733
733
  `DcbTag.stringForKey(~key=...)` override on the (inner) schema; otherwise the field
734
734
  name is used.
735
735
 
@@ -1123,7 +1123,7 @@ let idFieldsOfProperties = (properties: dict<S.t<unknown>>): array<DcbScopeInfer
1123
1123
  let isIdName = (name: string) => name->String.endsWith("Ids") || name->String.endsWith("Id")
1124
1124
  // An explicit `@partitionTag` declares an identity the naming convention cannot
1125
1125
  // express — a domain's own identifier is often unsuffixed (`sku`, `isbn`).
1126
- // Without this the hint is extracted and then ignored, since `seedOf` honours
1126
+ // Without this the hint is extracted and then ignored, since `seedOf` honors
1127
1127
  // only a hint already among the produced keys, so the escape hatch would work
1128
1128
  // for every field except the ones that need it. Flagged, not folded in: taking
1129
1129
  // the annotation away takes the identity with it, which is what the redundancy
@@ -76,14 +76,14 @@ module type Spec = {
76
76
  /**
77
77
  Event sources this slice subscribes to, by topic key.
78
78
 
79
- `[]` — the default and the historical behaviour — means this plugin's own DCB
79
+ `[]` — the default and the historical behavior — means this plugin's own DCB
80
80
  event log. Naming sources explicitly subscribes to them instead: an Aggregate's
81
81
  `Spec.name`, or a DCB source name (conventionally `"<pluginName>DcbEventLog"`),
82
82
  matching the keys `AutomationSlice` mappings already use.
83
83
 
84
84
  This exists because an outbound slice is the framework's one component for
85
85
  *calling an external service and feeding the answer back*, and that job is not
86
- specific to DCB-modelled entities. An Aggregate whose events should trigger an
86
+ specific to DCB-modeled entities. An Aggregate whose events should trigger an
87
87
  outbound call had no route to one while this list was hard-wired.
88
88
 
89
89
  Unlike `AutomationSlice`, the sources are a flat list rather than per-source
@@ -451,7 +451,7 @@ whole structure.
451
451
  while it walks the components, because it is the only party that knows which one
452
452
  carried the declaration. Nothing in this record is a string a developer typed.
453
453
 
454
- It records ORIGIN, not behaviour — a grafted file is the host's to edit
454
+ It records ORIGIN, not behavior — a grafted file is the host's to edit
455
455
  afterwards. What answers "does it still behave like the trait" is the trait's own
456
456
  conformance suite, which runs in the consumer's build and is not this field.
457
457
  */
@@ -499,7 +499,7 @@ type pluginStructure = {
499
499
  component. Absent → None, read as []. The only signal a graft leaves that
500
500
  survives into a deployed plugin — every other one (the dependency, the
501
501
  variant spread, the rules alias, the conformance binding) is source-side.
502
- A claim about origin, never about behaviour: see `Trait`. */
502
+ A claim about origin, never about behavior: see `Trait`. */
503
503
  traitDeclarations: option<array<traitDeclaration>>,
504
504
  }
505
505
 
@@ -9,7 +9,7 @@ facts about one field.
9
9
  It is declared where the field is declared, and read by anything that composes
10
10
  text somebody will read. That is the whole reason it lives here rather than
11
11
  beside whichever consumer happens to need it first: a marking that exists in one
12
- consumer is a marking the rest of the system cannot honour, and the domain model
12
+ consumer is a marking the rest of the system cannot honor, and the domain model
13
13
  is the only place that knows which values are sensitive.
14
14
 
15
15
  ## Absent means "not stated", not "safe"
@@ -1,15 +1,15 @@
1
1
  // Per-aggregate persisted-snapshot configuration
2
2
  // (docs/plans/done/aggregate-snapshotting.md).
3
3
  //
4
- // Snapshotting is opt-in and lives on the Behavior module (where `state` is
5
- // defined): `Behavior.T.snapshot = None` (the default, auto-injected by
6
- // `@@reventless.behavior`) keeps full replay; `Some(config)` makes the
4
+ // Snapshotting is opt-in and lives on the Decision module (where `state` is
5
+ // defined): `Decision.T.snapshot = None` (the default, auto-injected by
6
+ // `@@reventless.decision`) keeps full replay; `Some(config)` makes the
7
7
  // aggregate write a keep-one snapshot every `interval` events and seed cold
8
8
  // replays from the latest one. Snapshots are a read optimization only — the
9
9
  // OCC append remains the sole consistency primitive, and a missing, corrupt,
10
10
  // or schema-drifted snapshot degrades to full replay.
11
11
  //
12
- // Enable via the file-level attribute on a behavior file:
12
+ // Enable via the file-level attribute on a decision file:
13
13
  //
14
14
  // @@reventless.snapshots(100)
15
15
  //
@@ -20,7 +20,7 @@
20
20
  type config<'state> = {
21
21
  /** Write a snapshot after every N appended events. Required — no default. */
22
22
  interval: int,
23
- /** Sury schema for the behavior's state — serializes the persisted snapshot
23
+ /** Sury schema for the decision's state — serializes the persisted snapshot
24
24
  and (hashed) gates staleness across state-shape changes. */
25
25
  stateSchema: S.t<'state>,
26
26
  }
@@ -9,7 +9,7 @@ on no concurrent changes to the same entities.
9
9
  Plan 02 splits the merged spec into two module types:
10
10
 
11
11
  - `Spec` — types, identity, schemas. The structural contract.
12
- - `Behavior` — `state`, `initialState`, `evolve`, `decide`. The state machine.
12
+ - `Decision` — `state`, `initialState`, `evolve`, `decide`. The state machine.
13
13
 
14
14
  @example
15
15
  ```rescript
@@ -43,7 +43,7 @@ let decide = (state, command) => switch command {
43
43
  */
44
44
  /**
45
45
  The lean Spec for a StateChangeSlice — types and schemas. State and
46
- state-evolution functions live in the sibling `Behavior` module type. A slice has
46
+ state-evolution functions live in the sibling `Decision` module type. A slice has
47
47
  no identity of its own: it decides about the one its partition field carries.
48
48
  */
49
49
  module type Spec = {
@@ -119,12 +119,13 @@ module type Spec = {
119
119
  }
120
120
 
121
121
  /**
122
- The Behavior — pure state machine that decides commands and folds events.
122
+ The Decision — pure state machine that decides commands and folds events, in
123
+ the shape of the decider pattern. Its file is `<Slice>_Decision.res`.
123
124
 
124
125
  `module Spec: Spec` shares the lean Spec's types so `evolve` references
125
126
  `Spec.consumedEvent`, `decide` references `Spec.command`/`Spec.event`/`Spec.error`.
126
127
  */
127
- module type Behavior = {
128
+ module type Decision = {
128
129
  module Spec: Spec
129
130
 
130
131
  /**
@@ -148,6 +149,6 @@ module type Behavior = {
148
149
  */
149
150
  let decide: (state, Spec.command) => result<array<Spec.event>, Spec.error>
150
151
 
151
- /** File URL of this Behavior module (`import.meta.url`). */
152
+ /** File URL of this Decision module (`import.meta.url`). */
152
153
  let moduleUrl: string
153
154
  }
@@ -19,7 +19,7 @@ the declaration by `named` — the ppx does it for a variant a state record uses
19
19
  and a framework type carrying a union does it beside its `Semantic.mark`.
20
20
 
21
21
  A union with no name is not emitted as one. It falls through to the IR's
22
- `Unknown`, which is a `String` in the SDL — the behaviour that predates this
22
+ `Unknown`, which is a `String` in the SDL — the behavior that predates this
23
23
  module — except that it is now reported at deploy time instead of silently.
24
24
  */
25
25
  let unionNameId: S.Metadata.Id.t<string> = S.Metadata.Id.make(
@@ -67,13 +67,13 @@ let renderAutomationSlices = (stems: array<string>): array<string> =>
67
67
  )
68
68
 
69
69
  let renderAggregates = (aggregates: array<Pairing.aggregateDef>): array<string> =>
70
- aggregates->Array.flatMap(({spec, behavior, eventMappings, isAsync}) => {
70
+ aggregates->Array.flatMap(({spec, decision, eventMappings, isAsync}) => {
71
71
  let em = eventMappings->Option.getOr("ReventlessInfra.NoEventMappings.Make(" ++ spec ++ ")")
72
72
  let factory = isAsync ? "MakeAsync" : "Make"
73
73
  [
74
74
  " module " ++ spec ++ "Aggregate = Platform.Aggregate." ++ factory ++ "(",
75
75
  " " ++ spec ++ ",",
76
- " " ++ behavior ++ ",",
76
+ " " ++ decision ++ ",",
77
77
  " " ++ em ++ ",",
78
78
  " )",
79
79
  ]
@@ -159,7 +159,7 @@ let renderExtensions = (extensions: array<string>): array<string> =>
159
159
  " module " ++ stem ++ " = Platform.Extension.Make(" ++ stem ++ ".Mapping)"
160
160
  )
161
161
 
162
- // AWS-specific renderers were removed in favour of the canonical ones — the
162
+ // AWS-specific renderers were removed in favor of the canonical ones — the
163
163
  // AWS Plugin.res now emits `open <Namespace>` so bare stems resolve, and the
164
164
  // AWS Platform.{Aggregate,ReadModel}.Make functors delegate to the
165
165
  // `*_Builder_Single` strategy under the hood (see reventless-aws/src/Platform.res).
@@ -630,7 +630,7 @@ let renderComposition = (
630
630
  // Header
631
631
  lines->Array.push("// AUTO-GENERATED — do not edit. Run `npm run generate` to update.")
632
632
  // Federation override URL — empty/unset means Auto UI renders every fragment.
633
- // Read once at module init; same env var is honoured by the AWS wrapper.
633
+ // Read once at module init; same env var is honored by the AWS wrapper.
634
634
  lines->Array.push("")
635
635
  lines->Array.push(
636
636
  "@val external uiBundleUrl: option<string> = \"process.env." ++
@@ -877,7 +877,7 @@ let render = (
877
877
  validateSliceTargets(~resolved)
878
878
  // Chapter map, keyed by each component's spec stem (= its `Spec.name` for every
879
879
  // graph-node kind, which is how `Plugin_Structure` looks the chapter up). Filtered
880
- // to the actual component stems so body files (`_Behavior`, `_Projections`, …) —
880
+ // to the actual component stems so body files (`_Decision`, `_Projections`, …) —
881
881
  // which `Discovery` also surfaces and which share their component's chapter — don't
882
882
  // leak noise entries into the generated call. Tasks / extensions / extension points
883
883
  // carry no chapter field, so they are excluded too.
@@ -42,7 +42,7 @@ function renderAggregates(aggregates) {
42
42
  return [
43
43
  " module " + spec + "Aggregate = Platform.Aggregate." + factory + "(",
44
44
  " " + spec + ",",
45
- " " + param.behavior + ",",
45
+ " " + param.decision + ",",
46
46
  " " + em + ",",
47
47
  " )"
48
48
  ];
@@ -28,7 +28,7 @@ type discoveredFile = {
28
28
  let folderToComponentType = ComponentKind.folderToKind
29
29
 
30
30
  // The intra-plugin grouping band ("chapter") a file belongs to, derived from its
31
- // path relative to `src/`: the first directory segment that is not a recognised
31
+ // path relative to `src/`: the first directory segment that is not a recognized
32
32
  // kind-folder. `src/<Chapter>/<Kind>/<Component>.res` → `Some("<Chapter>")`; a file
33
33
  // directly under a kind-folder (`src/<Kind>/<Component>.res`) or at the src root →
34
34
  // `None`. Uses the single-source `ComponentKind.isKindFolder`, so a chapter read
@@ -52,7 +52,7 @@ let chapterOf = (relPath: string): option<string> => {
52
52
  // Deduplicated (stem → chapter) pairs across the discovered files, sorted by stem
53
53
  // for deterministic codegen. Only files that carry a chapter are included; stems are
54
54
  // unique within a plugin (validated), so keying by stem cannot collide. Body files
55
- // (`_Behavior`, `_Projections`, …) share their component's chapter but are never
55
+ // (`_Decision`, `_Projections`, …) share their component's chapter but are never
56
56
  // looked up by `Spec.name`, so their presence is harmless.
57
57
  let chaptersByStem = (discovered: array<discoveredFile>): array<(string, string)> => {
58
58
  let byStem: Dict.t<string> = Dict.make()
@@ -116,7 +116,7 @@ let isSkipped = (stem: string): bool =>
116
116
  // `_EventMappings` and `_Mappings` files in per-entity Aggregate/ folders
117
117
  // are picked up by the dedicated [Pairing.findEventMappings] walker;
118
118
  // treating them as Aggregate specs would hunt for a non-existent
119
- // `_EventMappingsBehavior` / `_MappingsBehavior`.
119
+ // `_EventMappings_Decision` / `_Mappings_Decision`.
120
120
  stem->String.endsWith("_EventMappings") ||
121
121
  stem->String.endsWith("_Mappings")
122
122
 
@@ -1,10 +1,10 @@
1
1
  // Applies pairing rules to discovered files.
2
- // - Aggregates: pairs Foo + FooBehavior; finds Foo_EventMappings if present
2
+ // - Aggregates: pairs Foo + Foo_Decision; finds Foo_EventMappings if present
3
3
  // - ReadModels: pairs FooReadModel + FooProjections; extracts mapping module names from projections file
4
4
  // - Slices (Plan 02): pair X + X_<Kind> where Kind is the implementation
5
5
  // suffix matching the slice's folder. The slice arrays carry only spec
6
6
  // stems; the corresponding impl stem is `<stem>_<implSuffix>`.
7
- // * StateChangeSlice / StateViewSlice → Behavior / Projection
7
+ // * StateChangeSlice / StateViewSlice → Decision / Projection
8
8
  // * AutomationSlice → Automation
9
9
  // * In/OutboundTranslationSlice → Translation
10
10
  // - ExtensionPoints: groups by epGroup, counts mappings, selects Make variant
@@ -12,7 +12,7 @@
12
12
  // Implementation-file suffix per slice kind. Used to filter impl files out
13
13
  // of the slice arrays (they're paired with their spec stems via this
14
14
  // suffix at codegen time) and to compute the impl module reference.
15
- let implSuffixForStateChange = "_Behavior"
15
+ let implSuffixForStateChange = "_Decision"
16
16
  let implSuffixForStateView = "_Projection"
17
17
  let implSuffixForAutomation = "_Automation"
18
18
  let implSuffixForTranslation = "_Translation"
@@ -27,7 +27,7 @@ let isImplStem = (stem: string): bool =>
27
27
  stem->String.endsWith(implSuffixForTranslation) ||
28
28
  stem->String.endsWith(mappingsSuffixForAutomation)
29
29
 
30
- type aggregateDef = {spec: string, behavior: string, eventMappings: option<string>, isAsync: bool}
30
+ type aggregateDef = {spec: string, decision: string, eventMappings: option<string>, isAsync: bool}
31
31
  // readModelDef pairs a ReadModel spec with its sibling `_Projections.res`
32
32
  // file. Codegen emits `Platform.ReadModel.Make(<spec>, <projections>)` and
33
33
  // expects the projections file to declare `let mappings` via
@@ -252,7 +252,37 @@ let groupExtensionPoints = (epFiles: array<Discovery.discoveredFile>): array<ext
252
252
  extensionPoints
253
253
  }
254
254
 
255
+ // The logic file's former name. One beside its spec is refused rather than
256
+ // read as a component of its own.
257
+ let retiredSuffix = "_Behavior"
258
+
259
+ let retiredBehaviorFile = (discovered: array<Discovery.discoveredFile>): option<
260
+ Discovery.discoveredFile,
261
+ > => {
262
+ let stems = discovered->Array.map(({stem}) => stem)
263
+ discovered->Array.find(({stem}) =>
264
+ stem->String.endsWith(retiredSuffix) ||
265
+ (stem->String.endsWith("Behavior") &&
266
+ stems->Array.includes(stem->String.slice(~start=0, ~end=String.length(stem) - 8)))
267
+ )
268
+ }
269
+
270
+ let refuseBehaviorFiles = (discovered: array<Discovery.discoveredFile>) =>
271
+ switch retiredBehaviorFile(discovered) {
272
+ | None => ()
273
+ | Some({stem, relPath}) =>
274
+ let spec =
275
+ stem->String.endsWith(retiredSuffix)
276
+ ? stem->String.slice(~start=0, ~end=String.length(stem) - 9)
277
+ : stem->String.slice(~start=0, ~end=String.length(stem) - 8)
278
+ JsError.throwWithMessage(
279
+ `Generator: ${relPath} uses the former name. The logic file is now ` ++
280
+ `${spec}_Decision.res, annotated @@reventless.decision: rename it ` ++ `(or run reventless-rename-to-decision).`,
281
+ )
282
+ }
283
+
255
284
  let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): resolved => {
285
+ refuseBehaviorFiles(discovered)
256
286
  let eventMappings = findEventMappings(~srcDir)
257
287
 
258
288
  let stateChangeSlices = []
@@ -270,7 +300,7 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
270
300
  let stateChangeSliceRelPaths: Dict.t<string> = Dict.make()
271
301
  // Shared by StateViewSlice and StateViewSliceStream (stems are unique).
272
302
  let stateViewSliceRelPaths: Dict.t<string> = Dict.make()
273
- let aggregateBehaviors: array<string> = []
303
+ let aggregateDecisions: array<string> = []
274
304
  let readModelStems: array<string> = []
275
305
  // Stream-enabled read model spec stems (the `ReadModelStream/` folder).
276
306
  let readModelStreamStems: array<string> = []
@@ -282,7 +312,7 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
282
312
  let extensions = []
283
313
 
284
314
  // Classify discovered files by component type. For the five slice
285
- // families, impl files (X_Behavior / X_Projection / X_Automation /
315
+ // families, impl files (X_Decision / X_Projection / X_Automation /
286
316
  // X_Translation) are filtered out: they're paired with their spec stems
287
317
  // at codegen time via the `_<ImplKind>` suffix convention.
288
318
  discovered->Array.forEach(({stem, componentType, epGroup, relPath}) => {
@@ -318,8 +348,8 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
318
348
  outboundTranslationSliceRelPaths->Dict.set(stem, relPath)
319
349
  }
320
350
  | Aggregate =>
321
- if stem->String.endsWith("Behavior") {
322
- aggregateBehaviors->Array.push(stem)
351
+ if stem->String.endsWith("Decision") {
352
+ aggregateDecisions->Array.push(stem)
323
353
  } else {
324
354
  aggregateSpecs->Array.push(stem)
325
355
  aggregateSpecRelPaths->Dict.set(stem, relPath)
@@ -343,30 +373,29 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
343
373
  })
344
374
 
345
375
  // ── Aggregate pairing ──────────────────────────────────────────────────────
346
- // Tries the post-Phase-3.1 `<Spec>_Behavior` form first, then falls back
347
- // to the legacy `<Spec>Behavior` (no underscore).
376
+ // Tries `<Spec>_Decision` first, then `<Spec>Decision` (no underscore).
348
377
  let aggregates =
349
378
  aggregateSpecs
350
379
  ->sortedStems
351
380
  ->Array.filterMap(spec => {
352
- let underscored = spec ++ "_Behavior"
353
- let bare = spec ++ "Behavior"
354
- let behaviorStem = if aggregateBehaviors->Array.includes(underscored) {
381
+ let underscored = spec ++ "_Decision"
382
+ let bare = spec ++ "Decision"
383
+ let decisionStem = if aggregateDecisions->Array.includes(underscored) {
355
384
  Some(underscored)
356
- } else if aggregateBehaviors->Array.includes(bare) {
385
+ } else if aggregateDecisions->Array.includes(bare) {
357
386
  Some(bare)
358
387
  } else {
359
388
  None
360
389
  }
361
- switch behaviorStem {
362
- | Some(behavior) =>
390
+ switch decisionStem {
391
+ | Some(decision) =>
363
392
  let isAsync = switch Dict.get(aggregateSpecRelPaths, spec) {
364
393
  | None => false
365
394
  | Some(relPath) => hasAsyncAttribute(NodePath.join([srcDir, relPath]))
366
395
  }
367
396
  Some({
368
397
  spec,
369
- behavior,
398
+ decision,
370
399
  eventMappings: Dict.get(eventMappings, spec),
371
400
  isAsync,
372
401
  })
@@ -386,7 +415,7 @@ let resolve = (discovered: array<Discovery.discoveredFile>, ~srcDir: string): re
386
415
  // ── ReadModel pairing ──────────────────────────────────────────────────────
387
416
  // Pairs spec `<Plural>` with impl `<Plural>_Projections.res`. The legacy
388
417
  // unpaired forms (`<Plural>ReadModel.res` / `<Plural>Projections.res`) are
389
- // no longer recognised — see PR3-5 of the close-ppx-gaps-and-unify-naming
418
+ // no longer recognized — see PR3-5 of the close-ppx-gaps-and-unify-naming
390
419
  // plan for the migration; downstream codebases should rename to the
391
420
  // underscored form.
392
421
  let pairReadModel = (~stream: bool, rm: string): option<readModelDef> => {
@@ -4,9 +4,10 @@ import * as Nodefs from "node:fs";
4
4
  import * as Nodepath from "node:path";
5
5
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
6
6
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
7
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
7
8
  import * as Generator_Node$Reventless from "./Generator_Node.res.mjs";
8
9
 
9
- let implSuffixForStateChange = "_Behavior";
10
+ let implSuffixForStateChange = "_Decision";
10
11
 
11
12
  let implSuffixForStateView = "_Projection";
12
13
 
@@ -197,7 +198,34 @@ function groupExtensionPoints(epFiles) {
197
198
  return extensionPoints;
198
199
  }
199
200
 
201
+ let retiredSuffix = "_Behavior";
202
+
203
+ function retiredBehaviorFile(discovered) {
204
+ let stems = discovered.map(param => param.stem);
205
+ return discovered.find(param => {
206
+ let stem = param.stem;
207
+ if (stem.endsWith(retiredSuffix)) {
208
+ return true;
209
+ } else if (stem.endsWith("Behavior")) {
210
+ return stems.includes(stem.slice(0, stem.length - 8 | 0));
211
+ } else {
212
+ return false;
213
+ }
214
+ });
215
+ }
216
+
217
+ function refuseBehaviorFiles(discovered) {
218
+ let match = retiredBehaviorFile(discovered);
219
+ if (match === undefined) {
220
+ return;
221
+ }
222
+ let stem = match.stem;
223
+ let spec = stem.endsWith(retiredSuffix) ? stem.slice(0, stem.length - 9 | 0) : stem.slice(0, stem.length - 8 | 0);
224
+ Stdlib_JsError.throwWithMessage(`Generator: ` + match.relPath + ` uses the former name. The logic file is now ` + (spec + `_Decision.res, annotated @@reventless.decision: rename it `) + `(or run reventless-rename-to-decision).`);
225
+ }
226
+
200
227
  function resolve(discovered, srcDir) {
228
+ refuseBehaviorFiles(discovered);
201
229
  let eventMappings = findEventMappings(srcDir);
202
230
  let stateChangeSlices = [];
203
231
  let stateViewSlices = [];
@@ -212,7 +240,7 @@ function resolve(discovered, srcDir) {
212
240
  let aggregateSpecRelPaths = {};
213
241
  let stateChangeSliceRelPaths = {};
214
242
  let stateViewSliceRelPaths = {};
215
- let aggregateBehaviors = [];
243
+ let aggregateDecisions = [];
216
244
  let readModelStems = [];
217
245
  let readModelStreamStems = [];
218
246
  let projectionsByRelPath = {};
@@ -273,8 +301,8 @@ function resolve(discovered, srcDir) {
273
301
  return;
274
302
  }
275
303
  case "Aggregate" :
276
- if (stem.endsWith("Behavior")) {
277
- aggregateBehaviors.push(stem);
304
+ if (stem.endsWith("Decision")) {
305
+ aggregateDecisions.push(stem);
278
306
  } else {
279
307
  aggregateSpecs.push(stem);
280
308
  aggregateSpecRelPaths[stem] = relPath;
@@ -311,17 +339,17 @@ function resolve(discovered, srcDir) {
311
339
  }
312
340
  });
313
341
  let aggregates = Stdlib_Array.filterMap(sortedStems(aggregateSpecs), spec => {
314
- let underscored = spec + "_Behavior";
315
- let bare = spec + "Behavior";
316
- let behaviorStem = aggregateBehaviors.includes(underscored) ? underscored : (
317
- aggregateBehaviors.includes(bare) ? bare : undefined
342
+ let underscored = spec + "_Decision";
343
+ let bare = spec + "Decision";
344
+ let decisionStem = aggregateDecisions.includes(underscored) ? underscored : (
345
+ aggregateDecisions.includes(bare) ? bare : undefined
318
346
  );
319
- if (behaviorStem !== undefined) {
347
+ if (decisionStem !== undefined) {
320
348
  let relPath = aggregateSpecRelPaths[spec];
321
349
  let isAsync = relPath !== undefined ? hasFileAttribute(Nodepath.join(srcDir, relPath), "@@reventless.async") : false;
322
350
  return {
323
351
  spec: spec,
324
- behavior: behaviorStem,
352
+ decision: decisionStem,
325
353
  eventMappings: eventMappings[spec],
326
354
  isAsync: isAsync
327
355
  };
@@ -410,6 +438,9 @@ export {
410
438
  hasSystemCallableAttribute,
411
439
  effectiveSpecName,
412
440
  groupExtensionPoints,
441
+ retiredSuffix,
442
+ retiredBehaviorFile,
443
+ refuseBehaviorFiles,
413
444
  resolve,
414
445
  }
415
446
  /* node:fs Not a pure module */
@@ -3,7 +3,7 @@ The lifecycle model each example's own scenarios describe, and what it says abou
3
3
  the edges each command declares beside them.
4
4
 
5
5
  `Moves([Placed], Shipped)` is authored, and until now nothing compared it to
6
- behaviour. The compiler resolves the state constructors and the platform checks
6
+ behavior. The compiler resolves the state constructors and the platform checks
7
7
  they belong to the linked view — both much weaker properties than agreement: a
8
8
  command can declare `Moves([Deactivated], Active)` while its `decide` accepts an
9
9
  active row, or emit an event the view folds into something else, and every static
@@ -694,7 +694,7 @@ let observe = (
694
694
 
695
695
  scenarios->Array.filterMap(s =>
696
696
  switch s.whenElements->Array.get(0) {
697
- // `thenRefused`: the caller did not own the row, which says nothing about
697
+ // `thenForbidden`: the caller did not own the row, which says nothing about
698
698
  // the states the command is legal in.
699
699
  | Some(_) if s.thenKind == "forbidden" => None
700
700
  | Some(command) if s.whenKind == "command" =>
@@ -935,7 +935,7 @@ let compare = (
935
935
  // effect somewhere agrees with that rather than narrowing it — the corpus
936
936
  // covers the states somebody wrote a scenario for, and silence about the
937
937
  // rest is not refusal. What DOES refute the claim is a state the command was
938
- // exercised in and **refused**: that is the switch and the behaviour
938
+ // exercised in and **refused**: that is the switch and the behavior
939
939
  // disagreeing about the same row.
940
940
  //
941
941
  // 🚨 **Refused, not merely inert.** `Unrestricted` claims the command is
@@ -41,9 +41,9 @@ type t = {
41
41
  /**
42
42
  The capability set of a platform that provisions nothing.
43
43
 
44
- Every capability here answers `Unavailable`, which is a *modelled* outcome:
44
+ Every capability here answers `Unavailable`, which is a *modeled* outcome:
45
45
  `translate` maps it to `Error`, the item is retried and the sweep surfaces it.
46
- That is the behaviour wanted for a deployment that simply has no geocoder — the
46
+ That is the behavior wanted for a deployment that simply has no geocoder — the
47
47
  work stays queued and visible rather than being written off as a verdict on the
48
48
  address, which is what `NoMatch` would mean.
49
49
 
@@ -47,7 +47,7 @@ let toString = (need: t): string =>
47
47
 
48
48
  /** The inverse, for readers of a persisted structure or a committed manifest.
49
49
  `None` for a capability this build does not know — a newer plugin's need is
50
- reported as unrecognised, never silently dropped into a known arm. */
50
+ reported as unrecognized, never silently dropped into a known arm. */
51
51
  let fromString = (name: string): option<t> =>
52
52
  switch name {
53
53
  | "Geocoding" => Some(Geocoding)
@@ -65,13 +65,13 @@ Declared capability needs a platform does not provision.
65
65
 
66
66
  `declared` is `pluginStructure.requiredCapabilities` as persisted: `(capability,
67
67
  component)` pairs whose capability is a string, because a structure is replayed
68
- from an event log. A name this build does not recognise is **skipped** — it
68
+ from an event log. A name this build does not recognize is **skipped** — it
69
69
  belongs to a newer framework, and refusing a deploy over a capability this code
70
70
  cannot evaluate would be a guess dressed as a check.
71
71
 
72
72
  Only what was declared. A plugin that declares nothing is unaffected: an
73
73
  unprovisioned capability it never named degrades exactly as it always has, which
74
- is the modelled outcome for a deployment that simply does not have one.
74
+ is the modeled outcome for a deployment that simply does not have one.
75
75
  */
76
76
  let unmet = (~declared: array<(string, string)>, ~provisioned: array<t>): array<unmet> =>
77
77
  declared->Array.filterMap(((capability, component)) =>