@hyperscale0/udl 1.0.0 → 1.0.124

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +5 -216
  2. package/LICENSE.md +356 -0
  3. package/NOTICE.md +8 -0
  4. package/README.md +14 -106
  5. package/dist/canonical.d.ts.map +1 -1
  6. package/dist/canonical.js +1 -29
  7. package/dist/canonical.js.map +1 -1
  8. package/dist/diagnostics.d.ts +13 -165
  9. package/dist/diagnostics.d.ts.map +1 -1
  10. package/dist/diagnostics.js +33 -157
  11. package/dist/diagnostics.js.map +1 -1
  12. package/dist/field-value.d.ts +5 -0
  13. package/dist/field-value.d.ts.map +1 -0
  14. package/dist/field-value.js +85 -0
  15. package/dist/field-value.js.map +1 -0
  16. package/dist/finance.d.ts +10 -53
  17. package/dist/finance.d.ts.map +1 -1
  18. package/dist/finance.js +252 -536
  19. package/dist/finance.js.map +1 -1
  20. package/dist/index.d.ts +9 -13
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +9 -9
  23. package/dist/index.js.map +1 -1
  24. package/dist/json.d.ts +2 -0
  25. package/dist/json.d.ts.map +1 -0
  26. package/dist/json.js +30 -0
  27. package/dist/json.js.map +1 -0
  28. package/dist/limits.d.ts +3 -28
  29. package/dist/limits.d.ts.map +1 -1
  30. package/dist/limits.js +5 -31
  31. package/dist/limits.js.map +1 -1
  32. package/dist/object-contract.d.ts +140 -0
  33. package/dist/object-contract.d.ts.map +1 -0
  34. package/dist/object-contract.js +314 -0
  35. package/dist/object-contract.js.map +1 -0
  36. package/dist/object-semantics-schema.d.ts +5710 -0
  37. package/dist/object-semantics-schema.d.ts.map +1 -0
  38. package/dist/object-semantics-schema.js +119 -0
  39. package/dist/object-semantics-schema.js.map +1 -0
  40. package/dist/object-semantics.d.ts +6 -0
  41. package/dist/object-semantics.d.ts.map +1 -0
  42. package/dist/object-semantics.js +279 -0
  43. package/dist/object-semantics.js.map +1 -0
  44. package/dist/object-structures.d.ts +4 -0
  45. package/dist/object-structures.d.ts.map +1 -0
  46. package/dist/object-structures.js +134 -0
  47. package/dist/object-structures.js.map +1 -0
  48. package/dist/reporting-validation.d.ts +8 -0
  49. package/dist/reporting-validation.d.ts.map +1 -0
  50. package/dist/reporting-validation.js +252 -0
  51. package/dist/reporting-validation.js.map +1 -0
  52. package/dist/reporting.d.ts +477 -0
  53. package/dist/reporting.d.ts.map +1 -0
  54. package/dist/reporting.js +169 -0
  55. package/dist/reporting.js.map +1 -0
  56. package/dist/schema.d.ts +5572 -2710
  57. package/dist/schema.d.ts.map +1 -1
  58. package/dist/schema.js +444 -1121
  59. package/dist/schema.js.map +1 -1
  60. package/dist/validation.d.ts +10 -73
  61. package/dist/validation.d.ts.map +1 -1
  62. package/dist/validation.js +1109 -2569
  63. package/dist/validation.js.map +1 -1
  64. package/docs/assets/brand/manifest.json +30 -0
  65. package/docs/assets/brand/udl-horizontal-white.svg +1 -0
  66. package/docs/assets/brand/udl-horizontal.svg +1 -0
  67. package/docs/assets/brand/udl-stacked-white.svg +1 -0
  68. package/docs/assets/brand/udl-stacked.svg +1 -0
  69. package/docs/assets/udl.svg +11 -0
  70. package/package.json +13 -22
  71. package/spec/README.md +300 -172
  72. package/spec/cars.udl.json +249 -0
  73. package/spec/udl.schema.json +3066 -2242
  74. package/src/canonical.ts +1 -35
  75. package/src/diagnostics.ts +76 -209
  76. package/src/field-value.ts +87 -0
  77. package/src/finance.ts +327 -832
  78. package/src/index.ts +18 -95
  79. package/src/json.ts +34 -0
  80. package/src/limits.ts +5 -31
  81. package/src/object-contract.ts +559 -0
  82. package/src/object-semantics-schema.ts +148 -0
  83. package/src/object-semantics.ts +378 -0
  84. package/src/object-structures.ts +255 -0
  85. package/src/reporting-validation.ts +383 -0
  86. package/src/reporting.ts +178 -0
  87. package/src/schema.ts +510 -1277
  88. package/src/validation.ts +1630 -4545
  89. package/LICENSE +0 -661
  90. package/LICENSING.md +0 -51
  91. package/TRADEMARKS.md +0 -35
  92. package/conformance/README.md +0 -82
  93. package/conformance/evolution/action-contract.expected.json +0 -10
  94. package/conformance/evolution/action-contract.live.udl +0 -44
  95. package/conformance/evolution/action-contract.next.udl +0 -45
  96. package/conformance/evolution/product-identity.expected.json +0 -10
  97. package/conformance/evolution/product-identity.live.udl +0 -55
  98. package/conformance/evolution/product-identity.next.udl +0 -55
  99. package/conformance/evolution/version-required.expected.json +0 -10
  100. package/conformance/evolution/version-required.live.udl +0 -55
  101. package/conformance/evolution/version-required.next.udl +0 -58
  102. package/conformance/invalid/action-without-transition.expected.json +0 -10
  103. package/conformance/invalid/action-without-transition.udl +0 -60
  104. package/conformance/invalid/agent-description-too-long.expected.json +0 -10
  105. package/conformance/invalid/agent-description-too-long.udl +0 -57
  106. package/conformance/invalid/blank-title.expected.json +0 -10
  107. package/conformance/invalid/blank-title.udl +0 -54
  108. package/conformance/invalid/composition-dial-duplicate-key.expected.json +0 -10
  109. package/conformance/invalid/composition-dial-duplicate-key.udl +0 -73
  110. package/conformance/invalid/depth-budget.expected.json +0 -10
  111. package/conformance/invalid/depth-budget.udl +0 -49
  112. package/conformance/invalid/duplicate-subject.expected.json +0 -10
  113. package/conformance/invalid/duplicate-subject.udl +0 -230
  114. package/conformance/invalid/forged-effects.expected.json +0 -10
  115. package/conformance/invalid/forged-effects.udl +0 -76
  116. package/conformance/invalid/format-version.expected.json +0 -10
  117. package/conformance/invalid/format-version.udl +0 -54
  118. package/conformance/invalid/instrument-id-not-snake-case.expected.json +0 -10
  119. package/conformance/invalid/instrument-id-not-snake-case.udl +0 -54
  120. package/conformance/invalid/invalid-aggregate-gate-shape.expected.json +0 -10
  121. package/conformance/invalid/invalid-aggregate-gate-shape.udl +0 -1819
  122. package/conformance/invalid/invalid-check-duration.expected.json +0 -10
  123. package/conformance/invalid/invalid-check-duration.udl +0 -219
  124. package/conformance/invalid/invalid-dial-anchor.expected.json +0 -10
  125. package/conformance/invalid/invalid-dial-anchor.udl +0 -219
  126. package/conformance/invalid/invalid-exception-parent-ref.expected.json +0 -10
  127. package/conformance/invalid/invalid-exception-parent-ref.udl +0 -2487
  128. package/conformance/invalid/invalid-exposure-shape.expected.json +0 -10
  129. package/conformance/invalid/invalid-exposure-shape.udl +0 -1819
  130. package/conformance/invalid/invalid-remainder.expected.json +0 -10
  131. package/conformance/invalid/invalid-remainder.udl +0 -220
  132. package/conformance/invalid/invalid-schema-keyword.expected.json +0 -10
  133. package/conformance/invalid/invalid-schema-keyword.udl +0 -220
  134. package/conformance/invalid/invalid-utf8.expected.json +0 -10
  135. package/conformance/invalid/invalid-utf8.udl +0 -1
  136. package/conformance/invalid/malformed-json.expected.json +0 -10
  137. package/conformance/invalid/malformed-json.udl +0 -1
  138. package/conformance/invalid/missing-create-action.expected.json +0 -10
  139. package/conformance/invalid/missing-create-action.udl +0 -48
  140. package/conformance/invalid/missing-exception-amount-field.expected.json +0 -10
  141. package/conformance/invalid/missing-exception-amount-field.udl +0 -2487
  142. package/conformance/invalid/missing-exception-contract.expected.json +0 -14
  143. package/conformance/invalid/missing-exception-contract.udl +0 -2523
  144. package/conformance/invalid/missing-exception-reason-field.expected.json +0 -10
  145. package/conformance/invalid/missing-exception-reason-field.udl +0 -2487
  146. package/conformance/invalid/not-an-object.expected.json +0 -10
  147. package/conformance/invalid/not-an-object.udl +0 -1
  148. package/conformance/invalid/payout-reconcile-not-a-bank-debit.expected.json +0 -10
  149. package/conformance/invalid/payout-reconcile-not-a-bank-debit.udl +0 -259
  150. package/conformance/invalid/quote-freeze-set-incomplete.expected.json +0 -10
  151. package/conformance/invalid/quote-freeze-set-incomplete.udl +0 -261
  152. package/conformance/invalid/quote-named-reference-gate.expected.json +0 -10
  153. package/conformance/invalid/quote-named-reference-gate.udl +0 -50
  154. package/conformance/invalid/reconcile-named-reference-gate.expected.json +0 -10
  155. package/conformance/invalid/reconcile-named-reference-gate.udl +0 -50
  156. package/conformance/invalid/unknown-key.expected.json +0 -10
  157. package/conformance/invalid/unknown-key.udl +0 -55
  158. package/conformance/invalid/unknown-reference-gate-field.expected.json +0 -10
  159. package/conformance/invalid/unknown-reference-gate-field.udl +0 -1819
  160. package/conformance/invalid/unknown-required-field.expected.json +0 -10
  161. package/conformance/invalid/unknown-required-field.udl +0 -220
  162. package/conformance/invalid/unreachable-state.expected.json +0 -10
  163. package/conformance/invalid/unreachable-state.udl +0 -55
  164. package/conformance/invalid/wrong-exception-amount-field.expected.json +0 -10
  165. package/conformance/invalid/wrong-exception-amount-field.udl +0 -2487
  166. package/conformance/invalid/wrong-exception-reason-field.expected.json +0 -10
  167. package/conformance/invalid/wrong-exception-reason-field.udl +0 -2487
  168. package/conformance/valid/agent-description.expected.json +0 -6
  169. package/conformance/valid/agent-description.udl +0 -66
  170. package/conformance/valid/cards.expected.json +0 -6
  171. package/conformance/valid/cards.udl +0 -1579
  172. package/conformance/valid/commerce-escrow.expected.json +0 -6
  173. package/conformance/valid/commerce-escrow.udl +0 -1512
  174. package/conformance/valid/compiled-crowdfunding.expected.json +0 -6
  175. package/conformance/valid/compiled-crowdfunding.udl +0 -1843
  176. package/conformance/valid/compiled-watch-club.expected.json +0 -6
  177. package/conformance/valid/compiled-watch-club.udl +0 -2486
  178. package/conformance/valid/complete-contract.expected.json +0 -6
  179. package/conformance/valid/complete-contract.udl +0 -218
  180. package/conformance/valid/effect-signatures.expected.json +0 -6
  181. package/conformance/valid/effect-signatures.udl +0 -75
  182. package/conformance/valid/hand-edited.expected.json +0 -6
  183. package/conformance/valid/hand-edited.udl +0 -1
  184. package/conformance/valid/insured-car-marketplace.expected.json +0 -6
  185. package/conformance/valid/insured-car-marketplace.udl +0 -1050
  186. package/conformance/valid/insured-travel.expected.json +0 -6
  187. package/conformance/valid/insured-travel.udl +0 -3469
  188. package/conformance/valid/minimal.expected.json +0 -6
  189. package/conformance/valid/minimal.udl +0 -54
  190. package/conformance/valid/protection.expected.json +0 -6
  191. package/conformance/valid/protection.udl +0 -1551
  192. package/conformance/valid/string-escaping.expected.json +0 -6
  193. package/conformance/valid/string-escaping.udl +0 -54
  194. package/dist/check-profiles.d.ts +0 -57
  195. package/dist/check-profiles.d.ts.map +0 -1
  196. package/dist/check-profiles.js +0 -62
  197. package/dist/check-profiles.js.map +0 -1
  198. package/dist/cli.d.ts +0 -3
  199. package/dist/cli.d.ts.map +0 -1
  200. package/dist/cli.js +0 -184
  201. package/dist/cli.js.map +0 -1
  202. package/dist/effects.d.ts +0 -41
  203. package/dist/effects.d.ts.map +0 -1
  204. package/dist/effects.js +0 -110
  205. package/dist/effects.js.map +0 -1
  206. package/dist/evolution.d.ts +0 -120
  207. package/dist/evolution.d.ts.map +0 -1
  208. package/dist/evolution.js +0 -567
  209. package/dist/evolution.js.map +0 -1
  210. package/dist/parser.d.ts +0 -4
  211. package/dist/parser.d.ts.map +0 -1
  212. package/dist/parser.js +0 -44
  213. package/dist/parser.js.map +0 -1
  214. package/docs/README.md +0 -14
  215. package/docs/guide/01-a-document.md +0 -37
  216. package/docs/guide/02-money-steps.md +0 -23
  217. package/docs/guide/03-laws.md +0 -18
  218. package/docs/guide/04-fees-and-remainder.md +0 -15
  219. package/docs/guide/05-checks-updates-dials.md +0 -7
  220. package/docs/guide/06-effects.md +0 -11
  221. package/docs/guide/07-evolution.md +0 -11
  222. package/docs/guide/08-implementing.md +0 -13
  223. package/docs/llms-full.txt +0 -1241
  224. package/docs/llms.txt +0 -14
  225. package/docs/reference/canonical.md +0 -16
  226. package/docs/reference/clauses.md +0 -986
  227. package/docs/reference/cli.md +0 -22
  228. package/docs/reference/diagnostics.md +0 -32
  229. package/skills/udl/SKILL.md +0 -28
  230. package/src/check-profiles.ts +0 -80
  231. package/src/cli.ts +0 -196
  232. package/src/effects.ts +0 -163
  233. package/src/evolution.ts +0 -965
  234. package/src/parser.ts +0 -58
@@ -0,0 +1,378 @@
1
+ import {
2
+ subjectPartyRoles,
3
+ type UdlDocument,
4
+ type UdlInstrument,
5
+ type UdlField,
6
+ type UdlAction,
7
+ } from "./schema.js";
8
+ import { resolveField } from "./validation.js";
9
+ import type {
10
+ DocumentSemantics,
11
+ SemanticAccount,
12
+ SemanticOwner,
13
+ SemanticAction,
14
+ SemanticMoneyEffect,
15
+ SemanticRelationship,
16
+ } from "./object-semantics-schema.js";
17
+ import { projectStructures } from "./object-structures.js";
18
+
19
+ export function presentationLabel(name: string): string {
20
+ return name
21
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
22
+ .replace(/_/g, " ")
23
+ .replace(/^./, (letter) => letter.toUpperCase());
24
+ }
25
+ const labelled = <T extends UdlField>(field: T): T => ({
26
+ ...field,
27
+ label: field.label ?? presentationLabel(field.name),
28
+ });
29
+ const targets = (target: string | string[]) =>
30
+ typeof target === "string" ? [target] : target;
31
+
32
+ function owner(
33
+ document: UdlDocument,
34
+ instrument: UdlInstrument,
35
+ value: Extract<UdlField, { type: "account" }>["owner"],
36
+ ): SemanticOwner {
37
+ if (typeof value !== "string")
38
+ return { kind: "adapter", adapter: value.adapter };
39
+ if (value === "self")
40
+ return { kind: "instrument", instrument: instrument.id };
41
+ const binding = document.objects
42
+ .flatMap((object) => object.attachments)
43
+ .find((attachment) => attachment.instrument === instrument.id)?.parties[
44
+ value
45
+ ];
46
+ if (binding)
47
+ return "role" in binding
48
+ ? { kind: "role", role: binding.role }
49
+ : { kind: "party", party: binding.party };
50
+ const role = subjectPartyRoles.find((role) => role === value);
51
+ return instrument.subject && role
52
+ ? { kind: "role", role }
53
+ : { kind: "party", party: value };
54
+ }
55
+
56
+ /** Follow typed references, retaining every union member instead of guessing an owner. */
57
+ function scopes(
58
+ document: UdlDocument,
59
+ instrument: UdlInstrument,
60
+ path: string,
61
+ ): { instrument: UdlInstrument; path: string }[] {
62
+ const [root, key, ...rest] = path.split(".");
63
+ const field = instrument.fields.find((field) => field.name === key);
64
+ if (
65
+ root === "self" &&
66
+ rest.length &&
67
+ field?.type === "ref" &&
68
+ field.targetKind === "instrument"
69
+ )
70
+ return document.instruments
71
+ .filter((candidate) => targets(field.target).includes(candidate.id))
72
+ .flatMap((candidate) =>
73
+ scopes(document, candidate, `self.${rest.join(".")}`),
74
+ );
75
+ return [{ instrument, path }];
76
+ }
77
+ function accounts(
78
+ document: UdlDocument,
79
+ instrument: UdlInstrument,
80
+ path: string,
81
+ action?: UdlAction,
82
+ ): SemanticAccount[] {
83
+ return scopes(document, instrument, path).flatMap((scope) => {
84
+ const field = resolveField(
85
+ document,
86
+ scope.instrument,
87
+ scope.path,
88
+ action?.input,
89
+ action,
90
+ );
91
+ if (field?.type !== "account") return [];
92
+ return [
93
+ {
94
+ path,
95
+ scope: instrument.id,
96
+ instrument: scope.instrument.id,
97
+ field: field.name,
98
+ owner: owner(document, scope.instrument, field.owner),
99
+ book: field.book,
100
+ key: field.key ?? (field.owner === "self" ? field.name : "balance"),
101
+ contra: field.contra ?? false,
102
+ },
103
+ ];
104
+ });
105
+ }
106
+ function moneyEffects(
107
+ document: UdlDocument,
108
+ instrument: UdlInstrument,
109
+ action: UdlAction,
110
+ ): SemanticMoneyEffect[] {
111
+ return action.moves.map((move) => {
112
+ const reservations =
113
+ "transfer" in move
114
+ ? scopes(document, instrument, move.transfer).flatMap((scope) =>
115
+ scope.instrument.actionOrder.flatMap((name) =>
116
+ scope.instrument.actions[name]!.moves.flatMap((candidate) =>
117
+ candidate.operation === "internal_transfer.reserve" &&
118
+ scope.path === `self.${candidate.capture}`
119
+ ? [
120
+ {
121
+ instrument: scope.instrument,
122
+ action: name,
123
+ move: candidate,
124
+ },
125
+ ]
126
+ : [],
127
+ ),
128
+ ),
129
+ )
130
+ : [];
131
+ const movements = "amount" in move ? [{ instrument, move }] : reservations;
132
+ const from = movements.flatMap((entry) =>
133
+ accounts(document, entry.instrument, entry.move.from, action),
134
+ );
135
+ const to = movements.flatMap((entry) =>
136
+ accounts(document, entry.instrument, entry.move.to, action),
137
+ );
138
+ const amount =
139
+ "amount" in move
140
+ ? move.amount
141
+ : reservations.length === 1
142
+ ? reservations[0]!.move.amount
143
+ : null;
144
+ return {
145
+ key: move.key,
146
+ operation: move.operation,
147
+ amount,
148
+ from,
149
+ to,
150
+ disposition:
151
+ !from.length || !to.length
152
+ ? "unresolved"
153
+ : move.operation === "internal_transfer.void"
154
+ ? "released"
155
+ : move.operation === "internal_transfer.reserve"
156
+ ? "held"
157
+ : to.some((account) => account.book === "claim")
158
+ ? "claim"
159
+ : to.some((account) => account.owner.kind === "instrument")
160
+ ? "held"
161
+ : "paid",
162
+ transfer:
163
+ "transfer" in move
164
+ ? move.transfer
165
+ : move.operation === "internal_transfer.reserve"
166
+ ? `self.${move.capture}`
167
+ : null,
168
+ reservations: reservations.map((entry) => ({
169
+ instrument: entry.instrument.id,
170
+ action: entry.action,
171
+ key: entry.move.key,
172
+ })),
173
+ };
174
+ });
175
+ }
176
+ function relationships(
177
+ document: UdlDocument,
178
+ instrument: UdlInstrument,
179
+ ): SemanticRelationship[] {
180
+ const references: SemanticRelationship[] = instrument.fields.flatMap(
181
+ (field) => {
182
+ if (field.type === "ref")
183
+ return [
184
+ {
185
+ kind: "reference" as const,
186
+ via: "field" as const,
187
+ name: field.name,
188
+ targetKind: field.targetKind,
189
+ targets: targets(field.target),
190
+ cardinality: { min: field.optional ? 0 : 1, max: 1 },
191
+ },
192
+ ];
193
+ if (field.type === "list" && field.item === "ref" && field.target)
194
+ return [
195
+ {
196
+ kind: "reference" as const,
197
+ via: "field" as const,
198
+ name: field.name,
199
+ targetKind: field.targetKind ?? "instrument",
200
+ targets: [field.target],
201
+ cardinality: { min: 0, max: field.maxItems },
202
+ },
203
+ ];
204
+ return [];
205
+ },
206
+ );
207
+ for (const object of document.objects) {
208
+ const attachment = object.attachments.find(
209
+ (item) => item.instrument === instrument.id,
210
+ );
211
+ if (!attachment) continue;
212
+ for (const child of object.attachments.filter(
213
+ (item) => item.parent === attachment.name,
214
+ ))
215
+ references.push({
216
+ kind: "child",
217
+ via: "attachment",
218
+ name: child.name,
219
+ targetKind: "instrument",
220
+ targets: [child.instrument],
221
+ cardinality: { min: 0, max: null },
222
+ });
223
+ }
224
+ for (const name of instrument.actionOrder)
225
+ for (const call of instrument.actions[name]!.invoke ?? [])
226
+ if ("instrument" in call)
227
+ references.push({
228
+ kind: "child",
229
+ via: "invocation",
230
+ name,
231
+ targetKind: "instrument",
232
+ targets: [call.instrument],
233
+ // This bound is per invocation, not the lifetime number of children.
234
+ cardinality: { min: 0, max: call.range?.maximum ?? 1 },
235
+ });
236
+ for (const child of document.instruments) {
237
+ const parentActor = Object.values(child.actions).some(
238
+ (action) =>
239
+ typeof action.actor === "object" &&
240
+ "parent" in action.actor &&
241
+ targets(action.actor.parent).includes(instrument.id),
242
+ );
243
+ if (
244
+ parentActor &&
245
+ child.fields.some(
246
+ (field) =>
247
+ field.type === "ref" &&
248
+ field.targetKind === "instrument" &&
249
+ targets(field.target).includes(instrument.id),
250
+ )
251
+ )
252
+ references.push({
253
+ kind: "child",
254
+ via: "parent_actor",
255
+ name: child.id,
256
+ targetKind: "instrument",
257
+ targets: [child.id],
258
+ cardinality: { min: 0, max: null },
259
+ });
260
+ }
261
+ return references;
262
+ }
263
+ function projectAction(
264
+ document: UdlDocument,
265
+ instrument: UdlInstrument,
266
+ name: string,
267
+ ): SemanticAction {
268
+ const action = instrument.actions[name]!;
269
+ const transition = instrument.lifecycle.transitions[name];
270
+ const actor = action.actor;
271
+ return {
272
+ name,
273
+ alias: action.publicAction ?? null,
274
+ title: action.title ?? presentationLabel(action.publicAction ?? name),
275
+ actor,
276
+ roles: [
277
+ typeof actor === "string"
278
+ ? { kind: actor }
279
+ : "party" in actor
280
+ ? owner(document, instrument, actor.party)
281
+ : { kind: "parent", instruments: targets(actor.parent) },
282
+ ],
283
+ from: transition?.from ?? [],
284
+ to:
285
+ name === "create"
286
+ ? instrument.lifecycle.initial
287
+ : (transition?.to ?? null),
288
+ ...(name === "create"
289
+ ? {
290
+ references: instrument.fields.filter(
291
+ (field) =>
292
+ field.type === "ref" ||
293
+ (field.type === "list" && field.item === "ref"),
294
+ ),
295
+ invokedBy: document.instruments.flatMap((parent) =>
296
+ parent.actionOrder.flatMap((parentName) =>
297
+ (parent.actions[parentName]!.invoke ?? []).flatMap((call) =>
298
+ "instrument" in call && call.instrument === instrument.id
299
+ ? [
300
+ {
301
+ instrument: parent.id,
302
+ action: parentName,
303
+ alias: parent.actions[parentName]!.publicAction ?? null,
304
+ invocation: call,
305
+ },
306
+ ]
307
+ : [],
308
+ ),
309
+ ),
310
+ ),
311
+ }
312
+ : {}),
313
+ inputs: action.input.map(labelled),
314
+ subject: action.subject,
315
+ prerequisites: action.requires,
316
+ due: action.due ?? null,
317
+ deadline: action.deadline ?? null,
318
+ calculations: action.calculate ?? [],
319
+ assignments: action.set ?? {},
320
+ moves: action.moves,
321
+ moneyEffects: moneyEffects(document, instrument, action),
322
+ invokes: action.invoke ?? [],
323
+ };
324
+ }
325
+
326
+ /** Shared static meaning for authoring and discovery. No balances or eligibility are evaluated here. */
327
+ export function projectDocumentSemantics(
328
+ document: UdlDocument,
329
+ ): DocumentSemantics {
330
+ const instruments: DocumentSemantics["instruments"] =
331
+ document.instruments.map((instrument) => ({
332
+ id: instrument.id,
333
+ title: instrument.title,
334
+ subject: instrument.subject,
335
+ fields: instrument.fields.map(labelled),
336
+ accounts: instrument.fields.flatMap((field) =>
337
+ field.type === "account"
338
+ ? accounts(document, instrument, `self.${field.name}`)
339
+ : [],
340
+ ),
341
+ relationships: relationships(document, instrument),
342
+ calculations: instrument.calculate,
343
+ invariants: instrument.invariants ?? [],
344
+ initialState: instrument.lifecycle.initial,
345
+ lifecycle: {
346
+ initial: instrument.lifecycle.initial,
347
+ states: instrument.lifecycle.states.map((name) => ({
348
+ name,
349
+ label: instrument.lifecycle.labels?.[name] ?? presentationLabel(name),
350
+ terminal: !Object.values(instrument.lifecycle.transitions).some(
351
+ (transition) => transition.from.includes(name),
352
+ ),
353
+ })),
354
+ cancellationActions: [],
355
+ },
356
+ actions: instrument.actionOrder.map((name) =>
357
+ projectAction(document, instrument, name),
358
+ ),
359
+ reports: instrument.reports ?? [],
360
+ structures: [],
361
+ }));
362
+ for (const instrument of instruments)
363
+ projectStructures(instrument, instruments);
364
+ return {
365
+ currency: document.currency,
366
+ parties: document.parties,
367
+ objects: document.objects.map((object) => ({
368
+ ...object,
369
+ fields: object.fields.map(labelled),
370
+ attachments: object.attachments.map((attachment) => ({
371
+ ...attachment,
372
+ title: attachment.title ?? presentationLabel(attachment.name),
373
+ cardinality: { min: 0, max: null },
374
+ })),
375
+ })),
376
+ instruments,
377
+ };
378
+ }
@@ -0,0 +1,255 @@
1
+ import type {
2
+ SemanticAccount,
3
+ SemanticAction,
4
+ SemanticInstrument,
5
+ } from "./object-semantics-schema.js";
6
+
7
+ const hasMoney = (instrument: SemanticInstrument) =>
8
+ instrument.actions.some((action) => action.moneyEffects.length > 0);
9
+ const timed = (action: SemanticAction) =>
10
+ action.due !== null || action.deadline !== null;
11
+ const sameAccount = (left: SemanticAccount, right: SemanticAccount) =>
12
+ left.book === right.book &&
13
+ left.key === right.key &&
14
+ JSON.stringify(left.owner) === JSON.stringify(right.owner);
15
+
16
+ /** Select only structures proved by declarations. Names, families and prose carry no authority. */
17
+ export function projectStructures(
18
+ instrument: SemanticInstrument,
19
+ instruments: SemanticInstrument[],
20
+ ): void {
21
+ const actions = instrument.actions;
22
+ const effects = actions.flatMap((action) => action.moneyEffects);
23
+ const precedes = (before: SemanticAction, after: SemanticAction) => {
24
+ const pending = before.to === null ? [] : [before.to];
25
+ const seen = new Set<string>();
26
+ while (pending.length) {
27
+ const state = pending.pop()!;
28
+ if (after.from.includes(state)) return true;
29
+ if (seen.has(state)) continue;
30
+ seen.add(state);
31
+ for (const next of actions)
32
+ if (next !== after && next.from.includes(state) && next.to !== null)
33
+ pending.push(next.to);
34
+ }
35
+ return false;
36
+ };
37
+ const returnsMoney = (action: SemanticAction) => {
38
+ for (const effect of action.moneyEffects) {
39
+ if (
40
+ effect.operation === "internal_transfer.void" &&
41
+ effect.reservations.length
42
+ )
43
+ return true;
44
+ if (effect.operation !== "internal_transfer.create") continue;
45
+ for (const prior of actions) {
46
+ if (prior === action || !precedes(prior, action)) continue;
47
+ for (const earlier of prior.moneyEffects) {
48
+ if (earlier.operation !== "internal_transfer.create") continue;
49
+ if (
50
+ effect.from.some((from) =>
51
+ earlier.to.some((to) => sameAccount(from, to)),
52
+ ) &&
53
+ effect.to.some((to) =>
54
+ earlier.from.some((from) => sameAccount(to, from)),
55
+ )
56
+ )
57
+ return true;
58
+ }
59
+ }
60
+ }
61
+ return false;
62
+ };
63
+ const abandonsInitial = (action: SemanticAction) =>
64
+ action.from.includes(instrument.initialState) &&
65
+ !action.moneyEffects.length &&
66
+ !action.invokes.length &&
67
+ !actions.find((candidate) => candidate.name === "create")?.moneyEffects
68
+ .length &&
69
+ actions.some(
70
+ (alternative) =>
71
+ alternative !== action &&
72
+ alternative.moneyEffects.length > 0 &&
73
+ alternative.from.some((from) => action.from.includes(from)),
74
+ );
75
+ const cancellation = actions.filter(
76
+ (action) =>
77
+ action.from.length &&
78
+ instrument.lifecycle.states.some(
79
+ (state) => state.name === action.to && state.terminal,
80
+ ) &&
81
+ (returnsMoney(action) || abandonsInitial(action)),
82
+ );
83
+ instrument.lifecycle.cancellationActions = cancellation.map(
84
+ (action) => action.name,
85
+ );
86
+
87
+ const linked = instruments.filter((candidate) =>
88
+ instrument.relationships.some(
89
+ (relationship) =>
90
+ relationship.kind === "child" &&
91
+ relationship.targets.includes(candidate.id),
92
+ ),
93
+ );
94
+ const calendar = (candidate: SemanticInstrument) =>
95
+ hasMoney(candidate) &&
96
+ candidate.actions.some((action) => action.due !== null) &&
97
+ candidate.relationships.some(
98
+ (relationship) =>
99
+ relationship.kind === "reference" &&
100
+ relationship.targetKind === "instrument",
101
+ );
102
+ const hasParent = instruments.some((parent) =>
103
+ parent.relationships.some(
104
+ (relationship) =>
105
+ relationship.kind === "child" &&
106
+ relationship.targets.includes(instrument.id),
107
+ ),
108
+ );
109
+ const reserved = actions.some((action) =>
110
+ action.moneyEffects.some(
111
+ (reserve) =>
112
+ reserve.operation === "internal_transfer.reserve" &&
113
+ ["internal_transfer.post", "internal_transfer.void"].every(
114
+ (operation) =>
115
+ effects.some(
116
+ (effect) =>
117
+ effect.operation === operation &&
118
+ effect.reservations.some(
119
+ (reference) =>
120
+ reference.instrument === instrument.id &&
121
+ reference.action === action.name &&
122
+ reference.key === reserve.key,
123
+ ),
124
+ ),
125
+ ),
126
+ ),
127
+ );
128
+ const contributions = [
129
+ ...[
130
+ ...instrument.invariants,
131
+ ...actions.flatMap((action) => action.prerequisites),
132
+ ].flatMap((requirement) =>
133
+ requirement.kind === "aggregate" ? [requirement.selection] : [],
134
+ ),
135
+ ...[
136
+ ...instrument.calculations,
137
+ ...actions.flatMap((action) => action.calculations),
138
+ ].flatMap((calculation) =>
139
+ calculation.op === "aggregate" ? [calculation.selection] : [],
140
+ ),
141
+ ...actions.flatMap((action) =>
142
+ action.invokes.flatMap((call) =>
143
+ "selection" in call ? [call.selection] : [],
144
+ ),
145
+ ),
146
+ ];
147
+ const fundedAccount = instrument.accounts.filter(
148
+ (account) => account.owner.kind === "instrument" && account.book === "cash",
149
+ );
150
+ const funding = contributions.some((selection) => {
151
+ if (selection.anchor !== "self.id") return false;
152
+ const ids =
153
+ typeof selection.instrument === "string"
154
+ ? [selection.instrument]
155
+ : selection.instrument;
156
+ const children = instruments.filter(
157
+ (candidate) =>
158
+ ids.includes(candidate.id) &&
159
+ candidate.relationships.some(
160
+ (reference) =>
161
+ reference.name === selection.reference &&
162
+ reference.targets.includes(instrument.id),
163
+ ),
164
+ );
165
+ const receivesContribution = children.some((child) =>
166
+ child.actions.some((action) =>
167
+ action.moneyEffects.some(
168
+ (effect) =>
169
+ effect.operation === "internal_transfer.create" &&
170
+ effect.to.some((to) =>
171
+ fundedAccount.some((account) => sameAccount(to, account)),
172
+ ),
173
+ ),
174
+ ),
175
+ );
176
+ const balanceTarget = actions.some((action) =>
177
+ action.prerequisites.some((requirement) => {
178
+ if (requirement.kind !== "compare") return false;
179
+ const values = [requirement.left, requirement.right];
180
+ return (
181
+ values.some(
182
+ (value) =>
183
+ "field" in value &&
184
+ fundedAccount.some(
185
+ (account) => value.field === `${account.path}.balance`,
186
+ ),
187
+ ) &&
188
+ values.some(
189
+ (value) =>
190
+ "field" in value &&
191
+ instrument.fields.some(
192
+ (field) =>
193
+ field.type === "money" &&
194
+ value.field === `self.${field.name}`,
195
+ ),
196
+ )
197
+ );
198
+ }),
199
+ );
200
+ return receivesContribution && balanceTarget;
201
+ });
202
+ const structures: SemanticInstrument["structures"] = [];
203
+ if (actions.some((action) => action.alias !== null && action.from.length))
204
+ structures.push("stage_and_decisions");
205
+ if (
206
+ effects.some(
207
+ (effect) =>
208
+ effect.from.length &&
209
+ effect.to.length &&
210
+ effect.operation !== "internal_transfer.void",
211
+ )
212
+ )
213
+ structures.push("payment_review");
214
+ if (
215
+ fundedAccount.some((account) =>
216
+ actions.some(
217
+ (action) =>
218
+ action.from.length &&
219
+ action.moneyEffects.some((effect) =>
220
+ [...effect.from, ...effect.to].some((endpoint) =>
221
+ sameAccount(account, endpoint),
222
+ ),
223
+ ),
224
+ ),
225
+ )
226
+ )
227
+ structures.push("held_funds");
228
+ if (reserved) structures.push("reserved_payment");
229
+ if ((hasParent && calendar(instrument)) || linked.some(calendar))
230
+ structures.push("payment_calendar");
231
+ if (funding) structures.push("funding_progress");
232
+ if (
233
+ cancellation.some((action) =>
234
+ actions.some(
235
+ (alternative) =>
236
+ alternative !== action &&
237
+ alternative.from.some((state) => action.from.includes(state)) &&
238
+ (timed(action) || timed(alternative)),
239
+ ),
240
+ )
241
+ )
242
+ structures.push("cancellation_review");
243
+ if (
244
+ actions.some(
245
+ (action) =>
246
+ action.subject?.adapters.length ||
247
+ action.prerequisites.some(
248
+ (requirement) => requirement.kind === "evidence",
249
+ ),
250
+ )
251
+ )
252
+ structures.push("external_checks");
253
+ if (instrument.reports.length) structures.push("report_view");
254
+ instrument.structures = structures;
255
+ }