@reventlessdev/reventless-spec 3.0.0-alpha.119 → 3.0.0-alpha.120
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 +7 -0
- package/package.json +1 -1
- package/src/semantic/Geocoding.res +62 -83
- package/src/semantic/Geocoding.res.mjs +42 -8
- package/src/semantic/Geolocation.res +81 -0
- package/src/semantic/Geolocation.res.mjs +104 -0
- package/src/semantic/Semantic.res +35 -87
- package/src/semantic/Semantic.res.mjs +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
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.120 (2026-08-20)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **spec:** type a geocoder's answer as one Geolocation value ([157be7a](https://github.com/ReventlessDev/reventless-core/commit/157be7aca4806ae19dba1f58979af13b88dc1821))
|
|
11
|
+
|
|
12
|
+
|
|
6
13
|
# 3.0.0-alpha.119 (2026-08-20)
|
|
7
14
|
|
|
8
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
Turning an address into a point, and deciding whether to believe the answer.
|
|
3
3
|
|
|
4
|
-
The
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
a lookup can fail, and the rule for when a ranked list is confident enough to
|
|
8
|
-
write a coordinate into an event log.
|
|
9
|
-
|
|
10
|
-
That split matters because the confidence rule is the part that is easy to get
|
|
11
|
-
wrong and expensive to get wrong twice. A caller that re-derives it — takes
|
|
12
|
-
`results[0]` because the list was ranked — produces a plausible marker in the
|
|
13
|
-
wrong region, drawn without an error anywhere. Deciding it once, here, is what
|
|
14
|
-
stops each transport inventing its own answer.
|
|
4
|
+
The transport is provider-specific and lives with its provider. What is here is
|
|
5
|
+
provider-neutral: the shape of an answer, the two ways a lookup fails, and the
|
|
6
|
+
confidence rule — decided once, so no transport invents its own.
|
|
15
7
|
*/
|
|
16
8
|
|
|
17
9
|
/** One candidate a geocoder returned. */
|
|
@@ -19,25 +11,13 @@ type candidate = {
|
|
|
19
11
|
/** The provider's canonical rendering of the address it matched. */
|
|
20
12
|
label: string,
|
|
21
13
|
point: GeoPoint.t,
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
`None` when the provider does not score its results. That is not the same as a
|
|
26
|
-
low score and must not be read as a high one — `confidentMatch` declines it,
|
|
27
|
-
because an unscored list cannot support an unattended decision.
|
|
28
|
-
*/
|
|
14
|
+
/** How well this matches, 0…1. `None` when the provider does not score, which
|
|
15
|
+
`confidentMatch` declines rather than reading as high. */
|
|
29
16
|
relevance: option<float>,
|
|
30
17
|
}
|
|
31
18
|
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
Two constructors rather than one message because the caller's retry decision
|
|
36
|
-
turns on exactly this distinction, and a `switch` is the only form of it that
|
|
37
|
-
cannot be got wrong. A translator that cannot tell "no such address" from "the
|
|
38
|
-
service is down" turns one outage into a permanent verdict on every address in
|
|
39
|
-
flight.
|
|
40
|
-
*/
|
|
19
|
+
/** Why a lookup produced no usable point. Two constructors because the retry
|
|
20
|
+
decision turns on the distinction: an outage must not become a verdict. */
|
|
41
21
|
type failure =
|
|
42
22
|
| /** The provider could not be reached, or refused the call. Retry. */
|
|
43
23
|
Unavailable(string)
|
|
@@ -45,83 +25,82 @@ type failure =
|
|
|
45
25
|
NoMatch
|
|
46
26
|
|
|
47
27
|
/**
|
|
48
|
-
The port a caller reaches a geocoder through
|
|
49
|
-
|
|
50
|
-
Everything above says what an answer looks like; this says how one is asked for.
|
|
51
|
-
Written down as a type before anything is injected against it, so that swapping
|
|
52
|
-
the implementation underneath a caller is a change of *supplier* rather than a
|
|
53
|
-
change of call site: whatever eventually hands a translator its geocoder — an
|
|
54
|
-
HTTP client reading an endpoint out of its environment, a provider SDK called
|
|
55
|
-
directly — has to satisfy this, and the `await search(~text=…)` in the caller
|
|
56
|
-
does not move.
|
|
28
|
+
The port a caller reaches a geocoder through, so swapping the implementation is a
|
|
29
|
+
change of supplier rather than of call site.
|
|
57
30
|
|
|
58
|
-
`~text` is the
|
|
59
|
-
|
|
31
|
+
`~text` is unnormalised; the provider's canonical rendering comes back as
|
|
32
|
+
`candidate.label`.
|
|
60
33
|
*/
|
|
61
34
|
type search = (~text: string) => promise<result<array<candidate>, failure>>
|
|
62
35
|
|
|
63
36
|
/**
|
|
64
|
-
The default confidence floor,
|
|
65
|
-
|
|
66
|
-
Measured, not guessed — and the measurement moved it a long way. Esri does not
|
|
67
|
-
spread its scores over 0…1: everything it is willing to return at all lands in
|
|
68
|
-
roughly 0.9…1.0, so a floor of `0.8` admitted almost every answer and left the
|
|
69
|
-
ambiguity margin doing all the work. The separation is up at the top of the
|
|
70
|
-
range instead — in a 24-address corpus the correct pinpoint matches scored
|
|
71
|
-
≥ 0.988 while the wrong ones (a misspelling resolved to the wrong state, a
|
|
72
|
-
street name matched to a different street in the right city) scored 0.913…0.958.
|
|
37
|
+
The default confidence floor, measured against Amazon Location's Esri index:
|
|
38
|
+
correct matches scored ≥ 0.988, wrong ones 0.913…0.958, so `0.97` sits in the gap.
|
|
73
39
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
provider-neutral, which is exactly why both are labelled arguments: a geocoder
|
|
77
|
-
that scores on a different curve needs its own pair, and the defaults are the
|
|
78
|
-
Esri answer rather than a universal one.
|
|
40
|
+
Provider-calibrated, which is why it is a labelled argument — a geocoder scoring
|
|
41
|
+
on a different curve needs its own pair.
|
|
79
42
|
*/
|
|
80
43
|
let defaultMinRelevance = 0.97
|
|
81
44
|
|
|
82
|
-
/**
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
Deliberately small, for the same reason the floor is large. Genuine ambiguity in
|
|
86
|
-
Esri shows up as a *tie* — "Springfield" returns five states at exactly 1.0,
|
|
87
|
-
"221B Baker" five towns at exactly 0.8222 — not as a near miss. A wide margin
|
|
88
|
-
does not catch more of those; it only starts rejecting clear winners, because a
|
|
89
|
-
correct match at 0.991 routinely has a plausible runner-up at 0.962.
|
|
90
|
-
*/
|
|
45
|
+
/** How close the runner-up may come before the answer is ambiguous. Small, because
|
|
46
|
+
real ambiguity shows up as a tie; a wide margin only rejects clear winners. */
|
|
91
47
|
let defaultAmbiguityMargin = 0.01
|
|
92
48
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
*/
|
|
107
|
-
let confidentMatch = (
|
|
49
|
+
/** The confidence decision with the reason attached, for a caller that reports
|
|
50
|
+
the outcome rather than acting on it. */
|
|
51
|
+
type assessment =
|
|
52
|
+
| Confident(candidate)
|
|
53
|
+
| NoCandidates
|
|
54
|
+
| /** Unscored is not a low score, and must not read as a high one. */
|
|
55
|
+
Unscored(candidate)
|
|
56
|
+
| LowRelevance({top: candidate, score: float, floor: float})
|
|
57
|
+
| /** Several matches about equally well. */
|
|
58
|
+
Ambiguous({top: candidate, runnerUp: candidate, margin: float})
|
|
59
|
+
|
|
60
|
+
/** The confidence rule, stated once. Everything else here derives from it. */
|
|
61
|
+
let assess = (
|
|
108
62
|
candidates: array<candidate>,
|
|
109
63
|
~minRelevance: float=defaultMinRelevance,
|
|
110
64
|
~ambiguityMargin: float=defaultAmbiguityMargin,
|
|
111
|
-
):
|
|
65
|
+
): assessment =>
|
|
112
66
|
switch candidates->Array.get(0) {
|
|
113
|
-
| None =>
|
|
67
|
+
| None => NoCandidates
|
|
114
68
|
| Some(top) =>
|
|
115
69
|
switch top.relevance {
|
|
116
|
-
| None =>
|
|
70
|
+
| None => Unscored(top)
|
|
117
71
|
| Some(topScore) =>
|
|
118
72
|
if topScore < minRelevance {
|
|
119
|
-
|
|
73
|
+
LowRelevance({top, score: topScore, floor: minRelevance})
|
|
120
74
|
} else {
|
|
121
|
-
switch candidates->Array.get(1)
|
|
122
|
-
| Some(runnerUp)
|
|
123
|
-
|
|
75
|
+
switch candidates->Array.get(1) {
|
|
76
|
+
| Some(runnerUp) =>
|
|
77
|
+
switch runnerUp.relevance {
|
|
78
|
+
| Some(runnerUpScore) if topScore -. runnerUpScore < ambiguityMargin =>
|
|
79
|
+
Ambiguous({top, runnerUp, margin: ambiguityMargin})
|
|
80
|
+
| _ => Confident(top)
|
|
81
|
+
}
|
|
82
|
+
| None => Confident(top)
|
|
124
83
|
}
|
|
125
84
|
}
|
|
126
85
|
}
|
|
127
86
|
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
The one candidate confident enough to store, or `None` — which means "ask a
|
|
90
|
+
human", not "no result". Declines a top scoring below `minRelevance` and a
|
|
91
|
+
runner-up within `ambiguityMargin`. `assess` says which rule declined.
|
|
92
|
+
*/
|
|
93
|
+
let confidentMatch = (
|
|
94
|
+
candidates: array<candidate>,
|
|
95
|
+
~minRelevance: float=defaultMinRelevance,
|
|
96
|
+
~ambiguityMargin: float=defaultAmbiguityMargin,
|
|
97
|
+
): option<candidate> =>
|
|
98
|
+
// Listed rather than `_`, so a new case must be decided here.
|
|
99
|
+
switch candidates->assess(~minRelevance, ~ambiguityMargin) {
|
|
100
|
+
| Confident(top) => Some(top)
|
|
101
|
+
| NoCandidates
|
|
102
|
+
| Unscored(_)
|
|
103
|
+
| LowRelevance(_)
|
|
104
|
+
| Ambiguous(_) =>
|
|
105
|
+
None
|
|
106
|
+
}
|
|
@@ -1,26 +1,59 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
3
|
|
|
5
|
-
function
|
|
4
|
+
function assess(candidates, minRelevanceOpt, ambiguityMarginOpt) {
|
|
6
5
|
let minRelevance = minRelevanceOpt !== undefined ? minRelevanceOpt : 0.97;
|
|
7
6
|
let ambiguityMargin = ambiguityMarginOpt !== undefined ? ambiguityMarginOpt : 0.01;
|
|
8
7
|
let top = candidates[0];
|
|
9
8
|
if (top === undefined) {
|
|
10
|
-
return;
|
|
9
|
+
return "NoCandidates";
|
|
11
10
|
}
|
|
12
11
|
let topScore = top.relevance;
|
|
13
12
|
if (topScore === undefined) {
|
|
14
|
-
return
|
|
13
|
+
return {
|
|
14
|
+
TAG: "Unscored",
|
|
15
|
+
_0: top
|
|
16
|
+
};
|
|
15
17
|
}
|
|
16
18
|
if (topScore < minRelevance) {
|
|
17
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
TAG: "LowRelevance",
|
|
21
|
+
top: top,
|
|
22
|
+
score: topScore,
|
|
23
|
+
floor: minRelevance
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
let runnerUp = candidates[1];
|
|
27
|
+
if (runnerUp === undefined) {
|
|
28
|
+
return {
|
|
29
|
+
TAG: "Confident",
|
|
30
|
+
_0: top
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
let runnerUpScore = runnerUp.relevance;
|
|
34
|
+
if (runnerUpScore !== undefined && topScore - runnerUpScore < ambiguityMargin) {
|
|
35
|
+
return {
|
|
36
|
+
TAG: "Ambiguous",
|
|
37
|
+
top: top,
|
|
38
|
+
runnerUp: runnerUp,
|
|
39
|
+
margin: ambiguityMargin
|
|
40
|
+
};
|
|
41
|
+
} else {
|
|
42
|
+
return {
|
|
43
|
+
TAG: "Confident",
|
|
44
|
+
_0: top
|
|
45
|
+
};
|
|
18
46
|
}
|
|
19
|
-
|
|
20
|
-
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function confidentMatch(candidates, minRelevanceOpt, ambiguityMarginOpt) {
|
|
50
|
+
let minRelevance = minRelevanceOpt !== undefined ? minRelevanceOpt : 0.97;
|
|
51
|
+
let ambiguityMargin = ambiguityMarginOpt !== undefined ? ambiguityMarginOpt : 0.01;
|
|
52
|
+
let top = assess(candidates, minRelevance, ambiguityMargin);
|
|
53
|
+
if (typeof top !== "object" || top.TAG !== "Confident") {
|
|
21
54
|
return;
|
|
22
55
|
} else {
|
|
23
|
-
return top;
|
|
56
|
+
return top._0;
|
|
24
57
|
}
|
|
25
58
|
}
|
|
26
59
|
|
|
@@ -31,6 +64,7 @@ let defaultAmbiguityMargin = 0.01;
|
|
|
31
64
|
export {
|
|
32
65
|
defaultMinRelevance,
|
|
33
66
|
defaultAmbiguityMargin,
|
|
67
|
+
assess,
|
|
34
68
|
confidentMatch,
|
|
35
69
|
}
|
|
36
70
|
/* No side effect */
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A geocoder's answer as one value; `Geocoding`'s return type.
|
|
3
|
+
|
|
4
|
+
Three arms rather than `option<GeoPoint.t>`, whose `None` means both "has not run"
|
|
5
|
+
and "ran and failed". Emitted as a GraphQL union (see `Reventless.TaggedUnion`).
|
|
6
|
+
Replacing a point/status/note trio with it is wire-breaking.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
@schema
|
|
10
|
+
type t =
|
|
11
|
+
| /** `requestedFor` is the address asked about, so a stale answer is detectable. */
|
|
12
|
+
Pending({requestedFor: string})
|
|
13
|
+
| Located({point: GeoPoint.t})
|
|
14
|
+
| /** Answered, with nothing storable unattended. A verdict for a human. */
|
|
15
|
+
Unresolvable({reason: string})
|
|
16
|
+
|
|
17
|
+
/** Adds the two markers the shape cannot carry: the semantic, and the union name
|
|
18
|
+
the SDL and the `__typename` stamp share. */
|
|
19
|
+
let schema: S.t<t> =
|
|
20
|
+
schema->Semantic.mark(~id=Semantic.Id.geolocation)->TaggedUnion.named(~name="Geolocation")
|
|
21
|
+
|
|
22
|
+
/** The point, when there is one. */
|
|
23
|
+
let point = (geolocation: t): option<GeoPoint.t> =>
|
|
24
|
+
switch geolocation {
|
|
25
|
+
| Located({point}) => Some(point)
|
|
26
|
+
| Pending(_) | Unresolvable(_) => None
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
let isLocated = (geolocation: t): bool => geolocation->point->Option.isSome
|
|
30
|
+
|
|
31
|
+
/** Why it could not be resolved. `None` for `Pending`, which is still waiting. */
|
|
32
|
+
let reason = (geolocation: t): option<string> =>
|
|
33
|
+
switch geolocation {
|
|
34
|
+
| Unresolvable({reason}) => Some(reason)
|
|
35
|
+
| Pending(_) | Located(_) => None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
The geolocation a geocoder's answer implies, via `Geocoding.assess`.
|
|
40
|
+
|
|
41
|
+
`Error(Unavailable(_))` returns `None` — leave the row alone, since an outage
|
|
42
|
+
written as a verdict is permanent. Never returns `Pending`. Thresholds are
|
|
43
|
+
threaded because they are provider-calibrated.
|
|
44
|
+
*/
|
|
45
|
+
let ofSearch = (
|
|
46
|
+
~requestedFor: string,
|
|
47
|
+
~minRelevance: float=Geocoding.defaultMinRelevance,
|
|
48
|
+
~ambiguityMargin: float=Geocoding.defaultAmbiguityMargin,
|
|
49
|
+
answer: result<array<Geocoding.candidate>, Geocoding.failure>,
|
|
50
|
+
): option<t> =>
|
|
51
|
+
switch answer {
|
|
52
|
+
| Error(Unavailable(_)) => None
|
|
53
|
+
| Error(NoMatch) => Some(Unresolvable({reason: `no match for "${requestedFor}"`}))
|
|
54
|
+
| Ok(candidates) =>
|
|
55
|
+
// Reasons report what came back; the rule that rejected it stays in `assess`.
|
|
56
|
+
switch candidates->Geocoding.assess(~minRelevance, ~ambiguityMargin) {
|
|
57
|
+
| Confident(top) => Some(Located({point: top.point}))
|
|
58
|
+
| NoCandidates => Some(Unresolvable({reason: `no candidates for "${requestedFor}"`}))
|
|
59
|
+
| Unscored(top) =>
|
|
60
|
+
Some(
|
|
61
|
+
Unresolvable({
|
|
62
|
+
reason: `the geocoder returned "${top.label}" for "${requestedFor}" without scoring it, ` ++
|
|
63
|
+
`and an unscored answer cannot be accepted unattended`,
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
| LowRelevance({top, score, floor}) =>
|
|
67
|
+
Some(
|
|
68
|
+
Unresolvable({
|
|
69
|
+
reason: `the best match for "${requestedFor}" was "${top.label}" at relevance ` ++
|
|
70
|
+
`${Float.toString(score)}, below the ${Float.toString(floor)} needed to store one`,
|
|
71
|
+
}),
|
|
72
|
+
)
|
|
73
|
+
| Ambiguous({top, runnerUp}) =>
|
|
74
|
+
Some(
|
|
75
|
+
Unresolvable({
|
|
76
|
+
reason: `"${requestedFor}" matched "${top.label}" and "${runnerUp.label}" ` ++
|
|
77
|
+
`about equally well`,
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as GeoPoint$Reventless from "./GeoPoint.res.mjs";
|
|
6
|
+
import * as Semantic$Reventless from "./Semantic.res.mjs";
|
|
7
|
+
import * as Geocoding$Reventless from "./Geocoding.res.mjs";
|
|
8
|
+
import * as TaggedUnion$Reventless from "../components/TaggedUnion.res.mjs";
|
|
9
|
+
|
|
10
|
+
let schema = Sury.union([
|
|
11
|
+
Sury.$schema(s => ({
|
|
12
|
+
TAG: "Pending",
|
|
13
|
+
requestedFor: s.m(Sury.string)
|
|
14
|
+
})),
|
|
15
|
+
Sury.$schema(s => ({
|
|
16
|
+
TAG: "Located",
|
|
17
|
+
point: s.m(GeoPoint$Reventless.schema)
|
|
18
|
+
})),
|
|
19
|
+
Sury.$schema(s => ({
|
|
20
|
+
TAG: "Unresolvable",
|
|
21
|
+
reason: s.m(Sury.string)
|
|
22
|
+
}))
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
let schema$1 = TaggedUnion$Reventless.named("Geolocation", Semantic$Reventless.mark(schema, Semantic$Reventless.Id.geolocation, undefined));
|
|
26
|
+
|
|
27
|
+
function point(geolocation) {
|
|
28
|
+
switch (geolocation.TAG) {
|
|
29
|
+
case "Located" :
|
|
30
|
+
return geolocation.point;
|
|
31
|
+
case "Pending" :
|
|
32
|
+
case "Unresolvable" :
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isLocated(geolocation) {
|
|
38
|
+
return Stdlib_Option.isSome(point(geolocation));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reason(geolocation) {
|
|
42
|
+
switch (geolocation.TAG) {
|
|
43
|
+
case "Pending" :
|
|
44
|
+
case "Located" :
|
|
45
|
+
return;
|
|
46
|
+
case "Unresolvable" :
|
|
47
|
+
return geolocation.reason;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ofSearch(requestedFor, minRelevanceOpt, ambiguityMarginOpt, answer) {
|
|
52
|
+
let minRelevance = minRelevanceOpt !== undefined ? minRelevanceOpt : Geocoding$Reventless.defaultMinRelevance;
|
|
53
|
+
let ambiguityMargin = ambiguityMarginOpt !== undefined ? ambiguityMarginOpt : Geocoding$Reventless.defaultAmbiguityMargin;
|
|
54
|
+
if (answer.TAG === "Ok") {
|
|
55
|
+
let top = Geocoding$Reventless.assess(answer._0, minRelevance, ambiguityMargin);
|
|
56
|
+
if (typeof top !== "object") {
|
|
57
|
+
return {
|
|
58
|
+
TAG: "Unresolvable",
|
|
59
|
+
reason: `no candidates for "` + requestedFor + `"`
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
switch (top.TAG) {
|
|
63
|
+
case "Confident" :
|
|
64
|
+
return {
|
|
65
|
+
TAG: "Located",
|
|
66
|
+
point: top._0.point
|
|
67
|
+
};
|
|
68
|
+
case "Unscored" :
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Unresolvable",
|
|
71
|
+
reason: `the geocoder returned "` + top._0.label + `" for "` + requestedFor + `" without scoring it, and an unscored answer cannot be accepted unattended`
|
|
72
|
+
};
|
|
73
|
+
case "LowRelevance" :
|
|
74
|
+
return {
|
|
75
|
+
TAG: "Unresolvable",
|
|
76
|
+
reason: `the best match for "` + requestedFor + `" was "` + top.top.label + `" at relevance ` + (top.score.toString() + `, below the ` + top.floor.toString() + ` needed to store one`)
|
|
77
|
+
};
|
|
78
|
+
case "Ambiguous" :
|
|
79
|
+
return {
|
|
80
|
+
TAG: "Unresolvable",
|
|
81
|
+
reason: `"` + requestedFor + `" matched "` + top.top.label + `" and "` + top.runnerUp.label + `" about equally well`
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
let tmp = answer._0;
|
|
86
|
+
if (typeof tmp !== "object") {
|
|
87
|
+
return {
|
|
88
|
+
TAG: "Unresolvable",
|
|
89
|
+
reason: `no match for "` + requestedFor + `"`
|
|
90
|
+
};
|
|
91
|
+
} else {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
schema$1 as schema,
|
|
99
|
+
point,
|
|
100
|
+
isLocated,
|
|
101
|
+
reason,
|
|
102
|
+
ofSearch,
|
|
103
|
+
}
|
|
104
|
+
/* schema Not a pure module */
|
|
@@ -1,34 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
2
|
The one marker every typed semantic marks itself with.
|
|
3
3
|
|
|
4
|
-
A semantic
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
own metadata id, `Reference` carried another, and the schema walk detected both
|
|
12
|
-
by hardcoded special case. That made every new typed marker new detection code.
|
|
13
|
-
One shared marker means the walk reads a semantic generically and a new semantic
|
|
14
|
-
type is a new *value*, not a new branch.
|
|
15
|
-
|
|
16
|
-
The payload is a real variant rather than free-form JSON. The semantic
|
|
17
|
-
vocabulary is framework-owned — an application declares a field *is* a storage
|
|
18
|
-
ref, it does not invent what a storage ref means — so the set is closed, and a
|
|
19
|
-
closed set typed here is one the compiler checks at every producer and consumer.
|
|
20
|
-
It also keeps `Reference.getTarget` a total typed function instead of a decode
|
|
21
|
-
that can fail at runtime.
|
|
4
|
+
A semantic says what a field's value *is*, as a property of its type; validation,
|
|
5
|
+
the wire contract and the UI widget all derive from that one declaration. One
|
|
6
|
+
shared marker means the schema walk reads semantics generically, so a new
|
|
7
|
+
semantic is a new value rather than a new branch.
|
|
8
|
+
|
|
9
|
+
The payload is a typed variant because the vocabulary is framework-owned and
|
|
10
|
+
closed — which also keeps `Reference.getTarget` total.
|
|
22
11
|
*/
|
|
23
12
|
|
|
24
13
|
/** Which entity a reference field points to. */
|
|
25
14
|
type referenceTarget = {entity: string, plugin: option<string>}
|
|
26
15
|
|
|
27
|
-
/** Which object store
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
field may declare (`None` for `@storageRef`, which is always a ref, and for
|
|
31
|
-
`@offload` fields that leave it to the platform default). */
|
|
16
|
+
/** Which object store the value lives in. `plugin` is absent for the declaring
|
|
17
|
+
plugin's own store; `threshold` is `@offload`'s per-field byte cut, `None`
|
|
18
|
+
when it defers to the platform default. */
|
|
32
19
|
type storeTarget = {plugin: option<string>, store: string, threshold: option<int>}
|
|
33
20
|
|
|
34
21
|
/** Per-semantic detail, for the semantics that carry any. */
|
|
@@ -40,37 +27,25 @@ type payload =
|
|
|
40
27
|
/** A field's semantic: the vocabulary id, plus its detail. */
|
|
41
28
|
type t = {id: string, payload: payload}
|
|
42
29
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
These strings are the wire vocabulary — they are what `x-reventless-semantic`
|
|
47
|
-
carries, and the same vocabulary the string annotation path already uses, so the
|
|
48
|
-
type path and the annotation path converge on one wire format rather than two.
|
|
49
|
-
*/
|
|
30
|
+
/** The semantic ids the framework defines. These strings are the wire vocabulary
|
|
31
|
+
`x-reventless-semantic` carries, shared with the annotation path. */
|
|
50
32
|
module Id = {
|
|
51
33
|
let dateTime = "dateTime"
|
|
52
34
|
let reference = "reference"
|
|
53
35
|
let storageRef = "storageRef"
|
|
54
|
-
//
|
|
55
|
-
// store and carries by reference (inline below a size threshold). Sibling of
|
|
56
|
-
// `storageRef`: same `StoredIn` store declaration, but an inline-or-reference
|
|
57
|
-
// value rather than an always-a-ref path string.
|
|
36
|
+
// Like `storageRef`, but inline-or-reference rather than always a ref path.
|
|
58
37
|
let offload = "offload"
|
|
59
38
|
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// renderer and an upload endpoint. The store is derived from the field name by
|
|
63
|
-
// the ppx rather than written on the type.
|
|
39
|
+
// `storageRef` plus what the value is, so one declaration picks a renderer and
|
|
40
|
+
// an upload endpoint. The ppx derives the store from the field name.
|
|
64
41
|
let uploadableImage = "uploadableImage"
|
|
65
42
|
let uploadableFile = "uploadableFile"
|
|
66
43
|
|
|
67
|
-
// The same content facts with no store
|
|
68
|
-
// reads but does not own. Nothing is provisioned for these.
|
|
44
|
+
// The same content facts with no store — nothing is provisioned.
|
|
69
45
|
let imageRef = "imageRef"
|
|
70
46
|
let fileRef = "fileRef"
|
|
71
47
|
|
|
72
|
-
//
|
|
73
|
-
// its shape, so a field gains one of these without anything stored changing.
|
|
48
|
+
// Branded scalars: a refinement, so adopting one changes nothing stored.
|
|
74
49
|
let email = "email"
|
|
75
50
|
let phone = "phone"
|
|
76
51
|
let url = "url"
|
|
@@ -79,23 +54,18 @@ module Id = {
|
|
|
79
54
|
let duration = "duration"
|
|
80
55
|
let color = "color"
|
|
81
56
|
|
|
82
|
-
// The first composite
|
|
83
|
-
// this one changes a field's *shape* — a number becomes an object — so it is
|
|
84
|
-
// a wire-breaking declaration rather than a refinement of one.
|
|
57
|
+
// The first composite: changes a field's shape, so it is wire-breaking.
|
|
85
58
|
let money = "money"
|
|
86
59
|
|
|
87
|
-
//
|
|
88
|
-
// span the UI used to guess from a `start*`/`end*` name pair. Like `money` it
|
|
89
|
-
// is an object on the wire; unlike it, adopting it as a *new* optional field
|
|
90
|
-
// is additive — an absent optional decodes to `None`.
|
|
60
|
+
// A pair of ISO-8601 instants. Adopting it as a new optional field is additive.
|
|
91
61
|
let dateRange = "dateRange"
|
|
92
62
|
|
|
93
|
-
//
|
|
94
|
-
// one value, replacing a point the UI used to guess from a `lat`/`lng` name
|
|
95
|
-
// pair. Most coordinate fields already store `{lat, lng}` as a hand-rolled
|
|
96
|
-
// record, so retyping one is shape-preserving: the wire is unchanged and
|
|
97
|
-
// nothing stored needs upcasting.
|
|
63
|
+
// A lat/lng pair. Cheapest to adopt: `{lat, lng}` is already the stored shape.
|
|
98
64
|
let geoPoint = "geoPoint"
|
|
65
|
+
|
|
66
|
+
// The first composite that is a union rather than an object. Collapses fields,
|
|
67
|
+
// so adopting it changes the wire and rebuilds a derived view.
|
|
68
|
+
let geolocation = "geolocation"
|
|
99
69
|
}
|
|
100
70
|
|
|
101
71
|
let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless", ~name="semantic")
|
|
@@ -104,20 +74,10 @@ let semanticId: S.Metadata.Id.t<t> = S.Metadata.Id.make(~namespace="reventless",
|
|
|
104
74
|
let mark = (schema: S.t<'a>, ~id: string, ~payload: payload=Plain): S.t<'a> =>
|
|
105
75
|
schema->S.Metadata.set(~id=semanticId, {id, payload})
|
|
106
76
|
|
|
107
|
-
/**
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
defines the grammar, and a schema that must agree with it — and `StorageRef`
|
|
112
|
-
established that the schema is *derived* from the constructor rather than
|
|
113
|
-
hand-rolling a second check beside it. Deriving it here makes that structural:
|
|
114
|
-
there is one place a grammar can be written, so there is nowhere for a second
|
|
115
|
-
one to drift.
|
|
116
|
-
*/
|
|
117
|
-
// sury's refiner is a predicate with a fixed message, so the per-value reason
|
|
118
|
-
// `check` returns is not threaded into the schema error; call the scalar's own
|
|
119
|
-
// `fromString`/`fromFloat` directly when the caller needs to report which rule
|
|
120
|
-
// the value broke.
|
|
77
|
+
/** A schema that validates with `check` and carries the semantic `id`, derived
|
|
78
|
+
from the constructor so no second grammar can drift from it. */
|
|
79
|
+
// sury's refiner takes a fixed message, so `check`'s per-value reason is lost
|
|
80
|
+
// here; call the scalar's own `fromString`/`fromFloat` to report which rule broke.
|
|
121
81
|
let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.t<'a> =>
|
|
122
82
|
base
|
|
123
83
|
->S.refine(
|
|
@@ -130,22 +90,16 @@ let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.
|
|
|
130
90
|
)
|
|
131
91
|
->mark(~id)
|
|
132
92
|
|
|
133
|
-
/** A value as it should read back to
|
|
134
|
-
|
|
93
|
+
/** A value as it should read back to whoever typed it — rejection messages quote
|
|
94
|
+
the offending value. */
|
|
135
95
|
let showString = (raw: string): string => raw->JSON.Encode.string->JSON.stringify
|
|
136
96
|
|
|
137
97
|
/**
|
|
138
98
|
The schema an optional field's wrapper stands for, if it is one.
|
|
139
99
|
|
|
140
|
-
sury-ppx compiles `f?: X` to a union
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
needs the same one-level unwrap, so it lives here once rather than once per
|
|
144
|
-
reader.
|
|
145
|
-
|
|
146
|
-
Only a union with exactly one non-null variant is followed: that is the shape an
|
|
147
|
-
optional field has, and a genuine multi-variant union has no single inner schema
|
|
148
|
-
that could stand for the whole.
|
|
100
|
+
sury-ppx compiles `f?: X` to a union with `Undefined`/`Null`, and that wrapper
|
|
101
|
+
carries no metadata of its own. Only a union with exactly one non-null variant is
|
|
102
|
+
followed — a real multi-variant union has no single inner schema.
|
|
149
103
|
*/
|
|
150
104
|
let unwrapOptional = (schema: S.t<unknown>): option<S.t<unknown>> =>
|
|
151
105
|
switch schema {
|
|
@@ -165,14 +119,8 @@ let unwrapOptional = (schema: S.t<unknown>): option<S.t<unknown>> =>
|
|
|
165
119
|
/**
|
|
166
120
|
The semantic a field's schema carries, if any.
|
|
167
121
|
|
|
168
|
-
An
|
|
169
|
-
|
|
170
|
-
`imageUrl?: string` as carrying no semantic at all — the store goes undeclared,
|
|
171
|
-
the reference goes uncollected, the branded scalar loses its brand. Every reader
|
|
172
|
-
converges here, so following the wrapper once here is what keeps "optional" a
|
|
173
|
-
statement about presence rather than a way to lose the field's type.
|
|
174
|
-
|
|
175
|
-
The outer schema is read first, so a marker set on the wrapper itself still wins.
|
|
122
|
+
An optional field keeps its marker inside the wrapper, so reading only the outer
|
|
123
|
+
schema loses it. The outer schema is read first, so a marker on the wrapper wins.
|
|
176
124
|
*/
|
|
177
125
|
let rec getFrom = (schema: S.t<unknown>): option<t> =>
|
|
178
126
|
switch S.Metadata.get(schema, ~id=semanticId) {
|