@reventlessdev/reventless-core 3.0.0-alpha.234 → 3.0.0-alpha.235

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,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.235 (2026-08-15)
7
+
8
+ ### Features
9
+
10
+ * **core:** curate the pages a shell builds across a plugin's views ([589835a](https://github.com/ReventlessDev/reventless-core/commit/589835a1ad428c5e2dea8bf6e4c64e49d2f67e0d))
11
+
12
+
6
13
  # 3.0.0-alpha.234 (2026-08-14)
7
14
 
8
15
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.234",
3
+ "version": "3.0.0-alpha.235",
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.6",
32
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.7",
33
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
+ "@reventlessdev/rescript-node": "2.0.0-alpha.7",
34
35
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.6",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.19",
37
- "@reventlessdev/rescript-ssh2": "2.0.0-alpha.6",
38
- "@reventlessdev/reventless-infra": "3.0.0-alpha.141",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
37
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.7",
40
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.113"
39
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.31",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.114",
41
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.142"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -20,6 +20,22 @@ type error =
20
20
  | UnknownPlugin(string)
21
21
  | UnknownView({plugin: string, view: string})
22
22
  | UnknownCommand({plugin: string, command: string})
23
+ | UnknownDerived({plugin: string, kind: string})
24
+
25
+ /** The pages a shell builds across a plugin's views rather than from any one of
26
+ them, as the vocabulary a deployment curates them by.
27
+
28
+ A closed set, and deliberately kinds rather than page names. A dashboard and
29
+ a lifecycle page are named by the framework, but a canvas is named by the
30
+ views it draws — from that deployment's hints and the view modes its shell
31
+ registered, neither of which this side can see. Curating by kind is the
32
+ question this side CAN answer, and it is also the question an author is
33
+ asking: "does this audience get calendars", not "does it get the one called
34
+ Deliveries".
35
+
36
+ Ordered as they appear in a menu (the dashboard tops the group, canvases and
37
+ schedulers tail it), so the vocabulary reads as the page set it curates. */
38
+ let derivedKinds = ["dashboard", "lifecycles", "canvas", "scheduler"]
23
39
 
24
40
  let describe = (e: error): string =>
25
41
  switch e {
@@ -28,16 +44,26 @@ let describe = (e: error): string =>
28
44
  `baked manifest: plugin "${plugin}" has no view named "${view}"`
29
45
  | UnknownCommand({plugin, command}) =>
30
46
  `baked manifest: plugin "${plugin}" has no command named "${command}"`
47
+ | UnknownDerived({plugin, kind}) =>
48
+ `baked manifest: plugin "${plugin}" names derived page kind "${kind}" — ` ++
49
+ `known kinds: ${derivedKinds->Array.join(", ")}`
31
50
  }
32
51
 
33
52
  // Views are named by component name — a view IS the surface. Commands are named
34
53
  // by command name, not by the write-side component that carries them: the
35
54
  // command is what a user invokes, and which slice or aggregate it lives on is an
36
55
  // implementation detail the author should not have to know to curate a shop.
56
+ //
57
+ // `derived` is neither: it names page KINDS, for the reason `derivedKinds`
58
+ // gives. Unlike the other two, what it selects is not filtered here — there is
59
+ // nothing in a `pluginStructure` to filter, because the pages do not exist until
60
+ // a shell builds them. It travels to the shell instead, which is the only place
61
+ // that can honour it.
37
62
  type selection = {
38
63
  plugin: string,
39
64
  views: option<array<string>>,
40
65
  commands: option<array<string>>,
66
+ derived: option<array<string>>,
41
67
  }
42
68
 
43
69
  let publicViews = (def: pluginStructure): array<queryableDef> =>
@@ -65,7 +91,16 @@ let validate = (~def: pluginStructure, sel: selection): result<unit, error> => {
65
91
  | None =>
66
92
  switch sel.commands->known(def->commandNames) {
67
93
  | Some(command) => Error(UnknownCommand({plugin: sel.plugin, command}))
68
- | None => Ok()
94
+ | None =>
95
+ // Against the vocabulary rather than against this plugin: a kind that
96
+ // generates no page here is a fact about the plugin's schemas, not a
97
+ // mistake — a shop naming `lifecycles` for a plugin whose views gain a
98
+ // status field next month is stating an intention, and failing it would
99
+ // punish the deployment for being early.
100
+ switch sel.derived->known(derivedKinds) {
101
+ | Some(kind) => Error(UnknownDerived({plugin: sel.plugin, kind}))
102
+ | None => Ok()
103
+ }
69
104
  }
70
105
  }
71
106
  }
@@ -182,7 +217,11 @@ let curate = (
182
217
  | Some((pluginId, def)) =>
183
218
  curateStructure(~pluginId, ~def, sel)->Result.map(curated => {
184
219
  entries->Array.push(
185
- Platform_ComponentDefinitionsApi.encodePluginStructureEntry(~pluginId, curated),
220
+ Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
221
+ ~pluginId,
222
+ ~derived=?sel.derived,
223
+ curated,
224
+ ),
186
225
  )
187
226
  entries
188
227
  })
@@ -6,6 +6,13 @@ import * as Stdlib_Result from "@rescript/runtime/lib/es6/Stdlib_Result.js";
6
6
  import * as Plugin$ReventlessCore from "../plugin/component/Plugin.res.mjs";
7
7
  import * as Platform_ComponentDefinitionsApi$ReventlessCore from "./Platform_ComponentDefinitionsApi.res.mjs";
8
8
 
9
+ let derivedKinds = [
10
+ "dashboard",
11
+ "lifecycles",
12
+ "canvas",
13
+ "scheduler"
14
+ ];
15
+
9
16
  function describe(e) {
10
17
  switch (e.TAG) {
11
18
  case "UnknownPlugin" :
@@ -14,6 +21,8 @@ function describe(e) {
14
21
  return `baked manifest: plugin "` + e.plugin + `" has no view named "` + e.view + `"`;
15
22
  case "UnknownCommand" :
16
23
  return `baked manifest: plugin "` + e.plugin + `" has no command named "` + e.command + `"`;
24
+ case "UnknownDerived" :
25
+ return `baked manifest: plugin "` + e.plugin + `" names derived page kind "` + e.kind + `" — known kinds: ` + derivedKinds.join(", ");
17
26
  }
18
27
  }
19
28
 
@@ -52,6 +61,17 @@ function validate(def, sel) {
52
61
  command: command
53
62
  }
54
63
  };
64
+ }
65
+ let kind = known(sel.derived, derivedKinds);
66
+ if (kind !== undefined) {
67
+ return {
68
+ TAG: "Error",
69
+ _0: {
70
+ TAG: "UnknownDerived",
71
+ plugin: sel.plugin,
72
+ kind: kind
73
+ }
74
+ };
55
75
  } else {
56
76
  return {
57
77
  TAG: "Ok",
@@ -189,13 +209,14 @@ function curate(structures, selections) {
189
209
  }
190
210
  let pluginId = match[0];
191
211
  return Stdlib_Result.map(curateStructure(pluginId, match[1], sel), curated => {
192
- entries.push(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(pluginId, curated));
212
+ entries.push(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry(pluginId, sel.derived, curated));
193
213
  return entries;
194
214
  });
195
215
  })), prim => prim);
196
216
  }
197
217
 
198
218
  export {
219
+ derivedKinds,
199
220
  describe,
200
221
  publicViews,
201
222
  writeSides,
@@ -207,8 +207,21 @@ let encodeExtensionDef = (e: extensionDef): JSON.t =>
207
207
  ("commandTypes", encodeStrings(e.commandTypes)),
208
208
  ])->JSON.Encode.object
209
209
 
210
- let encodePluginStructureEntry = (~pluginId: string, def: pluginStructure): JSON.t =>
211
- Dict.fromArray([
210
+ // `derived` is curation, not structure: it says which of the pages a shell
211
+ // builds across this plugin's views that shell may build. It therefore arrives
212
+ // as an argument rather than off `def` — nothing in a compiled plugin decides
213
+ // it, and the admin path, which curates nothing, passes none.
214
+ //
215
+ // Absent key ⇒ every kind, which is what an uncurated entry means and what
216
+ // every manifest written before the field says. A curated entry that wants none
217
+ // says so with `[]`, and the two are only distinguishable because the key is
218
+ // omitted rather than nulled.
219
+ let encodePluginStructureEntry = (
220
+ ~pluginId: string,
221
+ ~derived: option<array<string>>=?,
222
+ def: pluginStructure,
223
+ ): JSON.t => {
224
+ let entry = Dict.fromArray([
212
225
  ("pluginId", JSON.Encode.string(Plugin.name(pluginId))),
213
226
  (
214
227
  "readModels",
@@ -247,4 +260,9 @@ let encodePluginStructureEntry = (~pluginId: string, def: pluginStructure): JSON
247
260
  ->JSON.Encode.array,
248
261
  ),
249
262
  ("extensions", def.extensions->Array.map(encodeExtensionDef)->JSON.Encode.array),
250
- ])->JSON.Encode.object
263
+ ])
264
+ derived->Option.forEach(kinds =>
265
+ entry->Dict.set("derivedPages", kinds->Array.map(JSON.Encode.string)->JSON.Encode.array)
266
+ )
267
+ entry->JSON.Encode.object
268
+ }
@@ -339,8 +339,8 @@ function encodeExtensionDef(e) {
339
339
  ]);
340
340
  }
341
341
 
342
- function encodePluginStructureEntry(pluginId, def) {
343
- return Object.fromEntries([
342
+ function encodePluginStructureEntry(pluginId, derived, def) {
343
+ let entry = Object.fromEntries([
344
344
  [
345
345
  "pluginId",
346
346
  Plugin$ReventlessCore.name(pluginId)
@@ -382,6 +382,10 @@ function encodePluginStructureEntry(pluginId, def) {
382
382
  def.extensions.map(encodeExtensionDef)
383
383
  ]
384
384
  ]);
385
+ Stdlib_Option.forEach(derived, kinds => {
386
+ entry["derivedPages"] = kinds.map(prim => prim);
387
+ });
388
+ return entry;
385
389
  }
386
390
 
387
391
  let sdlQueryField = ` Platform_ComponentDefinitions: [Platform_ComponentDefinitionEntry!]!`;
@@ -89,6 +89,15 @@ let structures = [("Ordering@1.0.0", structure)]
89
89
  let bake = selections =>
90
90
  Platform_BakedManifest.curate(~structures, ~selections)
91
91
 
92
+ // Every case below varies one include-list and means "everything" by the rest,
93
+ // which is exactly what an omitted labelled argument says.
94
+ let sel = (~plugin, ~views=?, ~commands=?, ~derived=?): Platform_BakedManifest.selection => {
95
+ plugin,
96
+ views,
97
+ commands,
98
+ derived,
99
+ }
100
+
92
101
  let entries = (json: JSON.t) => json->JSON.Decode.array->Option.getOr([])
93
102
 
94
103
  let names = (entry: JSON.t, field: string) =>
@@ -110,19 +119,17 @@ let firstEntry = (r: result<JSON.t, Platform_BakedManifest.error>) =>
110
119
  describe("curate", () => {
111
120
  testSync("selects exactly the named components", () => {
112
121
  let entry =
113
- bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
114
- ->firstEntry
122
+ bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])->firstEntry
115
123
  expect((entry->names("stateViewSlices"), entry->names("readModels")))->toEqual((["Orders"], []))
116
124
  })
117
125
 
118
126
  testSync("drops a write side whose commands were all excluded", () => {
119
- let entry =
120
- bake([{plugin: "Ordering", views: Some([]), commands: Some(["PlaceOrder"])}])->firstEntry
127
+ let entry = bake([sel(~plugin="Ordering", ~views=[], ~commands=["PlaceOrder"])])->firstEntry
121
128
  expect(entry->names("stateChangeSlices"))->toEqual(["PlaceOrder"])
122
129
  })
123
130
 
124
131
  testSync("unset views and commands include every public component", () => {
125
- let entry = bake([{plugin: "Ordering", views: None, commands: None}])->firstEntry
132
+ let entry = bake([sel(~plugin="Ordering")])->firstEntry
126
133
  expect((
127
134
  entry->names("readModels"),
128
135
  entry->names("stateViewSlices"),
@@ -134,34 +141,72 @@ describe("curate", () => {
134
141
  // from, so an Internal view a kept command points at has to survive the bake.
135
142
  testSync("carries an Internal view referenced by an included command", () => {
136
143
  let entry =
137
- bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
144
+ bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])
138
145
  ->firstEntry
139
146
  expect(entry->names("internalQueryables"))->toEqual(["AvailableProducts"])
140
147
  })
141
148
 
142
149
  testSync("omits an Internal view no included command references", () => {
143
150
  let entry =
144
- bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["ImportOrders"])}])
151
+ bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["ImportOrders"])])
145
152
  ->firstEntry
146
153
  expect(entry->names("internalQueryables"))->toEqual([])
147
154
  })
148
155
 
149
- testSync("an Internal view is never selectable as a view", () =>
156
+ // The pages a shell builds across a plugin's views do not exist on this side,
157
+ // so what the bake can do about them is state which kinds an audience gets and
158
+ // carry that to the shell. Absent and empty therefore have to stay two
159
+ // different answers all the way out to the JSON, which is what these three
160
+ // cases pin: nothing said ⇒ no key ⇒ every kind, and `[]` ⇒ a key ⇒ none.
161
+ let derivedOf = (entry: JSON.t) =>
162
+ entry
163
+ ->JSON.Decode.object
164
+ ->Option.flatMap(d => d->Dict.get("derivedPages"))
165
+ ->Option.map(v => v->JSON.Decode.array->Option.getOr([])->Array.filterMap(JSON.Decode.string))
166
+
167
+ testSync("unset derived says nothing, leaving every kind to the shell", () =>
168
+ expect(bake([sel(~plugin="Ordering")])->firstEntry->derivedOf)->toEqual(None)
169
+ )
170
+
171
+ testSync("an empty derived list is carried, and is not the same as unset", () =>
172
+ expect(bake([sel(~plugin="Ordering", ~derived=[])])->firstEntry->derivedOf)->toEqual(Some([]))
173
+ )
174
+
175
+ testSync("carries exactly the derived kinds named", () =>
176
+ expect(
177
+ bake([sel(~plugin="Ordering", ~derived=["lifecycles", "canvas"])])->firstEntry->derivedOf,
178
+ )->toEqual(Some(["lifecycles", "canvas"]))
179
+ )
180
+
181
+ // Against the vocabulary, never against this plugin: a kind that generates no
182
+ // page for these schemas is a fact about the plugin, not a typo, and a shop
183
+ // that names `lifecycles` before its views carry a status is early, not wrong.
184
+ testSync("fails naming a derived kind outside the vocabulary", () =>
185
+ expect(bake([sel(~plugin="Ordering", ~derived=["dashboards"])]))->toEqual(
186
+ Error(Platform_BakedManifest.UnknownDerived({plugin: "Ordering", kind: "dashboards"})),
187
+ )
188
+ )
189
+
190
+ testSync("accepts a kind this plugin generates no page for", () =>
150
191
  expect(
151
- bake([{plugin: "Ordering", views: Some(["AvailableProducts"]), commands: None}]),
152
- )->toEqual(
192
+ bake([sel(~plugin="Ordering", ~derived=["scheduler"])])->firstEntry->derivedOf,
193
+ )->toEqual(Some(["scheduler"]))
194
+ )
195
+
196
+ testSync("an Internal view is never selectable as a view", () =>
197
+ expect(bake([sel(~plugin="Ordering", ~views=["AvailableProducts"])]))->toEqual(
153
198
  Error(Platform_BakedManifest.UnknownView({plugin: "Ordering", view: "AvailableProducts"})),
154
199
  )
155
200
  )
156
201
 
157
202
  testSync("fails naming an unknown plugin", () =>
158
- expect(bake([{plugin: "Shipping", views: None, commands: None}]))->toEqual(
203
+ expect(bake([sel(~plugin="Shipping")]))->toEqual(
159
204
  Error(Platform_BakedManifest.UnknownPlugin("Shipping")),
160
205
  )
161
206
  )
162
207
 
163
208
  testSync("fails naming an unknown command", () =>
164
- expect(bake([{plugin: "Ordering", views: None, commands: Some(["CancelOrder"])}]))->toEqual(
209
+ expect(bake([sel(~plugin="Ordering", ~commands=["CancelOrder"])]))->toEqual(
165
210
  Error(Platform_BakedManifest.UnknownCommand({plugin: "Ordering", command: "CancelOrder"})),
166
211
  )
167
212
  )
@@ -169,7 +214,7 @@ describe("curate", () => {
169
214
  // The anti-drift assertion: include everything and the baked entry IS the
170
215
  // served entry. Any future divergence in the encoder shows up here first.
171
216
  testSync("an all-inclusive bake is byte-identical to the served entry", () => {
172
- let baked = bake([{plugin: "Ordering", views: None, commands: None}])->firstEntry
217
+ let baked = bake([sel(~plugin="Ordering")])->firstEntry
173
218
  let served = Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
174
219
  ~pluginId="Ordering@1.0.0",
175
220
  structure,
@@ -182,8 +227,8 @@ describe("curate", () => {
182
227
  Platform_BakedManifest.curate(
183
228
  ~structures=[("Ordering@1.0.0", structure), ("Catalog@1.0.0", structure)],
184
229
  ~selections=[
185
- {plugin: "Catalog", views: Some([]), commands: Some([])},
186
- {plugin: "Ordering", views: Some([]), commands: Some([])},
230
+ sel(~plugin="Catalog", ~views=[], ~commands=[]),
231
+ sel(~plugin="Ordering", ~views=[], ~commands=[]),
187
232
  ],
188
233
  )
189
234
  let ids =
@@ -208,7 +253,7 @@ describe("curate", () => {
208
253
  // makes this hold — the assertion guards that property, not a second code path.
209
254
  testSync("a curated entry carries the owner field the served entry carries", () => {
210
255
  let entry =
211
- bake([{plugin: "Ordering", views: Some(["Orders"]), commands: Some(["PlaceOrder"])}])
256
+ bake([sel(~plugin="Ordering", ~views=["Orders"], ~commands=["PlaceOrder"])])
212
257
  ->firstEntry
213
258
  let ownerOf = (key, name) =>
214
259
  entry
@@ -109,6 +109,15 @@ function bake(selections) {
109
109
  return Platform_BakedManifest$ReventlessCore.curate(structures, selections);
110
110
  }
111
111
 
112
+ function sel(plugin, views, commands, derived) {
113
+ return {
114
+ plugin: plugin,
115
+ views: views,
116
+ commands: commands,
117
+ derived: derived
118
+ };
119
+ }
120
+
112
121
  function entries(json) {
113
122
  return Stdlib_Option.getOr(Stdlib_JSON.Decode.array(json), []);
114
123
  }
@@ -127,11 +136,7 @@ function firstEntry(r) {
127
136
 
128
137
  globalThis.describe("curate", () => {
129
138
  globalThis.test("selects exactly the named components", () => {
130
- let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
131
- plugin: "Ordering",
132
- views: ["Orders"],
133
- commands: ["PlaceOrder"]
134
- }]));
139
+ let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
135
140
  globalThis.expect([
136
141
  names(entry, "stateViewSlices"),
137
142
  names(entry, "readModels")
@@ -141,18 +146,15 @@ globalThis.describe("curate", () => {
141
146
  ]);
142
147
  });
143
148
  globalThis.test("drops a write side whose commands were all excluded", () => {
144
- let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
145
- plugin: "Ordering",
146
- views: [],
147
- commands: ["PlaceOrder"]
148
- }]));
149
+ let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", [], ["PlaceOrder"], undefined)]));
149
150
  globalThis.expect(names(entry, "stateChangeSlices")).toEqual(["PlaceOrder"]);
150
151
  });
151
152
  globalThis.test("unset views and commands include every public component", () => {
152
153
  let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
153
154
  plugin: "Ordering",
154
155
  views: undefined,
155
- commands: undefined
156
+ commands: undefined,
157
+ derived: undefined
156
158
  }]));
157
159
  globalThis.expect([
158
160
  names(entry, "readModels"),
@@ -168,27 +170,49 @@ globalThis.describe("curate", () => {
168
170
  ]);
169
171
  });
170
172
  globalThis.test("carries an Internal view referenced by an included command", () => {
171
- let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
172
- plugin: "Ordering",
173
- views: ["Orders"],
174
- commands: ["PlaceOrder"]
175
- }]));
173
+ let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
176
174
  globalThis.expect(names(entry, "internalQueryables")).toEqual(["AvailableProducts"]);
177
175
  });
178
176
  globalThis.test("omits an Internal view no included command references", () => {
179
- let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
180
- plugin: "Ordering",
181
- views: ["Orders"],
182
- commands: ["ImportOrders"]
183
- }]));
177
+ let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["ImportOrders"], undefined)]));
184
178
  globalThis.expect(names(entry, "internalQueryables")).toEqual([]);
185
179
  });
186
- globalThis.test("an Internal view is never selectable as a view", () => {
187
- globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
180
+ let derivedOf = entry => Stdlib_Option.map(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d["derivedPages"]), v => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_JSON.Decode.array(v), []), Stdlib_JSON.Decode.string));
181
+ globalThis.test("unset derived says nothing, leaving every kind to the shell", () => {
182
+ globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
188
183
  plugin: "Ordering",
189
- views: ["AvailableProducts"],
190
- commands: undefined
191
- }])).toEqual({
184
+ views: undefined,
185
+ commands: undefined,
186
+ derived: undefined
187
+ }])))).toEqual(undefined);
188
+ });
189
+ globalThis.test("an empty derived list is carried, and is not the same as unset", () => {
190
+ globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, [])])))).toEqual([]);
191
+ });
192
+ globalThis.test("carries exactly the derived kinds named", () => {
193
+ globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, [
194
+ "lifecycles",
195
+ "canvas"
196
+ ])])))).toEqual([
197
+ "lifecycles",
198
+ "canvas"
199
+ ]);
200
+ });
201
+ globalThis.test("fails naming a derived kind outside the vocabulary", () => {
202
+ globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, ["dashboards"])])).toEqual({
203
+ TAG: "Error",
204
+ _0: {
205
+ TAG: "UnknownDerived",
206
+ plugin: "Ordering",
207
+ kind: "dashboards"
208
+ }
209
+ });
210
+ });
211
+ globalThis.test("accepts a kind this plugin generates no page for", () => {
212
+ globalThis.expect(derivedOf(firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, undefined, ["scheduler"])])))).toEqual(["scheduler"]);
213
+ });
214
+ globalThis.test("an Internal view is never selectable as a view", () => {
215
+ globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["AvailableProducts"], undefined, undefined)])).toEqual({
192
216
  TAG: "Error",
193
217
  _0: {
194
218
  TAG: "UnknownView",
@@ -201,7 +225,8 @@ globalThis.describe("curate", () => {
201
225
  globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
202
226
  plugin: "Shipping",
203
227
  views: undefined,
204
- commands: undefined
228
+ commands: undefined,
229
+ derived: undefined
205
230
  }])).toEqual({
206
231
  TAG: "Error",
207
232
  _0: {
@@ -211,11 +236,7 @@ globalThis.describe("curate", () => {
211
236
  });
212
237
  });
213
238
  globalThis.test("fails naming an unknown command", () => {
214
- globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [{
215
- plugin: "Ordering",
216
- views: undefined,
217
- commands: ["CancelOrder"]
218
- }])).toEqual({
239
+ globalThis.expect(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", undefined, ["CancelOrder"], undefined)])).toEqual({
219
240
  TAG: "Error",
220
241
  _0: {
221
242
  TAG: "UnknownCommand",
@@ -228,9 +249,10 @@ globalThis.describe("curate", () => {
228
249
  let baked = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
229
250
  plugin: "Ordering",
230
251
  views: undefined,
231
- commands: undefined
252
+ commands: undefined,
253
+ derived: undefined
232
254
  }]));
233
- let served = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering@1.0.0", structure);
255
+ let served = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering@1.0.0", undefined, structure);
234
256
  globalThis.expect(JSON.stringify(baked)).toEqual(JSON.stringify(served));
235
257
  });
236
258
  globalThis.test("one entry per selection, in declaration order", () => {
@@ -244,16 +266,8 @@ globalThis.describe("curate", () => {
244
266
  structure
245
267
  ]
246
268
  ], [
247
- {
248
- plugin: "Catalog",
249
- views: [],
250
- commands: []
251
- },
252
- {
253
- plugin: "Ordering",
254
- views: [],
255
- commands: []
256
- }
269
+ sel("Catalog", [], [], undefined),
270
+ sel("Ordering", [], [], undefined)
257
271
  ]);
258
272
  let ids;
259
273
  ids = json.TAG === "Ok" ? Stdlib_Array.filterMap(entries(json._0), e => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(e), d => d["pluginId"]), Stdlib_JSON.Decode.string)) : [];
@@ -263,11 +277,7 @@ globalThis.describe("curate", () => {
263
277
  ]);
264
278
  });
265
279
  globalThis.test("a curated entry carries the owner field the served entry carries", () => {
266
- let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [{
267
- plugin: "Ordering",
268
- views: ["Orders"],
269
- commands: ["PlaceOrder"]
270
- }]));
280
+ let entry = firstEntry(Platform_BakedManifest$ReventlessCore.curate(structures, [sel("Ordering", ["Orders"], ["PlaceOrder"], undefined)]));
271
281
  let ownerOf = (key, name) => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d[key]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.object).find(d => Primitive_object.equal(Stdlib_Option.flatMap(d["name"], Stdlib_JSON.Decode.string), name)), d => d["ownerField"]), Stdlib_JSON.Decode.string);
272
282
  let commandOwner = Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_Array.filterMap(Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), d => d["stateChangeSlices"]), Stdlib_JSON.Decode.array), []), Stdlib_JSON.Decode.object).flatMap(w => Stdlib_Option.getOr(Stdlib_Option.flatMap(w["commands"], Stdlib_JSON.Decode.array), [])), Stdlib_JSON.Decode.object)[0], d => d["ownerField"]), Stdlib_JSON.Decode.string);
273
283
  globalThis.expect([
@@ -287,6 +297,7 @@ export {
287
297
  structure,
288
298
  structures,
289
299
  bake,
300
+ sel,
290
301
  entries,
291
302
  names,
292
303
  firstEntry,
@@ -147,7 +147,7 @@ let structure = {
147
147
  requiredStoreDeclarations: undefined
148
148
  };
149
149
 
150
- let encoded = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", structure);
150
+ let encoded = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, structure);
151
151
 
152
152
  let json = JSON.stringify(encoded);
153
153
 
@@ -255,7 +255,7 @@ globalThis.describe("translation-slice externalSystem round-trip", () => {
255
255
  requiredStores: undefined,
256
256
  requiredStoreDeclarations: undefined
257
257
  };
258
- let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", externalStructure));
258
+ let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, externalStructure));
259
259
  globalThis.test("surfaces Some outbound externalSystem as its inner string", () => {
260
260
  globalThis.expect(externalJson.includes("\"externalSystem\":\"ShipperGateway\"")).toEqual(true);
261
261
  });
@@ -341,7 +341,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
341
341
  requiredStores: undefined,
342
342
  requiredStoreDeclarations: undefined
343
343
  };
344
- let mixedEntry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", mixed);
344
+ let mixedEntry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", undefined, mixed);
345
345
  let namesAt = (entry, key) => Stdlib_Array.filterMap(Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(entry), o => o[key]), Stdlib_JSON.Decode.array), []), c => Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(c), o => o["name"]), Stdlib_JSON.Decode.string));
346
346
  globalThis.test("excludes an Internal queryableDef from the encoded read-side", () => {
347
347
  globalThis.expect(namesAt(mixedEntry, "readModels")).toEqual(["Products"]);
@@ -357,7 +357,7 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
357
357
  ]);
358
358
  });
359
359
  globalThis.test("encodes internalQueryables as [] for an all-Public entry, not a missing key", () => {
360
- let entry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", {
360
+ let entry = Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Ordering", undefined, {
361
361
  readModels: [qbl],
362
362
  stateViewSlices: [],
363
363
  stateChangeSlices: structure_stateChangeSlices,
@@ -459,7 +459,7 @@ globalThis.describe("allowedStates + statusField populated", () => {
459
459
  requiredStores: undefined,
460
460
  requiredStoreDeclarations: undefined
461
461
  };
462
- let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Platform", structureWithStates));
462
+ let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Platform", undefined, structureWithStates));
463
463
  globalThis.test("encodes populated allowedStates as a JSON array", () => {
464
464
  globalThis.expect(json.includes("\"allowedStates\":[\"Inactive\"]")).toEqual(true);
465
465
  });
@@ -481,7 +481,7 @@ globalThis.describe("singleQueryField reaches the caller", () => {
481
481
  globalThis.expect(json.includes("\"singleQueryField\":\"Catalog_Product\"")).toEqual(true);
482
482
  });
483
483
  globalThis.test("encodes an unstated singular as null", () => {
484
- let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
484
+ let unstated = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
485
485
  readModels: [{
486
486
  name: "Products",
487
487
  queryField: "Catalog_Products",
@@ -526,7 +526,7 @@ globalThis.describe("idField / idFieldSource reach the caller", () => {
526
526
  globalThis.expect(json.includes("\"idFieldSource\":\"convention\"")).toEqual(true);
527
527
  });
528
528
  globalThis.test("encodes an unresolved key as null on both fields", () => {
529
- let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", {
529
+ let unresolved = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", undefined, {
530
530
  readModels: [{
531
531
  name: "Products",
532
532
  queryField: "Catalog_Products",
@@ -120,7 +120,7 @@ globalThis.describe("requiredAccess derived from the authorization rule", () =>
120
120
  globalThis.expect(viewNamed("Orders")).toEqual(undefined);
121
121
  });
122
122
  globalThis.test("nothing to require encodes as null, not an empty list", () => {
123
- let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("AccessPlugin", structure));
123
+ let json = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("AccessPlugin", undefined, structure));
124
124
  globalThis.expect([
125
125
  json.includes(`"requiredAccess":null`),
126
126
  json.includes(`"requiredAccess":["Admin","Ops"]`),