@reventlessdev/reventless-core 3.0.0-alpha.149 → 3.0.0-alpha.151

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.151 (2026-07-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ * **plugin-lifecycle:** backfill plugin kind onto existing rows via deploy-time RedetectPlugin ([502dbd6](https://github.com/ReventlessDev/reventless-core/commit/502dbd65d1fdd5b8beae8a7b96df06dcfa32fe3e))
11
+ * **plugin-lifecycle:** make read-model kind nullable so kind-less rows don't null the admin Plugins query ([671c4f6](https://github.com/ReventlessDev/reventless-core/commit/671c4f6cbbdde32a8dced7290fc8aa5b0063536f))
12
+
13
+
14
+ # 3.0.0-alpha.150 (2026-07-10)
15
+
16
+ ### Features
17
+
18
+ * **reventless-ppx:** add [@group](https://github.com/group)By state-field annotation → x-reventless-group-by ([9e7a7b2](https://github.com/ReventlessDev/reventless-core/commit/9e7a7b29e54fb4eda1c6a145c0e7b6dcf26940ee))
19
+
20
+
6
21
  # 3.0.0-alpha.149 (2026-07-09)
7
22
 
8
23
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.149",
3
+ "version": "3.0.0-alpha.151",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -30,20 +30,20 @@
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
32
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
33
- "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
34
- "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
35
33
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
34
+ "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
35
+ "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
37
37
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
38
- "@reventlessdev/reventless-infra": "3.0.0-alpha.91",
39
- "@reventlessdev/reventless-spec": "3.0.0-alpha.69",
40
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.24"
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.93",
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.71"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "^12.3.0",
45
45
  "sury-ppx": "11.0.0-alpha.2",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.51"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.52"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.0"
@@ -44,6 +44,11 @@ let mergeAnnotations = (
44
44
  if spec.summary->Array.includes(fieldName) {
45
45
  obj->Dict.set("x-reventless-summary", JSON.Encode.bool(true))
46
46
  }
47
+ switch spec.groupBy {
48
+ | Some(name) if name === fieldName =>
49
+ obj->Dict.set("x-reventless-group-by", JSON.Encode.bool(true))
50
+ | _ => ()
51
+ }
47
52
  switch spec.drillTargets->Array.find(((field, _)) => field === fieldName) {
48
53
  | Some((_, sliceName)) =>
49
54
  obj->Dict.set("x-reventless-drillTarget", JSON.Encode.string(sliceName))
@@ -41,6 +41,10 @@ function mergeAnnotations(fieldSchema, fieldName, spec) {
41
41
  if (spec.summary.includes(fieldName)) {
42
42
  obj["x-reventless-summary"] = true;
43
43
  }
44
+ let name = spec.groupBy;
45
+ if (name !== undefined && name === fieldName) {
46
+ obj["x-reventless-group-by"] = true;
47
+ }
44
48
  let match$1 = spec.drillTargets.find(param => param[0] === fieldName);
45
49
  if (match$1 !== undefined) {
46
50
  obj["x-reventless-drillTarget"] = match$1[1];
@@ -150,6 +150,14 @@ module Make = (Spec: Spec) => {
150
150
  // 1 additional minute to allow additional latency
151
151
  HandleDirective(directiveHandler, CreateDisconnectSchedule(id, interval + 2)),
152
152
  ]
153
+ | RedetectPlugin(interval) => [
154
+ // Deploy-time re-detect: drive Redetect (not Heartbeat) so an already-connected
155
+ // version re-runs the handshake and refreshes its stored definition. Still
156
+ // re-arm the disconnect schedule exactly like a heartbeat so liveness tracking
157
+ // continues from the deploy moment.
158
+ PublishCommand(Plugin.name(id), Delegate.Redetect(Plugin.version(id))),
159
+ HandleDirective(directiveHandler, CreateDisconnectSchedule(id, interval + 2)),
160
+ ]
153
161
  | ConnectPlugin(pluginDefinition) =>
154
162
  // Validate protocol versions declared by the connecting plugin.
155
163
  // On mismatch, emit a ReportIncompatibility command so that an
@@ -183,6 +183,26 @@ function Make(Spec) {
183
183
  _0: cmd._0
184
184
  }
185
185
  }];
186
+ case "RedetectPlugin" :
187
+ return [
188
+ {
189
+ TAG: "PublishCommand",
190
+ _0: Plugin$ReventlessCore.name(id),
191
+ _1: {
192
+ TAG: "Redetect",
193
+ _0: Plugin$ReventlessCore.version(id)
194
+ }
195
+ },
196
+ {
197
+ TAG: "HandleDirective",
198
+ _0: directiveHandler,
199
+ _1: {
200
+ TAG: "CreateDisconnectSchedule",
201
+ _0: id,
202
+ _1: cmd._0 + 2 | 0
203
+ }
204
+ }
205
+ ];
186
206
  }
187
207
  };
188
208
  let mapOutgoingEvent = (id, event, _meta, _queryEngine) => {
@@ -145,10 +145,24 @@ let decide = (state, command) =>
145
145
  | Some({status: Disconnected, definition}) => Ok(connectEvents(state, v, definition)) // reconnect
146
146
  | Some({status: Inactive | Retired}) => Ok([]) // not heartbeat-revivable (admin only)
147
147
  }
148
+ | Redetect(v) =>
149
+ // Deploy-time re-detect: unlike Heartbeat, a *connected* version re-runs the
150
+ // handshake (VersionDetected → ConnectPlugin → Connect) so the plugin re-answers
151
+ // with its current definition and Connect below refreshes the stored def. Unknown
152
+ // and disconnected versions detect exactly as a heartbeat would; archived versions
153
+ // stay admin-only.
154
+ switch state.known->Dict.get(v) {
155
+ | Some({status: Inactive | Retired}) => Ok([]) // archived — not re-detect-revivable
156
+ | None | Some({status: Connected | Disconnected}) => Ok([VersionDetected(v)])
157
+ }
148
158
  | Connect(def) =>
149
159
  let v = def.version
150
160
  switch state.known->Dict.get(v) {
151
- | Some({status: Connected}) => Ok([]) // idempotent
161
+ // Idempotent when the definition is unchanged; when a redeploy carries a changed
162
+ // definition (e.g. a newly added `kind`, updated uiFragments/protocols) re-emit
163
+ // VersionConnected to overwrite the stored def and re-project the row. The version
164
+ // is already current, so connectEvents' supersede stays empty — a bare refresh.
165
+ | Some({status: Connected, definition}) => definition == def ? Ok([]) : Ok([VersionConnected(def)])
152
166
  | _ => Ok(connectEvents(state, v, def))
153
167
  }
154
168
  | Disconnect(v) =>
@@ -172,55 +172,94 @@ function decide(state, command) {
172
172
  _0: []
173
173
  };
174
174
  }
175
- case "Connect" :
176
- let def = command._0;
177
- let v$1 = def.version;
175
+ case "Redetect" :
176
+ let v$1 = command._0;
178
177
  let match$1 = state.known[v$1];
179
- if (match$1 !== undefined && match$1.status === "Connected") {
178
+ if (match$1 === undefined) {
180
179
  return {
181
180
  TAG: "Ok",
182
- _0: []
181
+ _0: [{
182
+ TAG: "VersionDetected",
183
+ _0: v$1
184
+ }]
183
185
  };
186
+ }
187
+ switch (match$1.status) {
188
+ case "Connected" :
189
+ case "Disconnected" :
190
+ return {
191
+ TAG: "Ok",
192
+ _0: [{
193
+ TAG: "VersionDetected",
194
+ _0: v$1
195
+ }]
196
+ };
197
+ case "Inactive" :
198
+ case "Retired" :
199
+ return {
200
+ TAG: "Ok",
201
+ _0: []
202
+ };
203
+ }
204
+ case "Connect" :
205
+ let def = command._0;
206
+ let v$2 = def.version;
207
+ let match$2 = state.known[v$2];
208
+ if (match$2 !== undefined && match$2.status === "Connected") {
209
+ if (Primitive_object.equal(match$2.definition, def)) {
210
+ return {
211
+ TAG: "Ok",
212
+ _0: []
213
+ };
214
+ } else {
215
+ return {
216
+ TAG: "Ok",
217
+ _0: [{
218
+ TAG: "VersionConnected",
219
+ _0: def
220
+ }]
221
+ };
222
+ }
184
223
  } else {
185
224
  return {
186
225
  TAG: "Ok",
187
- _0: connectEvents(state, v$1, def)
226
+ _0: connectEvents(state, v$2, def)
188
227
  };
189
228
  }
190
229
  case "Disconnect" :
191
- let v$2 = command._0;
192
- let match$2 = state.known[v$2];
193
- if (match$2 === undefined) {
230
+ let v$3 = command._0;
231
+ let match$3 = state.known[v$3];
232
+ if (match$3 === undefined) {
194
233
  return {
195
234
  TAG: "Ok",
196
235
  _0: []
197
236
  };
198
237
  }
199
- if (match$2.status !== "Connected") {
238
+ if (match$3.status !== "Connected") {
200
239
  return {
201
240
  TAG: "Ok",
202
241
  _0: []
203
242
  };
204
243
  }
205
- let definition = match$2.definition;
244
+ let definition = match$3.definition;
206
245
  return {
207
246
  TAG: "Ok",
208
247
  _0: [{
209
248
  TAG: "VersionDisconnected",
210
249
  _0: definition
211
- }].concat(uiDeregisterEvents(definition.id, definition.uiFragments)).concat(promoteEvents(state, v$2))
250
+ }].concat(uiDeregisterEvents(definition.id, definition.uiFragments)).concat(promoteEvents(state, v$3))
212
251
  };
213
252
  case "Activate" :
214
- let v$3 = command._0;
215
- let match$3 = state.known[v$3];
216
- if (match$3 === undefined) {
253
+ let v$4 = command._0;
254
+ let match$4 = state.known[v$4];
255
+ if (match$4 === undefined) {
217
256
  return {
218
257
  TAG: "Error",
219
258
  _0: "UnknownVersion"
220
259
  };
221
260
  }
222
- let definition$1 = match$3.definition;
223
- switch (match$3.status) {
261
+ let definition$1 = match$4.definition;
262
+ switch (match$4.status) {
224
263
  case "Connected" :
225
264
  return {
226
265
  TAG: "Ok",
@@ -240,17 +279,17 @@ function decide(state, command) {
240
279
  _0: definition$1
241
280
  }].concat(uiRegisterEvents(definition$1.id, definition$1.uiFragments));
242
281
  let supersede;
243
- if (currentAfterConnect(state.known, v$3) === v$3) {
282
+ if (currentAfterConnect(state.known, v$4) === v$4) {
244
283
  let c = state.current;
245
284
  if (c !== undefined) {
246
- if (c !== v$3) {
247
- let match$4 = state.known[c];
248
- supersede = match$4 !== undefined ? [{
285
+ if (c !== v$4) {
286
+ let match$5 = state.known[c];
287
+ supersede = match$5 !== undefined ? [{
249
288
  TAG: "VersionSuperseded",
250
289
  _0: {
251
290
  supersededVersion: c,
252
- supersededDefinition: match$4.definition,
253
- newVersion: v$3,
291
+ supersededDefinition: match$5.definition,
292
+ newVersion: v$4,
254
293
  newDefinition: definition$1
255
294
  }
256
295
  }] : [];
@@ -269,16 +308,16 @@ function decide(state, command) {
269
308
  };
270
309
  break;
271
310
  case "Deactivate" :
272
- let v$4 = command._0;
273
- let match$5 = state.known[v$4];
274
- if (match$5 === undefined) {
311
+ let v$5 = command._0;
312
+ let match$6 = state.known[v$5];
313
+ if (match$6 === undefined) {
275
314
  return {
276
315
  TAG: "Error",
277
316
  _0: "UnknownVersion"
278
317
  };
279
318
  }
280
- let definition$2 = match$5.definition;
281
- switch (match$5.status) {
319
+ let definition$2 = match$6.definition;
320
+ switch (match$6.status) {
282
321
  case "Connected" :
283
322
  case "Disconnected" :
284
323
  break;
@@ -294,7 +333,7 @@ function decide(state, command) {
294
333
  _0: [{
295
334
  TAG: "VersionDeactivated",
296
335
  _0: definition$2
297
- }].concat(uiDeregisterEvents(definition$2.id, definition$2.uiFragments)).concat(promoteEvents(state, v$4))
336
+ }].concat(uiDeregisterEvents(definition$2.id, definition$2.uiFragments)).concat(promoteEvents(state, v$5))
298
337
  };
299
338
  break;
300
339
  case "ReportIncompatibility" :
@@ -306,16 +345,16 @@ function decide(state, command) {
306
345
  }]
307
346
  };
308
347
  case "Retire" :
309
- let v$5 = command._0;
310
- let match$6 = state.known[v$5];
311
- if (match$6 === undefined) {
348
+ let v$6 = command._0;
349
+ let match$7 = state.known[v$6];
350
+ if (match$7 === undefined) {
312
351
  return {
313
352
  TAG: "Error",
314
353
  _0: "UnknownVersion"
315
354
  };
316
355
  }
317
- let definition$3 = match$6.definition;
318
- if (match$6.status === "Retired") {
356
+ let definition$3 = match$7.definition;
357
+ if (match$7.status === "Retired") {
319
358
  return {
320
359
  TAG: "Ok",
321
360
  _0: []
@@ -326,7 +365,7 @@ function decide(state, command) {
326
365
  _0: [{
327
366
  TAG: "VersionRetired",
328
367
  _0: definition$3
329
- }].concat(uiDeregisterEvents(definition$3.id, definition$3.uiFragments)).concat(promoteEvents(state, v$5))
368
+ }].concat(uiDeregisterEvents(definition$3.id, definition$3.uiFragments)).concat(promoteEvents(state, v$6))
330
369
  };
331
370
  }
332
371
  }
@@ -10,6 +10,10 @@ open Reventless.Plugin
10
10
  @schema
11
11
  type command =
12
12
  | @noApi Heartbeat(version)
13
+ // Deploy-time re-detect (from RedetectPlugin). Forces the connect handshake to
14
+ // re-run for an already-connected version so its definition is refreshed on the
15
+ // row — unlike Heartbeat, which no-ops a connected keep-alive.
16
+ | @noApi Redetect(version)
13
17
  | @noApi Connect(pluginDefinition)
14
18
  | @noApi Disconnect(version)
15
19
  // Admin lifecycle commands — API-exposed (auto-derived admin mutations,
@@ -9,6 +9,10 @@ let commandSchema = S.union([
9
9
  TAG: "Heartbeat",
10
10
  _0: s.m(Plugin$Reventless.versionSchema)
11
11
  })),
12
+ S.schema(s => ({
13
+ TAG: "Redetect",
14
+ _0: s.m(Plugin$Reventless.versionSchema)
15
+ })),
12
16
  S.schema(s => ({
13
17
  TAG: "Connect",
14
18
  _0: s.m(Plugin$Reventless.pluginDefinitionSchema)
@@ -117,6 +121,7 @@ let errorSchema = S.union([
117
121
 
118
122
  let commandSchema$1 = Api$ReventlessInfra.markNoApiVariants(commandSchema, [
119
123
  "Heartbeat",
124
+ "Redetect",
120
125
  "Connect",
121
126
  "Disconnect",
122
127
  "ReportIncompatibility"
@@ -58,7 +58,10 @@ let displayState = (
58
58
  uiFragments: def.uiFragments,
59
59
  structure: def.structure,
60
60
  dcbEventLog: def.dcbEventLog,
61
- kind: def.kind,
61
+ // A freshly projected row always knows its kind (the definition carries it as a
62
+ // mandatory field). `option` here is purely for rows persisted before `kind`
63
+ // existed — see PluginsReadModelSpec.state.kind.
64
+ kind: Some(def.kind),
62
65
  // The current version is never listed among the "other" connected versions.
63
66
  otherConnectedVersions: otherConnectedVersions->Util.without(def.version),
64
67
  }
@@ -42,7 +42,18 @@ type state = {
42
42
  // @scan opts the field into server-side equality filtering so the connection gains
43
43
  // a `kindEq` filter — the panel split (main list vs System/Infrastructure) pages
44
44
  // correctly server-side rather than filtering a page client-side.
45
- @scan kind: Reventless.Plugin.pluginKind,
45
+ // @groupBy tags this field as the list view's section key (emitted as
46
+ // `x-reventless-group-by`); AutoUI sections the admin Plugins view by kind.
47
+ //
48
+ // **Nullable on purpose.** Rows projected before `kind` existed carry no `kind`
49
+ // attribute, and the deploy-time RedetectPlugin backfill only stamps a row on the
50
+ // owning plugin's next deploy. A non-null `PluginKind!` would make GraphQL null the
51
+ // entire `Platform_Plugins` query the moment a single kind-less row is returned
52
+ // (existing rows, a newly-connecting plugin before it's stamped, or an internal
53
+ // bookkeeping row). Optional here → nullable `Platform_PluginKind` in the SDL, so a
54
+ // blank kind degrades to a missing group (AutoListView's trailing group) instead of
55
+ // taking down the whole list. Backfill then upgrades blanks to real kinds.
56
+ @scan @groupBy kind: option<Reventless.Plugin.pluginKind>,
46
57
  // Other versions of this name that are currently **Connected** — excludes this
47
58
  // row's own `version` (whose status is the `status` field above). Lets the
48
59
  // projection recompute which version is current (highest Connected) during a
@@ -67,7 +78,7 @@ type queryResult = {
67
78
  uiFragments: option<Reventless.Plugin.uiFragmentManifest>,
68
79
  structure: option<Reventless.Plugin.pluginStructure>,
69
80
  dcbEventLog: option<Reventless.Plugin.dcbEventLogDefinition>,
70
- kind: Reventless.Plugin.pluginKind,
81
+ kind: option<Reventless.Plugin.pluginKind>,
71
82
  }
72
83
 
73
84
 
@@ -28,7 +28,7 @@ let stateSchema = S.schema(s => ({
28
28
  uiFragments: s.m(Plugin$Reventless.uiFragmentManifestOptionSchema),
29
29
  structure: s.m(Plugin$Reventless.pluginStructureOptionSchema),
30
30
  dcbEventLog: s.m(Plugin$Reventless.dcbEventLogOptionSchema),
31
- kind: s.m(Plugin$Reventless.pluginKindSchema),
31
+ kind: s.m(S.option(Plugin$Reventless.pluginKindSchema)),
32
32
  otherConnectedVersions: s.m(S.array(Plugin$Reventless.versionSchema))
33
33
  }));
34
34
 
@@ -48,6 +48,7 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
48
48
  scan: ["kind"],
49
49
  scanSort: [],
50
50
  status: undefined,
51
+ groupBy: "kind",
51
52
  visibility: undefined
52
53
  });
53
54
 
@@ -30,6 +30,7 @@ let emptySpec: Reventless.StateAnnotations.stateAnnotationSpec = {
30
30
  scan: [],
31
31
  scanSort: [],
32
32
  status: None,
33
+ groupBy: None,
33
34
  visibility: None,
34
35
  }
35
36
 
@@ -305,6 +306,25 @@ describe("SuryToJsonSchema:", () => {
305
306
  ))->toEqual((Some(true), None))
306
307
  })
307
308
 
309
+ testSync("emits x-reventless-group-by on the field named by groupBy", () => {
310
+ let schema = S.schema(s =>
311
+ {
312
+ "id": s.matches(S.string),
313
+ "kind": s.matches(S.string),
314
+ }
315
+ )->S.castToUnknown
316
+ let schema' = schema->withSpec({...emptySpec, groupBy: Some("kind")})
317
+ let json = SuryToJsonSchema.deriveObjectSchema(schema')
318
+ let kindSchema = getPropertyOf(json, "kind")
319
+ let idSchema = getPropertyOf(json, "id")
320
+ expect((
321
+ kindSchema
322
+ ->Option.flatMap(s => getProperty(s, "x-reventless-group-by"))
323
+ ->Option.flatMap(JSON.Decode.bool),
324
+ idSchema->Option.flatMap(s => getProperty(s, "x-reventless-group-by")),
325
+ ))->toEqual((Some(true), None))
326
+ })
327
+
308
328
  testSync("emits x-reventless-collapsed on field listed in collapsed", () => {
309
329
  let schema = S.schema(s =>
310
330
  {
@@ -64,6 +64,7 @@ let emptySpec = {
64
64
  scan: emptySpec_scan,
65
65
  scanSort: emptySpec_scanSort,
66
66
  status: undefined,
67
+ groupBy: undefined,
67
68
  visibility: undefined
68
69
  };
69
70
 
@@ -100,6 +101,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
100
101
  scan: emptySpec_scan,
101
102
  scanSort: emptySpec_scanSort,
102
103
  status: undefined,
104
+ groupBy: undefined,
103
105
  visibility: undefined
104
106
  });
105
107
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -129,6 +131,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
129
131
  scan: emptySpec_scan,
130
132
  scanSort: emptySpec_scanSort,
131
133
  status: undefined,
134
+ groupBy: undefined,
132
135
  visibility: undefined
133
136
  });
134
137
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -161,6 +164,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
161
164
  scan: emptySpec_scan,
162
165
  scanSort: emptySpec_scanSort,
163
166
  status: undefined,
167
+ groupBy: undefined,
164
168
  visibility: undefined
165
169
  });
166
170
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -189,6 +193,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
189
193
  scan: emptySpec_scan,
190
194
  scanSort: emptySpec_scanSort,
191
195
  status: undefined,
196
+ groupBy: undefined,
192
197
  visibility: undefined
193
198
  });
194
199
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -217,6 +222,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
217
222
  scan: emptySpec_scan,
218
223
  scanSort: emptySpec_scanSort,
219
224
  status: undefined,
225
+ groupBy: undefined,
220
226
  visibility: undefined
221
227
  });
222
228
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -242,6 +248,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
242
248
  scan: emptySpec_scan,
243
249
  scanSort: emptySpec_scanSort,
244
250
  status: undefined,
251
+ groupBy: undefined,
245
252
  visibility: undefined
246
253
  });
247
254
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -267,6 +274,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
267
274
  scan: emptySpec_scan,
268
275
  scanSort: emptySpec_scanSort,
269
276
  status: undefined,
277
+ groupBy: undefined,
270
278
  visibility: undefined
271
279
  });
272
280
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -299,6 +307,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
299
307
  scan: emptySpec_scan,
300
308
  scanSort: emptySpec_scanSort,
301
309
  status: undefined,
310
+ groupBy: undefined,
302
311
  visibility: undefined
303
312
  });
304
313
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -334,6 +343,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
334
343
  scan: emptySpec_scan,
335
344
  scanSort: emptySpec_scanSort,
336
345
  status: undefined,
346
+ groupBy: undefined,
337
347
  visibility: undefined
338
348
  });
339
349
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -372,6 +382,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
372
382
  scan: emptySpec_scan,
373
383
  scanSort: emptySpec_scanSort,
374
384
  status: undefined,
385
+ groupBy: undefined,
375
386
  visibility: undefined
376
387
  });
377
388
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -400,6 +411,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
400
411
  scan: emptySpec_scan,
401
412
  scanSort: emptySpec_scanSort,
402
413
  status: undefined,
414
+ groupBy: undefined,
403
415
  visibility: undefined
404
416
  });
405
417
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -425,6 +437,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
425
437
  scan: ["status"],
426
438
  scanSort: emptySpec_scanSort,
427
439
  status: undefined,
440
+ groupBy: undefined,
428
441
  visibility: undefined
429
442
  });
430
443
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -457,6 +470,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
457
470
  scan: emptySpec_scan,
458
471
  scanSort: ["name"],
459
472
  status: undefined,
473
+ groupBy: undefined,
460
474
  visibility: undefined
461
475
  });
462
476
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -470,6 +484,39 @@ globalThis.describe("SuryToJsonSchema:", () => {
470
484
  undefined
471
485
  ]);
472
486
  });
487
+ globalThis.test("emits x-reventless-group-by on the field named by groupBy", () => {
488
+ let schema = S.schema(s => ({
489
+ id: s.m(S.string),
490
+ kind: s.m(S.string)
491
+ }));
492
+ let schema$p = withSpec(schema, {
493
+ ids: emptySpec_ids,
494
+ compositeIds: emptySpec_compositeIds,
495
+ subIds: emptySpec_subIds,
496
+ compositeSubIds: emptySpec_compositeSubIds,
497
+ indexes: emptySpec_indexes,
498
+ hidden: emptySpec_hidden,
499
+ summary: emptySpec_summary,
500
+ drillTargets: emptySpec_drillTargets,
501
+ drillTargetKeys: emptySpec_drillTargetKeys,
502
+ collapsed: emptySpec_collapsed,
503
+ scan: emptySpec_scan,
504
+ scanSort: emptySpec_scanSort,
505
+ status: undefined,
506
+ groupBy: "kind",
507
+ visibility: undefined
508
+ });
509
+ let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
510
+ let kindSchema = getPropertyOf(json, "kind");
511
+ let idSchema = getPropertyOf(json, "id");
512
+ globalThis.expect([
513
+ Stdlib_Option.flatMap(Stdlib_Option.flatMap(kindSchema, s => getProperty(s, "x-reventless-group-by")), Stdlib_JSON.Decode.bool),
514
+ Stdlib_Option.flatMap(idSchema, s => getProperty(s, "x-reventless-group-by"))
515
+ ]).toEqual([
516
+ true,
517
+ undefined
518
+ ]);
519
+ });
473
520
  globalThis.test("emits x-reventless-collapsed on field listed in collapsed", () => {
474
521
  let schema = S.schema(s => ({
475
522
  id: s.m(S.string),
@@ -489,6 +536,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
489
536
  scan: emptySpec_scan,
490
537
  scanSort: emptySpec_scanSort,
491
538
  status: undefined,
539
+ groupBy: undefined,
492
540
  visibility: undefined
493
541
  });
494
542
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -520,6 +568,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
520
568
  scan: emptySpec_scan,
521
569
  scanSort: emptySpec_scanSort,
522
570
  status: undefined,
571
+ groupBy: undefined,
523
572
  visibility: "Internal"
524
573
  });
525
574
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -543,6 +592,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
543
592
  scan: emptySpec_scan,
544
593
  scanSort: emptySpec_scanSort,
545
594
  status: undefined,
595
+ groupBy: undefined,
546
596
  visibility: undefined
547
597
  });
548
598
  let json = SuryToJsonSchema$ReventlessCore.deriveObjectSchema(schema$p);
@@ -606,6 +656,7 @@ globalThis.describe("SuryToJsonSchema:", () => {
606
656
  scan: emptySpec_scan,
607
657
  scanSort: emptySpec_scanSort,
608
658
  status: undefined,
659
+ groupBy: undefined,
609
660
  visibility: undefined
610
661
  });
611
662
  let native = S.toJSONSchema(schema$p);
@@ -65,6 +65,7 @@ let stateSchema$1 = S.Metadata.set(stateSchema, StateAnnotations$Reventless.stat
65
65
  scan: [],
66
66
  scanSort: [],
67
67
  status: undefined,
68
+ groupBy: undefined,
68
69
  visibility: undefined
69
70
  });
70
71