@hyperscale0/udl 2.6.0 → 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.
- package/CHANGELOG.md +2 -322
- package/LICENSING.md +1 -2
- package/README.md +3 -114
- package/TRADEMARKS.md +2 -2
- package/dist/diagnostics.d.ts +21 -201
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +29 -193
- package/dist/diagnostics.js.map +1 -1
- package/dist/evolution.d.ts +3 -132
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +29 -633
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts +5 -72
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +233 -737
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +7 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -12
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts.map +1 -1
- package/dist/instrument-references.js +3 -2
- package/dist/instrument-references.js.map +1 -1
- package/dist/schema.d.ts +2079 -4564
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +270 -1609
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +8 -104
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +653 -3271
- package/dist/validation.js.map +1 -1
- package/docs/README.md +147 -14
- package/package.json +7 -11
- package/spec/README.md +151 -164
- package/spec/darb.udl.json +249 -0
- package/spec/udl.schema.json +4104 -3202
- package/src/diagnostics.ts +51 -246
- package/src/evolution.ts +39 -1045
- package/src/finance.ts +287 -1127
- package/src/index.ts +18 -142
- package/src/instrument-references.ts +3 -2
- package/src/schema.ts +285 -1843
- package/src/validation.ts +870 -5611
- package/conformance/README.md +0 -82
- package/conformance/evolution/action-contract.expected.json +0 -10
- package/conformance/evolution/action-contract.live.udl +0 -44
- package/conformance/evolution/action-contract.next.udl +0 -45
- package/conformance/evolution/product-identity.expected.json +0 -10
- package/conformance/evolution/product-identity.live.udl +0 -55
- package/conformance/evolution/product-identity.next.udl +0 -55
- package/conformance/evolution/version-required.expected.json +0 -10
- package/conformance/evolution/version-required.live.udl +0 -55
- package/conformance/evolution/version-required.next.udl +0 -58
- package/conformance/invalid/action-without-transition.expected.json +0 -10
- package/conformance/invalid/action-without-transition.udl +0 -60
- package/conformance/invalid/agent-description-too-long.expected.json +0 -10
- package/conformance/invalid/agent-description-too-long.udl +0 -57
- package/conformance/invalid/blank-title.expected.json +0 -10
- package/conformance/invalid/blank-title.udl +0 -54
- package/conformance/invalid/call-binds-results.expected.json +0 -10
- package/conformance/invalid/call-binds-results.udl +0 -314
- package/conformance/invalid/call-unknown-action.expected.json +0 -10
- package/conformance/invalid/call-unknown-action.udl +0 -314
- package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
- package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
- package/conformance/invalid/depth-budget.expected.json +0 -10
- package/conformance/invalid/depth-budget.udl +0 -49
- package/conformance/invalid/duplicate-subject.expected.json +0 -10
- package/conformance/invalid/duplicate-subject.udl +0 -230
- package/conformance/invalid/forged-effects.expected.json +0 -10
- package/conformance/invalid/forged-effects.udl +0 -76
- package/conformance/invalid/format-version.expected.json +0 -10
- package/conformance/invalid/format-version.udl +0 -54
- package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
- package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
- package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
- package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
- package/conformance/invalid/invalid-check-duration.expected.json +0 -10
- package/conformance/invalid/invalid-check-duration.udl +0 -219
- package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
- package/conformance/invalid/invalid-dial-anchor.udl +0 -219
- package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
- package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
- package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
- package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
- package/conformance/invalid/invalid-journeys.expected.json +0 -10
- package/conformance/invalid/invalid-journeys.udl +0 -77
- package/conformance/invalid/invalid-remainder.expected.json +0 -10
- package/conformance/invalid/invalid-remainder.udl +0 -220
- package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
- package/conformance/invalid/invalid-schema-keyword.udl +0 -220
- package/conformance/invalid/invalid-utf8.expected.json +0 -10
- package/conformance/invalid/invalid-utf8.udl +0 -1
- package/conformance/invalid/leaf-effect-mismatch.expected.json +0 -10
- package/conformance/invalid/leaf-effect-mismatch.udl +0 -314
- package/conformance/invalid/malformed-json.expected.json +0 -10
- package/conformance/invalid/malformed-json.udl +0 -1
- package/conformance/invalid/missing-create-action.expected.json +0 -10
- package/conformance/invalid/missing-create-action.udl +0 -48
- package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
- package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
- package/conformance/invalid/missing-exception-contract.expected.json +0 -14
- package/conformance/invalid/missing-exception-contract.udl +0 -2523
- package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
- package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
- package/conformance/invalid/not-an-object.expected.json +0 -10
- package/conformance/invalid/not-an-object.udl +0 -1
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
- package/conformance/invalid/piece-plan-without-partition.expected.json +0 -10
- package/conformance/invalid/piece-plan-without-partition.udl +0 -305
- package/conformance/invalid/private-action-independent-approval.expected.json +0 -10
- package/conformance/invalid/private-action-independent-approval.udl +0 -314
- package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
- package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
- package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
- package/conformance/invalid/quote-named-reference-gate.udl +0 -50
- package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
- package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
- package/conformance/invalid/unfund-order-not-reversed.expected.json +0 -10
- package/conformance/invalid/unfund-order-not-reversed.udl +0 -314
- package/conformance/invalid/unknown-key.expected.json +0 -10
- package/conformance/invalid/unknown-key.udl +0 -55
- package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
- package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
- package/conformance/invalid/unknown-required-field.expected.json +0 -10
- package/conformance/invalid/unknown-required-field.udl +0 -220
- package/conformance/invalid/unreachable-state.expected.json +0 -10
- package/conformance/invalid/unreachable-state.udl +0 -55
- package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
- package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
- package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
- package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
- package/conformance/valid/agent-description.expected.json +0 -6
- package/conformance/valid/agent-description.udl +0 -66
- package/conformance/valid/attested.expected.json +0 -6
- package/conformance/valid/attested.udl +0 -251
- package/conformance/valid/cards.expected.json +0 -6
- package/conformance/valid/cards.udl +0 -1579
- package/conformance/valid/commerce-escrow.expected.json +0 -6
- package/conformance/valid/commerce-escrow.udl +0 -1512
- package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
- package/conformance/valid/compiled-crowdfunding.udl +0 -1843
- package/conformance/valid/compiled-watch-club.expected.json +0 -6
- package/conformance/valid/compiled-watch-club.udl +0 -2486
- package/conformance/valid/complete-contract.expected.json +0 -6
- package/conformance/valid/complete-contract.udl +0 -218
- package/conformance/valid/effect-signatures.expected.json +0 -6
- package/conformance/valid/effect-signatures.udl +0 -75
- package/conformance/valid/hand-edited.expected.json +0 -6
- package/conformance/valid/hand-edited.udl +0 -1
- package/conformance/valid/insured-car-marketplace.expected.json +0 -6
- package/conformance/valid/insured-car-marketplace.udl +0 -1050
- package/conformance/valid/insured-travel.expected.json +0 -6
- package/conformance/valid/insured-travel.udl +0 -3469
- package/conformance/valid/minimal.expected.json +0 -6
- package/conformance/valid/minimal.udl +0 -62
- package/conformance/valid/piece-plan-calls.expected.json +0 -6
- package/conformance/valid/piece-plan-calls.udl +0 -314
- package/conformance/valid/protection.expected.json +0 -6
- package/conformance/valid/protection.udl +0 -1551
- package/conformance/valid/string-escaping.expected.json +0 -6
- package/conformance/valid/string-escaping.udl +0 -54
- package/conformance/valid/vocabulary.expected.json +0 -6
- package/conformance/valid/vocabulary.udl +0 -2012
- package/dist/allocation.d.ts +0 -60
- package/dist/allocation.d.ts.map +0 -1
- package/dist/allocation.js +0 -177
- package/dist/allocation.js.map +0 -1
- package/dist/check-profiles.d.ts +0 -57
- package/dist/check-profiles.d.ts.map +0 -1
- package/dist/check-profiles.js +0 -62
- package/dist/check-profiles.js.map +0 -1
- package/dist/distribution.d.ts +0 -15
- package/dist/distribution.d.ts.map +0 -1
- package/dist/distribution.js +0 -49
- package/dist/distribution.js.map +0 -1
- package/dist/effects.d.ts +0 -58
- package/dist/effects.d.ts.map +0 -1
- package/dist/effects.js +0 -1126
- package/dist/effects.js.map +0 -1
- package/dist/reference.d.ts +0 -3
- package/dist/reference.d.ts.map +0 -1
- package/dist/reference.js +0 -28
- package/dist/reference.js.map +0 -1
- package/dist/vocabulary.d.ts +0 -23
- package/dist/vocabulary.d.ts.map +0 -1
- package/dist/vocabulary.js +0 -1052
- package/dist/vocabulary.js.map +0 -1
- package/docs/funding-custody.md +0 -165
- package/docs/guide/01-a-document.md +0 -37
- package/docs/guide/02-money-steps.md +0 -23
- package/docs/guide/03-laws.md +0 -18
- package/docs/guide/04-fees-and-remainder.md +0 -36
- package/docs/guide/05-checks-updates-dials.md +0 -7
- package/docs/guide/06-effects.md +0 -11
- package/docs/guide/07-evolution.md +0 -11
- package/docs/guide/08-implementing.md +0 -13
- package/docs/guide/09-schedules-and-allocation.md +0 -132
- package/docs/llms-full.txt +0 -2008
- package/docs/llms.txt +0 -14
- package/docs/piece-plans.md +0 -148
- package/docs/reference/canonical.md +0 -16
- package/docs/reference/clauses.md +0 -1585
- package/docs/reference/cli.md +0 -24
- package/docs/reference/diagnostics.md +0 -38
- package/skills/udl/SKILL.md +0 -28
- package/src/allocation.ts +0 -259
- package/src/check-profiles.ts +0 -80
- package/src/distribution.ts +0 -61
- package/src/effects.ts +0 -1920
- package/src/reference.ts +0 -31
- package/src/vocabulary.ts +0 -1635
package/dist/effects.js
DELETED
|
@@ -1,1126 +0,0 @@
|
|
|
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";
|
|
5
|
-
function boundPath(move, endpoint) {
|
|
6
|
-
const binding = move.bind?.[endpoint];
|
|
7
|
-
if (binding === null || typeof binding !== "object")
|
|
8
|
-
return undefined;
|
|
9
|
-
const path = binding.path;
|
|
10
|
-
return typeof path === "string" ? path : undefined;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Maps a UDL money operation to the meter and pricing class that owns it.
|
|
14
|
-
* Internal-transfer endpoint bindings are product accounts by construction.
|
|
15
|
-
* External collection, deposit, and payout operations carry their role in the
|
|
16
|
-
* operation family because their remote endpoint is not a UDL account binding.
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* The typed kind of an instance ref a call binding may name. Refs exist only
|
|
20
|
-
* through step and move captures on the same instrument; a capture of a leaf's
|
|
21
|
-
* `accountId` output is an account, every other capture is opaque text.
|
|
22
|
-
*/
|
|
23
|
-
function capturedRefKind(instrument, refName) {
|
|
24
|
-
for (const action of Object.values(instrument.actions)) {
|
|
25
|
-
for (const step of [...action.steps, ...action.moves]) {
|
|
26
|
-
const output = step.capture?.[refName];
|
|
27
|
-
if (output !== undefined) {
|
|
28
|
-
return output === "accountId" ? "account" : "text";
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
export function movementClass(move) {
|
|
35
|
-
if (move.operation.startsWith("internal_transfer.")) {
|
|
36
|
-
const source = boundPath(move, "sourceAccountId");
|
|
37
|
-
const destination = boundPath(move, "destinationAccountId");
|
|
38
|
-
if (source?.startsWith("fields.") &&
|
|
39
|
-
destination === "refs.escrowAccountId") {
|
|
40
|
-
return "collection.pay_in";
|
|
41
|
-
}
|
|
42
|
-
return "transfer.internal";
|
|
43
|
-
}
|
|
44
|
-
if (move.operation.startsWith("collection.pay_in.")) {
|
|
45
|
-
return "collection.pay_in";
|
|
46
|
-
}
|
|
47
|
-
if (move.operation.startsWith("deposit.")) {
|
|
48
|
-
return "deposit.attributed";
|
|
49
|
-
}
|
|
50
|
-
if (move.operation.startsWith("payout.")) {
|
|
51
|
-
return "payout.external";
|
|
52
|
-
}
|
|
53
|
-
throw new Error(`cannot classify UDL movement operation ${move.operation}`);
|
|
54
|
-
}
|
|
55
|
-
/** Derive the ABI effect rows declared by the action-clause vocabulary. */
|
|
56
|
-
export function deriveUdlActionEffects(action, vocabulary) {
|
|
57
|
-
const effects = {};
|
|
58
|
-
for (const clause of vocabulary) {
|
|
59
|
-
if (clause.scope !== "action" || !clause.effects)
|
|
60
|
-
continue;
|
|
61
|
-
const value = actionClauseValue(action, clause.target);
|
|
62
|
-
if (value === undefined)
|
|
63
|
-
continue;
|
|
64
|
-
for (const descriptor of clause.effects) {
|
|
65
|
-
const values = descriptor.per === "element" && Array.isArray(value) ? value : [value];
|
|
66
|
-
for (const [index, candidate] of values.entries()) {
|
|
67
|
-
const object = recordValue(candidate);
|
|
68
|
-
if (descriptor.kind === "holds" &&
|
|
69
|
-
"fixed" in descriptor.signature &&
|
|
70
|
-
descriptor.signature.fixed === "reserve" &&
|
|
71
|
-
object?.operation !== "internal_transfer.reserve") {
|
|
72
|
-
continue;
|
|
73
|
-
}
|
|
74
|
-
if (descriptor.kind === "moves" &&
|
|
75
|
-
((clause.target === "allocate" && object?.mode === "write_off") ||
|
|
76
|
-
(clause.target === "receiptDistribution" &&
|
|
77
|
-
object?.mode === "loss")))
|
|
78
|
-
continue;
|
|
79
|
-
const suffix = effectSignatureSuffix(descriptor, object);
|
|
80
|
-
if (!suffix)
|
|
81
|
-
continue;
|
|
82
|
-
(effects[descriptor.kind] ??= []).push({
|
|
83
|
-
signature: `${descriptor.kind}.${suffix}`,
|
|
84
|
-
source: descriptor.per === "element"
|
|
85
|
-
? `${clause.target}[${index}]`
|
|
86
|
-
: clause.target,
|
|
87
|
-
...(descriptor.kind === "notifies" &&
|
|
88
|
-
typeof object?.channel === "string" &&
|
|
89
|
-
typeof object.role === "string"
|
|
90
|
-
? { channel: object.channel, role: object.role }
|
|
91
|
-
: {}),
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
return effects;
|
|
97
|
-
}
|
|
98
|
-
function actionClauseValue(action, target) {
|
|
99
|
-
const [head, tail] = target.split(".");
|
|
100
|
-
if (!head)
|
|
101
|
-
return undefined;
|
|
102
|
-
const value = head === "transitionsRefs"
|
|
103
|
-
? [
|
|
104
|
-
...(Array.isArray(action.transitionsRefs)
|
|
105
|
-
? action.transitionsRefs
|
|
106
|
-
: []),
|
|
107
|
-
...(Array.isArray(action.requiresRefs)
|
|
108
|
-
? action.requiresRefs
|
|
109
|
-
: []).flatMap((gate) => {
|
|
110
|
-
const requirement = recordValue(gate);
|
|
111
|
-
const attests = recordValue(requirement?.attests);
|
|
112
|
-
return attests
|
|
113
|
-
? [{ field: requirement?.field, action: attests.consume }]
|
|
114
|
-
: [];
|
|
115
|
-
}),
|
|
116
|
-
]
|
|
117
|
-
: action[head];
|
|
118
|
-
if (head === "transitionsRefs" && Array.isArray(value) && value.length === 0)
|
|
119
|
-
return undefined;
|
|
120
|
-
if (!tail)
|
|
121
|
-
return value;
|
|
122
|
-
return recordValue(value)?.[tail];
|
|
123
|
-
}
|
|
124
|
-
function effectSignatureSuffix(descriptor, value) {
|
|
125
|
-
if ("fixed" in descriptor.signature)
|
|
126
|
-
return descriptor.signature.fixed;
|
|
127
|
-
if ("fromField" in descriptor.signature) {
|
|
128
|
-
const field = value?.[descriptor.signature.fromField];
|
|
129
|
-
return typeof field === "string" ? field : undefined;
|
|
130
|
-
}
|
|
131
|
-
if (!value || !("movementClass" in descriptor.signature))
|
|
132
|
-
return undefined;
|
|
133
|
-
const operation = value.operation;
|
|
134
|
-
const bind = recordValue(value.bind);
|
|
135
|
-
return typeof operation === "string"
|
|
136
|
-
? movementClass({ ...(bind ? { bind } : {}), operation })
|
|
137
|
-
: undefined;
|
|
138
|
-
}
|
|
139
|
-
function recordValue(value) {
|
|
140
|
-
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
141
|
-
? value
|
|
142
|
-
: undefined;
|
|
143
|
-
}
|
|
144
|
-
export function deriveActionEffectsFromPlan(leaves) {
|
|
145
|
-
const effects = {};
|
|
146
|
-
for (const leaf of leaves) {
|
|
147
|
-
for (const eff of leaf.effects) {
|
|
148
|
-
const kind = eff.kind;
|
|
149
|
-
(effects[kind] ??= []).push({
|
|
150
|
-
signature: eff.signature,
|
|
151
|
-
source: leaf.originPath.join("."),
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
return effects;
|
|
156
|
-
}
|
|
157
|
-
function combineDerivedEffects(direct, leaves) {
|
|
158
|
-
const result = {};
|
|
159
|
-
for (const kind of udlEffectKinds) {
|
|
160
|
-
const directRows = direct[kind] ?? [];
|
|
161
|
-
const leafRows = leaves[kind] ?? [];
|
|
162
|
-
if (directRows.length > 0 || leafRows.length > 0) {
|
|
163
|
-
result[kind] = [...directRows, ...leafRows];
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
return result;
|
|
167
|
-
}
|
|
168
|
-
function encodeOriginPathKey(originPath) {
|
|
169
|
-
const parts = originPath.map((seg) => `${seg.length}_${seg}`);
|
|
170
|
-
return `k_${parts.join("_")}`;
|
|
171
|
-
}
|
|
172
|
-
function sameEffectSignatures(left, right) {
|
|
173
|
-
const count = (effects) => {
|
|
174
|
-
const counts = new Map();
|
|
175
|
-
for (const eff of effects) {
|
|
176
|
-
const key = `${eff.kind}:${eff.signature}`;
|
|
177
|
-
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
178
|
-
}
|
|
179
|
-
return counts;
|
|
180
|
-
};
|
|
181
|
-
const leftCounts = count(left);
|
|
182
|
-
const rightCounts = count(right);
|
|
183
|
-
if (leftCounts.size !== rightCounts.size)
|
|
184
|
-
return false;
|
|
185
|
-
for (const [key, n] of leftCounts) {
|
|
186
|
-
if (rightCounts.get(key) !== n)
|
|
187
|
-
return false;
|
|
188
|
-
}
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
function expectedLeafEffects(step) {
|
|
192
|
-
if (step.operation === "internal_transfer.reserve") {
|
|
193
|
-
const moveClass = movementClass(step);
|
|
194
|
-
return [
|
|
195
|
-
{ kind: "moves", signature: `moves.${moveClass}` },
|
|
196
|
-
{ kind: "holds", signature: "holds.reserve" },
|
|
197
|
-
];
|
|
198
|
-
}
|
|
199
|
-
if (step.operation.startsWith("internal_transfer.")) {
|
|
200
|
-
const moveClass = movementClass(step);
|
|
201
|
-
return [{ kind: "moves", signature: `moves.${moveClass}` }];
|
|
202
|
-
}
|
|
203
|
-
if (step.operation === "account.escrow.provision") {
|
|
204
|
-
return [{ kind: "holds", signature: "holds.escrow" }];
|
|
205
|
-
}
|
|
206
|
-
if (step.operation === "account.freeze") {
|
|
207
|
-
return [{ kind: "holds", signature: "holds.freeze" }];
|
|
208
|
-
}
|
|
209
|
-
if (step.operation === "account.unfreeze") {
|
|
210
|
-
return [{ kind: "holds", signature: "holds.unfreeze" }];
|
|
211
|
-
}
|
|
212
|
-
return [];
|
|
213
|
-
}
|
|
214
|
-
const accountPattern = "^acct_(sandbox|live)_[a-z0-9]{8,64}$";
|
|
215
|
-
const positiveMoneyPattern = "^[1-9][0-9]{0,17}$";
|
|
216
|
-
const nonNegativeMoneyPattern = "^(0|[1-9][0-9]{0,17})$";
|
|
217
|
-
const currencyPattern = "^[A-Z]{3}$";
|
|
218
|
-
function isMoneySchema(schema) {
|
|
219
|
-
if (!schema || typeof schema !== "object")
|
|
220
|
-
return false;
|
|
221
|
-
const s = schema;
|
|
222
|
-
return (s.type === "string" &&
|
|
223
|
-
(s.pattern === positiveMoneyPattern ||
|
|
224
|
-
s.pattern === nonNegativeMoneyPattern));
|
|
225
|
-
}
|
|
226
|
-
function isAccountSchema(schema) {
|
|
227
|
-
if (!schema || typeof schema !== "object")
|
|
228
|
-
return false;
|
|
229
|
-
const s = schema;
|
|
230
|
-
return s.type === "string" && s.pattern === accountPattern;
|
|
231
|
-
}
|
|
232
|
-
function isStringSchema(schema) {
|
|
233
|
-
if (!schema || typeof schema !== "object")
|
|
234
|
-
return false;
|
|
235
|
-
const s = schema;
|
|
236
|
-
return s.type === "string";
|
|
237
|
-
}
|
|
238
|
-
function isDeniedCrossTenantSegment(segment) {
|
|
239
|
-
return (segment === "tenant" ||
|
|
240
|
-
segment === "tenantId" ||
|
|
241
|
-
segment === "org" ||
|
|
242
|
-
segment === "environment");
|
|
243
|
-
}
|
|
244
|
-
function checkCrossTenant(rawBind) {
|
|
245
|
-
if (rawBind.startsWith("/"))
|
|
246
|
-
return true;
|
|
247
|
-
const segments = rawBind.split(/[./]/);
|
|
248
|
-
return segments.some(isDeniedCrossTenantSegment);
|
|
249
|
-
}
|
|
250
|
-
function instrumentConcreteCurrency(instrument) {
|
|
251
|
-
const mutableFields = new Set(instrument.update?.fields ?? []);
|
|
252
|
-
const allUpdatedFields = new Set(Object.values(instrument.actions).flatMap((a) => a.updates ?? []));
|
|
253
|
-
const currencyFields = Object.entries(instrument.fields).filter(([, schema]) => typeof schema === "object" &&
|
|
254
|
-
schema !== null &&
|
|
255
|
-
schema.pattern === currencyPattern);
|
|
256
|
-
if (currencyFields.length === 1) {
|
|
257
|
-
const [fieldName, fieldDefObj] = currencyFields[0];
|
|
258
|
-
if (mutableFields.has(fieldName) || allUpdatedFields.has(fieldName)) {
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
const fieldDef = fieldDefObj;
|
|
262
|
-
if (typeof fieldDef.const === "string" &&
|
|
263
|
-
/^[A-Z]{3}$/.test(fieldDef.const)) {
|
|
264
|
-
return fieldDef.const;
|
|
265
|
-
}
|
|
266
|
-
if (Array.isArray(fieldDef.enum) &&
|
|
267
|
-
fieldDef.enum.length === 1 &&
|
|
268
|
-
typeof fieldDef.enum[0] === "string" &&
|
|
269
|
-
/^[A-Z]{3}$/.test(fieldDef.enum[0])) {
|
|
270
|
-
return fieldDef.enum[0];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
function pieceCurrency(piece, instrument) {
|
|
276
|
-
const amountSchema = instrument.fields[piece.amount];
|
|
277
|
-
if (amountSchema &&
|
|
278
|
-
typeof amountSchema["x-hyperscale-currency"] === "string" &&
|
|
279
|
-
/^[A-Z]{3}$/.test(amountSchema["x-hyperscale-currency"])) {
|
|
280
|
-
return amountSchema["x-hyperscale-currency"];
|
|
281
|
-
}
|
|
282
|
-
return instrumentConcreteCurrency(instrument);
|
|
283
|
-
}
|
|
284
|
-
export function resolveUdlActionPlans(instrument) {
|
|
285
|
-
const issues = [];
|
|
286
|
-
const plans = [];
|
|
287
|
-
if (instrument.piecePlan) {
|
|
288
|
-
for (const piece of instrument.piecePlan.pieces) {
|
|
289
|
-
if (pieceCurrency(piece, instrument) === undefined) {
|
|
290
|
-
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`));
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
const libraryMap = instrument.actionLibrary ?? {};
|
|
295
|
-
function hasLibrary(lib) {
|
|
296
|
-
if (!instrument.actionLibrary)
|
|
297
|
-
return false;
|
|
298
|
-
return Object.hasOwn(instrument.actionLibrary, lib);
|
|
299
|
-
}
|
|
300
|
-
function getAction(lib, act) {
|
|
301
|
-
if (!instrument.actionLibrary)
|
|
302
|
-
return undefined;
|
|
303
|
-
if (!Object.hasOwn(instrument.actionLibrary, lib))
|
|
304
|
-
return undefined;
|
|
305
|
-
const mod = instrument.actionLibrary[lib];
|
|
306
|
-
if (!mod ||
|
|
307
|
-
typeof mod !== "object" ||
|
|
308
|
-
!mod.actions ||
|
|
309
|
-
typeof mod.actions !== "object")
|
|
310
|
-
return undefined;
|
|
311
|
-
if (!Object.hasOwn(mod.actions, act))
|
|
312
|
-
return undefined;
|
|
313
|
-
return mod.actions[act];
|
|
314
|
-
}
|
|
315
|
-
// Check all private actions for structure, complete order, collisions, and authority
|
|
316
|
-
for (const [libKey, libModule] of Object.entries(libraryMap)) {
|
|
317
|
-
const libPath = `$.actionLibrary.${libKey}`;
|
|
318
|
-
const actionKeys = Object.keys(libModule.actions);
|
|
319
|
-
const orderSet = new Set(libModule.actionOrder);
|
|
320
|
-
if (new Set(libModule.actionOrder).size !== libModule.actionOrder.length) {
|
|
321
|
-
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `actionOrder in library ${libKey} contains duplicate action names`));
|
|
322
|
-
}
|
|
323
|
-
for (const actionKey of actionKeys) {
|
|
324
|
-
if (!orderSet.has(actionKey)) {
|
|
325
|
-
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `action ${actionKey} is declared in library ${libKey} but missing from actionOrder`));
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
for (const orderKey of libModule.actionOrder) {
|
|
329
|
-
if (!Object.hasOwn(libModule.actions, orderKey)) {
|
|
330
|
-
issues.push(issue("UDL2010", `${libPath}.actionOrder`, `actionOrder in library ${libKey} references unknown action ${orderKey}`));
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
for (const [actKey, privAction] of Object.entries(libModule.actions)) {
|
|
334
|
-
const actPath = `${libPath}.actions.${actKey}`;
|
|
335
|
-
if (privAction.approval === "independent") {
|
|
336
|
-
issues.push(issue("UDL2012", `${actPath}.approval`, `private action ${libKey}.${actKey} cannot declare independent approval`));
|
|
337
|
-
}
|
|
338
|
-
if (privAction.recovery === "external") {
|
|
339
|
-
issues.push(issue("UDL2012", `${actPath}.recovery`, `private action ${libKey}.${actKey} cannot declare external recovery`));
|
|
340
|
-
}
|
|
341
|
-
const leafIds = privAction.leaves.map((l) => l.id);
|
|
342
|
-
const callIds = privAction.calls.map((c) => c.id);
|
|
343
|
-
const leafIdSet = new Set(leafIds);
|
|
344
|
-
const callIdSet = new Set(callIds);
|
|
345
|
-
if (leafIdSet.size !== leafIds.length) {
|
|
346
|
-
issues.push(issue("UDL2010", `${actPath}.leaves`, `duplicate leaf id in private action ${libKey}.${actKey}`));
|
|
347
|
-
}
|
|
348
|
-
if (callIdSet.size !== callIds.length) {
|
|
349
|
-
issues.push(issue("UDL2010", `${actPath}.calls`, `duplicate call id in private action ${libKey}.${actKey}`));
|
|
350
|
-
}
|
|
351
|
-
for (const id of leafIds) {
|
|
352
|
-
if (callIdSet.has(id)) {
|
|
353
|
-
issues.push(issue("UDL2010", `${actPath}.order`, `collision between leaf id and call id ${id} in private action ${libKey}.${actKey}`));
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
const allIds = new Set([...leafIds, ...callIds]);
|
|
357
|
-
const orderIds = privAction.order;
|
|
358
|
-
if (orderIds.length !== allIds.size ||
|
|
359
|
-
!orderIds.every((id) => allIds.has(id)) ||
|
|
360
|
-
new Set(orderIds).size !== orderIds.length) {
|
|
361
|
-
issues.push(issue("UDL2010", `${actPath}.order`, `order in private action ${libKey}.${actKey} must be an exact permutation of leaf and call ids`));
|
|
362
|
-
}
|
|
363
|
-
for (const call of privAction.calls) {
|
|
364
|
-
const [targetLib, targetAct] = call.action.split(".");
|
|
365
|
-
if (!targetLib || !targetAct || !hasLibrary(targetLib)) {
|
|
366
|
-
issues.push(issue("UDL2010", `${actPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${targetLib ?? ""}`));
|
|
367
|
-
}
|
|
368
|
-
else {
|
|
369
|
-
const targetActionDef = getAction(targetLib, targetAct);
|
|
370
|
-
if (!targetActionDef) {
|
|
371
|
-
issues.push(issue("UDL2010", `${actPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${targetAct} in library ${targetLib}`));
|
|
372
|
-
}
|
|
373
|
-
else {
|
|
374
|
-
for (const key of Object.keys(call.bind)) {
|
|
375
|
-
if (!Object.hasOwn(targetActionDef.parameters, key)) {
|
|
376
|
-
issues.push(issue("UDL2011", `${actPath}.calls.${call.id}.bind.${key}`, `unexpected parameter ${key} in call to ${call.action}`));
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
for (const paramName of Object.keys(targetActionDef.parameters)) {
|
|
380
|
-
if (!Object.hasOwn(call.bind, paramName)) {
|
|
381
|
-
issues.push(issue("UDL2011", `${actPath}.calls.${call.id}.bind.${paramName}`, `missing binding for parameter ${paramName} in call to ${call.action}`));
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
// Cycle detection in private actions
|
|
390
|
-
const actionVisited = new Set();
|
|
391
|
-
const recursionStack = new Set();
|
|
392
|
-
function checkCycle(currentActionRef, path) {
|
|
393
|
-
const [lib, act] = currentActionRef.split(".");
|
|
394
|
-
if (path.length > UDL_LIMITS.maxDepth) {
|
|
395
|
-
issues.push(issue("UDL2010", `$.actionLibrary.${lib ?? ""}.actions.${act ?? ""}`, `action call depth exceeds maximum depth of ${UDL_LIMITS.maxDepth}`));
|
|
396
|
-
return true;
|
|
397
|
-
}
|
|
398
|
-
actionVisited.add(currentActionRef);
|
|
399
|
-
recursionStack.add(currentActionRef);
|
|
400
|
-
const actionDef = lib && act ? getAction(lib, act) : undefined;
|
|
401
|
-
if (actionDef) {
|
|
402
|
-
for (const call of actionDef.calls) {
|
|
403
|
-
const [targetLib, targetAct] = call.action.split(".");
|
|
404
|
-
if (!targetLib || !targetAct || !getAction(targetLib, targetAct)) {
|
|
405
|
-
continue;
|
|
406
|
-
}
|
|
407
|
-
if (recursionStack.has(call.action)) {
|
|
408
|
-
issues.push(issue("UDL2010", `$.actionLibrary.${lib}.actions.${act}`, `action call cycle detected: ${[...path, call.action].join(" -> ")}`));
|
|
409
|
-
return true;
|
|
410
|
-
}
|
|
411
|
-
if (!actionVisited.has(call.action)) {
|
|
412
|
-
if (checkCycle(call.action, [...path, call.action]))
|
|
413
|
-
return true;
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
recursionStack.delete(currentActionRef);
|
|
418
|
-
return false;
|
|
419
|
-
}
|
|
420
|
-
for (const [libKey, libModule] of Object.entries(libraryMap)) {
|
|
421
|
-
for (const actKey of Object.keys(libModule.actions)) {
|
|
422
|
-
const ref = `${libKey}.${actKey}`;
|
|
423
|
-
if (!actionVisited.has(ref)) {
|
|
424
|
-
checkCycle(ref, [ref]);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
function resolveBinding(rawBind, scope, leafPath) {
|
|
429
|
-
if (rawBind.includes("$results")) {
|
|
430
|
-
issues.push(issue("UDL2011", leafPath, `results is not an implicit binding scope: ${rawBind}`));
|
|
431
|
-
return undefined;
|
|
432
|
-
}
|
|
433
|
-
if (checkCrossTenant(rawBind)) {
|
|
434
|
-
issues.push(issue("UDL2012", leafPath, `cross-tenant field paths are not allowed: ${rawBind}`));
|
|
435
|
-
return undefined;
|
|
436
|
-
}
|
|
437
|
-
if (rawBind === "$instance") {
|
|
438
|
-
return {
|
|
439
|
-
binding: { from: "instance", path: "instrumentInstanceId" },
|
|
440
|
-
kind: "instance",
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
|
-
if (rawBind === "$piece") {
|
|
444
|
-
const pieceVal = scope.get("piece");
|
|
445
|
-
if (!pieceVal || pieceVal.kind !== "piece") {
|
|
446
|
-
issues.push(issue("UDL2011", leafPath, `$piece reference is not available in caller scope`));
|
|
447
|
-
return undefined;
|
|
448
|
-
}
|
|
449
|
-
return {
|
|
450
|
-
binding: { from: "const", value: pieceVal.piece.id },
|
|
451
|
-
kind: "piece",
|
|
452
|
-
piece: pieceVal.piece,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
if (rawBind.startsWith("$fields.")) {
|
|
456
|
-
const parts = rawBind.slice("$fields.".length).split(".");
|
|
457
|
-
if (parts.length !== 1) {
|
|
458
|
-
issues.push(issue("UDL2011", leafPath, `invalid member access on field: ${rawBind}`));
|
|
459
|
-
return undefined;
|
|
460
|
-
}
|
|
461
|
-
const fieldName = parts[0];
|
|
462
|
-
const fieldSchema = instrument.fields[fieldName];
|
|
463
|
-
if (!fieldSchema) {
|
|
464
|
-
issues.push(issue("UDL2011", leafPath, `referenced field ${fieldName} is not declared on instrument`));
|
|
465
|
-
return undefined;
|
|
466
|
-
}
|
|
467
|
-
if (isMoneySchema(fieldSchema)) {
|
|
468
|
-
const cur = fieldSchema["x-hyperscale-currency"] ?? instrumentConcreteCurrency(instrument);
|
|
469
|
-
return {
|
|
470
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
471
|
-
currency: cur,
|
|
472
|
-
kind: "money",
|
|
473
|
-
path: `fields.${fieldName}`,
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
if (isAccountSchema(fieldSchema)) {
|
|
477
|
-
return {
|
|
478
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
479
|
-
kind: "account",
|
|
480
|
-
path: `fields.${fieldName}`,
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
if (isStringSchema(fieldSchema)) {
|
|
484
|
-
return {
|
|
485
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
486
|
-
kind: "text",
|
|
487
|
-
path: `fields.${fieldName}`,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
issues.push(issue("UDL2011", leafPath, `field ${fieldName} is not an account, money, or text field`));
|
|
491
|
-
return undefined;
|
|
492
|
-
}
|
|
493
|
-
if (rawBind.startsWith("$")) {
|
|
494
|
-
const parts = rawBind.slice(1).split(".");
|
|
495
|
-
const paramName = parts[0];
|
|
496
|
-
const val = scope.get(paramName);
|
|
497
|
-
if (!val) {
|
|
498
|
-
issues.push(issue("UDL2011", leafPath, `unbound parameter $${paramName} in binding ${rawBind}`));
|
|
499
|
-
return undefined;
|
|
500
|
-
}
|
|
501
|
-
if (val.kind === "piece") {
|
|
502
|
-
if (parts.length === 1) {
|
|
503
|
-
return {
|
|
504
|
-
binding: { from: "const", value: val.piece.id },
|
|
505
|
-
kind: "piece",
|
|
506
|
-
piece: val.piece,
|
|
507
|
-
};
|
|
508
|
-
}
|
|
509
|
-
if (parts.length !== 2) {
|
|
510
|
-
issues.push(issue("UDL2011", leafPath, `invalid trailing member access on piece reference: ${rawBind}`));
|
|
511
|
-
return undefined;
|
|
512
|
-
}
|
|
513
|
-
const member = parts[1];
|
|
514
|
-
if (member === "id") {
|
|
515
|
-
return {
|
|
516
|
-
binding: { from: "const", value: val.piece.id },
|
|
517
|
-
kind: "text",
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
if (member === "amount") {
|
|
521
|
-
const cur = pieceCurrency(val.piece, instrument);
|
|
522
|
-
if (cur === undefined) {
|
|
523
|
-
issues.push(issue("UDL4002", leafPath, `piece ${val.piece.id} amount field lacks a concrete currency`));
|
|
524
|
-
}
|
|
525
|
-
return {
|
|
526
|
-
binding: { from: "instance", path: `fields.${val.piece.amount}` },
|
|
527
|
-
currency: cur ?? "UNKNOWN",
|
|
528
|
-
kind: "money",
|
|
529
|
-
path: `fields.${val.piece.amount}`,
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
if (member === "release_to") {
|
|
533
|
-
return {
|
|
534
|
-
binding: {
|
|
535
|
-
from: "instance",
|
|
536
|
-
path: `fields.${val.piece.release_to}`,
|
|
537
|
-
},
|
|
538
|
-
kind: "account",
|
|
539
|
-
path: `fields.${val.piece.release_to}`,
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
if (member === "refund_to") {
|
|
543
|
-
return {
|
|
544
|
-
binding: {
|
|
545
|
-
from: "instance",
|
|
546
|
-
path: `fields.${val.piece.refund_to}`,
|
|
547
|
-
},
|
|
548
|
-
kind: "account",
|
|
549
|
-
path: `fields.${val.piece.refund_to}`,
|
|
550
|
-
};
|
|
551
|
-
}
|
|
552
|
-
if (member === "currency") {
|
|
553
|
-
const cur = pieceCurrency(val.piece, instrument);
|
|
554
|
-
if (cur === undefined) {
|
|
555
|
-
issues.push(issue("UDL4002", leafPath, `piece ${val.piece.id} currency cannot be resolved to a concrete currency`));
|
|
556
|
-
}
|
|
557
|
-
return {
|
|
558
|
-
binding: { from: "const", value: cur ?? "UNKNOWN" },
|
|
559
|
-
kind: "text",
|
|
560
|
-
};
|
|
561
|
-
}
|
|
562
|
-
issues.push(issue("UDL2011", leafPath, `unknown piece member ${member} in ${rawBind}`));
|
|
563
|
-
return undefined;
|
|
564
|
-
}
|
|
565
|
-
if (val.kind === "instance") {
|
|
566
|
-
if (parts.length === 1) {
|
|
567
|
-
return {
|
|
568
|
-
binding: { from: "instance", path: "instrumentInstanceId" },
|
|
569
|
-
kind: "instance",
|
|
570
|
-
};
|
|
571
|
-
}
|
|
572
|
-
if (parts[1] === "fields") {
|
|
573
|
-
if (parts.length !== 3) {
|
|
574
|
-
issues.push(issue("UDL2011", leafPath, `invalid trailing member access on instance field: ${rawBind}`));
|
|
575
|
-
return undefined;
|
|
576
|
-
}
|
|
577
|
-
const fieldName = parts[2];
|
|
578
|
-
const fieldSchema = instrument.fields[fieldName];
|
|
579
|
-
if (!fieldSchema) {
|
|
580
|
-
issues.push(issue("UDL2011", leafPath, `referenced field ${fieldName} is not declared on instrument`));
|
|
581
|
-
return undefined;
|
|
582
|
-
}
|
|
583
|
-
if (isMoneySchema(fieldSchema)) {
|
|
584
|
-
const cur = fieldSchema["x-hyperscale-currency"] ??
|
|
585
|
-
instrumentConcreteCurrency(instrument);
|
|
586
|
-
return {
|
|
587
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
588
|
-
currency: cur,
|
|
589
|
-
kind: "money",
|
|
590
|
-
path: `fields.${fieldName}`,
|
|
591
|
-
};
|
|
592
|
-
}
|
|
593
|
-
if (isAccountSchema(fieldSchema)) {
|
|
594
|
-
return {
|
|
595
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
596
|
-
kind: "account",
|
|
597
|
-
path: `fields.${fieldName}`,
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
if (isStringSchema(fieldSchema)) {
|
|
601
|
-
return {
|
|
602
|
-
binding: { from: "instance", path: `fields.${fieldName}` },
|
|
603
|
-
kind: "text",
|
|
604
|
-
path: `fields.${fieldName}`,
|
|
605
|
-
};
|
|
606
|
-
}
|
|
607
|
-
issues.push(issue("UDL2011", leafPath, `field ${fieldName} is not an account, money, or text field`));
|
|
608
|
-
return undefined;
|
|
609
|
-
}
|
|
610
|
-
if (parts.length === 2 &&
|
|
611
|
-
(parts[1] === "instrumentInstanceId" || parts[1] === "productId")) {
|
|
612
|
-
return {
|
|
613
|
-
binding: { from: "instance", path: parts[1] },
|
|
614
|
-
kind: "text",
|
|
615
|
-
path: parts[1],
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
if (parts[1] === "refs") {
|
|
619
|
-
if (parts.length !== 3) {
|
|
620
|
-
issues.push(issue("UDL2011", leafPath, `invalid trailing member access on instance ref: ${rawBind}`));
|
|
621
|
-
return undefined;
|
|
622
|
-
}
|
|
623
|
-
const refName = parts[2];
|
|
624
|
-
const captured = capturedRefKind(instrument, refName);
|
|
625
|
-
if (!captured) {
|
|
626
|
-
issues.push(issue("UDL2011", leafPath, `referenced ref ${refName} is not captured by any step or move on instrument`));
|
|
627
|
-
return undefined;
|
|
628
|
-
}
|
|
629
|
-
return {
|
|
630
|
-
binding: { from: "instance", path: `refs.${refName}` },
|
|
631
|
-
kind: captured,
|
|
632
|
-
path: `refs.${refName}`,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
issues.push(issue("UDL2011", leafPath, `invalid or trailing member access on instance reference: ${rawBind}`));
|
|
636
|
-
return undefined;
|
|
637
|
-
}
|
|
638
|
-
// Money, Account, Text parameter references do not allow member access
|
|
639
|
-
if (parts.length > 1) {
|
|
640
|
-
issues.push(issue("UDL2011", leafPath, `member access is not allowed on ${val.kind} parameter: ${rawBind}`));
|
|
641
|
-
return undefined;
|
|
642
|
-
}
|
|
643
|
-
if (val.kind === "account") {
|
|
644
|
-
return {
|
|
645
|
-
binding: { from: "instance", path: val.path },
|
|
646
|
-
kind: "account",
|
|
647
|
-
path: val.path,
|
|
648
|
-
};
|
|
649
|
-
}
|
|
650
|
-
if (val.kind === "money") {
|
|
651
|
-
return {
|
|
652
|
-
binding: { from: "instance", path: val.path },
|
|
653
|
-
currency: val.currency,
|
|
654
|
-
kind: "money",
|
|
655
|
-
path: val.path,
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
if (val.kind === "text") {
|
|
659
|
-
return {
|
|
660
|
-
binding: val.path
|
|
661
|
-
? { from: "instance", path: val.path }
|
|
662
|
-
: { from: "const", value: val.value ?? "" },
|
|
663
|
-
kind: "text",
|
|
664
|
-
path: val.path,
|
|
665
|
-
value: val.value,
|
|
666
|
-
};
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
issues.push(issue("UDL2011", leafPath, `invalid or unsupported binding reference: ${rawBind}`));
|
|
670
|
-
return undefined;
|
|
671
|
-
}
|
|
672
|
-
function resolveCallParameters(call, targetActionDef, scope, callPath, issuesList, piece) {
|
|
673
|
-
const callScope = new Map();
|
|
674
|
-
// Check extra keys
|
|
675
|
-
for (const key of Object.keys(call.bind)) {
|
|
676
|
-
if (!Object.hasOwn(targetActionDef.parameters, key)) {
|
|
677
|
-
issuesList.push(issue("UDL2011", `${callPath}.bind.${key}`, `unexpected parameter ${key} in call to ${call.action}`));
|
|
678
|
-
}
|
|
679
|
-
}
|
|
680
|
-
// Check missing keys & resolve bindings
|
|
681
|
-
for (const [pName, pDef] of Object.entries(targetActionDef.parameters)) {
|
|
682
|
-
if (!Object.hasOwn(call.bind, pName)) {
|
|
683
|
-
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `missing binding for parameter ${pName} in call to ${call.action}`));
|
|
684
|
-
continue;
|
|
685
|
-
}
|
|
686
|
-
const raw = call.bind[pName];
|
|
687
|
-
const resolved = resolveBinding(raw, scope, `${callPath}.bind.${pName}`);
|
|
688
|
-
if (resolved) {
|
|
689
|
-
if (pDef.kind !== resolved.kind) {
|
|
690
|
-
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `parameter ${pName} expected kind ${pDef.kind} but received ${resolved.kind}`));
|
|
691
|
-
}
|
|
692
|
-
if (pDef.kind === "money" &&
|
|
693
|
-
pDef.currency &&
|
|
694
|
-
resolved.currency &&
|
|
695
|
-
pDef.currency !== resolved.currency) {
|
|
696
|
-
issuesList.push(issue("UDL2011", `${callPath}.bind.${pName}`, `currency mismatch for money parameter ${pName}: expected ${pDef.currency} but received ${resolved.currency}`));
|
|
697
|
-
}
|
|
698
|
-
if (resolved.kind === "piece") {
|
|
699
|
-
callScope.set(pName, {
|
|
700
|
-
kind: "piece",
|
|
701
|
-
piece: resolved.piece ?? piece,
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
else if (resolved.kind === "instance") {
|
|
705
|
-
callScope.set(pName, { kind: "instance" });
|
|
706
|
-
}
|
|
707
|
-
else if (resolved.kind === "account") {
|
|
708
|
-
callScope.set(pName, { kind: "account", path: resolved.path ?? "" });
|
|
709
|
-
}
|
|
710
|
-
else if (resolved.kind === "money") {
|
|
711
|
-
callScope.set(pName, {
|
|
712
|
-
currency: resolved.currency ?? pDef.currency,
|
|
713
|
-
kind: "money",
|
|
714
|
-
path: resolved.path ?? "",
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
else {
|
|
718
|
-
callScope.set(pName, {
|
|
719
|
-
kind: "text",
|
|
720
|
-
path: resolved.path,
|
|
721
|
-
value: resolved.value,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
return callScope;
|
|
727
|
-
}
|
|
728
|
-
// Counts private-action expansions for one public action plan. Reset before
|
|
729
|
-
// each plan so the bound is per plan, not per document.
|
|
730
|
-
let totalCumulativeCalls = 0;
|
|
731
|
-
function expandPrivateAction(targetLibKey, targetActionKey, callArgs, originPrefix, callChain, callerPrincipal, capturedDestinations, consumedSources, expandedLeaves) {
|
|
732
|
-
totalCumulativeCalls += 1;
|
|
733
|
-
if (totalCumulativeCalls > UDL_LIMITS.maxActionExpansion) {
|
|
734
|
-
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action exceeds cumulative call bound of ${UDL_LIMITS.maxActionExpansion}`));
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (callChain.length > UDL_LIMITS.maxDepth) {
|
|
738
|
-
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action recursion depth exceeded ${UDL_LIMITS.maxDepth}`));
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
const actionRef = `${targetLibKey}.${targetActionKey}`;
|
|
742
|
-
if (callChain.includes(actionRef)) {
|
|
743
|
-
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action graph cycle detected in call to ${actionRef}`));
|
|
744
|
-
return;
|
|
745
|
-
}
|
|
746
|
-
const privAction = getAction(targetLibKey, targetActionKey);
|
|
747
|
-
if (!privAction) {
|
|
748
|
-
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `call references unknown action ${targetActionKey} in library ${targetLibKey}`));
|
|
749
|
-
return;
|
|
750
|
-
}
|
|
751
|
-
if (privAction.principal !== callerPrincipal) {
|
|
752
|
-
issues.push(issue("UDL2012", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.principal`, `principal mismatch: caller requires ${callerPrincipal} but ${actionRef} requires ${privAction.principal}`));
|
|
753
|
-
}
|
|
754
|
-
const leavesMap = new Map(privAction.leaves.map((l) => [l.id, l]));
|
|
755
|
-
const callsMap = new Map(privAction.calls.map((c) => [c.id, c]));
|
|
756
|
-
for (const itemId of privAction.order) {
|
|
757
|
-
if (expandedLeaves.length >= UDL_LIMITS.maxActionLeaves) {
|
|
758
|
-
issues.push(issue("UDL2010", `$.actions.${originPrefix[0]}`, `action exceeds cumulative expanded leaf bound of ${UDL_LIMITS.maxActionLeaves}`));
|
|
759
|
-
return;
|
|
760
|
-
}
|
|
761
|
-
if (leavesMap.has(itemId)) {
|
|
762
|
-
const leaf = leavesMap.get(itemId);
|
|
763
|
-
const leafPath = `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.leaves.${leaf.id}`;
|
|
764
|
-
const resolvedBinds = {};
|
|
765
|
-
for (const [key, bindStr] of Object.entries(leaf.bind)) {
|
|
766
|
-
const resolved = resolveBinding(bindStr, callArgs, `${leafPath}.bind.${key}`);
|
|
767
|
-
if (resolved) {
|
|
768
|
-
resolvedBinds[key] = resolved.binding;
|
|
769
|
-
if (key === "amount" && resolved.kind !== "money") {
|
|
770
|
-
issues.push(issue("UDL2011", `${leafPath}.bind.${key}`, `amount operand must resolve to money`));
|
|
771
|
-
}
|
|
772
|
-
if ((key === "sourceAccountId" ||
|
|
773
|
-
key === "destinationAccountId" ||
|
|
774
|
-
key === "accountId") &&
|
|
775
|
-
resolved.kind !== "account") {
|
|
776
|
-
issues.push(issue("UDL2011", `${leafPath}.bind.${key}`, `${key} operand must resolve to account`));
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
// Check leaf evidence
|
|
781
|
-
if (!leaf.evidence || leaf.evidence.trim().length === 0) {
|
|
782
|
-
issues.push(issue("UDL2013", `${leafPath}.evidence`, `leaf evidence is mandatory and cannot be blank`));
|
|
783
|
-
}
|
|
784
|
-
// Check leaf captures: destination ref is the map key
|
|
785
|
-
if (leaf.capture) {
|
|
786
|
-
for (const capKey of Object.keys(leaf.capture)) {
|
|
787
|
-
if (capturedDestinations.has(capKey)) {
|
|
788
|
-
issues.push(issue("UDL2011", `${leafPath}.capture.${capKey}`, `duplicate capture destination ${capKey} across expanded leaves`));
|
|
789
|
-
}
|
|
790
|
-
capturedDestinations.add(capKey);
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
|
-
const originPath = [...originPrefix, leaf.id];
|
|
794
|
-
let step;
|
|
795
|
-
if (leaf.operation.startsWith("internal_transfer.")) {
|
|
796
|
-
step = {
|
|
797
|
-
bind: resolvedBinds,
|
|
798
|
-
...(leaf.capture ? { capture: leaf.capture } : {}),
|
|
799
|
-
key: encodeOriginPathKey(originPath),
|
|
800
|
-
operation: leaf.operation,
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
else {
|
|
804
|
-
step = {
|
|
805
|
-
bind: resolvedBinds,
|
|
806
|
-
...(leaf.capture ? { capture: leaf.capture } : {}),
|
|
807
|
-
operation: leaf.operation,
|
|
808
|
-
};
|
|
809
|
-
}
|
|
810
|
-
// Check repeated consumption of money amount or hold identity
|
|
811
|
-
const amountSrc = boundPath(step, "amount");
|
|
812
|
-
if (amountSrc) {
|
|
813
|
-
if (consumedSources.has(amountSrc)) {
|
|
814
|
-
issues.push(issue("UDL2011", `${leafPath}.bind.amount`, `repeated consumption of money source ${amountSrc} in one expanded action`));
|
|
815
|
-
}
|
|
816
|
-
consumedSources.add(amountSrc);
|
|
817
|
-
}
|
|
818
|
-
const holdSrc = boundPath(step, "holdId");
|
|
819
|
-
if (holdSrc) {
|
|
820
|
-
const holdKey = `hold:${holdSrc}`;
|
|
821
|
-
if (consumedSources.has(holdKey)) {
|
|
822
|
-
issues.push(issue("UDL2011", `${leafPath}.bind.holdId`, `repeated consumption of hold ${holdSrc} in one expanded action`));
|
|
823
|
-
}
|
|
824
|
-
consumedSources.add(holdKey);
|
|
825
|
-
}
|
|
826
|
-
// A library leaf is written once and expanded under several callers,
|
|
827
|
-
// so its declaration names the operation's generic class (the class
|
|
828
|
-
// with no bindings). Each expansion then carries the class its
|
|
829
|
-
// resolved bindings select, exactly as an inline move would: a buyer
|
|
830
|
-
// funding escrow is a pay-in, the same leaf paying out is internal.
|
|
831
|
-
const expectedEffects = expectedLeafEffects(step);
|
|
832
|
-
const genericEffects = expectedLeafEffects({ ...step, bind: {} });
|
|
833
|
-
if (!sameEffectSignatures(leaf.effects, expectedEffects) &&
|
|
834
|
-
!sameEffectSignatures(leaf.effects, genericEffects)) {
|
|
835
|
-
issues.push(issue("UDL2013", `${leafPath}.effects`, `declared leaf effects do not match expected effects for operation ${leaf.operation}`));
|
|
836
|
-
}
|
|
837
|
-
expandedLeaves.push({
|
|
838
|
-
effects: expectedEffects,
|
|
839
|
-
evidence: leaf.evidence,
|
|
840
|
-
originPath,
|
|
841
|
-
step,
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
else if (callsMap.has(itemId)) {
|
|
845
|
-
const nextCall = callsMap.get(itemId);
|
|
846
|
-
const [nextLib, nextAct] = nextCall.action.split(".");
|
|
847
|
-
if (!nextLib || !nextAct || !hasLibrary(nextLib)) {
|
|
848
|
-
issues.push(issue("UDL2010", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`, `call ${nextCall.id} references unknown library ${nextLib ?? ""}`));
|
|
849
|
-
continue;
|
|
850
|
-
}
|
|
851
|
-
const nextActionDef = getAction(nextLib, nextAct);
|
|
852
|
-
if (!nextActionDef) {
|
|
853
|
-
issues.push(issue("UDL2010", `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}.action`, `call ${nextCall.id} references unknown action ${nextAct} in library ${nextLib}`));
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
const nextScope = resolveCallParameters(nextCall, nextActionDef, callArgs, `$.actionLibrary.${targetLibKey}.actions.${targetActionKey}.calls.${nextCall.id}`, issues);
|
|
857
|
-
expandPrivateAction(nextLib, nextAct, nextScope, [...originPrefix, nextCall.id], [...callChain, actionRef], privAction.principal, capturedDestinations, consumedSources, expandedLeaves);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
// Iterate over public actions in actionOrder
|
|
862
|
-
for (const actionKey of instrument.actionOrder) {
|
|
863
|
-
const action = instrument.actions[actionKey];
|
|
864
|
-
if (!action)
|
|
865
|
-
continue;
|
|
866
|
-
const actionPath = `$.actions.${actionKey}`;
|
|
867
|
-
const publicPrincipal = action.principal ?? "api_key";
|
|
868
|
-
// Reject mixed direct steps/moves and calls
|
|
869
|
-
if (action.calls &&
|
|
870
|
-
action.calls.length > 0 &&
|
|
871
|
-
(action.steps.length > 0 || action.moves.length > 0)) {
|
|
872
|
-
issues.push(issue("UDL2010", `${actionPath}.calls`, `action ${actionKey} cannot mix calls with direct steps or moves`));
|
|
873
|
-
}
|
|
874
|
-
if (action.calls) {
|
|
875
|
-
const publicCallIds = action.calls.map((c) => c.id);
|
|
876
|
-
if (new Set(publicCallIds).size !== publicCallIds.length) {
|
|
877
|
-
issues.push(issue("UDL2010", `${actionPath}.calls`, `duplicate call id in action ${actionKey}`));
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (action.pieceStage) {
|
|
881
|
-
if (!action.calls || action.calls.length === 0) {
|
|
882
|
-
issues.push(issue("UDL5013", `${actionPath}.calls`, `pieceStage action ${actionKey} must move money through calls, never through direct moves or steps`));
|
|
883
|
-
}
|
|
884
|
-
if (actionKey === "create") {
|
|
885
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `create action cannot declare pieceStage`));
|
|
886
|
-
}
|
|
887
|
-
if (!instrument.piecePlan ||
|
|
888
|
-
instrument.piecePlan.id !== action.pieceStage.plan) {
|
|
889
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage.plan`, `pieceStage references unknown plan ${action.pieceStage.plan}`));
|
|
890
|
-
}
|
|
891
|
-
const stage = action.pieceStage.stage;
|
|
892
|
-
const stageOrder = stage === "fund"
|
|
893
|
-
? (instrument.piecePlan?.fund_order ?? [])
|
|
894
|
-
: stage === "release"
|
|
895
|
-
? (instrument.piecePlan?.release_order ?? [])
|
|
896
|
-
: stage === "refund"
|
|
897
|
-
? (instrument.piecePlan?.refund_order ?? [])
|
|
898
|
-
: (instrument.piecePlan?.unfund_order ?? []);
|
|
899
|
-
if (stageOrder.length === 0) {
|
|
900
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage.stage`, `piece stage ${stage} is empty`));
|
|
901
|
-
}
|
|
902
|
-
// Check input must be present and exact
|
|
903
|
-
if (!action.input) {
|
|
904
|
-
issues.push(issue("UDL5013", `${actionPath}.input`, `pieceStage action input is required and must declare exact pieceId enum matching stage order`));
|
|
905
|
-
}
|
|
906
|
-
else {
|
|
907
|
-
const inp = action.input;
|
|
908
|
-
const props = (inp.properties ?? {});
|
|
909
|
-
const pieceIdProp = (props.pieceId ?? {});
|
|
910
|
-
const enumVals = Array.isArray(pieceIdProp.enum)
|
|
911
|
-
? pieceIdProp.enum
|
|
912
|
-
: [];
|
|
913
|
-
if (inp.type !== "object" ||
|
|
914
|
-
inp.additionalProperties !== false ||
|
|
915
|
-
!Array.isArray(inp.required) ||
|
|
916
|
-
inp.required.length !== 1 ||
|
|
917
|
-
inp.required[0] !== "pieceId" ||
|
|
918
|
-
Object.keys(props).length !== 1 ||
|
|
919
|
-
pieceIdProp.type !== "string" ||
|
|
920
|
-
enumVals.length !== stageOrder.length ||
|
|
921
|
-
!enumVals.every((id, idx) => id === stageOrder[idx])) {
|
|
922
|
-
issues.push(issue("UDL5013", `${actionPath}.input`, `pieceStage action input must declare exact pieceId enum matching stage order`));
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
const variantMultiplicities = [];
|
|
926
|
-
for (const pieceId of stageOrder) {
|
|
927
|
-
const piece = instrument.piecePlan?.pieces.find((p) => p.id === pieceId);
|
|
928
|
-
if (!piece) {
|
|
929
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `piece id ${pieceId} in stage order is not declared in piecePlan`));
|
|
930
|
-
continue;
|
|
931
|
-
}
|
|
932
|
-
const expandedLeaves = [];
|
|
933
|
-
const capturedDestinations = new Set();
|
|
934
|
-
const consumedSources = new Set();
|
|
935
|
-
totalCumulativeCalls = 0;
|
|
936
|
-
const scope = new Map([
|
|
937
|
-
["piece", { kind: "piece", piece }],
|
|
938
|
-
["instance", { kind: "instance" }],
|
|
939
|
-
]);
|
|
940
|
-
for (const call of action.calls ?? []) {
|
|
941
|
-
const [lib, act] = call.action.split(".");
|
|
942
|
-
if (!lib || !act || !hasLibrary(lib)) {
|
|
943
|
-
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${lib ?? ""}`));
|
|
944
|
-
continue;
|
|
945
|
-
}
|
|
946
|
-
const targetActionDef = getAction(lib, act);
|
|
947
|
-
if (!targetActionDef) {
|
|
948
|
-
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${act} in library ${lib}`));
|
|
949
|
-
continue;
|
|
950
|
-
}
|
|
951
|
-
const callScope = resolveCallParameters(call, targetActionDef, scope, `${actionPath}.calls.${call.id}`, issues, piece);
|
|
952
|
-
expandPrivateAction(lib, act, callScope, [actionKey, call.id], [], publicPrincipal, capturedDestinations, consumedSources, expandedLeaves);
|
|
953
|
-
}
|
|
954
|
-
// Refuse input amount/destination binding in leaves and validate piece bindings
|
|
955
|
-
for (const leaf of expandedLeaves) {
|
|
956
|
-
const binds = leaf.step.bind ?? {};
|
|
957
|
-
for (const [k, b] of Object.entries(binds)) {
|
|
958
|
-
if ((k === "amount" ||
|
|
959
|
-
k === "sourceAccountId" ||
|
|
960
|
-
k === "destinationAccountId") &&
|
|
961
|
-
typeof b === "object" &&
|
|
962
|
-
b !== null &&
|
|
963
|
-
b.from === "input") {
|
|
964
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage leaves cannot bind amount or destination to input: ${k}`));
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
if (leaf.step.operation.startsWith("internal_transfer.")) {
|
|
968
|
-
const amt = binds.amount;
|
|
969
|
-
if (!amt ||
|
|
970
|
-
typeof amt !== "object" ||
|
|
971
|
-
amt.from !== "instance" ||
|
|
972
|
-
amt.path !== `fields.${piece.amount}`) {
|
|
973
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage transfer amount must resolve to selected piece.amount (${piece.amount})`));
|
|
974
|
-
}
|
|
975
|
-
if (stage === "release") {
|
|
976
|
-
const dest = binds.destinationAccountId;
|
|
977
|
-
if (!dest ||
|
|
978
|
-
typeof dest !== "object" ||
|
|
979
|
-
dest.from !== "instance" ||
|
|
980
|
-
dest.path !==
|
|
981
|
-
`fields.${piece.release_to}`) {
|
|
982
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage release destination must resolve to selected piece.release_to (${piece.release_to})`));
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
else if (stage === "refund") {
|
|
986
|
-
const dest = binds.destinationAccountId;
|
|
987
|
-
if (!dest ||
|
|
988
|
-
typeof dest !== "object" ||
|
|
989
|
-
dest.from !== "instance" ||
|
|
990
|
-
dest.path !== `fields.${piece.refund_to}`) {
|
|
991
|
-
issues.push(issue("UDL5013", `${actionPath}.pieceStage`, `pieceStage refund destination must resolve to selected piece.refund_to (${piece.refund_to})`));
|
|
992
|
-
}
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
const counts = new Map();
|
|
997
|
-
for (const leaf of expandedLeaves) {
|
|
998
|
-
for (const eff of leaf.effects) {
|
|
999
|
-
counts.set(eff.signature, (counts.get(eff.signature) ?? 0) + 1);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
variantMultiplicities.push(counts);
|
|
1003
|
-
const directEffects = deriveUdlActionEffects(action, udlClauseVocabulary);
|
|
1004
|
-
const leafEffects = action.calls && action.calls.length > 0
|
|
1005
|
-
? deriveActionEffectsFromPlan(expandedLeaves)
|
|
1006
|
-
: {};
|
|
1007
|
-
const effects = combineDerivedEffects(directEffects, leafEffects);
|
|
1008
|
-
plans.push({
|
|
1009
|
-
action: actionKey,
|
|
1010
|
-
effects,
|
|
1011
|
-
leaves: expandedLeaves,
|
|
1012
|
-
pieceId,
|
|
1013
|
-
});
|
|
1014
|
-
}
|
|
1015
|
-
// Verify identical effect signature multiplicities across piece variants
|
|
1016
|
-
if (variantMultiplicities.length > 1) {
|
|
1017
|
-
const baseCounts = variantMultiplicities[0];
|
|
1018
|
-
for (let i = 1; i < variantMultiplicities.length; i++) {
|
|
1019
|
-
const comp = variantMultiplicities[i];
|
|
1020
|
-
let identical = baseCounts.size === comp.size;
|
|
1021
|
-
if (identical) {
|
|
1022
|
-
for (const [sig, count] of baseCounts.entries()) {
|
|
1023
|
-
if (comp.get(sig) !== count) {
|
|
1024
|
-
identical = false;
|
|
1025
|
-
break;
|
|
1026
|
-
}
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
if (!identical) {
|
|
1030
|
-
issues.push(issue("UDL2013", `${actionPath}.pieceStage`, `piece variants of action ${actionKey} have differing effect signature multiplicities`));
|
|
1031
|
-
break;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
else if (action.calls && action.calls.length > 0) {
|
|
1037
|
-
const expandedLeaves = [];
|
|
1038
|
-
const capturedDestinations = new Set();
|
|
1039
|
-
const consumedSources = new Set();
|
|
1040
|
-
totalCumulativeCalls = 0;
|
|
1041
|
-
const scope = new Map([
|
|
1042
|
-
["instance", { kind: "instance" }],
|
|
1043
|
-
]);
|
|
1044
|
-
for (const call of action.calls) {
|
|
1045
|
-
const [lib, act] = call.action.split(".");
|
|
1046
|
-
if (!lib || !act || !hasLibrary(lib)) {
|
|
1047
|
-
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown library ${lib ?? ""}`));
|
|
1048
|
-
continue;
|
|
1049
|
-
}
|
|
1050
|
-
const targetActionDef = getAction(lib, act);
|
|
1051
|
-
if (!targetActionDef) {
|
|
1052
|
-
issues.push(issue("UDL2010", `${actionPath}.calls.${call.id}.action`, `call ${call.id} references unknown action ${act} in library ${lib}`));
|
|
1053
|
-
continue;
|
|
1054
|
-
}
|
|
1055
|
-
const callScope = resolveCallParameters(call, targetActionDef, scope, `${actionPath}.calls.${call.id}`, issues);
|
|
1056
|
-
expandPrivateAction(lib, act, callScope, [actionKey, call.id], [], publicPrincipal, capturedDestinations, consumedSources, expandedLeaves);
|
|
1057
|
-
}
|
|
1058
|
-
const directEffects = deriveUdlActionEffects(action, udlClauseVocabulary);
|
|
1059
|
-
const leafEffects = deriveActionEffectsFromPlan(expandedLeaves);
|
|
1060
|
-
const effects = combineDerivedEffects(directEffects, leafEffects);
|
|
1061
|
-
plans.push({
|
|
1062
|
-
action: actionKey,
|
|
1063
|
-
effects,
|
|
1064
|
-
leaves: expandedLeaves,
|
|
1065
|
-
});
|
|
1066
|
-
}
|
|
1067
|
-
else {
|
|
1068
|
-
// Ordinary actions already are flat. Preserve the executor's order and
|
|
1069
|
-
// authored step bytes; origin paths belong to the plan, not the ABI.
|
|
1070
|
-
const effects = deriveUdlActionEffects(action, udlClauseVocabulary);
|
|
1071
|
-
const leafEffects = (source) => Object.entries(effects).flatMap(([kind, rows]) => rows
|
|
1072
|
-
.filter((row) => row.source === source)
|
|
1073
|
-
.map((row) => ({
|
|
1074
|
-
kind: kind,
|
|
1075
|
-
signature: row.signature,
|
|
1076
|
-
})));
|
|
1077
|
-
const leaves = action.steps.map((step, index) => ({
|
|
1078
|
-
step,
|
|
1079
|
-
originPath: [actionKey, "steps", String(index)],
|
|
1080
|
-
effects: leafEffects(`steps[${index}]`),
|
|
1081
|
-
evidence: "parent_receipt",
|
|
1082
|
-
}));
|
|
1083
|
-
if (action.payout) {
|
|
1084
|
-
const intent = action.payout;
|
|
1085
|
-
leaves.push({
|
|
1086
|
-
step: {
|
|
1087
|
-
operation: "payout.create",
|
|
1088
|
-
bind: {
|
|
1089
|
-
amount: { from: "instance", path: intent.amount },
|
|
1090
|
-
beneficiaryId: {
|
|
1091
|
-
from: "instance",
|
|
1092
|
-
path: `fields.${intent.beneficiaryField}`,
|
|
1093
|
-
},
|
|
1094
|
-
currency: {
|
|
1095
|
-
from: "instance",
|
|
1096
|
-
path: `fields.${intent.currencyField}`,
|
|
1097
|
-
},
|
|
1098
|
-
sourceAccountId: {
|
|
1099
|
-
from: "instance",
|
|
1100
|
-
path: `fields.${intent.sourceAccountField}`,
|
|
1101
|
-
},
|
|
1102
|
-
speed: { from: "const", value: intent.speed },
|
|
1103
|
-
},
|
|
1104
|
-
capture: { [intent.capture]: "payoutId" },
|
|
1105
|
-
},
|
|
1106
|
-
originPath: [actionKey, "payout"],
|
|
1107
|
-
effects: leafEffects("payout"),
|
|
1108
|
-
evidence: "parent_receipt",
|
|
1109
|
-
});
|
|
1110
|
-
}
|
|
1111
|
-
leaves.push(...action.moves.map((step, index) => ({
|
|
1112
|
-
step,
|
|
1113
|
-
originPath: [actionKey, "moves", String(index)],
|
|
1114
|
-
effects: leafEffects(`moves[${index}]`),
|
|
1115
|
-
evidence: "parent_receipt",
|
|
1116
|
-
})));
|
|
1117
|
-
plans.push({
|
|
1118
|
-
action: actionKey,
|
|
1119
|
-
leaves,
|
|
1120
|
-
effects,
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
return { issues, plans };
|
|
1125
|
-
}
|
|
1126
|
-
//# sourceMappingURL=effects.js.map
|