@hyperscale0/udl 2.4.0 → 2.6.0

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/src/schema.ts CHANGED
@@ -909,6 +909,20 @@ const udlRequestAuthoritySchema = z.strictObject({
909
909
  expiresField: udlFieldNameSchema,
910
910
  });
911
911
 
912
+ const udlCascadeEntrySchema = z.union([
913
+ z.strictObject({
914
+ instrumentId: udlInstrumentIdSchema,
915
+ action: udlActionNameSchema,
916
+ inputField: udlFieldNameSchema,
917
+ }),
918
+ z.strictObject({
919
+ instrumentId: udlInstrumentIdSchema,
920
+ action: udlActionNameSchema,
921
+ refField: udlFieldNameSchema,
922
+ statuses: z.array(udlSnakeCaseSchema).min(1),
923
+ }),
924
+ ]);
925
+
912
926
  const udlActionShape = {
913
927
  requestAuthority: udlRequestAuthoritySchema.optional(),
914
928
  funding: udlFundingSchema.optional(),
@@ -954,6 +968,7 @@ const udlActionShape = {
954
968
  .min(1)
955
969
  .max(16)
956
970
  .optional(),
971
+ cascade: z.array(udlCascadeEntrySchema).min(1).max(8).optional(),
957
972
  agentDescription: udlAgentDescriptionSchema.optional(),
958
973
  calls: z.array(udlCallSchema).min(1).optional(),
959
974
  captureInput: z.record(udlFieldNameSchema, udlFieldNameSchema).optional(),
@@ -1217,6 +1232,19 @@ export const udlClauseVocabulary = [
1217
1232
  },
1218
1233
  ],
1219
1234
  },
1235
+ {
1236
+ cardinality: "many",
1237
+ scope: "action",
1238
+ spelling: "cascade",
1239
+ target: "cascade",
1240
+ effects: [
1241
+ {
1242
+ kind: "decides",
1243
+ per: "element",
1244
+ signature: { fixed: "cascade_transition" },
1245
+ },
1246
+ ],
1247
+ },
1220
1248
  {
1221
1249
  cardinality: "one",
1222
1250
  literalKeys: true,
@@ -1893,3 +1921,4 @@ export type UdlActionLibraryModule = z.infer<
1893
1921
  typeof udlActionLibraryModuleSchema
1894
1922
  >;
1895
1923
  export type UdlActionLibrary = z.infer<typeof udlActionLibrarySchema>;
1924
+ export type UdlCascadeEntry = z.infer<typeof udlCascadeEntrySchema>;
package/src/validation.ts CHANGED
@@ -5509,7 +5509,8 @@ const sealedFormatValidators: Readonly<
5509
5509
  Record<string, (value: string) => boolean>
5510
5510
  > = {
5511
5511
  "hyperscale-date": (value) => z.iso.date().safeParse(value).success,
5512
- "hyperscale-date-time": (value) => z.iso.datetime().safeParse(value).success,
5512
+ "hyperscale-date-time": (value) =>
5513
+ z.iso.datetime({ offset: true }).safeParse(value).success,
5513
5514
  "hyperscale-email": (value) => z.email().safeParse(value).success,
5514
5515
  "hyperscale-uri": (value) => z.url().safeParse(value).success,
5515
5516
  };
package/src/vocabulary.ts CHANGED
@@ -32,6 +32,12 @@ const mutable = (instrument: UdlInstrument, field: string) =>
32
32
  Object.values(instrument.actions).some((action) =>
33
33
  action.updates?.includes(field),
34
34
  );
35
+ function instrumentInstanceKey(instrumentId: string): string {
36
+ const camel = instrumentId.replaceAll(/_([a-z])/g, (_, letter: string) =>
37
+ letter.toUpperCase(),
38
+ );
39
+ return `${camel}Id`;
40
+ }
35
41
 
36
42
  /** Validate product laws independently of any instrument catalogue. */
37
43
  /** The children buckets of an allocation owner that select this instrument or its template alias. */
@@ -952,6 +958,127 @@ export function validateVocabulary(
952
958
  outgoing.push(`${target.id}.${transition.action}`);
953
959
  }
954
960
  edges.set(`${instrument.id}.${name}`, outgoing);
961
+ if (action.cascade) {
962
+ const ap = [...path, "cascade"];
963
+ if (name === "create" || action.decision) {
964
+ add(
965
+ ap,
966
+ "cascade is not allowed on create or actions with a decision",
967
+ );
968
+ }
969
+ const seenInputFields = new Set<string>();
970
+ for (const [i, entry] of action.cascade.entries()) {
971
+ const ep = [...ap, i];
972
+ const hasInput =
973
+ "inputField" in entry && entry.inputField !== undefined;
974
+ const hasRef = "refField" in entry && entry.refField !== undefined;
975
+ if ((!hasInput && !hasRef) || (hasInput && hasRef)) {
976
+ add(
977
+ ep,
978
+ "cascade must specify exactly one of inputField or refField",
979
+ );
980
+ }
981
+ const target = byId.get(entry.instrumentId);
982
+ if (!target) {
983
+ add(
984
+ ep,
985
+ "cascade target instrument must be declared in the document",
986
+ );
987
+ } else {
988
+ const targetAction = target.actions[entry.action];
989
+ const targetTransition = target.lifecycle.transitions[entry.action];
990
+ if (
991
+ !targetAction ||
992
+ !targetTransition ||
993
+ entry.action === "create"
994
+ ) {
995
+ add(
996
+ ep,
997
+ "cascade target action must exist in lifecycle and cannot be create",
998
+ );
999
+ } else {
1000
+ if (targetAction.engineOwned) {
1001
+ add(ep, "cascade target action cannot be engine-owned");
1002
+ }
1003
+ if (targetAction.cascade) {
1004
+ add(
1005
+ ep,
1006
+ "cascade target action cannot have a cascade of its own",
1007
+ );
1008
+ }
1009
+ if (targetAction.decision) {
1010
+ add(ep, "cascade target action cannot have a decision");
1011
+ }
1012
+ if (targetAction.port && !action.port) {
1013
+ add(
1014
+ ep,
1015
+ `cascade target action ${entry.action} declares a port; the parent action ${name} must declare a port so the actor is forwarded`,
1016
+ );
1017
+ }
1018
+ if (
1019
+ Array.isArray(targetAction.input?.required) &&
1020
+ targetAction.input.required.length > 0
1021
+ ) {
1022
+ add(ep, "cascade target action cannot have required inputs");
1023
+ }
1024
+ }
1025
+ if ("refField" in entry && entry.refField !== undefined) {
1026
+ if (!entry.statuses || entry.statuses.length === 0) {
1027
+ add(ep, "cascade with refField requires statuses");
1028
+ }
1029
+ const targetRef = reference(target, entry.refField, ep);
1030
+ if (targetRef && targetRef.id !== instrument.id) {
1031
+ add(
1032
+ ep,
1033
+ "cascade target refField must reference this instrument",
1034
+ );
1035
+ }
1036
+ if (targetTransition) {
1037
+ for (const status of entry.statuses ?? []) {
1038
+ if (!targetTransition.from.includes(status)) {
1039
+ add(
1040
+ ep,
1041
+ `cascade status ${status} is not an allowed from-state for ${target.id}.${entry.action}`,
1042
+ );
1043
+ }
1044
+ }
1045
+ }
1046
+ }
1047
+ }
1048
+ if ("inputField" in entry && entry.inputField !== undefined) {
1049
+ const reservedFields = new Set([
1050
+ "tenantId",
1051
+ "productId",
1052
+ "actorAccountId",
1053
+ instrumentInstanceKey(instrument.id),
1054
+ ]);
1055
+ if (reservedFields.has(entry.inputField)) {
1056
+ add(
1057
+ ep,
1058
+ `cascade inputField ${entry.inputField} collides with a reserved field`,
1059
+ );
1060
+ }
1061
+ if (Object.hasOwn(instrument.fields, entry.inputField)) {
1062
+ add(
1063
+ ep,
1064
+ `cascade inputField ${entry.inputField} collides with an instrument field`,
1065
+ );
1066
+ }
1067
+ if (
1068
+ Object.hasOwn(object(action.input?.properties), entry.inputField)
1069
+ ) {
1070
+ add(
1071
+ ep,
1072
+ `cascade inputField ${entry.inputField} collides with an action input property`,
1073
+ );
1074
+ }
1075
+ if (seenInputFields.has(entry.inputField)) {
1076
+ add(ep, `cascade inputField ${entry.inputField} is repeated`);
1077
+ }
1078
+ seenInputFields.add(entry.inputField);
1079
+ }
1080
+ }
1081
+ }
955
1082
  if (action.allocate || action.contributionStage) {
956
1083
  if (
957
1084
  name === "create" ||