@reventlessdev/reventless-spec 3.0.0-alpha.86 → 3.0.0-alpha.87
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 +1 -1
- package/src/components/CapabilityManifest.res +10 -3
- package/src/components/CapabilityManifest.res.mjs +4 -2
- package/src/components/Plugin.res +8 -0
- package/src/components/Plugin.res.mjs +2 -1
- package/src/generator/PlatformCodegen.res +20 -16
- package/src/generator/PlatformCodegen.res.mjs +11 -12
- package/src/semantic/Semantic.res +37 -2
- package/src/semantic/Semantic.res.mjs +29 -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.87 (2026-07-30)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **spec,core:** record the storageRef annotation instead of inferring it ([06fb5d6](https://github.com/ReventlessDev/reventless-core/commit/06fb5d671db91fb536acefd0c2db69d98671da39))
|
|
11
|
+
* **spec:** read a semantic marker through an optional field's wrapper ([abebaa9](https://github.com/ReventlessDev/reventless-core/commit/abebaa9e6c73986765bb3de589ba0414eb0d85da))
|
|
12
|
+
|
|
13
|
+
|
|
6
14
|
# 3.0.0-alpha.86 (2026-07-30)
|
|
7
15
|
|
|
8
16
|
### Features
|
package/package.json
CHANGED
|
@@ -17,9 +17,14 @@ emits the platform's capability list from them.
|
|
|
17
17
|
type kind = ObjectStore
|
|
18
18
|
|
|
19
19
|
/** The declaration site: the component's spec name and the field carrying the
|
|
20
|
-
`@storageRef` annotation.
|
|
20
|
+
`@storageRef` annotation, plus the store exactly as that field spells it.
|
|
21
|
+
|
|
22
|
+
`annotation` is optional only to keep reading a manifest emitted before it
|
|
23
|
+
existed: a plugin built earlier still parses, and a reader that cannot say
|
|
24
|
+
what the source says omits the claim rather than inventing one. Every
|
|
25
|
+
manifest emitted now carries it. */
|
|
21
26
|
@schema
|
|
22
|
-
type provenance = {component: string, field: string}
|
|
27
|
+
type provenance = {component: string, field: string, annotation?: string}
|
|
23
28
|
|
|
24
29
|
@schema
|
|
25
30
|
type entry = {
|
|
@@ -50,7 +55,9 @@ let fromStructure = (structure: Plugin.pluginStructure): t => {
|
|
|
50
55
|
kind: ObjectStore,
|
|
51
56
|
key,
|
|
52
57
|
declaredBy: declarations->Array.filterMap(d =>
|
|
53
|
-
d.store == key
|
|
58
|
+
d.store == key
|
|
59
|
+
? Some({component: d.component, field: d.field, annotation: d.annotation})
|
|
60
|
+
: None
|
|
54
61
|
),
|
|
55
62
|
}),
|
|
56
63
|
}
|
|
@@ -8,7 +8,8 @@ let kindSchema = S.literal("ObjectStore");
|
|
|
8
8
|
|
|
9
9
|
let provenanceSchema = S.schema(s => ({
|
|
10
10
|
component: s.m(S.string),
|
|
11
|
-
field: s.m(S.string)
|
|
11
|
+
field: s.m(S.string),
|
|
12
|
+
annotation: s.m(S.option(S.string))
|
|
12
13
|
}));
|
|
13
14
|
|
|
14
15
|
let entrySchema = S.schema(s => ({
|
|
@@ -31,7 +32,8 @@ function fromStructure(structure) {
|
|
|
31
32
|
if (d.store === key) {
|
|
32
33
|
return {
|
|
33
34
|
component: d.component,
|
|
34
|
-
field: d.field
|
|
35
|
+
field: d.field,
|
|
36
|
+
annotation: d.annotation
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
39
|
})
|
|
@@ -386,12 +386,20 @@ One field's store requirement, with its provenance.
|
|
|
386
386
|
carries; `component` and `field` name the declaration site. The site matters
|
|
387
387
|
because a requirement only ever changes by editing a field — when a rename
|
|
388
388
|
removes a store from the manifest, the diff has to say which field caused it.
|
|
389
|
+
|
|
390
|
+
`annotation` is the store exactly as the field spells it — bare for a store
|
|
391
|
+
the declaring plugin owns, qualified for a foreign one. It is recorded rather
|
|
392
|
+
than reconstructed: only here is the owning plugin unambiguous, so anything
|
|
393
|
+
downstream would have to infer it by comparing a registered plugin name with
|
|
394
|
+
whatever name a deploy manifest happened to use, and those were never required
|
|
395
|
+
to match.
|
|
389
396
|
*/
|
|
390
397
|
@schema
|
|
391
398
|
type requiredStoreDeclaration = {
|
|
392
399
|
store: string,
|
|
393
400
|
component: string,
|
|
394
401
|
field: string,
|
|
402
|
+
annotation: string,
|
|
395
403
|
}
|
|
396
404
|
|
|
397
405
|
let requiredStoreDeclarationArrayOptionSchema = _jsNullable(
|
|
@@ -181,7 +181,8 @@ let extensionPointDefArrayOptionSchema = SuryResMjs.js_nullable(S.array(extensio
|
|
|
181
181
|
let requiredStoreDeclarationSchema = S.schema(s => ({
|
|
182
182
|
store: s.m(S.string),
|
|
183
183
|
component: s.m(S.string),
|
|
184
|
-
field: s.m(S.string)
|
|
184
|
+
field: s.m(S.string),
|
|
185
|
+
annotation: s.m(S.string)
|
|
185
186
|
}));
|
|
186
187
|
|
|
187
188
|
let requiredStoreDeclarationArrayOptionSchema = SuryResMjs.js_nullable(S.array(requiredStoreDeclarationSchema));
|
|
@@ -22,6 +22,7 @@ type provenance = {
|
|
|
22
22
|
pluginName: string,
|
|
23
23
|
component: string,
|
|
24
24
|
field: string,
|
|
25
|
+
annotation: option<string>,
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/** One capability the platform must provision: the qualified `{plugin}.{store}`
|
|
@@ -45,6 +46,7 @@ let union = (manifests: array<pluginManifest>): array<unionEntry> => {
|
|
|
45
46
|
pluginName,
|
|
46
47
|
component: site.component,
|
|
47
48
|
field: site.field,
|
|
49
|
+
annotation: site.annotation,
|
|
48
50
|
})
|
|
49
51
|
switch byKey->Dict.get(entry.key) {
|
|
50
52
|
| Some(existing) =>
|
|
@@ -71,17 +73,17 @@ let splitKey = (key: string): option<(string, string)> =>
|
|
|
71
73
|
key->String.slice(~start=i + 1),
|
|
72
74
|
))
|
|
73
75
|
|
|
74
|
-
// The provenance comment quotes the annotation as its author wrote it
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
76
|
+
// The provenance comment quotes the annotation as its author wrote it — taken
|
|
77
|
+
// from the manifest, which recorded it at the one place the owning plugin was
|
|
78
|
+
// unambiguous. Inferring it here is what this replaced: the manifest carries a
|
|
79
|
+
// deploy-manifest entry name and the key carries the registered name, and no
|
|
80
|
+
// rule relates the two (`platform-inspector` against `PlatformInspector` is an
|
|
81
|
+
// ordinary pairing), so any comparison here eventually quotes a string that is
|
|
82
|
+
// in no source file.
|
|
83
|
+
//
|
|
84
|
+
// A site with no recorded annotation predates the recording. It still names the
|
|
85
|
+
// field, which is the comment's job; it just makes no claim about the source
|
|
86
|
+
// text it cannot see.
|
|
85
87
|
let renderEntry = (entry: unionEntry): result<array<string>, string> =>
|
|
86
88
|
switch splitKey(entry.key) {
|
|
87
89
|
| None =>
|
|
@@ -90,11 +92,13 @@ let renderEntry = (entry: unionEntry): result<array<string>, string> =>
|
|
|
90
92
|
)
|
|
91
93
|
| Some((plugin, store)) => {
|
|
92
94
|
let comments =
|
|
93
|
-
entry.declaredBy->Array.map(site =>
|
|
94
|
-
` // ${site.pluginName}: ${site.component}.${site.field}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
entry.declaredBy->Array.map(site => {
|
|
96
|
+
let site_ = ` // ${site.pluginName}: ${site.component}.${site.field}`
|
|
97
|
+
switch site.annotation {
|
|
98
|
+
| Some(annotation) => `${site_} @storageRef(${quote(annotation)})`
|
|
99
|
+
| None => site_
|
|
100
|
+
}
|
|
101
|
+
})
|
|
98
102
|
let line = switch entry.kind {
|
|
99
103
|
| ObjectStore => ` ObjectStore({plugin: ${quote(plugin)}, store: ${quote(store)}}),`
|
|
100
104
|
}
|
|
@@ -14,7 +14,8 @@ function union(manifests) {
|
|
|
14
14
|
let sites = entry.declaredBy.map(site => ({
|
|
15
15
|
pluginName: pluginName,
|
|
16
16
|
component: site.component,
|
|
17
|
-
field: site.field
|
|
17
|
+
field: site.field,
|
|
18
|
+
annotation: site.annotation
|
|
18
19
|
}));
|
|
19
20
|
let existing = byKey[entry.key];
|
|
20
21
|
if (existing !== undefined) {
|
|
@@ -46,15 +47,6 @@ function splitKey(key) {
|
|
|
46
47
|
]);
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
function annotationStore(pluginName, key) {
|
|
50
|
-
let match = splitKey(key);
|
|
51
|
-
if (match !== undefined && match[0].toLowerCase() === pluginName.toLowerCase()) {
|
|
52
|
-
return match[1];
|
|
53
|
-
} else {
|
|
54
|
-
return key;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
50
|
function renderEntry(entry) {
|
|
59
51
|
let match = splitKey(entry.key);
|
|
60
52
|
if (match === undefined) {
|
|
@@ -63,7 +55,15 @@ function renderEntry(entry) {
|
|
|
63
55
|
_0: `malformed capability key ` + JSON.stringify(entry.key) + ` — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`
|
|
64
56
|
};
|
|
65
57
|
}
|
|
66
|
-
let comments = entry.declaredBy.map(site =>
|
|
58
|
+
let comments = entry.declaredBy.map(site => {
|
|
59
|
+
let site_ = ` // ` + site.pluginName + `: ` + site.component + `.` + site.field;
|
|
60
|
+
let annotation = site.annotation;
|
|
61
|
+
if (annotation !== undefined) {
|
|
62
|
+
return site_ + ` @storageRef(` + JSON.stringify(annotation) + `)`;
|
|
63
|
+
} else {
|
|
64
|
+
return site_;
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
67
|
let line = ` ObjectStore({plugin: ` + JSON.stringify(match[0]) + `, store: ` + JSON.stringify(match[1]) + `}),`;
|
|
68
68
|
return {
|
|
69
69
|
TAG: "Ok",
|
|
@@ -108,7 +108,6 @@ export {
|
|
|
108
108
|
union,
|
|
109
109
|
quote,
|
|
110
110
|
splitKey,
|
|
111
|
-
annotationStore,
|
|
112
111
|
renderEntry,
|
|
113
112
|
header,
|
|
114
113
|
render,
|
|
@@ -90,8 +90,43 @@ let refined = (base: S.t<'a>, ~id: string, ~check: 'a => result<'a, string>): S.
|
|
|
90
90
|
reach forms through `validateInput`, so they quote the offending value. */
|
|
91
91
|
let showString = (raw: string): string => raw->JSON.Encode.string->JSON.stringify
|
|
92
92
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
93
|
+
/**
|
|
94
|
+
The semantic a field's schema carries, if any.
|
|
95
|
+
|
|
96
|
+
An **optional** field keeps its marker one level down. The ppx annotates the
|
|
97
|
+
field's `string`, and sury-ppx then wraps that schema in a union with
|
|
98
|
+
`Undefined`/`Null`; the wrapper is a new schema and carries no metadata of its
|
|
99
|
+
own. So a walk that reads only the outer schema sees `imageUrl?: string` as
|
|
100
|
+
carrying no semantic at all — the store goes undeclared, the reference goes
|
|
101
|
+
uncollected, the branded scalar loses its brand. Every reader converges here, so
|
|
102
|
+
following the wrapper once here is what keeps "optional" a statement about
|
|
103
|
+
presence rather than a way to lose the field's type.
|
|
104
|
+
|
|
105
|
+
The outer schema is read first, so a marker set on the wrapper itself still wins.
|
|
106
|
+
Only a union with exactly one non-null variant is followed: that is the shape an
|
|
107
|
+
optional field has, and a genuine multi-variant union has no single inner schema
|
|
108
|
+
whose semantic could stand for the whole.
|
|
109
|
+
*/
|
|
110
|
+
let rec getFrom = (schema: S.t<unknown>): option<t> =>
|
|
111
|
+
switch S.Metadata.get(schema, ~id=semanticId) {
|
|
112
|
+
| Some(_) as found => found
|
|
113
|
+
| None =>
|
|
114
|
+
switch schema {
|
|
115
|
+
| Union({anyOf}) =>
|
|
116
|
+
switch anyOf->Array.filter(v =>
|
|
117
|
+
switch v {
|
|
118
|
+
| Null(_) | Undefined(_) => false
|
|
119
|
+
| _ => true
|
|
120
|
+
}
|
|
121
|
+
) {
|
|
122
|
+
| [inner] => getFrom(inner)
|
|
123
|
+
| _ => None
|
|
124
|
+
}
|
|
125
|
+
| _ => None
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let get = (fieldSchema: S.t<'a>): option<t> => fieldSchema->S.castToUnknown->getFrom
|
|
95
130
|
|
|
96
131
|
/** Whether a field's schema carries this specific semantic. */
|
|
97
132
|
let has = (fieldSchema: S.t<'a>, ~id: string): bool =>
|
|
@@ -40,12 +40,37 @@ function showString(raw) {
|
|
|
40
40
|
return JSON.stringify(raw);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
function
|
|
44
|
-
|
|
43
|
+
function getFrom(_schema) {
|
|
44
|
+
while (true) {
|
|
45
|
+
let schema = _schema;
|
|
46
|
+
let found = S.Metadata.get(schema, semanticId);
|
|
47
|
+
if (found !== undefined) {
|
|
48
|
+
return found;
|
|
49
|
+
}
|
|
50
|
+
if (schema.type !== "union") {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
let match = schema.anyOf.filter(v => {
|
|
54
|
+
switch (v.type) {
|
|
55
|
+
case "null" :
|
|
56
|
+
case "undefined" :
|
|
57
|
+
return false;
|
|
58
|
+
default:
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
if (match.length !== 1) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
_schema = match[0];
|
|
66
|
+
continue;
|
|
67
|
+
};
|
|
45
68
|
}
|
|
46
69
|
|
|
70
|
+
let get = getFrom;
|
|
71
|
+
|
|
47
72
|
function has(fieldSchema, id) {
|
|
48
|
-
let s =
|
|
73
|
+
let s = getFrom(fieldSchema);
|
|
49
74
|
if (s !== undefined) {
|
|
50
75
|
return s.id === id;
|
|
51
76
|
} else {
|
|
@@ -59,6 +84,7 @@ export {
|
|
|
59
84
|
mark,
|
|
60
85
|
refined,
|
|
61
86
|
showString,
|
|
87
|
+
getFrom,
|
|
62
88
|
get,
|
|
63
89
|
has,
|
|
64
90
|
}
|