@reventlessdev/reventless-core 3.0.0-alpha.206 → 3.0.0-alpha.207
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 +8 -0
- package/package.json +5 -5
- package/src/admin/Platform_Admin.res +10 -0
- package/src/admin/Platform_Admin.res.mjs +5 -1
- package/src/components/Dcb/Dcb_Builder.res +11 -1
- package/src/components/Dcb/Dcb_Builder.res.mjs +3 -2
- package/src/plugin/component/Plugin_Builder.res +10 -0
- package/src/plugin/component/Plugin_Builder.res.mjs +5 -1
- package/tests/api/GeocodingTest.res +36 -5
- package/tests/api/GeocodingTest.res.mjs +17 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
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.207 (2026-08-03)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **dcb:** let the produced/consumed check see aggregate producers ([0075360](https://github.com/ReventlessDev/reventless-core/commit/0075360fc5915b166ab25b6acca2c8580e5b4ecb))
|
|
11
|
+
* **geocoding:** calibrate the confidence rule against a real index ([cd72b27](https://github.com/ReventlessDev/reventless-core/commit/cd72b2741460c871db2915a8603f7837b8191187))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.206 (2026-08-03)
|
|
7
15
|
|
|
8
16
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reventlessdev/reventless-core",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.207",
|
|
4
4
|
"description": "Core package for Reventless framework",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"jest": {
|
|
@@ -29,16 +29,16 @@
|
|
|
29
29
|
"sury": "11.0.0-alpha.4",
|
|
30
30
|
"uuid": "^13.0.0",
|
|
31
31
|
"@reventlessdev/rescript-effect": "0.1.0-alpha.32",
|
|
32
|
-
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.1",
|
|
33
32
|
"@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
|
|
34
33
|
"@reventlessdev/rescript-node": "2.0.0-alpha.1",
|
|
35
34
|
"@reventlessdev/rescript-jest": "1.0.0-alpha.10",
|
|
35
|
+
"@reventlessdev/rescript-fast-csv": "2.0.0-alpha.1",
|
|
36
36
|
"@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.18",
|
|
37
|
+
"@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
|
|
37
38
|
"@reventlessdev/rescript-ssh2": "2.0.0-alpha.1",
|
|
39
|
+
"@reventlessdev/reventless-infra": "3.0.0-alpha.124",
|
|
38
40
|
"@reventlessdev/reventless-interop": "3.0.0-alpha.30",
|
|
39
|
-
"@reventlessdev/reventless-
|
|
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 => {
|
|
@@ -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(
|
|
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
|
}
|
|
@@ -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
|
|
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
|
|
39
|
-
//
|
|
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
|
|
42
|
-
candidate(~label="Springfield, MA", ~relevance=0
|
|
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
|
|
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
|
|
41
|
-
candidate("Springfield, MA", 0
|
|
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
|
});
|