@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.
Files changed (41) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/field-value.d.ts +5 -0
  3. package/dist/field-value.d.ts.map +1 -0
  4. package/dist/field-value.js +85 -0
  5. package/dist/field-value.js.map +1 -0
  6. package/dist/index.d.ts +1 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/object-contract.d.ts +35 -12
  11. package/dist/object-contract.d.ts.map +1 -1
  12. package/dist/object-contract.js +33 -91
  13. package/dist/object-contract.js.map +1 -1
  14. package/dist/object-semantics-schema.d.ts +5565 -0
  15. package/dist/object-semantics-schema.d.ts.map +1 -0
  16. package/dist/object-semantics-schema.js +119 -0
  17. package/dist/object-semantics-schema.js.map +1 -0
  18. package/dist/object-semantics.d.ts +6 -0
  19. package/dist/object-semantics.d.ts.map +1 -0
  20. package/dist/object-semantics.js +279 -0
  21. package/dist/object-semantics.js.map +1 -0
  22. package/dist/object-structures.d.ts +4 -0
  23. package/dist/object-structures.d.ts.map +1 -0
  24. package/dist/object-structures.js +134 -0
  25. package/dist/object-structures.js.map +1 -0
  26. package/dist/schema.d.ts +205 -0
  27. package/dist/schema.d.ts.map +1 -1
  28. package/dist/schema.js +5 -1
  29. package/dist/schema.js.map +1 -1
  30. package/dist/validation.js +1 -1
  31. package/dist/validation.js.map +1 -1
  32. package/package.json +2 -2
  33. package/spec/udl.schema.json +296 -245
  34. package/src/field-value.ts +87 -0
  35. package/src/index.ts +1 -0
  36. package/src/object-contract.ts +87 -99
  37. package/src/object-semantics-schema.ts +148 -0
  38. package/src/object-semantics.ts +378 -0
  39. package/src/object-structures.ts +255 -0
  40. package/src/schema.ts +5 -1
  41. 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 "./object-contract.js";
22
+ import { udlFieldValueSchema } from "./field-value.js";
23
23
 
24
24
  export const RESERVED_OBJECT_NAMES = [
25
25
  "objectId",