@hyperscale0/udl 2.6.1 → 3.0.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.
Files changed (213) hide show
  1. package/CHANGELOG.md +2 -330
  2. package/LICENSING.md +1 -2
  3. package/README.md +3 -114
  4. package/TRADEMARKS.md +2 -2
  5. package/dist/diagnostics.d.ts +21 -201
  6. package/dist/diagnostics.d.ts.map +1 -1
  7. package/dist/diagnostics.js +29 -193
  8. package/dist/diagnostics.js.map +1 -1
  9. package/dist/evolution.d.ts +3 -132
  10. package/dist/evolution.d.ts.map +1 -1
  11. package/dist/evolution.js +29 -633
  12. package/dist/evolution.js.map +1 -1
  13. package/dist/finance.d.ts +5 -72
  14. package/dist/finance.d.ts.map +1 -1
  15. package/dist/finance.js +233 -737
  16. package/dist/finance.js.map +1 -1
  17. package/dist/index.d.ts +7 -18
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -12
  20. package/dist/index.js.map +1 -1
  21. package/dist/instrument-references.d.ts.map +1 -1
  22. package/dist/instrument-references.js +3 -2
  23. package/dist/instrument-references.js.map +1 -1
  24. package/dist/schema.d.ts +2079 -4564
  25. package/dist/schema.d.ts.map +1 -1
  26. package/dist/schema.js +270 -1609
  27. package/dist/schema.js.map +1 -1
  28. package/dist/validation.d.ts +8 -104
  29. package/dist/validation.d.ts.map +1 -1
  30. package/dist/validation.js +653 -3271
  31. package/dist/validation.js.map +1 -1
  32. package/docs/README.md +147 -14
  33. package/package.json +7 -11
  34. package/spec/README.md +151 -164
  35. package/spec/darb.udl.json +249 -0
  36. package/spec/udl.schema.json +4104 -3202
  37. package/src/diagnostics.ts +51 -246
  38. package/src/evolution.ts +39 -1045
  39. package/src/finance.ts +287 -1127
  40. package/src/index.ts +18 -142
  41. package/src/instrument-references.ts +3 -2
  42. package/src/schema.ts +285 -1843
  43. package/src/validation.ts +870 -5611
  44. package/conformance/README.md +0 -82
  45. package/conformance/evolution/action-contract.expected.json +0 -10
  46. package/conformance/evolution/action-contract.live.udl +0 -44
  47. package/conformance/evolution/action-contract.next.udl +0 -45
  48. package/conformance/evolution/product-identity.expected.json +0 -10
  49. package/conformance/evolution/product-identity.live.udl +0 -55
  50. package/conformance/evolution/product-identity.next.udl +0 -55
  51. package/conformance/evolution/version-required.expected.json +0 -10
  52. package/conformance/evolution/version-required.live.udl +0 -55
  53. package/conformance/evolution/version-required.next.udl +0 -58
  54. package/conformance/invalid/action-without-transition.expected.json +0 -10
  55. package/conformance/invalid/action-without-transition.udl +0 -60
  56. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  57. package/conformance/invalid/agent-description-too-long.udl +0 -57
  58. package/conformance/invalid/blank-title.expected.json +0 -10
  59. package/conformance/invalid/blank-title.udl +0 -54
  60. package/conformance/invalid/call-binds-results.expected.json +0 -10
  61. package/conformance/invalid/call-binds-results.udl +0 -314
  62. package/conformance/invalid/call-unknown-action.expected.json +0 -10
  63. package/conformance/invalid/call-unknown-action.udl +0 -314
  64. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  65. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  66. package/conformance/invalid/depth-budget.expected.json +0 -10
  67. package/conformance/invalid/depth-budget.udl +0 -49
  68. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  69. package/conformance/invalid/duplicate-subject.udl +0 -230
  70. package/conformance/invalid/forged-effects.expected.json +0 -10
  71. package/conformance/invalid/forged-effects.udl +0 -76
  72. package/conformance/invalid/format-version.expected.json +0 -10
  73. package/conformance/invalid/format-version.udl +0 -54
  74. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  75. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  76. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  77. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  78. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  79. package/conformance/invalid/invalid-check-duration.udl +0 -219
  80. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  81. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  82. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  83. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  84. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  85. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  86. package/conformance/invalid/invalid-journeys.expected.json +0 -10
  87. package/conformance/invalid/invalid-journeys.udl +0 -77
  88. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  89. package/conformance/invalid/invalid-remainder.udl +0 -220
  90. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  91. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  92. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  93. package/conformance/invalid/invalid-utf8.udl +0 -1
  94. package/conformance/invalid/leaf-effect-mismatch.expected.json +0 -10
  95. package/conformance/invalid/leaf-effect-mismatch.udl +0 -314
  96. package/conformance/invalid/malformed-json.expected.json +0 -10
  97. package/conformance/invalid/malformed-json.udl +0 -1
  98. package/conformance/invalid/missing-create-action.expected.json +0 -10
  99. package/conformance/invalid/missing-create-action.udl +0 -48
  100. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  101. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  102. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  103. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  104. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  105. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  106. package/conformance/invalid/not-an-object.expected.json +0 -10
  107. package/conformance/invalid/not-an-object.udl +0 -1
  108. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  109. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  110. package/conformance/invalid/piece-plan-without-partition.expected.json +0 -10
  111. package/conformance/invalid/piece-plan-without-partition.udl +0 -305
  112. package/conformance/invalid/private-action-independent-approval.expected.json +0 -10
  113. package/conformance/invalid/private-action-independent-approval.udl +0 -314
  114. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  115. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  116. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  117. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  118. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  119. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  120. package/conformance/invalid/unfund-order-not-reversed.expected.json +0 -10
  121. package/conformance/invalid/unfund-order-not-reversed.udl +0 -314
  122. package/conformance/invalid/unknown-key.expected.json +0 -10
  123. package/conformance/invalid/unknown-key.udl +0 -55
  124. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  125. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  126. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  127. package/conformance/invalid/unknown-required-field.udl +0 -220
  128. package/conformance/invalid/unreachable-state.expected.json +0 -10
  129. package/conformance/invalid/unreachable-state.udl +0 -55
  130. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  131. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  132. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  133. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  134. package/conformance/valid/agent-description.expected.json +0 -6
  135. package/conformance/valid/agent-description.udl +0 -66
  136. package/conformance/valid/attested.expected.json +0 -6
  137. package/conformance/valid/attested.udl +0 -251
  138. package/conformance/valid/cards.expected.json +0 -6
  139. package/conformance/valid/cards.udl +0 -1579
  140. package/conformance/valid/commerce-escrow.expected.json +0 -6
  141. package/conformance/valid/commerce-escrow.udl +0 -1512
  142. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  143. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  144. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  145. package/conformance/valid/compiled-watch-club.udl +0 -2486
  146. package/conformance/valid/complete-contract.expected.json +0 -6
  147. package/conformance/valid/complete-contract.udl +0 -218
  148. package/conformance/valid/effect-signatures.expected.json +0 -6
  149. package/conformance/valid/effect-signatures.udl +0 -75
  150. package/conformance/valid/hand-edited.expected.json +0 -6
  151. package/conformance/valid/hand-edited.udl +0 -1
  152. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  153. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  154. package/conformance/valid/insured-travel.expected.json +0 -6
  155. package/conformance/valid/insured-travel.udl +0 -3469
  156. package/conformance/valid/minimal.expected.json +0 -6
  157. package/conformance/valid/minimal.udl +0 -62
  158. package/conformance/valid/piece-plan-calls.expected.json +0 -6
  159. package/conformance/valid/piece-plan-calls.udl +0 -314
  160. package/conformance/valid/protection.expected.json +0 -6
  161. package/conformance/valid/protection.udl +0 -1551
  162. package/conformance/valid/string-escaping.expected.json +0 -6
  163. package/conformance/valid/string-escaping.udl +0 -54
  164. package/conformance/valid/vocabulary.expected.json +0 -6
  165. package/conformance/valid/vocabulary.udl +0 -2012
  166. package/dist/allocation.d.ts +0 -60
  167. package/dist/allocation.d.ts.map +0 -1
  168. package/dist/allocation.js +0 -177
  169. package/dist/allocation.js.map +0 -1
  170. package/dist/check-profiles.d.ts +0 -57
  171. package/dist/check-profiles.d.ts.map +0 -1
  172. package/dist/check-profiles.js +0 -62
  173. package/dist/check-profiles.js.map +0 -1
  174. package/dist/distribution.d.ts +0 -15
  175. package/dist/distribution.d.ts.map +0 -1
  176. package/dist/distribution.js +0 -49
  177. package/dist/distribution.js.map +0 -1
  178. package/dist/effects.d.ts +0 -58
  179. package/dist/effects.d.ts.map +0 -1
  180. package/dist/effects.js +0 -1126
  181. package/dist/effects.js.map +0 -1
  182. package/dist/reference.d.ts +0 -3
  183. package/dist/reference.d.ts.map +0 -1
  184. package/dist/reference.js +0 -28
  185. package/dist/reference.js.map +0 -1
  186. package/dist/vocabulary.d.ts +0 -23
  187. package/dist/vocabulary.d.ts.map +0 -1
  188. package/dist/vocabulary.js +0 -1052
  189. package/dist/vocabulary.js.map +0 -1
  190. package/docs/funding-custody.md +0 -165
  191. package/docs/guide/01-a-document.md +0 -37
  192. package/docs/guide/02-money-steps.md +0 -23
  193. package/docs/guide/03-laws.md +0 -18
  194. package/docs/guide/04-fees-and-remainder.md +0 -36
  195. package/docs/guide/05-checks-updates-dials.md +0 -7
  196. package/docs/guide/06-effects.md +0 -11
  197. package/docs/guide/07-evolution.md +0 -11
  198. package/docs/guide/08-implementing.md +0 -13
  199. package/docs/guide/09-schedules-and-allocation.md +0 -132
  200. package/docs/llms-full.txt +0 -2008
  201. package/docs/llms.txt +0 -14
  202. package/docs/piece-plans.md +0 -148
  203. package/docs/reference/canonical.md +0 -16
  204. package/docs/reference/clauses.md +0 -1585
  205. package/docs/reference/cli.md +0 -24
  206. package/docs/reference/diagnostics.md +0 -38
  207. package/skills/udl/SKILL.md +0 -28
  208. package/src/allocation.ts +0 -259
  209. package/src/check-profiles.ts +0 -80
  210. package/src/distribution.ts +0 -61
  211. package/src/effects.ts +0 -1920
  212. package/src/reference.ts +0 -31
  213. package/src/vocabulary.ts +0 -1635
package/src/effects.ts DELETED
@@ -1,1920 +0,0 @@
1
- import { issue, type UdlIssue } from "./diagnostics.js";
2
- import { UDL_LIMITS } from "./limits.js";
3
- import {
4
- udlClauseVocabulary,
5
- udlEffectKinds,
6
- type UdlEffectKind,
7
- type UdlBinding,
8
- type UdlCall,
9
- type UdlInstrument,
10
- type UdlMove,
11
- type UdlPiece,
12
- type UdlPrivateAction,
13
- type UdlPrivateParameterKind,
14
- type UdlStep,
15
- } from "./schema.js";
16
-
17
- export { udlEffectKinds, type UdlEffectKind } from "./schema.js";
18
-
19
- type EffectDescriptor = {
20
- readonly kind: UdlEffectKind;
21
- readonly per: "clause" | "element";
22
- readonly signature:
23
- | { readonly fixed: string }
24
- | { readonly fromField: string }
25
- | { readonly movementClass: true };
26
- };
27
-
28
- type ActionClauseDescriptor = {
29
- readonly effects?: readonly EffectDescriptor[];
30
- readonly scope: "action" | "instrument";
31
- readonly target: string;
32
- };
33
-
34
- type EffectRow = {
35
- readonly channel?: string;
36
- readonly role?: string;
37
- readonly signature: string;
38
- readonly source: string;
39
- };
40
-
41
- export type DerivedUdlEffects = Partial<
42
- Readonly<Record<UdlEffectKind, readonly EffectRow[]>>
43
- >;
44
-
45
- export type UdlMovementClass =
46
- | "collection.pay_in"
47
- | "deposit.attributed"
48
- | "payout.external"
49
- | "transfer.internal";
50
-
51
- interface Movement {
52
- readonly bind?: Readonly<Record<string, unknown>>;
53
- readonly operation: string;
54
- }
55
-
56
- function boundPath(move: Movement, endpoint: string): string | undefined {
57
- const binding = move.bind?.[endpoint];
58
- if (binding === null || typeof binding !== "object") return undefined;
59
- const path = (binding as { readonly path?: unknown }).path;
60
- return typeof path === "string" ? path : undefined;
61
- }
62
-
63
- /**
64
- * Maps a UDL money operation to the meter and pricing class that owns it.
65
- * Internal-transfer endpoint bindings are product accounts by construction.
66
- * External collection, deposit, and payout operations carry their role in the
67
- * operation family because their remote endpoint is not a UDL account binding.
68
- */
69
- /**
70
- * The typed kind of an instance ref a call binding may name. Refs exist only
71
- * through step and move captures on the same instrument; a capture of a leaf's
72
- * `accountId` output is an account, every other capture is opaque text.
73
- */
74
- function capturedRefKind(
75
- instrument: UdlInstrument,
76
- refName: string,
77
- ): "account" | "text" | undefined {
78
- for (const action of Object.values(instrument.actions)) {
79
- for (const step of [...action.steps, ...action.moves]) {
80
- const output = step.capture?.[refName];
81
- if (output !== undefined) {
82
- return output === "accountId" ? "account" : "text";
83
- }
84
- }
85
- }
86
- return undefined;
87
- }
88
-
89
- export function movementClass(move: Movement): UdlMovementClass {
90
- if (move.operation.startsWith("internal_transfer.")) {
91
- const source = boundPath(move, "sourceAccountId");
92
- const destination = boundPath(move, "destinationAccountId");
93
- if (
94
- source?.startsWith("fields.") &&
95
- destination === "refs.escrowAccountId"
96
- ) {
97
- return "collection.pay_in";
98
- }
99
- return "transfer.internal";
100
- }
101
- if (move.operation.startsWith("collection.pay_in.")) {
102
- return "collection.pay_in";
103
- }
104
- if (move.operation.startsWith("deposit.")) {
105
- return "deposit.attributed";
106
- }
107
- if (move.operation.startsWith("payout.")) {
108
- return "payout.external";
109
- }
110
- throw new Error(`cannot classify UDL movement operation ${move.operation}`);
111
- }
112
-
113
- /** Derive the ABI effect rows declared by the action-clause vocabulary. */
114
- export function deriveUdlActionEffects(
115
- action: Readonly<Record<string, unknown>>,
116
- vocabulary: readonly ActionClauseDescriptor[],
117
- ): DerivedUdlEffects {
118
- const effects: Partial<Record<UdlEffectKind, EffectRow[]>> = {};
119
- for (const clause of vocabulary) {
120
- if (clause.scope !== "action" || !clause.effects) continue;
121
- const value = actionClauseValue(action, clause.target);
122
- if (value === undefined) continue;
123
- for (const descriptor of clause.effects) {
124
- const values =
125
- descriptor.per === "element" && Array.isArray(value) ? value : [value];
126
- for (const [index, candidate] of values.entries()) {
127
- const object = recordValue(candidate);
128
- if (
129
- descriptor.kind === "holds" &&
130
- "fixed" in descriptor.signature &&
131
- descriptor.signature.fixed === "reserve" &&
132
- object?.operation !== "internal_transfer.reserve"
133
- ) {
134
- continue;
135
- }
136
- if (
137
- descriptor.kind === "moves" &&
138
- ((clause.target === "allocate" && object?.mode === "write_off") ||
139
- (clause.target === "receiptDistribution" &&
140
- object?.mode === "loss"))
141
- )
142
- continue;
143
- const suffix = effectSignatureSuffix(descriptor, object);
144
- if (!suffix) continue;
145
- (effects[descriptor.kind] ??= []).push({
146
- signature: `${descriptor.kind}.${suffix}`,
147
- source:
148
- descriptor.per === "element"
149
- ? `${clause.target}[${index}]`
150
- : clause.target,
151
- ...(descriptor.kind === "notifies" &&
152
- typeof object?.channel === "string" &&
153
- typeof object.role === "string"
154
- ? { channel: object.channel, role: object.role }
155
- : {}),
156
- });
157
- }
158
- }
159
- }
160
- return effects;
161
- }
162
-
163
- function actionClauseValue(
164
- action: Readonly<Record<string, unknown>>,
165
- target: string,
166
- ): unknown {
167
- const [head, tail] = target.split(".");
168
- if (!head) return undefined;
169
- const value =
170
- head === "transitionsRefs"
171
- ? [
172
- ...(Array.isArray(action.transitionsRefs)
173
- ? action.transitionsRefs
174
- : []),
175
- ...(Array.isArray(action.requiresRefs)
176
- ? action.requiresRefs
177
- : []
178
- ).flatMap((gate) => {
179
- const requirement = recordValue(gate);
180
- const attests = recordValue(requirement?.attests);
181
- return attests
182
- ? [{ field: requirement?.field, action: attests.consume }]
183
- : [];
184
- }),
185
- ]
186
- : action[head];
187
- if (head === "transitionsRefs" && Array.isArray(value) && value.length === 0)
188
- return undefined;
189
- if (!tail) return value;
190
- return recordValue(value)?.[tail];
191
- }
192
-
193
- function effectSignatureSuffix(
194
- descriptor: EffectDescriptor,
195
- value: Readonly<Record<string, unknown>> | undefined,
196
- ): string | undefined {
197
- if ("fixed" in descriptor.signature) return descriptor.signature.fixed;
198
- if ("fromField" in descriptor.signature) {
199
- const field = value?.[descriptor.signature.fromField];
200
- return typeof field === "string" ? field : undefined;
201
- }
202
- if (!value || !("movementClass" in descriptor.signature)) return undefined;
203
- const operation = value.operation;
204
- const bind = recordValue(value.bind);
205
- return typeof operation === "string"
206
- ? movementClass({ ...(bind ? { bind } : {}), operation })
207
- : undefined;
208
- }
209
-
210
- function recordValue(
211
- value: unknown,
212
- ): Readonly<Record<string, unknown>> | undefined {
213
- return value !== null && typeof value === "object" && !Array.isArray(value)
214
- ? (value as Readonly<Record<string, unknown>>)
215
- : undefined;
216
- }
217
-
218
- export interface ResolvedActionPlanLeaf {
219
- readonly effects: readonly {
220
- readonly kind: UdlEffectKind;
221
- readonly signature: string;
222
- }[];
223
- readonly evidence: string;
224
- readonly originPath: readonly string[];
225
- readonly step:
226
- | UdlMove
227
- | (Omit<UdlStep, "operation"> & {
228
- readonly operation: UdlStep["operation"] | "payout.create";
229
- });
230
- }
231
-
232
- export interface ResolvedActionPlan {
233
- readonly action: string;
234
- readonly effects: DerivedUdlEffects;
235
- readonly leaves: readonly ResolvedActionPlanLeaf[];
236
- readonly pieceId?: string;
237
- }
238
-
239
- export interface ResolvedActionPlansResult {
240
- readonly issues: readonly UdlIssue[];
241
- readonly plans: readonly ResolvedActionPlan[];
242
- }
243
-
244
- export function deriveActionEffectsFromPlan(
245
- leaves: readonly ResolvedActionPlanLeaf[],
246
- ): DerivedUdlEffects {
247
- const effects: Partial<Record<UdlEffectKind, EffectRow[]>> = {};
248
- for (const leaf of leaves) {
249
- for (const eff of leaf.effects) {
250
- const kind = eff.kind;
251
- (effects[kind] ??= []).push({
252
- signature: eff.signature,
253
- source: leaf.originPath.join("."),
254
- });
255
- }
256
- }
257
- return effects;
258
- }
259
-
260
- function combineDerivedEffects(
261
- direct: DerivedUdlEffects,
262
- leaves: DerivedUdlEffects,
263
- ): DerivedUdlEffects {
264
- const result: Partial<Record<UdlEffectKind, EffectRow[]>> = {};
265
- for (const kind of udlEffectKinds) {
266
- const directRows = direct[kind] ?? [];
267
- const leafRows = leaves[kind] ?? [];
268
- if (directRows.length > 0 || leafRows.length > 0) {
269
- result[kind] = [...directRows, ...leafRows];
270
- }
271
- }
272
- return result;
273
- }
274
-
275
- function encodeOriginPathKey(originPath: readonly string[]): string {
276
- const parts = originPath.map((seg) => `${seg.length}_${seg}`);
277
- return `k_${parts.join("_")}`;
278
- }
279
-
280
- function sameEffectSignatures(
281
- left: readonly { readonly kind: UdlEffectKind; readonly signature: string }[],
282
- right: readonly {
283
- readonly kind: UdlEffectKind;
284
- readonly signature: string;
285
- }[],
286
- ): boolean {
287
- const count = (
288
- effects: readonly {
289
- readonly kind: UdlEffectKind;
290
- readonly signature: string;
291
- }[],
292
- ) => {
293
- const counts = new Map<string, number>();
294
- for (const eff of effects) {
295
- const key = `${eff.kind}:${eff.signature}`;
296
- counts.set(key, (counts.get(key) ?? 0) + 1);
297
- }
298
- return counts;
299
- };
300
- const leftCounts = count(left);
301
- const rightCounts = count(right);
302
- if (leftCounts.size !== rightCounts.size) return false;
303
- for (const [key, n] of leftCounts) {
304
- if (rightCounts.get(key) !== n) return false;
305
- }
306
- return true;
307
- }
308
-
309
- function expectedLeafEffects(
310
- step: UdlStep | UdlMove,
311
- ): readonly { readonly kind: UdlEffectKind; readonly signature: string }[] {
312
- if (step.operation === "internal_transfer.reserve") {
313
- const moveClass = movementClass(step);
314
- return [
315
- { kind: "moves", signature: `moves.${moveClass}` },
316
- { kind: "holds", signature: "holds.reserve" },
317
- ];
318
- }
319
- if (step.operation.startsWith("internal_transfer.")) {
320
- const moveClass = movementClass(step);
321
- return [{ kind: "moves", signature: `moves.${moveClass}` }];
322
- }
323
- if (step.operation === "account.escrow.provision") {
324
- return [{ kind: "holds", signature: "holds.escrow" }];
325
- }
326
- if (step.operation === "account.freeze") {
327
- return [{ kind: "holds", signature: "holds.freeze" }];
328
- }
329
- if (step.operation === "account.unfreeze") {
330
- return [{ kind: "holds", signature: "holds.unfreeze" }];
331
- }
332
- return [];
333
- }
334
-
335
- const accountPattern = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
336
- const positiveMoneyPattern = "^[1-9][0-9]{0,17}$";
337
- const nonNegativeMoneyPattern = "^(0|[1-9][0-9]{0,17})$";
338
- const currencyPattern = "^[A-Z]{3}$";
339
-
340
- function isMoneySchema(schema: unknown): boolean {
341
- if (!schema || typeof schema !== "object") return false;
342
- const s = schema as Record<string, unknown>;
343
- return (
344
- s.type === "string" &&
345
- (s.pattern === positiveMoneyPattern ||
346
- s.pattern === nonNegativeMoneyPattern)
347
- );
348
- }
349
-
350
- function isAccountSchema(schema: unknown): boolean {
351
- if (!schema || typeof schema !== "object") return false;
352
- const s = schema as Record<string, unknown>;
353
- return s.type === "string" && s.pattern === accountPattern;
354
- }
355
-
356
- function isStringSchema(schema: unknown): boolean {
357
- if (!schema || typeof schema !== "object") return false;
358
- const s = schema as Record<string, unknown>;
359
- return s.type === "string";
360
- }
361
-
362
- function isDeniedCrossTenantSegment(segment: string): boolean {
363
- return (
364
- segment === "tenant" ||
365
- segment === "tenantId" ||
366
- segment === "org" ||
367
- segment === "environment"
368
- );
369
- }
370
-
371
- function checkCrossTenant(rawBind: string): boolean {
372
- if (rawBind.startsWith("/")) return true;
373
- const segments = rawBind.split(/[./]/);
374
- return segments.some(isDeniedCrossTenantSegment);
375
- }
376
-
377
- function instrumentConcreteCurrency(
378
- instrument: UdlInstrument,
379
- ): string | undefined {
380
- const mutableFields = new Set(instrument.update?.fields ?? []);
381
- const allUpdatedFields = new Set(
382
- Object.values(instrument.actions).flatMap((a) => a.updates ?? []),
383
- );
384
- const currencyFields = Object.entries(instrument.fields).filter(
385
- ([, schema]) =>
386
- typeof schema === "object" &&
387
- schema !== null &&
388
- (schema as Record<string, unknown>).pattern === currencyPattern,
389
- );
390
- if (currencyFields.length === 1) {
391
- const [fieldName, fieldDefObj] = currencyFields[0]!;
392
- if (mutableFields.has(fieldName) || allUpdatedFields.has(fieldName)) {
393
- return undefined;
394
- }
395
- const fieldDef = fieldDefObj as Record<string, unknown>;
396
- if (
397
- typeof fieldDef.const === "string" &&
398
- /^[A-Z]{3}$/.test(fieldDef.const)
399
- ) {
400
- return fieldDef.const;
401
- }
402
- if (
403
- Array.isArray(fieldDef.enum) &&
404
- fieldDef.enum.length === 1 &&
405
- typeof fieldDef.enum[0] === "string" &&
406
- /^[A-Z]{3}$/.test(fieldDef.enum[0])
407
- ) {
408
- return fieldDef.enum[0];
409
- }
410
- }
411
- return undefined;
412
- }
413
-
414
- function pieceCurrency(
415
- piece: UdlPiece,
416
- instrument: UdlInstrument,
417
- ): string | undefined {
418
- const amountSchema = instrument.fields[piece.amount] as
419
- | Record<string, unknown>
420
- | undefined;
421
- if (
422
- amountSchema &&
423
- typeof amountSchema["x-hyperscale-currency"] === "string" &&
424
- /^[A-Z]{3}$/.test(amountSchema["x-hyperscale-currency"])
425
- ) {
426
- return amountSchema["x-hyperscale-currency"];
427
- }
428
- return instrumentConcreteCurrency(instrument);
429
- }
430
-
431
- export function resolveUdlActionPlans(
432
- instrument: UdlInstrument,
433
- ): ResolvedActionPlansResult {
434
- const issues: UdlIssue[] = [];
435
- const plans: ResolvedActionPlan[] = [];
436
-
437
- if (instrument.piecePlan) {
438
- for (const piece of instrument.piecePlan.pieces) {
439
- if (pieceCurrency(piece, instrument) === undefined) {
440
- issues.push(
441
- issue(
442
- "UDL4002",
443
- `$.piecePlan.pieces.${piece.id}`,
444
- `piece ${piece.id} requires a valid concrete ISO currency from amount tag or immutable instrument currency declaration`,
445
- ),
446
- );
447
- }
448
- }
449
- }
450
-
451
- const libraryMap = instrument.actionLibrary ?? {};
452
-
453
- function hasLibrary(lib: string): boolean {
454
- if (!instrument.actionLibrary) return false;
455
- return Object.hasOwn(instrument.actionLibrary, lib);
456
- }
457
-
458
- function getAction(lib: string, act: string): UdlPrivateAction | undefined {
459
- if (!instrument.actionLibrary) return undefined;
460
- if (!Object.hasOwn(instrument.actionLibrary, lib)) return undefined;
461
- const mod = instrument.actionLibrary[lib];
462
- if (
463
- !mod ||
464
- typeof mod !== "object" ||
465
- !mod.actions ||
466
- typeof mod.actions !== "object"
467
- )
468
- return undefined;
469
- if (!Object.hasOwn(mod.actions, act)) return undefined;
470
- return mod.actions[act];
471
- }
472
-
473
- // Check all private actions for structure, complete order, collisions, and authority
474
- for (const [libKey, libModule] of Object.entries(libraryMap)) {
475
- const libPath = `$.actionLibrary.${libKey}`;
476
- const actionKeys = Object.keys(libModule.actions);
477
- const orderSet = new Set(libModule.actionOrder);
478
-
479
- if (new Set(libModule.actionOrder).size !== libModule.actionOrder.length) {
480
- issues.push(
481
- issue(
482
- "UDL2010",
483
- `${libPath}.actionOrder`,
484
- `actionOrder in library ${libKey} contains duplicate action names`,
485
- ),
486
- );
487
- }
488
- for (const actionKey of actionKeys) {
489
- if (!orderSet.has(actionKey)) {
490
- issues.push(
491
- issue(
492
- "UDL2010",
493
- `${libPath}.actionOrder`,
494
- `action ${actionKey} is declared in library ${libKey} but missing from actionOrder`,
495
- ),
496
- );
497
- }
498
- }
499
- for (const orderKey of libModule.actionOrder) {
500
- if (!Object.hasOwn(libModule.actions, orderKey)) {
501
- issues.push(
502
- issue(
503
- "UDL2010",
504
- `${libPath}.actionOrder`,
505
- `actionOrder in library ${libKey} references unknown action ${orderKey}`,
506
- ),
507
- );
508
- }
509
- }
510
-
511
- for (const [actKey, privAction] of Object.entries(libModule.actions)) {
512
- const actPath = `${libPath}.actions.${actKey}`;
513
- if (privAction.approval === "independent") {
514
- issues.push(
515
- issue(
516
- "UDL2012",
517
- `${actPath}.approval`,
518
- `private action ${libKey}.${actKey} cannot declare independent approval`,
519
- ),
520
- );
521
- }
522
- if (privAction.recovery === "external") {
523
- issues.push(
524
- issue(
525
- "UDL2012",
526
- `${actPath}.recovery`,
527
- `private action ${libKey}.${actKey} cannot declare external recovery`,
528
- ),
529
- );
530
- }
531
-
532
- const leafIds = privAction.leaves.map((l) => l.id);
533
- const callIds = privAction.calls.map((c) => c.id);
534
- const leafIdSet = new Set(leafIds);
535
- const callIdSet = new Set(callIds);
536
-
537
- if (leafIdSet.size !== leafIds.length) {
538
- issues.push(
539
- issue(
540
- "UDL2010",
541
- `${actPath}.leaves`,
542
- `duplicate leaf id in private action ${libKey}.${actKey}`,
543
- ),
544
- );
545
- }
546
- if (callIdSet.size !== callIds.length) {
547
- issues.push(
548
- issue(
549
- "UDL2010",
550
- `${actPath}.calls`,
551
- `duplicate call id in private action ${libKey}.${actKey}`,
552
- ),
553
- );
554
- }
555
-
556
- for (const id of leafIds) {
557
- if (callIdSet.has(id)) {
558
- issues.push(
559
- issue(
560
- "UDL2010",
561
- `${actPath}.order`,
562
- `collision between leaf id and call id ${id} in private action ${libKey}.${actKey}`,
563
- ),
564
- );
565
- }
566
- }
567
-
568
- const allIds = new Set([...leafIds, ...callIds]);
569
- const orderIds = privAction.order;
570
- if (
571
- orderIds.length !== allIds.size ||
572
- !orderIds.every((id) => allIds.has(id)) ||
573
- new Set(orderIds).size !== orderIds.length
574
- ) {
575
- issues.push(
576
- issue(
577
- "UDL2010",
578
- `${actPath}.order`,
579
- `order in private action ${libKey}.${actKey} must be an exact permutation of leaf and call ids`,
580
- ),
581
- );
582
- }
583
-
584
- for (const call of privAction.calls) {
585
- const [targetLib, targetAct] = call.action.split(".");
586
- if (!targetLib || !targetAct || !hasLibrary(targetLib)) {
587
- issues.push(
588
- issue(
589
- "UDL2010",
590
- `${actPath}.calls.${call.id}.action`,
591
- `call ${call.id} references unknown library ${targetLib ?? ""}`,
592
- ),
593
- );
594
- } else {
595
- const targetActionDef = getAction(targetLib, targetAct);
596
- if (!targetActionDef) {
597
- issues.push(
598
- issue(
599
- "UDL2010",
600
- `${actPath}.calls.${call.id}.action`,
601
- `call ${call.id} references unknown action ${targetAct} in library ${targetLib}`,
602
- ),
603
- );
604
- } else {
605
- for (const key of Object.keys(call.bind)) {
606
- if (!Object.hasOwn(targetActionDef.parameters, key)) {
607
- issues.push(
608
- issue(
609
- "UDL2011",
610
- `${actPath}.calls.${call.id}.bind.${key}`,
611
- `unexpected parameter ${key} in call to ${call.action}`,
612
- ),
613
- );
614
- }
615
- }
616
- for (const paramName of Object.keys(targetActionDef.parameters)) {
617
- if (!Object.hasOwn(call.bind, paramName)) {
618
- issues.push(
619
- issue(
620
- "UDL2011",
621
- `${actPath}.calls.${call.id}.bind.${paramName}`,
622
- `missing binding for parameter ${paramName} in call to ${call.action}`,
623
- ),
624
- );
625
- }
626
- }
627
- }
628
- }
629
- }
630
- }
631
- }
632
-
633
- // Cycle detection in private actions
634
- const actionVisited = new Set<string>();
635
- const recursionStack = new Set<string>();
636
-
637
- function checkCycle(currentActionRef: string, path: string[]): boolean {
638
- const [lib, act] = currentActionRef.split(".");
639
- if (path.length > UDL_LIMITS.maxDepth) {
640
- issues.push(
641
- issue(
642
- "UDL2010",
643
- `$.actionLibrary.${lib ?? ""}.actions.${act ?? ""}`,
644
- `action call depth exceeds maximum depth of ${UDL_LIMITS.maxDepth}`,
645
- ),
646
- );
647
- return true;
648
- }
649
- actionVisited.add(currentActionRef);
650
- recursionStack.add(currentActionRef);
651
-
652
- const actionDef = lib && act ? getAction(lib, act) : undefined;
653
- if (actionDef) {
654
- for (const call of actionDef.calls) {
655
- const [targetLib, targetAct] = call.action.split(".");
656
- if (!targetLib || !targetAct || !getAction(targetLib, targetAct)) {
657
- continue;
658
- }
659
- if (recursionStack.has(call.action)) {
660
- issues.push(
661
- issue(
662
- "UDL2010",
663
- `$.actionLibrary.${lib}.actions.${act}`,
664
- `action call cycle detected: ${[...path, call.action].join(" -> ")}`,
665
- ),
666
- );
667
- return true;
668
- }
669
- if (!actionVisited.has(call.action)) {
670
- if (checkCycle(call.action, [...path, call.action])) return true;
671
- }
672
- }
673
- }
674
- recursionStack.delete(currentActionRef);
675
- return false;
676
- }
677
-
678
- for (const [libKey, libModule] of Object.entries(libraryMap)) {
679
- for (const actKey of Object.keys(libModule.actions)) {
680
- const ref = `${libKey}.${actKey}`;
681
- if (!actionVisited.has(ref)) {
682
- checkCycle(ref, [ref]);
683
- }
684
- }
685
- }
686
-
687
- type ParamValue =
688
- | { readonly kind: "instance" }
689
- | { readonly kind: "piece"; readonly piece: UdlPiece }
690
- | { readonly kind: "account"; readonly path: string }
691
- | {
692
- readonly kind: "money";
693
- readonly currency?: string | undefined;
694
- readonly path: string;
695
- }
696
- | {
697
- readonly kind: "text";
698
- readonly value?: string | undefined;
699
- readonly path?: string | undefined;
700
- };
701
-
702
- function resolveBinding(
703
- rawBind: string,
704
- scope: ReadonlyMap<string, ParamValue>,
705
- leafPath: string,
706
- ):
707
- | {
708
- readonly binding: UdlBinding;
709
- readonly currency?: string | undefined;
710
- readonly kind: UdlPrivateParameterKind;
711
- readonly path?: string | undefined;
712
- readonly piece?: UdlPiece | undefined;
713
- readonly value?: string | undefined;
714
- }
715
- | undefined {
716
- if (rawBind.includes("$results")) {
717
- issues.push(
718
- issue(
719
- "UDL2011",
720
- leafPath,
721
- `results is not an implicit binding scope: ${rawBind}`,
722
- ),
723
- );
724
- return undefined;
725
- }
726
- if (checkCrossTenant(rawBind)) {
727
- issues.push(
728
- issue(
729
- "UDL2012",
730
- leafPath,
731
- `cross-tenant field paths are not allowed: ${rawBind}`,
732
- ),
733
- );
734
- return undefined;
735
- }
736
-
737
- if (rawBind === "$instance") {
738
- return {
739
- binding: { from: "instance", path: "instrumentInstanceId" },
740
- kind: "instance",
741
- };
742
- }
743
-
744
- if (rawBind === "$piece") {
745
- const pieceVal = scope.get("piece");
746
- if (!pieceVal || pieceVal.kind !== "piece") {
747
- issues.push(
748
- issue(
749
- "UDL2011",
750
- leafPath,
751
- `$piece reference is not available in caller scope`,
752
- ),
753
- );
754
- return undefined;
755
- }
756
- return {
757
- binding: { from: "const", value: pieceVal.piece.id },
758
- kind: "piece",
759
- piece: pieceVal.piece,
760
- };
761
- }
762
-
763
- if (rawBind.startsWith("$fields.")) {
764
- const parts = rawBind.slice("$fields.".length).split(".");
765
- if (parts.length !== 1) {
766
- issues.push(
767
- issue(
768
- "UDL2011",
769
- leafPath,
770
- `invalid member access on field: ${rawBind}`,
771
- ),
772
- );
773
- return undefined;
774
- }
775
- const fieldName = parts[0]!;
776
- const fieldSchema = instrument.fields[fieldName];
777
- if (!fieldSchema) {
778
- issues.push(
779
- issue(
780
- "UDL2011",
781
- leafPath,
782
- `referenced field ${fieldName} is not declared on instrument`,
783
- ),
784
- );
785
- return undefined;
786
- }
787
- if (isMoneySchema(fieldSchema)) {
788
- const cur =
789
- ((fieldSchema as Record<string, unknown>)["x-hyperscale-currency"] as
790
- | string
791
- | undefined) ?? instrumentConcreteCurrency(instrument);
792
- return {
793
- binding: { from: "instance", path: `fields.${fieldName}` },
794
- currency: cur,
795
- kind: "money",
796
- path: `fields.${fieldName}`,
797
- };
798
- }
799
- if (isAccountSchema(fieldSchema)) {
800
- return {
801
- binding: { from: "instance", path: `fields.${fieldName}` },
802
- kind: "account",
803
- path: `fields.${fieldName}`,
804
- };
805
- }
806
- if (isStringSchema(fieldSchema)) {
807
- return {
808
- binding: { from: "instance", path: `fields.${fieldName}` },
809
- kind: "text",
810
- path: `fields.${fieldName}`,
811
- };
812
- }
813
- issues.push(
814
- issue(
815
- "UDL2011",
816
- leafPath,
817
- `field ${fieldName} is not an account, money, or text field`,
818
- ),
819
- );
820
- return undefined;
821
- }
822
-
823
- if (rawBind.startsWith("$")) {
824
- const parts = rawBind.slice(1).split(".");
825
- const paramName = parts[0]!;
826
- const val = scope.get(paramName);
827
- if (!val) {
828
- issues.push(
829
- issue(
830
- "UDL2011",
831
- leafPath,
832
- `unbound parameter $${paramName} in binding ${rawBind}`,
833
- ),
834
- );
835
- return undefined;
836
- }
837
-
838
- if (val.kind === "piece") {
839
- if (parts.length === 1) {
840
- return {
841
- binding: { from: "const", value: val.piece.id },
842
- kind: "piece",
843
- piece: val.piece,
844
- };
845
- }
846
- if (parts.length !== 2) {
847
- issues.push(
848
- issue(
849
- "UDL2011",
850
- leafPath,
851
- `invalid trailing member access on piece reference: ${rawBind}`,
852
- ),
853
- );
854
- return undefined;
855
- }
856
- const member = parts[1]!;
857
- if (member === "id") {
858
- return {
859
- binding: { from: "const", value: val.piece.id },
860
- kind: "text",
861
- };
862
- }
863
- if (member === "amount") {
864
- const cur = pieceCurrency(val.piece, instrument);
865
- if (cur === undefined) {
866
- issues.push(
867
- issue(
868
- "UDL4002",
869
- leafPath,
870
- `piece ${val.piece.id} amount field lacks a concrete currency`,
871
- ),
872
- );
873
- }
874
- return {
875
- binding: { from: "instance", path: `fields.${val.piece.amount}` },
876
- currency: cur ?? "UNKNOWN",
877
- kind: "money",
878
- path: `fields.${val.piece.amount}`,
879
- };
880
- }
881
- if (member === "release_to") {
882
- return {
883
- binding: {
884
- from: "instance",
885
- path: `fields.${val.piece.release_to}`,
886
- },
887
- kind: "account",
888
- path: `fields.${val.piece.release_to}`,
889
- };
890
- }
891
- if (member === "refund_to") {
892
- return {
893
- binding: {
894
- from: "instance",
895
- path: `fields.${val.piece.refund_to}`,
896
- },
897
- kind: "account",
898
- path: `fields.${val.piece.refund_to}`,
899
- };
900
- }
901
- if (member === "currency") {
902
- const cur = pieceCurrency(val.piece, instrument);
903
- if (cur === undefined) {
904
- issues.push(
905
- issue(
906
- "UDL4002",
907
- leafPath,
908
- `piece ${val.piece.id} currency cannot be resolved to a concrete currency`,
909
- ),
910
- );
911
- }
912
- return {
913
- binding: { from: "const", value: cur ?? "UNKNOWN" },
914
- kind: "text",
915
- };
916
- }
917
- issues.push(
918
- issue(
919
- "UDL2011",
920
- leafPath,
921
- `unknown piece member ${member} in ${rawBind}`,
922
- ),
923
- );
924
- return undefined;
925
- }
926
-
927
- if (val.kind === "instance") {
928
- if (parts.length === 1) {
929
- return {
930
- binding: { from: "instance", path: "instrumentInstanceId" },
931
- kind: "instance",
932
- };
933
- }
934
- if (parts[1] === "fields") {
935
- if (parts.length !== 3) {
936
- issues.push(
937
- issue(
938
- "UDL2011",
939
- leafPath,
940
- `invalid trailing member access on instance field: ${rawBind}`,
941
- ),
942
- );
943
- return undefined;
944
- }
945
- const fieldName = parts[2]!;
946
- const fieldSchema = instrument.fields[fieldName];
947
- if (!fieldSchema) {
948
- issues.push(
949
- issue(
950
- "UDL2011",
951
- leafPath,
952
- `referenced field ${fieldName} is not declared on instrument`,
953
- ),
954
- );
955
- return undefined;
956
- }
957
- if (isMoneySchema(fieldSchema)) {
958
- const cur =
959
- ((fieldSchema as Record<string, unknown>)[
960
- "x-hyperscale-currency"
961
- ] as string | undefined) ??
962
- instrumentConcreteCurrency(instrument);
963
- return {
964
- binding: { from: "instance", path: `fields.${fieldName}` },
965
- currency: cur,
966
- kind: "money",
967
- path: `fields.${fieldName}`,
968
- };
969
- }
970
- if (isAccountSchema(fieldSchema)) {
971
- return {
972
- binding: { from: "instance", path: `fields.${fieldName}` },
973
- kind: "account",
974
- path: `fields.${fieldName}`,
975
- };
976
- }
977
- if (isStringSchema(fieldSchema)) {
978
- return {
979
- binding: { from: "instance", path: `fields.${fieldName}` },
980
- kind: "text",
981
- path: `fields.${fieldName}`,
982
- };
983
- }
984
- issues.push(
985
- issue(
986
- "UDL2011",
987
- leafPath,
988
- `field ${fieldName} is not an account, money, or text field`,
989
- ),
990
- );
991
- return undefined;
992
- }
993
- if (
994
- parts.length === 2 &&
995
- (parts[1] === "instrumentInstanceId" || parts[1] === "productId")
996
- ) {
997
- return {
998
- binding: { from: "instance", path: parts[1] },
999
- kind: "text",
1000
- path: parts[1],
1001
- };
1002
- }
1003
- if (parts[1] === "refs") {
1004
- if (parts.length !== 3) {
1005
- issues.push(
1006
- issue(
1007
- "UDL2011",
1008
- leafPath,
1009
- `invalid trailing member access on instance ref: ${rawBind}`,
1010
- ),
1011
- );
1012
- return undefined;
1013
- }
1014
- const refName = parts[2]!;
1015
- const captured = capturedRefKind(instrument, refName);
1016
- if (!captured) {
1017
- issues.push(
1018
- issue(
1019
- "UDL2011",
1020
- leafPath,
1021
- `referenced ref ${refName} is not captured by any step or move on instrument`,
1022
- ),
1023
- );
1024
- return undefined;
1025
- }
1026
- return {
1027
- binding: { from: "instance", path: `refs.${refName}` },
1028
- kind: captured,
1029
- path: `refs.${refName}`,
1030
- };
1031
- }
1032
- issues.push(
1033
- issue(
1034
- "UDL2011",
1035
- leafPath,
1036
- `invalid or trailing member access on instance reference: ${rawBind}`,
1037
- ),
1038
- );
1039
- return undefined;
1040
- }
1041
-
1042
- // Money, Account, Text parameter references do not allow member access
1043
- if (parts.length > 1) {
1044
- issues.push(
1045
- issue(
1046
- "UDL2011",
1047
- leafPath,
1048
- `member access is not allowed on ${val.kind} parameter: ${rawBind}`,
1049
- ),
1050
- );
1051
- return undefined;
1052
- }
1053
-
1054
- if (val.kind === "account") {
1055
- return {
1056
- binding: { from: "instance", path: val.path },
1057
- kind: "account",
1058
- path: val.path,
1059
- };
1060
- }
1061
-
1062
- if (val.kind === "money") {
1063
- return {
1064
- binding: { from: "instance", path: val.path },
1065
- currency: val.currency,
1066
- kind: "money",
1067
- path: val.path,
1068
- };
1069
- }
1070
-
1071
- if (val.kind === "text") {
1072
- return {
1073
- binding: val.path
1074
- ? { from: "instance", path: val.path }
1075
- : { from: "const", value: val.value ?? "" },
1076
- kind: "text",
1077
- path: val.path,
1078
- value: val.value,
1079
- };
1080
- }
1081
- }
1082
-
1083
- issues.push(
1084
- issue(
1085
- "UDL2011",
1086
- leafPath,
1087
- `invalid or unsupported binding reference: ${rawBind}`,
1088
- ),
1089
- );
1090
- return undefined;
1091
- }
1092
-
1093
- function resolveCallParameters(
1094
- call: UdlCall,
1095
- targetActionDef: UdlPrivateAction,
1096
- scope: ReadonlyMap<string, ParamValue>,
1097
- callPath: string,
1098
- issuesList: UdlIssue[],
1099
- piece?: UdlPiece,
1100
- ): Map<string, ParamValue> {
1101
- const callScope = new Map<string, ParamValue>();
1102
-
1103
- // Check extra keys
1104
- for (const key of Object.keys(call.bind)) {
1105
- if (!Object.hasOwn(targetActionDef.parameters, key)) {
1106
- issuesList.push(
1107
- issue(
1108
- "UDL2011",
1109
- `${callPath}.bind.${key}`,
1110
- `unexpected parameter ${key} in call to ${call.action}`,
1111
- ),
1112
- );
1113
- }
1114
- }
1115
-
1116
- // Check missing keys & resolve bindings
1117
- for (const [pName, pDef] of Object.entries(targetActionDef.parameters)) {
1118
- if (!Object.hasOwn(call.bind, pName)) {
1119
- issuesList.push(
1120
- issue(
1121
- "UDL2011",
1122
- `${callPath}.bind.${pName}`,
1123
- `missing binding for parameter ${pName} in call to ${call.action}`,
1124
- ),
1125
- );
1126
- continue;
1127
- }
1128
- const raw = call.bind[pName]!;
1129
- const resolved = resolveBinding(raw, scope, `${callPath}.bind.${pName}`);
1130
- if (resolved) {
1131
- if (pDef.kind !== resolved.kind) {
1132
- issuesList.push(
1133
- issue(
1134
- "UDL2011",
1135
- `${callPath}.bind.${pName}`,
1136
- `parameter ${pName} expected kind ${pDef.kind} but received ${resolved.kind}`,
1137
- ),
1138
- );
1139
- }
1140
- if (
1141
- pDef.kind === "money" &&
1142
- pDef.currency &&
1143
- resolved.currency &&
1144
- pDef.currency !== resolved.currency
1145
- ) {
1146
- issuesList.push(
1147
- issue(
1148
- "UDL2011",
1149
- `${callPath}.bind.${pName}`,
1150
- `currency mismatch for money parameter ${pName}: expected ${pDef.currency} but received ${resolved.currency}`,
1151
- ),
1152
- );
1153
- }
1154
- if (resolved.kind === "piece") {
1155
- callScope.set(pName, {
1156
- kind: "piece",
1157
- piece: resolved.piece ?? piece!,
1158
- });
1159
- } else if (resolved.kind === "instance") {
1160
- callScope.set(pName, { kind: "instance" });
1161
- } else if (resolved.kind === "account") {
1162
- callScope.set(pName, { kind: "account", path: resolved.path ?? "" });
1163
- } else if (resolved.kind === "money") {
1164
- callScope.set(pName, {
1165
- currency: resolved.currency ?? pDef.currency,
1166
- kind: "money",
1167
- path: resolved.path ?? "",
1168
- });
1169
- } else {
1170
- callScope.set(pName, {
1171
- kind: "text",
1172
- path: resolved.path,
1173
- value: resolved.value,
1174
- });
1175
- }
1176
- }
1177
- }
1178
-
1179
- return callScope;
1180
- }
1181
-
1182
- // Counts private-action expansions for one public action plan. Reset before
1183
- // each plan so the bound is per plan, not per document.
1184
- let totalCumulativeCalls = 0;
1185
-
1186
- function expandPrivateAction(
1187
- targetLibKey: string,
1188
- targetActionKey: string,
1189
- callArgs: ReadonlyMap<string, ParamValue>,
1190
- originPrefix: readonly string[],
1191
- callChain: readonly string[],
1192
- callerPrincipal: "api_key" | "user_session",
1193
- capturedDestinations: Set<string>,
1194
- consumedSources: Set<string>,
1195
- expandedLeaves: ResolvedActionPlanLeaf[],
1196
- ): void {
1197
- totalCumulativeCalls += 1;
1198
- if (totalCumulativeCalls > UDL_LIMITS.maxActionExpansion) {
1199
- issues.push(
1200
- issue(
1201
- "UDL2010",
1202
- `$.actions.${originPrefix[0]}`,
1203
- `action exceeds cumulative call bound of ${UDL_LIMITS.maxActionExpansion}`,
1204
- ),
1205
- );
1206
- return;
1207
- }
1208
-
1209
- if (callChain.length > UDL_LIMITS.maxDepth) {
1210
- issues.push(
1211
- issue(
1212
- "UDL2010",
1213
- `$.actions.${originPrefix[0]}`,
1214
- `action recursion depth exceeded ${UDL_LIMITS.maxDepth}`,
1215
- ),
1216
- );
1217
- return;
1218
- }
1219
-
1220
- const actionRef = `${targetLibKey}.${targetActionKey}`;
1221
- if (callChain.includes(actionRef)) {
1222
- issues.push(
1223
- issue(
1224
- "UDL2010",
1225
- `$.actions.${originPrefix[0]}`,
1226
- `action graph cycle detected in call to ${actionRef}`,
1227
- ),
1228
- );
1229
- return;
1230
- }
1231
-
1232
- const privAction = getAction(targetLibKey, targetActionKey);
1233
- if (!privAction) {
1234
- issues.push(
1235
- issue(
1236
- "UDL2010",
1237
- `$.actions.${originPrefix[0]}`,
1238
- `call references unknown action ${targetActionKey} in library ${targetLibKey}`,
1239
- ),
1240
- );
1241
- return;
1242
- }
1243
-
1244
- if (privAction.principal !== callerPrincipal) {
1245
- issues.push(
1246
- issue(
1247
- "UDL2012",
1248
- `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.principal`,
1249
- `principal mismatch: caller requires ${callerPrincipal} but ${actionRef} requires ${privAction.principal}`,
1250
- ),
1251
- );
1252
- }
1253
-
1254
- const leavesMap = new Map(privAction.leaves.map((l) => [l.id, l]));
1255
- const callsMap = new Map(privAction.calls.map((c) => [c.id, c]));
1256
-
1257
- for (const itemId of privAction.order) {
1258
- if (expandedLeaves.length >= UDL_LIMITS.maxActionLeaves) {
1259
- issues.push(
1260
- issue(
1261
- "UDL2010",
1262
- `$.actions.${originPrefix[0]}`,
1263
- `action exceeds cumulative expanded leaf bound of ${UDL_LIMITS.maxActionLeaves}`,
1264
- ),
1265
- );
1266
- return;
1267
- }
1268
-
1269
- if (leavesMap.has(itemId)) {
1270
- const leaf = leavesMap.get(itemId)!;
1271
- const leafPath = `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.leaves.${leaf.id}`;
1272
- const resolvedBinds: Record<string, UdlBinding> = {};
1273
-
1274
- for (const [key, bindStr] of Object.entries(leaf.bind)) {
1275
- const resolved = resolveBinding(
1276
- bindStr,
1277
- callArgs,
1278
- `${leafPath}.bind.${key}`,
1279
- );
1280
- if (resolved) {
1281
- resolvedBinds[key] = resolved.binding;
1282
- if (key === "amount" && resolved.kind !== "money") {
1283
- issues.push(
1284
- issue(
1285
- "UDL2011",
1286
- `${leafPath}.bind.${key}`,
1287
- `amount operand must resolve to money`,
1288
- ),
1289
- );
1290
- }
1291
- if (
1292
- (key === "sourceAccountId" ||
1293
- key === "destinationAccountId" ||
1294
- key === "accountId") &&
1295
- resolved.kind !== "account"
1296
- ) {
1297
- issues.push(
1298
- issue(
1299
- "UDL2011",
1300
- `${leafPath}.bind.${key}`,
1301
- `${key} operand must resolve to account`,
1302
- ),
1303
- );
1304
- }
1305
- }
1306
- }
1307
-
1308
- // Check leaf evidence
1309
- if (!leaf.evidence || leaf.evidence.trim().length === 0) {
1310
- issues.push(
1311
- issue(
1312
- "UDL2013",
1313
- `${leafPath}.evidence`,
1314
- `leaf evidence is mandatory and cannot be blank`,
1315
- ),
1316
- );
1317
- }
1318
-
1319
- // Check leaf captures: destination ref is the map key
1320
- if (leaf.capture) {
1321
- for (const capKey of Object.keys(leaf.capture)) {
1322
- if (capturedDestinations.has(capKey)) {
1323
- issues.push(
1324
- issue(
1325
- "UDL2011",
1326
- `${leafPath}.capture.${capKey}`,
1327
- `duplicate capture destination ${capKey} across expanded leaves`,
1328
- ),
1329
- );
1330
- }
1331
- capturedDestinations.add(capKey);
1332
- }
1333
- }
1334
-
1335
- const originPath = [...originPrefix, leaf.id];
1336
- let step: UdlStep | UdlMove;
1337
-
1338
- if (leaf.operation.startsWith("internal_transfer.")) {
1339
- step = {
1340
- bind: resolvedBinds,
1341
- ...(leaf.capture ? { capture: leaf.capture } : {}),
1342
- key: encodeOriginPathKey(originPath),
1343
- operation: leaf.operation as UdlMove["operation"],
1344
- };
1345
- } else {
1346
- step = {
1347
- bind: resolvedBinds,
1348
- ...(leaf.capture ? { capture: leaf.capture } : {}),
1349
- operation: leaf.operation as UdlStep["operation"],
1350
- };
1351
- }
1352
-
1353
- // Check repeated consumption of money amount or hold identity
1354
- const amountSrc = boundPath(step, "amount");
1355
- if (amountSrc) {
1356
- if (consumedSources.has(amountSrc)) {
1357
- issues.push(
1358
- issue(
1359
- "UDL2011",
1360
- `${leafPath}.bind.amount`,
1361
- `repeated consumption of money source ${amountSrc} in one expanded action`,
1362
- ),
1363
- );
1364
- }
1365
- consumedSources.add(amountSrc);
1366
- }
1367
- const holdSrc = boundPath(step, "holdId");
1368
- if (holdSrc) {
1369
- const holdKey = `hold:${holdSrc}`;
1370
- if (consumedSources.has(holdKey)) {
1371
- issues.push(
1372
- issue(
1373
- "UDL2011",
1374
- `${leafPath}.bind.holdId`,
1375
- `repeated consumption of hold ${holdSrc} in one expanded action`,
1376
- ),
1377
- );
1378
- }
1379
- consumedSources.add(holdKey);
1380
- }
1381
-
1382
- // A library leaf is written once and expanded under several callers,
1383
- // so its declaration names the operation's generic class (the class
1384
- // with no bindings). Each expansion then carries the class its
1385
- // resolved bindings select, exactly as an inline move would: a buyer
1386
- // funding escrow is a pay-in, the same leaf paying out is internal.
1387
- const expectedEffects = expectedLeafEffects(step);
1388
- const genericEffects = expectedLeafEffects({ ...step, bind: {} });
1389
- if (
1390
- !sameEffectSignatures(leaf.effects, expectedEffects) &&
1391
- !sameEffectSignatures(leaf.effects, genericEffects)
1392
- ) {
1393
- issues.push(
1394
- issue(
1395
- "UDL2013",
1396
- `${leafPath}.effects`,
1397
- `declared leaf effects do not match expected effects for operation ${leaf.operation}`,
1398
- ),
1399
- );
1400
- }
1401
-
1402
- expandedLeaves.push({
1403
- effects: expectedEffects,
1404
- evidence: leaf.evidence,
1405
- originPath,
1406
- step,
1407
- });
1408
- } else if (callsMap.has(itemId)) {
1409
- const nextCall = callsMap.get(itemId)!;
1410
- const [nextLib, nextAct] = nextCall.action.split(".");
1411
- if (!nextLib || !nextAct || !hasLibrary(nextLib)) {
1412
- issues.push(
1413
- issue(
1414
- "UDL2010",
1415
- `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`,
1416
- `call ${nextCall.id} references unknown library ${nextLib ?? ""}`,
1417
- ),
1418
- );
1419
- continue;
1420
- }
1421
- const nextActionDef = getAction(nextLib, nextAct);
1422
- if (!nextActionDef) {
1423
- issues.push(
1424
- issue(
1425
- "UDL2010",
1426
- `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`,
1427
- `call ${nextCall.id} references unknown action ${nextAct} in library ${nextLib}`,
1428
- ),
1429
- );
1430
- continue;
1431
- }
1432
-
1433
- const nextScope = resolveCallParameters(
1434
- nextCall,
1435
- nextActionDef,
1436
- callArgs,
1437
- `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}`,
1438
- issues,
1439
- );
1440
-
1441
- expandPrivateAction(
1442
- nextLib,
1443
- nextAct,
1444
- nextScope,
1445
- [...originPrefix, nextCall.id],
1446
- [...callChain, actionRef],
1447
- privAction.principal,
1448
- capturedDestinations,
1449
- consumedSources,
1450
- expandedLeaves,
1451
- );
1452
- }
1453
- }
1454
- }
1455
-
1456
- // Iterate over public actions in actionOrder
1457
- for (const actionKey of instrument.actionOrder) {
1458
- const action = instrument.actions[actionKey];
1459
- if (!action) continue;
1460
-
1461
- const actionPath = `$.actions.${actionKey}`;
1462
- const publicPrincipal = action.principal ?? "api_key";
1463
-
1464
- // Reject mixed direct steps/moves and calls
1465
- if (
1466
- action.calls &&
1467
- action.calls.length > 0 &&
1468
- (action.steps.length > 0 || action.moves.length > 0)
1469
- ) {
1470
- issues.push(
1471
- issue(
1472
- "UDL2010",
1473
- `${actionPath}.calls`,
1474
- `action ${actionKey} cannot mix calls with direct steps or moves`,
1475
- ),
1476
- );
1477
- }
1478
-
1479
- if (action.calls) {
1480
- const publicCallIds = action.calls.map((c) => c.id);
1481
- if (new Set(publicCallIds).size !== publicCallIds.length) {
1482
- issues.push(
1483
- issue(
1484
- "UDL2010",
1485
- `${actionPath}.calls`,
1486
- `duplicate call id in action ${actionKey}`,
1487
- ),
1488
- );
1489
- }
1490
- }
1491
-
1492
- if (action.pieceStage) {
1493
- if (!action.calls || action.calls.length === 0) {
1494
- issues.push(
1495
- issue(
1496
- "UDL5013",
1497
- `${actionPath}.calls`,
1498
- `pieceStage action ${actionKey} must move money through calls, never through direct moves or steps`,
1499
- ),
1500
- );
1501
- }
1502
-
1503
- if (actionKey === "create") {
1504
- issues.push(
1505
- issue(
1506
- "UDL5013",
1507
- `${actionPath}.pieceStage`,
1508
- `create action cannot declare pieceStage`,
1509
- ),
1510
- );
1511
- }
1512
-
1513
- if (
1514
- !instrument.piecePlan ||
1515
- instrument.piecePlan.id !== action.pieceStage.plan
1516
- ) {
1517
- issues.push(
1518
- issue(
1519
- "UDL5013",
1520
- `${actionPath}.pieceStage.plan`,
1521
- `pieceStage references unknown plan ${action.pieceStage.plan}`,
1522
- ),
1523
- );
1524
- }
1525
-
1526
- const stage = action.pieceStage.stage;
1527
- const stageOrder: readonly string[] =
1528
- stage === "fund"
1529
- ? (instrument.piecePlan?.fund_order ?? [])
1530
- : stage === "release"
1531
- ? (instrument.piecePlan?.release_order ?? [])
1532
- : stage === "refund"
1533
- ? (instrument.piecePlan?.refund_order ?? [])
1534
- : (instrument.piecePlan?.unfund_order ?? []);
1535
-
1536
- if (stageOrder.length === 0) {
1537
- issues.push(
1538
- issue(
1539
- "UDL5013",
1540
- `${actionPath}.pieceStage.stage`,
1541
- `piece stage ${stage} is empty`,
1542
- ),
1543
- );
1544
- }
1545
-
1546
- // Check input must be present and exact
1547
- if (!action.input) {
1548
- issues.push(
1549
- issue(
1550
- "UDL5013",
1551
- `${actionPath}.input`,
1552
- `pieceStage action input is required and must declare exact pieceId enum matching stage order`,
1553
- ),
1554
- );
1555
- } else {
1556
- const inp = action.input as Record<string, unknown>;
1557
- const props = (inp.properties ?? {}) as Record<string, unknown>;
1558
- const pieceIdProp = (props.pieceId ?? {}) as Record<string, unknown>;
1559
- const enumVals = Array.isArray(pieceIdProp.enum)
1560
- ? pieceIdProp.enum
1561
- : [];
1562
-
1563
- if (
1564
- inp.type !== "object" ||
1565
- inp.additionalProperties !== false ||
1566
- !Array.isArray(inp.required) ||
1567
- inp.required.length !== 1 ||
1568
- inp.required[0] !== "pieceId" ||
1569
- Object.keys(props).length !== 1 ||
1570
- pieceIdProp.type !== "string" ||
1571
- enumVals.length !== stageOrder.length ||
1572
- !enumVals.every((id, idx) => id === stageOrder[idx])
1573
- ) {
1574
- issues.push(
1575
- issue(
1576
- "UDL5013",
1577
- `${actionPath}.input`,
1578
- `pieceStage action input must declare exact pieceId enum matching stage order`,
1579
- ),
1580
- );
1581
- }
1582
- }
1583
-
1584
- const variantMultiplicities: Map<string, number>[] = [];
1585
-
1586
- for (const pieceId of stageOrder) {
1587
- const piece = instrument.piecePlan?.pieces.find(
1588
- (p) => p.id === pieceId,
1589
- );
1590
- if (!piece) {
1591
- issues.push(
1592
- issue(
1593
- "UDL5013",
1594
- `${actionPath}.pieceStage`,
1595
- `piece id ${pieceId} in stage order is not declared in piecePlan`,
1596
- ),
1597
- );
1598
- continue;
1599
- }
1600
-
1601
- const expandedLeaves: ResolvedActionPlanLeaf[] = [];
1602
- const capturedDestinations = new Set<string>();
1603
- const consumedSources = new Set<string>();
1604
- totalCumulativeCalls = 0;
1605
-
1606
- const scope = new Map<string, ParamValue>([
1607
- ["piece", { kind: "piece", piece }],
1608
- ["instance", { kind: "instance" }],
1609
- ]);
1610
-
1611
- for (const call of action.calls ?? []) {
1612
- const [lib, act] = call.action.split(".");
1613
- if (!lib || !act || !hasLibrary(lib)) {
1614
- issues.push(
1615
- issue(
1616
- "UDL2010",
1617
- `${actionPath}.calls.${call.id}.action`,
1618
- `call ${call.id} references unknown library ${lib ?? ""}`,
1619
- ),
1620
- );
1621
- continue;
1622
- }
1623
- const targetActionDef = getAction(lib, act);
1624
- if (!targetActionDef) {
1625
- issues.push(
1626
- issue(
1627
- "UDL2010",
1628
- `${actionPath}.calls.${call.id}.action`,
1629
- `call ${call.id} references unknown action ${act} in library ${lib}`,
1630
- ),
1631
- );
1632
- continue;
1633
- }
1634
-
1635
- const callScope = resolveCallParameters(
1636
- call,
1637
- targetActionDef,
1638
- scope,
1639
- `${actionPath}.calls.${call.id}`,
1640
- issues,
1641
- piece,
1642
- );
1643
-
1644
- expandPrivateAction(
1645
- lib,
1646
- act,
1647
- callScope,
1648
- [actionKey, call.id],
1649
- [],
1650
- publicPrincipal,
1651
- capturedDestinations,
1652
- consumedSources,
1653
- expandedLeaves,
1654
- );
1655
- }
1656
-
1657
- // Refuse input amount/destination binding in leaves and validate piece bindings
1658
- for (const leaf of expandedLeaves) {
1659
- const binds = leaf.step.bind ?? {};
1660
- for (const [k, b] of Object.entries(binds)) {
1661
- if (
1662
- (k === "amount" ||
1663
- k === "sourceAccountId" ||
1664
- k === "destinationAccountId") &&
1665
- typeof b === "object" &&
1666
- b !== null &&
1667
- (b as { from?: string }).from === "input"
1668
- ) {
1669
- issues.push(
1670
- issue(
1671
- "UDL5013",
1672
- `${actionPath}.pieceStage`,
1673
- `pieceStage leaves cannot bind amount or destination to input: ${k}`,
1674
- ),
1675
- );
1676
- }
1677
- }
1678
-
1679
- if (leaf.step.operation.startsWith("internal_transfer.")) {
1680
- const amt = binds.amount;
1681
- if (
1682
- !amt ||
1683
- typeof amt !== "object" ||
1684
- (amt as { from?: string }).from !== "instance" ||
1685
- (amt as { path?: string }).path !== `fields.${piece.amount}`
1686
- ) {
1687
- issues.push(
1688
- issue(
1689
- "UDL5013",
1690
- `${actionPath}.pieceStage`,
1691
- `pieceStage transfer amount must resolve to selected piece.amount (${piece.amount})`,
1692
- ),
1693
- );
1694
- }
1695
-
1696
- if (stage === "release") {
1697
- const dest = binds.destinationAccountId;
1698
- if (
1699
- !dest ||
1700
- typeof dest !== "object" ||
1701
- (dest as { from?: string }).from !== "instance" ||
1702
- (dest as { path?: string }).path !==
1703
- `fields.${piece.release_to}`
1704
- ) {
1705
- issues.push(
1706
- issue(
1707
- "UDL5013",
1708
- `${actionPath}.pieceStage`,
1709
- `pieceStage release destination must resolve to selected piece.release_to (${piece.release_to})`,
1710
- ),
1711
- );
1712
- }
1713
- } else if (stage === "refund") {
1714
- const dest = binds.destinationAccountId;
1715
- if (
1716
- !dest ||
1717
- typeof dest !== "object" ||
1718
- (dest as { from?: string }).from !== "instance" ||
1719
- (dest as { path?: string }).path !== `fields.${piece.refund_to}`
1720
- ) {
1721
- issues.push(
1722
- issue(
1723
- "UDL5013",
1724
- `${actionPath}.pieceStage`,
1725
- `pieceStage refund destination must resolve to selected piece.refund_to (${piece.refund_to})`,
1726
- ),
1727
- );
1728
- }
1729
- }
1730
- }
1731
- }
1732
-
1733
- const counts = new Map<string, number>();
1734
- for (const leaf of expandedLeaves) {
1735
- for (const eff of leaf.effects) {
1736
- counts.set(eff.signature, (counts.get(eff.signature) ?? 0) + 1);
1737
- }
1738
- }
1739
- variantMultiplicities.push(counts);
1740
-
1741
- const directEffects = deriveUdlActionEffects(
1742
- action as Readonly<Record<string, unknown>>,
1743
- udlClauseVocabulary,
1744
- );
1745
- const leafEffects =
1746
- action.calls && action.calls.length > 0
1747
- ? deriveActionEffectsFromPlan(expandedLeaves)
1748
- : {};
1749
- const effects = combineDerivedEffects(directEffects, leafEffects);
1750
-
1751
- plans.push({
1752
- action: actionKey,
1753
- effects,
1754
- leaves: expandedLeaves,
1755
- pieceId,
1756
- });
1757
- }
1758
-
1759
- // Verify identical effect signature multiplicities across piece variants
1760
- if (variantMultiplicities.length > 1) {
1761
- const baseCounts = variantMultiplicities[0]!;
1762
- for (let i = 1; i < variantMultiplicities.length; i++) {
1763
- const comp = variantMultiplicities[i]!;
1764
- let identical = baseCounts.size === comp.size;
1765
- if (identical) {
1766
- for (const [sig, count] of baseCounts.entries()) {
1767
- if (comp.get(sig) !== count) {
1768
- identical = false;
1769
- break;
1770
- }
1771
- }
1772
- }
1773
- if (!identical) {
1774
- issues.push(
1775
- issue(
1776
- "UDL2013",
1777
- `${actionPath}.pieceStage`,
1778
- `piece variants of action ${actionKey} have differing effect signature multiplicities`,
1779
- ),
1780
- );
1781
- break;
1782
- }
1783
- }
1784
- }
1785
- } else if (action.calls && action.calls.length > 0) {
1786
- const expandedLeaves: ResolvedActionPlanLeaf[] = [];
1787
- const capturedDestinations = new Set<string>();
1788
- const consumedSources = new Set<string>();
1789
- totalCumulativeCalls = 0;
1790
- const scope = new Map<string, ParamValue>([
1791
- ["instance", { kind: "instance" }],
1792
- ]);
1793
-
1794
- for (const call of action.calls) {
1795
- const [lib, act] = call.action.split(".");
1796
- if (!lib || !act || !hasLibrary(lib)) {
1797
- issues.push(
1798
- issue(
1799
- "UDL2010",
1800
- `${actionPath}.calls.${call.id}.action`,
1801
- `call ${call.id} references unknown library ${lib ?? ""}`,
1802
- ),
1803
- );
1804
- continue;
1805
- }
1806
- const targetActionDef = getAction(lib, act);
1807
- if (!targetActionDef) {
1808
- issues.push(
1809
- issue(
1810
- "UDL2010",
1811
- `${actionPath}.calls.${call.id}.action`,
1812
- `call ${call.id} references unknown action ${act} in library ${lib}`,
1813
- ),
1814
- );
1815
- continue;
1816
- }
1817
-
1818
- const callScope = resolveCallParameters(
1819
- call,
1820
- targetActionDef,
1821
- scope,
1822
- `${actionPath}.calls.${call.id}`,
1823
- issues,
1824
- );
1825
-
1826
- expandPrivateAction(
1827
- lib,
1828
- act,
1829
- callScope,
1830
- [actionKey, call.id],
1831
- [],
1832
- publicPrincipal,
1833
- capturedDestinations,
1834
- consumedSources,
1835
- expandedLeaves,
1836
- );
1837
- }
1838
-
1839
- const directEffects = deriveUdlActionEffects(
1840
- action as Readonly<Record<string, unknown>>,
1841
- udlClauseVocabulary,
1842
- );
1843
- const leafEffects = deriveActionEffectsFromPlan(expandedLeaves);
1844
- const effects = combineDerivedEffects(directEffects, leafEffects);
1845
-
1846
- plans.push({
1847
- action: actionKey,
1848
- effects,
1849
- leaves: expandedLeaves,
1850
- });
1851
- } else {
1852
- // Ordinary actions already are flat. Preserve the executor's order and
1853
- // authored step bytes; origin paths belong to the plan, not the ABI.
1854
- const effects = deriveUdlActionEffects(
1855
- action as Readonly<Record<string, unknown>>,
1856
- udlClauseVocabulary,
1857
- );
1858
- const leafEffects = (source: string): ResolvedActionPlanLeaf["effects"] =>
1859
- Object.entries(effects).flatMap(([kind, rows]) =>
1860
- rows
1861
- .filter((row) => row.source === source)
1862
- .map((row) => ({
1863
- kind: kind as UdlEffectKind,
1864
- signature: row.signature,
1865
- })),
1866
- );
1867
- const leaves: ResolvedActionPlanLeaf[] = action.steps.map(
1868
- (step, index) => ({
1869
- step,
1870
- originPath: [actionKey, "steps", String(index)],
1871
- effects: leafEffects(`steps[${index}]`),
1872
- evidence: "parent_receipt",
1873
- }),
1874
- );
1875
- if (action.payout) {
1876
- const intent = action.payout;
1877
- leaves.push({
1878
- step: {
1879
- operation: "payout.create",
1880
- bind: {
1881
- amount: { from: "instance", path: intent.amount },
1882
- beneficiaryId: {
1883
- from: "instance",
1884
- path: `fields.${intent.beneficiaryField}`,
1885
- },
1886
- currency: {
1887
- from: "instance",
1888
- path: `fields.${intent.currencyField}`,
1889
- },
1890
- sourceAccountId: {
1891
- from: "instance",
1892
- path: `fields.${intent.sourceAccountField}`,
1893
- },
1894
- speed: { from: "const", value: intent.speed },
1895
- },
1896
- capture: { [intent.capture]: "payoutId" },
1897
- },
1898
- originPath: [actionKey, "payout"],
1899
- effects: leafEffects("payout"),
1900
- evidence: "parent_receipt",
1901
- });
1902
- }
1903
- leaves.push(
1904
- ...action.moves.map((step, index) => ({
1905
- step,
1906
- originPath: [actionKey, "moves", String(index)],
1907
- effects: leafEffects(`moves[${index}]`),
1908
- evidence: "parent_receipt",
1909
- })),
1910
- );
1911
- plans.push({
1912
- action: actionKey,
1913
- leaves,
1914
- effects,
1915
- });
1916
- }
1917
- }
1918
-
1919
- return { issues, plans };
1920
- }