@hyperscale0/udl 2.6.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/CHANGELOG.md +2 -330
  2. package/LICENSING.md +1 -2
  3. package/README.md +3 -114
  4. package/TRADEMARKS.md +2 -2
  5. package/dist/diagnostics.d.ts +21 -201
  6. package/dist/diagnostics.d.ts.map +1 -1
  7. package/dist/diagnostics.js +29 -193
  8. package/dist/diagnostics.js.map +1 -1
  9. package/dist/evolution.d.ts +3 -132
  10. package/dist/evolution.d.ts.map +1 -1
  11. package/dist/evolution.js +29 -633
  12. package/dist/evolution.js.map +1 -1
  13. package/dist/finance.d.ts +5 -72
  14. package/dist/finance.d.ts.map +1 -1
  15. package/dist/finance.js +233 -737
  16. package/dist/finance.js.map +1 -1
  17. package/dist/index.d.ts +7 -18
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +7 -12
  20. package/dist/index.js.map +1 -1
  21. package/dist/instrument-references.d.ts.map +1 -1
  22. package/dist/instrument-references.js +3 -2
  23. package/dist/instrument-references.js.map +1 -1
  24. package/dist/schema.d.ts +2079 -4564
  25. package/dist/schema.d.ts.map +1 -1
  26. package/dist/schema.js +270 -1609
  27. package/dist/schema.js.map +1 -1
  28. package/dist/validation.d.ts +8 -104
  29. package/dist/validation.d.ts.map +1 -1
  30. package/dist/validation.js +653 -3271
  31. package/dist/validation.js.map +1 -1
  32. package/docs/README.md +147 -14
  33. package/package.json +7 -11
  34. package/spec/README.md +151 -164
  35. package/spec/darb.udl.json +249 -0
  36. package/spec/udl.schema.json +4104 -3202
  37. package/src/diagnostics.ts +51 -246
  38. package/src/evolution.ts +39 -1045
  39. package/src/finance.ts +287 -1127
  40. package/src/index.ts +18 -142
  41. package/src/instrument-references.ts +3 -2
  42. package/src/schema.ts +285 -1843
  43. package/src/validation.ts +870 -5611
  44. package/conformance/README.md +0 -82
  45. package/conformance/evolution/action-contract.expected.json +0 -10
  46. package/conformance/evolution/action-contract.live.udl +0 -44
  47. package/conformance/evolution/action-contract.next.udl +0 -45
  48. package/conformance/evolution/product-identity.expected.json +0 -10
  49. package/conformance/evolution/product-identity.live.udl +0 -55
  50. package/conformance/evolution/product-identity.next.udl +0 -55
  51. package/conformance/evolution/version-required.expected.json +0 -10
  52. package/conformance/evolution/version-required.live.udl +0 -55
  53. package/conformance/evolution/version-required.next.udl +0 -58
  54. package/conformance/invalid/action-without-transition.expected.json +0 -10
  55. package/conformance/invalid/action-without-transition.udl +0 -60
  56. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  57. package/conformance/invalid/agent-description-too-long.udl +0 -57
  58. package/conformance/invalid/blank-title.expected.json +0 -10
  59. package/conformance/invalid/blank-title.udl +0 -54
  60. package/conformance/invalid/call-binds-results.expected.json +0 -10
  61. package/conformance/invalid/call-binds-results.udl +0 -314
  62. package/conformance/invalid/call-unknown-action.expected.json +0 -10
  63. package/conformance/invalid/call-unknown-action.udl +0 -314
  64. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  65. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  66. package/conformance/invalid/depth-budget.expected.json +0 -10
  67. package/conformance/invalid/depth-budget.udl +0 -49
  68. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  69. package/conformance/invalid/duplicate-subject.udl +0 -230
  70. package/conformance/invalid/forged-effects.expected.json +0 -10
  71. package/conformance/invalid/forged-effects.udl +0 -76
  72. package/conformance/invalid/format-version.expected.json +0 -10
  73. package/conformance/invalid/format-version.udl +0 -54
  74. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  75. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  76. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  77. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  78. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  79. package/conformance/invalid/invalid-check-duration.udl +0 -219
  80. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  81. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  82. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  83. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  84. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  85. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  86. package/conformance/invalid/invalid-journeys.expected.json +0 -10
  87. package/conformance/invalid/invalid-journeys.udl +0 -77
  88. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  89. package/conformance/invalid/invalid-remainder.udl +0 -220
  90. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  91. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  92. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  93. package/conformance/invalid/invalid-utf8.udl +0 -1
  94. package/conformance/invalid/leaf-effect-mismatch.expected.json +0 -10
  95. package/conformance/invalid/leaf-effect-mismatch.udl +0 -314
  96. package/conformance/invalid/malformed-json.expected.json +0 -10
  97. package/conformance/invalid/malformed-json.udl +0 -1
  98. package/conformance/invalid/missing-create-action.expected.json +0 -10
  99. package/conformance/invalid/missing-create-action.udl +0 -48
  100. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  101. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  102. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  103. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  104. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  105. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  106. package/conformance/invalid/not-an-object.expected.json +0 -10
  107. package/conformance/invalid/not-an-object.udl +0 -1
  108. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  109. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  110. package/conformance/invalid/piece-plan-without-partition.expected.json +0 -10
  111. package/conformance/invalid/piece-plan-without-partition.udl +0 -305
  112. package/conformance/invalid/private-action-independent-approval.expected.json +0 -10
  113. package/conformance/invalid/private-action-independent-approval.udl +0 -314
  114. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  115. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  116. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  117. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  118. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  119. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  120. package/conformance/invalid/unfund-order-not-reversed.expected.json +0 -10
  121. package/conformance/invalid/unfund-order-not-reversed.udl +0 -314
  122. package/conformance/invalid/unknown-key.expected.json +0 -10
  123. package/conformance/invalid/unknown-key.udl +0 -55
  124. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  125. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  126. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  127. package/conformance/invalid/unknown-required-field.udl +0 -220
  128. package/conformance/invalid/unreachable-state.expected.json +0 -10
  129. package/conformance/invalid/unreachable-state.udl +0 -55
  130. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  131. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  132. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  133. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  134. package/conformance/valid/agent-description.expected.json +0 -6
  135. package/conformance/valid/agent-description.udl +0 -66
  136. package/conformance/valid/attested.expected.json +0 -6
  137. package/conformance/valid/attested.udl +0 -251
  138. package/conformance/valid/cards.expected.json +0 -6
  139. package/conformance/valid/cards.udl +0 -1579
  140. package/conformance/valid/commerce-escrow.expected.json +0 -6
  141. package/conformance/valid/commerce-escrow.udl +0 -1512
  142. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  143. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  144. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  145. package/conformance/valid/compiled-watch-club.udl +0 -2486
  146. package/conformance/valid/complete-contract.expected.json +0 -6
  147. package/conformance/valid/complete-contract.udl +0 -218
  148. package/conformance/valid/effect-signatures.expected.json +0 -6
  149. package/conformance/valid/effect-signatures.udl +0 -75
  150. package/conformance/valid/hand-edited.expected.json +0 -6
  151. package/conformance/valid/hand-edited.udl +0 -1
  152. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  153. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  154. package/conformance/valid/insured-travel.expected.json +0 -6
  155. package/conformance/valid/insured-travel.udl +0 -3469
  156. package/conformance/valid/minimal.expected.json +0 -6
  157. package/conformance/valid/minimal.udl +0 -62
  158. package/conformance/valid/piece-plan-calls.expected.json +0 -6
  159. package/conformance/valid/piece-plan-calls.udl +0 -314
  160. package/conformance/valid/protection.expected.json +0 -6
  161. package/conformance/valid/protection.udl +0 -1551
  162. package/conformance/valid/string-escaping.expected.json +0 -6
  163. package/conformance/valid/string-escaping.udl +0 -54
  164. package/conformance/valid/vocabulary.expected.json +0 -6
  165. package/conformance/valid/vocabulary.udl +0 -2012
  166. package/dist/allocation.d.ts +0 -60
  167. package/dist/allocation.d.ts.map +0 -1
  168. package/dist/allocation.js +0 -177
  169. package/dist/allocation.js.map +0 -1
  170. package/dist/check-profiles.d.ts +0 -57
  171. package/dist/check-profiles.d.ts.map +0 -1
  172. package/dist/check-profiles.js +0 -62
  173. package/dist/check-profiles.js.map +0 -1
  174. package/dist/distribution.d.ts +0 -15
  175. package/dist/distribution.d.ts.map +0 -1
  176. package/dist/distribution.js +0 -49
  177. package/dist/distribution.js.map +0 -1
  178. package/dist/effects.d.ts +0 -58
  179. package/dist/effects.d.ts.map +0 -1
  180. package/dist/effects.js +0 -1126
  181. package/dist/effects.js.map +0 -1
  182. package/dist/reference.d.ts +0 -3
  183. package/dist/reference.d.ts.map +0 -1
  184. package/dist/reference.js +0 -28
  185. package/dist/reference.js.map +0 -1
  186. package/dist/vocabulary.d.ts +0 -23
  187. package/dist/vocabulary.d.ts.map +0 -1
  188. package/dist/vocabulary.js +0 -1052
  189. package/dist/vocabulary.js.map +0 -1
  190. package/docs/funding-custody.md +0 -165
  191. package/docs/guide/01-a-document.md +0 -37
  192. package/docs/guide/02-money-steps.md +0 -23
  193. package/docs/guide/03-laws.md +0 -18
  194. package/docs/guide/04-fees-and-remainder.md +0 -36
  195. package/docs/guide/05-checks-updates-dials.md +0 -7
  196. package/docs/guide/06-effects.md +0 -11
  197. package/docs/guide/07-evolution.md +0 -11
  198. package/docs/guide/08-implementing.md +0 -13
  199. package/docs/guide/09-schedules-and-allocation.md +0 -132
  200. package/docs/llms-full.txt +0 -2008
  201. package/docs/llms.txt +0 -14
  202. package/docs/piece-plans.md +0 -148
  203. package/docs/reference/canonical.md +0 -16
  204. package/docs/reference/clauses.md +0 -1585
  205. package/docs/reference/cli.md +0 -24
  206. package/docs/reference/diagnostics.md +0 -38
  207. package/skills/udl/SKILL.md +0 -28
  208. package/src/allocation.ts +0 -259
  209. package/src/check-profiles.ts +0 -80
  210. package/src/distribution.ts +0 -61
  211. package/src/effects.ts +0 -1920
  212. package/src/reference.ts +0 -31
  213. package/src/vocabulary.ts +0 -1635
package/dist/finance.js CHANGED
@@ -1,755 +1,251 @@
1
- import { UDL_LIMITS } from "./limits.js";
2
- const EMPTY = { kind: "empty" };
3
- const UNKNOWN = { kind: "unknown" };
4
- // A funded balance is the exact set of amount identities currently held.
5
- // Distinct identities stack (a partitioned funding); re-crediting an identity
6
- // already present degrades to unknown, which keeps the state space finite.
7
- function funded(amounts) {
8
- if (amounts.length === 0)
9
- return EMPTY;
10
- return { kind: "funded", amounts: [...amounts].sort() };
11
- }
12
- function withoutPiece(amounts, amount) {
13
- const index = amounts.indexOf(amount);
14
- return amounts.filter((_, position) => position !== index);
15
- }
16
- /**
17
- * One financial typestate owner for canonical UDL and static instrument contracts.
18
- * It proves a tracked escrow account holds an exact set of funded amounts,
19
- * models held transfers through reserve/post/void, consumes every direct
20
- * debit piece by piece, and makes the quoted refund's sole remainder an
21
- * exactly-once penalty payout.
22
- */
23
- export function analyzeInstrumentFinance(instrument, options = {}) {
24
- const issues = [];
25
- const seenIssues = new Set();
26
- const add = (path, message, code = "UDL4001") => {
27
- const key = `${path.join(".")}\0${message}`;
28
- if (seenIssues.has(key))
29
- return;
30
- seenIssues.add(key);
31
- issues.push({ code, message, path });
32
- };
33
- const admissionProblem = financeAdmissionProblem(instrument);
34
- if (admissionProblem) {
35
- add(["lifecycle"], admissionProblem, "UDL1004");
36
- return issues;
37
- }
38
- const trackedAccounts = productEscrowAccounts(instrument);
39
- const quotes = quoteFacts(instrument, options);
40
- const refundSources = new Map();
41
- for (const quote of quotes) {
42
- const commitTransfer = instrument.actions[quote.commit]?.moves?.filter((move) => move.operation === "internal_transfer.create")[0];
43
- const source = canonicalAccount(instrument, commitTransfer?.bind.sourceAccountId);
44
- if (!source)
45
- continue;
46
- refundSources.set(quote.commit, source);
47
- trackedAccounts.add(source);
48
- }
49
- const refundSourceAccounts = new Set(refundSources.values());
50
- for (const quote of quotes) {
51
- validateChargePayout(instrument, quote, refundSources.get(quote.commit), add);
52
- }
53
- if (trackedAccounts.size === 0)
54
- return issues;
55
- if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
56
- add(["actions"], `financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`, "UDL1004");
57
- return issues;
58
- }
59
- const reservations = reservationsByKey(instrument);
60
- let pathVariants = 0;
61
- let work = 0;
62
- for (const account of trackedAccounts) {
63
- const effects = effectsByAction(instrument, account, reservations, quotes);
64
- const createEffects = effects.get("create") ?? [];
65
- work += 1 + createEffects.length;
66
- if (work > UDL_LIMITS.financeWork) {
67
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
68
- return issues;
69
- }
70
- const initial = applyEffects(instrument, account, "create", { balance: EMPTY, holds: {} }, createEffects, reservations, refundSourceAccounts.has(account), add);
71
- const states = new Map([
72
- [instrument.lifecycle.initial, new Map([[stateKey(initial), initial]])],
73
- ]);
74
- pathVariants += 1;
75
- const pending = [instrument.lifecycle.initial];
76
- while (pending.length > 0) {
77
- const from = pending.shift();
78
- const sourceStates = states.get(from);
79
- if (!sourceStates)
80
- continue;
81
- for (const [action, transition] of Object.entries(instrument.lifecycle.transitions)) {
82
- if (!transition.from.includes(from))
83
- continue;
84
- const targetStates = states.get(transition.to) ?? new Map();
85
- states.set(transition.to, targetStates);
86
- let grew = false;
87
- for (const sourceState of sourceStates.values()) {
88
- const transitionEffects = effects.get(action) ?? [];
89
- work += 1 + transitionEffects.length;
90
- if (work > UDL_LIMITS.financeWork) {
91
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
92
- return issues;
93
- }
94
- const targetState = applyEffects(instrument, account, action, sourceState, transitionEffects, reservations, refundSourceAccounts.has(account), add);
95
- const key = stateKey(targetState);
96
- if (targetStates.has(key))
97
- continue;
98
- targetStates.set(key, targetState);
99
- pathVariants += 1;
100
- if (pathVariants > UDL_LIMITS.financePathVariants) {
101
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financePathVariants} distinct path variants`, "UDL1004");
102
- return issues;
103
- }
104
- grew = true;
105
- }
106
- if (grew && !pending.includes(transition.to))
107
- pending.push(transition.to);
108
- }
109
- }
110
- const nonterminalStates = new Set(Object.values(instrument.lifecycle.transitions).flatMap((transition) => transition.from));
111
- if (account.startsWith("ref:")) {
112
- for (const [state, variants] of states) {
113
- if (nonterminalStates.has(state))
114
- continue;
115
- for (const variant of variants.values()) {
116
- if (variant.balance.kind === "empty" &&
117
- Object.keys(variant.holds).length === 0) {
118
- continue;
1
+ const add = (a, b, sign = 1n) => {
2
+ if (a === null || b === null)
3
+ return null;
4
+ const result = new Map(a);
5
+ for (const [key, value] of b) {
6
+ const next = (result.get(key) ?? 0n) + value * sign;
7
+ if (next)
8
+ result.set(key, next);
9
+ else
10
+ result.delete(key);
11
+ }
12
+ return result;
13
+ };
14
+ const show = (sum) => sum === null
15
+ ? "unknown"
16
+ : JSON.stringify([...sum]
17
+ .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
18
+ .map(([key, value]) => [key, String(value)]));
19
+ const same = (a, b) => show(a) === show(b);
20
+ /** Join loop balances conservatively; a balance equality proves an unknown balance empty. */
21
+ export function analyzeInstrumentFinance(instrument, document) {
22
+ const owned = new Set(instrument.fields
23
+ .filter((f) => f.type === "account" && f.owner === "self")
24
+ .map((f) => `self.${f.name}`));
25
+ const shared = new Set();
26
+ for (const owner of document?.instruments ?? []) {
27
+ if (owner.id === instrument.id)
28
+ continue;
29
+ for (const action of Object.values(owner.actions))
30
+ for (const move of action.moves)
31
+ if ("amount" in move)
32
+ for (const endpoint of [move.from, move.to]) {
33
+ const parts = endpoint.split(".");
34
+ if (parts.shift() !== "self")
35
+ continue;
36
+ let targets = [owner];
37
+ while (parts.length > 1) {
38
+ const key = parts.shift();
39
+ targets = targets.flatMap((target) => {
40
+ const reference = target.fields.find((field) => field.name === key);
41
+ if (reference?.type !== "ref")
42
+ return [];
43
+ const ids = typeof reference.target === "string"
44
+ ? [reference.target]
45
+ : reference.target;
46
+ return (document?.instruments.filter((candidate) => ids.includes(candidate.id)) ?? []);
47
+ });
48
+ }
49
+ if (parts.length === 1 &&
50
+ targets.some((target) => target.id === instrument.id) &&
51
+ owned.has(`self.${parts[0]}`))
52
+ shared.add(`self.${parts[0]}`);
119
53
  }
120
- add(["lifecycle", "states", instrument.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
121
- }
122
- }
123
- }
124
- }
125
- return issues;
126
- }
127
- export function financeAdmissionProblem(instrument) {
128
- if (instrument.lifecycle.states.length > UDL_LIMITS.financeStates) {
129
- return `financial analysis exceeds ${UDL_LIMITS.financeStates} lifecycle states`;
130
- }
131
- let transitionCount = 0;
132
- let transitionEdges = 0;
133
- for (const name in instrument.lifecycle.transitions) {
134
- if (!Object.hasOwn(instrument.lifecycle.transitions, name))
135
- continue;
136
- transitionCount += 1;
137
- if (transitionCount > UDL_LIMITS.financeTransitions) {
138
- return `financial analysis exceeds ${UDL_LIMITS.financeTransitions} lifecycle transitions`;
139
- }
140
- transitionEdges += instrument.lifecycle.transitions[name]?.from.length ?? 0;
141
- if (transitionEdges > UDL_LIMITS.financeTransitionEdges) {
142
- return `financial analysis exceeds ${UDL_LIMITS.financeTransitionEdges} lifecycle transition edges`;
143
- }
144
54
  }
145
- let actionCount = 0;
146
- let effectCount = 0;
147
- for (const name in instrument.actions) {
148
- if (!Object.hasOwn(instrument.actions, name))
149
- continue;
150
- actionCount += 1;
151
- if (actionCount > UDL_LIMITS.financeActions) {
152
- return `financial analysis exceeds ${UDL_LIMITS.financeActions} actions`;
153
- }
154
- const action = instrument.actions[name];
155
- effectCount += (action?.steps.length ?? 0) + (action?.moves?.length ?? 0);
156
- if (effectCount > UDL_LIMITS.financeEffects) {
157
- return `financial analysis exceeds ${UDL_LIMITS.financeEffects} kernel effects`;
158
- }
159
- }
160
- return undefined;
161
- }
162
- function productEscrowAccounts(instrument) {
163
- const accounts = new Set();
164
- for (const action of Object.values(instrument.actions)) {
165
- for (const step of action.steps) {
166
- // This operation fixes the escrow role; an omitted redundant binding
167
- // must not exempt its captured account from the conservation proof.
168
- if (step.operation !== "account.escrow.provision")
169
- continue;
170
- for (const [key, result] of Object.entries(step.capture ?? {})) {
171
- if (result === "accountId")
172
- accounts.add(`ref:${key}`);
173
- }
174
- }
175
- }
176
- // `product_escrow` also backs open-ended balance products such as wallets.
177
- // Only instance-bound amounts claim static conservation; caller-sized flows
178
- // remain runtime balance checks. Commit sources are added separately above.
179
- return new Set([...accounts].filter((account) => Object.values(instrument.actions).some((action) => {
180
- if (action.contributionStage &&
181
- canonicalAccountPath(instrument, action.contributionStage.accountPath) === account) {
182
- return true;
183
- }
184
- return (action.moves ?? []).some((step) => {
185
- if ((step.operation !== "internal_transfer.create" &&
186
- step.operation !== "internal_transfer.reserve") ||
187
- step.bind.amount?.from !== "instance") {
188
- return false;
189
- }
190
- return (canonicalAccount(instrument, step.bind.sourceAccountId) ===
191
- account ||
192
- canonicalAccount(instrument, step.bind.destinationAccountId) ===
193
- account);
194
- });
195
- })));
196
- }
197
- function reservationsByKey(instrument) {
198
- const reservations = new Map();
199
- for (const action of Object.values(instrument.actions)) {
200
- for (const step of action.moves ?? []) {
201
- if (step.operation !== "internal_transfer.reserve")
202
- continue;
203
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
204
- const destination = canonicalAccount(instrument, step.bind.destinationAccountId);
205
- const amount = amountIdentity(step.bind.amount);
206
- for (const [key, result] of Object.entries(step.capture ?? {})) {
207
- if (result === "transferId") {
208
- reservations.set(key, {
209
- amount,
210
- ...(destination ? { destination } : {}),
211
- ...(source ? { source } : {}),
212
- });
213
- }
214
- }
215
- }
216
- }
217
- return reservations;
218
- }
219
- function effectsByAction(instrument, account, reservations, quotes) {
220
- const quoteByCommit = new Map(quotes.map((quote) => [quote.commit, quote]));
221
- const chargePaths = new Map(quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]));
222
- return new Map(Object.entries(instrument.actions).map(([actionName, action]) => {
223
- const stageEffects = [];
224
- if (action.contributionStage && instrument.contributions) {
225
- const stage = action.contributionStage;
226
- const stageAccount = canonicalAccountPath(instrument, stage.accountPath);
227
- if (stageAccount &&
228
- accountsMayAlias(instrument, stageAccount, account)) {
229
- const path = ["actions", actionName, "contributionStage"];
230
- const totalAmount = `fields.${instrument.contributions.totalField}`;
231
- if (stage.stage === "fund") {
232
- stageEffects.push({
233
- amount: totalAmount,
234
- kind: "credit",
235
- path,
236
- });
237
- }
238
- else if (stage.stage === "refund") {
239
- stageEffects.push({
240
- amount: totalAmount,
241
- kind: "debit",
242
- path,
243
- });
244
- }
245
- }
246
- }
247
- const moveEffects = (action.moves ?? []).flatMap((step, stepIndex) => {
248
- const path = [
249
- "actions",
250
- actionName,
251
- "moves",
252
- stepIndex,
253
- "bind",
254
- ];
255
- if (step.operation === "internal_transfer.create") {
256
- const effects = [];
257
- const amount = amountIdentity(step.bind.amount);
258
- if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
259
- account) {
260
- effects.push({ amount, kind: "credit", path });
261
- }
262
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
263
- if (source && accountsMayAlias(instrument, source, account)) {
264
- const committed = quoteByCommit.get(actionName);
265
- const amountPath = step.bind.amount?.from === "instance"
266
- ? step.bind.amount.path
267
- : undefined;
268
- const refunded = committed && amountPath === `refs.${committed.netRef}`
269
- ? committed
270
- : undefined;
271
- const charged = amountPath
272
- ? chargePaths.get(amountPath)
273
- : undefined;
274
- const kind = refunded
275
- ? "refund"
276
- : charged
277
- ? "penalty_debit"
278
- : amountPath !== undefined
279
- ? "debit"
280
- : "dynamic_debit";
281
- effects.push({
282
- amount: refunded && !refunded.chargeCanBeNonzero
283
- ? "refund_without_penalty"
284
- : amount,
285
- kind,
286
- path,
287
- ...(refunded ? { refundable: refunded.baseField } : {}),
288
- });
289
- }
290
- return effects;
291
- }
292
- if (step.operation === "internal_transfer.reserve") {
293
- const reservation = transferRefKey(step) ?? `${actionName}:${stepIndex}`;
294
- const effects = [];
295
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
296
- if (source && accountsMayAlias(instrument, source, account)) {
297
- effects.push({
298
- amount: amountIdentity(step.bind.amount),
299
- kind: "outgoing_reserve",
300
- path,
301
- reservation,
302
- });
303
- }
304
- if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
305
- account) {
306
- effects.push({
307
- amount: amountIdentity(step.bind.amount),
308
- kind: "incoming_reserve",
309
- path,
310
- reservation,
311
- });
312
- }
313
- return effects;
314
- }
315
- if (step.operation === "internal_transfer.post" ||
316
- step.operation === "internal_transfer.void") {
317
- const reservation = boundTransferRefKey(step);
318
- if (!reservation)
319
- return [];
320
- const reserved = reservations.get(reservation);
321
- if (!reserved ||
322
- (!(reserved.source &&
323
- accountsMayAlias(instrument, reserved.source, account)) &&
324
- reserved.destination !== account)) {
325
- return [];
326
- }
327
- return [
328
- {
329
- amount: reserved.amount,
330
- kind: step.operation === "internal_transfer.post" ? "post" : "void",
331
- path,
332
- reservation,
333
- },
55
+ const problems = [];
56
+ const report = (message) => problems.push({ path: ".actions", message });
57
+ const equations = new Map();
58
+ for (const field of instrument.fields)
59
+ if (field.type === "money" && field.value !== undefined)
60
+ equations.set(`self.${field.name}`, new Map([["#", BigInt(field.value)]]));
61
+ for (const calc of instrument.calculate) {
62
+ if (calc.op === "sum" || calc.op === "subtract") {
63
+ const terms = calc.op === "sum"
64
+ ? calc.values.map((v) => [v, 1n])
65
+ : [
66
+ [calc.base, 1n],
67
+ ...calc.subtract.map((v) => [v, -1n]),
334
68
  ];
335
- }
336
- return [];
337
- });
338
- return [actionName, [...stageEffects, ...moveEffects]];
339
- }));
340
- }
341
- function getInstrumentPartitions(instrument) {
342
- if (!instrument.partitions)
343
- return [];
344
- const partitions = [];
345
- for (const p of instrument.partitions) {
346
- if (!p.totalField || !p.pieceFields || p.pieceFields.length === 0)
347
- continue;
348
- const total = `fields.${p.totalField}`;
349
- const pieces = p.pieceFields.map((f) => `fields.${f}`);
350
- if (new Set(pieces).size !== pieces.length)
351
- continue;
352
- if (pieces.includes(total) && pieces.length > 1)
353
- continue;
354
- partitions.push({ total, pieces });
355
- }
356
- return partitions;
357
- }
358
- function tryPartitionSubstitution(instrument, balance, holds, exchanged, targetAmount) {
359
- if (exchanged)
360
- return null;
361
- if (balance.kind !== "funded")
362
- return null;
363
- if (Object.keys(holds).length > 0)
364
- return null;
365
- const declaredPartitions = getInstrumentPartitions(instrument);
366
- if (declaredPartitions.length === 0)
367
- return null;
368
- for (const pOut of declaredPartitions) {
369
- const total = pOut.total;
370
- let candidateOutPieces = null;
371
- if (pOut.pieces.includes(targetAmount)) {
372
- candidateOutPieces = pOut.pieces;
373
- }
374
- else if (targetAmount === total) {
375
- candidateOutPieces = [total];
376
- }
377
- if (!candidateOutPieces)
378
- continue;
379
- if (candidateOutPieces.length === 0)
380
- continue;
381
- if (new Set(candidateOutPieces).size !== candidateOutPieces.length)
382
- continue;
383
- const incomingCandidates = [];
384
- if (balance.amounts.includes(total)) {
385
- incomingCandidates.push([total]);
386
- }
387
- for (const pIn of declaredPartitions) {
388
- if (pIn.total !== total)
389
- continue;
390
- if (pIn.pieces.every((piece) => balance.amounts.includes(piece))) {
391
- incomingCandidates.push(pIn.pieces);
392
- }
393
- }
394
- for (const candidateInPieces of incomingCandidates) {
395
- if (candidateInPieces.length === candidateOutPieces.length &&
396
- candidateInPieces.every((piece) => candidateOutPieces.includes(piece))) {
397
- continue;
398
- }
399
- if (balance.amounts.length === candidateInPieces.length &&
400
- candidateInPieces.every((piece) => balance.amounts.includes(piece))) {
401
- return funded(candidateOutPieces);
402
- }
403
- }
404
- }
405
- return null;
406
- }
407
- function applyEffects(instrument, account, action, input, effects, reservations, exactBalance, add) {
408
- let balance = input.balance;
409
- const holds = { ...input.holds };
410
- let exchanged = input.exchanged ?? false;
411
- const accountLabel = formatAccount(account);
412
- for (const effect of effects) {
413
- const fail = (message) => add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
414
- if (effect.kind === "credit") {
415
- if (exactBalance &&
416
- (balance.kind !== "empty" || Object.keys(holds).length > 0)) {
417
- add([...effect.path, "destinationAccountId"], `action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`);
418
- balance = UNKNOWN;
419
- continue;
420
- }
421
- const pieces = balance.kind === "funded" ? balance.amounts : [];
422
- const stacks = (balance.kind === "empty" || balance.kind === "funded") &&
423
- !pieces.includes(effect.amount);
424
- balance = stacks ? funded([...pieces, effect.amount]) : UNKNOWN;
425
- continue;
426
- }
427
- if (effect.kind === "incoming_reserve")
428
- continue;
429
- if (effect.kind === "outgoing_reserve") {
430
- let held = balance.kind === "funded" && balance.amounts.includes(effect.amount);
431
- if (!held && balance.kind === "funded") {
432
- const substituted = tryPartitionSubstitution(instrument, balance, holds, exchanged, effect.amount);
433
- if (substituted !== null) {
434
- exchanged = true;
435
- balance = substituted;
436
- held =
437
- balance.kind === "funded" &&
438
- balance.amounts.includes(effect.amount);
439
- }
440
- }
441
- if (!held) {
442
- fail(`cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`);
443
- }
444
- holds[effect.reservation] = held
445
- ? { kind: "funded", amounts: [effect.amount] }
446
- : UNKNOWN;
447
- balance =
448
- held && balance.kind === "funded"
449
- ? funded(withoutPiece(balance.amounts, effect.amount))
450
- : EMPTY;
451
- continue;
452
- }
453
- if (effect.kind === "post" || effect.kind === "void") {
454
- const key = effect.reservation;
455
- const reservation = reservations.get(key);
456
- if (reservation?.source &&
457
- accountsMayAlias(instrument, reservation.source, account)) {
458
- const held = holds[key];
459
- delete holds[key];
460
- if (effect.kind === "void" && held) {
461
- const pieces = balance.kind === "funded" ? balance.amounts : [];
462
- const restorable = held.kind === "funded" &&
463
- (balance.kind === "empty" ||
464
- (balance.kind === "funded" &&
465
- held.amounts.every((piece) => !pieces.includes(piece))));
466
- if (restorable && held.kind === "funded") {
467
- balance = funded([...pieces, ...held.amounts]);
468
- }
469
- else if (balance.kind === "empty") {
470
- balance = held;
69
+ let result = new Map();
70
+ for (const [value, sign] of terms)
71
+ result = add(result, raw(value, "create"), sign);
72
+ equations.set(`self.${calc.target}`, result);
73
+ }
74
+ }
75
+ function raw(value, invocation) {
76
+ return "field" in value
77
+ ? new Map([
78
+ [
79
+ value.field.startsWith("input.")
80
+ ? `${invocation}:${value.field}`
81
+ : value.field,
82
+ 1n,
83
+ ],
84
+ ])
85
+ : new Map([["#", BigInt(String(value.literal))]]);
86
+ }
87
+ function expand(sum, seen = new Set()) {
88
+ let result = new Map();
89
+ for (const [key, coefficient] of sum) {
90
+ const equation = equations.get(key);
91
+ result = add(result, equation && !seen.has(key)
92
+ ? expand(equation, new Set([...seen, key]))
93
+ : new Map([[key, 1n]]), coefficient);
94
+ }
95
+ return result;
96
+ }
97
+ let invocation = 0;
98
+ const apply = (state, action, name) => {
99
+ const scope = `${name}#${invocation++}`;
100
+ const calculated = new Map();
101
+ const local = new Map((action.calculate ?? []).map((c) => [`self.${c.target}`, c]));
102
+ const amount = (value, seen = new Set()) => {
103
+ if ("field" in value) {
104
+ if (calculated.has(value.field))
105
+ return calculated.get(value.field);
106
+ if (value.field.endsWith(".balance") &&
107
+ owned.has(value.field.slice(0, -8)))
108
+ return state.balances.get(value.field.slice(0, -8)) ?? null;
109
+ const calculation = local.get(value.field);
110
+ if (calculation && !seen.has(value.field)) {
111
+ const next = new Set([...seen, value.field]);
112
+ if (calculation.op === "sum")
113
+ return calculation.values.reduce((sum, value) => add(sum, amount(value, next)), new Map());
114
+ if (calculation.op === "subtract")
115
+ return calculation.subtract.reduce((sum, value) => add(sum, amount(value, next), -1n), amount(calculation.base, next));
116
+ return new Map([[`${scope}:${value.field}`, 1n]]);
117
+ }
118
+ }
119
+ return expand(raw(value, scope));
120
+ };
121
+ for (const requirement of action.requires)
122
+ if (requirement.kind === "compare" && requirement.operator === "==") {
123
+ for (const [left, right] of [
124
+ [requirement.left, requirement.right],
125
+ [requirement.right, requirement.left],
126
+ ]) {
127
+ if ("field" in left && left.field.endsWith(".balance")) {
128
+ const account = left.field.slice(0, -8);
129
+ if (owned.has(account))
130
+ state.balances.set(account, amount(right));
471
131
  }
472
- else {
473
- add([...effect.path, "transferId"], `action ${action} restores a held ${accountLabel} balance on top of existing value`);
474
- balance = UNKNOWN;
475
- }
476
- }
477
- }
478
- if (effect.kind === "post" && reservation?.destination === account) {
479
- if (balance.kind !== "empty" || Object.keys(holds).length > 0) {
480
- add([...effect.path, "transferId"], `action ${action} posts funding into ${accountLabel} while earlier value may remain`);
481
- balance = UNKNOWN;
482
- }
483
- else {
484
- balance = { kind: "funded", amounts: [effect.amount] };
485
132
  }
486
133
  }
487
- continue;
488
- }
489
- if (effect.kind === "refund") {
490
- const refundable = `fields.${effect.refundable ?? ""}`;
491
- if (balance.kind !== "funded" ||
492
- balance.amounts.length !== 1 ||
493
- balance.amounts[0] !== refundable) {
494
- fail(`cannot refund ${refundable} from ${accountLabel}; that exact balance is not guaranteed`);
495
- balance = UNKNOWN;
134
+ for (const key of local.keys())
135
+ calculated.set(key, amount({ field: key }));
136
+ const debit = (account, value) => {
137
+ if (owned.has(account))
138
+ state.balances.set(account, add(state.balances.has(account)
139
+ ? state.balances.get(account)
140
+ : new Map(), value, -1n));
141
+ };
142
+ const credit = (account, value) => {
143
+ if (owned.has(account))
144
+ state.balances.set(account, add(state.balances.has(account)
145
+ ? state.balances.get(account)
146
+ : new Map(), value));
147
+ };
148
+ for (const move of action.moves) {
149
+ if ("amount" in move) {
150
+ const value = amount(move.amount);
151
+ debit(move.from, value);
152
+ if (move.operation === "internal_transfer.reserve") {
153
+ const key = `self.${move.capture}`;
154
+ if (state.holds.has(key))
155
+ report(`${move.capture} overwrites a live reservation`);
156
+ state.holds.set(key, { from: move.from, to: move.to, amount: value });
157
+ }
158
+ else
159
+ credit(move.to, value);
496
160
  }
497
161
  else {
498
- balance =
499
- effect.amount === "refund_without_penalty"
500
- ? EMPTY
501
- : { kind: "penalty" };
502
- }
503
- continue;
504
- }
505
- if (effect.kind === "penalty_debit") {
506
- if (balance.kind !== "penalty") {
507
- fail(`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`);
508
- }
509
- balance = EMPTY;
510
- continue;
511
- }
512
- if (effect.kind === "dynamic_debit") {
513
- if (balance.kind === "empty") {
514
- fail(`can debit unfunded ${accountLabel}`);
515
- }
516
- else if (balance.kind === "penalty") {
517
- fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
518
- }
519
- balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
520
- continue;
521
- }
522
- if (balance.kind === "funded") {
523
- if (balance.amounts.includes(effect.amount)) {
524
- balance = funded(withoutPiece(balance.amounts, effect.amount));
525
- continue;
526
- }
527
- const substituted = tryPartitionSubstitution(instrument, balance, holds, exchanged, effect.amount);
528
- if (substituted !== null) {
529
- exchanged = true;
530
- balance = substituted;
531
- if (balance.kind === "funded" &&
532
- balance.amounts.includes(effect.amount)) {
533
- balance = funded(withoutPiece(balance.amounts, effect.amount));
162
+ const hold = state.holds.get(move.transfer);
163
+ if (!hold) {
164
+ report(`${move.key} consumes a missing reservation ${move.transfer}`);
534
165
  continue;
535
166
  }
167
+ credit(move.operation === "internal_transfer.post" ? hold.to : hold.from, hold.amount);
168
+ state.holds.delete(move.transfer);
536
169
  }
537
170
  }
538
- if (balance.kind === "empty") {
539
- fail(exchanged
540
- ? `cannot prove ${effect.amount} is available after the substituted partition was spent from ${accountLabel}`
541
- : `can debit unfunded ${accountLabel}`);
542
- }
543
- else if (balance.kind === "penalty") {
544
- fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
545
- }
546
- else if (balance.kind === "funded" || exactBalance) {
547
- fail(`cannot prove ${effect.amount} is the exact available balance of ${accountLabel}`);
548
- }
549
- balance = EMPTY;
550
- }
551
- // A held account permits at most one equal-partition substitution per funded
552
- // balance, preventing cycles, duplicate minting, and double spending. When an
553
- // account is completely drained to empty with no active holds, the exchange
554
- // record resets for any distinct subsequent funding.
555
- if (balance.kind === "empty" && Object.keys(holds).length === 0) {
556
- exchanged = false;
557
- }
558
- return { balance, holds, exchanged };
559
- }
560
- /**
561
- * Flattens every quote-commit pair the instrument declares. A quoting action
562
- * with no committing action, or a commit naming a non-quoting action, is a
563
- * validator failure rather than a typestate one, so it is simply skipped here.
564
- */
565
- function quoteFacts(instrument, options) {
566
- const facts = [];
567
- for (const [quoting, action] of Object.entries(instrument.actions)) {
568
- const quote = action.quote;
569
- if (!quote)
570
- continue;
571
- const commit = Object.entries(instrument.actions).find(([, candidate]) => candidate.commit === quoting)?.[0];
572
- if (commit === undefined)
573
- continue;
574
- const chargePath = `refs.${quote.chargeRef}`;
575
- const declaresChargePayout = Object.values(instrument.actions).some((candidate) => (candidate.moves ?? []).some((move) => Object.values(move.bind).some((binding) => binding.from === "instance" && binding.path === chargePath)));
576
- facts.push({
577
- baseField: quote.baseField,
578
- chargeCanBeNonzero: options.penaltyMayBeNonzero === true ||
579
- declaresChargePayout ||
580
- quote.charges.some((tier) => tier.bps > 0),
581
- chargeRef: quote.chargeRef,
582
- chargeRetainedBy: quote.chargeRetainedBy,
583
- commit,
584
- netRef: quote.netRef,
585
- quoting,
586
- });
587
- }
588
- return facts;
589
- }
590
- /**
591
- * The quoted charge is the exact remainder the commit leaves behind, so it may
592
- * fund one transfer, out of the same account the refund left, in its own
593
- * one-way action directly after the commit.
594
- */
595
- function validateChargePayout(instrument, quote, refundSource, add) {
596
- const chargePath = `refs.${quote.chargeRef}`;
597
- const uses = Object.entries(instrument.actions).flatMap(([actionName, action]) => (action.moves ?? []).flatMap((step, stepIndex) => Object.entries(step.bind).flatMap(([target, binding]) => binding.from === "instance" && binding.path === chargePath
598
- ? [{ binding, step, stepIndex, target, action, actionName }]
599
- : [])));
600
- if (quote.chargeRetainedBy !== undefined) {
601
- const role = quote.chargeRetainedBy;
602
- const partyField = Object.hasOwn(instrument.parties ?? {}, role)
603
- ? instrument.parties?.[role]
604
- : undefined;
605
- if (!partyField) {
606
- add(["actions", quote.quoting, "quote", "chargeRetainedBy"], `quoting action ${quote.quoting} retains charge by undeclared party role ${role}`);
607
- }
608
- else {
609
- const expectedSource = `field:${partyField}`;
610
- if (refundSource === undefined) {
611
- add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source cannot be resolved to charge-retaining party field ${partyField}`);
612
- }
613
- else if (refundSource !== expectedSource) {
614
- if (refundSource.startsWith("ref:")) {
615
- add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source cannot be a product escrow ref for a retained quote`);
616
- }
617
- else {
618
- add(["actions", quote.commit, "moves"], `commit action ${quote.commit} refund source ${refundSource} does not match charge-retaining party field ${partyField}`);
619
- }
620
- }
621
- }
622
- if (uses.length > 0) {
623
- add(["actions"], `${chargePath} is retained by ${role} and cannot be consumed by an action`);
624
- for (const use of uses) {
625
- add([
626
- "actions",
627
- use.actionName,
628
- "moves",
629
- use.stepIndex,
630
- "bind",
631
- use.target,
632
- ], `${chargePath} is retained by ${role} and cannot be consumed by an action`);
633
- }
634
- }
635
- return;
636
- }
637
- if (quote.chargeCanBeNonzero && uses.length !== 1) {
638
- add(["actions"], `${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`);
639
- }
640
- if (uses.length === 0)
641
- return;
642
- if (uses.length !== 1) {
643
- if (!quote.chargeCanBeNonzero) {
644
- add(["actions"], `${chargePath} is consumed ${uses.length} times; it may fund at most one payout`);
645
- }
646
- return;
647
- }
648
- const use = uses[0];
649
- const payoutTransition = instrument.lifecycle.transitions[use.actionName];
650
- const commitTarget = instrument.lifecycle.transitions[quote.commit]?.to;
651
- const source = canonicalAccount(instrument, use.step.bind.sourceAccountId);
652
- const destination = canonicalAccount(instrument, use.step.bind.destinationAccountId);
653
- const directAfterCommit = commitTarget !== undefined &&
654
- payoutTransition?.from.length === 1 &&
655
- payoutTransition.from[0] === commitTarget;
656
- const oneWay = payoutTransition !== undefined &&
657
- payoutTransition.to !== commitTarget &&
658
- !stateCanReach(instrument, payoutTransition.to, commitTarget);
659
- const valid = use.target === "amount" &&
660
- use.step.operation === "internal_transfer.create" &&
661
- (use.action.moves?.length ?? 0) === 1 &&
662
- directAfterCommit &&
663
- oneWay &&
664
- source !== undefined &&
665
- source === refundSource &&
666
- destination !== undefined &&
667
- !accountsMayAlias(instrument, source, destination);
668
- if (!valid) {
669
- add(["actions", use.actionName, "moves", use.stepIndex, "bind", use.target], `${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}`);
670
- }
671
- }
672
- function canonicalAccount(instrument, binding) {
673
- if (binding?.from !== "instance")
674
- return undefined;
675
- return canonicalAccountPath(instrument, binding.path);
676
- }
677
- function canonicalAccountPath(instrument, path) {
678
- const [root, key, extra] = path.split(".");
679
- if (!key || extra !== undefined)
680
- return undefined;
681
- if (root === "fields")
682
- return `field:${key}`;
683
- if (root === "refs")
684
- return `ref:${key}`;
685
- if (root === "party") {
686
- const field = instrument.parties?.[key];
687
- return field ? `field:${field}` : undefined;
688
- }
689
- return undefined;
690
- }
691
- function accountsMayAlias(instrument, left, right) {
692
- if (left === right)
693
- return true;
694
- if (left.startsWith("field:") && right.startsWith("field:")) {
695
- const leftField = left.slice("field:".length);
696
- const rightField = right.slice("field:".length);
697
- if (leftField === rightField)
698
- return true;
699
- if (instrument.parties) {
700
- const leftParty = Object.entries(instrument.parties).find(([, field]) => field === leftField)?.[0];
701
- const rightParty = Object.entries(instrument.parties).find(([, field]) => field === rightField)?.[0];
702
- if (leftParty !== undefined &&
703
- rightParty !== undefined &&
704
- leftParty !== rightParty) {
705
- return false;
706
- }
171
+ if (Object.keys(action.set ?? {}).some((key) => instrument.fields.some((f) => f.name === key && f.type === "money"))) {
172
+ for (const account of owned)
173
+ state.balances.set(account, null);
707
174
  }
708
- return true;
709
- }
710
- return false;
711
- }
712
- function amountIdentity(binding) {
713
- if (binding?.from === "instance")
714
- return binding.path;
715
- return binding?.from === "const" ? `const:${binding.value}` : "dynamic";
716
- }
717
- function transferRefKey(step) {
718
- return Object.entries(step.capture ?? {}).find(([, result]) => result === "transferId")?.[0];
719
- }
720
- function boundTransferRefKey(step) {
721
- const binding = step.bind.transferId;
722
- return binding?.from === "instance" && binding.path.startsWith("refs.")
723
- ? binding.path.slice("refs.".length)
724
- : undefined;
725
- }
726
- function stateCanReach(instrument, start, target) {
727
- if (!target)
728
- return false;
729
- const seen = new Set([start]);
730
- const pending = [start];
731
- while (pending.length > 0) {
732
- const state = pending.shift();
733
- if (state === target)
734
- return true;
735
- for (const transition of Object.values(instrument.lifecycle.transitions)) {
736
- if (!transition.from.includes(state) || seen.has(transition.to))
175
+ };
176
+ const initial = {
177
+ status: instrument.lifecycle.initial,
178
+ balances: new Map([...owned].map((key) => [key, new Map()])),
179
+ holds: new Map(),
180
+ };
181
+ if (instrument.actions.create)
182
+ apply(initial, instrument.actions.create, "create");
183
+ const pending = [initial];
184
+ const reached = new Map();
185
+ const copy = (state) => ({
186
+ status: state.status,
187
+ balances: new Map(state.balances),
188
+ holds: new Map([...state.holds].map(([key, hold]) => [key, { ...hold }])),
189
+ });
190
+ let work = 0;
191
+ while (pending.length && !problems.length) {
192
+ if (++work > 4096) {
193
+ report("owned proof exceeds its finite work budget");
194
+ break;
195
+ }
196
+ let state = pending.pop();
197
+ const key = JSON.stringify([state.status, [...state.holds.keys()].sort()]);
198
+ const previous = reached.get(key);
199
+ if (previous) {
200
+ const joined = copy(previous);
201
+ let changed = false;
202
+ for (const account of owned) {
203
+ const before = previous.balances.has(account)
204
+ ? previous.balances.get(account)
205
+ : new Map();
206
+ const after = state.balances.has(account)
207
+ ? state.balances.get(account)
208
+ : new Map();
209
+ if (before !== null && !same(before, after)) {
210
+ joined.balances.set(account, null);
211
+ changed = true;
212
+ }
213
+ }
214
+ for (const [name, hold] of joined.holds) {
215
+ const next = state.holds.get(name);
216
+ if (hold.from !== next.from || hold.to !== next.to) {
217
+ report(`${name} changes reservation endpoints across paths`);
218
+ break;
219
+ }
220
+ if (hold.amount !== null && !same(hold.amount, next.amount)) {
221
+ hold.amount = null;
222
+ changed = true;
223
+ }
224
+ }
225
+ if (!changed)
737
226
  continue;
738
- seen.add(transition.to);
739
- pending.push(transition.to);
227
+ state = joined;
228
+ }
229
+ if (reached.size >= 256 && !previous) {
230
+ report("owned proof exceeds 256 state and reservation combinations");
231
+ break;
232
+ }
233
+ reached.set(key, copy(state));
234
+ const edges = Object.entries(instrument.lifecycle.transitions).filter(([, edge]) => edge.from.includes(state.status));
235
+ if (!edges.length &&
236
+ ([...state.balances].some(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0)) ||
237
+ state.holds.size))
238
+ report(`terminal state ${state.status} may strand owned money or an open reservation`);
239
+ for (const [name, edge] of edges) {
240
+ const action = instrument.actions[name];
241
+ if (!action)
242
+ continue;
243
+ const next = copy(state);
244
+ next.status = edge.to;
245
+ apply(next, action, name);
246
+ pending.push(next);
740
247
  }
741
248
  }
742
- return false;
743
- }
744
- function stateKey(state) {
745
- return JSON.stringify({
746
- balance: state.balance,
747
- holds: Object.fromEntries(Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b))),
748
- exchanged: state.exchanged ?? false,
749
- });
750
- }
751
- function formatAccount(account) {
752
- const [kind, key] = account.split(":");
753
- return `${kind === "field" ? "fields" : "refs"}.${key}`;
249
+ return problems;
754
250
  }
755
251
  //# sourceMappingURL=finance.js.map