@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.
- package/CHANGELOG.md +22 -0
- package/README.md +17 -3
- package/package.json +5 -4
- package/run-rename-to-decision.mjs +3 -0
- package/src/AnsiStyle.res +3 -3
- package/src/CliArgs.res +1 -1
- package/src/LogPrefix.res +1 -1
- package/src/codemod/RenameToDecision.res +207 -0
- package/src/codemod/RenameToDecision.res.mjs +334 -0
- package/src/components/Aggregate.res +1 -1
- package/src/components/CapabilityManifest.res +1 -1
- package/src/components/ComponentKind.res +2 -2
- package/src/components/ComponentKind.res.mjs +1 -1
- package/src/components/DcbScopeInference.res +2 -2
- package/src/components/DcbTag.res +5 -5
- package/src/components/OutboundTranslationSlice.res +2 -2
- package/src/components/Plugin.res +2 -2
- package/src/components/Sensitive.res +1 -1
- package/src/components/Snapshot.res +5 -5
- package/src/components/StateChangeSlice.res +6 -5
- package/src/components/TaggedUnion.res +1 -1
- package/src/generator/Codegen.res +5 -5
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/generator/Discovery.res +3 -3
- package/src/generator/Pairing.res +47 -18
- package/src/generator/Pairing.res.mjs +41 -10
- package/src/lifecycle/CheckLifecycleModel.res +3 -3
- package/src/semantic/Capabilities.res +2 -2
- package/src/semantic/CapabilityNeed.res +3 -3
- package/src/semantic/Color.res +8 -8
- package/src/semantic/IdentityProvider.res +1 -1
- package/src/semantic/Messaging.res +2 -2
- package/src/types/{Behavior.res → Decision.res} +10 -6
- package/src/types/Message.res +1 -1
- package/src/types/OwnerScope.res +5 -5
- package/src/types/ShellEnvironment.res +39 -0
- package/src/types/ShellEnvironment.res.mjs +39 -0
- package/src/types/Trait.res +1 -1
- /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 `
|
|
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
|
|
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
|
|
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
|
-
"
|
|
102
|
+
"_Decision",
|
|
103
103
|
"_Automation",
|
|
104
104
|
"_Translation",
|
|
105
105
|
]
|
|
@@ -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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
5
|
-
// defined): `
|
|
6
|
-
// `@@reventless.
|
|
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
|
|
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
|
|
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
|
-
- `
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
" " ++
|
|
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
|
|
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
|
|
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 (`
|
|
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.
|
|
@@ -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
|
|
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
|
-
// (`
|
|
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
|
-
// `
|
|
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 +
|
|
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 →
|
|
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 = "
|
|
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,
|
|
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
|
|
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 (
|
|
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("
|
|
322
|
-
|
|
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
|
|
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 ++ "
|
|
353
|
-
let bare = spec ++ "
|
|
354
|
-
let
|
|
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
|
|
385
|
+
} else if aggregateDecisions->Array.includes(bare) {
|
|
357
386
|
Some(bare)
|
|
358
387
|
} else {
|
|
359
388
|
None
|
|
360
389
|
}
|
|
361
|
-
switch
|
|
362
|
-
| Some(
|
|
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
|
-
|
|
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
|
|
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 = "
|
|
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
|
|
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("
|
|
277
|
-
|
|
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 + "
|
|
315
|
-
let bare = spec + "
|
|
316
|
-
let
|
|
317
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
// `
|
|
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
|
|
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 *
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)) =>
|