@hyperscale0/udl 1.0.0 → 1.0.123

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 (234) hide show
  1. package/CHANGELOG.md +5 -216
  2. package/LICENSE.md +356 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +14 -106
  5. package/dist/canonical.d.ts.map +1 -1
  6. package/dist/canonical.js +1 -29
  7. package/dist/canonical.js.map +1 -1
  8. package/dist/diagnostics.d.ts +13 -165
  9. package/dist/diagnostics.d.ts.map +1 -1
  10. package/dist/diagnostics.js +33 -157
  11. package/dist/diagnostics.js.map +1 -1
  12. package/dist/field-value.d.ts +5 -0
  13. package/dist/field-value.d.ts.map +1 -0
  14. package/dist/field-value.js +85 -0
  15. package/dist/field-value.js.map +1 -0
  16. package/dist/finance.d.ts +10 -53
  17. package/dist/finance.d.ts.map +1 -1
  18. package/dist/finance.js +252 -536
  19. package/dist/finance.js.map +1 -1
  20. package/dist/index.d.ts +9 -13
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/json.d.ts +2 -0
  25. package/dist/json.d.ts.map +1 -0
  26. package/dist/json.js +30 -0
  27. package/dist/json.js.map +1 -0
  28. package/dist/limits.d.ts +3 -28
  29. package/dist/limits.d.ts.map +1 -1
  30. package/dist/limits.js +5 -31
  31. package/dist/limits.js.map +1 -1
  32. package/dist/object-contract.d.ts +140 -0
  33. package/dist/object-contract.d.ts.map +1 -0
  34. package/dist/object-contract.js +314 -0
  35. package/dist/object-contract.js.map +1 -0
  36. package/dist/object-semantics-schema.d.ts +5710 -0
  37. package/dist/object-semantics-schema.d.ts.map +1 -0
  38. package/dist/object-semantics-schema.js +119 -0
  39. package/dist/object-semantics-schema.js.map +1 -0
  40. package/dist/object-semantics.d.ts +6 -0
  41. package/dist/object-semantics.d.ts.map +1 -0
  42. package/dist/object-semantics.js +279 -0
  43. package/dist/object-semantics.js.map +1 -0
  44. package/dist/object-structures.d.ts +4 -0
  45. package/dist/object-structures.d.ts.map +1 -0
  46. package/dist/object-structures.js +134 -0
  47. package/dist/object-structures.js.map +1 -0
  48. package/dist/reporting-validation.d.ts +8 -0
  49. package/dist/reporting-validation.d.ts.map +1 -0
  50. package/dist/reporting-validation.js +252 -0
  51. package/dist/reporting-validation.js.map +1 -0
  52. package/dist/reporting.d.ts +477 -0
  53. package/dist/reporting.d.ts.map +1 -0
  54. package/dist/reporting.js +169 -0
  55. package/dist/reporting.js.map +1 -0
  56. package/dist/schema.d.ts +5572 -2710
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +444 -1121
  59. package/dist/schema.js.map +1 -1
  60. package/dist/validation.d.ts +10 -73
  61. package/dist/validation.d.ts.map +1 -1
  62. package/dist/validation.js +1109 -2569
  63. package/dist/validation.js.map +1 -1
  64. package/docs/assets/brand/manifest.json +30 -0
  65. package/docs/assets/brand/udl-horizontal-white.svg +1 -0
  66. package/docs/assets/brand/udl-horizontal.svg +1 -0
  67. package/docs/assets/brand/udl-stacked-white.svg +1 -0
  68. package/docs/assets/brand/udl-stacked.svg +1 -0
  69. package/docs/assets/udl.svg +11 -0
  70. package/package.json +13 -22
  71. package/spec/README.md +300 -172
  72. package/spec/cars.udl.json +249 -0
  73. package/spec/udl.schema.json +3066 -2242
  74. package/src/canonical.ts +1 -35
  75. package/src/diagnostics.ts +76 -209
  76. package/src/field-value.ts +87 -0
  77. package/src/finance.ts +327 -832
  78. package/src/index.ts +18 -95
  79. package/src/json.ts +34 -0
  80. package/src/limits.ts +5 -31
  81. package/src/object-contract.ts +559 -0
  82. package/src/object-semantics-schema.ts +148 -0
  83. package/src/object-semantics.ts +378 -0
  84. package/src/object-structures.ts +255 -0
  85. package/src/reporting-validation.ts +383 -0
  86. package/src/reporting.ts +178 -0
  87. package/src/schema.ts +510 -1277
  88. package/src/validation.ts +1630 -4545
  89. package/LICENSE +0 -661
  90. package/LICENSING.md +0 -51
  91. package/TRADEMARKS.md +0 -35
  92. package/conformance/README.md +0 -82
  93. package/conformance/evolution/action-contract.expected.json +0 -10
  94. package/conformance/evolution/action-contract.live.udl +0 -44
  95. package/conformance/evolution/action-contract.next.udl +0 -45
  96. package/conformance/evolution/product-identity.expected.json +0 -10
  97. package/conformance/evolution/product-identity.live.udl +0 -55
  98. package/conformance/evolution/product-identity.next.udl +0 -55
  99. package/conformance/evolution/version-required.expected.json +0 -10
  100. package/conformance/evolution/version-required.live.udl +0 -55
  101. package/conformance/evolution/version-required.next.udl +0 -58
  102. package/conformance/invalid/action-without-transition.expected.json +0 -10
  103. package/conformance/invalid/action-without-transition.udl +0 -60
  104. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  105. package/conformance/invalid/agent-description-too-long.udl +0 -57
  106. package/conformance/invalid/blank-title.expected.json +0 -10
  107. package/conformance/invalid/blank-title.udl +0 -54
  108. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  109. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  110. package/conformance/invalid/depth-budget.expected.json +0 -10
  111. package/conformance/invalid/depth-budget.udl +0 -49
  112. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  113. package/conformance/invalid/duplicate-subject.udl +0 -230
  114. package/conformance/invalid/forged-effects.expected.json +0 -10
  115. package/conformance/invalid/forged-effects.udl +0 -76
  116. package/conformance/invalid/format-version.expected.json +0 -10
  117. package/conformance/invalid/format-version.udl +0 -54
  118. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  119. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  120. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  121. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  122. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  123. package/conformance/invalid/invalid-check-duration.udl +0 -219
  124. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  125. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  126. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  127. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  128. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  129. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  130. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  131. package/conformance/invalid/invalid-remainder.udl +0 -220
  132. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  133. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  134. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  135. package/conformance/invalid/invalid-utf8.udl +0 -1
  136. package/conformance/invalid/malformed-json.expected.json +0 -10
  137. package/conformance/invalid/malformed-json.udl +0 -1
  138. package/conformance/invalid/missing-create-action.expected.json +0 -10
  139. package/conformance/invalid/missing-create-action.udl +0 -48
  140. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  141. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  142. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  143. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  144. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  145. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  146. package/conformance/invalid/not-an-object.expected.json +0 -10
  147. package/conformance/invalid/not-an-object.udl +0 -1
  148. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  149. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  150. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  151. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  152. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  153. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  154. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  155. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  156. package/conformance/invalid/unknown-key.expected.json +0 -10
  157. package/conformance/invalid/unknown-key.udl +0 -55
  158. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  159. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  160. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  161. package/conformance/invalid/unknown-required-field.udl +0 -220
  162. package/conformance/invalid/unreachable-state.expected.json +0 -10
  163. package/conformance/invalid/unreachable-state.udl +0 -55
  164. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  165. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  166. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  167. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  168. package/conformance/valid/agent-description.expected.json +0 -6
  169. package/conformance/valid/agent-description.udl +0 -66
  170. package/conformance/valid/cards.expected.json +0 -6
  171. package/conformance/valid/cards.udl +0 -1579
  172. package/conformance/valid/commerce-escrow.expected.json +0 -6
  173. package/conformance/valid/commerce-escrow.udl +0 -1512
  174. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  175. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  176. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  177. package/conformance/valid/compiled-watch-club.udl +0 -2486
  178. package/conformance/valid/complete-contract.expected.json +0 -6
  179. package/conformance/valid/complete-contract.udl +0 -218
  180. package/conformance/valid/effect-signatures.expected.json +0 -6
  181. package/conformance/valid/effect-signatures.udl +0 -75
  182. package/conformance/valid/hand-edited.expected.json +0 -6
  183. package/conformance/valid/hand-edited.udl +0 -1
  184. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  185. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  186. package/conformance/valid/insured-travel.expected.json +0 -6
  187. package/conformance/valid/insured-travel.udl +0 -3469
  188. package/conformance/valid/minimal.expected.json +0 -6
  189. package/conformance/valid/minimal.udl +0 -54
  190. package/conformance/valid/protection.expected.json +0 -6
  191. package/conformance/valid/protection.udl +0 -1551
  192. package/conformance/valid/string-escaping.expected.json +0 -6
  193. package/conformance/valid/string-escaping.udl +0 -54
  194. package/dist/check-profiles.d.ts +0 -57
  195. package/dist/check-profiles.d.ts.map +0 -1
  196. package/dist/check-profiles.js +0 -62
  197. package/dist/check-profiles.js.map +0 -1
  198. package/dist/cli.d.ts +0 -3
  199. package/dist/cli.d.ts.map +0 -1
  200. package/dist/cli.js +0 -184
  201. package/dist/cli.js.map +0 -1
  202. package/dist/effects.d.ts +0 -41
  203. package/dist/effects.d.ts.map +0 -1
  204. package/dist/effects.js +0 -110
  205. package/dist/effects.js.map +0 -1
  206. package/dist/evolution.d.ts +0 -120
  207. package/dist/evolution.d.ts.map +0 -1
  208. package/dist/evolution.js +0 -567
  209. package/dist/evolution.js.map +0 -1
  210. package/dist/parser.d.ts +0 -4
  211. package/dist/parser.d.ts.map +0 -1
  212. package/dist/parser.js +0 -44
  213. package/dist/parser.js.map +0 -1
  214. package/docs/README.md +0 -14
  215. package/docs/guide/01-a-document.md +0 -37
  216. package/docs/guide/02-money-steps.md +0 -23
  217. package/docs/guide/03-laws.md +0 -18
  218. package/docs/guide/04-fees-and-remainder.md +0 -15
  219. package/docs/guide/05-checks-updates-dials.md +0 -7
  220. package/docs/guide/06-effects.md +0 -11
  221. package/docs/guide/07-evolution.md +0 -11
  222. package/docs/guide/08-implementing.md +0 -13
  223. package/docs/llms-full.txt +0 -1241
  224. package/docs/llms.txt +0 -14
  225. package/docs/reference/canonical.md +0 -16
  226. package/docs/reference/clauses.md +0 -986
  227. package/docs/reference/cli.md +0 -22
  228. package/docs/reference/diagnostics.md +0 -32
  229. package/skills/udl/SKILL.md +0 -28
  230. package/src/check-profiles.ts +0 -80
  231. package/src/cli.ts +0 -196
  232. package/src/effects.ts +0 -163
  233. package/src/evolution.ts +0 -965
  234. package/src/parser.ts +0 -58
package/dist/finance.js CHANGED
@@ -1,567 +1,283 @@
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;
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);
37
11
  }
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)
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)
45
28
  continue;
46
- refundSources.set(quote.commit, source);
47
- trackedAccounts.add(source);
48
- }
49
- const refundSourceAccounts = new Set(refundSources.values());
50
- if (trackedAccounts.size === 0)
51
- return [];
52
- if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
53
- add(["actions"], `financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`, "UDL1004");
54
- return issues;
55
- }
56
- const reservations = reservationsByKey(instrument);
57
- for (const quote of quotes) {
58
- validateChargePayout(instrument, quote, refundSources.get(quote.commit), add);
59
- }
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(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;
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]}`);
93
53
  }
94
- const targetState = applyEffects(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
- for (const [state, variants] of states) {
112
- if (nonterminalStates.has(state))
113
- continue;
114
- for (const variant of variants.values()) {
115
- if (variant.balance.kind === "empty" &&
116
- Object.keys(variant.holds).length === 0) {
117
- continue;
118
- }
119
- add(["lifecycle", "states", instrument.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
120
- }
121
- }
122
54
  }
123
- return issues;
124
- }
125
- export function financeAdmissionProblem(instrument) {
126
- if (instrument.lifecycle.states.length > UDL_LIMITS.financeStates) {
127
- return `financial analysis exceeds ${UDL_LIMITS.financeStates} lifecycle states`;
128
- }
129
- let transitionCount = 0;
130
- let transitionEdges = 0;
131
- for (const name in instrument.lifecycle.transitions) {
132
- if (!Object.hasOwn(instrument.lifecycle.transitions, name))
133
- continue;
134
- transitionCount += 1;
135
- if (transitionCount > UDL_LIMITS.financeTransitions) {
136
- return `financial analysis exceeds ${UDL_LIMITS.financeTransitions} lifecycle transitions`;
137
- }
138
- transitionEdges += instrument.lifecycle.transitions[name]?.from.length ?? 0;
139
- if (transitionEdges > UDL_LIMITS.financeTransitionEdges) {
140
- return `financial analysis exceeds ${UDL_LIMITS.financeTransitionEdges} lifecycle transition edges`;
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]),
68
+ ];
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);
141
73
  }
142
74
  }
143
- let actionCount = 0;
144
- let effectCount = 0;
145
- for (const name in instrument.actions) {
146
- if (!Object.hasOwn(instrument.actions, name))
147
- continue;
148
- actionCount += 1;
149
- if (actionCount > UDL_LIMITS.financeActions) {
150
- return `financial analysis exceeds ${UDL_LIMITS.financeActions} actions`;
151
- }
152
- const action = instrument.actions[name];
153
- effectCount += (action?.steps.length ?? 0) + (action?.moves?.length ?? 0);
154
- if (effectCount > UDL_LIMITS.financeEffects) {
155
- return `financial analysis exceeds ${UDL_LIMITS.financeEffects} kernel effects`;
156
- }
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))]]);
157
86
  }
158
- return undefined;
159
- }
160
- function productEscrowAccounts(instrument) {
161
- const accounts = new Set();
162
- for (const action of Object.values(instrument.actions)) {
163
- for (const step of action.steps) {
164
- if (step.operation !== "account.escrow.provision" ||
165
- step.bind.role?.from !== "const" ||
166
- step.bind.role.value !== "product_escrow") {
167
- continue;
168
- }
169
- for (const [key, result] of Object.entries(step.capture ?? {})) {
170
- if (result === "accountId")
171
- accounts.add(`ref:${key}`);
172
- }
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);
173
94
  }
95
+ return result;
174
96
  }
175
- // `product_escrow` also backs open-ended balance products such as wallets.
176
- // Only instance-bound amounts claim static conservation; caller-sized flows
177
- // remain runtime balance checks. Commit sources are added separately above.
178
- return new Set([...accounts].filter((account) => Object.values(instrument.actions).some((action) => (action.moves ?? []).some((step) => {
179
- if ((step.operation !== "internal_transfer.create" &&
180
- step.operation !== "internal_transfer.reserve") ||
181
- step.bind.amount?.from !== "instance") {
182
- return false;
183
- }
184
- return (canonicalAccount(instrument, step.bind.sourceAccountId) ===
185
- account ||
186
- canonicalAccount(instrument, step.bind.destinationAccountId) ===
187
- account);
188
- }))));
189
- }
190
- function reservationsByKey(instrument) {
191
- const reservations = new Map();
192
- for (const action of Object.values(instrument.actions)) {
193
- for (const step of action.moves ?? []) {
194
- if (step.operation !== "internal_transfer.reserve")
195
- continue;
196
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
197
- const destination = canonicalAccount(instrument, step.bind.destinationAccountId);
198
- const amount = amountIdentity(step.bind.amount);
199
- for (const [key, result] of Object.entries(step.capture ?? {})) {
200
- if (result === "transferId") {
201
- reservations.set(key, {
202
- amount,
203
- ...(destination ? { destination } : {}),
204
- ...(source ? { source } : {}),
205
- });
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]]);
206
117
  }
207
118
  }
208
- }
209
- }
210
- return reservations;
211
- }
212
- function effectsByAction(instrument, account, reservations, quotes) {
213
- const quoteByCommit = new Map(quotes.map((quote) => [quote.commit, quote]));
214
- const chargePaths = new Map(quotes.map((quote) => [`refs.${quote.chargeRef}`, quote]));
215
- return new Map(Object.entries(instrument.actions).map(([actionName, action]) => [
216
- actionName,
217
- (action.moves ?? []).flatMap((step, stepIndex) => {
218
- const path = [
219
- "actions",
220
- actionName,
221
- "moves",
222
- stepIndex,
223
- "bind",
224
- ];
225
- if (step.operation === "internal_transfer.create") {
226
- const effects = [];
227
- const amount = amountIdentity(step.bind.amount);
228
- if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
229
- account) {
230
- effects.push({ amount, kind: "credit", path });
231
- }
232
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
233
- if (source && accountsMayAlias(source, account)) {
234
- const committed = quoteByCommit.get(actionName);
235
- const amountPath = step.bind.amount?.from === "instance"
236
- ? step.bind.amount.path
237
- : undefined;
238
- const refunded = committed && amountPath === `refs.${committed.netRef}`
239
- ? committed
240
- : undefined;
241
- const charged = amountPath
242
- ? chargePaths.get(amountPath)
243
- : undefined;
244
- const kind = refunded
245
- ? "refund"
246
- : charged
247
- ? "penalty_debit"
248
- : amountPath !== undefined
249
- ? "debit"
250
- : "dynamic_debit";
251
- effects.push({
252
- amount: refunded && !refunded.chargeCanBeNonzero
253
- ? "refund_without_penalty"
254
- : amount,
255
- kind,
256
- path,
257
- ...(refunded ? { refundable: refunded.baseField } : {}),
258
- });
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));
131
+ }
259
132
  }
260
- return effects;
261
133
  }
262
- if (step.operation === "internal_transfer.reserve") {
263
- const reservation = transferRefKey(step) ?? `${actionName}:${stepIndex}`;
264
- const effects = [];
265
- const source = canonicalAccount(instrument, step.bind.sourceAccountId);
266
- if (source && accountsMayAlias(source, account)) {
267
- effects.push({
268
- amount: amountIdentity(step.bind.amount),
269
- kind: "outgoing_reserve",
270
- path,
271
- reservation,
272
- });
273
- }
274
- if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
275
- account) {
276
- effects.push({
277
- amount: amountIdentity(step.bind.amount),
278
- kind: "incoming_reserve",
279
- path,
280
- reservation,
281
- });
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 });
282
157
  }
283
- return effects;
158
+ else
159
+ credit(move.to, value);
284
160
  }
285
- if (step.operation === "internal_transfer.post" ||
286
- step.operation === "internal_transfer.void") {
287
- const reservation = boundTransferRefKey(step);
288
- if (!reservation)
289
- return [];
290
- const reserved = reservations.get(reservation);
291
- if (!reserved ||
292
- (!(reserved.source && accountsMayAlias(reserved.source, account)) &&
293
- reserved.destination !== account)) {
294
- return [];
161
+ else {
162
+ const hold = state.holds.get(move.transfer);
163
+ if (!hold) {
164
+ report(`${move.key} consumes a missing reservation ${move.transfer}`);
165
+ continue;
295
166
  }
296
- return [
297
- {
298
- amount: reserved.amount,
299
- kind: step.operation === "internal_transfer.post" ? "post" : "void",
300
- path,
301
- reservation,
302
- },
303
- ];
167
+ credit(move.operation === "internal_transfer.post" ? hold.to : hold.from, hold.amount);
168
+ state.holds.delete(move.transfer);
304
169
  }
305
- return [];
306
- }),
307
- ]));
308
- }
309
- function applyEffects(account, action, input, effects, reservations, exactBalance, add) {
310
- let balance = input.balance;
311
- const holds = { ...input.holds };
312
- const accountLabel = formatAccount(account);
313
- for (const effect of effects) {
314
- const fail = (message) => add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
315
- if (effect.kind === "credit") {
316
- if (exactBalance &&
317
- (balance.kind !== "empty" || Object.keys(holds).length > 0)) {
318
- add([...effect.path, "destinationAccountId"], `action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`);
319
- balance = UNKNOWN;
320
- continue;
321
- }
322
- const pieces = balance.kind === "funded" ? balance.amounts : [];
323
- const stacks = (balance.kind === "empty" || balance.kind === "funded") &&
324
- !pieces.includes(effect.amount);
325
- balance = stacks ? funded([...pieces, effect.amount]) : UNKNOWN;
326
- continue;
327
170
  }
328
- if (effect.kind === "incoming_reserve")
329
- continue;
330
- if (effect.kind === "outgoing_reserve") {
331
- const held = balance.kind === "funded" && balance.amounts.includes(effect.amount);
332
- if (!held) {
333
- fail(`cannot reserve ${effect.amount} from ${accountLabel}; that exact balance is not guaranteed`);
334
- }
335
- holds[effect.reservation] = held
336
- ? { kind: "funded", amounts: [effect.amount] }
337
- : UNKNOWN;
338
- balance =
339
- held && balance.kind === "funded"
340
- ? funded(withoutPiece(balance.amounts, effect.amount))
341
- : EMPTY;
342
- continue;
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);
343
174
  }
344
- if (effect.kind === "post" || effect.kind === "void") {
345
- const key = effect.reservation;
346
- const reservation = reservations.get(key);
347
- if (reservation?.source &&
348
- accountsMayAlias(reservation.source, account)) {
349
- const held = holds[key];
350
- delete holds[key];
351
- if (effect.kind === "void" && held) {
352
- const pieces = balance.kind === "funded" ? balance.amounts : [];
353
- const restorable = held.kind === "funded" &&
354
- (balance.kind === "empty" ||
355
- (balance.kind === "funded" &&
356
- held.amounts.every((piece) => !pieces.includes(piece))));
357
- if (restorable && held.kind === "funded") {
358
- balance = funded([...pieces, ...held.amounts]);
359
- }
360
- else if (balance.kind === "empty") {
361
- balance = held;
362
- }
363
- else {
364
- add([...effect.path, "transferId"], `action ${action} restores a held ${accountLabel} balance on top of existing value`);
365
- balance = UNKNOWN;
366
- }
175
+ };
176
+ const initial = {
177
+ status: instrument.lifecycle.initial,
178
+ actions: [],
179
+ paths: new Map(),
180
+ balances: new Map([...owned].map((key) => [key, new Map()])),
181
+ holds: new Map(),
182
+ };
183
+ if (instrument.actions.create) {
184
+ apply(initial, instrument.actions.create, "create");
185
+ for (const [account, balance] of initial.balances)
186
+ if (balance === null || balance.size)
187
+ initial.paths.set(account, ["create"]);
188
+ }
189
+ const pending = [initial];
190
+ const reached = new Map();
191
+ const copy = (state) => ({
192
+ status: state.status,
193
+ actions: [...state.actions],
194
+ paths: new Map(state.paths),
195
+ balances: new Map(state.balances),
196
+ holds: new Map([...state.holds].map(([key, hold]) => [key, { ...hold }])),
197
+ });
198
+ let work = 0;
199
+ while (pending.length && !problems.length) {
200
+ if (++work > 4096) {
201
+ report("owned proof exceeds its finite work budget");
202
+ break;
203
+ }
204
+ let state = pending.pop();
205
+ const key = JSON.stringify([state.status, [...state.holds.keys()].sort()]);
206
+ const previous = reached.get(key);
207
+ if (previous) {
208
+ const joined = copy(previous);
209
+ let changed = false;
210
+ for (const account of owned) {
211
+ const before = previous.balances.has(account)
212
+ ? previous.balances.get(account)
213
+ : new Map();
214
+ const after = state.balances.has(account)
215
+ ? state.balances.get(account)
216
+ : new Map();
217
+ if (before !== null && !same(before, after)) {
218
+ joined.balances.set(account, null);
219
+ if (!before.size && state.paths.has(account))
220
+ joined.paths.set(account, state.paths.get(account));
221
+ changed = true;
367
222
  }
368
223
  }
369
- if (effect.kind === "post" && reservation?.destination === account) {
370
- if (balance.kind !== "empty" || Object.keys(holds).length > 0) {
371
- add([...effect.path, "transferId"], `action ${action} posts funding into ${accountLabel} while earlier value may remain`);
372
- balance = UNKNOWN;
224
+ for (const [name, hold] of joined.holds) {
225
+ const next = state.holds.get(name);
226
+ if (hold.from !== next.from || hold.to !== next.to) {
227
+ report(`${name} changes reservation endpoints across paths`);
228
+ break;
373
229
  }
374
- else {
375
- balance = { kind: "funded", amounts: [effect.amount] };
230
+ if (hold.amount !== null && !same(hold.amount, next.amount)) {
231
+ hold.amount = null;
232
+ changed = true;
376
233
  }
377
234
  }
378
- continue;
379
- }
380
- if (effect.kind === "refund") {
381
- const refundable = `fields.${effect.refundable ?? ""}`;
382
- if (balance.kind !== "funded" ||
383
- balance.amounts.length !== 1 ||
384
- balance.amounts[0] !== refundable) {
385
- fail(`cannot refund ${refundable} from ${accountLabel}; that exact balance is not guaranteed`);
386
- balance = UNKNOWN;
387
- }
388
- else {
389
- balance =
390
- effect.amount === "refund_without_penalty"
391
- ? EMPTY
392
- : { kind: "penalty" };
393
- }
394
- continue;
395
- }
396
- if (effect.kind === "penalty_debit") {
397
- if (balance.kind !== "penalty") {
398
- fail(`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`);
399
- }
400
- balance = EMPTY;
401
- continue;
402
- }
403
- if (effect.kind === "dynamic_debit") {
404
- if (balance.kind === "empty") {
405
- fail(`can debit unfunded ${accountLabel}`);
406
- }
407
- else if (balance.kind === "penalty") {
408
- fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
409
- }
410
- balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
411
- continue;
412
- }
413
- if (balance.kind === "funded" && balance.amounts.includes(effect.amount)) {
414
- balance = funded(withoutPiece(balance.amounts, effect.amount));
415
- continue;
416
- }
417
- if (balance.kind === "empty") {
418
- fail(`can debit unfunded ${accountLabel}`);
419
- }
420
- else if (balance.kind === "penalty") {
421
- fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
422
- }
423
- else if (balance.kind === "funded" || exactBalance) {
424
- fail(`cannot prove ${effect.amount} is the exact available balance of ${accountLabel}`);
235
+ if (!changed)
236
+ continue;
237
+ state = joined;
425
238
  }
426
- balance = EMPTY;
427
- }
428
- return { balance, holds };
429
- }
430
- /**
431
- * Flattens every quote-commit pair the instrument declares. A quoting action
432
- * with no committing action, or a commit naming a non-quoting action, is a
433
- * validator failure rather than a typestate one, so it is simply skipped here.
434
- */
435
- function quoteFacts(instrument, options) {
436
- const facts = [];
437
- for (const [quoting, action] of Object.entries(instrument.actions)) {
438
- const quote = action.quote;
439
- if (!quote)
440
- continue;
441
- const commit = Object.entries(instrument.actions).find(([, candidate]) => candidate.commit === quoting)?.[0];
442
- if (commit === undefined)
443
- continue;
444
- const chargePath = `refs.${quote.chargeRef}`;
445
- const declaresChargePayout = Object.values(instrument.actions).some((candidate) => (candidate.moves ?? []).some((move) => Object.values(move.bind).some((binding) => binding.from === "instance" && binding.path === chargePath)));
446
- facts.push({
447
- baseField: quote.baseField,
448
- chargeCanBeNonzero: options.penaltyMayBeNonzero === true ||
449
- declaresChargePayout ||
450
- quote.charges.some((tier) => tier.bps > 0),
451
- chargeRef: quote.chargeRef,
452
- commit,
453
- netRef: quote.netRef,
454
- quoting,
455
- });
456
- }
457
- return facts;
458
- }
459
- /**
460
- * The quoted charge is the exact remainder the commit leaves behind, so it may
461
- * fund one transfer, out of the same account the refund left, in its own
462
- * one-way action directly after the commit.
463
- */
464
- function validateChargePayout(instrument, quote, refundSource, add) {
465
- const chargePath = `refs.${quote.chargeRef}`;
466
- 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
467
- ? [{ binding, step, stepIndex, target, action, actionName }]
468
- : [])));
469
- if (quote.chargeCanBeNonzero && uses.length !== 1) {
470
- add(["actions"], `${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`);
471
- }
472
- if (uses.length === 0)
473
- return;
474
- if (uses.length !== 1) {
475
- if (!quote.chargeCanBeNonzero) {
476
- add(["actions"], `${chargePath} is consumed ${uses.length} times; it may fund at most one payout`);
239
+ if (reached.size >= 256 && !previous) {
240
+ report("owned proof exceeds 256 state and reservation combinations");
241
+ break;
477
242
  }
478
- return;
479
- }
480
- const use = uses[0];
481
- const payoutTransition = instrument.lifecycle.transitions[use.actionName];
482
- const commitTarget = instrument.lifecycle.transitions[quote.commit]?.to;
483
- const source = canonicalAccount(instrument, use.step.bind.sourceAccountId);
484
- const destination = canonicalAccount(instrument, use.step.bind.destinationAccountId);
485
- const directAfterCommit = commitTarget !== undefined &&
486
- payoutTransition?.from.length === 1 &&
487
- payoutTransition.from[0] === commitTarget;
488
- const oneWay = payoutTransition !== undefined &&
489
- payoutTransition.to !== commitTarget &&
490
- !stateCanReach(instrument, payoutTransition.to, commitTarget);
491
- const valid = use.target === "amount" &&
492
- use.step.operation === "internal_transfer.create" &&
493
- (use.action.moves?.length ?? 0) === 1 &&
494
- directAfterCommit &&
495
- oneWay &&
496
- source !== undefined &&
497
- source === refundSource &&
498
- destination !== undefined &&
499
- !accountsMayAlias(source, destination);
500
- if (!valid) {
501
- 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}`);
502
- }
503
- }
504
- function canonicalAccount(instrument, binding) {
505
- if (binding?.from !== "instance")
506
- return undefined;
507
- const [root, key, extra] = binding.path.split(".");
508
- if (!key || extra !== undefined)
509
- return undefined;
510
- if (root === "fields")
511
- return `field:${key}`;
512
- if (root === "refs")
513
- return `ref:${key}`;
514
- if (root === "party") {
515
- const field = instrument.parties?.[key];
516
- return field ? `field:${field}` : undefined;
517
- }
518
- return undefined;
519
- }
520
- function accountsMayAlias(left, right) {
521
- if (left.startsWith("field:") && right.startsWith("field:"))
522
- return true;
523
- return left === right;
524
- }
525
- function amountIdentity(binding) {
526
- if (binding?.from === "instance")
527
- return binding.path;
528
- return binding?.from === "const" ? `const:${binding.value}` : "dynamic";
529
- }
530
- function transferRefKey(step) {
531
- return Object.entries(step.capture ?? {}).find(([, result]) => result === "transferId")?.[0];
532
- }
533
- function boundTransferRefKey(step) {
534
- const binding = step.bind.transferId;
535
- return binding?.from === "instance" && binding.path.startsWith("refs.")
536
- ? binding.path.slice("refs.".length)
537
- : undefined;
538
- }
539
- function stateCanReach(instrument, start, target) {
540
- if (!target)
541
- return false;
542
- const seen = new Set([start]);
543
- const pending = [start];
544
- while (pending.length > 0) {
545
- const state = pending.shift();
546
- if (state === target)
547
- return true;
548
- for (const transition of Object.values(instrument.lifecycle.transitions)) {
549
- if (!transition.from.includes(state) || seen.has(transition.to))
243
+ reached.set(key, copy(state));
244
+ const edges = Object.entries(instrument.lifecycle.transitions).filter(([, edge]) => edge.from.includes(state.status));
245
+ if (!edges.length &&
246
+ ([...state.balances].some(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0)) ||
247
+ state.holds.size))
248
+ problems.push({
249
+ path: ".actions",
250
+ message: `terminal state ${state.status} may strand owned money or an open reservation`,
251
+ stranded: {
252
+ state: state.status,
253
+ accounts: [...state.balances]
254
+ .filter(([account, balance]) => !shared.has(account) && (balance === null || balance.size > 0))
255
+ .map(([account]) => account.slice(5)),
256
+ actions: state.actions,
257
+ paths: Object.fromEntries([...state.paths].map(([account, path]) => [account.slice(5), path])),
258
+ },
259
+ });
260
+ for (const [name, edge] of edges) {
261
+ const action = instrument.actions[name];
262
+ if (!action)
550
263
  continue;
551
- seen.add(transition.to);
552
- pending.push(transition.to);
264
+ const next = copy(state);
265
+ if (edge.to !== "preserve")
266
+ next.status = edge.to;
267
+ apply(next, action, name);
268
+ next.actions.push(name);
269
+ for (const [account, balance] of next.balances) {
270
+ if (balance !== null && !balance.size)
271
+ next.paths.delete(account);
272
+ else
273
+ next.paths.set(account, [
274
+ ...(state.paths.get(account) ?? state.actions),
275
+ name,
276
+ ]);
277
+ }
278
+ pending.push(next);
553
279
  }
554
280
  }
555
- return false;
556
- }
557
- function stateKey(state) {
558
- return JSON.stringify({
559
- balance: state.balance,
560
- holds: Object.fromEntries(Object.entries(state.holds).sort(([a], [b]) => a.localeCompare(b))),
561
- });
562
- }
563
- function formatAccount(account) {
564
- const [kind, key] = account.split(":");
565
- return `${kind === "field" ? "fields" : "refs"}.${key}`;
281
+ return problems;
566
282
  }
567
283
  //# sourceMappingURL=finance.js.map