@hyperscale0/udl 1.0.0-beta.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/CHANGELOG.md +78 -25
  2. package/README.md +37 -99
  3. package/conformance/README.md +44 -41
  4. package/conformance/evolution/action-contract.expected.json +10 -0
  5. package/conformance/evolution/action-contract.live.udl +44 -0
  6. package/conformance/evolution/action-contract.next.udl +45 -0
  7. package/conformance/evolution/product-identity.expected.json +10 -0
  8. package/conformance/evolution/product-identity.live.udl +55 -0
  9. package/conformance/evolution/product-identity.next.udl +55 -0
  10. package/conformance/evolution/version-required.expected.json +10 -0
  11. package/conformance/evolution/version-required.live.udl +55 -0
  12. package/conformance/evolution/version-required.next.udl +58 -0
  13. package/conformance/invalid/action-without-transition.expected.json +10 -0
  14. package/conformance/invalid/{verb-without-transition.udl → action-without-transition.udl} +7 -2
  15. package/conformance/invalid/agent-description-too-long.expected.json +10 -0
  16. package/conformance/invalid/agent-description-too-long.udl +57 -0
  17. package/conformance/invalid/blank-title.expected.json +1 -1
  18. package/conformance/invalid/blank-title.udl +6 -2
  19. package/conformance/invalid/composition-dial-duplicate-key.expected.json +10 -0
  20. package/conformance/invalid/composition-dial-duplicate-key.udl +73 -0
  21. package/conformance/invalid/depth-budget.expected.json +1 -1
  22. package/conformance/invalid/duplicate-subject.expected.json +10 -0
  23. package/conformance/invalid/duplicate-subject.udl +230 -0
  24. package/conformance/invalid/forged-effects.expected.json +10 -0
  25. package/conformance/invalid/forged-effects.udl +76 -0
  26. package/conformance/invalid/format-version.expected.json +1 -1
  27. package/conformance/invalid/format-version.udl +6 -2
  28. package/conformance/invalid/instrument-id-not-snake-case.expected.json +10 -0
  29. package/conformance/invalid/{noun-id-not-snake-case.udl → instrument-id-not-snake-case.udl} +6 -2
  30. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +10 -0
  31. package/conformance/invalid/invalid-aggregate-gate-shape.udl +1819 -0
  32. package/conformance/invalid/invalid-check-duration.expected.json +10 -0
  33. package/conformance/invalid/invalid-check-duration.udl +219 -0
  34. package/conformance/invalid/invalid-dial-anchor.expected.json +10 -0
  35. package/conformance/invalid/invalid-dial-anchor.udl +219 -0
  36. package/conformance/invalid/invalid-exception-parent-ref.expected.json +10 -0
  37. package/conformance/invalid/invalid-exception-parent-ref.udl +2487 -0
  38. package/conformance/invalid/invalid-exposure-shape.expected.json +10 -0
  39. package/conformance/invalid/invalid-exposure-shape.udl +1819 -0
  40. package/conformance/invalid/invalid-remainder.expected.json +10 -0
  41. package/conformance/invalid/invalid-remainder.udl +220 -0
  42. package/conformance/invalid/invalid-schema-keyword.expected.json +10 -0
  43. package/conformance/invalid/invalid-schema-keyword.udl +220 -0
  44. package/conformance/invalid/invalid-utf8.expected.json +1 -1
  45. package/conformance/invalid/malformed-json.expected.json +1 -1
  46. package/conformance/invalid/missing-create-action.expected.json +10 -0
  47. package/conformance/invalid/{missing-create-verb.udl → missing-create-action.udl} +5 -2
  48. package/conformance/invalid/missing-exception-amount-field.expected.json +10 -0
  49. package/conformance/invalid/missing-exception-amount-field.udl +2487 -0
  50. package/conformance/invalid/missing-exception-contract.expected.json +14 -0
  51. package/conformance/invalid/missing-exception-contract.udl +2523 -0
  52. package/conformance/invalid/missing-exception-reason-field.expected.json +10 -0
  53. package/conformance/invalid/missing-exception-reason-field.udl +2487 -0
  54. package/conformance/invalid/not-an-object.expected.json +1 -1
  55. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +10 -0
  56. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +259 -0
  57. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +10 -0
  58. package/conformance/invalid/quote-freeze-set-incomplete.udl +261 -0
  59. package/conformance/invalid/quote-named-reference-gate.expected.json +10 -0
  60. package/conformance/invalid/quote-named-reference-gate.udl +50 -0
  61. package/conformance/invalid/reconcile-named-reference-gate.expected.json +10 -0
  62. package/conformance/invalid/reconcile-named-reference-gate.udl +50 -0
  63. package/conformance/invalid/unknown-key.expected.json +1 -1
  64. package/conformance/invalid/unknown-key.udl +6 -2
  65. package/conformance/invalid/unknown-reference-gate-field.expected.json +10 -0
  66. package/conformance/invalid/unknown-reference-gate-field.udl +1819 -0
  67. package/conformance/invalid/unknown-required-field.expected.json +10 -0
  68. package/conformance/invalid/unknown-required-field.udl +220 -0
  69. package/conformance/invalid/unreachable-state.expected.json +2 -2
  70. package/conformance/invalid/unreachable-state.udl +6 -2
  71. package/conformance/invalid/wrong-exception-amount-field.expected.json +10 -0
  72. package/conformance/invalid/wrong-exception-amount-field.udl +2487 -0
  73. package/conformance/invalid/wrong-exception-reason-field.expected.json +10 -0
  74. package/conformance/invalid/wrong-exception-reason-field.udl +2487 -0
  75. package/conformance/valid/agent-description.expected.json +6 -0
  76. package/conformance/valid/agent-description.udl +66 -0
  77. package/conformance/valid/cards.expected.json +1 -0
  78. package/conformance/valid/cards.udl +843 -356
  79. package/conformance/valid/commerce-escrow.expected.json +1 -0
  80. package/conformance/valid/commerce-escrow.udl +792 -291
  81. package/conformance/valid/compiled-crowdfunding.expected.json +6 -0
  82. package/conformance/valid/compiled-crowdfunding.udl +1843 -0
  83. package/conformance/valid/compiled-watch-club.expected.json +6 -0
  84. package/conformance/valid/compiled-watch-club.udl +2486 -0
  85. package/conformance/valid/complete-contract.expected.json +6 -0
  86. package/conformance/valid/complete-contract.udl +218 -0
  87. package/conformance/valid/effect-signatures.expected.json +6 -0
  88. package/conformance/valid/effect-signatures.udl +75 -0
  89. package/conformance/valid/hand-edited.expected.json +1 -0
  90. package/conformance/valid/hand-edited.udl +1 -1
  91. package/conformance/valid/insured-car-marketplace.expected.json +1 -0
  92. package/conformance/valid/insured-car-marketplace.udl +534 -315
  93. package/conformance/valid/insured-travel.expected.json +2 -1
  94. package/conformance/valid/insured-travel.udl +1489 -641
  95. package/conformance/valid/minimal.expected.json +1 -0
  96. package/conformance/valid/minimal.udl +18 -14
  97. package/conformance/valid/protection.expected.json +1 -0
  98. package/conformance/valid/protection.udl +719 -304
  99. package/conformance/valid/string-escaping.expected.json +6 -0
  100. package/conformance/valid/string-escaping.udl +54 -0
  101. package/dist/canonical.d.ts +2 -0
  102. package/dist/canonical.d.ts.map +1 -1
  103. package/dist/canonical.js +7 -0
  104. package/dist/canonical.js.map +1 -1
  105. package/dist/check-profiles.d.ts +57 -0
  106. package/dist/check-profiles.d.ts.map +1 -0
  107. package/dist/check-profiles.js +62 -0
  108. package/dist/check-profiles.js.map +1 -0
  109. package/dist/cli.js +48 -4
  110. package/dist/cli.js.map +1 -1
  111. package/dist/diagnostics.d.ts +168 -0
  112. package/dist/diagnostics.d.ts.map +1 -0
  113. package/dist/diagnostics.js +164 -0
  114. package/dist/diagnostics.js.map +1 -0
  115. package/dist/effects.d.ts +41 -0
  116. package/dist/effects.d.ts.map +1 -0
  117. package/dist/effects.js +110 -0
  118. package/dist/effects.js.map +1 -0
  119. package/dist/evolution.d.ts +51 -18
  120. package/dist/evolution.d.ts.map +1 -1
  121. package/dist/evolution.js +244 -102
  122. package/dist/evolution.js.map +1 -1
  123. package/dist/finance.d.ts +17 -13
  124. package/dist/finance.d.ts.map +1 -1
  125. package/dist/finance.js +165 -109
  126. package/dist/finance.js.map +1 -1
  127. package/dist/index.d.ts +12 -8
  128. package/dist/index.d.ts.map +1 -1
  129. package/dist/index.js +8 -5
  130. package/dist/index.js.map +1 -1
  131. package/dist/limits.d.ts +2 -2
  132. package/dist/limits.d.ts.map +1 -1
  133. package/dist/limits.js +4 -3
  134. package/dist/limits.js.map +1 -1
  135. package/dist/parser.d.ts.map +1 -1
  136. package/dist/parser.js +6 -16
  137. package/dist/parser.js.map +1 -1
  138. package/dist/schema.d.ts +2059 -135
  139. package/dist/schema.d.ts.map +1 -1
  140. package/dist/schema.js +759 -73
  141. package/dist/schema.js.map +1 -1
  142. package/dist/validation.d.ts +22 -9
  143. package/dist/validation.d.ts.map +1 -1
  144. package/dist/validation.js +1463 -457
  145. package/dist/validation.js.map +1 -1
  146. package/docs/README.md +14 -0
  147. package/docs/guide/01-a-document.md +37 -0
  148. package/docs/guide/02-money-steps.md +23 -0
  149. package/docs/guide/03-laws.md +18 -0
  150. package/docs/guide/04-fees-and-remainder.md +15 -0
  151. package/docs/guide/05-checks-updates-dials.md +7 -0
  152. package/docs/guide/06-effects.md +11 -0
  153. package/docs/guide/07-evolution.md +11 -0
  154. package/docs/guide/08-implementing.md +13 -0
  155. package/docs/llms-full.txt +1241 -0
  156. package/docs/llms.txt +14 -0
  157. package/docs/reference/canonical.md +16 -0
  158. package/docs/reference/clauses.md +986 -0
  159. package/docs/reference/cli.md +22 -0
  160. package/docs/reference/diagnostics.md +32 -0
  161. package/package.json +6 -2
  162. package/skills/udl/SKILL.md +28 -0
  163. package/spec/README.md +82 -51
  164. package/spec/udl.schema.json +1191 -119
  165. package/src/canonical.ts +11 -0
  166. package/src/check-profiles.ts +80 -0
  167. package/src/cli.ts +49 -3
  168. package/src/diagnostics.ts +221 -0
  169. package/src/effects.ts +163 -0
  170. package/src/evolution.ts +375 -125
  171. package/src/finance.ts +255 -160
  172. package/src/index.ts +50 -14
  173. package/src/limits.ts +4 -3
  174. package/src/parser.ts +15 -18
  175. package/src/schema.ts +855 -81
  176. package/src/validation.ts +2666 -560
  177. package/conformance/invalid/missing-create-verb.expected.json +0 -10
  178. package/conformance/invalid/noun-id-not-snake-case.expected.json +0 -10
  179. package/conformance/invalid/verb-without-transition.expected.json +0 -10
package/dist/finance.js CHANGED
@@ -14,90 +14,91 @@ function withoutPiece(amounts, amount) {
14
14
  return amounts.filter((_, position) => position !== index);
15
15
  }
16
16
  /**
17
- * One financial typestate owner for canonical UDL and static noun contracts.
17
+ * One financial typestate owner for canonical UDL and static instrument contracts.
18
18
  * It proves a tracked escrow account holds an exact set of funded amounts,
19
19
  * models held transfers through reserve/post/void, consumes every direct
20
- * debit piece by piece, and makes the unwind refund's sole remainder an
20
+ * debit piece by piece, and makes the quoted refund's sole remainder an
21
21
  * exactly-once penalty payout.
22
22
  */
23
- export function analyzeNounFinance(noun, options = {}) {
23
+ export function analyzeInstrumentFinance(instrument, options = {}) {
24
24
  const issues = [];
25
25
  const seenIssues = new Set();
26
- const add = (path, message) => {
26
+ const add = (path, message, code = "UDL4001") => {
27
27
  const key = `${path.join(".")}\0${message}`;
28
28
  if (seenIssues.has(key))
29
29
  return;
30
30
  seenIssues.add(key);
31
- issues.push({ message, path });
31
+ issues.push({ code, message, path });
32
32
  };
33
- const admissionProblem = financeAdmissionProblem(noun);
33
+ const admissionProblem = financeAdmissionProblem(instrument);
34
34
  if (admissionProblem) {
35
- add(["lifecycle"], admissionProblem);
35
+ add(["lifecycle"], admissionProblem, "UDL1004");
36
36
  return issues;
37
37
  }
38
- const trackedAccounts = workspaceEscrowAccounts(noun);
39
- const confirmTransfer = noun.unwind
40
- ? noun.verbs[noun.unwind.confirm]?.moves?.filter((move) => move.operation === "internal_transfer.create")[0]
41
- : undefined;
42
- const refundSource = canonicalAccount(noun, confirmTransfer?.bind.sourceAccountId);
43
- if (refundSource)
44
- trackedAccounts.add(refundSource);
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());
45
50
  if (trackedAccounts.size === 0)
46
51
  return [];
47
52
  if (trackedAccounts.size > UDL_LIMITS.financeAccounts) {
48
- add(["verbs"], `financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`);
53
+ add(["actions"], `financial analysis exceeds ${UDL_LIMITS.financeAccounts} tracked accounts`, "UDL1004");
49
54
  return issues;
50
55
  }
51
- const reservations = reservationsByKey(noun);
52
- const declaresPenaltyPayout = Object.values(noun.verbs).some((verb) => (verb.moves ?? []).some((move) => Object.values(move.bind).some((binding) => binding.from === "instance" && binding.path === "refs.unwindPenalty")));
53
- const penaltyCanBeNonzero = options.penaltyMayBeNonzero === true ||
54
- declaresPenaltyPayout ||
55
- noun.unwind?.penalty.some((tier) => tier.bps > 0) === true;
56
- if (noun.unwind) {
57
- validatePenaltyPayout(noun, refundSource, penaltyCanBeNonzero, add);
56
+ const reservations = reservationsByKey(instrument);
57
+ for (const quote of quotes) {
58
+ validateChargePayout(instrument, quote, refundSources.get(quote.commit), add);
58
59
  }
59
60
  let pathVariants = 0;
60
61
  let work = 0;
61
62
  for (const account of trackedAccounts) {
62
- const effects = effectsByVerb(noun, account, reservations, penaltyCanBeNonzero);
63
+ const effects = effectsByAction(instrument, account, reservations, quotes);
63
64
  const createEffects = effects.get("create") ?? [];
64
65
  work += 1 + createEffects.length;
65
66
  if (work > UDL_LIMITS.financeWork) {
66
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`);
67
+ add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
67
68
  return issues;
68
69
  }
69
- const initial = applyEffects(noun, account, "create", { balance: EMPTY, holds: {} }, createEffects, reservations, account === refundSource, add);
70
+ const initial = applyEffects(account, "create", { balance: EMPTY, holds: {} }, createEffects, reservations, refundSourceAccounts.has(account), add);
70
71
  const states = new Map([
71
- [noun.lifecycle.initial, new Map([[stateKey(initial), initial]])],
72
+ [instrument.lifecycle.initial, new Map([[stateKey(initial), initial]])],
72
73
  ]);
73
74
  pathVariants += 1;
74
- const pending = [noun.lifecycle.initial];
75
+ const pending = [instrument.lifecycle.initial];
75
76
  while (pending.length > 0) {
76
77
  const from = pending.shift();
77
78
  const sourceStates = states.get(from);
78
79
  if (!sourceStates)
79
80
  continue;
80
- for (const [verb, transition] of Object.entries(noun.lifecycle.transitions)) {
81
+ for (const [action, transition] of Object.entries(instrument.lifecycle.transitions)) {
81
82
  if (!transition.from.includes(from))
82
83
  continue;
83
84
  const targetStates = states.get(transition.to) ?? new Map();
84
85
  states.set(transition.to, targetStates);
85
86
  let grew = false;
86
87
  for (const sourceState of sourceStates.values()) {
87
- const transitionEffects = effects.get(verb) ?? [];
88
+ const transitionEffects = effects.get(action) ?? [];
88
89
  work += 1 + transitionEffects.length;
89
90
  if (work > UDL_LIMITS.financeWork) {
90
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`);
91
+ add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financeWork} deterministic work units`, "UDL1004");
91
92
  return issues;
92
93
  }
93
- const targetState = applyEffects(noun, account, verb, sourceState, transitionEffects, reservations, account === refundSource, add);
94
+ const targetState = applyEffects(account, action, sourceState, transitionEffects, reservations, refundSourceAccounts.has(account), add);
94
95
  const key = stateKey(targetState);
95
96
  if (targetStates.has(key))
96
97
  continue;
97
98
  targetStates.set(key, targetState);
98
99
  pathVariants += 1;
99
100
  if (pathVariants > UDL_LIMITS.financePathVariants) {
100
- add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financePathVariants} distinct path variants`);
101
+ add(["lifecycle"], `financial analysis exceeds ${UDL_LIMITS.financePathVariants} distinct path variants`, "UDL1004");
101
102
  return issues;
102
103
  }
103
104
  grew = true;
@@ -106,7 +107,7 @@ export function analyzeNounFinance(noun, options = {}) {
106
107
  pending.push(transition.to);
107
108
  }
108
109
  }
109
- const nonterminalStates = new Set(Object.values(noun.lifecycle.transitions).flatMap((transition) => transition.from));
110
+ const nonterminalStates = new Set(Object.values(instrument.lifecycle.transitions).flatMap((transition) => transition.from));
110
111
  for (const [state, variants] of states) {
111
112
  if (nonterminalStates.has(state))
112
113
  continue;
@@ -115,54 +116,54 @@ export function analyzeNounFinance(noun, options = {}) {
115
116
  Object.keys(variant.holds).length === 0) {
116
117
  continue;
117
118
  }
118
- add(["lifecycle", "states", noun.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
119
+ add(["lifecycle", "states", instrument.lifecycle.states.indexOf(state)], `terminal state ${state} can strand value in ${formatAccount(account)}`);
119
120
  }
120
121
  }
121
122
  }
122
123
  return issues;
123
124
  }
124
- export function financeAdmissionProblem(noun) {
125
- if (noun.lifecycle.states.length > UDL_LIMITS.financeStates) {
125
+ export function financeAdmissionProblem(instrument) {
126
+ if (instrument.lifecycle.states.length > UDL_LIMITS.financeStates) {
126
127
  return `financial analysis exceeds ${UDL_LIMITS.financeStates} lifecycle states`;
127
128
  }
128
129
  let transitionCount = 0;
129
130
  let transitionEdges = 0;
130
- for (const name in noun.lifecycle.transitions) {
131
- if (!Object.hasOwn(noun.lifecycle.transitions, name))
131
+ for (const name in instrument.lifecycle.transitions) {
132
+ if (!Object.hasOwn(instrument.lifecycle.transitions, name))
132
133
  continue;
133
134
  transitionCount += 1;
134
135
  if (transitionCount > UDL_LIMITS.financeTransitions) {
135
136
  return `financial analysis exceeds ${UDL_LIMITS.financeTransitions} lifecycle transitions`;
136
137
  }
137
- transitionEdges += noun.lifecycle.transitions[name]?.from.length ?? 0;
138
+ transitionEdges += instrument.lifecycle.transitions[name]?.from.length ?? 0;
138
139
  if (transitionEdges > UDL_LIMITS.financeTransitionEdges) {
139
140
  return `financial analysis exceeds ${UDL_LIMITS.financeTransitionEdges} lifecycle transition edges`;
140
141
  }
141
142
  }
142
- let verbCount = 0;
143
+ let actionCount = 0;
143
144
  let effectCount = 0;
144
- for (const name in noun.verbs) {
145
- if (!Object.hasOwn(noun.verbs, name))
145
+ for (const name in instrument.actions) {
146
+ if (!Object.hasOwn(instrument.actions, name))
146
147
  continue;
147
- verbCount += 1;
148
- if (verbCount > UDL_LIMITS.financeVerbs) {
149
- return `financial analysis exceeds ${UDL_LIMITS.financeVerbs} verbs`;
148
+ actionCount += 1;
149
+ if (actionCount > UDL_LIMITS.financeActions) {
150
+ return `financial analysis exceeds ${UDL_LIMITS.financeActions} actions`;
150
151
  }
151
- const verb = noun.verbs[name];
152
- effectCount += (verb?.steps.length ?? 0) + (verb?.moves?.length ?? 0);
152
+ const action = instrument.actions[name];
153
+ effectCount += (action?.steps.length ?? 0) + (action?.moves?.length ?? 0);
153
154
  if (effectCount > UDL_LIMITS.financeEffects) {
154
155
  return `financial analysis exceeds ${UDL_LIMITS.financeEffects} kernel effects`;
155
156
  }
156
157
  }
157
158
  return undefined;
158
159
  }
159
- function workspaceEscrowAccounts(noun) {
160
+ function productEscrowAccounts(instrument) {
160
161
  const accounts = new Set();
161
- for (const verb of Object.values(noun.verbs)) {
162
- for (const step of verb.steps) {
162
+ for (const action of Object.values(instrument.actions)) {
163
+ for (const step of action.steps) {
163
164
  if (step.operation !== "account.escrow.provision" ||
164
165
  step.bind.role?.from !== "const" ||
165
- step.bind.role.value !== "workspace_escrow") {
166
+ step.bind.role.value !== "product_escrow") {
166
167
  continue;
167
168
  }
168
169
  for (const [key, result] of Object.entries(step.capture ?? {})) {
@@ -171,27 +172,29 @@ function workspaceEscrowAccounts(noun) {
171
172
  }
172
173
  }
173
174
  }
174
- // `workspace_escrow` also backs open-ended balance products such as wallets.
175
+ // `product_escrow` also backs open-ended balance products such as wallets.
175
176
  // Only instance-bound amounts claim static conservation; caller-sized flows
176
- // remain runtime balance checks. Unwind sources are added separately above.
177
- return new Set([...accounts].filter((account) => Object.values(noun.verbs).some((verb) => (verb.moves ?? []).some((step) => {
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) => {
178
179
  if ((step.operation !== "internal_transfer.create" &&
179
180
  step.operation !== "internal_transfer.reserve") ||
180
181
  step.bind.amount?.from !== "instance") {
181
182
  return false;
182
183
  }
183
- return (canonicalAccount(noun, step.bind.sourceAccountId) === account ||
184
- canonicalAccount(noun, step.bind.destinationAccountId) === account);
184
+ return (canonicalAccount(instrument, step.bind.sourceAccountId) ===
185
+ account ||
186
+ canonicalAccount(instrument, step.bind.destinationAccountId) ===
187
+ account);
185
188
  }))));
186
189
  }
187
- function reservationsByKey(noun) {
190
+ function reservationsByKey(instrument) {
188
191
  const reservations = new Map();
189
- for (const verb of Object.values(noun.verbs)) {
190
- for (const step of verb.moves ?? []) {
192
+ for (const action of Object.values(instrument.actions)) {
193
+ for (const step of action.moves ?? []) {
191
194
  if (step.operation !== "internal_transfer.reserve")
192
195
  continue;
193
- const source = canonicalAccount(noun, step.bind.sourceAccountId);
194
- const destination = canonicalAccount(noun, step.bind.destinationAccountId);
196
+ const source = canonicalAccount(instrument, step.bind.sourceAccountId);
197
+ const destination = canonicalAccount(instrument, step.bind.destinationAccountId);
195
198
  const amount = amountIdentity(step.bind.amount);
196
199
  for (const [key, result] of Object.entries(step.capture ?? {})) {
197
200
  if (result === "transferId") {
@@ -206,43 +209,60 @@ function reservationsByKey(noun) {
206
209
  }
207
210
  return reservations;
208
211
  }
209
- function effectsByVerb(noun, account, reservations, penaltyCanBeNonzero) {
210
- return new Map(Object.entries(noun.verbs).map(([verbName, verb]) => [
211
- verbName,
212
- (verb.moves ?? []).flatMap((step, stepIndex) => {
213
- const path = ["verbs", verbName, "moves", stepIndex, "bind"];
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
+ ];
214
225
  if (step.operation === "internal_transfer.create") {
215
226
  const effects = [];
216
227
  const amount = amountIdentity(step.bind.amount);
217
- if (canonicalAccount(noun, step.bind.destinationAccountId) === account) {
228
+ if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
229
+ account) {
218
230
  effects.push({ amount, kind: "credit", path });
219
231
  }
220
- const source = canonicalAccount(noun, step.bind.sourceAccountId);
232
+ const source = canonicalAccount(instrument, step.bind.sourceAccountId);
221
233
  if (source && accountsMayAlias(source, account)) {
222
- const kind = verbName === noun.unwind?.confirm &&
223
- step.bind.amount?.from === "instance" &&
224
- step.bind.amount.path === "refs.unwindRefund"
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
225
245
  ? "refund"
226
- : step.bind.amount?.from === "instance" &&
227
- step.bind.amount.path === "refs.unwindPenalty"
246
+ : charged
228
247
  ? "penalty_debit"
229
- : step.bind.amount?.from === "instance"
248
+ : amountPath !== undefined
230
249
  ? "debit"
231
250
  : "dynamic_debit";
232
251
  effects.push({
233
- amount: kind === "refund" && !penaltyCanBeNonzero
252
+ amount: refunded && !refunded.chargeCanBeNonzero
234
253
  ? "refund_without_penalty"
235
254
  : amount,
236
255
  kind,
237
256
  path,
257
+ ...(refunded ? { refundable: refunded.baseField } : {}),
238
258
  });
239
259
  }
240
260
  return effects;
241
261
  }
242
262
  if (step.operation === "internal_transfer.reserve") {
243
- const reservation = transferRefKey(step) ?? `${verbName}:${stepIndex}`;
263
+ const reservation = transferRefKey(step) ?? `${actionName}:${stepIndex}`;
244
264
  const effects = [];
245
- const source = canonicalAccount(noun, step.bind.sourceAccountId);
265
+ const source = canonicalAccount(instrument, step.bind.sourceAccountId);
246
266
  if (source && accountsMayAlias(source, account)) {
247
267
  effects.push({
248
268
  amount: amountIdentity(step.bind.amount),
@@ -251,7 +271,8 @@ function effectsByVerb(noun, account, reservations, penaltyCanBeNonzero) {
251
271
  reservation,
252
272
  });
253
273
  }
254
- if (canonicalAccount(noun, step.bind.destinationAccountId) === account) {
274
+ if (canonicalAccount(instrument, step.bind.destinationAccountId) ===
275
+ account) {
255
276
  effects.push({
256
277
  amount: amountIdentity(step.bind.amount),
257
278
  kind: "incoming_reserve",
@@ -285,16 +306,16 @@ function effectsByVerb(noun, account, reservations, penaltyCanBeNonzero) {
285
306
  }),
286
307
  ]));
287
308
  }
288
- function applyEffects(noun, account, verb, input, effects, reservations, exactBalance, add) {
309
+ function applyEffects(account, action, input, effects, reservations, exactBalance, add) {
289
310
  let balance = input.balance;
290
311
  const holds = { ...input.holds };
291
312
  const accountLabel = formatAccount(account);
292
313
  for (const effect of effects) {
293
- const fail = (message) => add([...effect.path, "sourceAccountId"], `verb ${verb} ${message}`);
314
+ const fail = (message) => add([...effect.path, "sourceAccountId"], `action ${action} ${message}`);
294
315
  if (effect.kind === "credit") {
295
316
  if (exactBalance &&
296
317
  (balance.kind !== "empty" || Object.keys(holds).length > 0)) {
297
- add([...effect.path, "destinationAccountId"], `verb ${verb} funds ${accountLabel} while earlier value may remain; unwind funding must establish exactly one refundable balance`);
318
+ add([...effect.path, "destinationAccountId"], `action ${action} funds ${accountLabel} while earlier value may remain; quoted funding must establish exactly one refundable balance`);
298
319
  balance = UNKNOWN;
299
320
  continue;
300
321
  }
@@ -340,14 +361,14 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
340
361
  balance = held;
341
362
  }
342
363
  else {
343
- add([...effect.path, "transferId"], `verb ${verb} restores a held ${accountLabel} balance on top of existing value`);
364
+ add([...effect.path, "transferId"], `action ${action} restores a held ${accountLabel} balance on top of existing value`);
344
365
  balance = UNKNOWN;
345
366
  }
346
367
  }
347
368
  }
348
369
  if (effect.kind === "post" && reservation?.destination === account) {
349
370
  if (balance.kind !== "empty" || Object.keys(holds).length > 0) {
350
- add([...effect.path, "transferId"], `verb ${verb} posts funding into ${accountLabel} while earlier value may remain`);
371
+ add([...effect.path, "transferId"], `action ${action} posts funding into ${accountLabel} while earlier value may remain`);
351
372
  balance = UNKNOWN;
352
373
  }
353
374
  else {
@@ -357,7 +378,7 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
357
378
  continue;
358
379
  }
359
380
  if (effect.kind === "refund") {
360
- const refundable = `fields.${noun.unwind?.refundableField ?? ""}`;
381
+ const refundable = `fields.${effect.refundable ?? ""}`;
361
382
  if (balance.kind !== "funded" ||
362
383
  balance.amounts.length !== 1 ||
363
384
  balance.amounts[0] !== refundable) {
@@ -374,7 +395,7 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
374
395
  }
375
396
  if (effect.kind === "penalty_debit") {
376
397
  if (balance.kind !== "penalty") {
377
- fail(`cannot pay refs.unwindPenalty from ${accountLabel} before the refund leaves that exact remainder`);
398
+ fail(`cannot pay the quoted charge from ${accountLabel} before the refund leaves that exact remainder`);
378
399
  }
379
400
  balance = EMPTY;
380
401
  continue;
@@ -384,7 +405,7 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
384
405
  fail(`can debit unfunded ${accountLabel}`);
385
406
  }
386
407
  else if (balance.kind === "penalty") {
387
- fail(`can debit ${accountLabel} after its refund left only refs.unwindPenalty`);
408
+ fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
388
409
  }
389
410
  balance = balance.kind === "empty" ? EMPTY : { kind: "unknown" };
390
411
  continue;
@@ -397,7 +418,7 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
397
418
  fail(`can debit unfunded ${accountLabel}`);
398
419
  }
399
420
  else if (balance.kind === "penalty") {
400
- fail(`can debit ${accountLabel} after its refund left only refs.unwindPenalty`);
421
+ fail(`can debit ${accountLabel} after its refund left only the quoted charge`);
401
422
  }
402
423
  else if (balance.kind === "funded" || exactBalance) {
403
424
  fail(`cannot prove ${effect.amount} is the exact available balance of ${accountLabel}`);
@@ -406,46 +427,81 @@ function applyEffects(noun, account, verb, input, effects, reservations, exactBa
406
427
  }
407
428
  return { balance, holds };
408
429
  }
409
- function validatePenaltyPayout(noun, refundSource, required, add) {
410
- const uses = Object.entries(noun.verbs).flatMap(([verbName, verb]) => (verb.moves ?? []).flatMap((step, stepIndex) => Object.entries(step.bind).flatMap(([target, binding]) => binding.from === "instance" && binding.path === "refs.unwindPenalty"
411
- ? [{ binding, step, stepIndex, target, verb, verbName }]
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 }]
412
468
  : [])));
413
- if (required && uses.length !== 1) {
414
- add(["verbs"], `refs.unwindPenalty is consumed ${uses.length} times; a nonzero penalty schedule requires exactly one payout`);
469
+ if (quote.chargeCanBeNonzero && uses.length !== 1) {
470
+ add(["actions"], `${chargePath} is consumed ${uses.length} times; a nonzero charge schedule requires exactly one payout`);
415
471
  }
416
472
  if (uses.length === 0)
417
473
  return;
418
474
  if (uses.length !== 1) {
419
- if (!required) {
420
- add(["verbs"], `refs.unwindPenalty is consumed ${uses.length} times; it may fund at most one payout`);
475
+ if (!quote.chargeCanBeNonzero) {
476
+ add(["actions"], `${chargePath} is consumed ${uses.length} times; it may fund at most one payout`);
421
477
  }
422
478
  return;
423
479
  }
424
480
  const use = uses[0];
425
- const payoutTransition = noun.lifecycle.transitions[use.verbName];
426
- const confirmTarget = noun.lifecycle.transitions[noun.unwind.confirm]?.to;
427
- const source = canonicalAccount(noun, use.step.bind.sourceAccountId);
428
- const destination = canonicalAccount(noun, use.step.bind.destinationAccountId);
429
- const directAfterConfirm = confirmTarget !== undefined &&
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 &&
430
486
  payoutTransition?.from.length === 1 &&
431
- payoutTransition.from[0] === confirmTarget;
487
+ payoutTransition.from[0] === commitTarget;
432
488
  const oneWay = payoutTransition !== undefined &&
433
- payoutTransition.to !== confirmTarget &&
434
- !stateCanReach(noun, payoutTransition.to, confirmTarget);
489
+ payoutTransition.to !== commitTarget &&
490
+ !stateCanReach(instrument, payoutTransition.to, commitTarget);
435
491
  const valid = use.target === "amount" &&
436
492
  use.step.operation === "internal_transfer.create" &&
437
- (use.verb.moves?.length ?? 0) === 1 &&
438
- directAfterConfirm &&
493
+ (use.action.moves?.length ?? 0) === 1 &&
494
+ directAfterCommit &&
439
495
  oneWay &&
440
496
  source !== undefined &&
441
497
  source === refundSource &&
442
498
  destination !== undefined &&
443
499
  !accountsMayAlias(source, destination);
444
500
  if (!valid) {
445
- add(["verbs", use.verbName, "moves", use.stepIndex, "bind", use.target], `refs.unwindPenalty must be the amount of one internal transfer from the unwind refund source to a different account, in its own one-way verb directly after ${noun.unwind.confirm}`);
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}`);
446
502
  }
447
503
  }
448
- function canonicalAccount(noun, binding) {
504
+ function canonicalAccount(instrument, binding) {
449
505
  if (binding?.from !== "instance")
450
506
  return undefined;
451
507
  const [root, key, extra] = binding.path.split(".");
@@ -456,7 +512,7 @@ function canonicalAccount(noun, binding) {
456
512
  if (root === "refs")
457
513
  return `ref:${key}`;
458
514
  if (root === "party") {
459
- const field = noun.parties?.[key];
515
+ const field = instrument.parties?.[key];
460
516
  return field ? `field:${field}` : undefined;
461
517
  }
462
518
  return undefined;
@@ -480,7 +536,7 @@ function boundTransferRefKey(step) {
480
536
  ? binding.path.slice("refs.".length)
481
537
  : undefined;
482
538
  }
483
- function stateCanReach(noun, start, target) {
539
+ function stateCanReach(instrument, start, target) {
484
540
  if (!target)
485
541
  return false;
486
542
  const seen = new Set([start]);
@@ -489,7 +545,7 @@ function stateCanReach(noun, start, target) {
489
545
  const state = pending.shift();
490
546
  if (state === target)
491
547
  return true;
492
- for (const transition of Object.values(noun.lifecycle.transitions)) {
548
+ for (const transition of Object.values(instrument.lifecycle.transitions)) {
493
549
  if (!transition.from.includes(state) || seen.has(transition.to))
494
550
  continue;
495
551
  seen.add(transition.to);