@reventlessdev/reventless-spec 3.0.0-alpha.100

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 (139) hide show
  1. package/CHANGELOG.md +931 -0
  2. package/LICENSE +202 -0
  3. package/README.md +109 -0
  4. package/package.json +49 -0
  5. package/rescript.json +32 -0
  6. package/run-generator.mjs +2 -0
  7. package/run-platform-generator.mjs +2 -0
  8. package/scripts/generate-currency.mjs +215 -0
  9. package/scripts/iso-4217-list-one.xml +1956 -0
  10. package/src/AnsiStyle.res +40 -0
  11. package/src/AnsiStyle.res.mjs +54 -0
  12. package/src/LogPrefix.res +192 -0
  13. package/src/LogPrefix.res.mjs +159 -0
  14. package/src/PackageVersion.res +67 -0
  15. package/src/PackageVersion.res.mjs +81 -0
  16. package/src/components/Aggregate.res +64 -0
  17. package/src/components/Aggregate.res.mjs +2 -0
  18. package/src/components/AutomationSlice.res +279 -0
  19. package/src/components/AutomationSlice.res.mjs +30 -0
  20. package/src/components/CapabilityManifest.res +74 -0
  21. package/src/components/CapabilityManifest.res.mjs +61 -0
  22. package/src/components/ComponentKind.res +99 -0
  23. package/src/components/ComponentKind.res.mjs +125 -0
  24. package/src/components/Counter.res +24 -0
  25. package/src/components/Counter.res.mjs +2 -0
  26. package/src/components/DcbDecode.res +118 -0
  27. package/src/components/DcbDecode.res.mjs +100 -0
  28. package/src/components/DcbScopeInference.res +244 -0
  29. package/src/components/DcbScopeInference.res.mjs +177 -0
  30. package/src/components/DcbTag.res +1335 -0
  31. package/src/components/DcbTag.res.mjs +898 -0
  32. package/src/components/DcbValidation.res +427 -0
  33. package/src/components/DcbValidation.res.mjs +423 -0
  34. package/src/components/DisplayName.res +40 -0
  35. package/src/components/DisplayName.res.mjs +26 -0
  36. package/src/components/ExtensionPoint.res +27 -0
  37. package/src/components/ExtensionPoint.res.mjs +2 -0
  38. package/src/components/InboundTranslationSlice.res +85 -0
  39. package/src/components/InboundTranslationSlice.res.mjs +2 -0
  40. package/src/components/OutboundTranslationSlice.res +153 -0
  41. package/src/components/OutboundTranslationSlice.res.mjs +2 -0
  42. package/src/components/Plugin.res +538 -0
  43. package/src/components/Plugin.res.mjs +264 -0
  44. package/src/components/PluginName.res +39 -0
  45. package/src/components/PluginName.res.mjs +45 -0
  46. package/src/components/ReadModel.res +199 -0
  47. package/src/components/ReadModel.res.mjs +18 -0
  48. package/src/components/Reference.res +55 -0
  49. package/src/components/Reference.res.mjs +50 -0
  50. package/src/components/Snapshot.res +26 -0
  51. package/src/components/Snapshot.res.mjs +2 -0
  52. package/src/components/StateAnnotations.res +97 -0
  53. package/src/components/StateAnnotations.res.mjs +15 -0
  54. package/src/components/StateChangeSlice.res +131 -0
  55. package/src/components/StateChangeSlice.res.mjs +2 -0
  56. package/src/components/StateViewSlice.res +123 -0
  57. package/src/components/StateViewSlice.res.mjs +2 -0
  58. package/src/components/Task.res +62 -0
  59. package/src/components/Task.res.mjs +2 -0
  60. package/src/generator/Codegen.res +842 -0
  61. package/src/generator/Codegen.res.mjs +565 -0
  62. package/src/generator/Config.res +106 -0
  63. package/src/generator/Config.res.mjs +69 -0
  64. package/src/generator/Discovery.res +230 -0
  65. package/src/generator/Discovery.res.mjs +198 -0
  66. package/src/generator/Generator_Node.res +14 -0
  67. package/src/generator/Generator_Node.res.mjs +18 -0
  68. package/src/generator/Pairing.res +460 -0
  69. package/src/generator/Pairing.res.mjs +415 -0
  70. package/src/generator/PlatformCodegen.res +207 -0
  71. package/src/generator/PlatformCodegen.res.mjs +154 -0
  72. package/src/generator/PlatformGenerator.res +126 -0
  73. package/src/generator/PlatformGenerator.res.mjs +114 -0
  74. package/src/generator/PlatformManifests.res +203 -0
  75. package/src/generator/PlatformManifests.res.mjs +212 -0
  76. package/src/generator/PluginGenerator.res +57 -0
  77. package/src/generator/PluginGenerator.res.mjs +73 -0
  78. package/src/semantic/Bytes.res +54 -0
  79. package/src/semantic/Bytes.res.mjs +38 -0
  80. package/src/semantic/Capabilities.res +43 -0
  81. package/src/semantic/Capabilities.res.mjs +17 -0
  82. package/src/semantic/Color.res +51 -0
  83. package/src/semantic/Color.res.mjs +29 -0
  84. package/src/semantic/Currency.res +598 -0
  85. package/src/semantic/Currency.res.mjs +743 -0
  86. package/src/semantic/DateRange.res +148 -0
  87. package/src/semantic/DateRange.res.mjs +74 -0
  88. package/src/semantic/Duration.res +53 -0
  89. package/src/semantic/Duration.res.mjs +26 -0
  90. package/src/semantic/Email.res +51 -0
  91. package/src/semantic/Email.res.mjs +31 -0
  92. package/src/semantic/GeoPoint.res +226 -0
  93. package/src/semantic/GeoPoint.res.mjs +190 -0
  94. package/src/semantic/Geocoding.res +127 -0
  95. package/src/semantic/Geocoding.res.mjs +36 -0
  96. package/src/semantic/Money.res +196 -0
  97. package/src/semantic/Money.res.mjs +138 -0
  98. package/src/semantic/Offload.res +294 -0
  99. package/src/semantic/Offload.res.mjs +191 -0
  100. package/src/semantic/Percent.res +53 -0
  101. package/src/semantic/Percent.res.mjs +33 -0
  102. package/src/semantic/Phone.res +55 -0
  103. package/src/semantic/Phone.res.mjs +29 -0
  104. package/src/semantic/Semantic.res +162 -0
  105. package/src/semantic/Semantic.res.mjs +95 -0
  106. package/src/semantic/StorageRef.res +164 -0
  107. package/src/semantic/StorageRef.res.mjs +111 -0
  108. package/src/semantic/Url.res +66 -0
  109. package/src/semantic/Url.res.mjs +48 -0
  110. package/src/types/Authorization.res +23 -0
  111. package/src/types/Authorization.res.mjs +33 -0
  112. package/src/types/Behavior.res +86 -0
  113. package/src/types/Behavior.res.mjs +2 -0
  114. package/src/types/DateTime.res +29 -0
  115. package/src/types/DateTime.res.mjs +16 -0
  116. package/src/types/EventMapping.res +100 -0
  117. package/src/types/EventMapping.res.mjs +15 -0
  118. package/src/types/Handler.res +30 -0
  119. package/src/types/Handler.res.mjs +2 -0
  120. package/src/types/Id.res +75 -0
  121. package/src/types/Id.res.mjs +37 -0
  122. package/src/types/Identity.res +46 -0
  123. package/src/types/Identity.res.mjs +51 -0
  124. package/src/types/Message.res +326 -0
  125. package/src/types/Message.res.mjs +186 -0
  126. package/src/types/Projection.res +220 -0
  127. package/src/types/Projection.res.mjs +44 -0
  128. package/src/types/QueryEngine.res +123 -0
  129. package/src/types/QueryEngine.res.mjs +12 -0
  130. package/src/types/ReadConsistency.res +38 -0
  131. package/src/types/ReadConsistency.res.mjs +29 -0
  132. package/src/types/Schedule.res +65 -0
  133. package/src/types/Schedule.res.mjs +68 -0
  134. package/src/types/SideEffect.res +45 -0
  135. package/src/types/SideEffect.res.mjs +2 -0
  136. package/src/types/StoredEvent.res +46 -0
  137. package/src/types/StoredEvent.res.mjs +32 -0
  138. package/src/types/Visibility.res +24 -0
  139. package/src/types/Visibility.res.mjs +25 -0
@@ -0,0 +1,118 @@
1
+ // Decodes raw stored events against a consumed event schema.
2
+ // Supports the full spectrum of consumed events:
3
+ // - Payload-less variants (match by TAG only, bypass sury)
4
+ // - Partial projections (sury parse with field subset)
5
+ // - Full shape (sury parse as-is)
6
+
7
+ // Build a lookup of TAG name -> variant kind (payloadLess or withFields)
8
+ // from a consumed event schema. Cached per schema for performance.
9
+
10
+ type variantKind =
11
+ | PayloadLess
12
+ | WithFields
13
+
14
+ let buildVariantLookup = (schema: S.t<unknown>): dict<variantKind> => {
15
+ let lookup = Dict.make()
16
+ switch schema {
17
+ | Union({anyOf}) =>
18
+ anyOf->Array.forEach(variantSchema =>
19
+ switch variantSchema {
20
+ | Object({items, properties}) =>
21
+ switch DcbTag.variantTagName(items) {
22
+ | Some(name) =>
23
+ let kind = if properties->Dict.keysToArray->Array.length == 0 {
24
+ PayloadLess
25
+ } else {
26
+ WithFields
27
+ }
28
+ lookup->Dict.set(name, kind)
29
+ | None => ()
30
+ }
31
+ | String({const: ?Some(name)}) =>
32
+ // sury represents payload-less variants in some cases as bare String({const})
33
+ lookup->Dict.set(name, PayloadLess)
34
+ | _ => ()
35
+ }
36
+ )
37
+ | Object({items}) =>
38
+ // Single-variant schema with fields
39
+ switch DcbTag.variantTagName(items) {
40
+ | Some(name) => lookup->Dict.set(name, WithFields)
41
+ | None => ()
42
+ }
43
+ | String({const: ?Some(name)}) =>
44
+ // Single payload-less variant — sury compiles `type t = Foo` to String({const: "Foo"})
45
+ lookup->Dict.set(name, PayloadLess)
46
+ | _ => ()
47
+ }
48
+ lookup
49
+ }
50
+
51
+ type decoder<'event> = {
52
+ lookup: dict<variantKind>,
53
+ schema: S.t<'event>,
54
+ constructors: dict<'event>,
55
+ }
56
+
57
+ external unsafeCast: 'a => 'b = "%identity"
58
+
59
+ // Build a map of TAG name -> constructed payload-less variant value
60
+ // by encoding each payload-less variant through sury and storing the result.
61
+ // This is done once per schema, not per event.
62
+ let buildPayloadLessConstructors = (schema: S.t<'event>, lookup: dict<variantKind>): dict<'event> => {
63
+ let constructors = Dict.make()
64
+ // For payload-less variants, we need to figure out what ReScript value
65
+ // corresponds to each TAG name. sury's schema for a union with payload-less
66
+ // variants encodes them as bare strings. So `| ItemCreated` serializes to
67
+ // JSON string "ItemCreated". We can use S.parseJsonOrThrow with the string.
68
+ lookup->Dict.toArray->Array.forEach(((tagName, kind)) =>
69
+ switch kind {
70
+ | PayloadLess =>
71
+ try {
72
+ let value = JSON.String(tagName)->S.parseJsonOrThrow(schema)
73
+ constructors->Dict.set(tagName, value)
74
+ } catch {
75
+ | _ => ()
76
+ }
77
+ | WithFields => ()
78
+ }
79
+ )
80
+ constructors
81
+ }
82
+
83
+ type makeDecoderResult<'event> = {
84
+ decode: (~eventType: string, ~data: dict<JSON.t>) => option<'event>,
85
+ eventTypes: array<string>,
86
+ }
87
+
88
+ let makeDecoder = (schema: S.t<'event>): makeDecoderResult<'event> => {
89
+ let unknownSchema: S.t<unknown> = unsafeCast(schema)
90
+ let lookup = buildVariantLookup(unknownSchema)
91
+ let constructors = buildPayloadLessConstructors(schema, lookup)
92
+ let eventTypes = lookup->Dict.keysToArray
93
+
94
+ let decode = (~eventType: string, ~data: dict<JSON.t>): option<'event> =>
95
+ switch lookup->Dict.get(eventType) {
96
+ | None => None
97
+ | Some(PayloadLess) => constructors->Dict.get(eventType)
98
+ | Some(WithFields) =>
99
+ // Reconstruct the JSON with TAG for sury parsing
100
+ let jsonDict = data->Dict.copy
101
+ jsonDict->Dict.set("TAG", JSON.String(eventType))
102
+ try {
103
+ Some(JSON.Object(jsonDict)->S.parseJsonOrThrow(schema))
104
+ } catch {
105
+ | _ =>
106
+ // A parse failure here means a stored event no longer matches the
107
+ // current schema (drift). Dropping it silently hid real data loss;
108
+ // surface it as a warning so it's diagnosable.
109
+ Console.warn(
110
+ "DcbDecode: dropped event `" ++
111
+ eventType ++ "` — payload does not match the current schema (drift?)",
112
+ )
113
+ None
114
+ }
115
+ }
116
+
117
+ {decode, eventTypes}
118
+ }
@@ -0,0 +1,100 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
5
+ import * as DcbTag$Reventless from "./DcbTag.res.mjs";
6
+
7
+ function buildVariantLookup(schema) {
8
+ let lookup = {};
9
+ switch (schema.type) {
10
+ case "string" :
11
+ let name = schema.const;
12
+ if (name !== undefined) {
13
+ lookup[name] = "PayloadLess";
14
+ }
15
+ break;
16
+ case "object" :
17
+ let name$1 = DcbTag$Reventless.variantTagName(schema.items);
18
+ if (name$1 !== undefined) {
19
+ lookup[name$1] = "WithFields";
20
+ }
21
+ break;
22
+ case "union" :
23
+ schema.anyOf.forEach(variantSchema => {
24
+ switch (variantSchema.type) {
25
+ case "string" :
26
+ let name = variantSchema.const;
27
+ if (name !== undefined) {
28
+ lookup[name] = "PayloadLess";
29
+ return;
30
+ } else {
31
+ return;
32
+ }
33
+ case "object" :
34
+ let name$1 = DcbTag$Reventless.variantTagName(variantSchema.items);
35
+ if (name$1 === undefined) {
36
+ return;
37
+ }
38
+ let kind = Object.keys(variantSchema.properties).length === 0 ? "PayloadLess" : "WithFields";
39
+ lookup[name$1] = kind;
40
+ return;
41
+ default:
42
+ return;
43
+ }
44
+ });
45
+ break;
46
+ }
47
+ return lookup;
48
+ }
49
+
50
+ function buildPayloadLessConstructors(schema, lookup) {
51
+ let constructors = {};
52
+ Object.entries(lookup).forEach(param => {
53
+ let tagName = param[0];
54
+ if (param[1] !== "PayloadLess") {
55
+ return;
56
+ }
57
+ try {
58
+ let value = S.parseJsonOrThrow(tagName, schema);
59
+ constructors[tagName] = value;
60
+ return;
61
+ } catch (exn) {
62
+ return;
63
+ }
64
+ });
65
+ return constructors;
66
+ }
67
+
68
+ function makeDecoder(schema) {
69
+ let lookup = buildVariantLookup(schema);
70
+ let constructors = buildPayloadLessConstructors(schema, lookup);
71
+ let eventTypes = Object.keys(lookup);
72
+ let decode = (eventType, data) => {
73
+ let match = lookup[eventType];
74
+ if (match === undefined) {
75
+ return;
76
+ }
77
+ if (match === "PayloadLess") {
78
+ return constructors[eventType];
79
+ }
80
+ let jsonDict = Object.assign({}, data);
81
+ jsonDict["TAG"] = eventType;
82
+ try {
83
+ return Primitive_option.some(S.parseJsonOrThrow(jsonDict, schema));
84
+ } catch (exn) {
85
+ console.warn("DcbDecode: dropped event `" + eventType + "` — payload does not match the current schema (drift?)");
86
+ return;
87
+ }
88
+ };
89
+ return {
90
+ decode: decode,
91
+ eventTypes: eventTypes
92
+ };
93
+ }
94
+
95
+ export {
96
+ buildVariantLookup,
97
+ buildPayloadLessConstructors,
98
+ makeDecoder,
99
+ }
100
+ /* S Not a pure module */
@@ -0,0 +1,244 @@
1
+ /**
2
+ DCB tag-scope inference (Phase 1 — pure core).
3
+
4
+ Derives DCB tag scope from the *global* slice graph instead of hand-placed
5
+ `@partitionTag` / `@crossPartition` / `@noTag` annotations. This module is
6
+ deliberately **schema-agnostic**: its input carries only the structural shape of
7
+ each slice (variant names + `*Id`-shaped fields), never an `S.t` schema and never
8
+ a tag-metadata flag. That is precisely what we are replacing — so both the runtime
9
+ (building shapes from `S.t` schemas, via `DcbTag.sliceShapeFromSchemas`) and the
10
+ VS Code tooling (building shapes from parsed `.res` source) can feed the same
11
+ `infer`. See `docs/plans/dcb-tag-scope-inference.md` § "Phase 1 design".
12
+
13
+ The three rules (over the representation):
14
+
15
+ 1. **Owner / partition.** A slice's partition key is the key its *own* emitted
16
+ events are identified by — computed as `producedKeys(S)` minus the keys `S`
17
+ reads from a *foreign* producer (a consumed arm whose event type is produced by
18
+ a different slice). For `AddProduct` (`ProductAdded({productId, categoryId})`,
19
+ consuming `CategoryAdded({categoryId})`) the foreign-read `categoryId` is
20
+ removed, leaving `productId`. No fixpoint needed.
21
+
22
+ 2. **Cross-partition.** A key is cross-partition iff some slice reads it on a
23
+ *foreign* consumed event while partitioned by something else *and* the key is
24
+ another entity's partition. `AddProduct` reads `categoryId` (Category's
25
+ partition) while partitioned by `productId` ⇒ `categoryId` is cross-partition.
26
+
27
+ 3. **Index vs payload.** A `*Id` on an *emitted* event is indexed iff it is the
28
+ producing slice's own partition key. Foreign reference keys (e.g. `categoryId`
29
+ on `ProductAdded`) are payload ⇒ not indexed ⇒ the sibling-leak GSI write never
30
+ happens.
31
+ */
32
+
33
+ /** A `*Id` / `*Ids`-shaped field, identified by name only (no schema, no tag flag). */
34
+ type idField = {name: string, isList: bool}
35
+
36
+ /** One variant arm: its constructor name and the `*Id` fields it carries. */
37
+ type eventShape = {eventType: string, idFields: array<idField>}
38
+
39
+ /**
40
+ The structural shape of one slice, the boundary type both adapters produce:
41
+ - `command` — the `*Id` fields on the slice's command,
42
+ - `consumed` — the arms (and their `*Id` fields) the slice reads,
43
+ - `produced` — the arms (and their `*Id` fields) the slice writes,
44
+ - `partitionHint` — an explicit `@partitionTag` escape hatch (when the dev marked
45
+ the partition because the slice's own events legitimately carry two owned keys,
46
+ e.g. `RecordProductDemand`). Overrides the inferred partition.
47
+ */
48
+ type sliceShape = {
49
+ sliceName: string,
50
+ command: array<idField>,
51
+ consumed: array<eventShape>,
52
+ produced: array<eventShape>,
53
+ partitionHint: option<string>,
54
+ }
55
+
56
+ /** Derived scope for one tag key. */
57
+ type scope = Partition | CrossPartition | Payload
58
+
59
+ /**
60
+ The full derivation. `crossPartitionTagKeys` and `tagKeysByEventType` are the two
61
+ values the runtime threads today (via `DcbTag.extractCrossPartitionTagKeys` /
62
+ `mergeTagKeysByEventType`); under inference they are produced here.
63
+ */
64
+ type derived = {
65
+ /** sliceName -> its inferred partition key (absent when ambiguous). */
66
+ partitionBySlice: dict<string>,
67
+ /** tag key -> the name of a slice that owns it as its partition. */
68
+ ownerByKey: dict<string>,
69
+ /** keys read cross-partition by some slice (sorted, deduped). */
70
+ crossPartitionTagKeys: array<string>,
71
+ /** produced eventType -> its indexed (non-payload) tag keys (sorted). */
72
+ tagKeysByEventType: dict<array<string>>,
73
+ /** (sliceName, reason) for slices whose partition couldn't be inferred. */
74
+ ambiguities: array<(string, string)>,
75
+ }
76
+
77
+ /**
78
+ The tag key for a `*Id`-shaped field. A plural `*Ids: array<string>` shares the
79
+ singular producer's key (trailing `s` stripped — `productIds` -> `productId`);
80
+ a scalar `*Id` uses the field name verbatim. Mirrors the PPX's `*Ids` rule.
81
+ */
82
+ let tagKeyOf = (f: idField): string =>
83
+ if f.isList && f.name->String.endsWith("s") {
84
+ f.name->String.slice(~start=0, ~end=f.name->String.length - 1)
85
+ } else {
86
+ f.name
87
+ }
88
+
89
+ let dedupSorted = (keys: array<string>): array<string> => {
90
+ let seen = Set.make()
91
+ keys->Array.forEach(k => seen->Set.add(k))
92
+ Array.fromIterator(seen->Set.values)->Array.toSorted((a, b) => String.compare(a, b))
93
+ }
94
+
95
+ let keysOfEvent = (e: eventShape): array<string> => e.idFields->Array.map(tagKeyOf)
96
+
97
+ let producedKeys = (s: sliceShape): array<string> =>
98
+ dedupSorted(s.produced->Array.flatMap(keysOfEvent))
99
+
100
+ let consumedKeys = (s: sliceShape): array<string> =>
101
+ dedupSorted(s.consumed->Array.flatMap(keysOfEvent))
102
+
103
+ /**
104
+ Keys the command carries as a **scalar** (`*Id: string`, not `*Ids: array<string>`).
105
+ A scalar tag is AND-ed with the partition into one composite clause unless it is
106
+ read cross-partition, so only a scalar foreign reference needs the cross-partition
107
+ fan. A foreign key the command carries *only* as an array already fans per element
108
+ and stays partition-scoped (it reads the foreign entity's own partition) — the
109
+ `PlaceOrder`/`productIds` shape — so it is **not** cross-partition.
110
+ */
111
+ let commandScalarKeys = (s: sliceShape): array<string> =>
112
+ dedupSorted(s.command->Array.filter(f => !f.isList)->Array.map(tagKeyOf))
113
+
114
+ /**
115
+ Infers DCB tag scope for a set of slices (one DCB consistency boundary / plugin).
116
+ Pure and total — never throws; unresolvable partitions land in `ambiguities`.
117
+ */
118
+ /**
119
+ The keys a slice reads from a *foreign* event — a consumed arm whose event type
120
+ the slice does **not** itself produce. These are the candidate cross-entity
121
+ references; they cannot be the slice's own partition. Defined on a single shape
122
+ so it works both globally (in `infer`) and per-slice (in the GWT harness, which
123
+ sees only one slice).
124
+ */
125
+ let foreignConsumedKeys = (s: sliceShape): array<string> => {
126
+ let ownProduced = Set.make()
127
+ s.produced->Array.forEach(e => ownProduced->Set.add(e.eventType))
128
+ dedupSorted(
129
+ s.consumed->Array.flatMap(e => ownProduced->Set.has(e.eventType) ? [] : e->keysOfEvent),
130
+ )
131
+ }
132
+
133
+ /**
134
+ Per-slice cross-partition keys for the test harness, which has no global owner
135
+ map: a foreign-read key that is not the slice's own partition is read across
136
+ partitions. Matches `infer`'s global rule 2 for the common "reference another
137
+ entity" case; the harness unions this with any explicit `@crossPartition`
138
+ annotation so capacity/escape-hatch reads remain covered.
139
+ */
140
+ let crossPartitionForSlice = (s: sliceShape): array<string> => {
141
+ let foreign = foreignConsumedKeys(s)
142
+ let scalar = commandScalarKeys(s)
143
+ let partition = switch s.partitionHint {
144
+ | Some(h) if producedKeys(s)->Array.includes(h) => Some(h)
145
+ | _ =>
146
+ switch producedKeys(s)->Array.filter(k => !(foreign->Array.includes(k))) {
147
+ | [single] => Some(single)
148
+ | _ => None
149
+ }
150
+ }
151
+ // A foreign read is cross-partition only when the command carries the key as a
152
+ // scalar (must be fanned); an array-only foreign key auto-fans partition-scoped.
153
+ foreign->Array.filter(k => Some(k) != partition && scalar->Array.includes(k))
154
+ }
155
+
156
+ let infer = (slices: array<sliceShape>): derived => {
157
+ // Rule 1 — partition(S) = producedKeys(S) \ foreignConsumedKeys(S), where a
158
+ // foreign-consumed key rides a consumed arm the slice does not itself produce.
159
+ // An explicit @partitionTag hint overrides the derivation (escape hatch for
160
+ // slices whose own events legitimately carry two owned keys).
161
+ let partitionBySlice = Dict.make()
162
+ let ambiguities = []
163
+ slices->Array.forEach(s => {
164
+ let produced = producedKeys(s)
165
+ switch s.partitionHint {
166
+ | Some(h) if produced->Array.includes(h) => partitionBySlice->Dict.set(s.sliceName, h)
167
+ | _ =>
168
+ let foreign = foreignConsumedKeys(s)
169
+ switch produced->Array.filter(k => !(foreign->Array.includes(k))) {
170
+ | [single] => partitionBySlice->Dict.set(s.sliceName, single)
171
+ | [] =>
172
+ let _ = ambiguities->Array.push((
173
+ s.sliceName,
174
+ "no own partition key — every produced *Id is read from a foreign producer (pure join?); add an explicit @partitionTag",
175
+ ))
176
+ | many =>
177
+ let _ = ambiguities->Array.push((
178
+ s.sliceName,
179
+ `multiple candidate partition keys (${many->Array.join(", ")}) — add an explicit @partitionTag`,
180
+ ))
181
+ }
182
+ }
183
+ })
184
+
185
+ // owner map + the set of keys that are *some* entity's partition.
186
+ let ownerByKey = Dict.make()
187
+ let ownedPartitionKeys = Set.make()
188
+ slices->Array.forEach(s =>
189
+ switch partitionBySlice->Dict.get(s.sliceName) {
190
+ | Some(k) =>
191
+ ownedPartitionKeys->Set.add(k)
192
+ switch ownerByKey->Dict.get(k) {
193
+ | Some(_) => ()
194
+ | None => ownerByKey->Dict.set(k, s.sliceName)
195
+ }
196
+ | None => ()
197
+ }
198
+ )
199
+
200
+ // Rule 2 — a key read on a foreign consumed event that is another entity's
201
+ // partition (and not this slice's own partition) is cross-partition — but only
202
+ // when the command carries it as a SCALAR. A foreign key the command carries
203
+ // only as an array auto-fans per element and stays partition-scoped (it reads
204
+ // the foreign entity's own partition), so it is not cross-partition.
205
+ let crossKeys = Set.make()
206
+ slices->Array.forEach(s => {
207
+ let own = partitionBySlice->Dict.get(s.sliceName)
208
+ let scalar = commandScalarKeys(s)
209
+ s->consumedKeys->Array.forEach(k =>
210
+ if ownedPartitionKeys->Set.has(k) && Some(k) != own && scalar->Array.includes(k) {
211
+ crossKeys->Set.add(k)
212
+ }
213
+ )
214
+ })
215
+ let crossPartitionTagKeys =
216
+ Array.fromIterator(crossKeys->Set.values)->Array.toSorted((a, b) => String.compare(a, b))
217
+
218
+ // Rule 3 — a produced key is indexed iff it is the producing slice's own
219
+ // partition OR some slice issues a decision read of *that event type* by it
220
+ // (the key appears on a consumed arm naming the event type). Foreign reference
221
+ // keys that nobody reads this event type by are payload ⇒ no GSI write ⇒ the
222
+ // sibling leak is impossible. The read-by-anybody arm is what keeps a composite
223
+ // own-stream read (e.g. ProductDemandRecorded read by orderId) — and an M:N
224
+ // capacity read — correctly indexed without a hand annotation.
225
+ let readKeysByEventType = Dict.make()
226
+ slices->Array.forEach(s =>
227
+ s.consumed->Array.forEach(e => {
228
+ let prev = readKeysByEventType->Dict.get(e.eventType)->Option.getOr([])
229
+ readKeysByEventType->Dict.set(e.eventType, prev->Array.concat(e->keysOfEvent))
230
+ })
231
+ )
232
+ let tagKeysByEventType = Dict.make()
233
+ slices->Array.forEach(s => {
234
+ let own = partitionBySlice->Dict.get(s.sliceName)
235
+ s.produced->Array.forEach(e => {
236
+ let readKeys = readKeysByEventType->Dict.get(e.eventType)->Option.getOr([])
237
+ let indexed =
238
+ e->keysOfEvent->Array.filter(k => Some(k) == own || readKeys->Array.includes(k))
239
+ tagKeysByEventType->Dict.set(e.eventType, dedupSorted(indexed))
240
+ })
241
+ })
242
+
243
+ {partitionBySlice, ownerByKey, crossPartitionTagKeys, tagKeysByEventType, ambiguities}
244
+ }
@@ -0,0 +1,177 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
4
+ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
+ import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
6
+
7
+ function tagKeyOf(f) {
8
+ if (f.isList && f.name.endsWith("s")) {
9
+ return f.name.slice(0, f.name.length - 1 | 0);
10
+ } else {
11
+ return f.name;
12
+ }
13
+ }
14
+
15
+ function dedupSorted(keys) {
16
+ let seen = new Set();
17
+ keys.forEach(k => {
18
+ seen.add(k);
19
+ });
20
+ return Array.from(seen.values()).toSorted(Primitive_string.compare);
21
+ }
22
+
23
+ function keysOfEvent(e) {
24
+ return e.idFields.map(tagKeyOf);
25
+ }
26
+
27
+ function producedKeys(s) {
28
+ return dedupSorted(s.produced.flatMap(keysOfEvent));
29
+ }
30
+
31
+ function consumedKeys(s) {
32
+ return dedupSorted(s.consumed.flatMap(keysOfEvent));
33
+ }
34
+
35
+ function commandScalarKeys(s) {
36
+ return dedupSorted(s.command.filter(f => !f.isList).map(tagKeyOf));
37
+ }
38
+
39
+ function foreignConsumedKeys(s) {
40
+ let ownProduced = new Set();
41
+ s.produced.forEach(e => {
42
+ ownProduced.add(e.eventType);
43
+ });
44
+ return dedupSorted(s.consumed.flatMap(e => {
45
+ if (ownProduced.has(e.eventType)) {
46
+ return [];
47
+ } else {
48
+ return e.idFields.map(tagKeyOf);
49
+ }
50
+ }));
51
+ }
52
+
53
+ function crossPartitionForSlice(s) {
54
+ let foreign = foreignConsumedKeys(s);
55
+ let scalar = commandScalarKeys(s);
56
+ let h = s.partitionHint;
57
+ let partition;
58
+ let exit = 0;
59
+ if (h !== undefined && producedKeys(s).includes(h)) {
60
+ partition = h;
61
+ } else {
62
+ exit = 1;
63
+ }
64
+ if (exit === 1) {
65
+ let match = producedKeys(s).filter(k => !foreign.includes(k));
66
+ partition = match.length !== 1 ? undefined : match[0];
67
+ }
68
+ return foreign.filter(k => {
69
+ if (Primitive_object.notequal(k, partition)) {
70
+ return scalar.includes(k);
71
+ } else {
72
+ return false;
73
+ }
74
+ });
75
+ }
76
+
77
+ function infer(slices) {
78
+ let partitionBySlice = {};
79
+ let ambiguities = [];
80
+ slices.forEach(s => {
81
+ let produced = producedKeys(s);
82
+ let h = s.partitionHint;
83
+ if (h !== undefined && produced.includes(h)) {
84
+ partitionBySlice[s.sliceName] = h;
85
+ return;
86
+ }
87
+ let foreign = foreignConsumedKeys(s);
88
+ let many = produced.filter(k => !foreign.includes(k));
89
+ let len = many.length;
90
+ if (len !== 1) {
91
+ if (len !== 0) {
92
+ ambiguities.push([
93
+ s.sliceName,
94
+ `multiple candidate partition keys (` + many.join(", ") + `) — add an explicit @partitionTag`
95
+ ]);
96
+ } else {
97
+ ambiguities.push([
98
+ s.sliceName,
99
+ "no own partition key — every produced *Id is read from a foreign producer (pure join?); add an explicit @partitionTag"
100
+ ]);
101
+ }
102
+ return;
103
+ }
104
+ let single = many[0];
105
+ partitionBySlice[s.sliceName] = single;
106
+ });
107
+ let ownerByKey = {};
108
+ let ownedPartitionKeys = new Set();
109
+ slices.forEach(s => {
110
+ let k = partitionBySlice[s.sliceName];
111
+ if (k === undefined) {
112
+ return;
113
+ }
114
+ ownedPartitionKeys.add(k);
115
+ let match = ownerByKey[k];
116
+ if (match !== undefined) {
117
+ return;
118
+ } else {
119
+ ownerByKey[k] = s.sliceName;
120
+ return;
121
+ }
122
+ });
123
+ let crossKeys = new Set();
124
+ slices.forEach(s => {
125
+ let own = partitionBySlice[s.sliceName];
126
+ let scalar = commandScalarKeys(s);
127
+ consumedKeys(s).forEach(k => {
128
+ if (ownedPartitionKeys.has(k) && Primitive_object.notequal(k, own) && scalar.includes(k)) {
129
+ crossKeys.add(k);
130
+ return;
131
+ }
132
+ });
133
+ });
134
+ let crossPartitionTagKeys = Array.from(crossKeys.values()).toSorted(Primitive_string.compare);
135
+ let readKeysByEventType = {};
136
+ slices.forEach(s => {
137
+ s.consumed.forEach(e => {
138
+ let prev = Stdlib_Option.getOr(readKeysByEventType[e.eventType], []);
139
+ readKeysByEventType[e.eventType] = prev.concat(e.idFields.map(tagKeyOf));
140
+ });
141
+ });
142
+ let tagKeysByEventType = {};
143
+ slices.forEach(s => {
144
+ let own = partitionBySlice[s.sliceName];
145
+ s.produced.forEach(e => {
146
+ let readKeys = Stdlib_Option.getOr(readKeysByEventType[e.eventType], []);
147
+ let indexed = e.idFields.map(tagKeyOf).filter(k => {
148
+ if (Primitive_object.equal(k, own)) {
149
+ return true;
150
+ } else {
151
+ return readKeys.includes(k);
152
+ }
153
+ });
154
+ tagKeysByEventType[e.eventType] = dedupSorted(indexed);
155
+ });
156
+ });
157
+ return {
158
+ partitionBySlice: partitionBySlice,
159
+ ownerByKey: ownerByKey,
160
+ crossPartitionTagKeys: crossPartitionTagKeys,
161
+ tagKeysByEventType: tagKeysByEventType,
162
+ ambiguities: ambiguities
163
+ };
164
+ }
165
+
166
+ export {
167
+ tagKeyOf,
168
+ dedupSorted,
169
+ keysOfEvent,
170
+ producedKeys,
171
+ consumedKeys,
172
+ commandScalarKeys,
173
+ foreignConsumedKeys,
174
+ crossPartitionForSlice,
175
+ infer,
176
+ }
177
+ /* No side effect */