@reventlessdev/reventless-core 3.0.0-alpha.235 → 3.0.0-alpha.236

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.236 (2026-08-15)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **core:** describe a DCB event log by every event it carries ([047bc0b](https://github.com/ReventlessDev/reventless-core/commit/047bc0beb9be1468410fde37a7f787a3d68f92d2))
11
+ ### Features
12
+
13
+ * **aws:** serve one baked manifest per journey ([16d6307](https://github.com/ReventlessDev/reventless-core/commit/16d63071f059cf1fc7025f9be9206dda51f71053))
14
+
15
+
6
16
  # 3.0.0-alpha.235 (2026-08-15)
7
17
 
8
18
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.235",
3
+ "version": "3.0.0-alpha.236",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -30,15 +30,15 @@
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.32",
32
32
  "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
33
  "@reventlessdev/rescript-node": "2.0.0-alpha.7",
35
34
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
36
35
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
36
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.7",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.142",
39
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.114",
41
- "@reventlessdev/reventless-infra": "3.0.0-alpha.142"
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.114"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -204,6 +204,58 @@ let journeyKey = (~group: string): string => {
204
204
  `component-manifest-${slug}.json`
205
205
  }
206
206
 
207
+ let toSelection = (s: ReventlessInfra.Platform.bakedManifestSelection): selection => {
208
+ plugin: s.plugin,
209
+ views: s.views,
210
+ commands: s.commands,
211
+ derived: s.derived,
212
+ }
213
+
214
+ /** One declared journey, with everything a platform needs to serve it: the group
215
+ it serves, the file it is written to, and the include-list that decides the
216
+ file's contents. */
217
+ type journeyFile = {group: string, key: string, selections: array<selection>}
218
+
219
+ /**
220
+ The declared journeys, in declaration order, with their keys resolved.
221
+
222
+ The default journey is deliberately not among them: it is the file
223
+ `manifestUrl` points at, which is what a caller matching no declared group reads
224
+ and what every deployment had before journeys existed.
225
+ */
226
+ let journeyFiles = (~config: ReventlessInfra.Platform.bakedManifest): array<journeyFile> =>
227
+ config.journeys
228
+ ->Option.getOr([])
229
+ ->Array.map(j => {
230
+ group: j.group,
231
+ key: j.key->Option.getOr(journeyKey(~group=j.group)),
232
+ selections: j.components->Array.map(toSelection),
233
+ })
234
+
235
+ /**
236
+ Every file a bake declaration produces, as (key, selections) pairs.
237
+
238
+ The default journey first and always — it is what a caller matching no declared
239
+ group gets, which includes the no-bearer identity a local dev session starts
240
+ from — then one per declared journey.
241
+
242
+ Here rather than on either platform because several separate things have to
243
+ agree about this set on a deploy: the objects the bake writes, the IAM grant
244
+ that lets it write them, and the URLs `config.json` sends a caller to. A key
245
+ derived twice is a shop whose file is written where nothing fetches it, and the
246
+ symptom is an empty menu rather than a missing file.
247
+ */
248
+ let files = (
249
+ ~config: ReventlessInfra.Platform.bakedManifest,
250
+ ): array<(string, array<selection>)> =>
251
+ [
252
+ (config.key->Option.getOr(defaultKey), config.components->Array.map(toSelection)),
253
+ ]->Array.concat(journeyFiles(~config)->Array.map(j => (j.key, j.selections)))
254
+
255
+ /** Each declared journey's group and the URL a shell fetches its file from. */
256
+ let journeyUrls = (~config: ReventlessInfra.Platform.bakedManifest): array<(string, string)> =>
257
+ journeyFiles(~config)->Array.map(j => (j.group, urlForKey(Some(j.key))))
258
+
207
259
  // The whole file: one entry per selection, in the order the deployment declared
208
260
  // them, so the include-list also states the order a consumer reads plugins in.
209
261
  let curate = (
@@ -192,6 +192,40 @@ function journeyKey(group) {
192
192
  return `component-manifest-` + slug + `.json`;
193
193
  }
194
194
 
195
+ function toSelection(s) {
196
+ return {
197
+ plugin: s.plugin,
198
+ views: s.views,
199
+ commands: s.commands,
200
+ derived: s.derived
201
+ };
202
+ }
203
+
204
+ function journeyFiles(config) {
205
+ return Stdlib_Option.getOr(config.journeys, []).map(j => ({
206
+ group: j.group,
207
+ key: Stdlib_Option.getOr(j.key, journeyKey(j.group)),
208
+ selections: j.components.map(toSelection)
209
+ }));
210
+ }
211
+
212
+ function files(config) {
213
+ return [[
214
+ Stdlib_Option.getOr(config.key, defaultKey),
215
+ config.components.map(toSelection)
216
+ ]].concat(journeyFiles(config).map(j => [
217
+ j.key,
218
+ j.selections
219
+ ]));
220
+ }
221
+
222
+ function journeyUrls(config) {
223
+ return journeyFiles(config).map(j => [
224
+ j.group,
225
+ urlForKey(j.key)
226
+ ]);
227
+ }
228
+
195
229
  function curate(structures, selections) {
196
230
  return Stdlib_Result.map(Stdlib_Array.reduce(selections, {
197
231
  TAG: "Ok",
@@ -228,6 +262,10 @@ export {
228
262
  defaultKey,
229
263
  urlForKey,
230
264
  journeyKey,
265
+ toSelection,
266
+ journeyFiles,
267
+ files,
268
+ journeyUrls,
231
269
  curate,
232
270
  }
233
271
  /* No side effect */
@@ -21,6 +21,52 @@ let toRelativePath: string => string = %raw(`function toRelativePath(moduleUrl)
21
21
  }
22
22
  }`)
23
23
 
24
+ // The tag of one compiled variant arm: `{TAG: "<Name>", …}` for a payload-bearing
25
+ // constructor, a bare string const for a payload-less one.
26
+ let variantTag = (variant: S.t<unknown>) =>
27
+ switch variant {
28
+ | S.Object({properties}) =>
29
+ switch properties->Dict.get("TAG") {
30
+ | Some(String({const: ?Some(tag)})) => Some(tag)
31
+ | _ => None
32
+ }
33
+ | S.String({const: ?Some(tag)}) => Some(tag)
34
+ | _ => None
35
+ }
36
+
37
+ /**
38
+ The event schema of a DCB event log: one union over every event its slices produce.
39
+
40
+ Flattened, not nested. Every reader of an `eventLogSchemaEntry` walks exactly one
41
+ level — `Union` → per-variant `Object` with a `TAG` const — so a union OF unions
42
+ would present each slice as an arm with no tag and read as a log carrying no
43
+ event types at all. Arms are deduped by tag because two slices producing the same
44
+ event would otherwise hand sury two arms with one discriminant.
45
+ */
46
+ let mergedEventSchema = (schemas: array<S.t<unknown>>) =>
47
+ switch schemas {
48
+ | [single] => single
49
+ | many =>
50
+ let seen = Set.make()
51
+ many
52
+ ->Array.flatMap(schema =>
53
+ switch schema {
54
+ | S.Union({anyOf}) => anyOf
55
+ | other => [other]
56
+ }
57
+ )
58
+ ->Array.filter(arm =>
59
+ switch arm->variantTag {
60
+ | Some(tag) if seen->Set.has(tag) => false
61
+ | Some(tag) =>
62
+ seen->Set.add(tag)
63
+ true
64
+ | None => true
65
+ }
66
+ )
67
+ ->S.union
68
+ }
69
+
24
70
  type dcbResult = {
25
71
  dcbEventLogOutputs: option<DcbEventLog.outputs>,
26
72
  stateChangeSlicesOutputs: dict<StateChangeSlice.outputs>,
@@ -1047,7 +1093,11 @@ module Make = (
1047
1093
  }
1048
1094
  })
1049
1095
 
1050
- // Collect all event schemas from produced events for eventLogEntries
1096
+ // Every event this plugin's slices produce lands in the one DCB event log,
1097
+ // so the log's schema entry is the union of all of them. Taking the first
1098
+ // slice's schema as representative left the readers of that entry — the
1099
+ // upload claimer's `CLAIM_REF_FIELDS`, MCP's event-history resource —
1100
+ // describing one slice and silently blind to the rest.
1051
1101
  let allProducedSchemas =
1052
1102
  stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
1053
1103
  Sc.Spec.eventSchema->S.castToUnknown
@@ -1102,12 +1152,11 @@ module Make = (
1102
1152
  ->Array.concat(outboundEntries)
1103
1153
  ->Array.concat(inboundEntries),
1104
1154
  eventLogEntries: if allProducedSchemas->Array.length > 0 {
1105
- // Use the first produced event schema as representative for the event log
1106
1155
  [
1107
1156
  {
1108
1157
  ReventlessInfra.Api.busKey: name ++ "DcbEventLog",
1109
1158
  displayName: name,
1110
- eventSchema: allProducedSchemas->Array.getUnsafe(0),
1159
+ eventSchema: allProducedSchemas->mergedEventSchema,
1111
1160
  },
1112
1161
  ]
1113
1162
  } else {
@@ -48,6 +48,60 @@ let toRelativePath = (function toRelativePath(moduleUrl) {
48
48
  }
49
49
  });
50
50
 
51
+ function variantTag(variant) {
52
+ switch (variant.type) {
53
+ case "string" :
54
+ let tag = variant.const;
55
+ if (tag !== undefined) {
56
+ return tag;
57
+ } else {
58
+ return;
59
+ }
60
+ case "object" :
61
+ let match = variant.properties["TAG"];
62
+ if (match === undefined) {
63
+ return;
64
+ }
65
+ if (match.type !== "string") {
66
+ return;
67
+ }
68
+ let tag$1 = match.const;
69
+ if (tag$1 !== undefined) {
70
+ return tag$1;
71
+ } else {
72
+ return;
73
+ }
74
+ default:
75
+ return;
76
+ }
77
+ }
78
+
79
+ function mergedEventSchema(schemas) {
80
+ if (schemas.length === 1) {
81
+ return schemas[0];
82
+ }
83
+ let seen = new Set();
84
+ return S.union(schemas.flatMap(schema => {
85
+ if (schema.type === "union") {
86
+ return schema.anyOf;
87
+ } else {
88
+ return [schema];
89
+ }
90
+ }).filter(arm => {
91
+ let tag = variantTag(arm);
92
+ if (tag !== undefined) {
93
+ if (seen.has(tag)) {
94
+ return false;
95
+ } else {
96
+ seen.add(tag);
97
+ return true;
98
+ }
99
+ } else {
100
+ return true;
101
+ }
102
+ }));
103
+ }
104
+
51
105
  let emptyResult_stateChangeSlicesOutputs = {};
52
106
 
53
107
  let emptyResult_stateViewSlicesOutputs = {};
@@ -583,7 +637,7 @@ function Make(DcbEventLogStorage) {
583
637
  eventLogEntries: allProducedSchemas.length !== 0 ? [{
584
638
  busKey: name + "DcbEventLog",
585
639
  displayName: name,
586
- eventSchema: allProducedSchemas[0]
640
+ eventSchema: mergedEventSchema(allProducedSchemas)
587
641
  }] : []
588
642
  };
589
643
  };
@@ -596,6 +650,8 @@ function Make(DcbEventLogStorage) {
596
650
  export {
597
651
  log,
598
652
  toRelativePath,
653
+ variantTag,
654
+ mergedEventSchema,
599
655
  emptyResult,
600
656
  Make,
601
657
  }
@@ -0,0 +1,77 @@
1
+ // What a DCB event log tells its readers it carries.
2
+ //
3
+ // A plugin's slices all append to ONE log, but each slice declares only its own
4
+ // events. The log's schema entry used to be the first slice's schema, taken as
5
+ // representative — and every reader of that entry believed it: the upload
6
+ // claimer granted and untagged refs for one slice's events, and MCP described
7
+ // the log's history as carrying that slice's event types and no others. Nothing
8
+ // failed; the rest was simply invisible, which for the claimer means objects
9
+ // that keep their pending tag and expire while an event references them.
10
+ //
11
+ // So the assertions here are about completeness, and about the shape that
12
+ // completeness has to arrive in: readers walk exactly one level, `Union` →
13
+ // per-variant `Object` with a `TAG`, so a union of unions would be as blind as
14
+ // the single schema it replaced while looking like it covered everything.
15
+
16
+ open JestGlobals
17
+
18
+ let variant = (~tag: string, fields: array<(string, S.t<unknown>)>): S.t<unknown> =>
19
+ S.object(s => {
20
+ let _ = s.field("TAG", S.literal(tag)->S.castToUnknown)
21
+ fields->Array.forEach(((name, schema)) => {
22
+ let _ = s.field(name, schema)
23
+ })
24
+ ()
25
+ })->S.castToUnknown
26
+
27
+ let ref_ = (~store: string) => Reventless.StorageRef.forStore(~store)->S.castToUnknown
28
+
29
+ let addCategory = S.union([
30
+ variant(~tag="CategoryAdded", [("imageUrl", ref_(~store="categoryImages"))]),
31
+ ])->S.castToUnknown
32
+
33
+ let addProduct = S.union([
34
+ variant(~tag="ProductAdded", [("imageUrl", ref_(~store="productImages"))]),
35
+ ])->S.castToUnknown
36
+
37
+ let renameCategory = S.union([
38
+ variant(~tag="CategoryRenamed", [("name", S.string->S.castToUnknown)]),
39
+ ])->S.castToUnknown
40
+
41
+ describe("Dcb_Builder.mergedEventSchema", () => {
42
+ testSync("carries every slice's event types, not the first slice's", () => {
43
+ let merged = [addCategory, addProduct, renameCategory]->Dcb_Builder.mergedEventSchema
44
+ expect(Reventless.DcbTag.extractVariantNames(merged))->toEqual([
45
+ "CategoryAdded",
46
+ "ProductAdded",
47
+ "CategoryRenamed",
48
+ ])
49
+ })
50
+
51
+ // The claimer's whole input. One slice short here is a store's objects
52
+ // expiring under a live reference.
53
+ testSync("surfaces the ref fields of every slice that declares one", () => {
54
+ let merged = [addCategory, addProduct, renameCategory]->Dcb_Builder.mergedEventSchema
55
+ expect(
56
+ StorageRefFields.fromEventSchema(~plugin="Catalog", merged)->Array.map(e => (
57
+ e.eventType,
58
+ e.fields->Array.map(f => f.store),
59
+ )),
60
+ )->toEqual([
61
+ ("CategoryAdded", ["Catalog.categoryImages"]),
62
+ ("ProductAdded", ["Catalog.productImages"]),
63
+ ])
64
+ })
65
+
66
+ // Two slices producing the same event would otherwise hand sury two arms
67
+ // under one discriminant, and every reader a duplicate.
68
+ testSync("keeps one arm per event type", () => {
69
+ let merged = [addProduct, addProduct]->Dcb_Builder.mergedEventSchema
70
+ expect(Reventless.DcbTag.extractVariantNames(merged))->toEqual(["ProductAdded"])
71
+ })
72
+
73
+ testSync("leaves a lone slice's schema as it is", () => {
74
+ let merged = [addProduct]->Dcb_Builder.mergedEventSchema
75
+ expect(Reventless.DcbTag.extractVariantNames(merged))->toEqual(["ProductAdded"])
76
+ })
77
+ })
@@ -0,0 +1,90 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as S from "sury/src/S.res.mjs";
4
+ import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
5
+ import * as StorageRef$Reventless from "@reventlessdev/reventless-spec/src/semantic/StorageRef.res.mjs";
6
+ import * as Dcb_Builder$ReventlessCore from "../../src/components/Dcb/Dcb_Builder.res.mjs";
7
+ import * as StorageRefFields$ReventlessCore from "../../src/components/Api/StorageRefFields.res.mjs";
8
+
9
+ function variant(tag, fields) {
10
+ return S.object(s => {
11
+ s.f("TAG", S.literal(tag));
12
+ fields.forEach(param => {
13
+ s.f(param[0], param[1]);
14
+ });
15
+ });
16
+ }
17
+
18
+ function ref_(store) {
19
+ return StorageRef$Reventless.forStore(undefined, store);
20
+ }
21
+
22
+ let addCategory = S.union([variant("CategoryAdded", [[
23
+ "imageUrl",
24
+ StorageRef$Reventless.forStore(undefined, "categoryImages")
25
+ ]])]);
26
+
27
+ let addProduct = S.union([variant("ProductAdded", [[
28
+ "imageUrl",
29
+ StorageRef$Reventless.forStore(undefined, "productImages")
30
+ ]])]);
31
+
32
+ let renameCategory = S.union([variant("CategoryRenamed", [[
33
+ "name",
34
+ S.string
35
+ ]])]);
36
+
37
+ globalThis.describe("Dcb_Builder.mergedEventSchema", () => {
38
+ globalThis.test("carries every slice's event types, not the first slice's", () => {
39
+ let merged = Dcb_Builder$ReventlessCore.mergedEventSchema([
40
+ addCategory,
41
+ addProduct,
42
+ renameCategory
43
+ ]);
44
+ globalThis.expect(DcbTag$Reventless.extractVariantNames(merged)).toEqual([
45
+ "CategoryAdded",
46
+ "ProductAdded",
47
+ "CategoryRenamed"
48
+ ]);
49
+ });
50
+ globalThis.test("surfaces the ref fields of every slice that declares one", () => {
51
+ let merged = Dcb_Builder$ReventlessCore.mergedEventSchema([
52
+ addCategory,
53
+ addProduct,
54
+ renameCategory
55
+ ]);
56
+ globalThis.expect(StorageRefFields$ReventlessCore.fromEventSchema("Catalog", merged).map(e => [
57
+ e.eventType,
58
+ e.fields.map(f => f.store)
59
+ ])).toEqual([
60
+ [
61
+ "CategoryAdded",
62
+ ["Catalog.categoryImages"]
63
+ ],
64
+ [
65
+ "ProductAdded",
66
+ ["Catalog.productImages"]
67
+ ]
68
+ ]);
69
+ });
70
+ globalThis.test("keeps one arm per event type", () => {
71
+ let merged = Dcb_Builder$ReventlessCore.mergedEventSchema([
72
+ addProduct,
73
+ addProduct
74
+ ]);
75
+ globalThis.expect(DcbTag$Reventless.extractVariantNames(merged)).toEqual(["ProductAdded"]);
76
+ });
77
+ globalThis.test("leaves a lone slice's schema as it is", () => {
78
+ let merged = Dcb_Builder$ReventlessCore.mergedEventSchema([addProduct]);
79
+ globalThis.expect(DcbTag$Reventless.extractVariantNames(merged)).toEqual(["ProductAdded"]);
80
+ });
81
+ });
82
+
83
+ export {
84
+ variant,
85
+ ref_,
86
+ addCategory,
87
+ addProduct,
88
+ renameCategory,
89
+ }
90
+ /* addCategory Not a pure module */