@hyperscale0/udl 2.5.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.
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.5.0. Edit the source, not llms-full.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.6.0. Edit the source, not llms-full.txt. -->
2
2
 
3
3
  # UDL complete reference
4
4
 
@@ -266,6 +266,8 @@ The engine proves the advance and supplies trusted paid/consumed facts to the pl
266
266
 
267
267
  `transitionsRefs` invokes named noncash lifecycle actions on referenced instruments in the same transaction. The engine applies every target gate, authority check and lifecycle precondition. Cycles, repeated target actions and cash-bearing or input-dependent target actions refuse. A child can lapse its cover; a refund can cancel a referenced funding record and prevent future actions through that record's lifecycle gates. This does not replace durable approval consumption tied to a downstream operation.
268
268
 
269
+ `cascade` executes a named lifecycle action on linked instances of another instrument inside the same transaction before evaluating the parent action's own admission gates. Target instances are identified either through caller input IDs via `inputField` or by matching a parent reference on target rows via `refField` filtered by lifecycle statuses. The cascading action cannot be create and cannot declare a decision. The target action must exist in lifecycle, cannot be create or engine-owned, cannot declare a decision, and cannot declare a cascade of its own. Because the cascade runs before the parent's admission gates, child status transitions take effect in time for parent aggregate requirements.
270
+
269
271
  `requiresRefs.unique` accepts `{namespace, byFields}`. The uniqueness key contains the Product, namespace, referenced instance identity and immutable key values. It excludes instrument aliases and current status. A waived or refunded fine still occupies its key; cost uses a different kind value. The engine enforces this key under a shared constraint or lock.
270
272
 
271
273
  Decision authentication, attestation verification and request hashing are admission concerns. The decision record must carry the requested instrument kind, instance, action, canonical input digest, deciding role and actor, expiry, verdict and evidence references. The engine computes the digest from the admitted request, authenticates the authority, checks expiry and revocation under locks, and binds consumption to the durable operation identity. Submitted actor and operation fields are not proof.
@@ -611,6 +613,28 @@ This page lists every entry in `udlClauseVocabulary`. The examples are copied fr
611
613
  ]
612
614
  ```
613
615
 
616
+ ## cascade
617
+
618
+ - Scope: action
619
+ - UDL target: `cascade`
620
+ - Cardinality: many
621
+ - Linear outputs: none
622
+ - Effects: decides.cascade_transition per element
623
+ - Law: Requirements-as-data law and closure law
624
+ - Conformance source: `vocabulary.udl`
625
+
626
+ A cascade target action that declares a `port` is allowed only when the parent action declares a `port`: the engine forwards the port actor.
627
+
628
+ ```json
629
+ [
630
+ {
631
+ "action": "close",
632
+ "inputField": "sliceIds",
633
+ "instrumentId": "slice"
634
+ }
635
+ ]
636
+ ```
637
+
614
638
  ## action library
615
639
 
616
640
  - Scope: instrument
package/docs/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.5.0. Edit the source, not llms.txt. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.6.0. Edit the source, not llms.txt. -->
2
2
 
3
3
  # UDL documentation
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.5.0. Edit the source, not clauses.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.6.0. Edit the source, not clauses.md. -->
2
2
 
3
3
  # Clause reference
4
4
 
@@ -254,6 +254,28 @@ This page lists every entry in `udlClauseVocabulary`. The examples are copied fr
254
254
  ]
255
255
  ```
256
256
 
257
+ ## cascade
258
+
259
+ - Scope: action
260
+ - UDL target: `cascade`
261
+ - Cardinality: many
262
+ - Linear outputs: none
263
+ - Effects: decides.cascade_transition per element
264
+ - Law: Requirements-as-data law and closure law
265
+ - Conformance source: `vocabulary.udl`
266
+
267
+ A cascade target action that declares a `port` is allowed only when the parent action declares a `port`: the engine forwards the port actor.
268
+
269
+ ```json
270
+ [
271
+ {
272
+ "action": "close",
273
+ "inputField": "sliceIds",
274
+ "instrumentId": "slice"
275
+ }
276
+ ]
277
+ ```
278
+
257
279
  ## action library
258
280
 
259
281
  - Scope: instrument
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.5.0. Edit the source, not cli.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.6.0. Edit the source, not cli.md. -->
2
2
 
3
3
  # Command reference
4
4
 
@@ -1,4 +1,4 @@
1
- <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.5.0. Edit the source, not diagnostics.md. -->
1
+ <!-- Generated by scripts/docs/build.ts from @hyperscale0/udl 2.6.0. Edit the source, not diagnostics.md. -->
2
2
 
3
3
  # Diagnostic reference
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperscale0/udl",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "The Universal Domain Language: format spec, parser, validator, canonical serializer, and evolution diff.",
5
5
  "keywords": [
6
6
  "udl",
@@ -83,5 +83,5 @@
83
83
  "Amir Ayub",
84
84
  "Sara AlBakaawi"
85
85
  ],
86
- "gitHead": "a537ff33ff29340cc2849ea049df4d65889c5d5c"
86
+ "gitHead": "cd1e1ff5c188de94ff8bb0761cb3258746196bf2"
87
87
  }
@@ -1719,6 +1719,68 @@
1719
1719
  "additionalProperties": false
1720
1720
  }
1721
1721
  },
1722
+ "cascade": {
1723
+ "minItems": 1,
1724
+ "maxItems": 8,
1725
+ "type": "array",
1726
+ "items": {
1727
+ "anyOf": [
1728
+ {
1729
+ "type": "object",
1730
+ "properties": {
1731
+ "instrumentId": {
1732
+ "type": "string",
1733
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$",
1734
+ "x-udl-reference": "instrument"
1735
+ },
1736
+ "action": {
1737
+ "type": "string",
1738
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
1739
+ },
1740
+ "inputField": {
1741
+ "type": "string",
1742
+ "pattern": "^[a-z][A-Za-z0-9]*$"
1743
+ }
1744
+ },
1745
+ "required": ["instrumentId", "action", "inputField"],
1746
+ "additionalProperties": false
1747
+ },
1748
+ {
1749
+ "type": "object",
1750
+ "properties": {
1751
+ "instrumentId": {
1752
+ "type": "string",
1753
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$",
1754
+ "x-udl-reference": "instrument"
1755
+ },
1756
+ "action": {
1757
+ "type": "string",
1758
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
1759
+ },
1760
+ "refField": {
1761
+ "type": "string",
1762
+ "pattern": "^[a-z][A-Za-z0-9]*$"
1763
+ },
1764
+ "statuses": {
1765
+ "minItems": 1,
1766
+ "type": "array",
1767
+ "items": {
1768
+ "type": "string",
1769
+ "pattern": "^[a-z][a-z0-9]*(?:_[a-z0-9]+)*$"
1770
+ }
1771
+ }
1772
+ },
1773
+ "required": [
1774
+ "instrumentId",
1775
+ "action",
1776
+ "refField",
1777
+ "statuses"
1778
+ ],
1779
+ "additionalProperties": false
1780
+ }
1781
+ ]
1782
+ }
1783
+ },
1722
1784
  "agentDescription": {
1723
1785
  "type": "string",
1724
1786
  "maxLength": 400
package/src/evolution.ts CHANGED
@@ -80,6 +80,8 @@ export interface EvolutionActionSnapshot {
80
80
  readonly setsAt: unknown;
81
81
  /** Missing on snapshots written before signed child sums existed. */
82
82
  readonly signedSum?: unknown;
83
+ /** Missing on snapshots written before cascade transitions existed. */
84
+ readonly cascade?: unknown;
83
85
  readonly moves: readonly EvolutionMoveSnapshot[];
84
86
  readonly steps: readonly EvolutionStepSnapshot[];
85
87
  }
@@ -578,6 +580,9 @@ function snapshotUdlAction(definition: UdlAction): EvolutionActionSnapshot {
578
580
  publicAction: definition.publicAction ?? null,
579
581
  setsAt: definition.setsAt ?? null,
580
582
  signedSum: definition.signedSum ?? null,
583
+ cascade: definition.cascade
584
+ ? definition.cascade.map((entry) => ({ ...entry }))
585
+ : null,
581
586
  moves: definition.moves.map((move) => ({
582
587
  bind: move.bind,
583
588
  capture: move.capture ?? {},
@@ -887,6 +892,12 @@ function diffActions(
887
892
  ) {
888
893
  violations.push(`action ${action} changed its signed child sum`);
889
894
  }
895
+ if (
896
+ descriptor.cascade !== undefined &&
897
+ stableStringify(descriptor.cascade) !== stableStringify(current.cascade)
898
+ ) {
899
+ violations.push(`action ${action} changed its cascade rules`);
900
+ }
890
901
  }
891
902
  for (const [action, descriptor] of Object.entries(next)) {
892
903
  if (previous[action] || descriptor.payout == null) continue;
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/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" ||