@hyperscale0/udl 2.0.4 → 2.1.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/CHANGELOG.md +16 -0
- package/README.md +15 -1
- package/conformance/invalid/call-binds-results.expected.json +10 -0
- package/conformance/invalid/call-binds-results.udl +314 -0
- package/conformance/invalid/call-unknown-action.expected.json +10 -0
- package/conformance/invalid/call-unknown-action.udl +314 -0
- package/conformance/invalid/leaf-effect-mismatch.expected.json +10 -0
- package/conformance/invalid/leaf-effect-mismatch.udl +314 -0
- package/conformance/invalid/piece-plan-without-partition.expected.json +10 -0
- package/conformance/invalid/piece-plan-without-partition.udl +305 -0
- package/conformance/invalid/private-action-independent-approval.expected.json +10 -0
- package/conformance/invalid/private-action-independent-approval.udl +314 -0
- package/conformance/invalid/unfund-order-not-reversed.expected.json +10 -0
- package/conformance/invalid/unfund-order-not-reversed.udl +314 -0
- package/conformance/valid/piece-plan-calls.expected.json +6 -0
- package/conformance/valid/piece-plan-calls.udl +314 -0
- package/dist/diagnostics.d.ts +36 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +36 -0
- package/dist/diagnostics.js.map +1 -1
- package/dist/effects.d.ts +24 -3
- package/dist/effects.d.ts.map +1 -1
- package/dist/effects.js +906 -8
- package/dist/effects.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -0
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +2 -0
- package/dist/limits.js.map +1 -1
- package/dist/schema.d.ts +594 -11
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +111 -1
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +289 -4
- package/dist/validation.js.map +1 -1
- package/docs/assets/brand/manifest.json +30 -0
- package/docs/assets/brand/udl-horizontal-white.svg +1 -0
- package/docs/assets/brand/udl-horizontal.svg +1 -0
- package/docs/assets/brand/udl-stacked-white.svg +1 -0
- package/docs/assets/brand/udl-stacked.svg +1 -0
- package/docs/assets/udl.svg +7 -14
- package/docs/llms-full.txt +199 -32
- package/docs/llms.txt +1 -1
- package/docs/reference/clauses.md +162 -1
- package/docs/reference/cli.md +1 -1
- package/docs/reference/diagnostics.md +38 -32
- package/package.json +2 -2
- package/spec/udl.schema.json +321 -1
- package/src/diagnostics.ts +36 -0
- package/src/effects.ts +1603 -9
- package/src/index.ts +15 -0
- package/src/limits.ts +2 -0
- package/src/schema.ts +149 -2
- package/src/validation.ts +469 -4
package/dist/effects.js
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
"notifies",
|
|
6
|
-
"reads",
|
|
7
|
-
"schedules",
|
|
8
|
-
];
|
|
1
|
+
import { issue } from "./diagnostics.js";
|
|
2
|
+
import { UDL_LIMITS } from "./limits.js";
|
|
3
|
+
import { udlClauseVocabulary, udlEffectKinds, } from "./schema.js";
|
|
4
|
+
export { udlEffectKinds } from "./schema.js";
|
|
9
5
|
function boundPath(move, endpoint) {
|
|
10
6
|
const binding = move.bind?.[endpoint];
|
|
11
7
|
if (binding === null || typeof binding !== "object")
|
|
@@ -107,4 +103,906 @@ function recordValue(value) {
|
|
|
107
103
|
? value
|
|
108
104
|
: undefined;
|
|
109
105
|
}
|
|
106
|
+
export function deriveActionEffectsFromPlan(leaves) {
|
|
107
|
+
const effects = {};
|
|
108
|
+
for (const leaf of leaves) {
|
|
109
|
+
for (const eff of leaf.effects) {
|
|
110
|
+
const kind = eff.kind;
|
|
111
|
+
(effects[kind] ??= []).push({
|
|
112
|
+
signature: eff.signature,
|
|
113
|
+
source: leaf.originPath.join("."),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return effects;
|
|
118
|
+
}
|
|
119
|
+
function combineDerivedEffects(direct, leaves) {
|
|
120
|
+
const result = {};
|
|
121
|
+
for (const kind of udlEffectKinds) {
|
|
122
|
+
const directRows = direct[kind] ?? [];
|
|
123
|
+
const leafRows = leaves[kind] ?? [];
|
|
124
|
+
if (directRows.length > 0 || leafRows.length > 0) {
|
|
125
|
+
result[kind] = [...directRows, ...leafRows];
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
function encodeOriginPathKey(originPath) {
|
|
131
|
+
const parts = originPath.map((seg) => `${seg.length}_${seg}`);
|
|
132
|
+
return `k_${parts.join("_")}`;
|
|
133
|
+
}
|
|
134
|
+
function expectedLeafEffects(step) {
|
|
135
|
+
if (step.operation === "internal_transfer.reserve") {
|
|
136
|
+
const moveClass = movementClass(step);
|
|
137
|
+
return [
|
|
138
|
+
{ kind: "moves", signature: `moves.${moveClass}` },
|
|
139
|
+
{ kind: "holds", signature: "holds.reserve" },
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
if (step.operation.startsWith("internal_transfer.")) {
|
|
143
|
+
const moveClass = movementClass(step);
|
|
144
|
+
return [{ kind: "moves", signature: `moves.${moveClass}` }];
|
|
145
|
+
}
|
|
146
|
+
if (step.operation === "account.escrow.provision") {
|
|
147
|
+
return [{ kind: "holds", signature: "holds.escrow" }];
|
|
148
|
+
}
|
|
149
|
+
if (step.operation === "account.freeze") {
|
|
150
|
+
return [{ kind: "holds", signature: "holds.freeze" }];
|
|
151
|
+
}
|
|
152
|
+
if (step.operation === "account.unfreeze") {
|
|
153
|
+
return [{ kind: "holds", signature: "holds.unfreeze" }];
|
|
154
|
+
}
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const accountPattern = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
|
|
158
|
+
const positiveMoneyPattern = "^[1-9][0-9]{0,17}$";
|
|
159
|
+
const nonNegativeMoneyPattern = "^(0|[1-9][0-9]{0,17})$";
|
|
160
|
+
const currencyPattern = "^[A-Z]{3}$";
|
|
161
|
+
function isMoneySchema(schema) {
|
|
162
|
+
if (!schema || typeof schema !== "object")
|
|
163
|
+
return false;
|
|
164
|
+
const s = schema;
|
|
165
|
+
return (s.type === "string" &&
|
|
166
|
+
(s.pattern === positiveMoneyPattern ||
|
|
167
|
+
s.pattern === nonNegativeMoneyPattern));
|
|
168
|
+
}
|
|
169
|
+
function isAccountSchema(schema) {
|
|
170
|
+
if (!schema || typeof schema !== "object")
|
|
171
|
+
return false;
|
|
172
|
+
const s = schema;
|
|
173
|
+
return s.type === "string" && s.pattern === accountPattern;
|
|
174
|
+
}
|
|
175
|
+
function isStringSchema(schema) {
|
|
176
|
+
if (!schema || typeof schema !== "object")
|
|
177
|
+
return false;
|
|
178
|
+
const s = schema;
|
|
179
|
+
return s.type === "string";
|
|
180
|
+
}
|
|
181
|
+
function isDeniedCrossTenantSegment(segment) {
|
|
182
|
+
return (segment === "tenant" ||
|
|
183
|
+
segment === "tenantId" ||
|
|
184
|
+
segment === "org" ||
|
|
185
|
+
segment === "environment");
|
|
186
|
+
}
|
|
187
|
+
function checkCrossTenant(rawBind) {
|
|
188
|
+
if (rawBind.startsWith("/"))
|
|
189
|
+
return true;
|
|
190
|
+
const segments = rawBind.split(/[./]/);
|
|
191
|
+
return segments.some(isDeniedCrossTenantSegment);
|
|
192
|
+
}
|
|
193
|
+
function instrumentConcreteCurrency(instrument) {
|
|
194
|
+
const mutableFields = new Set(instrument.update?.fields ?? []);
|
|
195
|
+
const allUpdatedFields = new Set(Object.values(instrument.actions).flatMap((a) => a.updates ?? []));
|
|
196
|
+
const currencyFields = Object.entries(instrument.fields).filter(([, schema]) => typeof schema === "object" &&
|
|
197
|
+
schema !== null &&
|
|
198
|
+
schema.pattern === currencyPattern);
|
|
199
|
+
if (currencyFields.length === 1) {
|
|
200
|
+
const [fieldName, fieldDefObj] = currencyFields[0];
|
|
201
|
+
if (mutableFields.has(fieldName) || allUpdatedFields.has(fieldName)) {
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const fieldDef = fieldDefObj;
|
|
205
|
+
if (typeof fieldDef.const === "string" &&
|
|
206
|
+
/^[A-Z]{3}$/.test(fieldDef.const)) {
|
|
207
|
+
return fieldDef.const;
|
|
208
|
+
}
|
|
209
|
+
if (Array.isArray(fieldDef.enum) &&
|
|
210
|
+
fieldDef.enum.length === 1 &&
|
|
211
|
+
typeof fieldDef.enum[0] === "string" &&
|
|
212
|
+
/^[A-Z]{3}$/.test(fieldDef.enum[0])) {
|
|
213
|
+
return fieldDef.enum[0];
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
function pieceCurrency(piece, instrument) {
|
|
219
|
+
const amountSchema = instrument.fields[piece.amount];
|
|
220
|
+
if (amountSchema &&
|
|
221
|
+
typeof amountSchema["x-hyperscale-currency"] === "string" &&
|
|
222
|
+
/^[A-Z]{3}$/.test(amountSchema["x-hyperscale-currency"])) {
|
|
223
|
+
return amountSchema["x-hyperscale-currency"];
|
|
224
|
+
}
|
|
225
|
+
return instrumentConcreteCurrency(instrument);
|
|
226
|
+
}
|
|
227
|
+
export function resolveUdlActionPlans(instrument) {
|
|
228
|
+
const issues = [];
|
|
229
|
+
const plans = [];
|
|
230
|
+
if (instrument.piecePlan) {
|
|
231
|
+
for (const piece of instrument.piecePlan.pieces) {
|
|
232
|
+
if (pieceCurrency(piece, instrument) === undefined) {
|
|
233
|
+
issues.push(issue("UDL4002", `$.piecePlan.pieces.${piece.id}`, `piece ${piece.id} requires a valid concrete ISO currency from amount tag or immutable instrument currency declaration`));
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
const libraryMap = instrument.actionLibrary ?? {};
|
|
238
|
+
function hasLibrary(lib) {
|
|
239
|
+
if (!instrument.actionLibrary)
|
|
240
|
+
return false;
|
|
241
|
+
return Object.hasOwn(instrument.actionLibrary, lib);
|
|
242
|
+
}
|
|
243
|
+
function getAction(lib, act) {
|
|
244
|
+
if (!instrument.actionLibrary)
|
|
245
|
+
return undefined;
|
|
246
|
+
if (!Object.hasOwn(instrument.actionLibrary, lib))
|
|
247
|
+
return undefined;
|
|
248
|
+
const mod = instrument.actionLibrary[lib];
|
|
249
|
+
if (!mod ||
|
|
250
|
+
typeof mod !== "object" ||
|
|
251
|
+
!mod.actions ||
|
|
252
|
+
typeof mod.actions !== "object")
|
|
253
|
+
return undefined;
|
|
254
|
+
if (!Object.hasOwn(mod.actions, act))
|
|
255
|
+
return undefined;
|
|
256
|
+
return mod.actions[act];
|
|
257
|
+
}
|
|
258
|
+
// Check all private actions for structure, complete order, collisions, and authority
|
|
259
|
+
for (const [libKey, libModule] of Object.entries(libraryMap)) {
|
|
260
|
+
const libPath = `$.actionLibrary.${libKey}`;
|
|
261
|
+
const actionKeys = Object.keys(libModule.actions);
|
|
262
|
+
const orderSet = new Set(libModule.actionOrder);
|
|
263
|
+
if (new Set(libModule.actionOrder).size !== libModule.actionOrder.length) {
|
|
264
|
+
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `actionOrder in library ${libKey} contains duplicate action names`));
|
|
265
|
+
}
|
|
266
|
+
for (const actionKey of actionKeys) {
|
|
267
|
+
if (!orderSet.has(actionKey)) {
|
|
268
|
+
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `action ${actionKey} is declared in library ${libKey} but missing from actionOrder`));
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
for (const orderKey of libModule.actionOrder) {
|
|
272
|
+
if (!Object.hasOwn(libModule.actions, orderKey)) {
|
|
273
|
+
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `actionOrder in library ${libKey} references unknown action ${orderKey}`));
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (const [actKey, privAction] of Object.entries(libModule.actions)) {
|
|
277
|
+
const actPath = `${libPath}.actions.${actKey}`;
|
|
278
|
+
if (privAction.approval === "independent") {
|
|
279
|
+
issues.push(issue("UDL2012", `${actPath}.approval`, `private action ${libKey}.${actKey} cannot declare independent approval`));
|
|
280
|
+
}
|
|
281
|
+
if (privAction.recovery === "external") {
|
|
282
|
+
issues.push(issue("UDL2012", `${actPath}.recovery`, `private action ${libKey}.${actKey} cannot declare external recovery`));
|
|
283
|
+
}
|
|
284
|
+
const leafIds = privAction.leaves.map((l) => l.id);
|
|
285
|
+
const callIds = privAction.calls.map((c) => c.id);
|
|
286
|
+
const leafIdSet = new Set(leafIds);
|
|
287
|
+
const callIdSet = new Set(callIds);
|
|
288
|
+
if (leafIdSet.size !== leafIds.length) {
|
|
289
|
+
issues.push(issue("UDL2010", `${actPath}.leaves`, `duplicate leaf id in private action ${libKey}.${actKey}`));
|
|
290
|
+
}
|
|
291
|
+
if (callIdSet.size !== callIds.length) {
|
|
292
|
+
issues.push(issue("UDL2010", `${actPath}.calls`, `duplicate call id in private action ${libKey}.${actKey}`));
|
|
293
|
+
}
|
|
294
|
+
for (const id of leafIds) {
|
|
295
|
+
if (callIdSet.has(id)) {
|
|
296
|
+
issues.push(issue("UDL2010", `${actPath}.order`, `collision between leaf id and call id ${id} in private action ${libKey}.${actKey}`));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const allIds = new Set([...leafIds, ...callIds]);
|
|
300
|
+
const orderIds = privAction.order;
|
|
301
|
+
if (orderIds.length !== allIds.size ||
|
|
302
|
+
!orderIds.every((id) => allIds.has(id)) ||
|
|
303
|
+
new Set(orderIds).size !== orderIds.length) {
|
|
304
|
+
issues.push(issue("UDL2010", `${actPath}.order`, `order in private action ${libKey}.${actKey} must be an exact permutation of leaf and call ids`));
|
|
305
|
+
}
|
|
306
|
+
for (const call of privAction.calls) {
|
|
307
|
+
const [targetLib, targetAct] = call.action.split(".");
|
|
308
|
+
if (!targetLib || !targetAct || !hasLibrary(targetLib)) {
|
|
309
|
+
issues.push(issue("UDL2010", `${actPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${targetLib ?? ""}`));
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
const targetActionDef = getAction(targetLib, targetAct);
|
|
313
|
+
if (!targetActionDef) {
|
|
314
|
+
issues.push(issue("UDL2010", `${actPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${targetAct} in library ${targetLib}`));
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
for (const key of Object.keys(call.bind)) {
|
|
318
|
+
if (!Object.hasOwn(targetActionDef.parameters, key)) {
|
|
319
|
+
issues.push(issue("UDL2011", `${actPath}.calls.${call.id}.bind.${key}`, `unexpected parameter ${key} in call to ${call.action}`));
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const paramName of Object.keys(targetActionDef.parameters)) {
|
|
323
|
+
if (!Object.hasOwn(call.bind, paramName)) {
|
|
324
|
+
issues.push(issue("UDL2011", `${actPath}.calls.${call.id}.bind.${paramName}`, `missing binding for parameter ${paramName} in call to ${call.action}`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
// Cycle detection in private actions
|
|
333
|
+
const actionVisited = new Set();
|
|
334
|
+
const recursionStack = new Set();
|
|
335
|
+
function checkCycle(currentActionRef, path) {
|
|
336
|
+
const [lib, act] = currentActionRef.split(".");
|
|
337
|
+
if (path.length > UDL_LIMITS.maxDepth) {
|
|
338
|
+
issues.push(issue("UDL2010", `$.actionLibrary.${lib ?? ""}.actions.${act ?? ""}`, `action call depth exceeds maximum depth of ${UDL_LIMITS.maxDepth}`));
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
actionVisited.add(currentActionRef);
|
|
342
|
+
recursionStack.add(currentActionRef);
|
|
343
|
+
const actionDef = lib && act ? getAction(lib, act) : undefined;
|
|
344
|
+
if (actionDef) {
|
|
345
|
+
for (const call of actionDef.calls) {
|
|
346
|
+
const [targetLib, targetAct] = call.action.split(".");
|
|
347
|
+
if (!targetLib || !targetAct || !getAction(targetLib, targetAct)) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
if (recursionStack.has(call.action)) {
|
|
351
|
+
issues.push(issue("UDL2010", `$.actionLibrary.${lib}.actions.${act}`, `action call cycle detected: ${[...path, call.action].join(" -> ")}`));
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
if (!actionVisited.has(call.action)) {
|
|
355
|
+
if (checkCycle(call.action, [...path, call.action]))
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
recursionStack.delete(currentActionRef);
|
|
361
|
+
return false;
|
|
362
|
+
}
|
|
363
|
+
for (const [libKey, libModule] of Object.entries(libraryMap)) {
|
|
364
|
+
for (const actKey of Object.keys(libModule.actions)) {
|
|
365
|
+
const ref = `${libKey}.${actKey}`;
|
|
366
|
+
if (!actionVisited.has(ref)) {
|
|
367
|
+
checkCycle(ref, [ref]);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function resolveBinding(rawBind, scope, leafPath) {
|
|
372
|
+
if (rawBind.includes("$results")) {
|
|
373
|
+
issues.push(issue("UDL2011", leafPath, `results is not an implicit binding scope: ${rawBind}`));
|
|
374
|
+
return undefined;
|
|
375
|
+
}
|
|
376
|
+
if (checkCrossTenant(rawBind)) {
|
|
377
|
+
issues.push(issue("UDL2012", leafPath, `cross-tenant field paths are not allowed: ${rawBind}`));
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
if (rawBind === "$instance") {
|
|
381
|
+
return {
|
|
382
|
+
binding: { from: "instance", path: "instrumentInstanceId" },
|
|
383
|
+
kind: "instance",
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (rawBind === "$piece") {
|
|
387
|
+
const pieceVal = scope.get("piece");
|
|
388
|
+
if (!pieceVal || pieceVal.kind !== "piece") {
|
|
389
|
+
issues.push(issue("UDL2011", leafPath, `$piece reference is not available in caller scope`));
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
binding: { from: "const", value: pieceVal.piece.id },
|
|
394
|
+
kind: "piece",
|
|
395
|
+
piece: pieceVal.piece,
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
if (rawBind.startsWith("$fields.")) {
|
|
399
|
+
const parts = rawBind.slice("$fields.".length).split(".");
|
|
400
|
+
if (parts.length !== 1) {
|
|
401
|
+
issues.push(issue("UDL2011", leafPath, `invalid member access on field: ${rawBind}`));
|
|
402
|
+
return undefined;
|
|
403
|
+
}
|
|
404
|
+
const fieldName = parts[0];
|
|
405
|
+
const fieldSchema = instrument.fields[fieldName];
|
|
406
|
+
if (!fieldSchema) {
|
|
407
|
+
issues.push(issue("UDL2011", leafPath, `referenced field ${fieldName} is not declared on instrument`));
|
|
408
|
+
return undefined;
|
|
409
|
+
}
|
|
410
|
+
if (isMoneySchema(fieldSchema)) {
|
|
411
|
+
const cur = fieldSchema["x-hyperscale-currency"] ?? instrumentConcreteCurrency(instrument);
|
|
412
|
+
return {
|
|
413
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
414
|
+
currency: cur,
|
|
415
|
+
kind: "money",
|
|
416
|
+
path: `fields.${fieldName}`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (isAccountSchema(fieldSchema)) {
|
|
420
|
+
return {
|
|
421
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
422
|
+
kind: "account",
|
|
423
|
+
path: `fields.${fieldName}`,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
if (isStringSchema(fieldSchema)) {
|
|
427
|
+
return {
|
|
428
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
429
|
+
kind: "text",
|
|
430
|
+
path: `fields.${fieldName}`,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
issues.push(issue("UDL2011", leafPath, `field ${fieldName} is not an account, money, or text field`));
|
|
434
|
+
return undefined;
|
|
435
|
+
}
|
|
436
|
+
if (rawBind.startsWith("$")) {
|
|
437
|
+
const parts = rawBind.slice(1).split(".");
|
|
438
|
+
const paramName = parts[0];
|
|
439
|
+
const val = scope.get(paramName);
|
|
440
|
+
if (!val) {
|
|
441
|
+
issues.push(issue("UDL2011", leafPath, `unbound parameter $${paramName} in binding ${rawBind}`));
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
444
|
+
if (val.kind === "piece") {
|
|
445
|
+
if (parts.length === 1) {
|
|
446
|
+
return {
|
|
447
|
+
binding: { from: "const", value: val.piece.id },
|
|
448
|
+
kind: "piece",
|
|
449
|
+
piece: val.piece,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
if (parts.length !== 2) {
|
|
453
|
+
issues.push(issue("UDL2011", leafPath, `invalid trailing member access on piece reference: ${rawBind}`));
|
|
454
|
+
return undefined;
|
|
455
|
+
}
|
|
456
|
+
const member = parts[1];
|
|
457
|
+
if (member === "id") {
|
|
458
|
+
return {
|
|
459
|
+
binding: { from: "const", value: val.piece.id },
|
|
460
|
+
kind: "text",
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
if (member === "amount") {
|
|
464
|
+
const cur = pieceCurrency(val.piece, instrument);
|
|
465
|
+
if (cur === undefined) {
|
|
466
|
+
issues.push(issue("UDL4002", leafPath, `piece ${val.piece.id} amount field lacks a concrete currency`));
|
|
467
|
+
}
|
|
468
|
+
return {
|
|
469
|
+
binding: { from: "instance", path: `fields.${val.piece.amount}` },
|
|
470
|
+
currency: cur ?? "UNKNOWN",
|
|
471
|
+
kind: "money",
|
|
472
|
+
path: `fields.${val.piece.amount}`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
if (member === "release_to") {
|
|
476
|
+
return {
|
|
477
|
+
binding: {
|
|
478
|
+
from: "instance",
|
|
479
|
+
path: `fields.${val.piece.release_to}`,
|
|
480
|
+
},
|
|
481
|
+
kind: "account",
|
|
482
|
+
path: `fields.${val.piece.release_to}`,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
if (member === "refund_to") {
|
|
486
|
+
return {
|
|
487
|
+
binding: {
|
|
488
|
+
from: "instance",
|
|
489
|
+
path: `fields.${val.piece.refund_to}`,
|
|
490
|
+
},
|
|
491
|
+
kind: "account",
|
|
492
|
+
path: `fields.${val.piece.refund_to}`,
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
if (member === "currency") {
|
|
496
|
+
const cur = pieceCurrency(val.piece, instrument);
|
|
497
|
+
if (cur === undefined) {
|
|
498
|
+
issues.push(issue("UDL4002", leafPath, `piece ${val.piece.id} currency cannot be resolved to a concrete currency`));
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
binding: { from: "const", value: cur ?? "UNKNOWN" },
|
|
502
|
+
kind: "text",
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
issues.push(issue("UDL2011", leafPath, `unknown piece member ${member} in ${rawBind}`));
|
|
506
|
+
return undefined;
|
|
507
|
+
}
|
|
508
|
+
if (val.kind === "instance") {
|
|
509
|
+
if (parts.length === 1) {
|
|
510
|
+
return {
|
|
511
|
+
binding: { from: "instance", path: "instrumentInstanceId" },
|
|
512
|
+
kind: "instance",
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (parts[1] === "fields") {
|
|
516
|
+
if (parts.length !== 3) {
|
|
517
|
+
issues.push(issue("UDL2011", leafPath, `invalid trailing member access on instance field: ${rawBind}`));
|
|
518
|
+
return undefined;
|
|
519
|
+
}
|
|
520
|
+
const fieldName = parts[2];
|
|
521
|
+
const fieldSchema = instrument.fields[fieldName];
|
|
522
|
+
if (!fieldSchema) {
|
|
523
|
+
issues.push(issue("UDL2011", leafPath, `referenced field ${fieldName} is not declared on instrument`));
|
|
524
|
+
return undefined;
|
|
525
|
+
}
|
|
526
|
+
if (isMoneySchema(fieldSchema)) {
|
|
527
|
+
const cur = fieldSchema["x-hyperscale-currency"] ??
|
|
528
|
+
instrumentConcreteCurrency(instrument);
|
|
529
|
+
return {
|
|
530
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
531
|
+
currency: cur,
|
|
532
|
+
kind: "money",
|
|
533
|
+
path: `fields.${fieldName}`,
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
if (isAccountSchema(fieldSchema)) {
|
|
537
|
+
return {
|
|
538
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
539
|
+
kind: "account",
|
|
540
|
+
path: `fields.${fieldName}`,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
if (isStringSchema(fieldSchema)) {
|
|
544
|
+
return {
|
|
545
|
+
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
546
|
+
kind: "text",
|
|
547
|
+
path: `fields.${fieldName}`,
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
issues.push(issue("UDL2011", leafPath, `field ${fieldName} is not an account, money, or text field`));
|
|
551
|
+
return undefined;
|
|
552
|
+
}
|
|
553
|
+
if (parts.length === 2 &&
|
|
554
|
+
(parts[1] === "instrumentInstanceId" || parts[1] === "productId")) {
|
|
555
|
+
return {
|
|
556
|
+
binding: { from: "instance", path: parts[1] },
|
|
557
|
+
kind: "text",
|
|
558
|
+
path: parts[1],
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
issues.push(issue("UDL2011", leafPath, `invalid or trailing member access on instance reference: ${rawBind}`));
|
|
562
|
+
return undefined;
|
|
563
|
+
}
|
|
564
|
+
// Money, Account, Text parameter references do not allow member access
|
|
565
|
+
if (parts.length > 1) {
|
|
566
|
+
issues.push(issue("UDL2011", leafPath, `member access is not allowed on ${val.kind} parameter: ${rawBind}`));
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
if (val.kind === "account") {
|
|
570
|
+
return {
|
|
571
|
+
binding: { from: "instance", path: val.path },
|
|
572
|
+
kind: "account",
|
|
573
|
+
path: val.path,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
if (val.kind === "money") {
|
|
577
|
+
return {
|
|
578
|
+
binding: { from: "instance", path: val.path },
|
|
579
|
+
currency: val.currency,
|
|
580
|
+
kind: "money",
|
|
581
|
+
path: val.path,
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
if (val.kind === "text") {
|
|
585
|
+
return {
|
|
586
|
+
binding: val.path
|
|
587
|
+
? { from: "instance", path: val.path }
|
|
588
|
+
: { from: "const", value: val.value ?? "" },
|
|
589
|
+
kind: "text",
|
|
590
|
+
path: val.path,
|
|
591
|
+
value: val.value,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
issues.push(issue("UDL2011", leafPath, `invalid or unsupported binding reference: ${rawBind}`));
|
|
596
|
+
return undefined;
|
|
597
|
+
}
|
|
598
|
+
function resolveCallParameters(call, targetActionDef, scope, callPath, issuesList, piece) {
|
|
599
|
+
const callScope = new Map();
|
|
600
|
+
// Check extra keys
|
|
601
|
+
for (const key of Object.keys(call.bind)) {
|
|
602
|
+
if (!Object.hasOwn(targetActionDef.parameters, key)) {
|
|
603
|
+
issuesList.push(issue("UDL2011", `${callPath}.bind.${key}`, `unexpected parameter ${key} in call to ${call.action}`));
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Check missing keys & resolve bindings
|
|
607
|
+
for (const [pName, pDef] of Object.entries(targetActionDef.parameters)) {
|
|
608
|
+
if (!Object.hasOwn(call.bind, pName)) {
|
|
609
|
+
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `missing binding for parameter ${pName} in call to ${call.action}`));
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
const raw = call.bind[pName];
|
|
613
|
+
const resolved = resolveBinding(raw, scope, `${callPath}.bind.${pName}`);
|
|
614
|
+
if (resolved) {
|
|
615
|
+
if (pDef.kind !== resolved.kind) {
|
|
616
|
+
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `parameter ${pName} expected kind ${pDef.kind} but received ${resolved.kind}`));
|
|
617
|
+
}
|
|
618
|
+
if (pDef.kind === "money" &&
|
|
619
|
+
pDef.currency &&
|
|
620
|
+
resolved.currency &&
|
|
621
|
+
pDef.currency !== resolved.currency) {
|
|
622
|
+
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `currency mismatch for money parameter ${pName}: expected ${pDef.currency} but received ${resolved.currency}`));
|
|
623
|
+
}
|
|
624
|
+
if (resolved.kind === "piece") {
|
|
625
|
+
callScope.set(pName, {
|
|
626
|
+
kind: "piece",
|
|
627
|
+
piece: resolved.piece ?? piece,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
else if (resolved.kind === "instance") {
|
|
631
|
+
callScope.set(pName, { kind: "instance" });
|
|
632
|
+
}
|
|
633
|
+
else if (resolved.kind === "account") {
|
|
634
|
+
callScope.set(pName, { kind: "account", path: resolved.path ?? "" });
|
|
635
|
+
}
|
|
636
|
+
else if (resolved.kind === "money") {
|
|
637
|
+
callScope.set(pName, {
|
|
638
|
+
currency: resolved.currency ?? pDef.currency,
|
|
639
|
+
kind: "money",
|
|
640
|
+
path: resolved.path ?? "",
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
callScope.set(pName, {
|
|
645
|
+
kind: "text",
|
|
646
|
+
path: resolved.path,
|
|
647
|
+
value: resolved.value,
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return callScope;
|
|
653
|
+
}
|
|
654
|
+
// Counts private-action expansions for one public action plan. Reset before
|
|
655
|
+
// each plan so the bound is per plan, not per document.
|
|
656
|
+
let totalCumulativeCalls = 0;
|
|
657
|
+
function expandPrivateAction(targetLibKey, targetActionKey, callArgs, originPrefix, callChain, callerPrincipal, capturedDestinations, consumedSources, expandedLeaves) {
|
|
658
|
+
totalCumulativeCalls += 1;
|
|
659
|
+
if (totalCumulativeCalls > UDL_LIMITS.maxActionExpansion) {
|
|
660
|
+
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action exceeds cumulative call bound of ${UDL_LIMITS.maxActionExpansion}`));
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (callChain.length > UDL_LIMITS.maxDepth) {
|
|
664
|
+
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action recursion depth exceeded ${UDL_LIMITS.maxDepth}`));
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
const actionRef = `${targetLibKey}.${targetActionKey}`;
|
|
668
|
+
if (callChain.includes(actionRef)) {
|
|
669
|
+
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action graph cycle detected in call to ${actionRef}`));
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
const privAction = getAction(targetLibKey, targetActionKey);
|
|
673
|
+
if (!privAction) {
|
|
674
|
+
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `call references unknown action ${targetActionKey} in library ${targetLibKey}`));
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (privAction.principal !== callerPrincipal) {
|
|
678
|
+
issues.push(issue("UDL2012", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.principal`, `principal mismatch: caller requires ${callerPrincipal} but ${actionRef} requires ${privAction.principal}`));
|
|
679
|
+
}
|
|
680
|
+
const leavesMap = new Map(privAction.leaves.map((l) => [l.id, l]));
|
|
681
|
+
const callsMap = new Map(privAction.calls.map((c) => [c.id, c]));
|
|
682
|
+
for (const itemId of privAction.order) {
|
|
683
|
+
if (expandedLeaves.length >= UDL_LIMITS.maxActionLeaves) {
|
|
684
|
+
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action exceeds cumulative expanded leaf bound of ${UDL_LIMITS.maxActionLeaves}`));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
if (leavesMap.has(itemId)) {
|
|
688
|
+
const leaf = leavesMap.get(itemId);
|
|
689
|
+
const leafPath = `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.leaves.${leaf.id}`;
|
|
690
|
+
const resolvedBinds = {};
|
|
691
|
+
for (const [key, bindStr] of Object.entries(leaf.bind)) {
|
|
692
|
+
const resolved = resolveBinding(bindStr, callArgs, `${leafPath}.bind.${key}`);
|
|
693
|
+
if (resolved) {
|
|
694
|
+
resolvedBinds[key] = resolved.binding;
|
|
695
|
+
if (key === "amount" && resolved.kind !== "money") {
|
|
696
|
+
issues.push(issue("UDL2011", `${leafPath}.bind.${key}`, `amount operand must resolve to money`));
|
|
697
|
+
}
|
|
698
|
+
if ((key === "sourceAccountId" ||
|
|
699
|
+
key === "destinationAccountId" ||
|
|
700
|
+
key === "accountId") &&
|
|
701
|
+
resolved.kind !== "account") {
|
|
702
|
+
issues.push(issue("UDL2011", `${leafPath}.bind.${key}`, `${key} operand must resolve to account`));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
// Check leaf evidence
|
|
707
|
+
if (!leaf.evidence || leaf.evidence.trim().length === 0) {
|
|
708
|
+
issues.push(issue("UDL2013", `${leafPath}.evidence`, `leaf evidence is mandatory and cannot be blank`));
|
|
709
|
+
}
|
|
710
|
+
// Check leaf captures: destination ref is the map key
|
|
711
|
+
if (leaf.capture) {
|
|
712
|
+
for (const capKey of Object.keys(leaf.capture)) {
|
|
713
|
+
if (capturedDestinations.has(capKey)) {
|
|
714
|
+
issues.push(issue("UDL2011", `${leafPath}.capture.${capKey}`, `duplicate capture destination ${capKey} across expanded leaves`));
|
|
715
|
+
}
|
|
716
|
+
capturedDestinations.add(capKey);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const originPath = [...originPrefix, leaf.id];
|
|
720
|
+
let step;
|
|
721
|
+
if (leaf.operation.startsWith("internal_transfer.")) {
|
|
722
|
+
step = {
|
|
723
|
+
bind: resolvedBinds,
|
|
724
|
+
...(leaf.capture ? { capture: leaf.capture } : {}),
|
|
725
|
+
key: encodeOriginPathKey(originPath),
|
|
726
|
+
operation: leaf.operation,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
step = {
|
|
731
|
+
bind: resolvedBinds,
|
|
732
|
+
...(leaf.capture ? { capture: leaf.capture } : {}),
|
|
733
|
+
operation: leaf.operation,
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
// Check repeated consumption of money amount or hold identity
|
|
737
|
+
const amountSrc = boundPath(step, "amount");
|
|
738
|
+
if (amountSrc) {
|
|
739
|
+
if (consumedSources.has(amountSrc)) {
|
|
740
|
+
issues.push(issue("UDL2011", `${leafPath}.bind.amount`, `repeated consumption of money source ${amountSrc} in one expanded action`));
|
|
741
|
+
}
|
|
742
|
+
consumedSources.add(amountSrc);
|
|
743
|
+
}
|
|
744
|
+
const holdSrc = boundPath(step, "holdId");
|
|
745
|
+
if (holdSrc) {
|
|
746
|
+
const holdKey = `hold:${holdSrc}`;
|
|
747
|
+
if (consumedSources.has(holdKey)) {
|
|
748
|
+
issues.push(issue("UDL2011", `${leafPath}.bind.holdId`, `repeated consumption of hold ${holdSrc} in one expanded action`));
|
|
749
|
+
}
|
|
750
|
+
consumedSources.add(holdKey);
|
|
751
|
+
}
|
|
752
|
+
const expectedEffects = expectedLeafEffects(step);
|
|
753
|
+
const actualCounts = new Map();
|
|
754
|
+
for (const eff of leaf.effects) {
|
|
755
|
+
const key = `${eff.kind}:${eff.signature}`;
|
|
756
|
+
actualCounts.set(key, (actualCounts.get(key) ?? 0) + 1);
|
|
757
|
+
}
|
|
758
|
+
const expectedCounts = new Map();
|
|
759
|
+
for (const eff of expectedEffects) {
|
|
760
|
+
const key = `${eff.kind}:${eff.signature}`;
|
|
761
|
+
expectedCounts.set(key, (expectedCounts.get(key) ?? 0) + 1);
|
|
762
|
+
}
|
|
763
|
+
let effectsMatch = actualCounts.size === expectedCounts.size;
|
|
764
|
+
if (effectsMatch) {
|
|
765
|
+
for (const [k, count] of actualCounts.entries()) {
|
|
766
|
+
if (expectedCounts.get(k) !== count) {
|
|
767
|
+
effectsMatch = false;
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
if (!effectsMatch) {
|
|
773
|
+
issues.push(issue("UDL2013", `${leafPath}.effects`, `declared leaf effects do not match expected effects for operation ${leaf.operation}`));
|
|
774
|
+
}
|
|
775
|
+
expandedLeaves.push({
|
|
776
|
+
effects: leaf.effects,
|
|
777
|
+
evidence: leaf.evidence,
|
|
778
|
+
originPath,
|
|
779
|
+
step,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
else if (callsMap.has(itemId)) {
|
|
783
|
+
const nextCall = callsMap.get(itemId);
|
|
784
|
+
const [nextLib, nextAct] = nextCall.action.split(".");
|
|
785
|
+
if (!nextLib || !nextAct || !hasLibrary(nextLib)) {
|
|
786
|
+
issues.push(issue("UDL2010", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`, `call ${nextCall.id} references unknown library ${nextLib ?? ""}`));
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
const nextActionDef = getAction(nextLib, nextAct);
|
|
790
|
+
if (!nextActionDef) {
|
|
791
|
+
issues.push(issue("UDL2010", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`, `call ${nextCall.id} references unknown action ${nextAct} in library ${nextLib}`));
|
|
792
|
+
continue;
|
|
793
|
+
}
|
|
794
|
+
const nextScope = resolveCallParameters(nextCall, nextActionDef, callArgs, `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}`, issues);
|
|
795
|
+
expandPrivateAction(nextLib, nextAct, nextScope, [...originPrefix, nextCall.id], [...callChain, actionRef], privAction.principal, capturedDestinations, consumedSources, expandedLeaves);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
// Iterate over public actions in actionOrder
|
|
800
|
+
for (const actionKey of instrument.actionOrder) {
|
|
801
|
+
const action = instrument.actions[actionKey];
|
|
802
|
+
if (!action)
|
|
803
|
+
continue;
|
|
804
|
+
const actionPath = `$.actions.${actionKey}`;
|
|
805
|
+
const publicPrincipal = action.principal ?? "api_key";
|
|
806
|
+
// Reject mixed direct steps/moves and calls
|
|
807
|
+
if (action.calls &&
|
|
808
|
+
action.calls.length > 0 &&
|
|
809
|
+
(action.steps.length > 0 || action.moves.length > 0)) {
|
|
810
|
+
issues.push(issue("UDL2010", `${actionPath}.calls`, `action ${actionKey} cannot mix calls with direct steps or moves`));
|
|
811
|
+
}
|
|
812
|
+
if (action.calls) {
|
|
813
|
+
const publicCallIds = action.calls.map((c) => c.id);
|
|
814
|
+
if (new Set(publicCallIds).size !== publicCallIds.length) {
|
|
815
|
+
issues.push(issue("UDL2010", `${actionPath}.calls`, `duplicate call id in action ${actionKey}`));
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (action.pieceStage) {
|
|
819
|
+
if (!action.calls || action.calls.length === 0) {
|
|
820
|
+
issues.push(issue("UDL5013", `${actionPath}.calls`, `pieceStage action ${actionKey} must move money through calls, never through direct moves or steps`));
|
|
821
|
+
}
|
|
822
|
+
if (actionKey === "create") {
|
|
823
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `create action cannot declare pieceStage`));
|
|
824
|
+
}
|
|
825
|
+
if (!instrument.piecePlan ||
|
|
826
|
+
instrument.piecePlan.id !== action.pieceStage.plan) {
|
|
827
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage.plan`, `pieceStage references unknown plan ${action.pieceStage.plan}`));
|
|
828
|
+
}
|
|
829
|
+
const stage = action.pieceStage.stage;
|
|
830
|
+
const stageOrder = stage === "fund"
|
|
831
|
+
? (instrument.piecePlan?.fund_order ?? [])
|
|
832
|
+
: stage === "release"
|
|
833
|
+
? (instrument.piecePlan?.release_order ?? [])
|
|
834
|
+
: stage === "refund"
|
|
835
|
+
? (instrument.piecePlan?.refund_order ?? [])
|
|
836
|
+
: (instrument.piecePlan?.unfund_order ?? []);
|
|
837
|
+
if (stageOrder.length === 0) {
|
|
838
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage.stage`, `piece stage ${stage} is empty`));
|
|
839
|
+
}
|
|
840
|
+
// Check input must be present and exact
|
|
841
|
+
if (!action.input) {
|
|
842
|
+
issues.push(issue("UDL5013", `${actionPath}.input`, `pieceStage action input is required and must declare exact pieceId enum matching stage order`));
|
|
843
|
+
}
|
|
844
|
+
else {
|
|
845
|
+
const inp = action.input;
|
|
846
|
+
const props = (inp.properties ?? {});
|
|
847
|
+
const pieceIdProp = (props.pieceId ?? {});
|
|
848
|
+
const enumVals = Array.isArray(pieceIdProp.enum)
|
|
849
|
+
? pieceIdProp.enum
|
|
850
|
+
: [];
|
|
851
|
+
if (inp.type !== "object" ||
|
|
852
|
+
inp.additionalProperties !== false ||
|
|
853
|
+
!Array.isArray(inp.required) ||
|
|
854
|
+
inp.required.length !== 1 ||
|
|
855
|
+
inp.required[0] !== "pieceId" ||
|
|
856
|
+
Object.keys(props).length !== 1 ||
|
|
857
|
+
pieceIdProp.type !== "string" ||
|
|
858
|
+
enumVals.length !== stageOrder.length ||
|
|
859
|
+
!enumVals.every((id, idx) => id === stageOrder[idx])) {
|
|
860
|
+
issues.push(issue("UDL5013", `${actionPath}.input`, `pieceStage action input must declare exact pieceId enum matching stage order`));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
const variantMultiplicities = [];
|
|
864
|
+
for (const pieceId of stageOrder) {
|
|
865
|
+
const piece = instrument.piecePlan?.pieces.find((p) => p.id === pieceId);
|
|
866
|
+
if (!piece) {
|
|
867
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `piece id ${pieceId} in stage order is not declared in piecePlan`));
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
const expandedLeaves = [];
|
|
871
|
+
const capturedDestinations = new Set();
|
|
872
|
+
const consumedSources = new Set();
|
|
873
|
+
totalCumulativeCalls = 0;
|
|
874
|
+
const scope = new Map([
|
|
875
|
+
["piece", { kind: "piece", piece }],
|
|
876
|
+
["instance", { kind: "instance" }],
|
|
877
|
+
]);
|
|
878
|
+
for (const call of action.calls ?? []) {
|
|
879
|
+
const [lib, act] = call.action.split(".");
|
|
880
|
+
if (!lib || !act || !hasLibrary(lib)) {
|
|
881
|
+
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${lib ?? ""}`));
|
|
882
|
+
continue;
|
|
883
|
+
}
|
|
884
|
+
const targetActionDef = getAction(lib, act);
|
|
885
|
+
if (!targetActionDef) {
|
|
886
|
+
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${act} in library ${lib}`));
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
const callScope = resolveCallParameters(call, targetActionDef, scope, `${actionPath}.calls.${call.id}`, issues, piece);
|
|
890
|
+
expandPrivateAction(lib, act, callScope, [actionKey, call.id], [], publicPrincipal, capturedDestinations, consumedSources, expandedLeaves);
|
|
891
|
+
}
|
|
892
|
+
// Refuse input amount/destination binding in leaves and validate piece bindings
|
|
893
|
+
for (const leaf of expandedLeaves) {
|
|
894
|
+
const binds = leaf.step.bind ?? {};
|
|
895
|
+
for (const [k, b] of Object.entries(binds)) {
|
|
896
|
+
if ((k === "amount" ||
|
|
897
|
+
k === "sourceAccountId" ||
|
|
898
|
+
k === "destinationAccountId") &&
|
|
899
|
+
typeof b === "object" &&
|
|
900
|
+
b !== null &&
|
|
901
|
+
b.from === "input") {
|
|
902
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage leaves cannot bind amount or destination to input: ${k}`));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (leaf.step.operation.startsWith("internal_transfer.")) {
|
|
906
|
+
const amt = binds.amount;
|
|
907
|
+
if (!amt ||
|
|
908
|
+
typeof amt !== "object" ||
|
|
909
|
+
amt.from !== "instance" ||
|
|
910
|
+
amt.path !== `fields.${piece.amount}`) {
|
|
911
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage transfer amount must resolve to selected piece.amount (${piece.amount})`));
|
|
912
|
+
}
|
|
913
|
+
if (stage === "release") {
|
|
914
|
+
const dest = binds.destinationAccountId;
|
|
915
|
+
if (!dest ||
|
|
916
|
+
typeof dest !== "object" ||
|
|
917
|
+
dest.from !== "instance" ||
|
|
918
|
+
dest.path !==
|
|
919
|
+
`fields.${piece.release_to}`) {
|
|
920
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage release destination must resolve to selected piece.release_to (${piece.release_to})`));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
else if (stage === "refund") {
|
|
924
|
+
const dest = binds.destinationAccountId;
|
|
925
|
+
if (!dest ||
|
|
926
|
+
typeof dest !== "object" ||
|
|
927
|
+
dest.from !== "instance" ||
|
|
928
|
+
dest.path !== `fields.${piece.refund_to}`) {
|
|
929
|
+
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage refund destination must resolve to selected piece.refund_to (${piece.refund_to})`));
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
const counts = new Map();
|
|
935
|
+
for (const leaf of expandedLeaves) {
|
|
936
|
+
for (const eff of leaf.effects) {
|
|
937
|
+
counts.set(eff.signature, (counts.get(eff.signature) ?? 0) + 1);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
variantMultiplicities.push(counts);
|
|
941
|
+
const directEffects = deriveUdlActionEffects(action, udlClauseVocabulary);
|
|
942
|
+
const leafEffects = action.calls && action.calls.length > 0
|
|
943
|
+
? deriveActionEffectsFromPlan(expandedLeaves)
|
|
944
|
+
: {};
|
|
945
|
+
const effects = combineDerivedEffects(directEffects, leafEffects);
|
|
946
|
+
plans.push({
|
|
947
|
+
action: actionKey,
|
|
948
|
+
effects,
|
|
949
|
+
leaves: expandedLeaves,
|
|
950
|
+
pieceId,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
// Verify identical effect signature multiplicities across piece variants
|
|
954
|
+
if (variantMultiplicities.length > 1) {
|
|
955
|
+
const baseCounts = variantMultiplicities[0];
|
|
956
|
+
for (let i = 1; i < variantMultiplicities.length; i++) {
|
|
957
|
+
const comp = variantMultiplicities[i];
|
|
958
|
+
let identical = baseCounts.size === comp.size;
|
|
959
|
+
if (identical) {
|
|
960
|
+
for (const [sig, count] of baseCounts.entries()) {
|
|
961
|
+
if (comp.get(sig) !== count) {
|
|
962
|
+
identical = false;
|
|
963
|
+
break;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (!identical) {
|
|
968
|
+
issues.push(issue("UDL2013", `${actionPath}.pieceStage`, `piece variants of action ${actionKey} have differing effect signature multiplicities`));
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
else if (action.calls && action.calls.length > 0) {
|
|
975
|
+
const expandedLeaves = [];
|
|
976
|
+
const capturedDestinations = new Set();
|
|
977
|
+
const consumedSources = new Set();
|
|
978
|
+
totalCumulativeCalls = 0;
|
|
979
|
+
const scope = new Map([
|
|
980
|
+
["instance", { kind: "instance" }],
|
|
981
|
+
]);
|
|
982
|
+
for (const call of action.calls) {
|
|
983
|
+
const [lib, act] = call.action.split(".");
|
|
984
|
+
if (!lib || !act || !hasLibrary(lib)) {
|
|
985
|
+
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${lib ?? ""}`));
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
const targetActionDef = getAction(lib, act);
|
|
989
|
+
if (!targetActionDef) {
|
|
990
|
+
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${act} in library ${lib}`));
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
const callScope = resolveCallParameters(call, targetActionDef, scope, `${actionPath}.calls.${call.id}`, issues);
|
|
994
|
+
expandPrivateAction(lib, act, callScope, [actionKey, call.id], [], publicPrincipal, capturedDestinations, consumedSources, expandedLeaves);
|
|
995
|
+
}
|
|
996
|
+
const directEffects = deriveUdlActionEffects(action, udlClauseVocabulary);
|
|
997
|
+
const leafEffects = deriveActionEffectsFromPlan(expandedLeaves);
|
|
998
|
+
const effects = combineDerivedEffects(directEffects, leafEffects);
|
|
999
|
+
plans.push({
|
|
1000
|
+
action: actionKey,
|
|
1001
|
+
effects,
|
|
1002
|
+
leaves: expandedLeaves,
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
return { issues, plans };
|
|
1007
|
+
}
|
|
110
1008
|
//# sourceMappingURL=effects.js.map
|