@reventlessdev/reventless-core 3.0.0-alpha.151 → 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,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.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
+
14
+ # 3.0.0-alpha.152 (2026-07-10)
15
+
16
+ ### Features
17
+
18
+ * **plugin-lifecycle:** expose command/event/consumed-event metadata on plugin hooks for StateChange/StateView slices ([194abf3](https://github.com/ReventlessDev/reventless-core/commit/194abf30460ae0140a7900d8ca67d671b15deb3a))
19
+
20
+
6
21
  # 3.0.0-alpha.151 (2026-07-10)
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.151",
3
+ "version": "3.0.0-alpha.153",
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.25",
32
+ "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
32
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.10",
33
34
  "@reventlessdev/rescript-node-streams": "1.1.0-alpha.10",
34
- "@reventlessdev/rescript-fast-csv": "1.2.0-alpha.11",
35
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
38
  "@reventlessdev/rescript-uuid": "1.1.0-alpha.14",
39
- "@reventlessdev/reventless-infra": "3.0.0-alpha.93",
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.94",
40
40
  "@reventlessdev/reventless-interop": "3.0.0-alpha.24",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.71"
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,19 +418,12 @@ 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)
399
425
  })
400
426
 
401
- let mapNames = (d: dict<_>, kind: string) =>
402
- d
403
- ->Dict.keysToArray
404
- ->Array.map(name => {
405
- let schema: Plugin_Helpers.pluginDeployedSchema = {}
406
- ({Plugin_Helpers.name, kind, schema}: Plugin_Helpers.pluginBuiltComponent)
407
- })
408
-
409
427
  // Routing slices (automation / inbound + outbound translation) expose their
410
428
  // command/event wiring and routing target on the hook surface, at parity
411
429
  // with aggregates and read models — mirroring the metadata Plugin_Structure
@@ -429,6 +447,7 @@ module Make = (
429
447
  consumedEventTypes,
430
448
  producedCommandTypes: extractTypes(As.Spec.commandSchema),
431
449
  targetName: As.Spec.targetName,
450
+ chapter: ?chapterFor(As.Spec.name),
432
451
  },
433
452
  )
434
453
  })
@@ -442,6 +461,9 @@ module Make = (
442
461
  commandTypes: extractTypes(Ots.Spec.inboundCommandSchema),
443
462
  // Outbound slices may have no target (option); pass it through.
444
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),
445
467
  },
446
468
  )
447
469
  )
@@ -453,6 +475,38 @@ module Make = (
453
475
  {
454
476
  commandTypes: extractTypes(Its.Spec.commandSchema),
455
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),
481
+ },
482
+ )
483
+ )
484
+
485
+ // StateChange / StateView slices carry no routing target, but they do carry
486
+ // command / event / consumed-event types. Surface those on the hook at parity
487
+ // with aggregates, read models, and routing slices — mirroring the metadata
488
+ // Plugin_Structure records on the static plugin def. Type strings stay
489
+ // unqualified to match the aggregate/read-model entries already on this same
490
+ // hook surface (Plugin_Structure additionally prefixes them with the plugin
491
+ // name).
492
+ let stateSchemas: Dict.t<Plugin_Helpers.pluginDeployedSchema> = Dict.make()
493
+ stateChangeSlices->Array.forEach((module(Scs: ReventlessInfra.StateChangeSlice.T)) =>
494
+ stateSchemas->Dict.set(
495
+ Scs.Spec.name,
496
+ {
497
+ commandTypes: extractTypes(Scs.Spec.commandSchema),
498
+ eventTypes: extractEventTypes(Scs.Spec.eventSchema->S.castToUnknown),
499
+ consumedEventTypes: extractEventTypes(Scs.Spec.consumedEventSchema->S.castToUnknown),
500
+ chapter: ?chapterFor(Scs.Spec.name),
501
+ },
502
+ )
503
+ )
504
+ stateViewSlices->Array.forEach((module(Svs: ReventlessInfra.StateViewSlice.T)) =>
505
+ stateSchemas->Dict.set(
506
+ Svs.Spec.name,
507
+ {
508
+ consumedEventTypes: extractEventTypes(Svs.Spec.consumedEventSchema->S.castToUnknown),
509
+ chapter: ?chapterFor(Svs.Spec.name),
456
510
  },
457
511
  )
458
512
  )
@@ -461,11 +515,15 @@ module Make = (
461
515
  // membership); enrich + register the schema where a matching spec module
462
516
  // was found above, so the deployed hook surfaces it too (the built-hook
463
517
  // components carry it inline).
464
- let routingComponents = (d: dict<_>, kind: string) =>
518
+ let enrichedComponents = (
519
+ d: dict<_>,
520
+ kind: string,
521
+ schemas: Dict.t<Plugin_Helpers.pluginDeployedSchema>,
522
+ ) =>
465
523
  d
466
524
  ->Dict.keysToArray
467
525
  ->Array.map(name => {
468
- let schema = routingSchemas->Dict.get(name)->Option.getOr({})
526
+ let schema = schemas->Dict.get(name)->Option.getOr({})
469
527
  Plugin_Helpers.componentSchemaRegistry->Dict.set(name, schema)
470
528
  ({Plugin_Helpers.name, kind, schema}: Plugin_Helpers.pluginBuiltComponent)
471
529
  })
@@ -473,11 +531,19 @@ module Make = (
473
531
  let components = Array.flat([
474
532
  aggregateComponents,
475
533
  readModelComponents,
476
- mapNames(dcbResult.stateChangeSlicesOutputs, "StateChangeSlice"),
477
- mapNames(dcbResult.stateViewSlicesOutputs, "StateViewSlice"),
478
- routingComponents(dcbResult.automationSlicesOutputs, "AutomationSlice"),
479
- routingComponents(dcbResult.outboundTranslationSlicesOutputs, "OutboundTranslationSlice"),
480
- routingComponents(dcbResult.inboundTranslationSlicesOutputs, "InboundTranslationSlice"),
534
+ enrichedComponents(dcbResult.stateChangeSlicesOutputs, "StateChangeSlice", stateSchemas),
535
+ enrichedComponents(dcbResult.stateViewSlicesOutputs, "StateViewSlice", stateSchemas),
536
+ enrichedComponents(dcbResult.automationSlicesOutputs, "AutomationSlice", routingSchemas),
537
+ enrichedComponents(
538
+ dcbResult.outboundTranslationSlicesOutputs,
539
+ "OutboundTranslationSlice",
540
+ routingSchemas,
541
+ ),
542
+ enrichedComponents(
543
+ dcbResult.inboundTranslationSlicesOutputs,
544
+ "InboundTranslationSlice",
545
+ routingSchemas,
546
+ ),
481
547
  ])
482
548
 
483
549
  switch Plugin_Helpers.onPluginBuiltHook.contents {
@@ -964,6 +1030,7 @@ module Make = (
964
1030
  ~inboundTranslationSlices: array<module(ReventlessInfra.InboundTranslationSlice.T)>=[],
965
1031
  ~extensions: array<module(ReventlessInfra.Extension.Blueprint)>=[],
966
1032
  ~extensionPoints: array<module(ReventlessInfra.ExtensionPointMapping.Mapping)>=[],
1033
+ ~componentChapters: dict<string>=Dict.make(),
967
1034
  ): Reventless.Plugin.pluginStructure =>
968
1035
  Plugin_Structure.make(
969
1036
  ~name,
@@ -976,6 +1043,7 @@ module Make = (
976
1043
  ~inboundTranslationSlices,
977
1044
  ~extensions,
978
1045
  ~extensionPoints,
1046
+ ~componentChapters,
979
1047
  )
980
1048
 
981
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 {
@@ -300,35 +321,50 @@ function Make(Spec) {
300
321
  schema: schema
301
322
  };
302
323
  });
303
- let mapNames = (d, kind) => Object.keys(d).map(name => ({
304
- name: name,
305
- kind: kind,
306
- schema: {}
307
- }));
308
324
  let routingSchemas = {};
309
325
  automationSlices.forEach(As => {
310
326
  let consumedEventTypes = Belt_SetString.toArray(Belt_SetString.fromArray(As.Automation.mappings.flatMap(M => DcbTag$Reventless.extractVariantNames(M.sourceEventSchema))));
311
327
  routingSchemas[As.Spec.name] = {
312
328
  consumedEventTypes: consumedEventTypes,
313
329
  producedCommandTypes: DcbTag$Reventless.extractAllVariantNames(As.Spec.commandSchema),
314
- targetName: As.Spec.targetName
330
+ targetName: As.Spec.targetName,
331
+ chapter: chapterByName[As.Spec.name]
315
332
  };
316
333
  });
317
334
  outboundTranslationSlices.forEach(Ots => {
318
335
  routingSchemas[Ots.Spec.name] = {
319
336
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Ots.Spec.inboundCommandSchema),
320
337
  consumedEventTypes: DcbTag$Reventless.extractVariantNames(Ots.Spec.consumedEventSchema),
321
- targetName: Ots.Spec.targetName
338
+ targetName: Ots.Spec.targetName,
339
+ externalSystem: Ots.Spec.externalSystem,
340
+ chapter: chapterByName[Ots.Spec.name]
322
341
  };
323
342
  });
324
343
  inboundTranslationSlices.forEach(Its => {
325
344
  routingSchemas[Its.Spec.name] = {
326
345
  commandTypes: DcbTag$Reventless.extractAllVariantNames(Its.Spec.commandSchema),
327
- targetName: Its.Spec.targetName
346
+ targetName: Its.Spec.targetName,
347
+ externalSystem: Its.Spec.externalSystem,
348
+ chapter: chapterByName[Its.Spec.name]
349
+ };
350
+ });
351
+ let stateSchemas = {};
352
+ stateChangeSlices.forEach(Scs => {
353
+ stateSchemas[Scs.Spec.name] = {
354
+ commandTypes: DcbTag$Reventless.extractAllVariantNames(Scs.Spec.commandSchema),
355
+ eventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.eventSchema),
356
+ consumedEventTypes: DcbTag$Reventless.extractVariantNames(Scs.Spec.consumedEventSchema),
357
+ chapter: chapterByName[Scs.Spec.name]
358
+ };
359
+ });
360
+ stateViewSlices.forEach(Svs => {
361
+ stateSchemas[Svs.Spec.name] = {
362
+ consumedEventTypes: DcbTag$Reventless.extractVariantNames(Svs.Spec.consumedEventSchema),
363
+ chapter: chapterByName[Svs.Spec.name]
328
364
  };
329
365
  });
330
- let routingComponents = (d, kind) => Object.keys(d).map(name => {
331
- let schema = Stdlib_Option.getOr(routingSchemas[name], {});
366
+ let enrichedComponents = (d, kind, schemas) => Object.keys(d).map(name => {
367
+ let schema = Stdlib_Option.getOr(schemas[name], {});
332
368
  Plugin_Helpers$ReventlessCore.componentSchemaRegistry[name] = schema;
333
369
  return {
334
370
  name: name,
@@ -339,11 +375,11 @@ function Make(Spec) {
339
375
  let components = [
340
376
  aggregateComponents,
341
377
  readModelComponents,
342
- mapNames(dcbResult.stateChangeSlicesOutputs, "StateChangeSlice"),
343
- mapNames(dcbResult.stateViewSlicesOutputs, "StateViewSlice"),
344
- routingComponents(dcbResult.automationSlicesOutputs, "AutomationSlice"),
345
- routingComponents(dcbResult.outboundTranslationSlicesOutputs, "OutboundTranslationSlice"),
346
- routingComponents(dcbResult.inboundTranslationSlicesOutputs, "InboundTranslationSlice")
378
+ enrichedComponents(dcbResult.stateChangeSlicesOutputs, "StateChangeSlice", stateSchemas),
379
+ enrichedComponents(dcbResult.stateViewSlicesOutputs, "StateViewSlice", stateSchemas),
380
+ enrichedComponents(dcbResult.automationSlicesOutputs, "AutomationSlice", routingSchemas),
381
+ enrichedComponents(dcbResult.outboundTranslationSlicesOutputs, "OutboundTranslationSlice", routingSchemas),
382
+ enrichedComponents(dcbResult.inboundTranslationSlicesOutputs, "InboundTranslationSlice", routingSchemas)
347
383
  ].flat();
348
384
  let hook = Plugin_Helpers$ReventlessCore.onPluginBuiltHook.contents;
349
385
  if (hook !== undefined) {
@@ -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 {