@reventlessdev/reventless-core 3.0.0-alpha.272 → 3.0.0-alpha.273

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,6 +3,13 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 3.0.0-alpha.273 (2026-09-24)
7
+
8
+ ### Features
9
+
10
+ * **dcb:** a command reference no decision reads by stays out of the query ([3ae149a](https://github.com/ReventlessDev/reventless-core/commit/3ae149ae91ebab63413be57df5827c5fd4390160))
11
+
12
+
6
13
  # 3.0.0-alpha.272 (2026-09-23)
7
14
 
8
15
  **Note:** Version bump only for package @reventlessdev/reventless-core
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-core",
3
- "version": "3.0.0-alpha.272",
3
+ "version": "3.0.0-alpha.273",
4
4
  "description": "Core package for Reventless framework",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -29,21 +29,21 @@
29
29
  "sury": "11.0.0",
30
30
  "uuid": "^13.0.0",
31
31
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
32
+ "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.15",
32
33
  "@reventlessdev/rescript-hash-object": "1.2.0-alpha.15",
33
34
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
34
35
  "@reventlessdev/rescript-node": "2.0.0-alpha.15",
35
- "@reventlessdev/rescript-fast-csv": "2.0.0-alpha.15",
36
36
  "@reventlessdev/rescript-pulumi-pulumi": "2.3.0-alpha.22",
37
37
  "@reventlessdev/rescript-ssh2": "2.0.0-alpha.15",
38
38
  "@reventlessdev/rescript-uuid": "2.0.0-alpha.1",
39
- "@reventlessdev/reventless-interop": "3.0.0-alpha.38",
40
- "@reventlessdev/reventless-infra": "3.0.0-alpha.172",
41
- "@reventlessdev/reventless-spec": "3.0.0-alpha.144"
39
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.173",
40
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.145",
41
+ "@reventlessdev/reventless-interop": "3.0.0-alpha.38"
42
42
  },
43
43
  "devDependencies": {
44
44
  "rescript": "12.3.1",
45
45
  "sury-ppx": "11.0.0",
46
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.89"
46
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.92"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "rescript": "^12.3.1"
@@ -306,21 +306,6 @@ module Make = (
306
306
  ->Array.map(schema => Reventless.DcbTag.extractTagKeysByEventType(schema))
307
307
  ->Reventless.DcbTag.mergeTagKeysByEventType
308
308
 
309
- // Warn on composite (multi-tag) decision reads that would silently miss an
310
- // event carrying extra tags (the tag_composite key is the event's full tag
311
- // set, so composite reads are exact-match). Non-fatal — today's slices are
312
- // aligned; the guard catches a tag added to a multi-tag event later.
313
- Reventless.DcbValidation.validateCompositeReads(
314
- ~slices=stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
315
- Sc.Spec.name,
316
- Sc.Spec.commandSchema->S.castToUnknown,
317
- Sc.Spec.consumedEventSchema->S.castToUnknown,
318
- )),
319
- ~producedTagKeys=tagKeysByEventType,
320
- )->Array.forEach(w =>
321
- log.warn(~comp="Dcb_Builder", `DCB composite-read warning (${w.sliceName}): ${w.message}`)
322
- )
323
-
324
309
  // --- Phase 2 (dcb-tag-scope-inference): derive scope from the global slice
325
310
  // graph and THREAD it into the decision-query wiring, replacing the
326
311
  // annotation-based extraction. The derived `tagKeysByEventType` is what
@@ -388,6 +373,32 @@ module Make = (
388
373
  let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys
389
374
  let effectiveTagKeysByEventType = effective.tagKeysByEventType
390
375
 
376
+ // Warn on composite (multi-tag) decision reads that would silently miss an
377
+ // event carrying extra tags (the tag_composite key is the event's full tag
378
+ // set, so composite reads are exact-match). Non-fatal — today's slices are
379
+ // aligned; the guard catches a tag added to a multi-tag event later.
380
+ Reventless.DcbValidation.validateCompositeReads(
381
+ ~slices=stateChangeSlices->Array.map((module(Sc: StateChangeSlice.T)) => (
382
+ Sc.Spec.name,
383
+ Sc.Spec.commandSchema->S.castToUnknown,
384
+ Sc.Spec.consumedEventSchema->S.castToUnknown,
385
+ )),
386
+ ~producedTagKeys=tagKeysByEventType,
387
+ ~payloadTagKeysBySlice=sliceSchemas
388
+ ->Array.map(slice => (
389
+ slice.name,
390
+ slice
391
+ ->Reventless.DcbTag.sliceShape
392
+ ->Reventless.DcbTag.commandPayloadTagKeys(
393
+ ~partitionTag=partitionTagOf(slice.name),
394
+ ~crossPartitionTagKeys=effectiveCrossPartitionTagKeys,
395
+ ),
396
+ ))
397
+ ->Dict.fromArray,
398
+ )->Array.forEach(w =>
399
+ log.warn(~comp="Dcb_Builder", `DCB composite-read warning (${w.sliceName}): ${w.message}`)
400
+ )
401
+
391
402
  // An ambiguity is a warning above because a boundary can fall back and
392
403
  // lose nothing. Losing a cross-partition key is not that: the decision
393
404
  // read for every slice that references another entity silently narrows to
@@ -198,11 +198,6 @@ function Make(DcbEventLogStorage) {
198
198
  });
199
199
  DcbValidation$Reventless.validateCrossPartitionScope(produced).forEach(err => log.error("Dcb_Builder", undefined, `DCB cross-partition scope error (` + err.sliceName + `): ` + err.message));
200
200
  let tagKeysByEventType = DcbTag$Reventless.mergeTagKeysByEventType(producedSchemas.map(DcbTag$Reventless.extractTagKeysByEventType));
201
- DcbValidation$Reventless.validateCompositeReads(stateChangeSlices.map(Sc => [
202
- Sc.Spec.name,
203
- Sc.Spec.commandSchema,
204
- Sc.Spec.consumedEventSchema
205
- ]), tagKeysByEventType).forEach(w => log.warn("Dcb_Builder", undefined, `DCB composite-read warning (` + w.sliceName + `): ` + w.message));
206
201
  let inferred = DcbScopeInference$Reventless.infer(inferenceShapes);
207
202
  if (Primitive_object.notequal(inferred.crossPartitionTagKeys, crossPartitionTagKeys)) {
208
203
  log.info("Dcb_Builder", undefined, `DCB scope-inference diff: crossPartitionTagKeys annotated=[` + crossPartitionTagKeys.join(", ") + `] inferred=[` + inferred.crossPartitionTagKeys.join(", ") + `]`);
@@ -227,6 +222,14 @@ function Make(DcbEventLogStorage) {
227
222
  let effective = DcbTag$Reventless.deriveEffectiveScope(sliceSchemas);
228
223
  let effectiveCrossPartitionTagKeys = effective.crossPartitionTagKeys;
229
224
  let effectiveTagKeysByEventType = effective.tagKeysByEventType;
225
+ DcbValidation$Reventless.validateCompositeReads(stateChangeSlices.map(Sc => [
226
+ Sc.Spec.name,
227
+ Sc.Spec.commandSchema,
228
+ Sc.Spec.consumedEventSchema
229
+ ]), tagKeysByEventType, Object.fromEntries(sliceSchemas.map(slice => [
230
+ slice.name,
231
+ DcbTag$Reventless.commandPayloadTagKeys(DcbTag$Reventless.sliceShape(slice), DcbTag$Reventless.slicePartitionTag(boundaryPartition, slice.name), effectiveCrossPartitionTagKeys)
232
+ ]))).forEach(w => log.warn("Dcb_Builder", undefined, `DCB composite-read warning (` + w.sliceName + `): ` + w.message));
230
233
  if (effective.droppedCrossPartitionTagKeys.length !== 0) {
231
234
  log.error("Dcb_Builder", undefined, `DCB scope degraded (` + name + `): cross-partition reads of [` + effective.droppedCrossPartitionTagKeys.join(", ") + `] were derived but dropped — an ambiguous slice forced the boundary onto @crossPartition annotations that do not carry them. Slices referencing another entity by these keys will decide against an empty history and reject valid commands. Resolve: ` + effective.ambiguities.map(param => param[0] + ` (` + param[1] + `)`).join(" | "));
232
235
  }
@@ -93,6 +93,15 @@ module Make = (
93
93
  }
94
94
  )
95
95
 
96
+ // A command key the slice does not decide by stays out of its query; which keys
97
+ // those are depends on the partition, so `handleCommands` asks per boundary.
98
+ let scopeShape = Reventless.DcbTag.sliceShapeFromSchemas(
99
+ ~name=Spec.name,
100
+ ~commandSchema=Spec.commandSchema,
101
+ ~consumedEventSchema=Spec.consumedEventSchema,
102
+ ~eventSchema=Spec.eventSchema,
103
+ )
104
+
96
105
  // Extracts the entity id of a read event from its own tags, for logging
97
106
  // which events the decision model was built from. Prefers this slice's
98
107
  // partition key, but consumed events from other sources are tagged by their
@@ -162,6 +171,7 @@ module Make = (
162
171
  let handleSingleCommand = (
163
172
  ~tagKeysByEventType,
164
173
  ~crossPartitionTagKeys,
174
+ ~payloadTagKeys,
165
175
  ~partitionTag,
166
176
  dcbEventLog: DcbEventLog.operations,
167
177
  command': Message.command'<Reventless.Id.String.t, Spec.command>,
@@ -186,6 +196,7 @@ module Make = (
186
196
  ~value=command'.command,
187
197
  ~tagKeysByEventType,
188
198
  ~crossPartitionTagKeys,
199
+ ~payloadTagKeys,
189
200
  )
190
201
 
191
202
  // Log the DCB query parameters — the OR clauses (event types + tags) the
@@ -504,11 +515,14 @@ module Make = (
504
515
  | Some(_) => partitionTag
505
516
  | None => slicePartitionTag->Lazy.get
506
517
  }
518
+ let payloadTagKeys =
519
+ scopeShape->Reventless.DcbTag.commandPayloadTagKeys(~partitionTag, ~crossPartitionTagKeys)
507
520
  stream
508
521
  ->Stream.mapEffect(({ReventlessInfra.CommandTopic.reference: reference, command}) =>
509
522
  handleSingleCommand(
510
523
  ~tagKeysByEventType,
511
524
  ~crossPartitionTagKeys,
525
+ ~payloadTagKeys,
512
526
  ~partitionTag,
513
527
  dcbEventLog,
514
528
  command,
@@ -36,6 +36,7 @@ function Make(Spec) {
36
36
  return;
37
37
  }
38
38
  });
39
+ let scopeShape = DcbTag$Reventless.sliceShapeFromSchemas(Spec.name, Spec.commandSchema, Spec.consumedEventSchema, Spec.eventSchema, undefined);
39
40
  let readEventId = (partitionTag, tags) => {
40
41
  let ownTagValues = () => {
41
42
  let vals = Stdlib_Array.reduce(tags, [], (acc, t) => {
@@ -70,10 +71,10 @@ function Make(Spec) {
70
71
  };
71
72
  let projectionCache = Lru$ReventlessCore.make(100);
72
73
  let resetCache = () => Lru$ReventlessCore.clear(projectionCache);
73
- let handleSingleCommand = (tagKeysByEventType, crossPartitionTagKeys, partitionTag, dcbEventLog, command$p) => {
74
+ let handleSingleCommand = (tagKeysByEventType, crossPartitionTagKeys, payloadTagKeys, partitionTag, dcbEventLog, command$p) => {
74
75
  let cmdJson = Message$ReventlessCore.commandJsonOfCommand$p(Id$Reventless.$$String.toString, Spec.commandSchema, command$p);
75
76
  Effect.runSync(EffectLogger$ReventlessCore.logInfo(comp, cmdJson.commandJson, `handling command: ` + LogFormat$ReventlessCore.cmdDetailNoId(cmdJson)));
76
- let query = DcbTag$Reventless.buildQueryFromCommand(queryEventTypes, Spec.commandSchema, command$p.command, tagKeysByEventType, crossPartitionTagKeys);
77
+ let query = DcbTag$Reventless.buildQueryFromCommand(queryEventTypes, Spec.commandSchema, command$p.command, tagKeysByEventType, crossPartitionTagKeys, payloadTagKeys);
77
78
  let queryDetail = query.map(qi => {
78
79
  let ts = qi.eventTypes;
79
80
  let types = ts !== undefined ? ts.map(LogFormat$ReventlessCore.bold).join("|") : "*";
@@ -315,9 +316,10 @@ function Make(Spec) {
315
316
  let tagKeysByEventType = tagKeysByEventTypeOpt !== undefined ? tagKeysByEventTypeOpt : ({});
316
317
  let crossPartitionTagKeys = crossPartitionTagKeysOpt !== undefined ? crossPartitionTagKeysOpt : [];
317
318
  let partitionTag$1 = partitionTag !== undefined ? partitionTag : Stdlib_Lazy.get(slicePartitionTag);
319
+ let payloadTagKeys = DcbTag$Reventless.commandPayloadTagKeys(scopeShape, partitionTag$1, crossPartitionTagKeys);
318
320
  return Stream.runCollect(Stream$1.mapEffect(stream, param => {
319
321
  let reference = param.reference;
320
- return Effect.map(handleSingleCommand(tagKeysByEventType, crossPartitionTagKeys, partitionTag$1, dcbEventLog, param.command), result => {
322
+ return Effect.map(handleSingleCommand(tagKeysByEventType, crossPartitionTagKeys, payloadTagKeys, partitionTag$1, dcbEventLog, param.command), result => {
321
323
  if (result.TAG === "Ok") {
322
324
  return {
323
325
  TAG: "Ok",
@@ -540,7 +540,7 @@ globalThis.describe("DcbTag:", () => {
540
540
  TAG: "CreateItem",
541
541
  itemId: "item-1",
542
542
  name: "Test"
543
- }, undefined, undefined)).toEqual([{
543
+ }, undefined, undefined, undefined)).toEqual([{
544
544
  eventTypes: eventTypes,
545
545
  tags: [{
546
546
  key: "itemId",
@@ -557,7 +557,7 @@ globalThis.describe("DcbTag:", () => {
557
557
  "prod-1",
558
558
  "prod-2"
559
559
  ]
560
- }, undefined, undefined)).toEqual([
560
+ }, undefined, undefined, undefined)).toEqual([
561
561
  {
562
562
  eventTypes: eventTypes,
563
563
  tags: [{
@@ -587,7 +587,7 @@ globalThis.describe("DcbTag:", () => {
587
587
  orderId: "ord-1",
588
588
  customerId: "cust-1",
589
589
  productId: ["prod-1"]
590
- }, undefined, undefined)).toEqual([
590
+ }, undefined, undefined, undefined)).toEqual([
591
591
  {
592
592
  eventTypes: eventTypes,
593
593
  tags: [{
@@ -610,7 +610,7 @@ globalThis.describe("DcbTag:", () => {
610
610
  orderId: "ord-1",
611
611
  customerId: "cust-1",
612
612
  productId: []
613
- }, undefined, undefined)).toEqual([{
613
+ }, undefined, undefined, undefined)).toEqual([{
614
614
  eventTypes: eventTypes,
615
615
  tags: [{
616
616
  key: "orderId",
@@ -644,7 +644,7 @@ globalThis.describe("DcbTag:", () => {
644
644
  orderId: "ord-1",
645
645
  customerId: "cust-1",
646
646
  productId: ["prod-1"]
647
- }, orderTagKeys, undefined)).toEqual([
647
+ }, orderTagKeys, undefined, undefined)).toEqual([
648
648
  {
649
649
  eventTypes: ["OrderPlaced"],
650
650
  tags: [{
@@ -672,7 +672,7 @@ globalThis.describe("DcbTag:", () => {
672
672
  TAG: "CreateItem",
673
673
  itemId: "item-1",
674
674
  name: "Test"
675
- }, orderTagKeys, undefined)).toEqual([{
675
+ }, orderTagKeys, undefined, undefined)).toEqual([{
676
676
  eventTypes: [],
677
677
  tags: [{
678
678
  key: "itemId",
@@ -689,7 +689,7 @@ globalThis.describe("DcbTag:", () => {
689
689
  orderId: "ord-1",
690
690
  customerId: "cust-1",
691
691
  productId: ["prod-1"]
692
- }, {}, undefined)).toEqual([
692
+ }, {}, undefined, undefined)).toEqual([
693
693
  {
694
694
  eventTypes: [
695
695
  "OrderPlaced",
@@ -723,7 +723,7 @@ globalThis.describe("DcbTag:", () => {
723
723
  TAG: "SubscribeStudent",
724
724
  courseId: "C1",
725
725
  studentId: "S1"
726
- }, undefined, ["studentId"])).toEqual([
726
+ }, undefined, ["studentId"], undefined)).toEqual([
727
727
  {
728
728
  eventTypes: ["StudentSubscribed"],
729
729
  tags: [{
@@ -745,7 +745,7 @@ globalThis.describe("DcbTag:", () => {
745
745
  TAG: "SubscribeStudent",
746
746
  courseId: "C1",
747
747
  studentId: "S1"
748
- }, undefined, undefined)).toEqual([{
748
+ }, undefined, undefined, undefined)).toEqual([{
749
749
  eventTypes: ["StudentSubscribed"],
750
750
  tags: [
751
751
  {
@@ -813,7 +813,7 @@ globalThis.describe("DcbTag:", () => {
813
813
  "prod-1",
814
814
  "prod-2"
815
815
  ]
816
- }, undefined, undefined)).toEqual([
816
+ }, undefined, undefined, undefined)).toEqual([
817
817
  {
818
818
  eventTypes: [
819
819
  "OrderPlaced",
@@ -372,7 +372,7 @@ globalThis.describe("DcbValidation:", () => {
372
372
  "RecordDemand",
373
373
  recordDemandCommandSchema,
374
374
  demandConsumedSchema
375
- ]], producedTagKeys);
375
+ ]], producedTagKeys, undefined);
376
376
  globalThis.expect(warnings.length).toBe(1);
377
377
  let w = warnings[0];
378
378
  globalThis.expect(w.sliceName).toBe("RecordDemand");
@@ -384,7 +384,7 @@ globalThis.describe("DcbValidation:", () => {
384
384
  "RecordDemand",
385
385
  recordDemandCommandSchema,
386
386
  demandConsumedSchema
387
- ]], producedTagKeys);
387
+ ]], producedTagKeys, undefined);
388
388
  globalThis.expect(warnings.some(w => w.message.includes("ProductDemandRecorded"))).toBe(false);
389
389
  });
390
390
  globalThis.test("ignores single-tag commands (partition read, not composite)", () => {
@@ -392,7 +392,7 @@ globalThis.describe("DcbValidation:", () => {
392
392
  "TouchDemand",
393
393
  touchDemandCommandSchema,
394
394
  demandConsumedSchema
395
- ]], producedTagKeys);
395
+ ]], producedTagKeys, undefined);
396
396
  globalThis.expect(warnings).toEqual([]);
397
397
  });
398
398
  globalThis.test("ignores tagged-array commands (per-element OR, not composite)", () => {
@@ -400,11 +400,11 @@ globalThis.describe("DcbValidation:", () => {
400
400
  "PlaceOrder",
401
401
  placeOrderCommandSchema,
402
402
  demandConsumedSchema
403
- ]], producedTagKeys);
403
+ ]], producedTagKeys, undefined);
404
404
  globalThis.expect(warnings).toEqual([]);
405
405
  });
406
406
  globalThis.test("no warnings when nothing reads compositely", () => {
407
- globalThis.expect(DcbValidation$Reventless.validateCompositeReads([], producedTagKeys)).toEqual([]);
407
+ globalThis.expect(DcbValidation$Reventless.validateCompositeReads([], producedTagKeys, undefined)).toEqual([]);
408
408
  });
409
409
  });
410
410
  globalThis.describe("validateCrossPartitionScope (Issue 13)", () => {