@reventlessdev/reventless-spec 3.0.0-alpha.135 → 3.0.0-alpha.136
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 +18 -0
- package/package.json +2 -2
- package/src/components/DcbScopeInference.res +224 -73
- package/src/components/DcbScopeInference.res.mjs +187 -49
- package/src/components/DcbTag.res +142 -186
- package/src/components/DcbTag.res.mjs +137 -128
- package/src/components/DcbValidation.res +50 -0
- package/src/components/DcbValidation.res.mjs +51 -0
- package/src/generator/Codegen.res +2 -1
- package/src/generator/Codegen.res.mjs +1 -1
- package/src/lifecycle/CheckLifecycleModel.res +114 -4
- package/src/lifecycle/CheckLifecycleModel.res.mjs +148 -4
- package/src/types/AccountsManifest.res +6 -0
- package/src/types/AccountsManifest.res.mjs +2 -1
|
@@ -10,6 +10,7 @@ import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js
|
|
|
10
10
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
11
11
|
import * as Semantic$Reventless from "../semantic/Semantic.res.mjs";
|
|
12
12
|
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
13
|
+
import * as ComponentKind$Reventless from "./ComponentKind.res.mjs";
|
|
13
14
|
import * as DcbScopeInference$Reventless from "./DcbScopeInference.res.mjs";
|
|
14
15
|
|
|
15
16
|
let tagSchema = Sury.$schema(s => ({
|
|
@@ -68,6 +69,10 @@ let derivedPartitionTagSchema = Sury.union([
|
|
|
68
69
|
Sury.$schema(s => ({
|
|
69
70
|
TAG: "Composite",
|
|
70
71
|
_0: s.m(compositePartitionSpecSchema)
|
|
72
|
+
})),
|
|
73
|
+
Sury.$schema(s => ({
|
|
74
|
+
TAG: "ByEventType",
|
|
75
|
+
_0: s.m(Sury.dict(Sury.string))
|
|
71
76
|
}))
|
|
72
77
|
]);
|
|
73
78
|
|
|
@@ -789,7 +794,23 @@ function extractPartitionTagFields(schema) {
|
|
|
789
794
|
}
|
|
790
795
|
}
|
|
791
796
|
|
|
792
|
-
function
|
|
797
|
+
function chapterOfModuleUrl(moduleUrl) {
|
|
798
|
+
let parts = moduleUrl.split("/src/");
|
|
799
|
+
let len = parts.length;
|
|
800
|
+
if (len === 1) {
|
|
801
|
+
return;
|
|
802
|
+
}
|
|
803
|
+
if (len === 0) {
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
let segments = parts[parts.length - 1 | 0].split("/");
|
|
807
|
+
let first = segments[0];
|
|
808
|
+
if (first !== undefined && segments.length > 1 && !ComponentKind$Reventless.isKindFolder(first)) {
|
|
809
|
+
return first;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function sliceShapeFromSchemas(name, commandSchema, consumedEventSchema, eventSchema, moduleUrl) {
|
|
793
814
|
let match = extractPartitionTagFields(eventSchema);
|
|
794
815
|
let partitionHint = match.length !== 1 ? undefined : match[0];
|
|
795
816
|
return {
|
|
@@ -797,10 +818,15 @@ function sliceShapeFromSchemas(name, commandSchema, consumedEventSchema, eventSc
|
|
|
797
818
|
command: eventShapesOfSchema(commandSchema).flatMap(e => e.idFields),
|
|
798
819
|
consumed: eventShapesOfSchema(consumedEventSchema),
|
|
799
820
|
produced: eventShapesOfSchema(eventSchema),
|
|
800
|
-
partitionHint: partitionHint
|
|
821
|
+
partitionHint: partitionHint,
|
|
822
|
+
chapter: Stdlib_Option.flatMap(moduleUrl, chapterOfModuleUrl)
|
|
801
823
|
};
|
|
802
824
|
}
|
|
803
825
|
|
|
826
|
+
function sliceShape(s) {
|
|
827
|
+
return sliceShapeFromSchemas(s.name, s.commandSchema, s.consumedEventSchema, s.eventSchema, s.moduleUrl);
|
|
828
|
+
}
|
|
829
|
+
|
|
804
830
|
function deriveEffectiveScope(slices) {
|
|
805
831
|
let producedSchemas = slices.map(s => s.eventSchema);
|
|
806
832
|
let seen = new Set();
|
|
@@ -813,8 +839,7 @@ function deriveEffectiveScope(slices) {
|
|
|
813
839
|
}
|
|
814
840
|
});
|
|
815
841
|
let annotatedTagKeys = mergeTagKeysByEventType(producedSchemas.map(extractTagKeysByEventType));
|
|
816
|
-
let
|
|
817
|
-
let inferred = DcbScopeInference$Reventless.infer(shapes);
|
|
842
|
+
let inferred = DcbScopeInference$Reventless.infer(slices.map(sliceShape));
|
|
818
843
|
let useInferred = inferred.ambiguities.length === 0;
|
|
819
844
|
return {
|
|
820
845
|
crossPartitionTagKeys: useInferred ? inferred.crossPartitionTagKeys : annotatedCross,
|
|
@@ -824,46 +849,6 @@ function deriveEffectiveScope(slices) {
|
|
|
824
849
|
};
|
|
825
850
|
}
|
|
826
851
|
|
|
827
|
-
function hasMultiTagVariant(schema) {
|
|
828
|
-
switch (schema.type) {
|
|
829
|
-
case "object" :
|
|
830
|
-
let tagCount = Object.entries(schema.properties).filter(param => Stdlib_Option.isSome(Sury.$Metadata_get(param[1], dcbTagId))).length;
|
|
831
|
-
return tagCount > 1;
|
|
832
|
-
case "anyOf" :
|
|
833
|
-
return schema.anyOf.some(variantSchema => {
|
|
834
|
-
if (variantSchema.type !== "object") {
|
|
835
|
-
return false;
|
|
836
|
-
}
|
|
837
|
-
let tagCount = Object.entries(variantSchema.properties).filter(param => Stdlib_Option.isSome(Sury.$Metadata_get(param[1], dcbTagId))).length;
|
|
838
|
-
return tagCount > 1;
|
|
839
|
-
});
|
|
840
|
-
default:
|
|
841
|
-
return false;
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
function findMultiTagVariantNames(schema) {
|
|
846
|
-
let variantName = variantSchema => {
|
|
847
|
-
if (variantSchema.type !== "object") {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
let properties = variantSchema.properties;
|
|
851
|
-
let tagCount = Object.entries(properties).filter(param => Stdlib_Option.isSome(Sury.$Metadata_get(param[1], dcbTagId))).length;
|
|
852
|
-
if (tagCount > 1) {
|
|
853
|
-
return Stdlib_Option.getOr(variantTagName(properties), "(unknown)");
|
|
854
|
-
}
|
|
855
|
-
};
|
|
856
|
-
if (schema.type === "anyOf") {
|
|
857
|
-
return Stdlib_Array.filterMap(schema.anyOf, variantName);
|
|
858
|
-
}
|
|
859
|
-
let name = variantName(schema);
|
|
860
|
-
if (name !== undefined) {
|
|
861
|
-
return [name];
|
|
862
|
-
} else {
|
|
863
|
-
return [];
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
852
|
function extractCompositePartitionFieldsFromProperties(properties) {
|
|
868
853
|
return Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
869
854
|
let meta = Sury.$Metadata_get(param[1], dcbCompositePartitionMemberId);
|
|
@@ -918,98 +903,118 @@ function getCompositePartitionKeyValue(tags, spec) {
|
|
|
918
903
|
}).join("");
|
|
919
904
|
}
|
|
920
905
|
|
|
921
|
-
function
|
|
922
|
-
let
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
let
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
seen$1.add(f);
|
|
939
|
-
return true;
|
|
940
|
-
}
|
|
941
|
-
});
|
|
942
|
-
if (hasComposite && allPartitionFields.length !== 0) {
|
|
943
|
-
Stdlib_JsError.throwWithMessage(`DCB spec mixes @compositePartitionTag and @partitionTag — use one strategy per schema`);
|
|
906
|
+
function compositePartitionOf(schemas) {
|
|
907
|
+
let dedupe = (items, keyOf) => {
|
|
908
|
+
let seen = new Set();
|
|
909
|
+
return items.filter(item => {
|
|
910
|
+
let k = keyOf(item);
|
|
911
|
+
if (seen.has(k)) {
|
|
912
|
+
return false;
|
|
913
|
+
} else {
|
|
914
|
+
seen.add(k);
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
};
|
|
919
|
+
let compositeFields = dedupe(schemas.flatMap(extractCompositePartitionFields), info => info.name);
|
|
920
|
+
let partitionFields = dedupe(schemas.flatMap(extractPartitionTagFields), f => f);
|
|
921
|
+
if (compositeFields.length === 0) {
|
|
922
|
+
return;
|
|
944
923
|
}
|
|
945
|
-
if (
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
924
|
+
if (partitionFields.length !== 0) {
|
|
925
|
+
return Stdlib_JsError.throwWithMessage(`DCB spec mixes @compositePartitionTag and @partitionTag — use one strategy per schema`);
|
|
926
|
+
}
|
|
927
|
+
if (compositeFields.length === 1) {
|
|
928
|
+
return Stdlib_JsError.throwWithMessage(`@compositePartitionTag requires at least 2 annotated fields — only 1 found`);
|
|
929
|
+
}
|
|
930
|
+
let sorted = compositeFields.toSorted((a, b) => Primitive_int.compare(a.position, b.position));
|
|
931
|
+
return {
|
|
932
|
+
keys: sorted.map(info => info.name),
|
|
933
|
+
seps: sorted.slice(0, sorted.length - 1 | 0).map(info => info.sep)
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
function deriveBoundaryPartition(slices) {
|
|
938
|
+
let spec = compositePartitionOf(slices.map(s => s.eventSchema));
|
|
939
|
+
if (spec !== undefined) {
|
|
952
940
|
return {
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
941
|
+
partitionBySlice: {},
|
|
942
|
+
partitionTag: {
|
|
943
|
+
TAG: "Composite",
|
|
944
|
+
_0: spec
|
|
957
945
|
}
|
|
958
946
|
};
|
|
959
947
|
}
|
|
960
|
-
let
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
948
|
+
let resolution = DcbScopeInference$Reventless.resolvePartitions(slices.map(sliceShape));
|
|
949
|
+
if (resolution.ambiguities.length !== 0) {
|
|
950
|
+
Stdlib_JsError.throwWithMessage(`DCB partition key cannot be inferred — ` + resolution.ambiguities.map(param => param[0] + `: ` + param[1]).join(" | "));
|
|
951
|
+
}
|
|
952
|
+
let byEventType = {};
|
|
953
|
+
slices.forEach(s => {
|
|
954
|
+
let key = resolution.partitionBySlice[s.name];
|
|
955
|
+
Object.entries(extractTagKeysByEventType(s.eventSchema)).forEach(param => {
|
|
956
|
+
let tagKeys = param[1];
|
|
957
|
+
let eventType = param[0];
|
|
958
|
+
if (tagKeys.length !== 0 && !tagKeys.includes(key)) {
|
|
959
|
+
Stdlib_JsError.throwWithMessage(`DCB slice ` + s.name + ` is partitioned by ` + key + `, but its event ` + eventType + ` carries no ` + key + ` tag (it carries ` + tagKeys.join(", ") + `) — add ` + key + ` to the event, or declare the partition with @partitionTag`);
|
|
960
|
+
}
|
|
961
|
+
let other = byEventType[eventType];
|
|
962
|
+
if (other !== undefined && other !== key) {
|
|
963
|
+
return Stdlib_JsError.throwWithMessage(`DCB event ` + eventType + ` is written under two partition keys (` + other + `, ` + key + `) — every slice writing it must be partitioned by the same key`);
|
|
964
|
+
} else {
|
|
965
|
+
byEventType[eventType] = key;
|
|
966
|
+
return;
|
|
967
|
+
}
|
|
968
|
+
});
|
|
968
969
|
});
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
970
|
+
return {
|
|
971
|
+
partitionBySlice: resolution.partitionBySlice,
|
|
972
|
+
partitionTag: {
|
|
973
|
+
TAG: "ByEventType",
|
|
974
|
+
_0: byEventType
|
|
973
975
|
}
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
return Stdlib_JsError.throwWithMessage(`DCB spec has multiple fields annotated with @partitionTag (` + allPartitionFields.join(", ") + `) — only one is allowed — affected: ` + context);
|
|
986
|
-
} else {
|
|
987
|
-
return Stdlib_JsError.throwWithMessage(`DCB spec has variants with multiple tagged fields (` + allTaggedFields.join(", ") + `) but none is annotated with @partitionTag — affected: ` + context + ` — mark one field as the partition key`);
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
let singlePartition = allPartitionFields[0];
|
|
991
|
-
return {
|
|
976
|
+
};
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function slicePartitionTag(bp, sliceName) {
|
|
980
|
+
let other = bp.partitionTag;
|
|
981
|
+
switch (other.TAG) {
|
|
982
|
+
case "Simple" :
|
|
983
|
+
case "Composite" :
|
|
984
|
+
return other;
|
|
985
|
+
case "ByEventType" :
|
|
986
|
+
return Stdlib_Option.map(bp.partitionBySlice[sliceName], key => ({
|
|
992
987
|
TAG: "Simple",
|
|
993
988
|
_0: {
|
|
994
|
-
key:
|
|
989
|
+
key: key
|
|
995
990
|
}
|
|
996
|
-
};
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
991
|
+
}));
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function deriveSlicePartition(slice) {
|
|
996
|
+
let tag = slicePartitionTag(deriveBoundaryPartition([slice]), slice.name);
|
|
997
|
+
if (tag !== undefined) {
|
|
998
|
+
return tag;
|
|
999
|
+
} else {
|
|
1000
|
+
return Stdlib_JsError.throwWithMessage(`DCB slice ` + slice.name + ` has no partition key`);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function partitionValueOfTags(tags, pt) {
|
|
1005
|
+
switch (pt.TAG) {
|
|
1006
|
+
case "Simple" :
|
|
1007
|
+
let key = pt._0.key;
|
|
1008
|
+
return Stdlib_Option.getOr(Stdlib_Array.findMap(tags, t => {
|
|
1009
|
+
if (t.key === key) {
|
|
1010
|
+
return t.value;
|
|
1011
|
+
}
|
|
1012
|
+
}), "");
|
|
1013
|
+
case "Composite" :
|
|
1014
|
+
return getCompositePartitionKeyValue(tags, pt._0);
|
|
1015
|
+
case "ByEventType" :
|
|
1016
|
+
return "";
|
|
1005
1017
|
}
|
|
1006
|
-
let singleField = allTaggedFields[0];
|
|
1007
|
-
return {
|
|
1008
|
-
TAG: "Simple",
|
|
1009
|
-
_0: {
|
|
1010
|
-
key: singleField
|
|
1011
|
-
}
|
|
1012
|
-
};
|
|
1013
1018
|
}
|
|
1014
1019
|
|
|
1015
1020
|
function getPartitionTagValue(query, pt) {
|
|
@@ -1081,14 +1086,18 @@ export {
|
|
|
1081
1086
|
idFieldsOfProperties,
|
|
1082
1087
|
eventShapesOfSchema,
|
|
1083
1088
|
extractPartitionTagFields,
|
|
1089
|
+
chapterOfModuleUrl,
|
|
1084
1090
|
sliceShapeFromSchemas,
|
|
1091
|
+
sliceShape,
|
|
1085
1092
|
deriveEffectiveScope,
|
|
1086
|
-
hasMultiTagVariant,
|
|
1087
|
-
findMultiTagVariantNames,
|
|
1088
1093
|
extractCompositePartitionFieldsFromProperties,
|
|
1089
1094
|
extractCompositePartitionFields,
|
|
1090
1095
|
getCompositePartitionKeyValue,
|
|
1091
|
-
|
|
1096
|
+
compositePartitionOf,
|
|
1097
|
+
deriveBoundaryPartition,
|
|
1098
|
+
slicePartitionTag,
|
|
1099
|
+
deriveSlicePartition,
|
|
1100
|
+
partitionValueOfTags,
|
|
1092
1101
|
getPartitionTagValue,
|
|
1093
1102
|
}
|
|
1094
1103
|
/* tagSchema Not a pure module */
|
|
@@ -481,3 +481,53 @@ let validateScopeVsInference = (
|
|
|
481
481
|
)
|
|
482
482
|
{contradictions, redundancies}
|
|
483
483
|
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
Checks each `@partitionTag` against what inference derives with that hint removed.
|
|
487
|
+
|
|
488
|
+
- **Contradiction** — inference resolves the slice to a different key, or the
|
|
489
|
+
hint names a key the slice only reads as a reference to another entity. Storage,
|
|
490
|
+
fence and read scope would all follow the wrong key.
|
|
491
|
+
- **Redundant** — inference reaches the same key unaided; the annotation can go.
|
|
492
|
+
|
|
493
|
+
A hint that names a key the slice's events do not carry is not checked here:
|
|
494
|
+
inference ignores it, and the boundary derivation reports the slice instead.
|
|
495
|
+
*/
|
|
496
|
+
let validatePartitionHintsVsInference = (
|
|
497
|
+
~shapes: array<DcbScopeInference.sliceShape>,
|
|
498
|
+
): scopeInferenceIssues => {
|
|
499
|
+
let contradictions: array<validationError> = []
|
|
500
|
+
let redundancies: array<validationError> = []
|
|
501
|
+
shapes->Array.forEach(s =>
|
|
502
|
+
switch s.partitionHint {
|
|
503
|
+
| Some(hint) if DcbScopeInference.producedKeys(s)->Array.includes(hint) =>
|
|
504
|
+
let unaided = DcbScopeInference.resolvePartitions(
|
|
505
|
+
shapes->Array.map(o => o.sliceName == s.sliceName ? {...o, partitionHint: None} : o),
|
|
506
|
+
)
|
|
507
|
+
switch unaided.partitionBySlice->Dict.get(s.sliceName) {
|
|
508
|
+
| Some(inferred) if inferred == hint =>
|
|
509
|
+
redundancies->Array.push({
|
|
510
|
+
sliceName: s.sliceName,
|
|
511
|
+
message: `@partitionTag ${hint} is what inference derives without it — the annotation is redundant and can be removed.`,
|
|
512
|
+
})
|
|
513
|
+
| Some(inferred) =>
|
|
514
|
+
contradictions->Array.push({
|
|
515
|
+
sliceName: s.sliceName,
|
|
516
|
+
message: `@partitionTag names ${hint}, but inference derives ${inferred} from the slice graph — ${hint} is read from another entity. Remove the annotation, or move it to ${inferred}.`,
|
|
517
|
+
})
|
|
518
|
+
| None =>
|
|
519
|
+
let candidates = unaided.candidatesBySlice->Dict.get(s.sliceName)->Option.getOr([])
|
|
520
|
+
if !(candidates->Array.includes(hint)) {
|
|
521
|
+
contradictions->Array.push({
|
|
522
|
+
sliceName: s.sliceName,
|
|
523
|
+
message: `@partitionTag names ${hint}, which this slice only reads as a reference to another entity (candidates: ${candidates->Array.join(
|
|
524
|
+
", ",
|
|
525
|
+
)}). Move the annotation to the slice's own key.`,
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
| _ => ()
|
|
530
|
+
}
|
|
531
|
+
)
|
|
532
|
+
{contradictions, redundancies}
|
|
533
|
+
}
|
|
@@ -5,6 +5,7 @@ import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
|
5
5
|
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
6
|
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
7
|
import * as DcbTag$Reventless from "./DcbTag.res.mjs";
|
|
8
|
+
import * as DcbScopeInference$Reventless from "./DcbScopeInference.res.mjs";
|
|
8
9
|
|
|
9
10
|
function extractVariantInfo(variantSchema) {
|
|
10
11
|
switch (variantSchema.type) {
|
|
@@ -425,6 +426,55 @@ function validateScopeVsInference(annotations, inferred) {
|
|
|
425
426
|
};
|
|
426
427
|
}
|
|
427
428
|
|
|
429
|
+
function validatePartitionHintsVsInference(shapes) {
|
|
430
|
+
let contradictions = [];
|
|
431
|
+
let redundancies = [];
|
|
432
|
+
shapes.forEach(s => {
|
|
433
|
+
let hint = s.partitionHint;
|
|
434
|
+
if (hint === undefined) {
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
if (!DcbScopeInference$Reventless.producedKeys(s).includes(hint)) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
let unaided = DcbScopeInference$Reventless.resolvePartitions(shapes.map(o => {
|
|
441
|
+
if (o.sliceName !== s.sliceName) {
|
|
442
|
+
return o;
|
|
443
|
+
}
|
|
444
|
+
let newrecord = {...o};
|
|
445
|
+
newrecord.partitionHint = undefined;
|
|
446
|
+
return newrecord;
|
|
447
|
+
}));
|
|
448
|
+
let inferred = unaided.partitionBySlice[s.sliceName];
|
|
449
|
+
if (inferred !== undefined) {
|
|
450
|
+
if (inferred === hint) {
|
|
451
|
+
redundancies.push({
|
|
452
|
+
sliceName: s.sliceName,
|
|
453
|
+
message: `@partitionTag ` + hint + ` is what inference derives without it — the annotation is redundant and can be removed.`
|
|
454
|
+
});
|
|
455
|
+
} else {
|
|
456
|
+
contradictions.push({
|
|
457
|
+
sliceName: s.sliceName,
|
|
458
|
+
message: `@partitionTag names ` + hint + `, but inference derives ` + inferred + ` from the slice graph — ` + hint + ` is read from another entity. Remove the annotation, or move it to ` + inferred + `.`
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
let candidates = Stdlib_Option.getOr(unaided.candidatesBySlice[s.sliceName], []);
|
|
464
|
+
if (!candidates.includes(hint)) {
|
|
465
|
+
contradictions.push({
|
|
466
|
+
sliceName: s.sliceName,
|
|
467
|
+
message: `@partitionTag names ` + hint + `, which this slice only reads as a reference to another entity (candidates: ` + candidates.join(", ") + `). Move the annotation to the slice's own key.`
|
|
468
|
+
});
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
return {
|
|
473
|
+
contradictions: contradictions,
|
|
474
|
+
redundancies: redundancies
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
428
478
|
export {
|
|
429
479
|
extractVariantInfo,
|
|
430
480
|
extractAllVariants,
|
|
@@ -438,5 +488,6 @@ export {
|
|
|
438
488
|
validateProducedAndConsumed,
|
|
439
489
|
validateCrossPartitionScope,
|
|
440
490
|
validateScopeVsInference,
|
|
491
|
+
validatePartitionHintsVsInference,
|
|
441
492
|
}
|
|
442
493
|
/* DcbTag-Reventless Not a pure module */
|
|
@@ -638,7 +638,8 @@ let renderComposition = (
|
|
|
638
638
|
`name: ${stem}.name, ` ++
|
|
639
639
|
`commandSchema: ${stem}.commandSchema->S.castToUnknown, ` ++
|
|
640
640
|
`consumedEventSchema: ${stem}.consumedEventSchema->S.castToUnknown, ` ++
|
|
641
|
-
`eventSchema: ${stem}.eventSchema->S.castToUnknown` ++
|
|
641
|
+
`eventSchema: ${stem}.eventSchema->S.castToUnknown, ` ++
|
|
642
|
+
`moduleUrl: ${stem}.moduleUrl` ++ "},",
|
|
642
643
|
)
|
|
643
644
|
)
|
|
644
645
|
lines->Array.push("]")
|
|
@@ -406,7 +406,7 @@ function renderComposition(config, resolved, componentChapters, hasLifecycleMode
|
|
|
406
406
|
lines.push("");
|
|
407
407
|
lines.push("let dcbSliceSchemas: array<Reventless.DcbTag.sliceSchemas> = [");
|
|
408
408
|
resolved.stateChangeSlices.forEach(stem => {
|
|
409
|
-
lines.push(" {" + (`name: ` + stem + `.name, `) + (`commandSchema: ` + stem + `.commandSchema->S.castToUnknown, `) + (`consumedEventSchema: ` + stem + `.consumedEventSchema->S.castToUnknown, `) + (`eventSchema: ` + stem + `.eventSchema->S.castToUnknown`) + "},");
|
|
409
|
+
lines.push(" {" + (`name: ` + stem + `.name, `) + (`commandSchema: ` + stem + `.commandSchema->S.castToUnknown, `) + (`consumedEventSchema: ` + stem + `.consumedEventSchema->S.castToUnknown, `) + (`eventSchema: ` + stem + `.eventSchema->S.castToUnknown, `) + (`moduleUrl: ` + stem + `.moduleUrl`) + "},");
|
|
410
410
|
});
|
|
411
411
|
lines.push("]");
|
|
412
412
|
}
|
|
@@ -681,6 +681,90 @@ let deriveCommands = (
|
|
|
681
681
|
})
|
|
682
682
|
}
|
|
683
683
|
|
|
684
|
+
// ── What each command yields ────────────────────────────────────────────────
|
|
685
|
+
|
|
686
|
+
/** Which command yields which event or error, as its scenarios show it — what a
|
|
687
|
+
consumer draws a command's outcome arrows from, read off the corpus rather
|
|
688
|
+
than guessed from names. It needs no lifecycle labelling, so every corpus a
|
|
689
|
+
plugin has is read, though only its command scenarios count.
|
|
690
|
+
|
|
691
|
+
An empty `then` is skipped rather than read as "accepted, nothing happened".
|
|
692
|
+
The PPX currently writes `then: []` for `thenNoEvent`, so an empty `then` is
|
|
693
|
+
indistinguishable from one the sidecar could not read, and an outcome shown
|
|
694
|
+
here has to be one a scenario actually states. */
|
|
695
|
+
type shownOutcome = {
|
|
696
|
+
kind: string, // "event" | "error" | "noEvent"
|
|
697
|
+
/** The whole `then`, in order: `thenEvents([A, B])` is one outcome of two
|
|
698
|
+
events — both, not either. Empty for `noEvent`. */
|
|
699
|
+
names: array<string>,
|
|
700
|
+
/** The title of every scenario showing this outcome, in corpus order. */
|
|
701
|
+
scenarios: array<string>,
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
type commandOutcomes = {
|
|
705
|
+
plugin: string,
|
|
706
|
+
component: string,
|
|
707
|
+
command: string,
|
|
708
|
+
/** One per distinct outcome, in order of first appearance. */
|
|
709
|
+
outcomes: array<shownOutcome>,
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
let shownOutcomeOf = (s: scenario): option<(string, array<string>)> =>
|
|
713
|
+
switch s.thenKind {
|
|
714
|
+
| "event" | "error" => Some((s.thenKind, s.thenElements->Array.map(e => e.name)))
|
|
715
|
+
| "noEvent" => Some(("noEvent", []))
|
|
716
|
+
// The empty `then` above, and a `then` that is not about this command's
|
|
717
|
+
// result — a side effect, a published command.
|
|
718
|
+
| _ => None
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
let byPluginComponentCommand = (xs: array<commandOutcomes>): array<commandOutcomes> =>
|
|
722
|
+
xs->Array.toSorted((a, b) =>
|
|
723
|
+
switch String.compare(a.plugin, b.plugin) {
|
|
724
|
+
| 0. =>
|
|
725
|
+
switch String.compare(a.component, b.component) {
|
|
726
|
+
| 0. => String.compare(a.command, b.command)
|
|
727
|
+
| c => c
|
|
728
|
+
}
|
|
729
|
+
| c => c
|
|
730
|
+
}
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
/** A command with no readable outcome is left out: an entry must say something. */
|
|
734
|
+
let commandOutcomes = (~plugin: string, ~corpora: array<corpus>): array<commandOutcomes> => {
|
|
735
|
+
let entries: array<commandOutcomes> = []
|
|
736
|
+
corpora->Array.forEach(c =>
|
|
737
|
+
c.scenarios->Array.forEach(s =>
|
|
738
|
+
switch (s.whenKind, s.whenElements->Array.get(0), shownOutcomeOf(s)) {
|
|
739
|
+
| ("command", Some(command), Some((kind, names))) =>
|
|
740
|
+
let entry = switch entries->Array.find(
|
|
741
|
+
e => e.component == c.component && e.command == command.name,
|
|
742
|
+
) {
|
|
743
|
+
| Some(e) => e
|
|
744
|
+
| None =>
|
|
745
|
+
let e: commandOutcomes = {
|
|
746
|
+
plugin,
|
|
747
|
+
component: c.component,
|
|
748
|
+
command: command.name,
|
|
749
|
+
outcomes: [],
|
|
750
|
+
}
|
|
751
|
+
entries->Array.push(e)
|
|
752
|
+
e
|
|
753
|
+
}
|
|
754
|
+
switch entry.outcomes->Array.find(o => o.kind == kind && o.names == names) {
|
|
755
|
+
| Some(o) =>
|
|
756
|
+
if !(o.scenarios->Array.includes(s.title)) {
|
|
757
|
+
o.scenarios->Array.push(s.title)
|
|
758
|
+
}
|
|
759
|
+
| None => entry.outcomes->Array.push({kind, names, scenarios: [s.title]})
|
|
760
|
+
}
|
|
761
|
+
| _ => ()
|
|
762
|
+
}
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
entries->byPluginComponentCommand
|
|
766
|
+
}
|
|
767
|
+
|
|
684
768
|
// ── The three verdicts ──────────────────────────────────────────────────────
|
|
685
769
|
|
|
686
770
|
type finding = {
|
|
@@ -953,6 +1037,7 @@ let runPlugin = async (
|
|
|
953
1037
|
~pluginDir: string,
|
|
954
1038
|
~findings: array<finding>,
|
|
955
1039
|
~opaque: array<opaque>,
|
|
1040
|
+
~outcomes: array<commandOutcomes>,
|
|
956
1041
|
): result<array<derivedCommand>, string> =>
|
|
957
1042
|
switch await readDeclared(~pluginDir) {
|
|
958
1043
|
| Error(msg) => Error(msg)
|
|
@@ -962,6 +1047,8 @@ let runPlugin = async (
|
|
|
962
1047
|
readCorpus,
|
|
963
1048
|
)
|
|
964
1049
|
|
|
1050
|
+
outcomes->Array.pushMany(commandOutcomes(~plugin, ~corpora))
|
|
1051
|
+
|
|
965
1052
|
// Every corpus, not only the ones the walk goes on to use: the kinds that
|
|
966
1053
|
// are unreadable in full — extension points, automation and translation
|
|
967
1054
|
// slices — are exactly the ones that sit outside the view/writable folders
|
|
@@ -1131,13 +1218,16 @@ let goldenPath = (~root: appRoot) => NodePath.join([root.dir, "schema", "lifecyc
|
|
|
1131
1218
|
ever exercised, which is a sharper "missing scenario" than a verdict.
|
|
1132
1219
|
- `opaque` — the corpora the walk cannot read, so a consumer can say "not
|
|
1133
1220
|
analysed" where it would otherwise say "not covered".
|
|
1221
|
+
- `outcomes` — which command yields which event or error, and the scenarios
|
|
1222
|
+
that show it, sorted by plugin, component and command.
|
|
1134
1223
|
|
|
1135
|
-
`
|
|
1136
|
-
change. */
|
|
1224
|
+
`version` is the schema's own version, bumped when a consumer would have to
|
|
1225
|
+
change. An added section is not such a change. */
|
|
1137
1226
|
let reportJson = (
|
|
1138
1227
|
~findings: array<finding>,
|
|
1139
1228
|
~opaque: array<opaque>,
|
|
1140
1229
|
~derived: array<(string, derivedCommand)>,
|
|
1230
|
+
~outcomes: array<commandOutcomes>,
|
|
1141
1231
|
~failures: array<string>,
|
|
1142
1232
|
): string => {
|
|
1143
1233
|
let strs = xs => JSON.Encode.array(xs->Array.map(JSON.Encode.string))
|
|
@@ -1174,11 +1264,30 @@ let reportJson = (
|
|
|
1174
1264
|
("unreadable", JSON.Encode.int(o.unreadable)),
|
|
1175
1265
|
])
|
|
1176
1266
|
|
|
1267
|
+
let shownOutcomeJson = (o: shownOutcome) =>
|
|
1268
|
+
obj([
|
|
1269
|
+
("kind", JSON.Encode.string(o.kind)),
|
|
1270
|
+
("names", strs(o.names)),
|
|
1271
|
+
("scenarios", strs(o.scenarios)),
|
|
1272
|
+
])
|
|
1273
|
+
|
|
1274
|
+
let commandOutcomesJson = (e: commandOutcomes) =>
|
|
1275
|
+
obj([
|
|
1276
|
+
("plugin", JSON.Encode.string(e.plugin)),
|
|
1277
|
+
("component", JSON.Encode.string(e.component)),
|
|
1278
|
+
("command", JSON.Encode.string(e.command)),
|
|
1279
|
+
("outcomes", JSON.Encode.array(e.outcomes->Array.map(shownOutcomeJson))),
|
|
1280
|
+
])
|
|
1281
|
+
|
|
1177
1282
|
JSON.stringify(
|
|
1178
1283
|
obj([
|
|
1179
1284
|
("version", JSON.Encode.int(1)),
|
|
1180
1285
|
("findings", JSON.Encode.array(findings->Array.map(findingJson))),
|
|
1181
1286
|
("commands", JSON.Encode.array(derived->Array.map(commandJson))),
|
|
1287
|
+
(
|
|
1288
|
+
"outcomes",
|
|
1289
|
+
JSON.Encode.array(outcomes->byPluginComponentCommand->Array.map(commandOutcomesJson)),
|
|
1290
|
+
),
|
|
1182
1291
|
("opaque", JSON.Encode.array(opaque->Array.map(opaqueJson))),
|
|
1183
1292
|
("unreadable", strs(failures)),
|
|
1184
1293
|
]),
|
|
@@ -1270,6 +1379,7 @@ let writeOrCompare = (~path: string, ~actual: string, ~label: string, ~drifted:
|
|
|
1270
1379
|
let main = async () => {
|
|
1271
1380
|
let findings = []
|
|
1272
1381
|
let opaque = []
|
|
1382
|
+
let outcomes = []
|
|
1273
1383
|
let failures = []
|
|
1274
1384
|
let drifted = []
|
|
1275
1385
|
let allDerived = []
|
|
@@ -1327,7 +1437,7 @@ let main = async () => {
|
|
|
1327
1437
|
| Some(pluginDir) =>
|
|
1328
1438
|
let plugin = NodePath.basename(pluginDir)
|
|
1329
1439
|
let qualified = `${example}/${plugin}`
|
|
1330
|
-
switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque) {
|
|
1440
|
+
switch await runPlugin(~plugin=qualified, ~pluginDir, ~findings, ~opaque, ~outcomes) {
|
|
1331
1441
|
| Ok(commands) =>
|
|
1332
1442
|
commands->Array.forEach(c => {
|
|
1333
1443
|
derived->Array.push(c)
|
|
@@ -1370,7 +1480,7 @@ let main = async () => {
|
|
|
1370
1480
|
let contradicted = of_("contradicted")
|
|
1371
1481
|
|
|
1372
1482
|
if json {
|
|
1373
|
-
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~failures))
|
|
1483
|
+
Console.log(reportJson(~findings, ~opaque, ~derived=allDerived, ~outcomes, ~failures))
|
|
1374
1484
|
} else {
|
|
1375
1485
|
["contradicted", "unverified", "undeclared", "level", "ambiguous"]->Array.forEach(severity => {
|
|
1376
1486
|
let group = of_(severity)
|