@reventlessdev/reventless-spec 3.0.0-alpha.86 → 3.0.0-alpha.88

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.88 (2026-07-30)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **core:** a command's field markers reach the wire, and its optional fields stay optional ([f1c1112](https://github.com/ReventlessDev/reventless-core/commit/f1c1112e9baa6b06e50097a5a618f49c9301cd0a))
11
+
12
+
13
+ # 3.0.0-alpha.87 (2026-07-30)
14
+
15
+ ### Bug Fixes
16
+
17
+ * **spec,core:** record the storageRef annotation instead of inferring it ([06fb5d6](https://github.com/ReventlessDev/reventless-core/commit/06fb5d671db91fb536acefd0c2db69d98671da39))
18
+ * **spec:** read a semantic marker through an optional field's wrapper ([abebaa9](https://github.com/ReventlessDev/reventless-core/commit/abebaa9e6c73986765bb3de589ba0414eb0d85da))
19
+
20
+
6
21
  # 3.0.0-alpha.86 (2026-07-30)
7
22
 
8
23
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-spec",
3
- "version": "3.0.0-alpha.86",
3
+ "version": "3.0.0-alpha.88",
4
4
  "description": "Specifications for Reventless",
5
5
  "license": "Apache-2.0",
6
6
  "bin": {
@@ -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 ? Some({component: d.component, field: d.field}) : None
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
  })
@@ -275,8 +275,10 @@ type queryableDef = {
275
275
  One emitted event of a write side, with its field schema. Mirrors `commandDef`
276
276
  but for the past-tense facts a write side produces: `name` is the event variant
277
277
  name (e.g. `OrderPlaced`), `schema` is the JSON Schema of that variant's payload
278
- (same `S.toJSONSchema` serialization as `commandDef.schema`, incl. `x-reventless-*`
279
- extensions and the `TAG` const), `references` its cross-entity field links.
278
+ (same serialization as `commandDef.schema`: `SuryToJsonSchema.deriveObjectSchema`,
279
+ so field-level `x-reventless-*` extensions are carried and the variant's `TAG`
280
+ discriminator is not — the constructor name is already `name`), `references` its
281
+ cross-entity field links.
280
282
  Carried so developer tools (the `reventless-dev` / VSCode domain graph) can show
281
283
  event field rows — AutoUI ignores it. */
282
284
  @schema
@@ -386,12 +388,20 @@ One field's store requirement, with its provenance.
386
388
  carries; `component` and `field` name the declaration site. The site matters
387
389
  because a requirement only ever changes by editing a field — when a rename
388
390
  removes a store from the manifest, the diff has to say which field caused it.
391
+
392
+ `annotation` is the store exactly as the field spells it — bare for a store
393
+ the declaring plugin owns, qualified for a foreign one. It is recorded rather
394
+ than reconstructed: only here is the owning plugin unambiguous, so anything
395
+ downstream would have to infer it by comparing a registered plugin name with
396
+ whatever name a deploy manifest happened to use, and those were never required
397
+ to match.
389
398
  */
390
399
  @schema
391
400
  type requiredStoreDeclaration = {
392
401
  store: string,
393
402
  component: string,
394
403
  field: string,
404
+ annotation: string,
395
405
  }
396
406
 
397
407
  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: a store
75
- // declared by its own plugin is unqualified, a foreign store keeps the
76
- // qualified form. The manifest carries only the deploy-manifest plugin name
77
- // (lowercase by convention) while the key carries the registered name, so the
78
- // comparison is case-insensitive — and it only shapes a comment.
79
- let annotationStore = (~pluginName, ~key): string =>
80
- switch splitKey(key) {
81
- | Some((keyPlugin, store)) if keyPlugin->String.toLowerCase == pluginName->String.toLowerCase => store
82
- | _ => key
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} @storageRef(${quote(
95
- annotationStore(~pluginName=site.pluginName, ~key=entry.key),
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 => ` // ` + site.pluginName + `: ` + site.component + `.` + site.field + ` @storageRef(` + JSON.stringify(annotationStore(site.pluginName, entry.key)) + `)`);
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
- /** The semantic a field's schema carries, if any. */
94
- let get = (fieldSchema: S.t<'a>): option<t> => S.Metadata.get(fieldSchema, ~id=semanticId)
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 get(fieldSchema) {
44
- return S.Metadata.get(fieldSchema, semanticId);
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 = S.Metadata.get(fieldSchema, semanticId);
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
  }