@hyperscale0/udl 4.5.1 → 4.6.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.
- package/CHANGELOG.md +11 -0
- package/dist/field-value.d.ts +5 -0
- package/dist/field-value.d.ts.map +1 -0
- package/dist/field-value.js +85 -0
- package/dist/field-value.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/object-contract.d.ts +35 -12
- package/dist/object-contract.d.ts.map +1 -1
- package/dist/object-contract.js +33 -91
- package/dist/object-contract.js.map +1 -1
- package/dist/object-semantics-schema.d.ts +5565 -0
- package/dist/object-semantics-schema.d.ts.map +1 -0
- package/dist/object-semantics-schema.js +119 -0
- package/dist/object-semantics-schema.js.map +1 -0
- package/dist/object-semantics.d.ts +6 -0
- package/dist/object-semantics.d.ts.map +1 -0
- package/dist/object-semantics.js +279 -0
- package/dist/object-semantics.js.map +1 -0
- package/dist/object-structures.d.ts +4 -0
- package/dist/object-structures.d.ts.map +1 -0
- package/dist/object-structures.js +134 -0
- package/dist/object-structures.js.map +1 -0
- package/dist/schema.d.ts +205 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +5 -1
- package/dist/schema.js.map +1 -1
- package/dist/validation.js +1 -1
- package/dist/validation.js.map +1 -1
- package/package.json +2 -2
- package/spec/udl.schema.json +296 -245
- package/src/field-value.ts +87 -0
- package/src/index.ts +1 -0
- package/src/object-contract.ts +87 -99
- package/src/object-semantics-schema.ts +148 -0
- package/src/object-semantics.ts +378 -0
- package/src/object-structures.ts +255 -0
- package/src/schema.ts +5 -1
- package/src/validation.ts +1 -1
|
@@ -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
|
+
}
|
package/src/schema.ts
CHANGED
|
@@ -40,6 +40,7 @@ export const udlFamilySchema = z.strictObject({
|
|
|
40
40
|
|
|
41
41
|
const fieldBase = {
|
|
42
42
|
name,
|
|
43
|
+
label: text.optional(),
|
|
43
44
|
optional: z.literal(true).optional(),
|
|
44
45
|
description: text.optional(),
|
|
45
46
|
sensitive: z.literal(true).optional(),
|
|
@@ -226,6 +227,7 @@ export type AttachmentPartyBinding = z.infer<
|
|
|
226
227
|
>;
|
|
227
228
|
export const udlObjectAttachmentSchema = z.strictObject({
|
|
228
229
|
name,
|
|
230
|
+
title: text.optional(),
|
|
229
231
|
parent: name.optional(),
|
|
230
232
|
instrument: instrumentId,
|
|
231
233
|
parties: z.record(name, attachmentPartyBindingSchema),
|
|
@@ -413,6 +415,7 @@ const clock = z.strictObject({
|
|
|
413
415
|
export const udlActionSchema = z.strictObject({
|
|
414
416
|
allowZero: z.literal(true).optional(),
|
|
415
417
|
summary: text,
|
|
418
|
+
title: text.optional(),
|
|
416
419
|
publicAction: name.optional(),
|
|
417
420
|
expansionLimit: z.literal(8192).optional(),
|
|
418
421
|
reminder: z
|
|
@@ -482,6 +485,7 @@ export const udlActionSchema = z.strictObject({
|
|
|
482
485
|
});
|
|
483
486
|
export const udlLifecycleSchema = z.strictObject({
|
|
484
487
|
states: z.array(name).min(1).max(64),
|
|
488
|
+
labels: z.record(name, text).optional(),
|
|
485
489
|
initial: name,
|
|
486
490
|
transitions: z.record(
|
|
487
491
|
name,
|
|
@@ -551,7 +555,7 @@ export function sameObjectField(
|
|
|
551
555
|
Object.fromEntries(
|
|
552
556
|
Object.entries(field).filter(
|
|
553
557
|
([key, value]) =>
|
|
554
|
-
!["name", "description", "optional"].includes(key) &&
|
|
558
|
+
!["name", "label", "description", "optional"].includes(key) &&
|
|
555
559
|
value !== undefined,
|
|
556
560
|
),
|
|
557
561
|
),
|
package/src/validation.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { issue, type UdlIssue } from "./diagnostics.js";
|
|
|
19
19
|
import { validateReportDefinition } from "./reporting-validation.js";
|
|
20
20
|
import { analyzeInstrumentFinance } from "./finance.js";
|
|
21
21
|
import { UDL_LIMITS } from "./limits.js";
|
|
22
|
-
import { udlFieldValueSchema } from "./
|
|
22
|
+
import { udlFieldValueSchema } from "./field-value.js";
|
|
23
23
|
|
|
24
24
|
export const RESERVED_OBJECT_NAMES = [
|
|
25
25
|
"objectId",
|