@reventlessdev/reventless-core 3.0.0-alpha.262 → 3.0.0-alpha.264

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.
Files changed (34) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/package.json +9 -9
  3. package/src/admin/Platform_Admin_Structure.res.mjs +1 -1
  4. package/src/components/Aggregate/Aggregate_Builder.res.mjs +1 -1
  5. package/src/components/CommandGenerator/CommandGenerator.res +4 -4
  6. package/src/components/CommandGenerator/CommandGenerator_Callback.res +13 -20
  7. package/src/components/CommandGenerator/CommandGenerator_Callback.res.mjs +3 -24
  8. package/src/components/Dcb/Dcb_Builder.res +45 -49
  9. package/src/components/Dcb/Dcb_Builder.res.mjs +31 -40
  10. package/src/components/StateChangeSlice/StateChangeSlice.res +2 -0
  11. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res +6 -0
  12. package/src/components/StateChangeSlice/StateChangeSlice_Builder.res.mjs +4 -4
  13. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res +51 -22
  14. package/src/components/StateChangeSlice/StateChangeSlice_Callback.res.mjs +47 -25
  15. package/src/plugin/component/Plugin_Structure.res +101 -4
  16. package/src/plugin/component/Plugin_Structure.res.mjs +82 -9
  17. package/tests/adapter/RuntimeExtensionHookReachTest.res.mjs +1 -1
  18. package/tests/commandgenerator/CommandGeneratorCallbackTest.res +1 -1
  19. package/tests/commandgenerator/CommandGeneratorCallbackTest.res.mjs +1 -1
  20. package/tests/commandgenerator/CommandGeneratorFixtures.res +2 -0
  21. package/tests/commandgenerator/CommandGeneratorFixtures.res.mjs +17 -3
  22. package/tests/commandgenerator/OwnerStampingTest.res.mjs +2 -2
  23. package/tests/commandtopic/CommandOutcomeHookTest.res.mjs +1 -1
  24. package/tests/dcb/DcbFixtures.res +18 -1
  25. package/tests/dcb/DcbFixtures.res.mjs +14 -0
  26. package/tests/dcb/DcbScopeInferenceTest.res +234 -0
  27. package/tests/dcb/DcbScopeInferenceTest.res.mjs +425 -13
  28. package/tests/dcb/DcbStateChangeSliceTest.res.mjs +22 -22
  29. package/tests/dcb/DcbTagTest.res +81 -68
  30. package/tests/dcb/DcbTagTest.res.mjs +88 -89
  31. package/tests/plugin/PluginStructureAccessTest.res +1 -0
  32. package/tests/plugin/PluginStructureAccessTest.res.mjs +1 -1
  33. package/tests/plugin/PluginStructureTest.res +47 -0
  34. package/tests/plugin/PluginStructureTest.res.mjs +50 -10
package/CHANGELOG.md CHANGED
@@ -3,6 +3,29 @@
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.264 (2026-09-20)
7
+
8
+ **Note:** Version bump only for package @reventlessdev/reventless-core
9
+
10
+
11
+
12
+
13
+
14
+ # 3.0.0-alpha.263 (2026-09-16)
15
+
16
+ * feat(spec)!: infer the DCB partition key per slice, and drop @partitionTag where inference agrees ([6c10161](https://github.com/ReventlessDev/reventless-core/commit/6c101611d438beea4bc9175ae8ea0ea1f6f7493b))
17
+ ### Features
18
+
19
+ * **core:** fail the build when an extension cannot find its slice's partition key ([cf2b093](https://github.com/ReventlessDev/reventless-core/commit/cf2b093dd5dc7cf9425cdac919cd3910fc4aeefd))
20
+ * **spec:** let a slice's chapter break a partition-key tie ([2b9ae77](https://github.com/ReventlessDev/reventless-core/commit/2b9ae77e98eb42177d2bdd4977bc62bb7c7911aa))
21
+
22
+ ### BREAKING CHANGES
23
+
24
+ * DcbTag.derivePartitionTag is removed; a boundary whose
25
+ partition cannot be inferred no longer deploys.
26
+
27
+
28
+
6
29
  # 3.0.0-alpha.262 (2026-09-11)
7
30
 
8
31
  * feat(spec)!: a secret is a capability, so a test can know it in advance ([71507c6](https://github.com/ReventlessDev/reventless-core/commit/71507c6459b49178746f00c5040cb479d7fdade8))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.262",
3
+ "version": "3.0.0-alpha.264",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -28,17 +28,17 @@
28
28
  "dependencies": {
29
29
  "sury": "11.0.0-rc.2",
30
30
  "uuid": "^13.0.0",
31
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.11",
31
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
32
32
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.14",
33
33
  "@reventlessdev/rescript-jest": "1.0.0-alpha.10",
34
- "@reventlessdev/rescript-effect": "0.1.0-alpha.33",
35
- "@reventlessdev/rescript-node": "2.0.0-alpha.11",
36
- "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.20",
37
- "@reventlessdev/rescript-ssh2": "2.0.0-alpha.11",
34
+ "@reventlessdev/rescript-node": "2.0.0-alpha.12",
35
+ "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.21",
36
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.12",
37
+ "@reventlessdev/rescript-ssh2": "2.0.0-alpha.12",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.0",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.36",
40
- "@reventlessdev/reventless-infra": "3.0.0-alpha.163",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.135"
39
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.37",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.137",
41
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.165"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.0",
@@ -7,7 +7,7 @@ import * as PluginsReadModelSpec$ReventlessCore from "../plugin/lifecycle/Plugin
7
7
 
8
8
  let pluginId = "Platform";
9
9
 
10
- let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
10
+ let pluginCommands = Plugin_Structure$ReventlessCore.extractCommandDefs(true, undefined, variantName => Api_Naming$ReventlessCore.adminField(PluginSpec$ReventlessCore.name + "_" + variantName), PluginSpec$ReventlessCore.commandAuthorization, PluginSpec$ReventlessCore.commandTransition, undefined, PluginSpec$ReventlessCore.commandSchema);
11
11
 
12
12
  let pluginAggregate_producedEventTypes = [];
13
13
 
@@ -61,7 +61,7 @@ function Make(Spec) {
61
61
  let fields = registeredFields !== undefined ? (
62
62
  registeredFields.length !== 0 ? registeredFields : []
63
63
  ) : [];
64
- let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, publishJsonsAndWait, Spec.name, Spec.commandSchema, "Aggregate", undefined);
64
+ let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(publishJsons, publishJsonsAndWait, Spec.name, Spec.commandSchema, "Aggregate", undefined, undefined);
65
65
  fields.forEach(field => bindHandler(field, generateCommand));
66
66
  } else {
67
67
  let resources = Component$ReventlessCore.outputs(commandTopic).resources;
@@ -7,10 +7,10 @@ The `id` field is the envelope id for the produced command message:
7
7
  - For **Aggregates**, it is required — the resolver populates it from the
8
8
  GraphQL `id: ID!` argument injected into the mutation SDL.
9
9
  - For **DCB StateChangeSlices**, it is optional. When the resolver does not
10
- supply one, `makeGenerateCommand` derives the envelope id from the command's
11
- `@partitionTag` / `@compositePartitionTag` field(s) using the same helpers
12
- the slice itself uses for its decision-model partition key, so envelope id
13
- and slice partition key share one source of truth.
10
+ supply one, `makeGenerateCommand` takes the envelope id from the command's
11
+ value of the handling slice's partition key — the key `Dcb_Builder` derives
12
+ for storage and the fence — so envelope id and slice partition key share one
13
+ source of truth.
14
14
  */
15
15
  type arguments = {id: string}
16
16
  type meta = {ip: array<string>, user: string, info: string}
@@ -112,6 +112,8 @@ let makeGenerateCommand = (
112
112
  ~commandSchema: S.t<unknown>,
113
113
  ~componentKind: commandComponentKind,
114
114
  ~stripIdFromParams: bool=true,
115
+ // The handling slice's partition; the envelope id is its value on the command.
116
+ ~partitionTag: option<Reventless.DcbTag.derivedPartitionTag>=?,
115
117
  ): CommandGenerator.commandGenerator => {
116
118
  (payload: CommandGenerator.payload) =>
117
119
  Effect.sync(() => {
@@ -157,9 +159,11 @@ let makeGenerateCommand = (
157
159
  }
158
160
  // Resolve the envelope id.
159
161
  // - Aggregates always rely on the resolver-supplied id (id: ID! arg in SDL).
160
- // - DCB StateChangeSlices fall back to deriving the id from the command's
161
- // partition tag(s) when the resolver didn't supply one — this is what makes
162
- // @compositePartitionTag work end-to-end without per-call-site id stuffing.
162
+ // - DCB StateChangeSlices fall back to the value of the handling slice's
163
+ // partition field on the command when the resolver didn't supply one —
164
+ // what makes @compositePartitionTag work end-to-end without per-call-site
165
+ // id stuffing. A command without that field, or a generator that was not
166
+ // given the partition (the permissive `S.json` fallback), gets `""`.
163
167
  let suppliedId = payload.arguments.id
164
168
  let id = switch componentKind {
165
169
  | Aggregate => suppliedId
@@ -167,23 +171,12 @@ let makeGenerateCommand = (
167
171
  switch (suppliedId->Obj.magic: Nullable.t<string>)->Nullable.toOption {
168
172
  | Some(idValue) => idValue
169
173
  | None =>
170
- try {
171
- let derived = Reventless.DcbTag.derivePartitionTag([(serviceName, "", commandSchema)])
172
- switch derived {
173
- | Simple(pt) =>
174
- let tags = Reventless.DcbTag.extractTagsFromJson(commandSchema, commandJson)
175
- Reventless.DcbTag.getPartitionTagValue([{tags: tags}], pt)->Option.getOr("")
176
- | Composite(spec) =>
177
- let tags = Reventless.DcbTag.extractTagsFromJson(commandSchema, commandJson)
178
- Reventless.DcbTag.getCompositePartitionKeyValue(tags, spec)
179
- }
180
- } catch {
181
- // Permissive schemas (e.g. S.json on the AppSync direct-invocation path)
182
- // have no tagged fields — derivePartitionTag throws. Fall back to "" so
183
- // existing transports keep their pre-fix behavior; the slice decoder
184
- // surfaces the missing id downstream as it did before.
185
- | _ => ""
186
- }
174
+ partitionTag->Option.mapOr("", pt =>
175
+ Reventless.DcbTag.extractTagsFromJson(
176
+ commandSchema,
177
+ commandJson,
178
+ )->Reventless.DcbTag.partitionValueOfTags(pt)
179
+ )
187
180
  }
188
181
  }
189
182
  (meta, commandJson, id)
@@ -59,7 +59,7 @@ function dropNullArguments(obj) {
59
59
  });
60
60
  }
61
61
 
62
- function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star) {
62
+ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, commandSchema, componentKind, $staropt$star, partitionTag) {
63
63
  return payload => {
64
64
  let stripIdFromParams = $staropt$star !== undefined ? $staropt$star : true;
65
65
  return Effect.flatMap(Effect.tap(Effect.sync(() => {
@@ -85,28 +85,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
85
85
  ]].concat(params)) : commandStr;
86
86
  let suppliedId = payload.arguments.id;
87
87
  let id;
88
- if (componentKind === "Aggregate" || !(suppliedId == null)) {
89
- id = suppliedId;
90
- } else {
91
- try {
92
- let derived = DcbTag$Reventless.derivePartitionTag([[
93
- serviceName,
94
- "",
95
- commandSchema
96
- ]]);
97
- if (derived.TAG === "Simple") {
98
- let tags = DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson);
99
- id = Stdlib_Option.getOr(DcbTag$Reventless.getPartitionTagValue([{
100
- tags: tags
101
- }], derived._0), "");
102
- } else {
103
- let tags$1 = DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson);
104
- id = DcbTag$Reventless.getCompositePartitionKeyValue(tags$1, derived._0);
105
- }
106
- } catch (exn) {
107
- id = "";
108
- }
109
- }
88
+ id = componentKind === "Aggregate" || !(suppliedId == null) ? suppliedId : Stdlib_Option.mapOr(partitionTag, "", pt => DcbTag$Reventless.partitionValueOfTags(DcbTag$Reventless.extractTagsFromJson(commandSchema, commandJson), pt));
110
89
  return [
111
90
  meta,
112
91
  commandJson,
@@ -181,7 +160,7 @@ function makeGenerateCommand(publishJsons, publishJsonsAndWait, serviceName, com
181
160
 
182
161
  function Make(Spec) {
183
162
  return AggregateSpec => {
184
- let generateCommand = makeGenerateCommand(Spec.publishJsons, Spec.publishJsonsAndWait, AggregateSpec.name, AggregateSpec.commandSchema, "Aggregate", undefined);
163
+ let generateCommand = makeGenerateCommand(Spec.publishJsons, Spec.publishJsonsAndWait, AggregateSpec.name, AggregateSpec.commandSchema, "Aggregate", undefined, undefined);
185
164
  return {
186
165
  generateCommand: generateCommand
187
166
  };
@@ -1,26 +1,5 @@
1
- // Converts a file:// module URL to a path relative to the project root (located by lerna.json),
2
- // resolving symlinks so workspace packages show their source location rather than node_modules.
3
- // Falls back to the bare URL string on any error.
4
1
  let log = Logger.fromEnv()
5
2
 
6
- let toRelativePath: string => string = %raw(`function toRelativePath(moduleUrl) {
7
- try {
8
- const fs = process.getBuiltinModule('node:fs');
9
- const path = process.getBuiltinModule('node:path');
10
- const { fileURLToPath } = process.getBuiltinModule('node:url');
11
- const builderReal = fs.realpathSync(fileURLToPath(import.meta.url));
12
- const sliceReal = fs.realpathSync(fileURLToPath(moduleUrl));
13
- let dir = path.dirname(builderReal);
14
- while (dir !== path.dirname(dir)) {
15
- if (fs.existsSync(path.join(dir, 'lerna.json'))) break;
16
- dir = path.dirname(dir);
17
- }
18
- return path.relative(dir, sliceReal).replace(/\.mjs$/, '.res');
19
- } catch(e) {
20
- return moduleUrl.replace('file://', '');
21
- }
22
- }`)
23
-
24
3
  // The tag of one compiled variant arm: `{TAG: "<Name>", …}` for a payload-bearing
25
4
  // constructor, a bare string const for a payload-less one.
26
5
  let variantTag = (variant: S.t<unknown>) =>
@@ -182,12 +161,6 @@ module Make = (
182
161
  Sc.Spec.name,
183
162
  Sc.Spec.eventSchema->S.castToUnknown,
184
163
  ))
185
- let producedNamed =
186
- stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
187
- Sc.Spec.name,
188
- toRelativePath(Sc.Spec.moduleUrl),
189
- Sc.Spec.eventSchema->S.castToUnknown,
190
- ))
191
164
  let consumed =
192
165
  stateChangeSlices
193
166
  ->Array.map((module(Sc: StateChangeSlice.T)) => (
@@ -253,7 +226,39 @@ module Make = (
253
226
  indexes
254
227
  }
255
228
 
256
- let partitionTag = Reventless.DcbTag.derivePartitionTag(producedNamed)
229
+ let sliceSchemas = stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
230
+ Reventless.DcbTag.name: Sc.Spec.name,
231
+ commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
232
+ consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
233
+ eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
234
+ moduleUrl: Sc.Spec.moduleUrl,
235
+ })
236
+ let inferenceShapes = sliceSchemas->Array.map(Reventless.DcbTag.sliceShape)
237
+
238
+ // A `@partitionTag` inference contradicts would steer storage, fence and read
239
+ // scope to a key the slice only references, so it stops the build. One that
240
+ // inference agrees with is noise, and says so.
241
+ let hintIssues = Reventless.DcbValidation.validatePartitionHintsVsInference(
242
+ ~shapes=inferenceShapes,
243
+ )
244
+ hintIssues.redundancies->Array.forEach(e =>
245
+ log.info(~comp="Dcb_Builder", `DCB partition (${e.sliceName}): ${e.message}`)
246
+ )
247
+ if hintIssues.contradictions->Array.length > 0 {
248
+ let detail =
249
+ hintIssues.contradictions
250
+ ->Array.map(e => `${e.sliceName}: ${e.message}`)
251
+ ->Array.join(" | ")
252
+ log.error(~comp="Dcb_Builder", `DCB partition contradiction (${name}): ${detail}`)
253
+ JsError.throwWithMessage(`DCB partition contradiction (${name}): ${detail}`)
254
+ }
255
+
256
+ // Where each event is stored — and so its fence, and each command's envelope
257
+ // id. Throws, naming the slice, when a partition cannot be inferred.
258
+ let boundaryPartition = Reventless.DcbTag.deriveBoundaryPartition(sliceSchemas)
259
+ let partitionTag = boundaryPartition.partitionTag
260
+ let partitionTagOf = sliceName =>
261
+ boundaryPartition->Reventless.DcbTag.slicePartitionTag(sliceName)
257
262
 
258
263
  // Tag keys declared `@crossPartition` across the produced event schemas.
259
264
  // Derived once at build time (the scope is a property of the tag key, and
@@ -325,15 +330,6 @@ module Make = (
325
330
  // preserved (generalised rule 3). Safety guard: if any slice's partition is
326
331
  // ambiguous we keep the annotated values for the whole boundary rather than
327
332
  // thread a partial inference. See docs/plans/done/dcb-tag-scope-inference.md.
328
- let inferenceShapes =
329
- stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) =>
330
- Reventless.DcbTag.sliceShapeFromSchemas(
331
- ~name=Sc.Spec.name,
332
- ~commandSchema=Sc.Spec.commandSchema->S.castToUnknown,
333
- ~consumedEventSchema=Sc.Spec.consumedEventSchema->S.castToUnknown,
334
- ~eventSchema=Sc.Spec.eventSchema->S.castToUnknown,
335
- )
336
- )
337
333
  let inferred = Reventless.DcbScopeInference.infer(inferenceShapes)
338
334
  if inferred.crossPartitionTagKeys != crossPartitionTagKeys {
339
335
  log.info(
@@ -388,14 +384,7 @@ module Make = (
388
384
  // (DcbCommandTopicEntryPoint.mjs) route through `DcbTag.deriveEffectiveScope`
389
385
  // so the runtime decision query cannot drift from this scope — see
390
386
  // docs/analysis/dcb-runtime-scope-annotation-drift.md.
391
- let effective = Reventless.DcbTag.deriveEffectiveScope(
392
- stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => {
393
- Reventless.DcbTag.name: Sc.Spec.name,
394
- commandSchema: Sc.Spec.commandSchema->S.castToUnknown,
395
- consumedEventSchema: Sc.Spec.consumedEventSchema->S.castToUnknown,
396
- eventSchema: Sc.Spec.eventSchema->S.castToUnknown,
397
- }),
398
- )
387
+ let effective = Reventless.DcbTag.deriveEffectiveScope(sliceSchemas)
399
388
  let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys
400
389
  let effectiveTagKeysByEventType = effective.tagKeysByEventType
401
390
 
@@ -487,6 +476,7 @@ module Make = (
487
476
  ~publishJsons,
488
477
  ~tagKeysByEventType=effectiveTagKeysByEventType,
489
478
  ~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
479
+ ~partitionTag=?partitionTagOf(StateChangeSlice.Spec.name),
490
480
  ~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
491
481
  ~opts,
492
482
  )
@@ -507,6 +497,7 @@ module Make = (
507
497
  ~publishJsons=asyncPublishJsons,
508
498
  ~tagKeysByEventType=effectiveTagKeysByEventType,
509
499
  ~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
500
+ ~partitionTag=?partitionTagOf(StateChangeSlice.Spec.name),
510
501
  ~runtime=?componentRuntime->Dict.get(StateChangeSlice.Spec.name),
511
502
  ~opts,
512
503
  )
@@ -572,6 +563,7 @@ module Make = (
572
563
  ~commandSchema=S.Spec.commandSchema->Obj.magic,
573
564
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
574
565
  ~stripIdFromParams=false,
566
+ ~partitionTag=?partitionTagOf(S.Spec.name),
575
567
  )
576
568
  Api_Naming.sliceMutationFields(
577
569
  ~plugin=apiNamePrefix,
@@ -602,6 +594,7 @@ module Make = (
602
594
  ~commandSchema=S.Spec.commandSchema->Obj.magic,
603
595
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
604
596
  ~stripIdFromParams=false,
597
+ ~partitionTag=?partitionTagOf(S.Spec.name),
605
598
  )
606
599
  Api_Naming.sliceMutationFields(
607
600
  ~plugin=apiNamePrefix,
@@ -813,7 +806,7 @@ module Make = (
813
806
  `Two DCB slices in "${name}" both declare the command "${tag}". ` ++
814
807
  "Owner stamping resolves a command by that name alone, so it cannot " ++ "tell them apart. Rename one of the constructors.",
815
808
  )
816
- | None => byTag->Dict.set(tag, commandSchema)
809
+ | None => byTag->Dict.set(tag, (commandSchema, partitionTagOf(S.Spec.name)))
817
810
  }
818
811
  })
819
812
  }
@@ -825,7 +818,7 @@ module Make = (
825
818
  dcbCommandTopic
826
819
  ->Component.operations
827
820
  ->Pulumi.Output.apply(ops => {
828
- let make = commandSchema =>
821
+ let make = (commandSchema, partitionTag) =>
829
822
  CommandGenerator_Callback.makeGenerateCommand(
830
823
  ~publishJsons=ops.publishJsons,
831
824
  ~publishJsonsAndWait=?ops.publishJsonsAndWait,
@@ -833,16 +826,19 @@ module Make = (
833
826
  ~commandSchema,
834
827
  ~componentKind=CommandGenerator_Callback.StateChangeSlice,
835
828
  ~stripIdFromParams=false,
829
+ ~partitionTag?,
836
830
  )
837
831
  let byTag = Dict.make()
838
832
  dcbCommandSchemasByTag
839
833
  ->Dict.toArray
840
- ->Array.forEach(((tag, schema)) => byTag->Dict.set(tag, make(schema)))
834
+ ->Array.forEach(((tag, (schema, partitionTag))) =>
835
+ byTag->Dict.set(tag, make(schema, partitionTag))
836
+ )
841
837
  // A command no slice claims keeps the permissive generator — what every
842
838
  // command got before this change — so nothing that reaches this handler
843
839
  // by another route starts failing. It also cannot stamp, which is why
844
840
  // the unknown case is logged rather than left to look like a success.
845
- (byTag, make(S.json->S.castToUnknown))
841
+ (byTag, make(S.json->S.castToUnknown, None))
846
842
  })
847
843
 
848
844
  let dcbHandler =
@@ -8,6 +8,7 @@ import * as Output$Pulumi from "@reventlessdev/rescript-pulumi-pulumi/src/Output
8
8
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
9
9
  import * as Effect from "effect/Effect";
10
10
  import * as Pulumi from "@pulumi/pulumi";
11
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
11
12
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
12
13
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
13
14
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
@@ -30,24 +31,6 @@ import * as OutboundTranslationSlice_Callback$ReventlessCore from "../OutboundTr
30
31
 
31
32
  let log = Logger$ReventlessCore.fromEnv();
32
33
 
33
- let toRelativePath = (function toRelativePath(moduleUrl) {
34
- try {
35
- const fs = process.getBuiltinModule('node:fs');
36
- const path = process.getBuiltinModule('node:path');
37
- const { fileURLToPath } = process.getBuiltinModule('node:url');
38
- const builderReal = fs.realpathSync(fileURLToPath(import.meta.url));
39
- const sliceReal = fs.realpathSync(fileURLToPath(moduleUrl));
40
- let dir = path.dirname(builderReal);
41
- while (dir !== path.dirname(dir)) {
42
- if (fs.existsSync(path.join(dir, 'lerna.json'))) break;
43
- dir = path.dirname(dir);
44
- }
45
- return path.relative(dir, sliceReal).replace(/\.mjs$/, '.res');
46
- } catch(e) {
47
- return moduleUrl.replace('file://', '');
48
- }
49
- });
50
-
51
34
  function variantTag(variant) {
52
35
  switch (variant.type) {
53
36
  case "string" :
@@ -157,11 +140,6 @@ function Make(DcbEventLogStorage) {
157
140
  Sc.Spec.name,
158
141
  Sc.Spec.eventSchema
159
142
  ]);
160
- let producedNamed = stateChangeSlices.map(Sc => [
161
- Sc.Spec.name,
162
- toRelativePath(Sc.Spec.moduleUrl),
163
- Sc.Spec.eventSchema
164
- ]);
165
143
  let consumed = stateChangeSlices.map(Sc => [
166
144
  Sc.Spec.name,
167
145
  Sc.Spec.consumedEventSchema
@@ -191,7 +169,23 @@ function Make(DcbEventLogStorage) {
191
169
  }
192
170
  }).map(tagKey => `tag_` + tagKey);
193
171
  let indexes$1 = indexes.length > 1 ? indexes.concat(["tag_composite"]) : indexes;
194
- let partitionTag = DcbTag$Reventless.derivePartitionTag(producedNamed);
172
+ let sliceSchemas = stateChangeSlices.map(Sc => ({
173
+ name: Sc.Spec.name,
174
+ commandSchema: Sc.Spec.commandSchema,
175
+ consumedEventSchema: Sc.Spec.consumedEventSchema,
176
+ eventSchema: Sc.Spec.eventSchema,
177
+ moduleUrl: Sc.Spec.moduleUrl
178
+ }));
179
+ let inferenceShapes = sliceSchemas.map(DcbTag$Reventless.sliceShape);
180
+ let hintIssues = DcbValidation$Reventless.validatePartitionHintsVsInference(inferenceShapes);
181
+ hintIssues.redundancies.forEach(e => log.info("Dcb_Builder", undefined, `DCB partition (` + e.sliceName + `): ` + e.message));
182
+ if (hintIssues.contradictions.length !== 0) {
183
+ let detail = hintIssues.contradictions.map(e => e.sliceName + `: ` + e.message).join(" | ");
184
+ log.error("Dcb_Builder", undefined, `DCB partition contradiction (` + name + `): ` + detail);
185
+ Stdlib_JsError.throwWithMessage(`DCB partition contradiction (` + name + `): ` + detail);
186
+ }
187
+ let boundaryPartition = DcbTag$Reventless.deriveBoundaryPartition(sliceSchemas);
188
+ let partitionTag = boundaryPartition.partitionTag;
195
189
  let arr$1 = producedSchemas.flatMap(DcbTag$Reventless.extractCrossPartitionTagKeys);
196
190
  let seen$1 = new Set();
197
191
  let crossPartitionTagKeys = arr$1.filter(f => {
@@ -209,7 +203,6 @@ function Make(DcbEventLogStorage) {
209
203
  Sc.Spec.commandSchema,
210
204
  Sc.Spec.consumedEventSchema
211
205
  ]), tagKeysByEventType).forEach(w => log.warn("Dcb_Builder", undefined, `DCB composite-read warning (` + w.sliceName + `): ` + w.message));
212
- let inferenceShapes = stateChangeSlices.map(Sc => DcbTag$Reventless.sliceShapeFromSchemas(Sc.Spec.name, Sc.Spec.commandSchema, Sc.Spec.consumedEventSchema, Sc.Spec.eventSchema));
213
206
  let inferred = DcbScopeInference$Reventless.infer(inferenceShapes);
214
207
  if (Primitive_object.notequal(inferred.crossPartitionTagKeys, crossPartitionTagKeys)) {
215
208
  log.info("Dcb_Builder", undefined, `DCB scope-inference diff: crossPartitionTagKeys annotated=[` + crossPartitionTagKeys.join(", ") + `] inferred=[` + inferred.crossPartitionTagKeys.join(", ") + `]`);
@@ -231,12 +224,7 @@ function Make(DcbEventLogStorage) {
231
224
  let scopeIssues = DcbValidation$Reventless.validateScopeVsInference(scopeAnnotations, inferred);
232
225
  scopeIssues.contradictions.forEach(e => log.warn("Dcb_Builder", undefined, `DCB scope contradiction (` + e.sliceName + `): ` + e.message));
233
226
  scopeIssues.redundancies.forEach(e => log.info("Dcb_Builder", undefined, `DCB scope (` + e.sliceName + `): ` + e.message));
234
- let effective = DcbTag$Reventless.deriveEffectiveScope(stateChangeSlices.map(Sc => ({
235
- name: Sc.Spec.name,
236
- commandSchema: Sc.Spec.commandSchema,
237
- consumedEventSchema: Sc.Spec.consumedEventSchema,
238
- eventSchema: Sc.Spec.eventSchema
239
- })));
227
+ let effective = DcbTag$Reventless.deriveEffectiveScope(sliceSchemas);
240
228
  let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys;
241
229
  let effectiveTagKeysByEventType = effective.tagKeysByEventType;
242
230
  if (effective.droppedCrossPartitionTagKeys.length !== 0) {
@@ -276,7 +264,7 @@ function Make(DcbEventLogStorage) {
276
264
  asyncDcbCommandTopicOpt = undefined;
277
265
  }
278
266
  let stateChangeSlicesOutputs = Object.fromEntries(syncSlices.map(StateChangeSlice => {
279
- let ch = StateChangeSlice.make(dcbEventLog, publishJsons, effectiveTagKeysByEventType, effectiveCrossPartitionTagKeys, componentRuntime[StateChangeSlice.Spec.name], opts);
267
+ let ch = StateChangeSlice.make(dcbEventLog, publishJsons, effectiveTagKeysByEventType, effectiveCrossPartitionTagKeys, DcbTag$Reventless.slicePartitionTag(boundaryPartition, StateChangeSlice.Spec.name), componentRuntime[StateChangeSlice.Spec.name], opts);
280
268
  return [
281
269
  StateChangeSlice.Spec.name,
282
270
  Component$ReventlessCore.outputs(ch)
@@ -286,7 +274,7 @@ function Make(DcbEventLogStorage) {
286
274
  if (asyncDcbCommandTopicOpt !== undefined) {
287
275
  let asyncPublishJsons = Component$ReventlessCore.operations(Primitive_option.valFromOption(asyncDcbCommandTopicOpt)).apply(ops => ops.publishJsons);
288
276
  asyncStateChangeSlicesOutputs = Object.fromEntries(asyncSlices.map(StateChangeSlice => {
289
- let ch = StateChangeSlice.make(dcbEventLog, asyncPublishJsons, effectiveTagKeysByEventType, effectiveCrossPartitionTagKeys, componentRuntime[StateChangeSlice.Spec.name], opts);
277
+ let ch = StateChangeSlice.make(dcbEventLog, asyncPublishJsons, effectiveTagKeysByEventType, effectiveCrossPartitionTagKeys, DcbTag$Reventless.slicePartitionTag(boundaryPartition, StateChangeSlice.Spec.name), componentRuntime[StateChangeSlice.Spec.name], opts);
290
278
  return [
291
279
  StateChangeSlice.Spec.name,
292
280
  Component$ReventlessCore.outputs(ch)
@@ -318,7 +306,7 @@ function Make(DcbEventLogStorage) {
318
306
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
319
307
  return;
320
308
  }
321
- let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
309
+ let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false, DcbTag$Reventless.slicePartitionTag(boundaryPartition, S.Spec.name));
322
310
  Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
323
311
  });
324
312
  });
@@ -329,7 +317,7 @@ function Make(DcbEventLogStorage) {
329
317
  if (ApiNoApiHelpers$ReventlessCore.isNoApi(commandSchema)) {
330
318
  return;
331
319
  }
332
- let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(asyncOps.publishJsons, asyncOps.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false);
320
+ let generateCommand = CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(asyncOps.publishJsons, asyncOps.publishJsonsAndWait, S.Spec.name, S.Spec.commandSchema, "StateChangeSlice", false, DcbTag$Reventless.slicePartitionTag(boundaryPartition, S.Spec.name));
333
321
  Api_Naming$ReventlessCore.sliceMutationFields(apiNamePrefix, S.Spec.name, commandSchema).forEach(param => bindHandler(param[0], generateCommand));
334
322
  });
335
323
  });
@@ -437,7 +425,10 @@ function Make(DcbEventLogStorage) {
437
425
  if (match !== undefined) {
438
426
  return log.error("Dcb_Builder", undefined, `Two DCB slices in "` + name + `" both declare the command "` + tag + `". ` + "Owner stamping resolves a command by that name alone, so it cannot tell them apart. Rename one of the constructors.");
439
427
  } else {
440
- byTag[tag] = commandSchema;
428
+ byTag[tag] = [
429
+ commandSchema,
430
+ DcbTag$Reventless.slicePartitionTag(boundaryPartition, S.Spec.name)
431
+ ];
441
432
  return;
442
433
  }
443
434
  });
@@ -445,14 +436,15 @@ function Make(DcbEventLogStorage) {
445
436
  }
446
437
  });
447
438
  let dcbGenerateCommandOutput = Component$ReventlessCore.operations(dcbCommandTopic).apply(ops => {
448
- let make = commandSchema => CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, name, commandSchema, "StateChangeSlice", false);
439
+ let make = (commandSchema, partitionTag) => CommandGenerator_Callback$ReventlessCore.makeGenerateCommand(ops.publishJsons, ops.publishJsonsAndWait, name, commandSchema, "StateChangeSlice", false, partitionTag);
449
440
  let byTag$1 = {};
450
441
  Object.entries(byTag).forEach(param => {
451
- byTag$1[param[0]] = make(param[1]);
442
+ let match = param[1];
443
+ byTag$1[param[0]] = make(match[0], match[1]);
452
444
  });
453
445
  return [
454
446
  byTag$1,
455
- make(Sury.json)
447
+ make(Sury.json, undefined)
456
448
  ];
457
449
  });
458
450
  let dcbHandler = Pulumi.all([
@@ -693,7 +685,6 @@ function Make(DcbEventLogStorage) {
693
685
 
694
686
  export {
695
687
  log,
696
- toRelativePath,
697
688
  variantTag,
698
689
  mergedEventSchema,
699
690
  emptyResult,
@@ -33,6 +33,8 @@ module type T = {
33
33
  clause (instead of an AND-ed composite) so the decision read is a real
34
34
  secondary-tag read. */
35
35
  ~crossPartitionTagKeys: array<string>=?,
36
+ /** This slice's partition, as `Dcb_Builder` derives it for the whole boundary. */
37
+ ~partitionTag: Reventless.DcbTag.derivedPartitionTag=?,
36
38
  ~runtime: ReventlessInfra.RuntimeHints.t=?,
37
39
  ~opts: Pulumi.ComponentResource.options=?,
38
40
  ) => component
@@ -13,6 +13,7 @@ module Make = (
13
13
  let makeJsonHandler = (
14
14
  ~tagKeysByEventType,
15
15
  ~crossPartitionTagKeys,
16
+ ~partitionTag,
16
17
  dcbEventLogOps: DcbEventLog.operations,
17
18
  ) => {
18
19
  let handler: CommandTopic.jsonCommandsHandler = stream => {
@@ -42,6 +43,7 @@ module Make = (
42
43
  Callback.handleCommands(
43
44
  ~tagKeysByEventType,
44
45
  ~crossPartitionTagKeys,
46
+ ~partitionTag?,
45
47
  dcbEventLogOps,
46
48
  decodedStream,
47
49
  )
@@ -54,6 +56,7 @@ module Make = (
54
56
  ~publishJsons: Pulumi.Output.t<CommandTopic.publishJsons>,
55
57
  ~tagKeysByEventType,
56
58
  ~crossPartitionTagKeys,
59
+ ~partitionTag,
57
60
  self,
58
61
  _name,
59
62
  ) => {
@@ -67,6 +70,7 @@ module Make = (
67
70
  let jsonHandler = makeJsonHandler(
68
71
  ~tagKeysByEventType,
69
72
  ~crossPartitionTagKeys,
73
+ ~partitionTag,
70
74
  dcbEventLogOps,
71
75
  )
72
76
  CommandTopic.registerHandler(
@@ -98,6 +102,7 @@ module Make = (
98
102
  ~publishJsons,
99
103
  ~tagKeysByEventType=Dict.make(),
100
104
  ~crossPartitionTagKeys=[],
105
+ ~partitionTag=?,
101
106
  ~runtime as _=?,
102
107
  ~opts=?,
103
108
  ): StateChangeSlice.component =>
@@ -109,6 +114,7 @@ module Make = (
109
114
  ~publishJsons,
110
115
  ~tagKeysByEventType,
111
116
  ~crossPartitionTagKeys,
117
+ ~partitionTag,
112
118
  ...
113
119
  ),
114
120
  ~opts,
@@ -18,7 +18,7 @@ let log = Logger$ReventlessCore.fromEnv();
18
18
  function Make(Spec) {
19
19
  return Behavior => {
20
20
  let Callback = StateChangeSlice_Callback$ReventlessCore.Make(Spec)(Behavior);
21
- let makeJsonHandler = (tagKeysByEventType, crossPartitionTagKeys, dcbEventLogOps) => (stream => {
21
+ let makeJsonHandler = (tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLogOps) => (stream => {
22
22
  let decodedStream = Stream.flatMap(Stream.mapEffect(stream, param => {
23
23
  let reference = param.reference;
24
24
  let json = param.command;
@@ -44,16 +44,16 @@ function Make(Spec) {
44
44
  return Stream.empty;
45
45
  }
46
46
  });
47
- return Callback.handleCommands(tagKeysByEventType, crossPartitionTagKeys, dcbEventLogOps, decodedStream);
47
+ return Callback.handleCommands(tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLogOps, decodedStream);
48
48
  });
49
- let make = (dcbEventLog, publishJsons, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, param, opts) => {
49
+ let make = (dcbEventLog, publishJsons, tagKeysByEventTypeOpt, crossPartitionTagKeysOpt, partitionTag, param, opts) => {
50
50
  let tagKeysByEventType = tagKeysByEventTypeOpt !== undefined ? tagKeysByEventTypeOpt : ({});
51
51
  let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
52
52
  return Component$ReventlessCore.make(ComponentType$ReventlessCore.toString(StateChangeSlice$ReventlessCore.componentType), Spec.name, (extra, extra$1) => {
53
53
  let commandSchema = Spec.commandSchema;
54
54
  let commandTypeNames = CommandTopic$ReventlessCore.extractTypeNamesFromSchema(commandSchema);
55
55
  Component$ReventlessCore.operations(dcbEventLog).apply(dcbEventLogOps => {
56
- let jsonHandler = makeJsonHandler(tagKeysByEventType, crossPartitionTagKeys, dcbEventLogOps);
56
+ let jsonHandler = makeJsonHandler(tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLogOps);
57
57
  CommandTopic$ReventlessCore.registerHandler(commandSchema, jsonHandler, commandTypeNames);
58
58
  });
59
59
  Component$ReventlessCore.setOperations(extra, publishJsons.apply(publishJsons => ({