@hyperscale0/udl 1.0.0-beta.1 → 1.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 +78 -25
- package/README.md +37 -99
- package/conformance/README.md +44 -41
- package/conformance/evolution/action-contract.expected.json +10 -0
- package/conformance/evolution/action-contract.live.udl +44 -0
- package/conformance/evolution/action-contract.next.udl +45 -0
- package/conformance/evolution/product-identity.expected.json +10 -0
- package/conformance/evolution/product-identity.live.udl +55 -0
- package/conformance/evolution/product-identity.next.udl +55 -0
- package/conformance/evolution/version-required.expected.json +10 -0
- package/conformance/evolution/version-required.live.udl +55 -0
- package/conformance/evolution/version-required.next.udl +58 -0
- package/conformance/invalid/action-without-transition.expected.json +10 -0
- package/conformance/invalid/{verb-without-transition.udl → action-without-transition.udl} +7 -2
- package/conformance/invalid/agent-description-too-long.expected.json +10 -0
- package/conformance/invalid/agent-description-too-long.udl +57 -0
- package/conformance/invalid/blank-title.expected.json +1 -1
- package/conformance/invalid/blank-title.udl +6 -2
- package/conformance/invalid/composition-dial-duplicate-key.expected.json +10 -0
- package/conformance/invalid/composition-dial-duplicate-key.udl +73 -0
- package/conformance/invalid/depth-budget.expected.json +1 -1
- package/conformance/invalid/duplicate-subject.expected.json +10 -0
- package/conformance/invalid/duplicate-subject.udl +230 -0
- package/conformance/invalid/forged-effects.expected.json +10 -0
- package/conformance/invalid/forged-effects.udl +76 -0
- package/conformance/invalid/format-version.expected.json +1 -1
- package/conformance/invalid/format-version.udl +6 -2
- package/conformance/invalid/instrument-id-not-snake-case.expected.json +10 -0
- package/conformance/invalid/{noun-id-not-snake-case.udl → instrument-id-not-snake-case.udl} +6 -2
- package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +10 -0
- package/conformance/invalid/invalid-aggregate-gate-shape.udl +1819 -0
- package/conformance/invalid/invalid-check-duration.expected.json +10 -0
- package/conformance/invalid/invalid-check-duration.udl +219 -0
- package/conformance/invalid/invalid-dial-anchor.expected.json +10 -0
- package/conformance/invalid/invalid-dial-anchor.udl +219 -0
- package/conformance/invalid/invalid-exception-parent-ref.expected.json +10 -0
- package/conformance/invalid/invalid-exception-parent-ref.udl +2487 -0
- package/conformance/invalid/invalid-exposure-shape.expected.json +10 -0
- package/conformance/invalid/invalid-exposure-shape.udl +1819 -0
- package/conformance/invalid/invalid-remainder.expected.json +10 -0
- package/conformance/invalid/invalid-remainder.udl +220 -0
- package/conformance/invalid/invalid-schema-keyword.expected.json +10 -0
- package/conformance/invalid/invalid-schema-keyword.udl +220 -0
- package/conformance/invalid/invalid-utf8.expected.json +1 -1
- package/conformance/invalid/malformed-json.expected.json +1 -1
- package/conformance/invalid/missing-create-action.expected.json +10 -0
- package/conformance/invalid/{missing-create-verb.udl → missing-create-action.udl} +5 -2
- package/conformance/invalid/missing-exception-amount-field.expected.json +10 -0
- package/conformance/invalid/missing-exception-amount-field.udl +2487 -0
- package/conformance/invalid/missing-exception-contract.expected.json +14 -0
- package/conformance/invalid/missing-exception-contract.udl +2523 -0
- package/conformance/invalid/missing-exception-reason-field.expected.json +10 -0
- package/conformance/invalid/missing-exception-reason-field.udl +2487 -0
- package/conformance/invalid/not-an-object.expected.json +1 -1
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +10 -0
- package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +259 -0
- package/conformance/invalid/quote-freeze-set-incomplete.expected.json +10 -0
- package/conformance/invalid/quote-freeze-set-incomplete.udl +261 -0
- package/conformance/invalid/quote-named-reference-gate.expected.json +10 -0
- package/conformance/invalid/quote-named-reference-gate.udl +50 -0
- package/conformance/invalid/reconcile-named-reference-gate.expected.json +10 -0
- package/conformance/invalid/reconcile-named-reference-gate.udl +50 -0
- package/conformance/invalid/unknown-key.expected.json +1 -1
- package/conformance/invalid/unknown-key.udl +6 -2
- package/conformance/invalid/unknown-reference-gate-field.expected.json +10 -0
- package/conformance/invalid/unknown-reference-gate-field.udl +1819 -0
- package/conformance/invalid/unknown-required-field.expected.json +10 -0
- package/conformance/invalid/unknown-required-field.udl +220 -0
- package/conformance/invalid/unreachable-state.expected.json +2 -2
- package/conformance/invalid/unreachable-state.udl +6 -2
- package/conformance/invalid/wrong-exception-amount-field.expected.json +10 -0
- package/conformance/invalid/wrong-exception-amount-field.udl +2487 -0
- package/conformance/invalid/wrong-exception-reason-field.expected.json +10 -0
- package/conformance/invalid/wrong-exception-reason-field.udl +2487 -0
- package/conformance/valid/agent-description.expected.json +6 -0
- package/conformance/valid/agent-description.udl +66 -0
- package/conformance/valid/cards.expected.json +1 -0
- package/conformance/valid/cards.udl +843 -356
- package/conformance/valid/commerce-escrow.expected.json +1 -0
- package/conformance/valid/commerce-escrow.udl +792 -291
- package/conformance/valid/compiled-crowdfunding.expected.json +6 -0
- package/conformance/valid/compiled-crowdfunding.udl +1843 -0
- package/conformance/valid/compiled-watch-club.expected.json +6 -0
- package/conformance/valid/compiled-watch-club.udl +2486 -0
- package/conformance/valid/complete-contract.expected.json +6 -0
- package/conformance/valid/complete-contract.udl +218 -0
- package/conformance/valid/effect-signatures.expected.json +6 -0
- package/conformance/valid/effect-signatures.udl +75 -0
- package/conformance/valid/hand-edited.expected.json +1 -0
- package/conformance/valid/hand-edited.udl +1 -1
- package/conformance/valid/insured-car-marketplace.expected.json +1 -0
- package/conformance/valid/insured-car-marketplace.udl +534 -315
- package/conformance/valid/insured-travel.expected.json +2 -1
- package/conformance/valid/insured-travel.udl +1489 -641
- package/conformance/valid/minimal.expected.json +1 -0
- package/conformance/valid/minimal.udl +18 -14
- package/conformance/valid/protection.expected.json +1 -0
- package/conformance/valid/protection.udl +719 -304
- package/conformance/valid/string-escaping.expected.json +6 -0
- package/conformance/valid/string-escaping.udl +54 -0
- package/dist/canonical.d.ts +2 -0
- package/dist/canonical.d.ts.map +1 -1
- package/dist/canonical.js +7 -0
- package/dist/canonical.js.map +1 -1
- package/dist/check-profiles.d.ts +57 -0
- package/dist/check-profiles.d.ts.map +1 -0
- package/dist/check-profiles.js +62 -0
- package/dist/check-profiles.js.map +1 -0
- package/dist/cli.js +48 -4
- package/dist/cli.js.map +1 -1
- package/dist/diagnostics.d.ts +168 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +164 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/effects.d.ts +41 -0
- package/dist/effects.d.ts.map +1 -0
- package/dist/effects.js +110 -0
- package/dist/effects.js.map +1 -0
- package/dist/evolution.d.ts +51 -18
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +244 -102
- package/dist/evolution.js.map +1 -1
- package/dist/finance.d.ts +17 -13
- package/dist/finance.d.ts.map +1 -1
- package/dist/finance.js +165 -109
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +12 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -5
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +2 -2
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +4 -3
- package/dist/limits.js.map +1 -1
- package/dist/parser.d.ts.map +1 -1
- package/dist/parser.js +6 -16
- package/dist/parser.js.map +1 -1
- package/dist/schema.d.ts +2059 -135
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +759 -73
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +22 -9
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1463 -457
- package/dist/validation.js.map +1 -1
- package/docs/README.md +14 -0
- package/docs/guide/01-a-document.md +37 -0
- package/docs/guide/02-money-steps.md +23 -0
- package/docs/guide/03-laws.md +18 -0
- package/docs/guide/04-fees-and-remainder.md +15 -0
- package/docs/guide/05-checks-updates-dials.md +7 -0
- package/docs/guide/06-effects.md +11 -0
- package/docs/guide/07-evolution.md +11 -0
- package/docs/guide/08-implementing.md +13 -0
- package/docs/llms-full.txt +1241 -0
- package/docs/llms.txt +14 -0
- package/docs/reference/canonical.md +16 -0
- package/docs/reference/clauses.md +986 -0
- package/docs/reference/cli.md +22 -0
- package/docs/reference/diagnostics.md +32 -0
- package/package.json +6 -2
- package/skills/udl/SKILL.md +28 -0
- package/spec/README.md +82 -51
- package/spec/udl.schema.json +1191 -119
- package/src/canonical.ts +11 -0
- package/src/check-profiles.ts +80 -0
- package/src/cli.ts +49 -3
- package/src/diagnostics.ts +221 -0
- package/src/effects.ts +163 -0
- package/src/evolution.ts +375 -125
- package/src/finance.ts +255 -160
- package/src/index.ts +50 -14
- package/src/limits.ts +4 -3
- package/src/parser.ts +15 -18
- package/src/schema.ts +855 -81
- package/src/validation.ts +2666 -560
- package/conformance/invalid/missing-create-verb.expected.json +0 -10
- package/conformance/invalid/noun-id-not-snake-case.expected.json +0 -10
- package/conformance/invalid/verb-without-transition.expected.json +0 -10
package/src/finance.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { UdlBinding } from "./schema.js";
|
|
2
2
|
import { UDL_LIMITS } from "./limits.js";
|
|
3
|
+
import type { UdlIssueCode } from "./diagnostics.js";
|
|
3
4
|
|
|
4
5
|
export interface FinanceIssue {
|
|
6
|
+
readonly code: UdlIssueCode;
|
|
5
7
|
readonly message: string;
|
|
6
8
|
readonly path: readonly PropertyKey[];
|
|
7
9
|
}
|
|
@@ -20,7 +22,7 @@ interface FinancialMove extends FinancialStep {
|
|
|
20
22
|
readonly key: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
interface
|
|
25
|
+
interface FinancialInstrument {
|
|
24
26
|
readonly lifecycle: {
|
|
25
27
|
readonly initial: string;
|
|
26
28
|
readonly states: readonly string[];
|
|
@@ -35,26 +37,40 @@ interface FinancialNoun {
|
|
|
35
37
|
readonly subjectHolder?: string | undefined;
|
|
36
38
|
}
|
|
37
39
|
| undefined;
|
|
38
|
-
readonly
|
|
39
|
-
| {
|
|
40
|
-
readonly confirm: string;
|
|
41
|
-
readonly penalty: readonly {
|
|
42
|
-
readonly bps: number;
|
|
43
|
-
}[];
|
|
44
|
-
readonly refundableField: string;
|
|
45
|
-
}
|
|
46
|
-
| undefined;
|
|
47
|
-
readonly verbs: Readonly<
|
|
40
|
+
readonly actions: Readonly<
|
|
48
41
|
Record<
|
|
49
42
|
string,
|
|
50
43
|
{
|
|
44
|
+
readonly commit?: string | undefined;
|
|
51
45
|
readonly moves?: readonly FinancialMove[];
|
|
46
|
+
readonly quote?:
|
|
47
|
+
| {
|
|
48
|
+
readonly baseField: string;
|
|
49
|
+
readonly chargeRef: string;
|
|
50
|
+
readonly charges: readonly { readonly bps: number }[];
|
|
51
|
+
readonly netRef: string;
|
|
52
|
+
}
|
|
53
|
+
| undefined;
|
|
52
54
|
readonly steps: readonly FinancialStep[];
|
|
53
55
|
}
|
|
54
56
|
>
|
|
55
57
|
>;
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
/**
|
|
61
|
+
* One quote-commit pair, flattened for the typestate. The quoting action prices
|
|
62
|
+
* `baseField` into the charge and the net; the committing action spends the net
|
|
63
|
+
* out of the account that funded the base.
|
|
64
|
+
*/
|
|
65
|
+
interface QuoteFacts {
|
|
66
|
+
readonly baseField: string;
|
|
67
|
+
readonly chargeRef: string;
|
|
68
|
+
readonly chargeCanBeNonzero: boolean;
|
|
69
|
+
readonly commit: string;
|
|
70
|
+
readonly netRef: string;
|
|
71
|
+
readonly quoting: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
58
74
|
type Balance =
|
|
59
75
|
| { readonly kind: "empty" }
|
|
60
76
|
| { readonly kind: "funded"; readonly amounts: readonly string[] }
|
|
@@ -74,6 +90,8 @@ interface Reservation {
|
|
|
74
90
|
|
|
75
91
|
interface Effect {
|
|
76
92
|
readonly amount: string;
|
|
93
|
+
/** The quoted base a refund effect returns; absent on every other kind. */
|
|
94
|
+
readonly refundable?: string;
|
|
77
95
|
readonly kind:
|
|
78
96
|
| "credit"
|
|
79
97
|
| "debit"
|
|
@@ -109,131 +127,128 @@ function withoutPiece(
|
|
|
109
127
|
}
|
|
110
128
|
|
|
111
129
|
/**
|
|
112
|
-
* One financial typestate owner for canonical UDL and static
|
|
130
|
+
* One financial typestate owner for canonical UDL and static instrument contracts.
|
|
113
131
|
* It proves a tracked escrow account holds an exact set of funded amounts,
|
|
114
132
|
* models held transfers through reserve/post/void, consumes every direct
|
|
115
|
-
* debit piece by piece, and makes the
|
|
133
|
+
* debit piece by piece, and makes the quoted refund's sole remainder an
|
|
116
134
|
* exactly-once penalty payout.
|
|
117
135
|
*/
|
|
118
|
-
export function
|
|
119
|
-
|
|
136
|
+
export function analyzeInstrumentFinance(
|
|
137
|
+
instrument: FinancialInstrument,
|
|
120
138
|
options: FinanceOptions = {},
|
|
121
139
|
): readonly FinanceIssue[] {
|
|
122
140
|
const issues: FinanceIssue[] = [];
|
|
123
141
|
const seenIssues = new Set<string>();
|
|
124
|
-
const add = (
|
|
142
|
+
const add = (
|
|
143
|
+
path: readonly PropertyKey[],
|
|
144
|
+
message: string,
|
|
145
|
+
code: UdlIssueCode = "UDL4001",
|
|
146
|
+
): void => {
|
|
125
147
|
const key = `${path.join(".")}\0${message}`;
|
|
126
148
|
if (seenIssues.has(key)) return;
|
|
127
149
|
seenIssues.add(key);
|
|
128
|
-
issues.push({ message, path });
|
|
150
|
+
issues.push({ code, message, path });
|
|
129
151
|
};
|
|
130
152
|
|
|
131
|
-
const admissionProblem = financeAdmissionProblem(
|
|
153
|
+
const admissionProblem = financeAdmissionProblem(instrument);
|
|
132
154
|
if (admissionProblem) {
|
|
133
|
-
add(["lifecycle"], admissionProblem);
|
|
155
|
+
add(["lifecycle"], admissionProblem, "UDL1004");
|
|
134
156
|
return issues;
|
|
135
157
|
}
|
|
136
158
|
|
|
137
|
-
const trackedAccounts =
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
const trackedAccounts = productEscrowAccounts(instrument);
|
|
160
|
+
const quotes = quoteFacts(instrument, options);
|
|
161
|
+
const refundSources = new Map<string, string>();
|
|
162
|
+
for (const quote of quotes) {
|
|
163
|
+
const commitTransfer = instrument.actions[quote.commit]?.moves?.filter(
|
|
164
|
+
(move) => move.operation === "internal_transfer.create",
|
|
165
|
+
)[0];
|
|
166
|
+
const source = canonicalAccount(
|
|
167
|
+
instrument,
|
|
168
|
+
commitTransfer?.bind.sourceAccountId,
|
|
169
|
+
);
|
|
170
|
+
if (!source) continue;
|
|
171
|
+
refundSources.set(quote.commit, source);
|
|
172
|
+
trackedAccounts.add(source);
|
|
173
|
+
}
|
|
174
|
+
const refundSourceAccounts = new Set(refundSources.values());
|
|
148
175
|
if (trackedAccounts.size === 0) return [];
|
|
149
176
|
if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
|
|
150
177
|
add(
|
|
151
|
-
["
|
|
178
|
+
["actions"],
|
|
152
179
|
`financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`,
|
|
180
|
+
"UDL1004",
|
|
153
181
|
);
|
|
154
182
|
return issues;
|
|
155
183
|
}
|
|
156
184
|
|
|
157
|
-
const reservations = reservationsByKey(
|
|
158
|
-
const
|
|
159
|
-
(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
)
|
|
165
|
-
);
|
|
166
|
-
const penaltyCanBeNonzero =
|
|
167
|
-
options.penaltyMayBeNonzero === true ||
|
|
168
|
-
declaresPenaltyPayout ||
|
|
169
|
-
noun.unwind?.penalty.some((tier) => tier.bps > 0) === true;
|
|
170
|
-
|
|
171
|
-
if (noun.unwind) {
|
|
172
|
-
validatePenaltyPayout(noun, refundSource, penaltyCanBeNonzero, add);
|
|
185
|
+
const reservations = reservationsByKey(instrument);
|
|
186
|
+
for (const quote of quotes) {
|
|
187
|
+
validateChargePayout(
|
|
188
|
+
instrument,
|
|
189
|
+
quote,
|
|
190
|
+
refundSources.get(quote.commit),
|
|
191
|
+
add,
|
|
192
|
+
);
|
|
173
193
|
}
|
|
174
194
|
|
|
175
195
|
let pathVariants = 0;
|
|
176
196
|
let work = 0;
|
|
177
197
|
for (const account of trackedAccounts) {
|
|
178
|
-
const effects =
|
|
179
|
-
noun,
|
|
180
|
-
account,
|
|
181
|
-
reservations,
|
|
182
|
-
penaltyCanBeNonzero,
|
|
183
|
-
);
|
|
198
|
+
const effects = effectsByAction(instrument, account, reservations, quotes);
|
|
184
199
|
const createEffects = effects.get("create") ?? [];
|
|
185
200
|
work += 1 + createEffects.length;
|
|
186
201
|
if (work > UDL_LIMITS.financeWork) {
|
|
187
202
|
add(
|
|
188
203
|
["lifecycle"],
|
|
189
204
|
`financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`,
|
|
205
|
+
"UDL1004",
|
|
190
206
|
);
|
|
191
207
|
return issues;
|
|
192
208
|
}
|
|
193
209
|
const initial = applyEffects(
|
|
194
|
-
noun,
|
|
195
210
|
account,
|
|
196
211
|
"create",
|
|
197
212
|
{ balance: EMPTY, holds: {} },
|
|
198
213
|
createEffects,
|
|
199
214
|
reservations,
|
|
200
|
-
account
|
|
215
|
+
refundSourceAccounts.has(account),
|
|
201
216
|
add,
|
|
202
217
|
);
|
|
203
218
|
const states = new Map<string, Map<string, AccountState>>([
|
|
204
|
-
[
|
|
219
|
+
[instrument.lifecycle.initial, new Map([[stateKey(initial), initial]])],
|
|
205
220
|
]);
|
|
206
221
|
pathVariants += 1;
|
|
207
|
-
const pending = [
|
|
222
|
+
const pending = [instrument.lifecycle.initial];
|
|
208
223
|
while (pending.length > 0) {
|
|
209
224
|
const from = pending.shift() as string;
|
|
210
225
|
const sourceStates = states.get(from);
|
|
211
226
|
if (!sourceStates) continue;
|
|
212
|
-
for (const [
|
|
213
|
-
|
|
227
|
+
for (const [action, transition] of Object.entries(
|
|
228
|
+
instrument.lifecycle.transitions,
|
|
214
229
|
)) {
|
|
215
230
|
if (!transition.from.includes(from)) continue;
|
|
216
231
|
const targetStates = states.get(transition.to) ?? new Map();
|
|
217
232
|
states.set(transition.to, targetStates);
|
|
218
233
|
let grew = false;
|
|
219
234
|
for (const sourceState of sourceStates.values()) {
|
|
220
|
-
const transitionEffects = effects.get(
|
|
235
|
+
const transitionEffects = effects.get(action) ?? [];
|
|
221
236
|
work += 1 + transitionEffects.length;
|
|
222
237
|
if (work > UDL_LIMITS.financeWork) {
|
|
223
238
|
add(
|
|
224
239
|
["lifecycle"],
|
|
225
240
|
`financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`,
|
|
241
|
+
"UDL1004",
|
|
226
242
|
);
|
|
227
243
|
return issues;
|
|
228
244
|
}
|
|
229
245
|
const targetState = applyEffects(
|
|
230
|
-
noun,
|
|
231
246
|
account,
|
|
232
|
-
|
|
247
|
+
action,
|
|
233
248
|
sourceState,
|
|
234
249
|
transitionEffects,
|
|
235
250
|
reservations,
|
|
236
|
-
account
|
|
251
|
+
refundSourceAccounts.has(account),
|
|
237
252
|
add,
|
|
238
253
|
);
|
|
239
254
|
const key = stateKey(targetState);
|
|
@@ -244,6 +259,7 @@ export function analyzeNounFinance(
|
|
|
244
259
|
add(
|
|
245
260
|
["lifecycle"],
|
|
246
261
|
`financial analysis exceeds ${UDL_LIMITS.financePathVariants} distinct path variants`,
|
|
262
|
+
"UDL1004",
|
|
247
263
|
);
|
|
248
264
|
return issues;
|
|
249
265
|
}
|
|
@@ -254,7 +270,7 @@ export function analyzeNounFinance(
|
|
|
254
270
|
}
|
|
255
271
|
}
|
|
256
272
|
const nonterminalStates = new Set(
|
|
257
|
-
Object.values(
|
|
273
|
+
Object.values(instrument.lifecycle.transitions).flatMap(
|
|
258
274
|
(transition) => transition.from,
|
|
259
275
|
),
|
|
260
276
|
);
|
|
@@ -268,7 +284,7 @@ export function analyzeNounFinance(
|
|
|
268
284
|
continue;
|
|
269
285
|
}
|
|
270
286
|
add(
|
|
271
|
-
["lifecycle", "states",
|
|
287
|
+
["lifecycle", "states", instrument.lifecycle.states.indexOf(state)],
|
|
272
288
|
`terminal state ${state} can strand value in ${formatAccount(account)}`,
|
|
273
289
|
);
|
|
274
290
|
}
|
|
@@ -279,34 +295,34 @@ export function analyzeNounFinance(
|
|
|
279
295
|
}
|
|
280
296
|
|
|
281
297
|
export function financeAdmissionProblem(
|
|
282
|
-
|
|
298
|
+
instrument: FinancialInstrument,
|
|
283
299
|
): string | undefined {
|
|
284
|
-
if (
|
|
300
|
+
if (instrument.lifecycle.states.length > UDL_LIMITS.financeStates) {
|
|
285
301
|
return `financial analysis exceeds ${UDL_LIMITS.financeStates} lifecycle states`;
|
|
286
302
|
}
|
|
287
303
|
let transitionCount = 0;
|
|
288
304
|
let transitionEdges = 0;
|
|
289
|
-
for (const name in
|
|
290
|
-
if (!Object.hasOwn(
|
|
305
|
+
for (const name in instrument.lifecycle.transitions) {
|
|
306
|
+
if (!Object.hasOwn(instrument.lifecycle.transitions, name)) continue;
|
|
291
307
|
transitionCount += 1;
|
|
292
308
|
if (transitionCount > UDL_LIMITS.financeTransitions) {
|
|
293
309
|
return `financial analysis exceeds ${UDL_LIMITS.financeTransitions} lifecycle transitions`;
|
|
294
310
|
}
|
|
295
|
-
transitionEdges +=
|
|
311
|
+
transitionEdges += instrument.lifecycle.transitions[name]?.from.length ?? 0;
|
|
296
312
|
if (transitionEdges > UDL_LIMITS.financeTransitionEdges) {
|
|
297
313
|
return `financial analysis exceeds ${UDL_LIMITS.financeTransitionEdges} lifecycle transition edges`;
|
|
298
314
|
}
|
|
299
315
|
}
|
|
300
|
-
let
|
|
316
|
+
let actionCount = 0;
|
|
301
317
|
let effectCount = 0;
|
|
302
|
-
for (const name in
|
|
303
|
-
if (!Object.hasOwn(
|
|
304
|
-
|
|
305
|
-
if (
|
|
306
|
-
return `financial analysis exceeds ${UDL_LIMITS.
|
|
318
|
+
for (const name in instrument.actions) {
|
|
319
|
+
if (!Object.hasOwn(instrument.actions, name)) continue;
|
|
320
|
+
actionCount += 1;
|
|
321
|
+
if (actionCount > UDL_LIMITS.financeActions) {
|
|
322
|
+
return `financial analysis exceeds ${UDL_LIMITS.financeActions} actions`;
|
|
307
323
|
}
|
|
308
|
-
const
|
|
309
|
-
effectCount += (
|
|
324
|
+
const action = instrument.actions[name];
|
|
325
|
+
effectCount += (action?.steps.length ?? 0) + (action?.moves?.length ?? 0);
|
|
310
326
|
if (effectCount > UDL_LIMITS.financeEffects) {
|
|
311
327
|
return `financial analysis exceeds ${UDL_LIMITS.financeEffects} kernel effects`;
|
|
312
328
|
}
|
|
@@ -314,14 +330,14 @@ export function financeAdmissionProblem(
|
|
|
314
330
|
return undefined;
|
|
315
331
|
}
|
|
316
332
|
|
|
317
|
-
function
|
|
333
|
+
function productEscrowAccounts(instrument: FinancialInstrument): Set<string> {
|
|
318
334
|
const accounts = new Set<string>();
|
|
319
|
-
for (const
|
|
320
|
-
for (const step of
|
|
335
|
+
for (const action of Object.values(instrument.actions)) {
|
|
336
|
+
for (const step of action.steps) {
|
|
321
337
|
if (
|
|
322
338
|
step.operation !== "account.escrow.provision" ||
|
|
323
339
|
step.bind.role?.from !== "const" ||
|
|
324
|
-
step.bind.role.value !== "
|
|
340
|
+
step.bind.role.value !== "product_escrow"
|
|
325
341
|
) {
|
|
326
342
|
continue;
|
|
327
343
|
}
|
|
@@ -331,13 +347,13 @@ function workspaceEscrowAccounts(noun: FinancialNoun): Set<string> {
|
|
|
331
347
|
}
|
|
332
348
|
}
|
|
333
349
|
|
|
334
|
-
// `
|
|
350
|
+
// `product_escrow` also backs open-ended balance products such as wallets.
|
|
335
351
|
// Only instance-bound amounts claim static conservation; caller-sized flows
|
|
336
|
-
// remain runtime balance checks.
|
|
352
|
+
// remain runtime balance checks. Commit sources are added separately above.
|
|
337
353
|
return new Set(
|
|
338
354
|
[...accounts].filter((account) =>
|
|
339
|
-
Object.values(
|
|
340
|
-
(
|
|
355
|
+
Object.values(instrument.actions).some((action) =>
|
|
356
|
+
(action.moves ?? []).some((step) => {
|
|
341
357
|
if (
|
|
342
358
|
(step.operation !== "internal_transfer.create" &&
|
|
343
359
|
step.operation !== "internal_transfer.reserve") ||
|
|
@@ -346,8 +362,10 @@ function workspaceEscrowAccounts(noun: FinancialNoun): Set<string> {
|
|
|
346
362
|
return false;
|
|
347
363
|
}
|
|
348
364
|
return (
|
|
349
|
-
canonicalAccount(
|
|
350
|
-
|
|
365
|
+
canonicalAccount(instrument, step.bind.sourceAccountId) ===
|
|
366
|
+
account ||
|
|
367
|
+
canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
368
|
+
account
|
|
351
369
|
);
|
|
352
370
|
}),
|
|
353
371
|
),
|
|
@@ -356,15 +374,15 @@ function workspaceEscrowAccounts(noun: FinancialNoun): Set<string> {
|
|
|
356
374
|
}
|
|
357
375
|
|
|
358
376
|
function reservationsByKey(
|
|
359
|
-
|
|
377
|
+
instrument: FinancialInstrument,
|
|
360
378
|
): ReadonlyMap<string, Reservation> {
|
|
361
379
|
const reservations = new Map<string, Reservation>();
|
|
362
|
-
for (const
|
|
363
|
-
for (const step of
|
|
380
|
+
for (const action of Object.values(instrument.actions)) {
|
|
381
|
+
for (const step of action.moves ?? []) {
|
|
364
382
|
if (step.operation !== "internal_transfer.reserve") continue;
|
|
365
|
-
const source = canonicalAccount(
|
|
383
|
+
const source = canonicalAccount(instrument, step.bind.sourceAccountId);
|
|
366
384
|
const destination = canonicalAccount(
|
|
367
|
-
|
|
385
|
+
instrument,
|
|
368
386
|
step.bind.destinationAccountId,
|
|
369
387
|
);
|
|
370
388
|
const amount = amountIdentity(step.bind.amount);
|
|
@@ -382,54 +400,80 @@ function reservationsByKey(
|
|
|
382
400
|
return reservations;
|
|
383
401
|
}
|
|
384
402
|
|
|
385
|
-
function
|
|
386
|
-
|
|
403
|
+
function effectsByAction(
|
|
404
|
+
instrument: FinancialInstrument,
|
|
387
405
|
account: string,
|
|
388
406
|
reservations: ReadonlyMap<string, Reservation>,
|
|
389
|
-
|
|
407
|
+
quotes: readonly QuoteFacts[],
|
|
390
408
|
): ReadonlyMap<string, readonly Effect[]> {
|
|
409
|
+
const quoteByCommit = new Map(quotes.map((quote) => [quote.commit, quote]));
|
|
410
|
+
const chargePaths = new Map(
|
|
411
|
+
quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]),
|
|
412
|
+
);
|
|
391
413
|
return new Map(
|
|
392
|
-
Object.entries(
|
|
393
|
-
|
|
394
|
-
(
|
|
395
|
-
const path = [
|
|
414
|
+
Object.entries(instrument.actions).map(([actionName, action]) => [
|
|
415
|
+
actionName,
|
|
416
|
+
(action.moves ?? []).flatMap((step, stepIndex) => {
|
|
417
|
+
const path = [
|
|
418
|
+
"actions",
|
|
419
|
+
actionName,
|
|
420
|
+
"moves",
|
|
421
|
+
stepIndex,
|
|
422
|
+
"bind",
|
|
423
|
+
] as const;
|
|
396
424
|
if (step.operation === "internal_transfer.create") {
|
|
397
425
|
const effects: Effect[] = [];
|
|
398
426
|
const amount = amountIdentity(step.bind.amount);
|
|
399
427
|
if (
|
|
400
|
-
canonicalAccount(
|
|
428
|
+
canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
429
|
+
account
|
|
401
430
|
) {
|
|
402
431
|
effects.push({ amount, kind: "credit", path });
|
|
403
432
|
}
|
|
404
|
-
const source = canonicalAccount(
|
|
433
|
+
const source = canonicalAccount(
|
|
434
|
+
instrument,
|
|
435
|
+
step.bind.sourceAccountId,
|
|
436
|
+
);
|
|
405
437
|
if (source && accountsMayAlias(source, account)) {
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
step.bind.amount?.from === "instance"
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
438
|
+
const committed = quoteByCommit.get(actionName);
|
|
439
|
+
const amountPath =
|
|
440
|
+
step.bind.amount?.from === "instance"
|
|
441
|
+
? step.bind.amount.path
|
|
442
|
+
: undefined;
|
|
443
|
+
const refunded =
|
|
444
|
+
committed && amountPath === `refs.${committed.netRef}`
|
|
445
|
+
? committed
|
|
446
|
+
: undefined;
|
|
447
|
+
const charged = amountPath
|
|
448
|
+
? chargePaths.get(amountPath)
|
|
449
|
+
: undefined;
|
|
450
|
+
const kind = refunded
|
|
451
|
+
? "refund"
|
|
452
|
+
: charged
|
|
453
|
+
? "penalty_debit"
|
|
454
|
+
: amountPath !== undefined
|
|
455
|
+
? "debit"
|
|
456
|
+
: "dynamic_debit";
|
|
417
457
|
effects.push({
|
|
418
458
|
amount:
|
|
419
|
-
|
|
459
|
+
refunded && !refunded.chargeCanBeNonzero
|
|
420
460
|
? "refund_without_penalty"
|
|
421
461
|
: amount,
|
|
422
462
|
kind,
|
|
423
463
|
path,
|
|
464
|
+
...(refunded ? { refundable: refunded.baseField } : {}),
|
|
424
465
|
});
|
|
425
466
|
}
|
|
426
467
|
return effects;
|
|
427
468
|
}
|
|
428
469
|
if (step.operation === "internal_transfer.reserve") {
|
|
429
470
|
const reservation =
|
|
430
|
-
transferRefKey(step) ?? `${
|
|
471
|
+
transferRefKey(step) ?? `${actionName}:${stepIndex}`;
|
|
431
472
|
const effects: Effect[] = [];
|
|
432
|
-
const source = canonicalAccount(
|
|
473
|
+
const source = canonicalAccount(
|
|
474
|
+
instrument,
|
|
475
|
+
step.bind.sourceAccountId,
|
|
476
|
+
);
|
|
433
477
|
if (source && accountsMayAlias(source, account)) {
|
|
434
478
|
effects.push({
|
|
435
479
|
amount: amountIdentity(step.bind.amount),
|
|
@@ -439,7 +483,8 @@ function effectsByVerb(
|
|
|
439
483
|
});
|
|
440
484
|
}
|
|
441
485
|
if (
|
|
442
|
-
canonicalAccount(
|
|
486
|
+
canonicalAccount(instrument, step.bind.destinationAccountId) ===
|
|
487
|
+
account
|
|
443
488
|
) {
|
|
444
489
|
effects.push({
|
|
445
490
|
amount: amountIdentity(step.bind.amount),
|
|
@@ -481,9 +526,8 @@ function effectsByVerb(
|
|
|
481
526
|
}
|
|
482
527
|
|
|
483
528
|
function applyEffects(
|
|
484
|
-
noun: FinancialNoun,
|
|
485
529
|
account: string,
|
|
486
|
-
|
|
530
|
+
action: string,
|
|
487
531
|
input: AccountState,
|
|
488
532
|
effects: readonly Effect[],
|
|
489
533
|
reservations: ReadonlyMap<string, Reservation>,
|
|
@@ -495,7 +539,7 @@ function applyEffects(
|
|
|
495
539
|
const accountLabel = formatAccount(account);
|
|
496
540
|
for (const effect of effects) {
|
|
497
541
|
const fail = (message: string): void =>
|
|
498
|
-
add([...effect.path, "sourceAccountId"], `
|
|
542
|
+
add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
|
|
499
543
|
if (effect.kind === "credit") {
|
|
500
544
|
if (
|
|
501
545
|
exactBalance &&
|
|
@@ -503,7 +547,7 @@ function applyEffects(
|
|
|
503
547
|
) {
|
|
504
548
|
add(
|
|
505
549
|
[...effect.path, "destinationAccountId"],
|
|
506
|
-
`
|
|
550
|
+
`action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`,
|
|
507
551
|
);
|
|
508
552
|
balance = UNKNOWN;
|
|
509
553
|
continue;
|
|
@@ -556,7 +600,7 @@ function applyEffects(
|
|
|
556
600
|
} else {
|
|
557
601
|
add(
|
|
558
602
|
[...effect.path, "transferId"],
|
|
559
|
-
`
|
|
603
|
+
`action ${action} restores a held ${accountLabel} balance on top of existing value`,
|
|
560
604
|
);
|
|
561
605
|
balance = UNKNOWN;
|
|
562
606
|
}
|
|
@@ -566,7 +610,7 @@ function applyEffects(
|
|
|
566
610
|
if (balance.kind !== "empty" || Object.keys(holds).length > 0) {
|
|
567
611
|
add(
|
|
568
612
|
[...effect.path, "transferId"],
|
|
569
|
-
`
|
|
613
|
+
`action ${action} posts funding into ${accountLabel} while earlier value may remain`,
|
|
570
614
|
);
|
|
571
615
|
balance = UNKNOWN;
|
|
572
616
|
} else {
|
|
@@ -576,7 +620,7 @@ function applyEffects(
|
|
|
576
620
|
continue;
|
|
577
621
|
}
|
|
578
622
|
if (effect.kind === "refund") {
|
|
579
|
-
const refundable = `fields.${
|
|
623
|
+
const refundable = `fields.${effect.refundable ?? ""}`;
|
|
580
624
|
if (
|
|
581
625
|
balance.kind !== "funded" ||
|
|
582
626
|
balance.amounts.length !== 1 ||
|
|
@@ -597,7 +641,7 @@ function applyEffects(
|
|
|
597
641
|
if (effect.kind === "penalty_debit") {
|
|
598
642
|
if (balance.kind !== "penalty") {
|
|
599
643
|
fail(
|
|
600
|
-
`cannot pay
|
|
644
|
+
`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`,
|
|
601
645
|
);
|
|
602
646
|
}
|
|
603
647
|
balance = EMPTY;
|
|
@@ -608,7 +652,7 @@ function applyEffects(
|
|
|
608
652
|
fail(`can debit unfunded ${accountLabel}`);
|
|
609
653
|
} else if (balance.kind === "penalty") {
|
|
610
654
|
fail(
|
|
611
|
-
`can debit ${accountLabel} after its refund left only
|
|
655
|
+
`can debit ${accountLabel} after its refund left only the quoted charge`,
|
|
612
656
|
);
|
|
613
657
|
}
|
|
614
658
|
balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
|
|
@@ -622,7 +666,7 @@ function applyEffects(
|
|
|
622
666
|
fail(`can debit unfunded ${accountLabel}`);
|
|
623
667
|
} else if (balance.kind === "penalty") {
|
|
624
668
|
fail(
|
|
625
|
-
`can debit ${accountLabel} after its refund left only
|
|
669
|
+
`can debit ${accountLabel} after its refund left only the quoted charge`,
|
|
626
670
|
);
|
|
627
671
|
} else if (balance.kind === "funded" || exactBalance) {
|
|
628
672
|
fail(
|
|
@@ -634,59 +678,108 @@ function applyEffects(
|
|
|
634
678
|
return { balance, holds };
|
|
635
679
|
}
|
|
636
680
|
|
|
637
|
-
|
|
638
|
-
|
|
681
|
+
/**
|
|
682
|
+
* Flattens every quote-commit pair the instrument declares. A quoting action
|
|
683
|
+
* with no committing action, or a commit naming a non-quoting action, is a
|
|
684
|
+
* validator failure rather than a typestate one, so it is simply skipped here.
|
|
685
|
+
*/
|
|
686
|
+
function quoteFacts(
|
|
687
|
+
instrument: FinancialInstrument,
|
|
688
|
+
options: FinanceOptions,
|
|
689
|
+
): readonly QuoteFacts[] {
|
|
690
|
+
const facts: QuoteFacts[] = [];
|
|
691
|
+
for (const [quoting, action] of Object.entries(instrument.actions)) {
|
|
692
|
+
const quote = action.quote;
|
|
693
|
+
if (!quote) continue;
|
|
694
|
+
const commit = Object.entries(instrument.actions).find(
|
|
695
|
+
([, candidate]) => candidate.commit === quoting,
|
|
696
|
+
)?.[0];
|
|
697
|
+
if (commit === undefined) continue;
|
|
698
|
+
const chargePath = `refs.${quote.chargeRef}`;
|
|
699
|
+
const declaresChargePayout = Object.values(instrument.actions).some(
|
|
700
|
+
(candidate) =>
|
|
701
|
+
(candidate.moves ?? []).some((move) =>
|
|
702
|
+
Object.values(move.bind).some(
|
|
703
|
+
(binding) =>
|
|
704
|
+
binding.from === "instance" && binding.path === chargePath,
|
|
705
|
+
),
|
|
706
|
+
),
|
|
707
|
+
);
|
|
708
|
+
facts.push({
|
|
709
|
+
baseField: quote.baseField,
|
|
710
|
+
chargeCanBeNonzero:
|
|
711
|
+
options.penaltyMayBeNonzero === true ||
|
|
712
|
+
declaresChargePayout ||
|
|
713
|
+
quote.charges.some((tier) => tier.bps > 0),
|
|
714
|
+
chargeRef: quote.chargeRef,
|
|
715
|
+
commit,
|
|
716
|
+
netRef: quote.netRef,
|
|
717
|
+
quoting,
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return facts;
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* The quoted charge is the exact remainder the commit leaves behind, so it may
|
|
725
|
+
* fund one transfer, out of the same account the refund left, in its own
|
|
726
|
+
* one-way action directly after the commit.
|
|
727
|
+
*/
|
|
728
|
+
function validateChargePayout(
|
|
729
|
+
instrument: FinancialInstrument,
|
|
730
|
+
quote: QuoteFacts,
|
|
639
731
|
refundSource: string | undefined,
|
|
640
|
-
required: boolean,
|
|
641
732
|
add: (path: readonly PropertyKey[], message: string) => void,
|
|
642
733
|
): void {
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
734
|
+
const chargePath = `refs.${quote.chargeRef}`;
|
|
735
|
+
const uses = Object.entries(instrument.actions).flatMap(
|
|
736
|
+
([actionName, action]) =>
|
|
737
|
+
(action.moves ?? []).flatMap((step, stepIndex) =>
|
|
738
|
+
Object.entries(step.bind).flatMap(([target, binding]) =>
|
|
739
|
+
binding.from === "instance" && binding.path === chargePath
|
|
740
|
+
? [{ binding, step, stepIndex, target, action, actionName }]
|
|
741
|
+
: [],
|
|
742
|
+
),
|
|
649
743
|
),
|
|
650
|
-
),
|
|
651
744
|
);
|
|
652
|
-
if (
|
|
745
|
+
if (quote.chargeCanBeNonzero && uses.length !== 1) {
|
|
653
746
|
add(
|
|
654
|
-
["
|
|
655
|
-
|
|
747
|
+
["actions"],
|
|
748
|
+
`${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`,
|
|
656
749
|
);
|
|
657
750
|
}
|
|
658
751
|
if (uses.length === 0) return;
|
|
659
752
|
if (uses.length !== 1) {
|
|
660
|
-
if (!
|
|
753
|
+
if (!quote.chargeCanBeNonzero) {
|
|
661
754
|
add(
|
|
662
|
-
["
|
|
663
|
-
|
|
755
|
+
["actions"],
|
|
756
|
+
`${chargePath} is consumed ${uses.length} times; it may fund at most one payout`,
|
|
664
757
|
);
|
|
665
758
|
}
|
|
666
759
|
return;
|
|
667
760
|
}
|
|
668
761
|
|
|
669
762
|
const use = uses[0] as (typeof uses)[number];
|
|
670
|
-
const payoutTransition =
|
|
671
|
-
const
|
|
672
|
-
const source = canonicalAccount(
|
|
763
|
+
const payoutTransition = instrument.lifecycle.transitions[use.actionName];
|
|
764
|
+
const commitTarget = instrument.lifecycle.transitions[quote.commit]?.to;
|
|
765
|
+
const source = canonicalAccount(instrument, use.step.bind.sourceAccountId);
|
|
673
766
|
const destination = canonicalAccount(
|
|
674
|
-
|
|
767
|
+
instrument,
|
|
675
768
|
use.step.bind.destinationAccountId,
|
|
676
769
|
);
|
|
677
|
-
const
|
|
678
|
-
|
|
770
|
+
const directAfterCommit =
|
|
771
|
+
commitTarget !== undefined &&
|
|
679
772
|
payoutTransition?.from.length === 1 &&
|
|
680
|
-
payoutTransition.from[0] ===
|
|
773
|
+
payoutTransition.from[0] === commitTarget;
|
|
681
774
|
const oneWay =
|
|
682
775
|
payoutTransition !== undefined &&
|
|
683
|
-
payoutTransition.to !==
|
|
684
|
-
!stateCanReach(
|
|
776
|
+
payoutTransition.to !== commitTarget &&
|
|
777
|
+
!stateCanReach(instrument, payoutTransition.to, commitTarget);
|
|
685
778
|
const valid =
|
|
686
779
|
use.target === "amount" &&
|
|
687
780
|
use.step.operation === "internal_transfer.create" &&
|
|
688
|
-
(use.
|
|
689
|
-
|
|
781
|
+
(use.action.moves?.length ?? 0) === 1 &&
|
|
782
|
+
directAfterCommit &&
|
|
690
783
|
oneWay &&
|
|
691
784
|
source !== undefined &&
|
|
692
785
|
source === refundSource &&
|
|
@@ -694,14 +787,14 @@ function validatePenaltyPayout(
|
|
|
694
787
|
!accountsMayAlias(source, destination);
|
|
695
788
|
if (!valid) {
|
|
696
789
|
add(
|
|
697
|
-
["
|
|
698
|
-
|
|
790
|
+
["actions", use.actionName, "moves", use.stepIndex, "bind", use.target],
|
|
791
|
+
`${chargePath} must be the amount of one internal transfer from the quoted refund source to a different account, in its own one-way action directly after ${quote.commit}`,
|
|
699
792
|
);
|
|
700
793
|
}
|
|
701
794
|
}
|
|
702
795
|
|
|
703
796
|
function canonicalAccount(
|
|
704
|
-
|
|
797
|
+
instrument: FinancialInstrument,
|
|
705
798
|
binding: UdlBinding | undefined,
|
|
706
799
|
): string | undefined {
|
|
707
800
|
if (binding?.from !== "instance") return undefined;
|
|
@@ -711,7 +804,9 @@ function canonicalAccount(
|
|
|
711
804
|
if (root === "refs") return `ref:${key}`;
|
|
712
805
|
if (root === "party") {
|
|
713
806
|
const field =
|
|
714
|
-
|
|
807
|
+
instrument.parties?.[
|
|
808
|
+
key as keyof NonNullable<FinancialInstrument["parties"]>
|
|
809
|
+
];
|
|
715
810
|
return field ? `field:${field}` : undefined;
|
|
716
811
|
}
|
|
717
812
|
return undefined;
|
|
@@ -741,7 +836,7 @@ function boundTransferRefKey(step: FinancialStep): string | undefined {
|
|
|
741
836
|
}
|
|
742
837
|
|
|
743
838
|
function stateCanReach(
|
|
744
|
-
|
|
839
|
+
instrument: FinancialInstrument,
|
|
745
840
|
start: string,
|
|
746
841
|
target: string | undefined,
|
|
747
842
|
): boolean {
|
|
@@ -751,7 +846,7 @@ function stateCanReach(
|
|
|
751
846
|
while (pending.length > 0) {
|
|
752
847
|
const state = pending.shift() as string;
|
|
753
848
|
if (state === target) return true;
|
|
754
|
-
for (const transition of Object.values(
|
|
849
|
+
for (const transition of Object.values(instrument.lifecycle.transitions)) {
|
|
755
850
|
if (!transition.from.includes(state) || seen.has(transition.to)) continue;
|
|
756
851
|
seen.add(transition.to);
|
|
757
852
|
pending.push(transition.to);
|