@reventlessdev/reventless-spec 3.0.0-alpha.169 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +32 -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 +14 -14
  18. package/src/components/Plugin.res.mjs +8 -8
  19. package/src/components/Sensitive.res +1 -1
  20. package/src/components/Snapshot.res +5 -5
  21. package/src/components/StateAnnotations.res +1 -1
  22. package/src/components/StateChangeSlice.res +6 -5
  23. package/src/components/StateViewSlice.res +3 -3
  24. package/src/components/TaggedUnion.res +1 -1
  25. package/src/generator/Codegen.res +5 -5
  26. package/src/generator/Codegen.res.mjs +1 -1
  27. package/src/generator/Discovery.res +3 -3
  28. package/src/generator/Pairing.res +47 -18
  29. package/src/generator/Pairing.res.mjs +41 -10
  30. package/src/lifecycle/CheckLifecycleModel.res +8 -7
  31. package/src/lifecycle/CheckLifecycleModel.res.mjs +2 -2
  32. package/src/semantic/Capabilities.res +2 -2
  33. package/src/semantic/CapabilityNeed.res +3 -3
  34. package/src/semantic/Color.res +8 -8
  35. package/src/semantic/IdentityProvider.res +1 -1
  36. package/src/semantic/Messaging.res +2 -2
  37. package/src/types/{Behavior.res → Decision.res} +10 -6
  38. package/src/types/Message.res +1 -1
  39. package/src/types/OwnerScope.res +5 -5
  40. package/src/types/RoleCoverage.res +6 -6
  41. package/src/types/RoleCoverage.res.mjs +6 -6
  42. package/src/types/Trait.res +1 -1
  43. /package/src/types/{Behavior.res.mjs → Decision.res.mjs} +0 -0
@@ -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
  }
@@ -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
@@ -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
@@ -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
@@ -694,7 +695,7 @@ let observe = (
694
695
 
695
696
  scenarios->Array.filterMap(s =>
696
697
  switch s.whenElements->Array.get(0) {
697
- // `thenRefused`: the caller did not own the row, which says nothing about
698
+ // `thenForbidden`: the caller did not own the row, which says nothing about
698
699
  // the states the command is legal in.
699
700
  | Some(_) if s.thenKind == "forbidden" => None
700
701
  | Some(command) if s.whenKind == "command" =>
@@ -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)),
@@ -935,10 +936,10 @@ let compare = (
935
936
  // effect somewhere agrees with that rather than narrowing it — the corpus
936
937
  // covers the states somebody wrote a scenario for, and silence about the
937
938
  // 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
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
  })),
@@ -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)) =>
@@ -1,15 +1,15 @@
1
1
  /**
2
- Marks a `string` field as a colour, written as a hex triplet.
2
+ Marks a `string` field as a color, written as a hex triplet.
3
3
 
4
4
  ## Why hex only
5
5
 
6
6
  A field carrying this semantic is rendered as a swatch by setting the value as a
7
- CSS `background`. CSS accepts far more than a colour there — `url(…)`, gradients,
8
- `var(…)` — so "any CSS colour" would make a colour field a way to write arbitrary
7
+ CSS `background`. CSS accepts far more than a color there — `url(…)`, gradients,
8
+ `var(…)` — so "any CSS colour" would make a color field a way to write arbitrary
9
9
  CSS into a log that cannot be edited afterwards. Hex is the form that is a
10
- colour and nothing else, and it is what a colour picker emits anyway.
10
+ color and nothing else, and it is what a color picker emits anyway.
11
11
 
12
- Named colours (`rebeccapurple`) and functional forms (`rgb(…)`, `oklch(…)`) are
12
+ Named colors (`rebeccapurple`) and functional forms (`rgb(…)`, `oklch(…)`) are
13
13
  rejected for the same reason, not because they are worse notation.
14
14
 
15
15
  ## The grammar
@@ -26,7 +26,7 @@ full triplet, and the triplet with alpha. Case is not significant.
26
26
  })
27
27
  ```
28
28
  */
29
- /** The colour's representation. Transparent `string`; see `Email.t`. */
29
+ /** The color's representation. Transparent `string`; see `Email.t`. */
30
30
  type t = string
31
31
 
32
32
  external unsafe: string => t = "%identity"
@@ -34,7 +34,7 @@ external toString: t => string = "%identity"
34
34
 
35
35
  let hex = /^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
36
36
 
37
- /** Validate a raw string as a hex colour, saying why when it is not one. */
37
+ /** Validate a raw string as a hex color, saying why when it is not one. */
38
38
  let fromString = (raw: string): result<t, string> =>
39
39
  if hex->RegExp.test(raw) {
40
40
  Ok(raw)
@@ -46,5 +46,5 @@ let fromString = (raw: string): result<t, string> =>
46
46
  )
47
47
  }
48
48
 
49
- /** The sury schema for a colour field. Use with `@s.matches(Reventless.Color.schema)`. */
49
+ /** The sury schema for a color field. Use with `@s.matches(Reventless.Color.schema)`. */
50
50
  let schema: S.t<t> = S.string->Semantic.refined(~id=Semantic.Id.color, ~check=fromString)
@@ -38,7 +38,7 @@ surface. Collapsing them either loses accounts to a transient blip or retries
38
38
  forever against a password policy.
39
39
  */
40
40
  type failure =
41
- /** The contact is already a principal. A modelled answer, not an exception —
41
+ /** The contact is already a principal. A modeled answer, not an exception —
42
42
  which is what a silent idempotent sign-up throws away. */
43
43
  | Conflict
44
44
  /** The provider is down or unreachable. The domain fact stands; retry. */
@@ -142,7 +142,7 @@ type message = {subject?: string, body: string}
142
142
 
143
143
  /** The provider accepted the message. `ref` is its own id for it — what a
144
144
  support conversation about a missing message is conducted with, and the only
145
- thing a caller can record that the provider will recognise. */
145
+ thing a caller can record that the provider will recognize. */
146
146
  type receipt = {ref: string}
147
147
 
148
148
  /**
@@ -203,7 +203,7 @@ type send = (~recipient: recipient, ~message: message) => promise<result<receipt
203
203
  The capability as a slice receives it: what can be attempted, and how.
204
204
 
205
205
  `channels` is here because a recipient cannot be offered a choice the deployment
206
- cannot honour. A platform provisions email only, or email and SMS; a preference
206
+ cannot honor. A platform provisions email only, or email and SMS; a preference
207
207
  centre that listed all three would collect a subscription every send then answers
208
208
  `UnsupportedChannel` for. Published rather than inferred from a failed send,
209
209
  because discovering a channel by failing on it costs a real message.
@@ -1,11 +1,15 @@
1
- // Behavior module type for aggregate business logic.
2
- // Lives in reventless-spec so application domain code can define behaviors
1
+ // Decision module type for an aggregate's write-side logic.
2
+ // Lives in reventless-spec so application domain code can define decisions
3
3
  // without importing the reventless implementation package.
4
4
 
5
5
  /**
6
- Module type that application code must implement to define aggregate business logic.
6
+ Module type that application code must implement to define an aggregate's
7
+ write-side logic: whether a command goes through, and what it emits.
7
8
 
8
- A `Behavior.T` module contains the `Spec` (command / event / error types), the
9
+ It implements the *decider* pattern: `initialState`, `evolve` and `decide`.
10
+ The file is `<Entity>_Decision.res`, annotated `@@reventless.decision`.
11
+
12
+ A `Decision.T` module contains the `Spec` (command / event / error types), the
9
13
  aggregate `state`, and two pure functions plus an initial state value:
10
14
 
11
15
  - `initialState` — the starting state before any events (represents "not yet created")
@@ -14,7 +18,7 @@ aggregate `state`, and two pure functions plus an initial state value:
14
18
 
15
19
  @example
16
20
  ```rescript
17
- // CategoryBehavior.res
21
+ // Category_Decision.res
18
22
  module Spec = Category
19
23
 
20
24
  @schema
@@ -73,7 +77,7 @@ module type T = {
73
77
  let decide: (state, Spec.command) => result<array<Spec.event>, Spec.error>
74
78
 
75
79
  /** Persisted-snapshot configuration (docs/plans/done/aggregate-snapshotting.md).
76
- `None` — the default, auto-injected by `@@reventless.behavior` — keeps
80
+ `None` — the default, auto-injected by `@@reventless.decision` — keeps
77
81
  full replay; `Some({interval, stateSchema})` writes a keep-one snapshot
78
82
  every `interval` events and seeds cold replays from the latest one.
79
83
  Enable with `@@reventless.snapshots(<interval>)` (requires
@@ -76,7 +76,7 @@ type meta = {
76
76
  /**
77
77
  Context passed into command handlers, carrying the aggregate ID and envelope metadata.
78
78
 
79
- Passed as the second argument to `Behavior.T.create` and `Behavior.T.execute`
79
+ Passed as the second argument to `Decision.T.create` and `Decision.T.execute`
80
80
  so business logic can read the caller identity or propagate correlation IDs.
81
81
  */
82
82
  @schema