@reventlessdev/reventless-core 3.0.0-alpha.206 → 3.0.0-alpha.208

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,21 @@
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.208 (2026-08-03)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **aws:** make the slice callback protocol one declaration, not three ([d49e543](https://github.com/ReventlessDev/reventless-core/commit/d49e543f4715c955c75cec8bd691ee0927b7e2f4))
11
+
12
+
13
+ # 3.0.0-alpha.207 (2026-08-03)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **dcb:** let the produced/consumed check see aggregate producers ([0075360](https://github.com/ReventlessDev/reventless-core/commit/0075360fc5915b166ab25b6acca2c8580e5b4ecb))
18
+ * **geocoding:** calibrate the confidence rule against a real index ([cd72b27](https://github.com/ReventlessDev/reventless-core/commit/cd72b2741460c871db2915a8603f7837b8191187))
19
+
20
+
6
21
  # 3.0.0-alpha.206 (2026-08-03)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.206",
3
+ "version": "3.0.0-alpha.208",
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.1",
33
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
33
34
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
34
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.1",
35
- "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.1",
38
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.124",
39
+ "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
38
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.30",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.123",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.98",
41
- "@reventlessdev/rescript-uuid": "2.0.0-alpha.0"
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.99"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -140,6 +140,15 @@ module Make = (
140
140
  aggregates->createAggregatesWithoutEventMappers(~api, ~componentRuntime=Dict.make(), opts)
141
141
  let aggregateEventTopics = Aggregate.allEventTopics(aggregatesWithoutEventMappers)
142
142
 
143
+ // As in Plugin_Builder: what the admin aggregates emit, so a slice naming one
144
+ // as its source is checked against real producers rather than reported as
145
+ // consuming events nothing produces.
146
+ let aggregateProducedEvents =
147
+ aggregates->Array.map((module(A: ReventlessInfra.Aggregate.T with type api = api)) => (
148
+ A.Spec.name,
149
+ A.Spec.eventSchema->S.castToUnknown,
150
+ ))
151
+
143
152
  // Wire CommandGenerator → AppSync / in-memory GraphQL resolvers via
144
153
  // `mutationResolverHook` and populate `aggregateMutationFieldsRegistry`.
145
154
  // The matching SDL entries are owned by
@@ -159,6 +168,7 @@ module Make = (
159
168
  // Domain API — routes the provider hook to `hooks.adminApi`.
160
169
  ~onAdminApi=true,
161
170
  ~aggregateEventTopics,
171
+ ~aggregateProducedEvents,
162
172
  ~stateChangeSlices,
163
173
  ~stateViewSlices,
164
174
  ~automationSlices,
@@ -73,8 +73,12 @@ function Make(RuntimeEnvironment) {
73
73
  });
74
74
  let aggregatesWithoutEventMappers = Builder_Helpers$ReventlessCore.createAggregatesWithoutEventMappers(aggregates, api, {}, opts);
75
75
  let aggregateEventTopics = Aggregate$ReventlessCore.allEventTopics(aggregatesWithoutEventMappers);
76
+ let aggregateProducedEvents = aggregates.map(A => [
77
+ A.Spec.name,
78
+ A.Spec.eventSchema
79
+ ]);
76
80
  Plugin_Helpers$ReventlessCore.registerAdminAggregateMutations(aggregates, Config.hooks);
77
- let dcbResult = DcbBuilder.construct(Platform_Admin_Structure$ReventlessCore.pluginId, Platform_Admin_Structure$ReventlessCore.pluginId, "Platform", true, undefined, undefined, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, undefined, undefined, undefined, opts);
81
+ let dcbResult = DcbBuilder.construct(Platform_Admin_Structure$ReventlessCore.pluginId, Platform_Admin_Structure$ReventlessCore.pluginId, "Platform", true, undefined, undefined, aggregateEventTopics, aggregateProducedEvents, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, undefined, undefined, undefined, opts);
78
82
  let slicePublishJsons = dcbResult.dcbPublishJsons;
79
83
  if (slicePublishJsons !== undefined) {
80
84
  stateChangeSlices.forEach(Sc => {
@@ -31,6 +31,30 @@ type todoRow = {
31
31
  retryCount: int,
32
32
  }
33
33
 
34
+ /**
35
+ The runtime protocol between a built callback and whoever drives it — the same
36
+ arrangement as `OutboundTranslationSlice_Callback`, and for the same reason. The
37
+ AWS entry point builds callbacks dynamically and has to ascribe a structural type
38
+ at the JS boundary; pointing that ascription at these aliases is what keeps it
39
+ from becoming an unreconcilable second declaration of the shape.
40
+
41
+ Unlike outbound, phase 1 here takes *raw* envelope JSONs: the callback routes by
42
+ `meta.service` and unwraps `{id, meta, event}` itself, per-source, through the
43
+ registered Mappings.
44
+ */
45
+ type phase1 = (array<JSON.t>, Reventless.AutomationSlice.context) => unit
46
+
47
+ /** Phase 2: process all pending items, publishing commands. */
48
+ type phase2 = ReventlessInfra.CommandTopic.publishJsons => promise<unit>
49
+
50
+ /** The whole protocol as a value, for a driver that holds a built callback and
51
+ knows nothing of the Spec behind it. */
52
+ type runtime = {
53
+ todoItems: Dict.t<todoRow>,
54
+ phase1: phase1,
55
+ phase2: phase2,
56
+ }
57
+
34
58
  module type T = {
35
59
  module Spec: Reventless.AutomationSlice.Spec
36
60
  module Automation: Reventless.AutomationSlice.Automation with module Spec := Spec
@@ -40,10 +64,10 @@ module type T = {
40
64
 
41
65
  /** Phase 1: update TODO list from a batch of raw event JSONs. Internally
42
66
  decodes per-source via the registered Mappings. */
43
- let phase1: (array<JSON.t>, Reventless.AutomationSlice.context) => unit
67
+ let phase1: phase1
44
68
 
45
69
  /** Phase 2: process all pending items, publishing commands via publishJsons. */
46
- let phase2: ReventlessInfra.CommandTopic.publishJsons => promise<unit>
70
+ let phase2: phase2
47
71
  }
48
72
 
49
73
  module Make = (
@@ -87,6 +87,13 @@ module Make = (
87
87
  ~environment: string="",
88
88
  ~platformName: string="",
89
89
  ~aggregateEventTopics: EventTopic.allOutputs=Dict.make(),
90
+ // `(Aggregate name, its event schema)` for every aggregate in this plugin.
91
+ // Feeds the produced/consumed check only — an outbound or automation slice
92
+ // may name an Aggregate as a source, and without these its events look like
93
+ // events nothing produces. Deliberately *not* folded into `produced`, which
94
+ // also drives DCB GSI creation: an aggregate's events live on its own
95
+ // EventLog and must not mint indexes on the DCB table.
96
+ ~aggregateProducedEvents: array<(string, S.t<unknown>)>=[],
90
97
  ~stateChangeSlices: array<module(StateChangeSlice.T)>,
91
98
  ~stateViewSlices: array<module(StateViewSlice.T)>,
92
99
  ~automationSlices: array<module(AutomationSlice.T)>,
@@ -145,7 +152,10 @@ module Make = (
145
152
  ),
146
153
  )
147
154
 
148
- switch Reventless.DcbValidation.validateProducedAndConsumed(~produced, ~consumed) {
155
+ switch Reventless.DcbValidation.validateProducedAndConsumed(
156
+ ~produced=produced->Array.concat(aggregateProducedEvents),
157
+ ~consumed,
158
+ ) {
149
159
  | Error(errors) =>
150
160
  errors->Array.forEach(err =>
151
161
  log.error(~comp="Dcb_Builder", `DCB validation error (${err.sliceName}): ${err.message}`)
@@ -83,12 +83,13 @@ let emptyResult = {
83
83
 
84
84
  function Make(DcbEventLogStorage) {
85
85
  return DcbEventTopicPublisher => (DcbCommandTopicChannel => (DcbCommandTopicChannelAsync => (RuntimeBuilder => (HooksConfig => {
86
- let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, componentRuntimeOpt, pluginStructure, opts) => {
86
+ let construct = (name, childName, apiNamePrefixOpt, onAdminApiOpt, environmentOpt, platformNameOpt, aggregateEventTopicsOpt, aggregateProducedEventsOpt, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponentsOpt, componentRuntimeOpt, pluginStructure, opts) => {
87
87
  let apiNamePrefix = apiNamePrefixOpt !== undefined ? apiNamePrefixOpt : name;
88
88
  let onAdminApi = onAdminApiOpt !== undefined ? onAdminApiOpt : false;
89
89
  let environment = environmentOpt !== undefined ? environmentOpt : "";
90
90
  let platformName = platformNameOpt !== undefined ? platformNameOpt : "";
91
91
  let aggregateEventTopics = aggregateEventTopicsOpt !== undefined ? aggregateEventTopicsOpt : ({});
92
+ let aggregateProducedEvents = aggregateProducedEventsOpt !== undefined ? aggregateProducedEventsOpt : [];
92
93
  let systemCallableComponents = systemCallableComponentsOpt !== undefined ? systemCallableComponentsOpt : [];
93
94
  let componentRuntime = componentRuntimeOpt !== undefined ? componentRuntimeOpt : ({});
94
95
  let hasDcb = stateChangeSlices.length !== 0;
@@ -119,7 +120,7 @@ function Make(DcbEventLogStorage) {
119
120
  O.Spec.name,
120
121
  O.Spec.consumedEventSchema
121
122
  ]));
122
- let errors = DcbValidation$Reventless.validateProducedAndConsumed(produced, consumed);
123
+ let errors = DcbValidation$Reventless.validateProducedAndConsumed(produced.concat(aggregateProducedEvents), consumed);
123
124
  if (errors.TAG !== "Ok") {
124
125
  errors._0.forEach(err => log.error("Dcb_Builder", undefined, `DCB validation error (` + err.sliceName + `): ` + err.message));
125
126
  }
@@ -22,6 +22,34 @@ type todoRow = {
22
22
  lastError?: string,
23
23
  }
24
24
 
25
+ /**
26
+ The runtime protocol between a built callback and whoever drives it.
27
+
28
+ Defined here, once, and referenced by every party — `module type T` below, the
29
+ in-process builder, and the AWS entry point, which cannot name `T` because it
30
+ builds callbacks dynamically per `HANDLER_CONFIG` entry and must ascribe a
31
+ structural type at the JS boundary.
32
+
33
+ That ascription is the reason these aliases exist. A driver that restates the
34
+ shape instead of referencing it becomes a second source of truth that the
35
+ compiler cannot reconcile with this one: when `phase1` gained its `sourceId`,
36
+ the AWS entry point kept passing bare events and nothing complained, because its
37
+ hand-written copy was internally consistent. The mismatch surfaced only at
38
+ runtime, as `undefined` destructured from a tuple that was never a tuple.
39
+ */
40
+ type phase1<'event> = array<(string, 'event)> => unit
41
+
42
+ /** Phase 2: translate all pending items, optionally publishing commands. */
43
+ type phase2 = ReventlessInfra.CommandTopic.publishJsons => promise<unit>
44
+
45
+ /** The whole protocol as a value — what a driver holding a built callback needs,
46
+ with no knowledge of the Spec it was built from. */
47
+ type runtime<'event> = {
48
+ todoItems: Dict.t<todoRow>,
49
+ phase1: phase1<'event>,
50
+ phase2: phase2,
51
+ }
52
+
25
53
  module type T = {
26
54
  module Spec: Reventless.OutboundTranslationSlice.Spec
27
55
  module Translation: Reventless.OutboundTranslationSlice.Translation with module Spec := Spec
@@ -32,10 +60,10 @@ module type T = {
32
60
  /** Phase 1: update TODO list from a batch of `(sourceId, event)` pairs
33
61
  (collect only, no resolve). `sourceId` is the envelope id of the entity the
34
62
  event was published for. */
35
- let phase1: array<(string, Spec.consumedEvent)> => unit
63
+ let phase1: phase1<Spec.consumedEvent>
36
64
 
37
65
  /** Phase 2: translate all pending items, optionally publishing commands via publishJsons. */
38
- let phase2: ReventlessInfra.CommandTopic.publishJsons => promise<unit>
66
+ let phase2: phase2
39
67
  }
40
68
 
41
69
  module Make = (
@@ -143,6 +143,15 @@ module Make = (
143
143
  aggregates->createAggregatesWithoutEventMappers(~api, ~componentRuntime, opts)
144
144
  let aggregateEventTopics = Aggregate.allEventTopics(aggregatesWithoutEventMappers)
145
145
 
146
+ // What each aggregate emits, for the DCB produced/consumed check. A slice
147
+ // naming an Aggregate as its source consumes those events, and the check
148
+ // otherwise sees no producer for them and reports correct code as broken.
149
+ let aggregateProducedEvents =
150
+ aggregates->Array.map((module(A: ReventlessInfra.Aggregate.T with type api = api)) => (
151
+ A.Spec.name,
152
+ A.Spec.eventSchema->S.castToUnknown,
153
+ ))
154
+
146
155
  // Construct DCB components and derive DCB-specific API schema entries
147
156
  module DcbBuilder = Dcb_Builder.Make(
148
157
  DcbEventLogStorage,
@@ -158,6 +167,7 @@ module Make = (
158
167
  ~environment=Spec.environment,
159
168
  ~platformName=Spec.platformName,
160
169
  ~aggregateEventTopics,
170
+ ~aggregateProducedEvents,
161
171
  ~stateChangeSlices,
162
172
  ~stateViewSlices,
163
173
  ~automationSlices,
@@ -149,10 +149,14 @@ function Make(Spec) {
149
149
  let childName = ComponentType$ReventlessCore.name(extra$1, Plugin$ReventlessCore.componentType);
150
150
  let aggregatesWithoutEventMappers = Plugin_Helpers$ReventlessCore.createAggregatesWithoutEventMappers(aggregates, api, componentRuntime, opts);
151
151
  let aggregateEventTopics = Aggregate$ReventlessCore.allEventTopics(aggregatesWithoutEventMappers);
152
+ let aggregateProducedEvents = aggregates.map(A => [
153
+ A.Spec.name,
154
+ A.Spec.eventSchema
155
+ ]);
152
156
  let DcbBuilder = Dcb_Builder$ReventlessCore.Make(DcbEventLogStorage)(DcbEventTopicPublisher)(DcbCommandTopicChannel)(DcbCommandTopicChannelAsync)(PluginRuntimeBuilder)({
153
157
  hooks: Spec.hooks
154
158
  });
155
- let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, componentRuntime, pluginStructure, opts);
159
+ let dcbResult = DcbBuilder.construct(extra$1, childName, undefined, undefined, Spec.environment, Spec.platformName, aggregateEventTopics, aggregateProducedEvents, stateChangeSlices, stateViewSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, systemCallableComponents, componentRuntime, pluginStructure, opts);
156
160
  let mutationEntriesFromAggregates = aggregates.flatMap(M => {
157
161
  let commandSchema = M.Spec.commandSchema;
158
162
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
@@ -24,7 +24,7 @@ let candidate = (~label, ~relevance=?): Geocoding.candidate => {
24
24
 
25
25
  describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
26
26
  testSync("a clear winner is the answer", () => {
27
- let top = candidate(~label="Stephansplatz 1, Vienna", ~relevance=0.97)
27
+ let top = candidate(~label="Stephansplatz 1, Vienna", ~relevance=1.0)
28
28
  let result = Geocoding.confidentMatch([top, candidate(~label="Elsewhere", ~relevance=0.4)])
29
29
  expect(result->Option.map(c => c.Geocoding.label))->toEqual(Some("Stephansplatz 1, Vienna"))
30
30
  })
@@ -35,11 +35,42 @@ describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
35
35
  })
36
36
 
37
37
  testSync("two near-equal candidates are ambiguous, not a winner", () => {
38
- // The bare-town-name case: the service matched several places about equally
39
- // well, and picking the first would pin a confident marker in the wrong one.
38
+ // The bare-town-name case, with the scores the live Esri index actually
39
+ // returns for it: five states at exactly 1.0. Genuine ambiguity there is a
40
+ // tie, not a near miss — which is why the margin is small and this case is
41
+ // still caught.
40
42
  let result = Geocoding.confidentMatch([
41
- candidate(~label="Springfield, IL", ~relevance=0.91),
42
- candidate(~label="Springfield, MA", ~relevance=0.89),
43
+ candidate(~label="Springfield, IL", ~relevance=1.0),
44
+ candidate(~label="Springfield, MA", ~relevance=1.0),
45
+ ])
46
+ expect(result->Option.isNone)->toBe(true)
47
+ })
48
+
49
+ testSync("a correct match with a plausible runner-up is still a winner", () => {
50
+ // Measured against the live index: "Baker Street 221B, London NW1 6XE"
51
+ // returns the right building — matching postcode — at 0.991, with an
52
+ // unrelated Baker Street at 0.962. The previous calibration (floor 0.8,
53
+ // margin 0.1) read that 0.029 gap as ambiguity and declined, so a perfectly
54
+ // good address was written off as unresolvable and, per the retry policy,
55
+ // never tried again. Three of twelve realistic addresses failed this way.
56
+ let result = Geocoding.confidentMatch([
57
+ candidate(~label="221 Baker Street, Marylebone, London NW1 6XE", ~relevance=0.991),
58
+ candidate(~label="Baker Street, elsewhere", ~relevance=0.962),
59
+ ])
60
+ expect(result->Option.map(c => c.Geocoding.label))->toEqual(
61
+ Some("221 Baker Street, Marylebone, London NW1 6XE"),
62
+ )
63
+ })
64
+
65
+ testSync("a wrong street in the right city is declined", () => {
66
+ // The other half of the same measurement, and the reason the floor moved up
67
+ // rather than the margin merely moving down: "Kaiserstrasse 12, 4020 Linz"
68
+ // resolves to *Rainerstrasse* 12 in Linz at 0.958. Its gap to the runner-up
69
+ // is 0.031 — wider than the correct match above — so no margin can separate
70
+ // the two. Only the absolute score can.
71
+ let result = Geocoding.confidentMatch([
72
+ candidate(~label="Rainerstraße 12, 4020 Linz", ~relevance=0.958),
73
+ candidate(~label="Somewhere else, Linz", ~relevance=0.927),
43
74
  ])
44
75
  expect(result->Option.isNone)->toBe(true)
45
76
  })
@@ -24,7 +24,7 @@ function candidate(label, relevance) {
24
24
 
25
25
  globalThis.describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
26
26
  globalThis.test("a clear winner is the answer", () => {
27
- let top = candidate("Stephansplatz 1, Vienna", 0.97);
27
+ let top = candidate("Stephansplatz 1, Vienna", 1.0);
28
28
  let result = Geocoding$Reventless.confidentMatch([
29
29
  top,
30
30
  candidate("Elsewhere", 0.4)
@@ -37,8 +37,22 @@ globalThis.describe("Geocoder_AwsLocation_Geocoding.confidentMatch", () => {
37
37
  });
38
38
  globalThis.test("two near-equal candidates are ambiguous, not a winner", () => {
39
39
  let result = Geocoding$Reventless.confidentMatch([
40
- candidate("Springfield, IL", 0.91),
41
- candidate("Springfield, MA", 0.89)
40
+ candidate("Springfield, IL", 1.0),
41
+ candidate("Springfield, MA", 1.0)
42
+ ], undefined, undefined);
43
+ globalThis.expect(Stdlib_Option.isNone(result)).toBe(true);
44
+ });
45
+ globalThis.test("a correct match with a plausible runner-up is still a winner", () => {
46
+ let result = Geocoding$Reventless.confidentMatch([
47
+ candidate("221 Baker Street, Marylebone, London NW1 6XE", 0.991),
48
+ candidate("Baker Street, elsewhere", 0.962)
49
+ ], undefined, undefined);
50
+ globalThis.expect(Stdlib_Option.map(result, c => c.label)).toEqual("221 Baker Street, Marylebone, London NW1 6XE");
51
+ });
52
+ globalThis.test("a wrong street in the right city is declined", () => {
53
+ let result = Geocoding$Reventless.confidentMatch([
54
+ candidate("Rainerstraße 12, 4020 Linz", 0.958),
55
+ candidate("Somewhere else, Linz", 0.927)
42
56
  ], undefined, undefined);
43
57
  globalThis.expect(Stdlib_Option.isNone(result)).toBe(true);
44
58
  });