@reventlessdev/reventless-core 3.0.0-alpha.152 → 3.0.0-alpha.153

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,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.153 (2026-07-10)
7
+
8
+ ### Features
9
+
10
+ * **plugin-structure:** capture per-component chapter grouping for the deployed graph ([f9c88a9](https://github.com/ReventlessDev/reventless-core/commit/f9c88a9a48d8c032ffe23f9e5277caf12c29e85c))
11
+ * **plugin-structure:** carry translation-slice externalSystem into the deployed structure ([80c0cda](https://github.com/ReventlessDev/reventless-core/commit/80c0cda3b6d5fc6625d4b285f41505028f1dd06f))
12
+
13
+
6
14
  # 3.0.0-alpha.152 (2026-07-10)
7
15
 
8
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.152",
3
+ "version": "3.0.0-alpha.153",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -31,14 +31,14 @@
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.25",
32
32
  "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
33
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
34
+ "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
34
35
  "@reventlessdev/rescript-jest": "1.0.0-alpha.6",
35
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.14",
36
- "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
37
37
  "@reventlessdev/rescript-ssh2": "1.1.0-alpha.11",
38
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.93",
40
- "@reventlessdev/reventless-spec": "3.0.0-alpha.71",
41
- "@reventlessdev/reventless-interop": "3.0.0-alpha.24"
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.94",
40
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
41
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.72"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "^12.3.0",
@@ -96,6 +96,7 @@ let pluginAggregate: writableDef = {
96
96
  linkedViews: ["Plugins"],
97
97
  consistencyRead: None,
98
98
  events: [],
99
+ chapter: None,
99
100
  }
100
101
 
101
102
  let pluginReadModel: queryableDef = {
@@ -110,6 +111,7 @@ let pluginReadModel: queryableDef = {
110
111
  searchableFields: ["name"],
111
112
  statusField: Some("status"),
112
113
  visibility: None,
114
+ chapter: None,
113
115
  }
114
116
 
115
117
  let structure: pluginStructure = {
@@ -110,7 +110,8 @@ let pluginAggregate = {
110
110
  consumedEventTypes: pluginAggregate_consumedEventTypes,
111
111
  linkedViews: pluginAggregate_linkedViews,
112
112
  consistencyRead: undefined,
113
- events: pluginAggregate_events
113
+ events: pluginAggregate_events,
114
+ chapter: undefined
114
115
  };
115
116
 
116
117
  let pluginReadModel_queryField = Api_Naming$ReventlessCore.adminField("Plugins");
@@ -134,7 +135,8 @@ let pluginReadModel = {
134
135
  labelField: "name",
135
136
  searchableFields: pluginReadModel_searchableFields,
136
137
  statusField: pluginReadModel_statusField,
137
- visibility: undefined
138
+ visibility: undefined,
139
+ chapter: undefined
138
140
  };
139
141
 
140
142
  let structure_readModels = [pluginReadModel];
@@ -72,6 +72,7 @@ let encodeQueryableDef = (r: queryableDef): JSON.t =>
72
72
  ("labelField", JSON.Encode.string(r.labelField)),
73
73
  ("searchableFields", encodeStrings(r.searchableFields)),
74
74
  ("statusField", r.statusField->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
75
+ ("chapter", r.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
75
76
  ])->JSON.Encode.object
76
77
 
77
78
  let encodeEventDef = (e: eventDef): JSON.t =>
@@ -94,6 +95,7 @@ let encodeWritableDef = (w: writableDef): JSON.t =>
94
95
  ("consumedEventTypes", encodeStrings(w.consumedEventTypes)),
95
96
  // Phase 6.3: emitted-event field schemas (None → [] on the wire).
96
97
  ("events", w.events->Array.map(encodeEventDef)->JSON.Encode.array),
98
+ ("chapter", w.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
97
99
  ])->JSON.Encode.object
98
100
 
99
101
  let encodeAutomationSliceDef = (a: automationSliceDef): JSON.t =>
@@ -101,6 +103,7 @@ let encodeAutomationSliceDef = (a: automationSliceDef): JSON.t =>
101
103
  ("name", JSON.Encode.string(a.name)),
102
104
  ("consumedEventTypes", encodeStrings(a.consumedEventTypes)),
103
105
  ("producedCommandTypes", encodeStrings(a.producedCommandTypes)),
106
+ ("chapter", a.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
104
107
  ])->JSON.Encode.object
105
108
 
106
109
  let encodeOutboundTranslationSliceDef = (o: outboundTranslationSliceDef): JSON.t =>
@@ -108,12 +111,20 @@ let encodeOutboundTranslationSliceDef = (o: outboundTranslationSliceDef): JSON.t
108
111
  ("name", JSON.Encode.string(o.name)),
109
112
  ("consumedEventTypes", encodeStrings(o.consumedEventTypes)),
110
113
  ("inboundCommandTypes", encodeStrings(o.inboundCommandTypes)),
114
+ // Foreign system this slice publishes to — lets a deployed-graph consumer draw
115
+ // the external-system boundary box without workspace access. None → null.
116
+ ("externalSystem", o.externalSystem->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
117
+ ("chapter", o.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
111
118
  ])->JSON.Encode.object
112
119
 
113
120
  let encodeInboundTranslationSliceDef = (i: inboundTranslationSliceDef): JSON.t =>
114
121
  Dict.fromArray([
115
122
  ("name", JSON.Encode.string(i.name)),
116
123
  ("commandTypes", encodeStrings(i.commandTypes)),
124
+ // Foreign system this slice receives from — lets a deployed-graph consumer draw
125
+ // the external-system boundary box without workspace access. None → null.
126
+ ("externalSystem", i.externalSystem->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
127
+ ("chapter", i.chapter->Option.mapOr(JSON.Encode.null, JSON.Encode.string)),
117
128
  ])->JSON.Encode.object
118
129
 
119
130
  let encodeExtensionDef = (e: extensionDef): JSON.t =>
@@ -115,6 +115,10 @@ function encodeQueryableDef(r) {
115
115
  [
116
116
  "statusField",
117
117
  Stdlib_Option.mapOr(r.statusField, null, prim => prim)
118
+ ],
119
+ [
120
+ "chapter",
121
+ Stdlib_Option.mapOr(r.chapter, null, prim => prim)
118
122
  ]
119
123
  ]);
120
124
  }
@@ -165,6 +169,10 @@ function encodeWritableDef(w) {
165
169
  [
166
170
  "events",
167
171
  w.events.map(encodeEventDef)
172
+ ],
173
+ [
174
+ "chapter",
175
+ Stdlib_Option.mapOr(w.chapter, null, prim => prim)
168
176
  ]
169
177
  ]);
170
178
  }
@@ -182,6 +190,10 @@ function encodeAutomationSliceDef(a) {
182
190
  [
183
191
  "producedCommandTypes",
184
192
  a.producedCommandTypes.map(prim => prim)
193
+ ],
194
+ [
195
+ "chapter",
196
+ Stdlib_Option.mapOr(a.chapter, null, prim => prim)
185
197
  ]
186
198
  ]);
187
199
  }
@@ -199,6 +211,14 @@ function encodeOutboundTranslationSliceDef(o) {
199
211
  [
200
212
  "inboundCommandTypes",
201
213
  o.inboundCommandTypes.map(prim => prim)
214
+ ],
215
+ [
216
+ "externalSystem",
217
+ Stdlib_Option.mapOr(o.externalSystem, null, prim => prim)
218
+ ],
219
+ [
220
+ "chapter",
221
+ Stdlib_Option.mapOr(o.chapter, null, prim => prim)
202
222
  ]
203
223
  ]);
204
224
  }
@@ -212,6 +232,14 @@ function encodeInboundTranslationSliceDef(i) {
212
232
  [
213
233
  "commandTypes",
214
234
  i.commandTypes.map(prim => prim)
235
+ ],
236
+ [
237
+ "externalSystem",
238
+ Stdlib_Option.mapOr(i.externalSystem, null, prim => prim)
239
+ ],
240
+ [
241
+ "chapter",
242
+ Stdlib_Option.mapOr(i.chapter, null, prim => prim)
215
243
  ]
216
244
  ]);
217
245
  }
@@ -46,6 +46,7 @@ module type T = {
46
46
  ~inboundTranslationSlices: array<module(ReventlessInfra.InboundTranslationSlice.T)>=?,
47
47
  ~extensions: array<module(ReventlessInfra.Extension.Blueprint)>=?,
48
48
  ~extensionPoints: array<module(ReventlessInfra.ExtensionPointMapping.Mapping)>=?,
49
+ ~componentChapters: dict<string>=?,
49
50
  ) => Reventless.Plugin.pluginStructure
50
51
  }
51
52
 
@@ -368,6 +368,30 @@ module Make = (
368
368
  // so only aggregates, read models, and DCB slice names are included.
369
369
 
370
370
  let extractTypes = schema => Reventless.DcbTag.extractAllVariantNames(schema)
371
+
372
+ // Chapter (intra-plugin grouping band) per component name, mirrored from the
373
+ // static pluginStructure so the hook surface reaches parity with it. The
374
+ // structure is the single source — it was populated by the generator's
375
+ // componentChapters map at makePluginDefinition time.
376
+ let chapterByName: Dict.t<string> = Dict.make()
377
+ switch pluginStructure {
378
+ | Some(ps) =>
379
+ let put = (n, c) =>
380
+ switch c {
381
+ | Some(ch) => chapterByName->Dict.set(n, ch)
382
+ | None => ()
383
+ }
384
+ ps.aggregates->Array.forEach(d => put(d.name, d.chapter))
385
+ ps.readModels->Array.forEach(d => put(d.name, d.chapter))
386
+ ps.stateChangeSlices->Array.forEach(d => put(d.name, d.chapter))
387
+ ps.stateViewSlices->Array.forEach(d => put(d.name, d.chapter))
388
+ ps.automationSlices->Array.forEach(d => put(d.name, d.chapter))
389
+ ps.outboundTranslationSlices->Array.forEach(d => put(d.name, d.chapter))
390
+ ps.inboundTranslationSlices->Array.forEach(d => put(d.name, d.chapter))
391
+ | None => ()
392
+ }
393
+ let chapterFor = (n: string): option<string> => chapterByName->Dict.get(n)
394
+
371
395
  // Build per-component schema data and register it for the deployed hook.
372
396
  let aggregateComponents = aggregates->Array.map((
373
397
  module(M: ReventlessInfra.Aggregate.T with type api = api),
@@ -382,6 +406,7 @@ module Make = (
382
406
  eventSchemas: [
383
407
  SchemaWalker.walk(M.Spec.name ++ ".event", M.Spec.eventSchema),
384
408
  ],
409
+ chapter: ?chapterFor(M.Spec.name),
385
410
  }
386
411
  Plugin_Helpers.componentSchemaRegistry->Dict.set(M.Spec.name, schema)
387
412
  ({name: M.Spec.name, kind: "Aggregate", schema}: Plugin_Helpers.pluginBuiltComponent)
@@ -393,6 +418,7 @@ module Make = (
393
418
  let qn = Api_Naming.queryFieldNamesForReadModel(~plugin=name, ~name=R.Spec.name)
394
419
  let schema: Plugin_Helpers.pluginDeployedSchema = {
395
420
  queryFields: [qn.singleFieldName, qn.listFieldName],
421
+ chapter: ?chapterFor(R.Spec.name),
396
422
  }
397
423
  Plugin_Helpers.componentSchemaRegistry->Dict.set(R.Spec.name, schema)
398
424
  ({name: R.Spec.name, kind: "ReadModel", schema}: Plugin_Helpers.pluginBuiltComponent)
@@ -421,6 +447,7 @@ module Make = (
421
447
  consumedEventTypes,
422
448
  producedCommandTypes: extractTypes(As.Spec.commandSchema),
423
449
  targetName: As.Spec.targetName,
450
+ chapter: ?chapterFor(As.Spec.name),
424
451
  },
425
452
  )
426
453
  })
@@ -434,6 +461,9 @@ module Make = (
434
461
  commandTypes: extractTypes(Ots.Spec.inboundCommandSchema),
435
462
  // Outbound slices may have no target (option); pass it through.
436
463
  targetName: ?Ots.Spec.targetName,
464
+ // Foreign system this slice publishes to (option); drives the external box.
465
+ externalSystem: ?Ots.Spec.externalSystem,
466
+ chapter: ?chapterFor(Ots.Spec.name),
437
467
  },
438
468
  )
439
469
  )
@@ -445,6 +475,9 @@ module Make = (
445
475
  {
446
476
  commandTypes: extractTypes(Its.Spec.commandSchema),
447
477
  targetName: Its.Spec.targetName,
478
+ // Foreign system this slice receives from (option); drives the external box.
479
+ externalSystem: ?Its.Spec.externalSystem,
480
+ chapter: ?chapterFor(Its.Spec.name),
448
481
  },
449
482
  )
450
483
  )
@@ -464,13 +497,17 @@ module Make = (
464
497
  commandTypes: extractTypes(Scs.Spec.commandSchema),
465
498
  eventTypes: extractEventTypes(Scs.Spec.eventSchema->S.castToUnknown),
466
499
  consumedEventTypes: extractEventTypes(Scs.Spec.consumedEventSchema->S.castToUnknown),
500
+ chapter: ?chapterFor(Scs.Spec.name),
467
501
  },
468
502
  )
469
503
  )
470
504
  stateViewSlices->Array.forEach((module(Svs: ReventlessInfra.StateViewSlice.T)) =>
471
505
  stateSchemas->Dict.set(
472
506
  Svs.Spec.name,
473
- {consumedEventTypes: extractEventTypes(Svs.Spec.consumedEventSchema->S.castToUnknown)},
507
+ {
508
+ consumedEventTypes: extractEventTypes(Svs.Spec.consumedEventSchema->S.castToUnknown),
509
+ chapter: ?chapterFor(Svs.Spec.name),
510
+ },
474
511
  )
475
512
  )
476
513
 
@@ -993,6 +1030,7 @@ module Make = (
993
1030
  ~inboundTranslationSlices: array<module(ReventlessInfra.InboundTranslationSlice.T)>=[],
994
1031
  ~extensions: array<module(ReventlessInfra.Extension.Blueprint)>=[],
995
1032
  ~extensionPoints: array<module(ReventlessInfra.ExtensionPointMapping.Mapping)>=[],
1033
+ ~componentChapters: dict<string>=Dict.make(),
996
1034
  ): Reventless.Plugin.pluginStructure =>
997
1035
  Plugin_Structure.make(
998
1036
  ~name,
@@ -1005,6 +1043,7 @@ module Make = (
1005
1043
  ~inboundTranslationSlices,
1006
1044
  ~extensions,
1007
1045
  ~extensionPoints,
1046
+ ~componentChapters,
1008
1047
  )
1009
1048
 
1010
1049
  let make = (
@@ -89,7 +89,7 @@ function Make(Spec) {
89
89
  pages: pages
90
90
  };
91
91
  };
92
- let makePluginDefinition = (name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt) => {
92
+ let makePluginDefinition = (name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) => {
93
93
  let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
94
94
  let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
95
95
  let stateViewSlices = stateViewSlicesOpt !== undefined ? stateViewSlicesOpt : [];
@@ -99,7 +99,8 @@ function Make(Spec) {
99
99
  let inboundTranslationSlices = inboundTranslationSlicesOpt !== undefined ? inboundTranslationSlicesOpt : [];
100
100
  let extensions = extensionsOpt !== undefined ? extensionsOpt : [];
101
101
  let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
102
- return Plugin_Structure$ReventlessCore.make(name, aggregates, readModels, stateViewSlices, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints);
102
+ let componentChapters = componentChaptersOpt !== undefined ? componentChaptersOpt : ({});
103
+ return Plugin_Structure$ReventlessCore.make(name, aggregates, readModels, stateViewSlices, stateChangeSlices, automationSlices, outboundTranslationSlices, inboundTranslationSlices, extensions, extensionPoints, componentChapters);
103
104
  };
104
105
  let make = (name, heartbeatInterval, extensionPointsOpt, extensionsOpt, aggregatesOpt, readModelsOpt, tasksOpt, stateChangeSlicesOpt, stateViewSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, systemCallableComponentsOpt, uiFragments, pluginStructure, opts) => {
105
106
  let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
@@ -264,18 +265,36 @@ function Make(Spec) {
264
265
  allQueryDbs[param[0] + "Todo"] = param[1].queryDb;
265
266
  });
266
267
  let queryEngine = QueryEngineAdapter.make(allQueryDbs);
268
+ let chapterByName = {};
269
+ if (pluginStructure !== undefined) {
270
+ let put = (n, c) => {
271
+ if (c !== undefined) {
272
+ chapterByName[n] = c;
273
+ return;
274
+ }
275
+ };
276
+ pluginStructure.aggregates.forEach(d => put(d.name, d.chapter));
277
+ pluginStructure.readModels.forEach(d => put(d.name, d.chapter));
278
+ pluginStructure.stateChangeSlices.forEach(d => put(d.name, d.chapter));
279
+ pluginStructure.stateViewSlices.forEach(d => put(d.name, d.chapter));
280
+ pluginStructure.automationSlices.forEach(d => put(d.name, d.chapter));
281
+ pluginStructure.outboundTranslationSlices.forEach(d => put(d.name, d.chapter));
282
+ pluginStructure.inboundTranslationSlices.forEach(d => put(d.name, d.chapter));
283
+ }
267
284
  let aggregateComponents = aggregates.map(M => {
268
285
  let schema_commandTypes = DcbTag$Reventless.extractAllVariantNames(M.Spec.commandSchema);
269
286
  let schema_eventTypes = DcbTag$Reventless.extractAllVariantNames(M.Spec.eventSchema);
270
287
  let schema_errorTypes = DcbTag$Reventless.extractAllVariantNames(M.Spec.errorSchema);
271
288
  let schema_commandSchemas = [SchemaWalker$ReventlessCore.walk(M.Spec.name + ".command", M.Spec.commandSchema)];
272
289
  let schema_eventSchemas = [SchemaWalker$ReventlessCore.walk(M.Spec.name + ".event", M.Spec.eventSchema)];
290
+ let schema_chapter = chapterByName[M.Spec.name];
273
291
  let schema = {
274
292
  commandTypes: schema_commandTypes,
275
293
  eventTypes: schema_eventTypes,
276
294
  errorTypes: schema_errorTypes,
277
295
  commandSchemas: schema_commandSchemas,
278
- eventSchemas: schema_eventSchemas
296
+ eventSchemas: schema_eventSchemas,
297
+ chapter: schema_chapter
279
298
  };
280
299
  Plugin_Helpers$ReventlessCore.componentSchemaRegistry[M.Spec.name] = schema;
281
300
  return {
@@ -290,8 +309,10 @@ function Make(Spec) {
290
309
  qn.singleFieldName,
291
310
  qn.listFieldName
292
311
  ];
312
+ let schema_chapter = chapterByName[R.Spec.name];
293
313
  let schema = {
294
- queryFields: schema_queryFields
314
+ queryFields: schema_queryFields,
315
+ chapter: schema_chapter
295
316
  };
296
317
  Plugin_Helpers$ReventlessCore.componentSchemaRegistry[R.Spec.name] = schema;
297
318
  return {
@@ -306,20 +327,25 @@ function Make(Spec) {
306
327
  routingSchemas[As.Spec.name] = {
307
328
  consumedEventTypes: consumedEventTypes,
308
329
  producedCommandTypes: DcbTag$Reventless.extractAllVariantNames(As.Spec.commandSchema),
309
- targetName: As.Spec.targetName
330
+ targetName: As.Spec.targetName,
331
+ chapter: chapterByName[As.Spec.name]
310
332
  };
311
333
  });
312
334
  outboundTranslationSlices.forEach(Ots => {
313
335
  routingSchemas[Ots.Spec.name] = {
314
336
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Ots.Spec.inboundCommandSchema),
315
337
  consumedEventTypes: DcbTag$Reventless.extractVariantNames(Ots.Spec.consumedEventSchema),
316
- targetName: Ots.Spec.targetName
338
+ targetName: Ots.Spec.targetName,
339
+ externalSystem: Ots.Spec.externalSystem,
340
+ chapter: chapterByName[Ots.Spec.name]
317
341
  };
318
342
  });
319
343
  inboundTranslationSlices.forEach(Its => {
320
344
  routingSchemas[Its.Spec.name] = {
321
345
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Its.Spec.commandSchema),
322
- targetName: Its.Spec.targetName
346
+ targetName: Its.Spec.targetName,
347
+ externalSystem: Its.Spec.externalSystem,
348
+ chapter: chapterByName[Its.Spec.name]
323
349
  };
324
350
  });
325
351
  let stateSchemas = {};
@@ -327,12 +353,14 @@ function Make(Spec) {
327
353
  stateSchemas[Scs.Spec.name] = {
328
354
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.commandSchema),
329
355
  eventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.eventSchema),
330
- consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema)
356
+ consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema),
357
+ chapter: chapterByName[Scs.Spec.name]
331
358
  };
332
359
  });
333
360
  stateViewSlices.forEach(Svs => {
334
361
  stateSchemas[Svs.Spec.name] = {
335
- consumedEventTypes: DcbTag$Reventless.extractVariantNames(Svs.Spec.consumedEventSchema)
362
+ consumedEventTypes: DcbTag$Reventless.extractVariantNames(Svs.Spec.consumedEventSchema),
363
+ chapter: chapterByName[Svs.Spec.name]
336
364
  };
337
365
  });
338
366
  let enrichedComponents = (d, kind, schemas) => Object.keys(d).map(name => {
@@ -78,6 +78,12 @@ type pluginDeployedSchema = {
78
78
  /** For routing slices (automation / inbound + outbound translation): the name
79
79
  of the writable component this slice routes its commands to. */
80
80
  targetName?: string,
81
+ /** For inbound / outbound translation slices: the foreign system this slice
82
+ integrates with, mirrored from the static `pluginStructure` for hook parity.
83
+ Drives the deployed graph's external-system boundary box. Absent when the slice
84
+ names no external system. See the translation-slice defs on
85
+ `Reventless.Plugin.pluginStructure`. */
86
+ externalSystem?: string,
81
87
  sharedBy?: array<string>,
82
88
  extensionPointName?: string,
83
89
  providerPlugin?: string,
@@ -88,6 +94,11 @@ type pluginDeployedSchema = {
88
94
  eventSchemas?: array<typeSchema>,
89
95
  /** Field-level schema for the state type. */
90
96
  stateSchema?: typeSchema,
97
+ /** Intra-plugin grouping band (the "chapter") this component belongs to, mirrored
98
+ from the static `pluginStructure` for parity on the hook surface. Absent when the
99
+ component carries no chapter (lives directly under a kind-folder). See
100
+ `Reventless.Plugin.queryableDef.chapter`. */
101
+ chapter?: string,
91
102
  }
92
103
 
93
104
  // ---------------------------------------------------------------------------
@@ -86,7 +86,15 @@ let make = (
86
86
  ~inboundTranslationSlices: array<module(ReventlessInfra.InboundTranslationSlice.T)>=[],
87
87
  ~extensions: array<module(ReventlessInfra.Extension.Blueprint)>=[],
88
88
  ~extensionPoints: array<module(ReventlessInfra.ExtensionPointMapping.Mapping)>=[],
89
+ // Component name → chapter (intra-plugin grouping band), captured at build time
90
+ // from each component's source folder by the plugin generator. A component whose
91
+ // spec name has no entry (or lives directly under a kind-folder) carries no chapter
92
+ // and renders flat. Keyed by `Spec.name`, which equals the source filename stem for
93
+ // every graph-node kind, so the generator can build this map from the discovered
94
+ // file paths. See `Codegen.chapterOf` and docs/plans/deployed-chapter-grouping.md.
95
+ ~componentChapters: dict<string>=Dict.make(),
89
96
  ): Reventless.Plugin.pluginStructure => {
97
+ let chapterOf = (compName: string): option<string> => componentChapters->Dict.get(compName)
90
98
  // Event schemas: filter out payload-less variants — DCB event-type lookups
91
99
  // can't WHERE-clause on bare-string events, so the plugin graph mustn't
92
100
  // claim cross-component edges that the runtime can't honour.
@@ -405,6 +413,7 @@ let make = (
405
413
  searchableFields,
406
414
  statusField: statusFieldFromStateSchema(stateSchema),
407
415
  visibility: visibilityTag(R.Spec.visibility),
416
+ chapter: chapterOf(R.Spec.name),
408
417
  }: Reventless.Plugin.queryableDef)
409
418
  })
410
419
 
@@ -427,6 +436,7 @@ let make = (
427
436
  searchableFields,
428
437
  statusField: statusFieldFromStateSchema(stateSchema),
429
438
  visibility: visibilityTag(SVS.Spec.visibility),
439
+ chapter: chapterOf(SVS.Spec.name),
430
440
  }: Reventless.Plugin.queryableDef)
431
441
  })
432
442
 
@@ -454,6 +464,7 @@ let make = (
454
464
  linkedViews: linkedSvsFor(produced),
455
465
  consistencyRead: consistencyReadFor(consumed),
456
466
  events: extractEventDefs(SCS.Spec.eventSchema->S.castToUnknown),
467
+ chapter: chapterOf(SCS.Spec.name),
457
468
  }: Reventless.Plugin.writableDef)
458
469
  })
459
470
 
@@ -475,6 +486,7 @@ let make = (
475
486
  linkedViews: Array.concat(linkedSvsFor(produced), linkedReadModelsFor(A.Spec.name)),
476
487
  consistencyRead: None,
477
488
  events: extractEventDefs(A.Spec.eventSchema->S.castToUnknown),
489
+ chapter: chapterOf(A.Spec.name),
478
490
  }: Reventless.Plugin.writableDef)
479
491
  })
480
492
 
@@ -497,6 +509,7 @@ let make = (
497
509
  consumedEventTypes: qualify(~prefix=name, allConsumedVariants),
498
510
  producedCommandTypes: qualify(~prefix=name, commandVariantNames(AS.Spec.commandSchema)),
499
511
  targetName: AS.Spec.targetName,
512
+ chapter: chapterOf(AS.Spec.name),
500
513
  }: Reventless.Plugin.automationSliceDef)
501
514
  })
502
515
 
@@ -509,6 +522,7 @@ let make = (
509
522
  inboundCommandTypes: qualify(~prefix=name, commandVariantNames(OTS.Spec.inboundCommandSchema)),
510
523
  targetName: OTS.Spec.targetName,
511
524
  externalSystem: OTS.Spec.externalSystem,
525
+ chapter: chapterOf(OTS.Spec.name),
512
526
  }: Reventless.Plugin.outboundTranslationSliceDef))
513
527
 
514
528
  // ── Inbound translation slices ────────────────────────────────────────────
@@ -519,6 +533,7 @@ let make = (
519
533
  commandTypes: qualify(~prefix=name, commandVariantNames(ITS.Spec.commandSchema)),
520
534
  targetName: ITS.Spec.targetName,
521
535
  externalSystem: ITS.Spec.externalSystem,
536
+ chapter: chapterOf(ITS.Spec.name),
522
537
  }: Reventless.Plugin.inboundTranslationSliceDef))
523
538
 
524
539
  // ── Extensions ───────────────────────────────────────────────────────────
@@ -67,7 +67,7 @@ function labelFieldsFromStateSchema(entityName, stateSchema) {
67
67
  }
68
68
  }
69
69
 
70
- function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt) {
70
+ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChangeSlicesOpt, automationSlicesOpt, outboundTranslationSlicesOpt, inboundTranslationSlicesOpt, extensionsOpt, extensionPointsOpt, componentChaptersOpt) {
71
71
  let aggregates = aggregatesOpt !== undefined ? aggregatesOpt : [];
72
72
  let readModels = readModelsOpt !== undefined ? readModelsOpt : [];
73
73
  let stateViewSlices = stateViewSlicesOpt !== undefined ? stateViewSlicesOpt : [];
@@ -77,6 +77,7 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
77
77
  let inboundTranslationSlices = inboundTranslationSlicesOpt !== undefined ? inboundTranslationSlicesOpt : [];
78
78
  let extensions = extensionsOpt !== undefined ? extensionsOpt : [];
79
79
  let extensionPoints = extensionPointsOpt !== undefined ? extensionPointsOpt : [];
80
+ let componentChapters = componentChaptersOpt !== undefined ? componentChaptersOpt : ({});
80
81
  let qualify = (prefix, names) => names.map(n => prefix + "." + n);
81
82
  let isCreateCommandName = name => [
82
83
  "Add",
@@ -333,7 +334,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
333
334
  labelField: match[0],
334
335
  searchableFields: match[1],
335
336
  statusField: statusFieldFromStateSchema(stateSchema),
336
- visibility: visibilityTag(R.Spec.visibility)
337
+ visibility: visibilityTag(R.Spec.visibility),
338
+ chapter: componentChapters[R.Spec.name]
337
339
  };
338
340
  });
339
341
  let stateViewDefs = stateViewSlices.map((SVS, i) => {
@@ -351,7 +353,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
351
353
  labelField: match$1[0],
352
354
  searchableFields: match$1[1],
353
355
  statusField: statusFieldFromStateSchema(stateSchema),
354
- visibility: visibilityTag(SVS.Spec.visibility)
356
+ visibility: visibilityTag(SVS.Spec.visibility),
357
+ chapter: componentChapters[SVS.Spec.name]
355
358
  };
356
359
  });
357
360
  let stateChangeDefs = stateChangeSlices.map((SCS, i) => {
@@ -366,7 +369,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
366
369
  consumedEventTypes: consumed,
367
370
  linkedViews: linkedSvsFor(produced),
368
371
  consistencyRead: consistencyReadFor(consumed),
369
- events: extractEventDefs(SCS.Spec.eventSchema)
372
+ events: extractEventDefs(SCS.Spec.eventSchema),
373
+ chapter: componentChapters[SCS.Spec.name]
370
374
  };
371
375
  });
372
376
  let aggregateDefs = aggregates.map((A, i) => {
@@ -379,7 +383,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
379
383
  consumedEventTypes: [],
380
384
  linkedViews: linkedSvsFor(produced).concat(linkedReadModelsFor(A.Spec.name)),
381
385
  consistencyRead: undefined,
382
- events: extractEventDefs(A.Spec.eventSchema)
386
+ events: extractEventDefs(A.Spec.eventSchema),
387
+ chapter: componentChapters[A.Spec.name]
383
388
  };
384
389
  });
385
390
  let automationSliceDefs = automationSlices.map(AS => {
@@ -388,7 +393,8 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
388
393
  name: AS.Spec.name,
389
394
  consumedEventTypes: qualify(name, allConsumedVariants),
390
395
  producedCommandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(AS.Spec.commandSchema)),
391
- targetName: AS.Spec.targetName
396
+ targetName: AS.Spec.targetName,
397
+ chapter: componentChapters[AS.Spec.name]
392
398
  };
393
399
  });
394
400
  let outboundTranslationSliceDefs = outboundTranslationSlices.map(OTS => ({
@@ -396,13 +402,15 @@ function make(name, aggregatesOpt, readModelsOpt, stateViewSlicesOpt, stateChang
396
402
  consumedEventTypes: qualify(name, DcbTag$Reventless.extractVariantNames(OTS.Spec.consumedEventSchema)),
397
403
  inboundCommandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(OTS.Spec.inboundCommandSchema)),
398
404
  targetName: OTS.Spec.targetName,
399
- externalSystem: OTS.Spec.externalSystem
405
+ externalSystem: OTS.Spec.externalSystem,
406
+ chapter: componentChapters[OTS.Spec.name]
400
407
  }));
401
408
  let inboundTranslationSliceDefs = inboundTranslationSlices.map(ITS => ({
402
409
  name: ITS.Spec.name,
403
410
  commandTypes: qualify(name, DcbTag$Reventless.extractAllVariantNames(ITS.Spec.commandSchema)),
404
411
  targetName: ITS.Spec.targetName,
405
- externalSystem: ITS.Spec.externalSystem
412
+ externalSystem: ITS.Spec.externalSystem,
413
+ chapter: componentChapters[ITS.Spec.name]
406
414
  }));
407
415
  let extensionDefs = extensions.map(E => {
408
416
  let delegateNames = E.mappings.map(M => M.delegateName);
@@ -28,6 +28,7 @@ let qbl: queryableDef = {
28
28
  searchableFields: ["displayName"],
29
29
  statusField: None,
30
30
  visibility: None,
31
+ chapter: None,
31
32
  }
32
33
 
33
34
  let wbl: writableDef = {
@@ -38,6 +39,7 @@ let wbl: writableDef = {
38
39
  producedEventTypes: ["ProductAdded"],
39
40
  consumedEventTypes: [],
40
41
  events: [{name: "ProductAdded", schema: "{}", references: []}],
42
+ chapter: None,
41
43
  }
42
44
 
43
45
  let structure: pluginStructure = {
@@ -51,6 +53,7 @@ let structure: pluginStructure = {
51
53
  consumedEventTypes: ["StockLow"],
52
54
  producedCommandTypes: ["Restock"],
53
55
  targetName: "Product",
56
+ chapter: None,
54
57
  },
55
58
  ],
56
59
  outboundTranslationSlices: [
@@ -60,10 +63,17 @@ let structure: pluginStructure = {
60
63
  inboundCommandTypes: ["Ship"],
61
64
  targetName: None,
62
65
  externalSystem: None,
66
+ chapter: None,
63
67
  },
64
68
  ],
65
69
  inboundTranslationSlices: [
66
- {name: "FromBilling", commandTypes: ["RecordPayment"], targetName: "Order", externalSystem: None},
70
+ {
71
+ name: "FromBilling",
72
+ commandTypes: ["RecordPayment"],
73
+ targetName: "Order",
74
+ externalSystem: None,
75
+ chapter: None,
76
+ },
67
77
  ],
68
78
  extensions: [
69
79
  {
@@ -143,6 +153,61 @@ describe("encodePluginStructureEntry", () => {
143
153
  testSync("encodes writableDef events", () =>
144
154
  expect(json->String.includes("\"events\":[{\"name\":\"ProductAdded\""))->toEqual(true)
145
155
  )
156
+
157
+ // Translation-slice externalSystem: absent on the fixture above (both None), so it
158
+ // must serialize as null — the "no external box" case.
159
+ testSync("encodes None outbound externalSystem as null", () =>
160
+ expect(json->String.includes("\"inboundCommandTypes\":[\"Ship\"],\"externalSystem\":null"))->toEqual(
161
+ true,
162
+ )
163
+ )
164
+
165
+ testSync("encodes None inbound externalSystem as null", () =>
166
+ expect(json->String.includes("\"commandTypes\":[\"RecordPayment\"],\"externalSystem\":null"))->toEqual(
167
+ true,
168
+ )
169
+ )
170
+ })
171
+
172
+ // A deployed-graph consumer draws the external-system boundary box off this field, so
173
+ // a slice whose spec sets `externalSystem = Some("X")` must surface "X" on the wire for
174
+ // both directions — the round-trip the plan pins.
175
+ describe("translation-slice externalSystem round-trip", () => {
176
+ let externalStructure: pluginStructure = {
177
+ ...structure,
178
+ outboundTranslationSlices: [
179
+ {
180
+ name: "ToShipper",
181
+ consumedEventTypes: ["OrderPlaced"],
182
+ inboundCommandTypes: ["Ship"],
183
+ targetName: None,
184
+ externalSystem: Some("ShipperGateway"),
185
+ chapter: None,
186
+ },
187
+ ],
188
+ inboundTranslationSlices: [
189
+ {
190
+ name: "FromBilling",
191
+ commandTypes: ["RecordPayment"],
192
+ targetName: "Order",
193
+ externalSystem: Some("BillingProvider"),
194
+ chapter: None,
195
+ },
196
+ ],
197
+ }
198
+ let externalJson =
199
+ Platform_ComponentDefinitionsApi.encodePluginStructureEntry(
200
+ ~pluginId="Catalog",
201
+ externalStructure,
202
+ )->JSON.stringify
203
+
204
+ testSync("surfaces Some outbound externalSystem as its inner string", () =>
205
+ expect(externalJson->String.includes("\"externalSystem\":\"ShipperGateway\""))->toEqual(true)
206
+ )
207
+
208
+ testSync("surfaces Some inbound externalSystem as its inner string", () =>
209
+ expect(externalJson->String.includes("\"externalSystem\":\"BillingProvider\""))->toEqual(true)
210
+ )
146
211
  })
147
212
 
148
213
  describe("visibility filtering (deployed AutoUI hides Internal)", () => {
@@ -159,6 +224,7 @@ describe("visibility filtering (deployed AutoUI hides Internal)", () => {
159
224
  searchableFields: ["name"],
160
225
  statusField: None,
161
226
  visibility: Some("Internal"),
227
+ chapter: None,
162
228
  }
163
229
  let mixed: pluginStructure = {
164
230
  ...structure,
@@ -199,6 +265,7 @@ describe("allowedStates + statusField populated", () => {
199
265
  searchableFields: ["name"],
200
266
  statusField: Some("status"),
201
267
  visibility: None,
268
+ chapter: None,
202
269
  }
203
270
 
204
271
  let wblWithStates: writableDef = {
@@ -209,6 +276,7 @@ describe("allowedStates + statusField populated", () => {
209
276
  producedEventTypes: [],
210
277
  consumedEventTypes: [],
211
278
  events: [],
279
+ chapter: None,
212
280
  }
213
281
 
214
282
  let structureWithStates: pluginStructure = {
@@ -39,7 +39,8 @@ let qbl = {
39
39
  labelField: "displayName",
40
40
  searchableFields: qbl_searchableFields,
41
41
  statusField: undefined,
42
- visibility: undefined
42
+ visibility: undefined,
43
+ chapter: undefined
43
44
  };
44
45
 
45
46
  let wbl_commands = [cmd];
@@ -63,7 +64,8 @@ let wbl = {
63
64
  consumedEventTypes: wbl_consumedEventTypes,
64
65
  linkedViews: wbl_linkedViews,
65
66
  consistencyRead: undefined,
66
- events: wbl_events
67
+ events: wbl_events,
68
+ chapter: undefined
67
69
  };
68
70
 
69
71
  let structure_readModels = [qbl];
@@ -78,7 +80,8 @@ let structure_automationSlices = [{
78
80
  name: "Restocker",
79
81
  consumedEventTypes: ["StockLow"],
80
82
  producedCommandTypes: ["Restock"],
81
- targetName: "Product"
83
+ targetName: "Product",
84
+ chapter: undefined
82
85
  }];
83
86
 
84
87
  let structure_outboundTranslationSlices = [{
@@ -86,14 +89,16 @@ let structure_outboundTranslationSlices = [{
86
89
  consumedEventTypes: ["OrderPlaced"],
87
90
  inboundCommandTypes: ["Ship"],
88
91
  targetName: undefined,
89
- externalSystem: undefined
92
+ externalSystem: undefined,
93
+ chapter: undefined
90
94
  }];
91
95
 
92
96
  let structure_inboundTranslationSlices = [{
93
97
  name: "FromBilling",
94
98
  commandTypes: ["RecordPayment"],
95
99
  targetName: "Order",
96
- externalSystem: undefined
100
+ externalSystem: undefined,
101
+ chapter: undefined
97
102
  }];
98
103
 
99
104
  let structure_extensions = [{
@@ -168,6 +173,54 @@ globalThis.describe("encodePluginStructureEntry", () => {
168
173
  globalThis.test("encodes writableDef events", () => {
169
174
  globalThis.expect(json.includes("\"events\":[{\"name\":\"ProductAdded\"")).toEqual(true);
170
175
  });
176
+ globalThis.test("encodes None outbound externalSystem as null", () => {
177
+ globalThis.expect(json.includes("\"inboundCommandTypes\":[\"Ship\"],\"externalSystem\":null")).toEqual(true);
178
+ });
179
+ globalThis.test("encodes None inbound externalSystem as null", () => {
180
+ globalThis.expect(json.includes("\"commandTypes\":[\"RecordPayment\"],\"externalSystem\":null")).toEqual(true);
181
+ });
182
+ });
183
+
184
+ globalThis.describe("translation-slice externalSystem round-trip", () => {
185
+ let externalStructure_readModels = structure_readModels;
186
+ let externalStructure_stateViewSlices = structure_stateViewSlices;
187
+ let externalStructure_stateChangeSlices = structure_stateChangeSlices;
188
+ let externalStructure_aggregates = structure_aggregates;
189
+ let externalStructure_automationSlices = structure_automationSlices;
190
+ let externalStructure_outboundTranslationSlices = [{
191
+ name: "ToShipper",
192
+ consumedEventTypes: ["OrderPlaced"],
193
+ inboundCommandTypes: ["Ship"],
194
+ targetName: undefined,
195
+ externalSystem: "ShipperGateway",
196
+ chapter: undefined
197
+ }];
198
+ let externalStructure_inboundTranslationSlices = [{
199
+ name: "FromBilling",
200
+ commandTypes: ["RecordPayment"],
201
+ targetName: "Order",
202
+ externalSystem: "BillingProvider",
203
+ chapter: undefined
204
+ }];
205
+ let externalStructure_extensions = structure_extensions;
206
+ let externalStructure = {
207
+ readModels: externalStructure_readModels,
208
+ stateViewSlices: externalStructure_stateViewSlices,
209
+ stateChangeSlices: externalStructure_stateChangeSlices,
210
+ aggregates: externalStructure_aggregates,
211
+ automationSlices: externalStructure_automationSlices,
212
+ outboundTranslationSlices: externalStructure_outboundTranslationSlices,
213
+ inboundTranslationSlices: externalStructure_inboundTranslationSlices,
214
+ extensions: externalStructure_extensions,
215
+ extensionPoints: undefined
216
+ };
217
+ let externalJson = JSON.stringify(Platform_ComponentDefinitionsApi$ReventlessCore.encodePluginStructureEntry("Catalog", externalStructure));
218
+ globalThis.test("surfaces Some outbound externalSystem as its inner string", () => {
219
+ globalThis.expect(externalJson.includes("\"externalSystem\":\"ShipperGateway\"")).toEqual(true);
220
+ });
221
+ globalThis.test("surfaces Some inbound externalSystem as its inner string", () => {
222
+ globalThis.expect(externalJson.includes("\"externalSystem\":\"BillingProvider\"")).toEqual(true);
223
+ });
171
224
  });
172
225
 
173
226
  globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", () => {
@@ -184,7 +237,8 @@ globalThis.describe("visibility filtering (deployed AutoUI hides Internal)", ()
184
237
  labelField: "name",
185
238
  searchableFields: internalQbl_searchableFields,
186
239
  statusField: undefined,
187
- visibility: internalQbl_visibility
240
+ visibility: internalQbl_visibility,
241
+ chapter: undefined
188
242
  };
189
243
  let mixed_readModels = [
190
244
  qbl,
@@ -245,7 +299,8 @@ globalThis.describe("allowedStates + statusField populated", () => {
245
299
  labelField: "name",
246
300
  searchableFields: qblWithStatus_searchableFields,
247
301
  statusField: qblWithStatus_statusField,
248
- visibility: undefined
302
+ visibility: undefined,
303
+ chapter: undefined
249
304
  };
250
305
  let wblWithStates_commands = [cmdWithStates];
251
306
  let wblWithStates_producedEventTypes = [];
@@ -259,7 +314,8 @@ globalThis.describe("allowedStates + statusField populated", () => {
259
314
  consumedEventTypes: wblWithStates_consumedEventTypes,
260
315
  linkedViews: wblWithStates_linkedViews,
261
316
  consistencyRead: undefined,
262
- events: wblWithStates_events
317
+ events: wblWithStates_events,
318
+ chapter: undefined
263
319
  };
264
320
  let structureWithStates_readModels = [qblWithStatus];
265
321
  let structureWithStates_stateViewSlices = [];
@@ -177,7 +177,7 @@ globalThis.describe("ReadModel visibility filter", () => {
177
177
  });
178
178
 
179
179
  globalThis.describe("Plugin_Structure.make — visibility tagging", () => {
180
- let structure = Plugin_Structure$ReventlessCore.make("VisibilityPlugin", undefined, readModels, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
180
+ let structure = Plugin_Structure$ReventlessCore.make("VisibilityPlugin", undefined, readModels, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined);
181
181
  globalThis.test("readModels carries both Public and Internal entries", () => {
182
182
  globalThis.expect(structure.readModels.length).toBe(2);
183
183
  });
@@ -295,4 +295,38 @@ describe("Plugin_Structure.make — Phase 2 graph fields", () => {
295
295
  )->toBe(None)
296
296
  })
297
297
  })
298
+
299
+ // The generator captures each component's chapter (source-folder grouping band)
300
+ // and passes it as ~componentChapters; Plugin_Structure looks it up by Spec.name
301
+ // and threads it onto every def, so a deployed-graph consumer can render chapter
302
+ // bands without workspace access. See docs/plans/deployed-chapter-grouping.md.
303
+ describe("componentChapters threading", () => {
304
+ let chaptered = Plugin_Structure.make(
305
+ ~name="TestPlugin",
306
+ ~stateChangeSlices=[module(PsPlaceOrderSlice), module(PsShipOrderSlice)],
307
+ ~stateViewSlices=[module(PsOrdersViewSlice)],
308
+ // PlaceOrder + Orders live under a chapter; ShipOrder does not.
309
+ ~componentChapters=Dict.fromArray([("PlaceOrder", "Order"), ("Orders", "Order")]),
310
+ )
311
+
312
+ testSync("a chaptered state-change slice carries Some(chapter)", () => {
313
+ let placeOrder = chaptered.stateChangeSlices->Array.getUnsafe(0)
314
+ expect(placeOrder.chapter)->toEqual(Some("Order"))
315
+ })
316
+
317
+ testSync("a chaptered state-view slice carries Some(chapter)", () => {
318
+ let orders = chaptered.stateViewSlices->Array.getUnsafe(0)
319
+ expect(orders.chapter)->toEqual(Some("Order"))
320
+ })
321
+
322
+ testSync("a component absent from the map carries None", () => {
323
+ let shipOrder = chaptered.stateChangeSlices->Array.getUnsafe(1)
324
+ expect(shipOrder.chapter)->toEqual(None)
325
+ })
326
+
327
+ testSync("no ~componentChapters → every def is chapterless (None)", () => {
328
+ let placeOrder = structure.stateChangeSlices->Array.getUnsafe(0)
329
+ expect(placeOrder.chapter)->toEqual(None)
330
+ })
331
+ })
298
332
  })
@@ -183,7 +183,7 @@ let structure = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, un
183
183
  ], [
184
184
  PsPlaceOrderSlice,
185
185
  PsShipOrderSlice
186
- ], undefined, undefined, undefined, undefined, undefined);
186
+ ], undefined, undefined, undefined, undefined, undefined, undefined);
187
187
 
188
188
  globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
189
189
  globalThis.describe("stateChangeSlices", () => {
@@ -374,6 +374,37 @@ globalThis.describe("Plugin_Structure.make — Phase 2 graph fields", () => {
374
374
  globalThis.expect(Stdlib_Option.flatMap(orderIdField, s => getProperty(s, "x-reventless-id"))).toBe(undefined);
375
375
  });
376
376
  });
377
+ globalThis.describe("componentChapters threading", () => {
378
+ let chaptered = Plugin_Structure$ReventlessCore.make("TestPlugin", undefined, undefined, [PsOrdersViewSlice], [
379
+ PsPlaceOrderSlice,
380
+ PsShipOrderSlice
381
+ ], undefined, undefined, undefined, undefined, undefined, Object.fromEntries([
382
+ [
383
+ "PlaceOrder",
384
+ "Order"
385
+ ],
386
+ [
387
+ "Orders",
388
+ "Order"
389
+ ]
390
+ ]));
391
+ globalThis.test("a chaptered state-change slice carries Some(chapter)", () => {
392
+ let placeOrder = chaptered.stateChangeSlices[0];
393
+ globalThis.expect(placeOrder.chapter).toEqual("Order");
394
+ });
395
+ globalThis.test("a chaptered state-view slice carries Some(chapter)", () => {
396
+ let orders = chaptered.stateViewSlices[0];
397
+ globalThis.expect(orders.chapter).toEqual("Order");
398
+ });
399
+ globalThis.test("a component absent from the map carries None", () => {
400
+ let shipOrder = chaptered.stateChangeSlices[1];
401
+ globalThis.expect(shipOrder.chapter).toEqual(undefined);
402
+ });
403
+ globalThis.test("no ~componentChapters → every def is chapterless (None)", () => {
404
+ let placeOrder = structure.stateChangeSlices[0];
405
+ globalThis.expect(placeOrder.chapter).toEqual(undefined);
406
+ });
407
+ });
377
408
  });
378
409
 
379
410
  export {