@hyperscale0/udl 2.6.1 → 3.0.1

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 +4 -328
  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 +148 -14
  33. package/package.json +7 -11
  34. package/spec/README.md +152 -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
@@ -1,1052 +0,0 @@
1
- import { referencePatternPrefix } from "./reference.js";
2
- import { fixedIsoDurationMs } from "./duration.js";
3
- const dates = new Set([
4
- "date",
5
- "date-time",
6
- "hyperscale-date",
7
- "hyperscale-date-time",
8
- ]);
9
- const moneyPatterns = new Set(["^[1-9][0-9]{0,17}$", "^(0|[1-9][0-9]{0,17})$"]);
10
- const money = (field) => field?.type === "string" && moneyPatterns.has(String(field.pattern));
11
- const date = (field) => field?.type === "string" && dates.has(String(field.format));
12
- const account = (field) => field !== undefined && referencePatternPrefix(field) === "acct";
13
- const object = (value) => value !== null && typeof value === "object" && !Array.isArray(value)
14
- ? value
15
- : {};
16
- const options = (field) => field?.const !== undefined
17
- ? [field.const]
18
- : Array.isArray(field?.enum)
19
- ? field.enum
20
- : [];
21
- const mutable = (instrument, field) => instrument.update?.fields.includes(field) ||
22
- Object.values(instrument.actions).some((action) => action.updates?.includes(field));
23
- function instrumentInstanceKey(instrumentId) {
24
- const camel = instrumentId.replaceAll(/_([a-z])/g, (_, letter) => letter.toUpperCase());
25
- return `${camel}Id`;
26
- }
27
- /** Validate product laws independently of any instrument catalogue. */
28
- /** The children buckets of an allocation owner that select this instrument or its template alias. */
29
- function assessmentBuckets(owner, instrument) {
30
- return (owner?.allocation?.buckets.filter((bucket) => {
31
- const source = bucket.source;
32
- return (source.from === "children" &&
33
- ("instrumentId" in source
34
- ? source.instrumentId === instrument.id
35
- : instrument.templateBinding?.id === source.template &&
36
- Object.entries(source.parameters).every(([key, value]) => instrument.templateBinding?.parameters[key] === value)));
37
- }) ?? []);
38
- }
39
- export function validateVocabulary(instruments, add) {
40
- const byId = new Map(instruments.map((instrument) => [instrument.id, instrument]));
41
- const byPrefix = new Map(instruments.map((instrument) => [instrument.idPrefix, instrument]));
42
- const targets = (field) => {
43
- const prefix = field ? referencePatternPrefix(field) : undefined;
44
- const target = prefix ? byPrefix.get(prefix) : undefined;
45
- return target ? [target] : [];
46
- };
47
- const reference = (owner, field, path) => {
48
- const matches = targets(owner.fields[field]);
49
- if (matches.length !== 1)
50
- add(path, `${owner.id}.${field} must reference exactly one instrument`);
51
- return matches.length === 1 ? matches[0] : undefined;
52
- };
53
- const required = (owner, field, predicate, kind, path) => {
54
- if (!predicate(owner.fields[field]) ||
55
- !(owner.required.includes(field) ||
56
- owner.derivedAmounts?.some((amount) => amount.field === field) ||
57
- owner.feeRules?.some((amount) => amount.amountField === field) ||
58
- owner.actions.create?.requiresRefs?.some((gate) => Object.hasOwn(gate.bind ?? {}, field))) ||
59
- mutable(owner, field)) {
60
- add(path, `${owner.id}.${field} must be a required immutable ${kind} field`);
61
- }
62
- };
63
- const currency = (owner, fields, path) => {
64
- const tags = fields.map((field) => owner.fields[field]?.["x-hyperscale-currency"]);
65
- if (new Set(tags).size > 1)
66
- add(path, "money operands must use the same currency");
67
- };
68
- const duration = (owner, offset, path) => {
69
- if (offset === undefined)
70
- return;
71
- if (typeof offset === "string") {
72
- if (fixedIsoDurationMs(offset) === null)
73
- add(path, "offset must be a fixed ISO duration");
74
- return;
75
- }
76
- required(owner, offset.field, (field) => field?.type === "string", "duration", path);
77
- const values = options(owner.fields[offset.field]);
78
- if (values.length === 0 ||
79
- values.some((value) => typeof value !== "string" || (fixedIsoDurationMs(value) ?? 0) <= 0))
80
- add(path, "duration field must enumerate positive fixed ISO durations");
81
- };
82
- const namespaces = new Map();
83
- const edges = new Map();
84
- instruments.forEach((instrument, index) => {
85
- const base = ["instruments", index];
86
- const partitionEdges = new Map();
87
- const computed = new Set([
88
- ...(instrument.derivedAmounts ?? []).map((value) => value.field),
89
- ...(instrument.feeRules ?? []).map((value) => value.amountField),
90
- ...Object.keys(instrument.actions.create?.requiresRefs?.reduce((result, gate) => ({ ...result, ...gate.bind }), {}) ?? {}),
91
- ]);
92
- for (const [i, partition] of (instrument.partitions ?? []).entries()) {
93
- const path = [...base, "partitions", i];
94
- const names = [partition.totalField, ...partition.pieceFields];
95
- if (new Set(names).size !== names.length)
96
- add(path, "partition total and piece fields must be distinct");
97
- for (const field of names) {
98
- if (!money(instrument.fields[field]) ||
99
- mutable(instrument, field) ||
100
- (!instrument.required.includes(field) && !computed.has(field)))
101
- add(path, "partition operands must be immutable money available at creation");
102
- }
103
- currency(instrument, names, path);
104
- partitionEdges.set(partition.totalField, [
105
- ...(partitionEdges.get(partition.totalField) ?? []),
106
- ...partition.pieceFields,
107
- ]);
108
- }
109
- const partitionVisited = new Set();
110
- const partitionActive = new Set();
111
- const visitPartition = (field) => {
112
- if (partitionActive.has(field)) {
113
- add([...base, "partitions"], "partition identities cannot contain cycles");
114
- return;
115
- }
116
- if (partitionVisited.has(field))
117
- return;
118
- partitionActive.add(field);
119
- for (const child of partitionEdges.get(field) ?? [])
120
- visitPartition(child);
121
- partitionActive.delete(field);
122
- partitionVisited.add(field);
123
- };
124
- for (const field of partitionEdges.keys())
125
- visitPartition(field);
126
- // Frozen prices and computed shares must fit each declared partition before admission.
127
- const constants = new Map();
128
- for (const [field, schema] of Object.entries(instrument.fields))
129
- if (money(schema) &&
130
- typeof schema.const === "string" &&
131
- /^\d+$/.test(schema.const))
132
- constants.set(field, BigInt(schema.const));
133
- for (const amount of instrument.derivedAmounts ?? []) {
134
- const source = constants.get(amount.sourceField);
135
- if (source === undefined)
136
- continue;
137
- if (amount.rule.kind === "minimum") {
138
- const cap = constants.get(amount.rule.capField);
139
- if (cap !== undefined)
140
- constants.set(amount.field, source < cap ? source : cap);
141
- }
142
- else {
143
- const bps = typeof amount.rule.bps === "number"
144
- ? amount.rule.bps
145
- : instrument.fields[amount.rule.bps.field]?.const;
146
- if (typeof bps === "number" && Number.isSafeInteger(bps))
147
- constants.set(amount.field, (source * BigInt(bps)) / 10000n);
148
- }
149
- }
150
- for (const [i, partition] of (instrument.partitions ?? []).entries()) {
151
- const total = constants.get(partition.totalField);
152
- if (total === undefined)
153
- continue;
154
- const known = partition.pieceFields.flatMap((field) => {
155
- const value = constants.get(field);
156
- return value === undefined ? [] : [value];
157
- });
158
- const minimum = partition.pieceFields.reduce((sum, field) => sum +
159
- (constants.get(field) ??
160
- (instrument.fields[field]?.pattern === "^[1-9][0-9]{0,17}$"
161
- ? 1n
162
- : 0n)), 0n);
163
- if (minimum > total ||
164
- (known.length === partition.pieceFields.length && minimum !== total))
165
- add([...base, "partitions", i], "constant partition pieces exceed or differ from their total");
166
- }
167
- for (const [i, order] of (instrument.dateOrder ?? []).entries()) {
168
- const path = [...base, "dateOrder", i];
169
- required(instrument, order.beforeField, date, "date", path);
170
- required(instrument, order.afterField, date, "date", path);
171
- if (order.beforeField === order.afterField)
172
- add(path, "date order needs two distinct fields");
173
- }
174
- for (const [i, amount] of (instrument.derivedAmounts ?? []).entries()) {
175
- const path = [...base, "derivedAmounts", i];
176
- required(instrument, amount.sourceField, money, "money", path);
177
- if (mutable(instrument, amount.field))
178
- add(path, "derived amount must be immutable");
179
- const sourceIndex = (instrument.derivedAmounts ?? []).findIndex((other) => other.field === amount.sourceField);
180
- if (sourceIndex >= i)
181
- add(path, "derived source must precede its consumer");
182
- if (amount.rule.kind === "minimum") {
183
- required(instrument, amount.rule.capField, money, "money", path);
184
- if (amount.rule.capField === amount.field)
185
- add(path, "minimum cannot read its own output");
186
- const capIndex = (instrument.derivedAmounts ?? []).findIndex((other) => amount.rule.kind === "minimum" &&
187
- other.field === amount.rule.capField);
188
- if (capIndex >= i)
189
- add(path, "derived cap must precede its consumer");
190
- currency(instrument, [amount.sourceField, amount.field, amount.rule.capField], path);
191
- }
192
- else if (typeof amount.rule.bps !== "number") {
193
- required(instrument, amount.rule.bps.field, (field) => {
194
- if (field?.type !== "integer")
195
- return false;
196
- const values = options(field);
197
- if (values.length)
198
- return values.every((value) => typeof value === "number" &&
199
- Number.isInteger(value) &&
200
- value >= 1 &&
201
- value <= 9999);
202
- return (typeof field.minimum === "number" &&
203
- field.minimum >= 1 &&
204
- typeof field.maximum === "number" &&
205
- field.maximum <= 9999);
206
- }, "integer rate between 1 and 9999", path);
207
- }
208
- currency(instrument, [amount.sourceField, amount.field], path);
209
- }
210
- for (const [name, action] of Object.entries(instrument.actions)) {
211
- const path = [...base, "actions", name];
212
- if (action.requestAuthority) {
213
- const clause = action.requestAuthority;
214
- const ap = [...path, "requestAuthority"];
215
- if (name !== "create" || action.moves.length || action.steps.length)
216
- add(ap, "request authority is create-only and moves no money");
217
- for (const field of [
218
- clause.instrumentField,
219
- clause.instanceField,
220
- clause.actionField,
221
- clause.roleField,
222
- ])
223
- required(instrument, field, (schema) => schema?.type === "string", "text", ap);
224
- required(instrument, clause.expiresField, date, "date", ap);
225
- const party = instrument.parties?.[clause.party];
226
- if (!party)
227
- add(ap, "request authority must bind a declared party");
228
- else
229
- required(instrument, party, account, "account", ap);
230
- if (instrument.fields[clause.digestField]?.pattern !== "^[a-f0-9]{64}$" ||
231
- mutable(instrument, clause.digestField))
232
- add(ap, "request authority digest must be an immutable lowercase SHA-256 field");
233
- if (instrument.fields[clause.inputField]?.type !== "object" ||
234
- mutable(instrument, clause.inputField))
235
- add(ap, "request authority material input must be an immutable object field");
236
- }
237
- for (const gate of action.requiresRefs ?? []) {
238
- if (!gate.dueBefore)
239
- continue;
240
- const target = reference(instrument, gate.field, path);
241
- if (target)
242
- required(target, gate.dueBefore.field, date, "date", path);
243
- if (fixedIsoDurationMs(gate.dueBefore.offset) === null)
244
- add(path, "reference age must use a fixed ISO duration");
245
- }
246
- for (const exposure of action.requiresExposure ?? []) {
247
- if (!exposure.groupField && !exposure.minimumField)
248
- continue;
249
- const anchor = reference(instrument, exposure.anchorField, path);
250
- const children = byId.get(exposure.childInstrumentId);
251
- if (exposure.groupField) {
252
- required(instrument, exposure.groupField, account, "account", path);
253
- if (children)
254
- required(children, exposure.groupField, account, "account", path);
255
- }
256
- if (exposure.minimumField && anchor)
257
- required(anchor, exposure.minimumField, money, "money", path);
258
- if (!exposure.capOnAnchor || exposure.measure)
259
- add(path, "grouped admission uses an anchor cap over stored money");
260
- }
261
- if (action.funding) {
262
- const funding = action.funding;
263
- const obligation = reference(instrument, funding.obligationField, path);
264
- const tickets = byId.get(funding.ticketInstrumentId);
265
- required(instrument, funding.principalField, money, "money", path);
266
- required(instrument, funding.destinationAccountField, account, "account", path);
267
- if (!tickets)
268
- add(path, "funding must select a declared ticket instrument");
269
- else {
270
- if (reference(tickets, funding.ticketRefField, path)?.id !==
271
- instrument.id)
272
- add(path, "funding tickets must reference their snapshot owner");
273
- required(tickets, funding.ticketAmountField, money, "money", path);
274
- required(tickets, funding.ticketInvestorField, account, "account", path);
275
- required(tickets, funding.ticketAccountField, account, "account", path);
276
- const collect = tickets.actions[funding.collectAction];
277
- if (!collect?.engineOwned ||
278
- collect.publicAction ||
279
- collect.due ||
280
- collect.moves.length ||
281
- collect.steps.length)
282
- add(path, "funding must own the ticket collection action");
283
- if (!tickets.lifecycle.transitions[funding.collectAction]?.from.includes(funding.ticketStatus))
284
- add(path, "funding collection must consume the selected ticket state");
285
- }
286
- if (!obligation?.allocation)
287
- add(path, "funding requires an allocation-backed obligation");
288
- const sourceCapture = funding.sourceAccountPath.replace(/^refs\./, "");
289
- if (!funding.sourceAccountPath.startsWith("refs.") ||
290
- !instrument.actions.create?.steps.some((step) => step.capture?.[sourceCapture] === "accountId"))
291
- add(path, "funding source must be an account captured when the round opens");
292
- if (!funding.terms[funding.principalField])
293
- add(path, "funding must bind its principal forward to the obligation");
294
- if (obligation)
295
- for (const [local, remote] of Object.entries(funding.terms)) {
296
- if (!instrument.fields[local] ||
297
- !obligation.fields[remote] ||
298
- mutable(instrument, local) ||
299
- mutable(obligation, remote))
300
- add(path, "funding terms must bind immutable declared fields");
301
- }
302
- if (name === "create" || action.moves.length || action.steps.length)
303
- add(path, "funding owns its postings on an existing round");
304
- }
305
- if (action.receiptDistribution) {
306
- const distribution = action.receiptDistribution;
307
- if (distribution.feeBps * (10000 + distribution.vatBps) > 10000 * 10000)
308
- add([...path, "receiptDistribution"], "fee and VAT must not exceed distributable profit");
309
- const round = reference(instrument, distribution.roundField, path);
310
- const receipt = reference(instrument, distribution.receiptField, path);
311
- if (!round ||
312
- !Object.values(round.actions).some((candidate) => candidate.funding?.capture === distribution.snapshotRef))
313
- add(path, "distribution requires a funding snapshot on its round");
314
- const capture = distribution.receiptPath.replace(/^refs\./, "");
315
- if (!distribution.receiptPath.startsWith("refs.") ||
316
- !receipt ||
317
- !Object.values(receipt.actions).some((candidate) => candidate.allocate?.capture === capture))
318
- add(path, "distribution must consume an allocation receipt capture");
319
- if (receipt &&
320
- !Object.values(receipt.actions).some((candidate) => candidate.allocate?.capture === capture &&
321
- (distribution.mode === "loss"
322
- ? candidate.allocate.mode === "write_off"
323
- : ["payment", "payoff"].includes(candidate.allocate.mode))))
324
- add(path, "distribution mode must match its allocation receipt");
325
- for (const field of [
326
- distribution.feeAccountField,
327
- distribution.taxAccountField,
328
- distribution.residualAccountField,
329
- ])
330
- required(instrument, field, account, "account", path);
331
- if (distribution.mode === "loss" &&
332
- (distribution.feeBps || distribution.vatBps))
333
- add(path, "loss distribution cannot charge fee or VAT");
334
- if (action.moves.length || action.steps.length)
335
- add(path, "receipt distribution owns its postings");
336
- }
337
- for (const [key, valuePath] of Object.entries(action.requiresInput ?? {})) {
338
- const inputField = object(object(action.input?.properties)[key]);
339
- const field = valuePath.startsWith("fields.")
340
- ? instrument.fields[valuePath.slice(7)]
341
- : undefined;
342
- if (!field ||
343
- !Array.isArray(action.input?.required) ||
344
- !action.input.required.includes(key) ||
345
- !["type", "format", "pattern", "x-hyperscale-currency"].every((tag) => inputField[tag] === field[tag]))
346
- add([...path, "requiresInput", key], "requiresInput must bind a required input to a stored field of the same type");
347
- if (action.updates?.includes(valuePath.slice(7)))
348
- add([...path, "requiresInput", key], "requiresInput cannot update the field it compares");
349
- }
350
- const portKeys = Object.values(instrument.actions).flatMap((a) => a.port?.capture ? [a.port.capture] : []);
351
- if (action.port?.capture &&
352
- !account(instrument.fields[action.port.capture]))
353
- add([...path, "port", "capture"], "port capture must name a declared account field");
354
- for (const key of portKeys) {
355
- if (Object.hasOwn(action.captureInput ?? {}, key) ||
356
- action.updates?.includes(key) ||
357
- instrument.update?.fields.includes(key) ||
358
- Object.hasOwn(object(action.input?.properties), key) ||
359
- [...action.steps, ...action.moves].some((step) => Object.hasOwn(step.capture ?? {}, key)))
360
- add([...path, "port", "capture"], "port capture is engine-written and cannot be supplied or updated by a caller");
361
- if (instrument.required.includes(key))
362
- add([...path, "port", "capture"], "port capture cannot be required at creation");
363
- }
364
- const engineKeys = new Set(Object.values(instrument.actions).flatMap((a) => Object.keys(a.captureEngine ?? {})));
365
- for (const key of engineKeys)
366
- if (instrument.fields[key])
367
- add([...path, "captureEngine", key], "engine-owned capture cannot be a caller-created stored field");
368
- if (action.engineOwned &&
369
- (name === "create" ||
370
- action.publicAction ||
371
- action.port ||
372
- action.decision ||
373
- action.input ||
374
- action.captureInput ||
375
- action.updates))
376
- add(path, "engine-owned actions cannot be created, exposed, or accept caller input");
377
- if (action.captureEngine && !action.engineOwned)
378
- add(path, "captureEngine requires an engine-owned action");
379
- for (const key of [
380
- ...Object.keys(action.captureInput ?? {}),
381
- ...Object.keys(object(action.input?.properties)),
382
- ...(action.updates ?? []),
383
- ...[...action.steps, ...action.moves].flatMap((step) => Object.keys(step.capture ?? {})),
384
- ])
385
- if (engineKeys.has(key))
386
- add([...path, "captureInput", key], "engine-owned capture cannot be supplied by a caller");
387
- if ((action.requiresRefs ?? []).filter((gate) => gate.attests).length > 1)
388
- add([...path, "attests"], "an action can declare only one attests gate");
389
- for (const [i, gate] of (action.requiresRefs ?? []).entries()) {
390
- if (!gate.attests)
391
- continue;
392
- const ap = [...path, "requiresRefs", i, "attests"];
393
- const target = reference(instrument, gate.field, ap);
394
- if (gate.attests.forAction &&
395
- !instrument.actions[gate.attests.forAction])
396
- add([...ap, "forAction"], "attests forAction must name an action on this instrument");
397
- if (name === "create" ||
398
- gate.optional ||
399
- !gate.match?.instrumentInstanceId)
400
- add(ap, "attests requires a mandatory instance-bound gate on an existing instance");
401
- if (!target)
402
- continue;
403
- for (const key of [
404
- "action",
405
- "digest",
406
- "role",
407
- "expiresAt",
408
- "instrument",
409
- ]) {
410
- const valuePath = gate.attests[key];
411
- const field = valuePath.startsWith("fields.")
412
- ? target.fields[valuePath.slice(7)]
413
- : undefined;
414
- const valid = key === "expiresAt"
415
- ? date(field)
416
- : field?.type === "string" &&
417
- (key !== "digest" || field.pattern === "^[a-f0-9]{64}$");
418
- if (!valid)
419
- add([...ap, key], `attests ${key} must name a declared ${key === "expiresAt" ? "date" : key === "digest" ? "lowercase SHA-256" : "text"} field`);
420
- }
421
- if (!Object.hasOwn(target.parties ?? {}, gate.attests.party))
422
- add([...ap, "party"], "attests party must name a declared party on the referenced instrument");
423
- const consume = target.actions[gate.attests.consume];
424
- const edge = target.lifecycle.transitions[gate.attests.consume];
425
- if (!consume?.engineOwned)
426
- add([...ap, "consume"], "attests consume must be engine-owned");
427
- if (!edge ||
428
- gate.statuses.some((status) => !edge.from.includes(status)))
429
- add([...ap, "consume"], "attests consume must be reachable from every admitted status");
430
- }
431
- // Literal offsets retain their existing action diagnostics.
432
- if (typeof action.due?.offset === "object")
433
- duration(instrument, action.due.offset, [...path, "due", "offset"]);
434
- if (typeof action.deadline?.offset === "object")
435
- duration(instrument, action.deadline.offset, [
436
- ...path,
437
- "deadline",
438
- "offset",
439
- ]);
440
- if (action.unique) {
441
- const up = [...path, "unique"];
442
- if (name !== "create")
443
- add(up, "subject unique is create-only");
444
- for (const field of action.unique.byFields) {
445
- required(instrument, field, (value) => value?.type === "string" || value?.type === "integer", "uniqueness key", up);
446
- if (Object.values(instrument.actions).some((a) => a.port?.capture === field ||
447
- Object.hasOwn(a.captureInput ?? {}, field)))
448
- add(up, "uniqueness key cannot be captured after creation");
449
- }
450
- if (new Set(action.unique.byFields).size !== action.unique.byFields.length)
451
- add(up, "uniqueness fields must be distinct");
452
- const signature = JSON.stringify([
453
- "subject",
454
- action.unique.byFields.map((field) => [
455
- field,
456
- instrument.fields[field]?.type,
457
- account(instrument.fields[field]),
458
- ]),
459
- ]);
460
- const prior = namespaces.get(action.unique.namespace);
461
- if (prior && prior !== signature)
462
- add(up, "uniqueness namespace must use one key definition and cannot mix subject and reference claims");
463
- namespaces.set(action.unique.namespace, signature);
464
- }
465
- for (const [i, gate] of (action.requiresRefs ?? []).entries()) {
466
- if (typeof gate.unique !== "object")
467
- continue;
468
- const target = reference(instrument, gate.field, [
469
- ...path,
470
- "requiresRefs",
471
- i,
472
- ]);
473
- for (const field of gate.unique.byFields)
474
- required(instrument, field, (value) => value?.type === "string" || value?.type === "integer", "uniqueness key", path);
475
- if (new Set(gate.unique.byFields).size !== gate.unique.byFields.length)
476
- add(path, "uniqueness fields must be distinct");
477
- const signature = JSON.stringify([
478
- target?.id,
479
- gate.unique.byFields.map((field) => [
480
- field,
481
- instrument.fields[field]?.type,
482
- ]),
483
- ]);
484
- const prior = namespaces.get(gate.unique.namespace);
485
- if (prior && prior !== signature)
486
- add(path, "uniqueness namespace must use one reference target and key definition across aliases");
487
- namespaces.set(gate.unique.namespace, signature);
488
- }
489
- for (const [i, exposure] of (action.requiresExposure ?? []).entries()) {
490
- if (!exposure.measure)
491
- continue;
492
- const ep = [...path, "requiresExposure", i, "measure"];
493
- const child = byId.get(exposure.childInstrumentId);
494
- const allocation = child?.allocation;
495
- const bucket = allocation?.buckets.find((b) => b.key === exposure.measure.allocation);
496
- if (!child || !allocation || !bucket) {
497
- add(ep, "exposure measure must name a bucket declared by the child allocation");
498
- continue;
499
- }
500
- // The parent total is the gross operand, tied to the slice source by an exact schedule.
501
- const source = bucket.source;
502
- const scheduled = source.from === "slice" &&
503
- Object.values(child.actions).some((a) => a.requiresAggregate?.some((r) => r.instrumentId === allocation.sliceInstrumentId &&
504
- r.refField === allocation.sliceRefField &&
505
- r.over === "children" &&
506
- !r.anchorField &&
507
- !r.dueBefore &&
508
- r.check.kind === "schedule" &&
509
- r.check.amounts.some((amount) => amount.amountField === source.amountField &&
510
- amount.totalField === exposure.amountField)));
511
- if (!scheduled)
512
- add(ep, "exposure amountField must be the parent total scheduled against the allocation slice bucket");
513
- required(child, exposure.amountField, money, "money", ep);
514
- }
515
- for (const [i, relation] of (action.requiresAggregate ?? []).entries()) {
516
- const rp = [...path, "requiresAggregate", i];
517
- // Older self-sibling clauses omit instrumentId. Their measured rows are the owner.
518
- const rows = relation.instrumentId
519
- ? byId.get(relation.instrumentId)
520
- : instrument;
521
- if (!rows) {
522
- add(rp, "aggregate instrument does not exist");
523
- continue;
524
- }
525
- const anchor = relation.anchorField
526
- ? reference(instrument, relation.anchorField, rp)
527
- : relation.over === "siblings"
528
- ? reference(instrument, relation.refField, rp)
529
- : instrument;
530
- if (relation.anchorField)
531
- required(instrument, relation.anchorField, (value) => targets(value).length === 1, "reference", rp);
532
- const rowTarget = reference(rows, relation.refField, rp);
533
- if (anchor && rowTarget && anchor.id !== rowTarget.id)
534
- add(rp, "aggregate rows must reference the same anchor as the owner");
535
- if (mutable(rows, relation.refField))
536
- add(rp, "aggregate reference must be immutable");
537
- for (const status of relation.statuses)
538
- if (!rows.lifecycle.states.includes(status))
539
- add(rp, `aggregate status ${status} does not exist`);
540
- if (relation.dueBefore) {
541
- required(rows, relation.dueBefore.field, date, "date", rp);
542
- duration(rows, relation.dueBefore.offset, rp);
543
- }
544
- const check = relation.check;
545
- if (check.kind === "count_at_least")
546
- required(instrument, check.targetField, (field) => field?.type === "integer", "integer count", rp);
547
- if (check.kind === "ordered" || check.kind === "schedule") {
548
- required(rows, check.positionField, (field) => field?.type === "integer", "integer position", rp);
549
- required(rows, check.kind === "ordered" ? check.field : check.dateField, date, "date", rp);
550
- }
551
- if (check.kind === "schedule") {
552
- if (relation.over !== "children" ||
553
- relation.anchorField ||
554
- relation.dueBefore)
555
- add(rp, "exact schedule must inspect all children of the owner without a clock filter");
556
- required(instrument, check.datesField, (field) => field?.type === "array" &&
557
- date(object(field.items)) &&
558
- typeof field.minItems === "number" &&
559
- field.minItems >= 1 &&
560
- typeof field.maxItems === "number" &&
561
- field.maxItems <= 366, "bounded date list", rp);
562
- const keys = check.amounts.map((amount) => amount.amountField);
563
- if (new Set(keys).size !== keys.length)
564
- add(rp, "schedule amount fields must be distinct");
565
- for (const amount of check.amounts) {
566
- required(rows, amount.amountField, money, "money", rp);
567
- required(instrument, amount.totalField, money, "money", rp);
568
- if (rows.fields[amount.amountField]?.["x-hyperscale-currency"] !==
569
- instrument.fields[amount.totalField]?.["x-hyperscale-currency"])
570
- add(rp, "schedule amounts must share a currency");
571
- }
572
- }
573
- }
574
- const outgoing = [];
575
- const transitioned = new Set();
576
- for (const [i, transition] of [
577
- ...(action.transitionsRefs ?? []),
578
- ...(action.requiresRefs ?? []).flatMap((gate) => gate.attests
579
- ? [{ field: gate.field, action: gate.attests.consume }]
580
- : []),
581
- ].entries()) {
582
- const tp = [...path, "transitionsRefs", i];
583
- if (i < (action.transitionsRefs?.length ?? 0))
584
- required(instrument, transition.field, (value) => targets(value).length === 1, "reference", tp);
585
- const target = reference(instrument, transition.field, tp);
586
- const next = target?.actions[transition.action];
587
- const edge = target?.lifecycle.transitions[transition.action];
588
- if (!target || !next || !edge || transition.action === "create") {
589
- add(tp, "referenced transition must name an existing lifecycle action");
590
- continue;
591
- }
592
- if (next.engineOwned && i < (action.transitionsRefs?.length ?? 0))
593
- add(tp, "engine-owned consumption requires an attests gate");
594
- if (next.moves.length ||
595
- next.steps.length ||
596
- next.payout ||
597
- next.calls?.length ||
598
- next.allocate ||
599
- next.contributionStage ||
600
- next.pieceStage ||
601
- next.captureInput ||
602
- next.updates?.length ||
603
- (Array.isArray(next.input?.required) &&
604
- next.input.required.length > 0))
605
- add(tp, "referenced transition must be noncash and cannot call kernel actions");
606
- const transitionKey = `${transition.field}.${transition.action}`;
607
- if (transitioned.has(transitionKey))
608
- add(tp, "referenced transition cannot repeat the same reference and action");
609
- transitioned.add(transitionKey);
610
- outgoing.push(`${target.id}.${transition.action}`);
611
- }
612
- edges.set(`${instrument.id}.${name}`, outgoing);
613
- if (action.cascade) {
614
- const ap = [...path, "cascade"];
615
- if (name === "create" || action.decision) {
616
- add(ap, "cascade is not allowed on create or actions with a decision");
617
- }
618
- const seenInputFields = new Set();
619
- for (const [i, entry] of action.cascade.entries()) {
620
- const ep = [...ap, i];
621
- const hasInput = "inputField" in entry && entry.inputField !== undefined;
622
- const hasRef = "refField" in entry && entry.refField !== undefined;
623
- if ((!hasInput && !hasRef) || (hasInput && hasRef)) {
624
- add(ep, "cascade must specify exactly one of inputField or refField");
625
- }
626
- const target = byId.get(entry.instrumentId);
627
- if (!target) {
628
- add(ep, "cascade target instrument must be declared in the document");
629
- }
630
- else {
631
- const targetAction = target.actions[entry.action];
632
- const targetTransition = target.lifecycle.transitions[entry.action];
633
- if (!targetAction ||
634
- !targetTransition ||
635
- entry.action === "create") {
636
- add(ep, "cascade target action must exist in lifecycle and cannot be create");
637
- }
638
- else {
639
- if (targetAction.engineOwned) {
640
- add(ep, "cascade target action cannot be engine-owned");
641
- }
642
- if (targetAction.cascade) {
643
- add(ep, "cascade target action cannot have a cascade of its own");
644
- }
645
- if (targetAction.decision) {
646
- add(ep, "cascade target action cannot have a decision");
647
- }
648
- if (targetAction.port && !action.port) {
649
- add(ep, `cascade target action ${entry.action} declares a port; the parent action ${name} must declare a port so the actor is forwarded`);
650
- }
651
- if (Array.isArray(targetAction.input?.required) &&
652
- targetAction.input.required.length > 0) {
653
- add(ep, "cascade target action cannot have required inputs");
654
- }
655
- }
656
- if ("refField" in entry && entry.refField !== undefined) {
657
- if (!entry.statuses || entry.statuses.length === 0) {
658
- add(ep, "cascade with refField requires statuses");
659
- }
660
- const targetRef = reference(target, entry.refField, ep);
661
- if (targetRef && targetRef.id !== instrument.id) {
662
- add(ep, "cascade target refField must reference this instrument");
663
- }
664
- if (targetTransition) {
665
- for (const status of entry.statuses ?? []) {
666
- if (!targetTransition.from.includes(status)) {
667
- add(ep, `cascade status ${status} is not an allowed from-state for ${target.id}.${entry.action}`);
668
- }
669
- }
670
- }
671
- }
672
- }
673
- if ("inputField" in entry && entry.inputField !== undefined) {
674
- const reservedFields = new Set([
675
- "tenantId",
676
- "productId",
677
- "actorAccountId",
678
- instrumentInstanceKey(instrument.id),
679
- ]);
680
- if (reservedFields.has(entry.inputField)) {
681
- add(ep, `cascade inputField ${entry.inputField} collides with a reserved field`);
682
- }
683
- if (Object.hasOwn(instrument.fields, entry.inputField)) {
684
- add(ep, `cascade inputField ${entry.inputField} collides with an instrument field`);
685
- }
686
- if (Object.hasOwn(object(action.input?.properties), entry.inputField)) {
687
- add(ep, `cascade inputField ${entry.inputField} collides with an action input property`);
688
- }
689
- if (seenInputFields.has(entry.inputField)) {
690
- add(ep, `cascade inputField ${entry.inputField} is repeated`);
691
- }
692
- seenInputFields.add(entry.inputField);
693
- }
694
- }
695
- }
696
- if (action.allocate || action.contributionStage) {
697
- if (name === "create" ||
698
- action.moves.length ||
699
- action.steps.length ||
700
- action.payout ||
701
- action.calls?.length ||
702
- action.pieceStage ||
703
- action.updates?.length ||
704
- action.remainder ||
705
- action.signedSum ||
706
- action.distribute ||
707
- action.quote ||
708
- action.commit ||
709
- action.captureInput ||
710
- (action.allocate && action.contributionStage))
711
- add(path, "allocation and contribution stages need a separate action without other money, capture or update clauses");
712
- const transition = instrument.lifecycle.transitions[name];
713
- if (!transition || transition.from.includes(transition.to))
714
- add(path, "money stage requires a consuming lifecycle transition");
715
- }
716
- if (action.requiresAllocation) {
717
- const clause = action.requiresAllocation;
718
- const ap = [...path, "requiresAllocation"];
719
- const parent = clause.refField
720
- ? reference(instrument, clause.refField, ap)
721
- : instrument;
722
- if (clause.refField)
723
- required(instrument, clause.refField, (value) => targets(value).length === 1, "reference", ap);
724
- const allocation = parent?.allocation;
725
- const slice = clause.slice === "self"
726
- ? instrument
727
- : reference(instrument, clause.slice.field, ap);
728
- if (!allocation ||
729
- (allocation.sliceInstrumentId !== slice?.id &&
730
- allocation.sliceInstrumentId !== slice?.templateBinding?.id))
731
- add(ap, "allocation gate must select the executing slice's obligation");
732
- if (new Set(clause.buckets).size !== clause.buckets.length ||
733
- clause.buckets.some((key) => !allocation?.buckets.some((bucket) => bucket.key === key)))
734
- add(ap, "allocation gate must name distinct declared buckets");
735
- }
736
- if (action.allocate) {
737
- const ap = [...path, "allocate"];
738
- const parent = action.allocate.refField === undefined
739
- ? instrument
740
- : reference(instrument, action.allocate.refField, ap);
741
- if (action.allocate.mode === "refund") {
742
- if (action.allocate.action === undefined) {
743
- // No action: reverse this assessment's recorded consumption across
744
- // every receipt that consumed it. The reference names the owner.
745
- if (action.allocate.refField === undefined ||
746
- assessmentBuckets(parent, instrument).length !== 1)
747
- add(ap, "refund without an action must reference the allocation owner of exactly one assessment bucket");
748
- }
749
- else {
750
- const prior = parent?.actions[action.allocate.action]?.allocate;
751
- if (!prior || (prior.mode !== "payment" && prior.mode !== "payoff"))
752
- add(ap, "refund must reference a payment or payoff allocation receipt");
753
- }
754
- if (action.allocate.assessmentField)
755
- required(instrument, action.allocate.assessmentField, (value) => value?.type === "string" &&
756
- typeof value.minLength === "number" &&
757
- value.minLength > 0, "assessment identity", ap);
758
- }
759
- else if (!parent?.allocation)
760
- add(ap, "allocate must reference an instrument that declares allocation");
761
- if (action.allocate.refField !== undefined)
762
- required(instrument, action.allocate.refField, (value) => targets(value).length === 1, "reference", ap);
763
- if (action.allocate.mode === "payment" ||
764
- action.allocate.mode === "payoff") {
765
- const allocation = action.allocate;
766
- const allowInput = (allocation.mode === "payoff" &&
767
- allocation.refField === undefined) ||
768
- (allocation.mode === "payment" && allocation.assessment === "self");
769
- const operand = (field, predicate, kind) => {
770
- const input = object(action.input);
771
- const properties = object(input.properties);
772
- const inInput = Array.isArray(input.required) && input.required.includes(field);
773
- const inFields = Object.hasOwn(instrument.fields, field);
774
- if (allowInput && inInput && !inFields) {
775
- if (!predicate(object(properties[field])))
776
- add(ap, `${field} must be a required input ${kind}`);
777
- }
778
- else if (inInput ||
779
- (!allowInput && Object.hasOwn(properties, field))) {
780
- add(ap, `${field} must resolve to exactly one permitted allocation operand`);
781
- }
782
- else
783
- required(instrument, field, predicate, kind, ap);
784
- };
785
- if (allocation.amountField !== undefined)
786
- operand(allocation.amountField, money, "money");
787
- else if (allocation.mode === "payment" &&
788
- allocation.assessment !== "self")
789
- add(ap, "payment without amountField requires assessment self");
790
- operand(allocation.sourceAccountField, account, "account");
791
- operand(action.allocate.paymentIdentityField, (value) => value?.type === "string" &&
792
- typeof value.minLength === "number" &&
793
- value.minLength > 0, "nonempty payment identity");
794
- if (allocation.mode === "payment" &&
795
- allocation.assessment === "self") {
796
- if (assessmentBuckets(parent, instrument).length !== 1)
797
- add(ap, "assessment self must match exactly one children allocation bucket");
798
- }
799
- // Account provisioning may alias a field through a capture. A variable payout cannot use a held balance without a separate proof.
800
- const source = `fields.${action.allocate.sourceAccountField}`;
801
- for (const candidate of Object.values(instrument.actions))
802
- for (const step of candidate.steps) {
803
- if (step.operation === "account.escrow.provision" &&
804
- Object.keys(step.capture ?? {}).some((key) => `fields.${key}` === source))
805
- add(ap, "allocation source cannot alias a provisioned account");
806
- }
807
- }
808
- }
809
- }
810
- const allocation = instrument.allocation;
811
- if (allocation) {
812
- const path = [...base, "allocation"];
813
- const slice = byId.get(allocation.sliceInstrumentId);
814
- if (!slice)
815
- add(path, "allocation slice instrument does not exist");
816
- required(instrument, allocation.earningRuleField, (field) => {
817
- const values = options(field);
818
- return (field?.type === "string" &&
819
- values.length > 0 &&
820
- values.every((value) => value === "per_slice_on_due" || value === "on_disbursement"));
821
- }, "earning rule", path);
822
- const keys = allocation.buckets.map((bucket) => bucket.key);
823
- if (new Set(keys).size !== keys.length ||
824
- !keys.includes("principal") ||
825
- !keys.includes("profit"))
826
- add(path, "allocation needs distinct buckets including principal and profit");
827
- if (slice) {
828
- for (const status of allocation.sliceStatuses)
829
- if (!slice.lifecycle.states.includes(status))
830
- add(path, "allocation slice status does not exist");
831
- if (reference(slice, allocation.sliceRefField, path)?.id !== instrument.id)
832
- add(path, "allocation slices must reference this obligation");
833
- required(slice, allocation.dueField, date, "date", path);
834
- required(slice, allocation.positionField, (field) => field?.type === "integer", "integer position", path);
835
- for (const bucket of allocation.buckets) {
836
- const source = bucket.source;
837
- const owners = source.from === "slice"
838
- ? [slice]
839
- : "instrumentId" in source
840
- ? [byId.get(source.instrumentId)]
841
- : instruments.filter((candidate) => candidate.templateBinding?.id === source.template &&
842
- Object.entries(source.parameters).every(([key, value]) => candidate.templateBinding?.parameters[key] === value));
843
- if ((bucket.key === "principal" || bucket.key === "profit") &&
844
- source.from !== "slice")
845
- add(path, "principal and profit must be declared on the slice");
846
- // A catalogue may declare a template selector before a product instantiates it.
847
- // Every matching instance must satisfy the same bucket contract.
848
- for (const owner of owners) {
849
- if (!owner) {
850
- add(path, "allocation bucket instrument does not exist");
851
- continue;
852
- }
853
- if (source.from === "children") {
854
- if (reference(owner, source.refField, path)?.id !== slice.id)
855
- add(path, "allocation assessment must reference its slice");
856
- for (const status of source.statuses)
857
- if (!owner.lifecycle.states.includes(status))
858
- add(path, "allocation assessment status does not exist");
859
- }
860
- required(owner, source.amountField, money, "money", path);
861
- const principal = allocation.buckets.find((candidate) => candidate.key === "principal")?.source;
862
- if (principal?.from === "slice" &&
863
- owner.fields[source.amountField]?.["x-hyperscale-currency"] !==
864
- slice.fields[principal.amountField]?.["x-hyperscale-currency"])
865
- add(path, "allocation buckets must share the principal currency");
866
- required(owner, source.destinationField, account, "account", path);
867
- }
868
- }
869
- }
870
- }
871
- const contributions = instrument.contributions;
872
- if (contributions) {
873
- const path = [...base, "contributions"];
874
- required(instrument, contributions.totalField, money, "money", path);
875
- required(instrument, contributions.field, (field) => field?.type === "array" &&
876
- typeof field.minItems === "number" &&
877
- field.minItems >= 1 &&
878
- typeof field.maxItems === "number" &&
879
- field.maxItems <= 256, "bounded contribution list", path);
880
- const item = object(instrument.fields[contributions.field]?.items);
881
- const properties = object(item.properties);
882
- const amount = object(properties[contributions.amountKey]);
883
- if (item.type !== "object" ||
884
- item.additionalProperties !== false ||
885
- !Array.isArray(item.required) ||
886
- !item.required.includes(contributions.amountKey) ||
887
- !item.required.includes(contributions.accountKey) ||
888
- !money(amount) ||
889
- amount.pattern !== "^[1-9][0-9]{0,17}$" ||
890
- !account(object(properties[contributions.accountKey])))
891
- add(path, "contribution entries require positive money and origin account in a closed object");
892
- if (amount["x-hyperscale-currency"] !==
893
- instrument.fields[contributions.totalField]?.["x-hyperscale-currency"])
894
- add(path, "contributions must share the total currency");
895
- const stages = Object.entries(instrument.actions).filter(([, action]) => action.contributionStage);
896
- const funds = stages.filter(([, action]) => action.contributionStage?.stage === "fund");
897
- if (funds.length !== 1)
898
- add(path, "contribution list needs exactly one funding action");
899
- const hold = funds[0]?.[1].contributionStage?.accountPath;
900
- const heldAccounts = new Set((instrument.actions.create?.steps ?? [])
901
- .filter((step) => step.operation === "account.escrow.provision")
902
- .flatMap((step) => Object.entries(step.capture ?? {})
903
- .filter(([, output]) => output === "accountId")
904
- .map(([name]) => `refs.${name}`)));
905
- if (!hold || !heldAccounts.has(hold))
906
- add(path, "contribution stages require an escrow account provisioned at creation");
907
- for (const [, action] of stages)
908
- if (action.contributionStage?.accountPath !== hold)
909
- add(path, "contribution refunds must use the original funded account");
910
- }
911
- else if (Object.values(instrument.actions).some((action) => action.contributionStage))
912
- add(base, "contribution stage needs an instrument contribution list");
913
- });
914
- const visited = new Set();
915
- const active = new Set();
916
- const visit = (key) => {
917
- if (active.has(key)) {
918
- add(["instruments"], `referenced transition cycle at ${key}`);
919
- return;
920
- }
921
- if (visited.has(key))
922
- return;
923
- active.add(key);
924
- for (const next of edges.get(key) ?? [])
925
- visit(next);
926
- active.delete(key);
927
- visited.add(key);
928
- };
929
- for (const key of edges.keys())
930
- visit(key);
931
- }
932
- function instant(value) {
933
- if (typeof value !== "string" ||
934
- !/^\d{4}-\d\d-\d\d(?:T\d\d:\d\d:\d\d(?:\.\d+)?(?:Z|[+-]\d\d:\d\d))?$/.test(value))
935
- throw new Error("invalid date");
936
- const time = Date.parse(value);
937
- if (!Number.isFinite(time) ||
938
- new Date(`${value.slice(0, 10)}T00:00:00Z`).toISOString().slice(0, 10) !==
939
- value.slice(0, 10))
940
- throw new Error("invalid date");
941
- return time;
942
- }
943
- const integerMoney = (value) => {
944
- if (typeof value !== "string" || !/^(0|[1-9][0-9]{0,17})$/.test(value))
945
- throw new Error("invalid money");
946
- return BigInt(value);
947
- };
948
- export function deriveUdlAmount(amount, fields) {
949
- const source = integerMoney(fields[amount.sourceField]);
950
- if (amount.rule.kind === "minimum") {
951
- const cap = integerMoney(fields[amount.rule.capField]);
952
- return (source < cap ? source : cap).toString();
953
- }
954
- const bps = typeof amount.rule.bps === "number"
955
- ? amount.rule.bps
956
- : fields[amount.rule.bps.field];
957
- if (typeof bps !== "number" ||
958
- !Number.isInteger(bps) ||
959
- bps < 1 ||
960
- bps > 9999)
961
- throw new Error("invalid basis-point rate");
962
- return ((source * BigInt(bps)) / 10000n).toString();
963
- }
964
- export function matchesDateOrder(order, fields) {
965
- const before = instant(fields[order.beforeField]);
966
- const after = instant(fields[order.afterField]);
967
- return order.operator === "<" ? before < after : before <= after;
968
- }
969
- /** Engine supplies the complete locked child set, before status filtering. */
970
- export function matchesSchedule(check, parent, rows) {
971
- const values = parent[check.datesField];
972
- if (!Array.isArray(values) ||
973
- values.length === 0 ||
974
- values.length > 366 ||
975
- values.length !== rows.length)
976
- return false;
977
- const signed = values.map(instant);
978
- if (signed.some((time, i) => i > 0 && time <= signed[i - 1]))
979
- return false;
980
- const sorted = [...rows].sort((a, b) => Number(a[check.positionField]) - Number(b[check.positionField]));
981
- return sorted.every((row, i) => row[check.positionField] === i + 1 &&
982
- instant(row[check.dateField]) === signed[i] &&
983
- check.amounts.every((amount) => {
984
- const total = integerMoney(parent[amount.totalField]);
985
- const count = BigInt(sorted.length);
986
- const expected = total / count + (i === 0 ? total % count : 0n);
987
- return integerMoney(row[amount.amountField]) === expected;
988
- }));
989
- }
990
- export function isDueBefore(filter, fields, asOf) {
991
- const offset = typeof filter.offset === "object"
992
- ? fields[filter.offset.field]
993
- : filter.offset;
994
- const duration = offset === undefined
995
- ? 0
996
- : typeof offset === "string"
997
- ? fixedIsoDurationMs(offset)
998
- : null;
999
- if (duration === null)
1000
- throw new Error("invalid due offset");
1001
- return instant(fields[filter.field]) + duration <= instant(asOf);
1002
- }
1003
- /** All entries fund together or return to their immutable origins together. */
1004
- export function planContributions(contract, fields, holdAccountId, stage) {
1005
- const rows = fields[contract.field];
1006
- if (!Array.isArray(rows) ||
1007
- rows.length === 0 ||
1008
- rows.length > 256 ||
1009
- !holdAccountId ||
1010
- (stage !== "fund" && stage !== "refund"))
1011
- throw new Error("invalid contribution stage");
1012
- const entries = rows.map((row) => {
1013
- const record = object(row);
1014
- const origin = record[contract.accountKey];
1015
- const amount = integerMoney(record[contract.amountKey]);
1016
- if (typeof origin !== "string" ||
1017
- !origin ||
1018
- origin === holdAccountId ||
1019
- amount <= 0n)
1020
- throw new Error("invalid contribution origin or amount");
1021
- return { origin, amount };
1022
- });
1023
- if (entries.reduce((sum, entry) => sum + entry.amount, 0n) !==
1024
- integerMoney(fields[contract.totalField]))
1025
- throw new Error("contribution total mismatch");
1026
- return entries.map((entry, index) => ({
1027
- index,
1028
- amount: entry.amount,
1029
- sourceAccountId: stage === "fund" ? entry.origin : holdAccountId,
1030
- destinationAccountId: stage === "fund" ? holdAccountId : entry.origin,
1031
- }));
1032
- }
1033
- export function matchesOrdered(check, rows) {
1034
- const sorted = [...rows].sort((a, b) => Number(a[check.positionField]) - Number(b[check.positionField]));
1035
- return (sorted.length > 0 &&
1036
- sorted.every((row, i) => row[check.positionField] === i + 1 &&
1037
- (i === 0 ||
1038
- instant(sorted[i - 1][check.field]) < instant(row[check.field]))));
1039
- }
1040
- export function deriveRemainder(clause, fields, collected = []) {
1041
- const read = (path) => {
1042
- const [root, key] = path.split(".");
1043
- return integerMoney(object(fields[root])[key]);
1044
- };
1045
- const result = read(clause.totalPath) -
1046
- (clause.subtractPaths ?? []).reduce((sum, path) => sum + read(path), 0n) -
1047
- collected.reduce((sum, value) => sum + integerMoney(value), 0n);
1048
- if (result < 0n || (result === 0n && clause.onZero === "refuse"))
1049
- throw new Error("remainder is not positive");
1050
- return result === 0n ? null : result.toString();
1051
- }
1052
- //# sourceMappingURL=vocabulary.js.map