@hyperscale0/udl 3.2.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +1 -1
  3. package/dist/diagnostics.d.ts +1 -1
  4. package/dist/diagnostics.d.ts.map +1 -1
  5. package/dist/diagnostics.js +11 -1
  6. package/dist/diagnostics.js.map +1 -1
  7. package/dist/evolution.d.ts.map +1 -1
  8. package/dist/evolution.js +5 -0
  9. package/dist/evolution.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +2 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/instrument-references.d.ts.map +1 -1
  15. package/dist/instrument-references.js +21 -1
  16. package/dist/instrument-references.js.map +1 -1
  17. package/dist/object-contract.d.ts +144 -0
  18. package/dist/object-contract.d.ts.map +1 -0
  19. package/dist/object-contract.js +279 -0
  20. package/dist/object-contract.js.map +1 -0
  21. package/dist/reporting.d.ts +0 -1
  22. package/dist/reporting.d.ts.map +1 -1
  23. package/dist/reporting.js +0 -1
  24. package/dist/reporting.js.map +1 -1
  25. package/dist/schema.d.ts +1815 -126
  26. package/dist/schema.d.ts.map +1 -1
  27. package/dist/schema.js +100 -33
  28. package/dist/schema.js.map +1 -1
  29. package/dist/validation.d.ts +5 -2
  30. package/dist/validation.d.ts.map +1 -1
  31. package/dist/validation.js +403 -146
  32. package/dist/validation.js.map +1 -1
  33. package/docs/README.md +66 -17
  34. package/package.json +4 -4
  35. package/spec/README.md +66 -17
  36. package/spec/{darb.udl.json → cars.udl.json} +7 -7
  37. package/spec/udl.schema.json +2876 -6516
  38. package/src/diagnostics.ts +27 -1
  39. package/src/evolution.ts +8 -0
  40. package/src/index.ts +3 -0
  41. package/src/instrument-references.ts +25 -1
  42. package/src/object-contract.ts +464 -0
  43. package/src/reporting.ts +0 -1
  44. package/src/schema.ts +155 -33
  45. package/src/validation.ts +635 -174
package/src/validation.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  udlDocumentSchema,
3
+ subjectPartyRoles,
4
+ sameObjectField,
3
5
  MAX_ACTION_EXPANSION,
4
6
  type UdlDocument,
5
7
  type UdlField,
@@ -7,11 +9,59 @@ import {
7
9
  type UdlValue,
8
10
  type UdlCalculation,
9
11
  type UdlSelection,
12
+ type UdlAction,
13
+ type UdlActionSubject,
14
+ type UdlSubjectRequirement,
15
+ type UdlObjectField,
16
+ type UdlObjectKind,
10
17
  } from "./schema.js";
11
18
  import { issue, type UdlIssue } from "./diagnostics.js";
12
19
  import { validateReportDefinition } from "./reporting-validation.js";
13
20
  import { analyzeInstrumentFinance } from "./finance.js";
14
21
 
22
+ export const RESERVED_OBJECT_NAMES = [
23
+ "objectId",
24
+ "externalId",
25
+ "revision",
26
+ "kind",
27
+ "tenantId",
28
+ "productId",
29
+ ] as const;
30
+ const reservedObjectNamesSet = new Set<string>(RESERVED_OBJECT_NAMES);
31
+ function hasParty(
32
+ document: UdlDocument,
33
+ instrument: UdlInstrument | undefined,
34
+ name: string,
35
+ ): boolean {
36
+ if (document.parties[name]) return true;
37
+ if (!instrument?.subject) return false;
38
+ return (
39
+ document.objects
40
+ .find((kind) => kind.id === instrument.subject)
41
+ ?.attachments.some(
42
+ (attachment) =>
43
+ (attachment.instrument === instrument.id ||
44
+ instrument.id.startsWith(`${attachment.instrument}_`)) &&
45
+ Object.values(attachment.parties).some((binding) =>
46
+ "role" in binding ? binding.role === name : binding.party === name,
47
+ ),
48
+ ) ?? false
49
+ );
50
+ }
51
+
52
+ function moneyParty(
53
+ document: UdlDocument,
54
+ instrument: UdlInstrument | undefined,
55
+ name: string,
56
+ ): boolean {
57
+ return (
58
+ hasParty(document, instrument, name) &&
59
+ (document.parties[name]?.kind === "business" ||
60
+ (!document.parties[name] && !!instrument?.subject) ||
61
+ (!instrument?.subject && document.parties[name]?.kind === "person"))
62
+ );
63
+ }
64
+
15
65
  export type UdlValidationResult =
16
66
  | { ok: true; value: UdlDocument }
17
67
  | { ok: false; issues: readonly UdlIssue[] };
@@ -58,7 +108,9 @@ const overlap = (a: string | string[], b: string | string[]) =>
58
108
  targetIds(a).some((id) => targetIds(b).includes(id));
59
109
 
60
110
  /** Union paths retain only fields with compatible types on every member. */
61
- function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
111
+ function commonField(
112
+ fields: (UdlField | UdlObjectField | undefined)[],
113
+ ): UdlField | undefined {
62
114
  const first = fields[0];
63
115
  if (
64
116
  !first ||
@@ -71,6 +123,16 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
71
123
  )
72
124
  return;
73
125
  if (first.type === "ref") {
126
+ if (
127
+ fields.some(
128
+ (field) =>
129
+ field?.type !== "ref" ||
130
+ field.targetKind !== first.targetKind ||
131
+ JSON.stringify(field.targetFamily) !==
132
+ JSON.stringify(first.targetFamily),
133
+ )
134
+ )
135
+ return;
74
136
  const targets = [
75
137
  ...new Set(
76
138
  fields.flatMap((field) =>
@@ -78,7 +140,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
78
140
  ),
79
141
  ),
80
142
  ];
81
- return { ...first, target: targets.length === 1 ? targets[0]! : targets };
143
+ return {
144
+ ...first,
145
+ targetKind: first.targetKind,
146
+ target: targets.length === 1 ? targets[0]! : targets,
147
+ ...(first.targetFamily ? { targetFamily: first.targetFamily } : {}),
148
+ } as UdlField;
82
149
  }
83
150
  if (
84
151
  first.type === "account" &&
@@ -88,7 +155,6 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
88
155
  field.book !== first.book ||
89
156
  field.owner !== first.owner ||
90
157
  field.key !== first.key ||
91
- field.external !== first.external ||
92
158
  field.contra !== first.contra,
93
159
  )
94
160
  )
@@ -109,11 +175,12 @@ function commonField(fields: (UdlField | undefined)[]): UdlField | undefined {
109
175
  (field) =>
110
176
  field?.type !== "list" ||
111
177
  field.item !== first.item ||
178
+ field.targetKind !== first.targetKind ||
112
179
  field.target !== first.target,
113
180
  )
114
181
  )
115
182
  return;
116
- return first;
183
+ return first as UdlField;
117
184
  }
118
185
 
119
186
  export function resolveField(
@@ -121,8 +188,71 @@ export function resolveField(
121
188
  instrument: UdlInstrument,
122
189
  path: string,
123
190
  input: readonly UdlField[] = [],
191
+ action?: UdlAction | UdlActionSubject,
124
192
  ): UdlField | undefined {
125
- return resolvePath(document, instrument, path, input, new Map());
193
+ return resolvePath(document, instrument, path, input, new Map(), action);
194
+ }
195
+
196
+ /** A subject expression names a requirement; objectField names its stored metadata. */
197
+ export function resolveSubjectRequirement(
198
+ instrument: UdlInstrument,
199
+ name: string,
200
+ action?: UdlAction | UdlActionSubject,
201
+ ): UdlSubjectRequirement | undefined {
202
+ const requirements = action
203
+ ? "requirements" in action
204
+ ? action.requirements
205
+ : action.subject?.requirements
206
+ : Object.values(instrument.actions).flatMap(
207
+ (item) => item.subject?.requirements ?? [],
208
+ );
209
+ return requirements?.find((requirement) => requirement.field.name === name);
210
+ }
211
+
212
+ function resolveReferencePath(
213
+ document: UdlDocument,
214
+ field: Extract<UdlField, { type: "ref" }>,
215
+ suffix: string,
216
+ cache: Map<string, UdlField | undefined>,
217
+ ): UdlField | undefined {
218
+ return commonField(
219
+ targetIds(field.target).map((id) => {
220
+ const key = `${field.targetKind}:${id}:${suffix}`;
221
+ if (!cache.has(key)) {
222
+ const object =
223
+ field.targetKind === "object" &&
224
+ document.objects.find((item) => item.id === id);
225
+ const instrument =
226
+ field.targetKind === "instrument" &&
227
+ document.instruments.find((item) => item.id === id);
228
+ cache.set(
229
+ key,
230
+ object
231
+ ? resolveObjectPath(document, object, suffix, cache)
232
+ : instrument
233
+ ? resolvePath(document, instrument, `self.${suffix}`, [], cache)
234
+ : undefined,
235
+ );
236
+ }
237
+ return cache.get(key);
238
+ }),
239
+ );
240
+ }
241
+
242
+ function resolveObjectPath(
243
+ document: UdlDocument,
244
+ object: UdlObjectKind,
245
+ path: string,
246
+ cache: Map<string, UdlField | undefined>,
247
+ ): UdlField | undefined {
248
+ const parts = path.split(".");
249
+ const key = parts.shift();
250
+ if (!key) return;
251
+ const field = object.fields.find((f) => f.name === key);
252
+ if (!field) return;
253
+ if (parts.length === 0) return field as UdlField;
254
+ if (field.type !== "ref") return;
255
+ return resolveReferencePath(document, field, parts.join("."), cache);
126
256
  }
127
257
 
128
258
  function resolvePath(
@@ -131,30 +261,73 @@ function resolvePath(
131
261
  path: string,
132
262
  input: readonly UdlField[],
133
263
  cache: Map<string, UdlField | undefined>,
264
+ action?: UdlAction | UdlActionSubject,
134
265
  ): UdlField | undefined {
135
266
  const parts = path.split(".");
136
267
  const root = parts.shift();
137
268
  const key = parts.shift();
138
269
  if (!key)
139
270
  return root === "self"
140
- ? { name: "id", type: "ref", target: instrument.id }
271
+ ? {
272
+ name: "id",
273
+ type: "ref",
274
+ targetKind: "instrument",
275
+ target: instrument.id,
276
+ }
141
277
  : undefined;
142
278
  if (root === "party") {
143
- if (!document.parties[key]) return;
279
+ if (!moneyParty(document, instrument, key)) return;
144
280
  if (parts.length === 0)
145
281
  return { name: key, type: "account", owner: key, book: "cash" };
146
282
  if (parts.length === 1 && ["balance", "reserved"].includes(parts[0]!))
147
283
  return { name: parts[0]!, type: "money" };
148
284
  return;
149
285
  }
286
+ if (root === "self" && key === "subject" && instrument.subject) {
287
+ if (parts.length === 0)
288
+ return {
289
+ name: "subject",
290
+ type: "ref",
291
+ targetKind: "object",
292
+ target: instrument.subject,
293
+ };
294
+ const target = document.objects.find((o) => o.id === instrument.subject);
295
+ if (!target) return;
296
+ const suffix = parts.join(".");
297
+ const cacheKey = `obj:${instrument.subject}:${suffix}`;
298
+ if (!cache.has(cacheKey))
299
+ cache.set(cacheKey, resolveObjectPath(document, target, suffix, cache));
300
+ return cache.get(cacheKey);
301
+ }
150
302
  if (root === "self" && parts.length === 0) {
151
- if (key === "id") return { name: key, type: "ref", target: instrument.id };
303
+ if (key === "id")
304
+ return {
305
+ name: key,
306
+ type: "ref",
307
+ targetKind: "instrument",
308
+ target: instrument.id,
309
+ };
152
310
  if (key === "now" || key === "createdAt")
153
311
  return { name: key, type: "date" };
154
312
  if (key === "productRevision") return { name: key, type: "text" };
155
313
  if (key === "status")
156
314
  return { name: key, type: "enum", values: instrument.lifecycle.states };
157
315
  }
316
+ if (root === "subject") {
317
+ const requirement = resolveSubjectRequirement(instrument, key, action);
318
+ if (!requirement || !instrument.subject) return;
319
+ const object = document.objects.find(
320
+ (item) => item.id === instrument.subject,
321
+ );
322
+ if (!object) return;
323
+ return resolveObjectPath(
324
+ document,
325
+ object,
326
+ [requirement.objectField ?? requirement.field.name, ...parts].join("."),
327
+ cache,
328
+ );
329
+ }
330
+
158
331
  const scope = instrument;
159
332
  let field = (
160
333
  root === "self" ? instrument.fields : root === "input" ? input : []
@@ -180,18 +353,11 @@ function resolvePath(
180
353
  values: ["reserved", "posted", "settled", "reversed", "voided"],
181
354
  };
182
355
  if (field?.type !== "ref") return;
183
- return commonField(
184
- targetIds(field.target).map((id) => {
185
- const target = document.instruments.find(
186
- (instrument) => instrument.id === id,
187
- );
188
- if (!target) return;
189
- const suffix = "self." + parts.slice(index).join(".");
190
- const key = `${id}:${suffix}`;
191
- if (!cache.has(key))
192
- cache.set(key, resolvePath(document, target, suffix, [], cache));
193
- return cache.get(key);
194
- }),
356
+ return resolveReferencePath(
357
+ document,
358
+ field,
359
+ parts.slice(index).join("."),
360
+ cache,
195
361
  );
196
362
  }
197
363
  return field;
@@ -238,10 +404,237 @@ export function validateUdl(value: unknown): UdlValidationResult {
238
404
  seen.add(v);
239
405
  }
240
406
  };
407
+ if (document.objects.length === 0 && document.instruments.length === 0)
408
+ add("$", "document requires at least one object or instrument", "UDL2002");
409
+
410
+ duplicate(
411
+ document.objects.map((o) => o.id),
412
+ "$.objects",
413
+ );
241
414
  duplicate(
242
415
  document.instruments.map((i) => i.id),
243
416
  "$.instruments",
244
417
  );
418
+
419
+ for (const name of subjectPartyRoles)
420
+ if (document.parties[name])
421
+ add(
422
+ `$.parties.${name}`,
423
+ `${name} is a reserved subject role`,
424
+ "party_name_reserved",
425
+ );
426
+
427
+ const byObjectId = new Map(document.objects.map((o) => [o.id, o]));
428
+ const byId = new Map(document.instruments.map((i) => [i.id, i]));
429
+
430
+ const checkFields = (
431
+ fields: readonly (UdlField | UdlObjectField)[],
432
+ where: string,
433
+ isObject = false,
434
+ instrument?: UdlInstrument,
435
+ ) => {
436
+ duplicate(
437
+ fields.map((f) => f.name),
438
+ where,
439
+ );
440
+ for (const f of fields) {
441
+ if (isObject) {
442
+ if (reservedObjectNamesSet.has(f.name))
443
+ add(where, `${f.name} is a reserved object name`);
444
+ } else {
445
+ if (f.type === "account" && f.contra && f.book !== "claim")
446
+ add(where, "contra accounts require the claim book");
447
+ if (
448
+ ["id", "status", "createdAt", "now", "productRevision"].includes(
449
+ f.name,
450
+ )
451
+ )
452
+ add(where, `${f.name} is a sealed instance field`);
453
+ if (
454
+ f.type === "account" &&
455
+ f.owner !== "self" &&
456
+ !moneyParty(document, instrument, f.owner)
457
+ )
458
+ add(
459
+ where,
460
+ `${f.name} needs an eligible money party as owner`,
461
+ hasParty(document, instrument, f.owner)
462
+ ? "party_kind_mismatch"
463
+ : "subject_party_unbound",
464
+ );
465
+ }
466
+ if (f.type === "text") {
467
+ if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
468
+ add(where, "text minimum exceeds maximum");
469
+ if (f.pattern) {
470
+ try {
471
+ new RegExp(f.pattern);
472
+ } catch {
473
+ add(where, "invalid text character class");
474
+ }
475
+ }
476
+ }
477
+ if (f.type === "ref") {
478
+ duplicate(targetIds(f.target), where);
479
+ for (const id of targetIds(f.target)) {
480
+ const exists =
481
+ f.targetKind === "object" ? byObjectId.has(id) : byId.has(id);
482
+ if (!exists) add(where, `unknown reference target ${id}`, "UDL5001");
483
+ }
484
+ if (f.targetFamily) {
485
+ if (f.targetKind !== "instrument") {
486
+ add(
487
+ where,
488
+ "targetFamily requires an instrument reference target",
489
+ "UDL5001",
490
+ );
491
+ } else {
492
+ for (const id of targetIds(f.target)) {
493
+ const targetInst = byId.get(id);
494
+ if (
495
+ targetInst &&
496
+ (!targetInst.family ||
497
+ targetInst.family.module !== f.targetFamily.module ||
498
+ targetInst.family.exportPath !== f.targetFamily.exportPath ||
499
+ targetInst.family.revision !== f.targetFamily.revision)
500
+ ) {
501
+ add(
502
+ where,
503
+ `targetFamily does not match target instrument ${id} family`,
504
+ "UDL5001",
505
+ );
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ if (f.type === "enum") {
512
+ duplicate(f.values, where);
513
+ if (f.value !== undefined && !f.values.includes(f.value))
514
+ add(where, `${f.name} constant is outside its enum`);
515
+ }
516
+ if (f.type === "list") {
517
+ const validRefList =
518
+ f.item === "ref" &&
519
+ f.targetKind &&
520
+ (f.targetKind === "object"
521
+ ? byObjectId.has(f.target ?? "")
522
+ : byId.has(f.target ?? ""));
523
+ const nonRefTarget =
524
+ f.item !== "ref" &&
525
+ (f.targetKind !== undefined || f.target !== undefined);
526
+ if (!validRefList && (f.item === "ref" || nonRefTarget))
527
+ add(
528
+ where,
529
+ `${f.name} needs a target exactly when its items are references`,
530
+ );
531
+ if (f.targetFamily) {
532
+ if (f.item !== "ref" || f.targetKind !== "instrument") {
533
+ add(
534
+ where,
535
+ "targetFamily requires an instrument reference target",
536
+ "UDL5001",
537
+ );
538
+ } else if (f.target) {
539
+ const targetInst = byId.get(f.target);
540
+ if (
541
+ targetInst &&
542
+ (!targetInst.family ||
543
+ targetInst.family.module !== f.targetFamily.module ||
544
+ targetInst.family.exportPath !== f.targetFamily.exportPath ||
545
+ targetInst.family.revision !== f.targetFamily.revision)
546
+ ) {
547
+ add(
548
+ where,
549
+ `targetFamily does not match target instrument ${f.target} family`,
550
+ "UDL5001",
551
+ );
552
+ }
553
+ }
554
+ }
555
+ }
556
+ if (
557
+ (f.type === "integer" || f.type === "money") &&
558
+ f.minimum !== undefined &&
559
+ f.maximum !== undefined &&
560
+ BigInt(f.minimum) > BigInt(f.maximum)
561
+ )
562
+ add(where, `${f.name} minimum exceeds maximum`);
563
+ if (
564
+ (f.type === "integer" || f.type === "money") &&
565
+ f.value !== undefined &&
566
+ ((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
567
+ (f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
568
+ )
569
+ add(where, `${f.name} constant violates its bounds`);
570
+ }
571
+ };
572
+
573
+ for (const [index, obj] of document.objects.entries()) {
574
+ const base = `$.objects[${index}]`;
575
+ if (reservedObjectNamesSet.has(obj.id))
576
+ add(base, `${obj.id} is a reserved object name`);
577
+ duplicate(
578
+ obj.attachments.map((attachment) => attachment.name),
579
+ `${base}.attachments`,
580
+ );
581
+ duplicate(
582
+ obj.attachments.map((attachment) => attachment.instrument),
583
+ `${base}.attachments`,
584
+ );
585
+ for (const [attachmentIndex, attachment] of obj.attachments.entries()) {
586
+ for (const [parameter, binding] of Object.entries(attachment.parties))
587
+ if ("party" in binding && !document.parties[binding.party])
588
+ add(
589
+ `${base}.attachments[${attachmentIndex}].parties.${parameter}`,
590
+ `unknown declared party ${binding.party}`,
591
+ "subject_party_unbound",
592
+ );
593
+ if (byId.get(attachment.instrument)?.subject !== obj.id)
594
+ add(
595
+ `${base}.attachments`,
596
+ `${attachment.instrument} must attach to ${obj.id}`,
597
+ );
598
+ }
599
+ duplicate(obj.authoredFields, `${base}.authoredFields`);
600
+ duplicate(obj.columns, `${base}.columns`);
601
+ checkFields(obj.fields, `${base}.fields`, true);
602
+ for (let i = 0; i < obj.authoredFields.length; i++) {
603
+ const af = obj.authoredFields[i]!;
604
+ if (reservedObjectNamesSet.has(af))
605
+ add(`${base}.authoredFields`, `${af} is a reserved object name`);
606
+ if (!obj.fields.some((f) => f.name === af))
607
+ add(
608
+ `${base}.authoredFields`,
609
+ `authored field ${af} is not in declared fields`,
610
+ "subject_field_unknown",
611
+ );
612
+ if (obj.fields[i]?.name !== af)
613
+ add(
614
+ base,
615
+ `authored field ${af} must precede attached requirements in fields`,
616
+ );
617
+ }
618
+ for (const col of obj.columns) {
619
+ if (reservedObjectNamesSet.has(col))
620
+ add(`${base}.columns`, `${col} is a reserved object name`);
621
+ if (!obj.fields.some((f) => f.name === col))
622
+ add(
623
+ `${base}.columns`,
624
+ `column ${col} is not a declared field`,
625
+ "subject_field_unknown",
626
+ );
627
+ }
628
+ const pubActions = document.instruments
629
+ .filter((i) => i.subject === obj.id)
630
+ .flatMap((i) =>
631
+ Object.values(i.actions).flatMap((a) =>
632
+ a.publicAction ? [a.publicAction] : [],
633
+ ),
634
+ );
635
+ duplicate(pubActions, `$.objects.${obj.id}.publicActions`);
636
+ }
637
+
245
638
  const reportIds = new Set<string>();
246
639
  for (const [index, instrument] of document.instruments.entries()) {
247
640
  for (const report of instrument.reports ?? []) {
@@ -259,19 +652,34 @@ export function validateUdl(value: unknown): UdlValidationResult {
259
652
  }
260
653
  }
261
654
  }
262
- const byId = new Map(document.instruments.map((i) => [i.id, i]));
655
+
263
656
  const calls = new Map<string, { targets: string[]; count: number }[]>();
264
657
  for (const [index, inst] of document.instruments.entries()) {
265
658
  const base = `$.instruments[${index}]`;
266
- const field = (p: string, input: readonly UdlField[] = []) =>
267
- resolveField(document, inst, p, input);
659
+ if (inst.subject && !byObjectId.has(inst.subject))
660
+ add(base, `unknown subject object ${inst.subject}`);
661
+ const field = (
662
+ p: string,
663
+ input: readonly UdlField[] = [],
664
+ action?: UdlAction | UdlActionSubject,
665
+ ) => resolveField(document, inst, p, input, action);
268
666
  const expect = (
269
667
  p: string,
270
668
  type: UdlField["type"],
271
669
  where: string,
272
670
  input: readonly UdlField[] = [],
671
+ action?: UdlAction | UdlActionSubject,
273
672
  ) => {
274
- const found = field(p, input);
673
+ const party = p.startsWith("party.") ? p.split(".")[1]! : undefined;
674
+ if (party && !moneyParty(document, inst, party))
675
+ add(
676
+ where,
677
+ `${party} cannot supply a money account`,
678
+ hasParty(document, inst, party)
679
+ ? "party_kind_mismatch"
680
+ : "subject_party_unbound",
681
+ );
682
+ const found = field(p, input, action);
275
683
  if (found?.type !== type)
276
684
  add(where, `${p} must name a ${type} field`, "UDL5001");
277
685
  return found;
@@ -281,9 +689,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
281
689
  type: UdlField["type"],
282
690
  where: string,
283
691
  input: readonly UdlField[] = [],
692
+ action?: UdlAction | UdlActionSubject,
284
693
  ) => {
285
694
  if ("field" in v) {
286
- expect(v.field, type, where, input);
695
+ expect(v.field, type, where, input, action);
287
696
  return;
288
697
  }
289
698
  const valid =
@@ -299,82 +708,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
299
708
  : typeof v.literal === "string";
300
709
  if (!valid) add(where, `literal must have type ${type}`);
301
710
  };
302
- const checkFields = (fields: readonly UdlField[], where: string) => {
303
- duplicate(
304
- fields.map((f) => f.name),
305
- where,
306
- );
307
- for (const f of fields) {
308
- if (
309
- f.type === "account" &&
310
- ((f.contra && f.book !== "claim") ||
311
- (f.external && (f.book !== "cash" || f.owner === "self")))
312
- )
313
- add(
314
- where,
315
- "contra accounts require the claim book; external accounts require a cash party owner",
316
- );
317
- if (
318
- ["id", "status", "createdAt", "now", "productRevision"].includes(
319
- f.name,
320
- )
321
- )
322
- add(where, `${f.name} is a sealed instance field`);
323
- if (f.type === "text") {
324
- if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
325
- add(where, "text minimum exceeds maximum");
326
- if (f.pattern) {
327
- try {
328
- new RegExp(f.pattern);
329
- } catch {
330
- add(where, "invalid text character class");
331
- }
332
- }
333
- }
334
- if (f.type === "ref") {
335
- duplicate(targetIds(f.target), where);
336
- for (const id of targetIds(f.target))
337
- if (!byId.has(id))
338
- add(where, `unknown reference target ${id}`, "UDL5001");
339
- }
340
- if (
341
- f.type === "account" &&
342
- f.owner !== "self" &&
343
- !document.parties[f.owner]
344
- )
345
- add(where, `${f.name} needs self or a declared party as owner`);
346
- if (f.type === "enum") {
347
- duplicate(f.values, where);
348
- if (f.value !== undefined && !f.values.includes(f.value))
349
- add(where, `${f.name} constant is outside its enum`);
350
- }
351
- if (
352
- f.type === "list" &&
353
- (f.item === "ref"
354
- ? !f.target || !byId.has(f.target)
355
- : f.target !== undefined)
356
- )
357
- add(
358
- where,
359
- `${f.name} needs a target exactly when its items are references`,
360
- );
361
- if (
362
- (f.type === "integer" || f.type === "money") &&
363
- f.minimum !== undefined &&
364
- f.maximum !== undefined &&
365
- BigInt(f.minimum) > BigInt(f.maximum)
366
- )
367
- add(where, `${f.name} minimum exceeds maximum`);
368
- if (
369
- (f.type === "integer" || f.type === "money") &&
370
- f.value !== undefined &&
371
- ((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
372
- (f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum)))
373
- )
374
- add(where, `${f.name} constant violates its bounds`);
375
- }
376
- };
377
- checkFields(inst.fields, `${base}.fields`);
711
+ checkFields(inst.fields, `${base}.fields`, false, inst);
378
712
  const captures = new Set(
379
713
  Object.values(inst.actions).flatMap((a) => [
380
714
  ...a.moves.flatMap((m) =>
@@ -409,9 +743,24 @@ export function validateUdl(value: unknown): UdlValidationResult {
409
743
  a &&
410
744
  b &&
411
745
  a.type === b.type &&
412
- (a.type !== "ref" || (b.type === "ref" && overlap(a.target, b.target)));
746
+ (a.type !== "ref" ||
747
+ (b.type === "ref" &&
748
+ a.targetKind === b.targetKind &&
749
+ overlap(a.target, b.target)));
413
750
  const anchor = field(selection.anchor, input);
414
751
  for (const target of targets) {
752
+ if (
753
+ selection.family &&
754
+ (!target.family ||
755
+ target.family.module !== selection.family.module ||
756
+ target.family.exportPath !== selection.family.exportPath ||
757
+ target.family.revision !== selection.family.revision)
758
+ )
759
+ add(
760
+ where,
761
+ `selection family does not match target instrument ${target.id} family`,
762
+ "UDL5001",
763
+ );
415
764
  if (
416
765
  selection.window &&
417
766
  target.fields.find((f) => f.name === selection.window!.field)
@@ -471,6 +820,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
471
820
  calculations: readonly UdlCalculation[],
472
821
  where: string,
473
822
  input: readonly UdlField[] = [],
823
+ act?: UdlAction,
474
824
  ) => {
475
825
  duplicate(
476
826
  calculations.map((c) => c.target),
@@ -484,7 +834,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
484
834
  const resultType =
485
835
  c.op === "at"
486
836
  ? (() => {
487
- const list = field(c.list, input);
837
+ const list = field(c.list, input, act);
488
838
  return list?.type === "list" ? list.item : "text";
489
839
  })()
490
840
  : (c.op === "aggregate" && c.measure === "count") ||
@@ -499,11 +849,11 @@ export function validateUdl(value: unknown): UdlValidationResult {
499
849
  const operands: UdlValue[] = [];
500
850
  const money = (v: UdlValue) => {
501
851
  operands.push(v);
502
- checkValue(v, arithmetic ? resultType : "money", where, input);
852
+ checkValue(v, arithmetic ? resultType : "money", where, input, act);
503
853
  };
504
854
  if (c.op === "at") {
505
- expect(c.list, "list", where, input);
506
- checkValue(c.position, "integer", where, input);
855
+ expect(c.list, "list", where, input, act);
856
+ checkValue(c.position, "integer", where, input, act);
507
857
  if ("literal" in c.position && Number(c.position.literal) < 1)
508
858
  add(where, "list positions start at one");
509
859
  operands.push({ field: c.list }, c.position);
@@ -522,15 +872,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
522
872
  money(c.amount);
523
873
  const type =
524
874
  "field" in c.numerator
525
- ? field(c.numerator.field, input)?.type
875
+ ? field(c.numerator.field, input, act)?.type
526
876
  : typeof c.numerator.literal === "number"
527
877
  ? "integer"
528
878
  : "money";
529
879
  if (type !== "integer" && type !== "money" && type !== "percent")
530
880
  add(where, "ratio weights must share a numeric type");
531
881
  else {
532
- checkValue(c.numerator, type, where, input);
533
- checkValue(c.denominator, type, where, input);
882
+ checkValue(c.numerator, type, where, input, act);
883
+ checkValue(c.denominator, type, where, input, act);
534
884
  }
535
885
  if (
536
886
  "literal" in c.denominator &&
@@ -546,13 +896,13 @@ export function validateUdl(value: unknown): UdlValidationResult {
546
896
  }
547
897
  if (c.op === "rate") {
548
898
  money(c.base);
549
- checkValue(c.bps, "percent", where, input);
899
+ checkValue(c.bps, "percent", where, input, act);
550
900
  operands.push(c.bps);
551
901
  }
552
902
  if (c.op === "multiply" || c.op === "divide") {
553
903
  money(c.amount);
554
904
  const count = c.op === "multiply" ? c.units : c.divisor;
555
- checkValue(count, "integer", where, input);
905
+ checkValue(count, "integer", where, input, act);
556
906
  operands.push(count);
557
907
  if (
558
908
  "literal" in count &&
@@ -566,8 +916,8 @@ export function validateUdl(value: unknown): UdlValidationResult {
566
916
  );
567
917
  }
568
918
  if (c.op === "shift") {
569
- checkValue(c.date, "date", where, input);
570
- checkValue(c.milliseconds, "duration", where, input);
919
+ checkValue(c.date, "date", where, input, act);
920
+ checkValue(c.milliseconds, "duration", where, input, act);
571
921
  operands.push(c.date, c.milliseconds);
572
922
  }
573
923
  dependencies.set(
@@ -628,13 +978,16 @@ export function validateUdl(value: unknown): UdlValidationResult {
628
978
  requirements: typeof inst.invariants,
629
979
  where: string,
630
980
  input: readonly UdlField[] = [],
981
+ act?: UdlAction,
631
982
  ) => {
632
983
  for (const req of requirements ?? []) {
633
984
  if (req.kind === "compare") {
634
985
  const left =
635
- "field" in req.left ? field(req.left.field, input) : undefined;
986
+ "field" in req.left ? field(req.left.field, input, act) : undefined;
636
987
  const right =
637
- "field" in req.right ? field(req.right.field, input) : undefined;
988
+ "field" in req.right
989
+ ? field(req.right.field, input, act)
990
+ : undefined;
638
991
  if (
639
992
  ("field" in req.left && !left) ||
640
993
  ("field" in req.right && !right)
@@ -646,11 +999,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
646
999
  (left.type !== right.type ||
647
1000
  (left.type === "ref" &&
648
1001
  right.type === "ref" &&
649
- !overlap(left.target, right.target)))
1002
+ (left.targetKind !== right.targetKind ||
1003
+ !overlap(left.target, right.target))))
650
1004
  )
651
1005
  add(where, "comparison operands have different types");
652
1006
  if (left && "literal" in req.right) {
653
- checkValue(req.right, left.type, where, input);
1007
+ checkValue(req.right, left.type, where, input, act);
654
1008
  if (
655
1009
  left.type === "enum" &&
656
1010
  !left.values.includes(String(req.right.literal))
@@ -658,7 +1012,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
658
1012
  add(where, "comparison names an undeclared enum value");
659
1013
  }
660
1014
  if (right && "literal" in req.left) {
661
- checkValue(req.left, right.type, where, input);
1015
+ checkValue(req.left, right.type, where, input, act);
662
1016
  if (
663
1017
  right.type === "enum" &&
664
1018
  !right.values.includes(String(req.left.literal))
@@ -666,9 +1020,10 @@ export function validateUdl(value: unknown): UdlValidationResult {
666
1020
  add(where, "comparison names an undeclared enum value");
667
1021
  }
668
1022
  } else if (req.kind === "state") {
669
- const target = expect(req.reference, "ref", where, input);
1023
+ const target = expect(req.reference, "ref", where, input, act);
670
1024
  if (
671
1025
  target?.type === "ref" &&
1026
+ target.targetKind === "instrument" &&
672
1027
  req.states.some((s) =>
673
1028
  targetIds(target.target).some(
674
1029
  (id) => !byId.get(id)?.lifecycle.states.includes(s),
@@ -678,28 +1033,27 @@ export function validateUdl(value: unknown): UdlValidationResult {
678
1033
  add(where, "reference gate names an undeclared state");
679
1034
  } else if (req.kind === "unique") {
680
1035
  for (const p of req.fields)
681
- if (!field(p, input)) add(where, `unknown identity field ${p}`);
682
- } else if (req.kind === "approval") {
683
- const target = expect(req.target, "ref", where, input);
684
- if (!document.parties[req.party])
685
- add(where, `unknown approving party ${req.party}`);
1036
+ if (!field(p, input, act))
1037
+ add(where, `unknown identity field ${p}`);
1038
+ } else if (req.kind === "evidence") {
1039
+ if (req.instruction) {
1040
+ expect(req.instruction, "text", where, input, act);
1041
+ if (!["confirmed", "rejected"].includes(req.result))
1042
+ add(where, "instruction evidence requires a terminal outcome");
1043
+ }
1044
+ const subject = field(req.subject, input, act);
686
1045
  if (
687
- req.action &&
688
- target?.type === "ref" &&
689
- targetIds(target.target).some(
690
- (id) => !byId.get(id)?.actions[req.action!],
1046
+ !subject ||
1047
+ !["account", "text", ...(req.instruction ? ["ref"] : [])].includes(
1048
+ subject.type,
691
1049
  )
692
1050
  )
693
- add(where, `unknown approved action ${req.action}`);
694
- } else if (req.kind === "evidence") {
695
- const subject = field(req.subject, input);
696
- if (!subject || !["account", "text"].includes(subject.type))
697
1051
  add(
698
1052
  where,
699
1053
  "evidence subject must be an account or text subject id",
700
1054
  );
701
1055
  } else if (req.kind === "hours") {
702
- expect(req.at, "date", where, input);
1056
+ expect(req.at, "date", where, input, act);
703
1057
  try {
704
1058
  new Intl.DateTimeFormat("en", { timeZone: req.timezone });
705
1059
  } catch {
@@ -723,6 +1077,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
723
1077
  req.measure === "count" ? "integer" : "money",
724
1078
  where,
725
1079
  input,
1080
+ act,
726
1081
  );
727
1082
  }
728
1083
  }
@@ -733,11 +1088,83 @@ export function validateUdl(value: unknown): UdlValidationResult {
733
1088
  const where = `${base}.actions.${actionName}`;
734
1089
  if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
735
1090
  add(where, "action needs a lifecycle transition", "UDL3001");
736
- checkFields(action.input, `${where}.input`);
1091
+ if (action.subject) {
1092
+ const subBase = `${where}.subject`;
1093
+ checkFields(
1094
+ action.subject.requirements.map((r) => ({
1095
+ ...r.field,
1096
+ name: r.objectField ?? r.field.name,
1097
+ })),
1098
+ `${subBase}.requirements`,
1099
+ true,
1100
+ );
1101
+ for (const req of action.subject.requirements) {
1102
+ if (req.field.optional)
1103
+ add(
1104
+ `${subBase}.requirements`,
1105
+ `subject requirement ${req.field.name} cannot be optional`,
1106
+ "UDL2002",
1107
+ );
1108
+ const name = req.objectField ?? req.field.name;
1109
+ if (reservedObjectNamesSet.has(name))
1110
+ add(subBase, `${name} is a reserved object name`);
1111
+ if (inst.subject) {
1112
+ const field = byObjectId
1113
+ .get(inst.subject)
1114
+ ?.fields.find((field) => field.name === name);
1115
+ if (!field || !sameObjectField(field, req.field))
1116
+ add(
1117
+ subBase,
1118
+ `object field ${name} must match requirement ${req.field.name}`,
1119
+ field ? "subject_field_conflict" : "subject_field_unknown",
1120
+ );
1121
+ }
1122
+ }
1123
+ duplicate(
1124
+ action.subject.adapters.map((a) => a.binding),
1125
+ `${subBase}.adapters`,
1126
+ );
1127
+ for (const adapter of action.subject.adapters) {
1128
+ if (adapter.snapshot) {
1129
+ checkFields(
1130
+ adapter.snapshot.requirements,
1131
+ `${subBase}.adapters.${adapter.binding}.requirements`,
1132
+ true,
1133
+ );
1134
+ for (const reqField of adapter.snapshot.requirements) {
1135
+ const objectName =
1136
+ adapter.renames?.[reqField.name] ?? reqField.name;
1137
+ const requirement = action.subject.requirements.find(
1138
+ (item) =>
1139
+ (item.objectField ?? item.field.name) === objectName &&
1140
+ sameObjectField(item.field, reqField),
1141
+ );
1142
+ if (!requirement)
1143
+ add(
1144
+ subBase,
1145
+ `adapter ${adapter.binding} requirement ${reqField.name} is absent from the action`,
1146
+ );
1147
+ if (reqField.optional)
1148
+ add(
1149
+ `${subBase}.adapters.${adapter.binding}`,
1150
+ `adapter requirement ${reqField.name} cannot be optional`,
1151
+ "UDL2002",
1152
+ );
1153
+ }
1154
+ }
1155
+ }
1156
+ }
1157
+ checkFields(action.input, `${where}.input`, false, inst);
737
1158
  if (action.reminder) {
738
- expect(action.reminder.installment, "ref", where, action.input);
739
- expect(action.reminder.recipient, "account", where, action.input);
740
- expect(action.reminder.dueAt, "date", where, action.input);
1159
+ expect(action.reminder.installment, "ref", where, action.input, action);
1160
+ expect(
1161
+ action.reminder.recipient,
1162
+ "account",
1163
+ where,
1164
+ action.input,
1165
+ action,
1166
+ );
1167
+ expect(action.reminder.dueAt, "date", where, action.input, action);
741
1168
  if (action.reminder.startHour >= action.reminder.endHour)
742
1169
  add(where, "reminder sending window is empty");
743
1170
  try {
@@ -773,12 +1200,18 @@ export function validateUdl(value: unknown): UdlValidationResult {
773
1200
  action.calculate ?? [],
774
1201
  `${where}.calculate`,
775
1202
  action.input,
1203
+ action,
1204
+ );
1205
+ checkRequirements(
1206
+ action.requires,
1207
+ `${where}.requires`,
1208
+ action.input,
1209
+ action,
776
1210
  );
777
- checkRequirements(action.requires, `${where}.requires`, action.input);
778
1211
  if (
779
1212
  typeof action.actor === "object" &&
780
1213
  "party" in action.actor &&
781
- !document.parties[action.actor.party]
1214
+ !hasParty(document, inst, action.actor.party)
782
1215
  )
783
1216
  add(where, "actor names an undeclared party");
784
1217
  if (
@@ -791,7 +1224,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
791
1224
  add(where, "clock action needs a due instant");
792
1225
  for (const clock of [action.due, action.deadline])
793
1226
  if (clock) {
794
- expect(clock.at, "date", where, action.input);
1227
+ expect(clock.at, "date", where, action.input, action);
795
1228
  if (clock.localDay) {
796
1229
  try {
797
1230
  new Intl.DateTimeFormat("en", {
@@ -803,10 +1236,28 @@ export function validateUdl(value: unknown): UdlValidationResult {
803
1236
  }
804
1237
  }
805
1238
  for (const move of action.moves) {
1239
+ if (
1240
+ move.operation === "internal_transfer.reserve" &&
1241
+ move.boundary &&
1242
+ !action.subject?.adapters.some(
1243
+ (binding) =>
1244
+ binding.binding === move.boundary!.adapter && binding.snapshot,
1245
+ )
1246
+ )
1247
+ add(
1248
+ where,
1249
+ "boundary reservation requires a retained adapter binding",
1250
+ );
806
1251
  if ("amount" in move) {
807
- checkValue(move.amount, "money", where, action.input);
808
- const from = expect(move.from, "account", where, action.input);
809
- const to = expect(move.to, "account", where, action.input);
1252
+ checkValue(move.amount, "money", where, action.input, action);
1253
+ const from = expect(
1254
+ move.from,
1255
+ "account",
1256
+ where,
1257
+ action.input,
1258
+ action,
1259
+ );
1260
+ const to = expect(move.to, "account", where, action.input, action);
810
1261
  if (
811
1262
  from?.type === "account" &&
812
1263
  to?.type === "account" &&
@@ -822,7 +1273,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
822
1273
  (from.key ?? "balance") === (to.key ?? "balance");
823
1274
  if (move.from === move.to || samePartyAccount)
824
1275
  add(where, "a transfer needs distinct accounts", "UDL4001");
825
- } else expect(move.transfer, "text", where, action.input);
1276
+ } else expect(move.transfer, "text", where, action.input, action);
826
1277
  }
827
1278
  duplicate(
828
1279
  action.moves.flatMap((m) =>
@@ -845,7 +1296,7 @@ export function validateUdl(value: unknown): UdlValidationResult {
845
1296
  inst.calculate.some((c) => c.target === key)
846
1297
  )
847
1298
  add(where, `cannot write immutable field ${key}`);
848
- else checkValue(v, target.type, where, action.input);
1299
+ else checkValue(v, target.type, where, action.input, action);
849
1300
  }
850
1301
  const checkArguments = (
851
1302
  targetId: string | undefined,
@@ -875,11 +1326,12 @@ export function validateUdl(value: unknown): UdlValidationResult {
875
1326
  add(where, "range binding requires an integer target input");
876
1327
  continue;
877
1328
  }
878
- checkValue(value, declared.type, where, action.input);
1329
+ checkValue(value, declared.type, where, action.input, action);
879
1330
  if (declared.type === "ref" && "field" in value) {
880
- const supplied = field(value.field, action.input);
1331
+ const supplied = field(value.field, action.input, action);
881
1332
  if (
882
1333
  supplied?.type !== "ref" ||
1334
+ supplied.targetKind !== declared.targetKind ||
883
1335
  !targetIds(supplied.target).every((id) =>
884
1336
  targetIds(declared.target).includes(id),
885
1337
  )
@@ -888,34 +1340,14 @@ export function validateUdl(value: unknown): UdlValidationResult {
888
1340
  }
889
1341
  }
890
1342
  };
891
- if (action.approval) {
892
- const reference = expect(
893
- action.approval.target,
894
- "ref",
895
- where,
896
- action.input,
897
- );
898
- if (
899
- reference?.type === "ref" &&
900
- targetIds(reference.target).some(
901
- (id) => !byId.get(id)?.actions[action.approval!.action],
902
- )
903
- )
904
- add(where, "approval target action does not exist");
905
- if (reference?.type === "ref")
906
- for (const id of targetIds(reference.target))
907
- checkArguments(id, action.approval.action, action.approval.input);
908
- expect(action.approval.expires, "date", where, action.input);
909
- if (!document.parties[action.approval.party])
910
- add(where, "approval needs a declared party");
911
- }
912
1343
  const targets: { targets: string[]; count: number }[] = [];
913
1344
  for (const call of action.invoke ?? []) {
914
- if (call.guard) checkRequirements([call.guard], where, action.input);
1345
+ if (call.guard)
1346
+ checkRequirements([call.guard], where, action.input, action);
915
1347
  const range = "instrument" in call ? call.range : undefined;
916
1348
  if (range) {
917
- checkValue(range.count, "integer", where, action.input);
918
- if (["self", "input", "party"].includes(range.bind))
1349
+ checkValue(range.count, "integer", where, action.input, action);
1350
+ if (["self", "input", "party", "subject"].includes(range.bind))
919
1351
  add(where, "range binding cannot shadow a reserved scope");
920
1352
  if (
921
1353
  "literal" in range.count &&
@@ -928,13 +1360,15 @@ export function validateUdl(value: unknown): UdlValidationResult {
928
1360
  if ("selection" in call)
929
1361
  checkSelection(call.selection, where, action.input);
930
1362
  const target =
931
- "reference" in call ? field(call.reference, action.input) : undefined;
1363
+ "reference" in call
1364
+ ? field(call.reference, action.input, action)
1365
+ : undefined;
932
1366
  const selected =
933
1367
  "instrument" in call
934
1368
  ? call.instrument
935
1369
  : "selection" in call
936
1370
  ? call.selection.instrument
937
- : target?.type === "ref"
1371
+ : target?.type === "ref" && target.targetKind === "instrument"
938
1372
  ? target.target
939
1373
  : undefined;
940
1374
  const ids = Array.isArray(selected)
@@ -950,8 +1384,35 @@ export function validateUdl(value: unknown): UdlValidationResult {
950
1384
  where,
951
1385
  "invocation references an existing instance and cannot create it again",
952
1386
  );
953
- if (!byId.get(targetId)?.actions[call.action])
1387
+ const targetInst = byId.get(targetId);
1388
+ if (!targetInst?.actions[call.action])
954
1389
  add(where, "invocation target must name a declared action");
1390
+ else if (!call.guard) {
1391
+ const targetAction = targetInst.actions[call.action]!;
1392
+ if (
1393
+ inst.subject &&
1394
+ targetInst.subject &&
1395
+ inst.subject === targetInst.subject
1396
+ ) {
1397
+ const callerRequirements = action.subject?.requirements ?? [];
1398
+ for (const targetReq of targetAction.subject?.requirements ??
1399
+ []) {
1400
+ const targetObjFieldName =
1401
+ targetReq.objectField ?? targetReq.field.name;
1402
+ const satisfied = callerRequirements.some(
1403
+ (cr) =>
1404
+ (cr.objectField ?? cr.field.name) === targetObjFieldName,
1405
+ );
1406
+ if (!satisfied) {
1407
+ add(
1408
+ `${where}.invoke`,
1409
+ `mandatory invocation of ${targetId}.${call.action} requires subject field ${targetReq.field.name}`,
1410
+ "UDL2002",
1411
+ );
1412
+ }
1413
+ }
1414
+ }
1415
+ }
955
1416
  }
956
1417
  targets.push({
957
1418
  targets: ids.map((id) => `${id}.${call.action}`),