@hyperscale0/udl 3.2.0 → 4.0.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.
@@ -1,7 +1,37 @@
1
- import { udlDocumentSchema, MAX_ACTION_EXPANSION, } from "./schema.js";
1
+ import { udlDocumentSchema, subjectPartyRoles, sameObjectField, MAX_ACTION_EXPANSION, } from "./schema.js";
2
2
  import { issue } from "./diagnostics.js";
3
3
  import { validateReportDefinition } from "./reporting-validation.js";
4
4
  import { analyzeInstrumentFinance } from "./finance.js";
5
+ export const RESERVED_OBJECT_NAMES = [
6
+ "objectId",
7
+ "externalId",
8
+ "revision",
9
+ "kind",
10
+ "tenantId",
11
+ "productId",
12
+ ];
13
+ const reservedObjectNamesSet = new Set(RESERVED_OBJECT_NAMES);
14
+ function hasParty(document, instrument, name) {
15
+ if (document.parties[name])
16
+ return true;
17
+ if (!instrument?.subject)
18
+ return false;
19
+ return (document.objects
20
+ .find((kind) => kind.id === instrument.subject)
21
+ ?.attachments.some((attachment) => (attachment.instrument === instrument.id ||
22
+ instrument.id.startsWith(`${attachment.instrument}_`)) &&
23
+ Object.values(attachment.parties).some((binding) => "role" in binding ? binding.role === name : binding.party === name)) ?? false);
24
+ }
25
+ function moneyParty(document, instrument, name) {
26
+ return (hasParty(document, instrument, name) &&
27
+ (document.parties[name]?.kind === "business" ||
28
+ (!document.parties[name] && !!instrument?.subject) ||
29
+ (!instrument?.subject && document.parties[name]?.kind === "person")));
30
+ }
31
+ function approvalParty(document, name) {
32
+ const party = document.parties[name];
33
+ return party?.kind === "staff" && !!party.role;
34
+ }
5
35
  export class UdlError extends Error {
6
36
  issues;
7
37
  constructor(issues) {
@@ -50,17 +80,26 @@ function commonField(fields) {
50
80
  field.optional !== first.optional))
51
81
  return;
52
82
  if (first.type === "ref") {
83
+ if (fields.some((field) => field?.type !== "ref" ||
84
+ field.targetKind !== first.targetKind ||
85
+ JSON.stringify(field.targetFamily) !==
86
+ JSON.stringify(first.targetFamily)))
87
+ return;
53
88
  const targets = [
54
89
  ...new Set(fields.flatMap((field) => field?.type === "ref" ? targetIds(field.target) : [])),
55
90
  ];
56
- return { ...first, target: targets.length === 1 ? targets[0] : targets };
91
+ return {
92
+ ...first,
93
+ targetKind: first.targetKind,
94
+ target: targets.length === 1 ? targets[0] : targets,
95
+ ...(first.targetFamily ? { targetFamily: first.targetFamily } : {}),
96
+ };
57
97
  }
58
98
  if (first.type === "account" &&
59
99
  fields.some((field) => field?.type !== "account" ||
60
100
  field.book !== first.book ||
61
101
  field.owner !== first.owner ||
62
102
  field.key !== first.key ||
63
- field.external !== first.external ||
64
103
  field.contra !== first.contra))
65
104
  return;
66
105
  if (first.type === "enum" &&
@@ -71,23 +110,69 @@ function commonField(fields) {
71
110
  if (first.type === "list" &&
72
111
  fields.some((field) => field?.type !== "list" ||
73
112
  field.item !== first.item ||
113
+ field.targetKind !== first.targetKind ||
74
114
  field.target !== first.target))
75
115
  return;
76
116
  return first;
77
117
  }
78
- export function resolveField(document, instrument, path, input = []) {
79
- return resolvePath(document, instrument, path, input, new Map());
118
+ export function resolveField(document, instrument, path, input = [], action) {
119
+ return resolvePath(document, instrument, path, input, new Map(), action);
120
+ }
121
+ /** A subject expression names a requirement; objectField names its stored metadata. */
122
+ export function resolveSubjectRequirement(instrument, name, action) {
123
+ const requirements = action
124
+ ? "requirements" in action
125
+ ? action.requirements
126
+ : action.subject?.requirements
127
+ : Object.values(instrument.actions).flatMap((item) => item.subject?.requirements ?? []);
128
+ return requirements?.find((requirement) => requirement.field.name === name);
129
+ }
130
+ function resolveReferencePath(document, field, suffix, cache) {
131
+ return commonField(targetIds(field.target).map((id) => {
132
+ const key = `${field.targetKind}:${id}:${suffix}`;
133
+ if (!cache.has(key)) {
134
+ const object = field.targetKind === "object" &&
135
+ document.objects.find((item) => item.id === id);
136
+ const instrument = field.targetKind === "instrument" &&
137
+ document.instruments.find((item) => item.id === id);
138
+ cache.set(key, object
139
+ ? resolveObjectPath(document, object, suffix, cache)
140
+ : instrument
141
+ ? resolvePath(document, instrument, `self.${suffix}`, [], cache)
142
+ : undefined);
143
+ }
144
+ return cache.get(key);
145
+ }));
146
+ }
147
+ function resolveObjectPath(document, object, path, cache) {
148
+ const parts = path.split(".");
149
+ const key = parts.shift();
150
+ if (!key)
151
+ return;
152
+ const field = object.fields.find((f) => f.name === key);
153
+ if (!field)
154
+ return;
155
+ if (parts.length === 0)
156
+ return field;
157
+ if (field.type !== "ref")
158
+ return;
159
+ return resolveReferencePath(document, field, parts.join("."), cache);
80
160
  }
81
- function resolvePath(document, instrument, path, input, cache) {
161
+ function resolvePath(document, instrument, path, input, cache, action) {
82
162
  const parts = path.split(".");
83
163
  const root = parts.shift();
84
164
  const key = parts.shift();
85
165
  if (!key)
86
166
  return root === "self"
87
- ? { name: "id", type: "ref", target: instrument.id }
167
+ ? {
168
+ name: "id",
169
+ type: "ref",
170
+ targetKind: "instrument",
171
+ target: instrument.id,
172
+ }
88
173
  : undefined;
89
174
  if (root === "party") {
90
- if (!document.parties[key])
175
+ if (!moneyParty(document, instrument, key))
91
176
  return;
92
177
  if (parts.length === 0)
93
178
  return { name: key, type: "account", owner: key, book: "cash" };
@@ -95,9 +180,31 @@ function resolvePath(document, instrument, path, input, cache) {
95
180
  return { name: parts[0], type: "money" };
96
181
  return;
97
182
  }
183
+ if (root === "self" && key === "subject" && instrument.subject) {
184
+ if (parts.length === 0)
185
+ return {
186
+ name: "subject",
187
+ type: "ref",
188
+ targetKind: "object",
189
+ target: instrument.subject,
190
+ };
191
+ const target = document.objects.find((o) => o.id === instrument.subject);
192
+ if (!target)
193
+ return;
194
+ const suffix = parts.join(".");
195
+ const cacheKey = `obj:${instrument.subject}:${suffix}`;
196
+ if (!cache.has(cacheKey))
197
+ cache.set(cacheKey, resolveObjectPath(document, target, suffix, cache));
198
+ return cache.get(cacheKey);
199
+ }
98
200
  if (root === "self" && parts.length === 0) {
99
201
  if (key === "id")
100
- return { name: key, type: "ref", target: instrument.id };
202
+ return {
203
+ name: key,
204
+ type: "ref",
205
+ targetKind: "instrument",
206
+ target: instrument.id,
207
+ };
101
208
  if (key === "now" || key === "createdAt")
102
209
  return { name: key, type: "date" };
103
210
  if (key === "productRevision")
@@ -105,6 +212,15 @@ function resolvePath(document, instrument, path, input, cache) {
105
212
  if (key === "status")
106
213
  return { name: key, type: "enum", values: instrument.lifecycle.states };
107
214
  }
215
+ if (root === "subject") {
216
+ const requirement = resolveSubjectRequirement(instrument, key, action);
217
+ if (!requirement || !instrument.subject)
218
+ return;
219
+ const object = document.objects.find((item) => item.id === instrument.subject);
220
+ if (!object)
221
+ return;
222
+ return resolveObjectPath(document, object, [requirement.objectField ?? requirement.field.name, ...parts].join("."), cache);
223
+ }
108
224
  const scope = instrument;
109
225
  let field = (root === "self" ? instrument.fields : root === "input" ? input : []).find((f) => f.name === key);
110
226
  for (const [index, part] of parts.entries()) {
@@ -123,16 +239,7 @@ function resolvePath(document, instrument, path, input, cache) {
123
239
  };
124
240
  if (field?.type !== "ref")
125
241
  return;
126
- return commonField(targetIds(field.target).map((id) => {
127
- const target = document.instruments.find((instrument) => instrument.id === id);
128
- if (!target)
129
- return;
130
- const suffix = "self." + parts.slice(index).join(".");
131
- const key = `${id}:${suffix}`;
132
- if (!cache.has(key))
133
- cache.set(key, resolvePath(document, target, suffix, [], cache));
134
- return cache.get(key);
135
- }));
242
+ return resolveReferencePath(document, field, parts.slice(index).join("."), cache);
136
243
  }
137
244
  return field;
138
245
  }
@@ -164,7 +271,150 @@ export function validateUdl(value) {
164
271
  seen.add(v);
165
272
  }
166
273
  };
274
+ if (document.objects.length === 0 && document.instruments.length === 0)
275
+ add("$", "document requires at least one object or instrument", "UDL2002");
276
+ duplicate(document.objects.map((o) => o.id), "$.objects");
167
277
  duplicate(document.instruments.map((i) => i.id), "$.instruments");
278
+ for (const name of subjectPartyRoles)
279
+ if (document.parties[name])
280
+ add(`$.parties.${name}`, `${name} is a reserved subject role`, "party_name_reserved");
281
+ const byObjectId = new Map(document.objects.map((o) => [o.id, o]));
282
+ const byId = new Map(document.instruments.map((i) => [i.id, i]));
283
+ const checkFields = (fields, where, isObject = false, instrument) => {
284
+ duplicate(fields.map((f) => f.name), where);
285
+ for (const f of fields) {
286
+ if (isObject) {
287
+ if (reservedObjectNamesSet.has(f.name))
288
+ add(where, `${f.name} is a reserved object name`);
289
+ }
290
+ else {
291
+ if (f.type === "account" && f.contra && f.book !== "claim")
292
+ add(where, "contra accounts require the claim book");
293
+ if (["id", "status", "createdAt", "now", "productRevision"].includes(f.name))
294
+ add(where, `${f.name} is a sealed instance field`);
295
+ if (f.type === "account" &&
296
+ f.owner !== "self" &&
297
+ !moneyParty(document, instrument, f.owner))
298
+ add(where, `${f.name} needs an eligible money party as owner`, hasParty(document, instrument, f.owner)
299
+ ? "party_kind_mismatch"
300
+ : "subject_party_unbound");
301
+ }
302
+ if (f.type === "text") {
303
+ if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
304
+ add(where, "text minimum exceeds maximum");
305
+ if (f.pattern) {
306
+ try {
307
+ new RegExp(f.pattern);
308
+ }
309
+ catch {
310
+ add(where, "invalid text character class");
311
+ }
312
+ }
313
+ }
314
+ if (f.type === "ref") {
315
+ duplicate(targetIds(f.target), where);
316
+ for (const id of targetIds(f.target)) {
317
+ const exists = f.targetKind === "object" ? byObjectId.has(id) : byId.has(id);
318
+ if (!exists)
319
+ add(where, `unknown reference target ${id}`, "UDL5001");
320
+ }
321
+ if (f.targetFamily) {
322
+ if (f.targetKind !== "instrument") {
323
+ add(where, "targetFamily requires an instrument reference target", "UDL5001");
324
+ }
325
+ else {
326
+ for (const id of targetIds(f.target)) {
327
+ const targetInst = byId.get(id);
328
+ if (targetInst &&
329
+ (!targetInst.family ||
330
+ targetInst.family.module !== f.targetFamily.module ||
331
+ targetInst.family.exportPath !== f.targetFamily.exportPath ||
332
+ targetInst.family.revision !== f.targetFamily.revision)) {
333
+ add(where, `targetFamily does not match target instrument ${id} family`, "UDL5001");
334
+ }
335
+ }
336
+ }
337
+ }
338
+ }
339
+ if (f.type === "enum") {
340
+ duplicate(f.values, where);
341
+ if (f.value !== undefined && !f.values.includes(f.value))
342
+ add(where, `${f.name} constant is outside its enum`);
343
+ }
344
+ if (f.type === "list") {
345
+ const validRefList = f.item === "ref" &&
346
+ f.targetKind &&
347
+ (f.targetKind === "object"
348
+ ? byObjectId.has(f.target ?? "")
349
+ : byId.has(f.target ?? ""));
350
+ const nonRefTarget = f.item !== "ref" &&
351
+ (f.targetKind !== undefined || f.target !== undefined);
352
+ if (!validRefList && (f.item === "ref" || nonRefTarget))
353
+ add(where, `${f.name} needs a target exactly when its items are references`);
354
+ if (f.targetFamily) {
355
+ if (f.item !== "ref" || f.targetKind !== "instrument") {
356
+ add(where, "targetFamily requires an instrument reference target", "UDL5001");
357
+ }
358
+ else if (f.target) {
359
+ const targetInst = byId.get(f.target);
360
+ if (targetInst &&
361
+ (!targetInst.family ||
362
+ targetInst.family.module !== f.targetFamily.module ||
363
+ targetInst.family.exportPath !== f.targetFamily.exportPath ||
364
+ targetInst.family.revision !== f.targetFamily.revision)) {
365
+ add(where, `targetFamily does not match target instrument ${f.target} family`, "UDL5001");
366
+ }
367
+ }
368
+ }
369
+ }
370
+ if ((f.type === "integer" || f.type === "money") &&
371
+ f.minimum !== undefined &&
372
+ f.maximum !== undefined &&
373
+ BigInt(f.minimum) > BigInt(f.maximum))
374
+ add(where, `${f.name} minimum exceeds maximum`);
375
+ if ((f.type === "integer" || f.type === "money") &&
376
+ f.value !== undefined &&
377
+ ((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
378
+ (f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum))))
379
+ add(where, `${f.name} constant violates its bounds`);
380
+ }
381
+ };
382
+ for (const [index, obj] of document.objects.entries()) {
383
+ const base = `$.objects[${index}]`;
384
+ if (reservedObjectNamesSet.has(obj.id))
385
+ add(base, `${obj.id} is a reserved object name`);
386
+ duplicate(obj.attachments.map((attachment) => attachment.name), `${base}.attachments`);
387
+ duplicate(obj.attachments.map((attachment) => attachment.instrument), `${base}.attachments`);
388
+ for (const [attachmentIndex, attachment] of obj.attachments.entries()) {
389
+ for (const [parameter, binding] of Object.entries(attachment.parties))
390
+ if ("party" in binding && !document.parties[binding.party])
391
+ add(`${base}.attachments[${attachmentIndex}].parties.${parameter}`, `unknown declared party ${binding.party}`, "subject_party_unbound");
392
+ if (byId.get(attachment.instrument)?.subject !== obj.id)
393
+ add(`${base}.attachments`, `${attachment.instrument} must attach to ${obj.id}`);
394
+ }
395
+ duplicate(obj.authoredFields, `${base}.authoredFields`);
396
+ duplicate(obj.columns, `${base}.columns`);
397
+ checkFields(obj.fields, `${base}.fields`, true);
398
+ for (let i = 0; i < obj.authoredFields.length; i++) {
399
+ const af = obj.authoredFields[i];
400
+ if (reservedObjectNamesSet.has(af))
401
+ add(`${base}.authoredFields`, `${af} is a reserved object name`);
402
+ if (!obj.fields.some((f) => f.name === af))
403
+ add(`${base}.authoredFields`, `authored field ${af} is not in declared fields`, "subject_field_unknown");
404
+ if (obj.fields[i]?.name !== af)
405
+ add(base, `authored field ${af} must precede attached requirements in fields`);
406
+ }
407
+ for (const col of obj.columns) {
408
+ if (reservedObjectNamesSet.has(col))
409
+ add(`${base}.columns`, `${col} is a reserved object name`);
410
+ if (!obj.fields.some((f) => f.name === col))
411
+ add(`${base}.columns`, `column ${col} is not a declared field`, "subject_field_unknown");
412
+ }
413
+ const pubActions = document.instruments
414
+ .filter((i) => i.subject === obj.id)
415
+ .flatMap((i) => Object.values(i.actions).flatMap((a) => a.publicAction ? [a.publicAction] : []));
416
+ duplicate(pubActions, `$.objects.${obj.id}.publicActions`);
417
+ }
168
418
  const reportIds = new Set();
169
419
  for (const [index, instrument] of document.instruments.entries()) {
170
420
  for (const report of instrument.reports ?? []) {
@@ -180,20 +430,26 @@ export function validateUdl(value) {
180
430
  }
181
431
  }
182
432
  }
183
- const byId = new Map(document.instruments.map((i) => [i.id, i]));
184
433
  const calls = new Map();
185
434
  for (const [index, inst] of document.instruments.entries()) {
186
435
  const base = `$.instruments[${index}]`;
187
- const field = (p, input = []) => resolveField(document, inst, p, input);
188
- const expect = (p, type, where, input = []) => {
189
- const found = field(p, input);
436
+ if (inst.subject && !byObjectId.has(inst.subject))
437
+ add(base, `unknown subject object ${inst.subject}`);
438
+ const field = (p, input = [], action) => resolveField(document, inst, p, input, action);
439
+ const expect = (p, type, where, input = [], action) => {
440
+ const party = p.startsWith("party.") ? p.split(".")[1] : undefined;
441
+ if (party && !moneyParty(document, inst, party))
442
+ add(where, `${party} cannot supply a money account`, hasParty(document, inst, party)
443
+ ? "party_kind_mismatch"
444
+ : "subject_party_unbound");
445
+ const found = field(p, input, action);
190
446
  if (found?.type !== type)
191
447
  add(where, `${p} must name a ${type} field`, "UDL5001");
192
448
  return found;
193
449
  };
194
- const checkValue = (v, type, where, input = []) => {
450
+ const checkValue = (v, type, where, input = [], action) => {
195
451
  if ("field" in v) {
196
- expect(v.field, type, where, input);
452
+ expect(v.field, type, where, input, action);
197
453
  return;
198
454
  }
199
455
  const valid = type === "money"
@@ -209,60 +465,7 @@ export function validateUdl(value) {
209
465
  if (!valid)
210
466
  add(where, `literal must have type ${type}`);
211
467
  };
212
- const checkFields = (fields, where) => {
213
- duplicate(fields.map((f) => f.name), where);
214
- for (const f of fields) {
215
- if (f.type === "account" &&
216
- ((f.contra && f.book !== "claim") ||
217
- (f.external && (f.book !== "cash" || f.owner === "self"))))
218
- add(where, "contra accounts require the claim book; external accounts require a cash party owner");
219
- if (["id", "status", "createdAt", "now", "productRevision"].includes(f.name))
220
- add(where, `${f.name} is a sealed instance field`);
221
- if (f.type === "text") {
222
- if ((f.minLength ?? 1) > (f.maxLength ?? 2048))
223
- add(where, "text minimum exceeds maximum");
224
- if (f.pattern) {
225
- try {
226
- new RegExp(f.pattern);
227
- }
228
- catch {
229
- add(where, "invalid text character class");
230
- }
231
- }
232
- }
233
- if (f.type === "ref") {
234
- duplicate(targetIds(f.target), where);
235
- for (const id of targetIds(f.target))
236
- if (!byId.has(id))
237
- add(where, `unknown reference target ${id}`, "UDL5001");
238
- }
239
- if (f.type === "account" &&
240
- f.owner !== "self" &&
241
- !document.parties[f.owner])
242
- add(where, `${f.name} needs self or a declared party as owner`);
243
- if (f.type === "enum") {
244
- duplicate(f.values, where);
245
- if (f.value !== undefined && !f.values.includes(f.value))
246
- add(where, `${f.name} constant is outside its enum`);
247
- }
248
- if (f.type === "list" &&
249
- (f.item === "ref"
250
- ? !f.target || !byId.has(f.target)
251
- : f.target !== undefined))
252
- add(where, `${f.name} needs a target exactly when its items are references`);
253
- if ((f.type === "integer" || f.type === "money") &&
254
- f.minimum !== undefined &&
255
- f.maximum !== undefined &&
256
- BigInt(f.minimum) > BigInt(f.maximum))
257
- add(where, `${f.name} minimum exceeds maximum`);
258
- if ((f.type === "integer" || f.type === "money") &&
259
- f.value !== undefined &&
260
- ((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
261
- (f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum))))
262
- add(where, `${f.name} constant violates its bounds`);
263
- }
264
- };
265
- checkFields(inst.fields, `${base}.fields`);
468
+ checkFields(inst.fields, `${base}.fields`, false, inst);
266
469
  const captures = new Set(Object.values(inst.actions).flatMap((a) => [
267
470
  ...a.moves.flatMap((m) => "capture" in m && m.capture ? [m.capture] : []),
268
471
  ]));
@@ -285,9 +488,18 @@ export function validateUdl(value) {
285
488
  const compatible = (a, b) => a &&
286
489
  b &&
287
490
  a.type === b.type &&
288
- (a.type !== "ref" || (b.type === "ref" && overlap(a.target, b.target)));
491
+ (a.type !== "ref" ||
492
+ (b.type === "ref" &&
493
+ a.targetKind === b.targetKind &&
494
+ overlap(a.target, b.target)));
289
495
  const anchor = field(selection.anchor, input);
290
496
  for (const target of targets) {
497
+ if (selection.family &&
498
+ (!target.family ||
499
+ target.family.module !== selection.family.module ||
500
+ target.family.exportPath !== selection.family.exportPath ||
501
+ target.family.revision !== selection.family.revision))
502
+ add(where, `selection family does not match target instrument ${target.id} family`, "UDL5001");
291
503
  if (selection.window &&
292
504
  target.fields.find((f) => f.name === selection.window.field)
293
505
  ?.type !== "date")
@@ -325,14 +537,14 @@ export function validateUdl(value) {
325
537
  }),
326
538
  };
327
539
  };
328
- const checkCalculations = (calculations, where, input = []) => {
540
+ const checkCalculations = (calculations, where, input = [], act) => {
329
541
  duplicate(calculations.map((c) => c.target), where);
330
542
  const dependencies = new Map();
331
543
  for (const c of calculations) {
332
544
  const arithmetic = ["sum", "subtract", "minimum", "divide"].includes(c.op);
333
545
  const resultType = c.op === "at"
334
546
  ? (() => {
335
- const list = field(c.list, input);
547
+ const list = field(c.list, input, act);
336
548
  return list?.type === "list" ? list.item : "text";
337
549
  })()
338
550
  : (c.op === "aggregate" && c.measure === "count") ||
@@ -347,11 +559,11 @@ export function validateUdl(value) {
347
559
  const operands = [];
348
560
  const money = (v) => {
349
561
  operands.push(v);
350
- checkValue(v, arithmetic ? resultType : "money", where, input);
562
+ checkValue(v, arithmetic ? resultType : "money", where, input, act);
351
563
  };
352
564
  if (c.op === "at") {
353
- expect(c.list, "list", where, input);
354
- checkValue(c.position, "integer", where, input);
565
+ expect(c.list, "list", where, input, act);
566
+ checkValue(c.position, "integer", where, input, act);
355
567
  if ("literal" in c.position && Number(c.position.literal) < 1)
356
568
  add(where, "list positions start at one");
357
569
  operands.push({ field: c.list }, c.position);
@@ -367,15 +579,15 @@ export function validateUdl(value) {
367
579
  if (c.op === "ratio") {
368
580
  money(c.amount);
369
581
  const type = "field" in c.numerator
370
- ? field(c.numerator.field, input)?.type
582
+ ? field(c.numerator.field, input, act)?.type
371
583
  : typeof c.numerator.literal === "number"
372
584
  ? "integer"
373
585
  : "money";
374
586
  if (type !== "integer" && type !== "money" && type !== "percent")
375
587
  add(where, "ratio weights must share a numeric type");
376
588
  else {
377
- checkValue(c.numerator, type, where, input);
378
- checkValue(c.denominator, type, where, input);
589
+ checkValue(c.numerator, type, where, input, act);
590
+ checkValue(c.denominator, type, where, input, act);
379
591
  }
380
592
  if ("literal" in c.denominator &&
381
593
  BigInt(c.denominator.literal.toString()) <= 0n)
@@ -390,13 +602,13 @@ export function validateUdl(value) {
390
602
  }
391
603
  if (c.op === "rate") {
392
604
  money(c.base);
393
- checkValue(c.bps, "percent", where, input);
605
+ checkValue(c.bps, "percent", where, input, act);
394
606
  operands.push(c.bps);
395
607
  }
396
608
  if (c.op === "multiply" || c.op === "divide") {
397
609
  money(c.amount);
398
610
  const count = c.op === "multiply" ? c.units : c.divisor;
399
- checkValue(count, "integer", where, input);
611
+ checkValue(count, "integer", where, input, act);
400
612
  operands.push(count);
401
613
  if ("literal" in count &&
402
614
  (c.op === "divide"
@@ -405,8 +617,8 @@ export function validateUdl(value) {
405
617
  add(where, "money multiplier must be nonnegative and divisor must be positive");
406
618
  }
407
619
  if (c.op === "shift") {
408
- checkValue(c.date, "date", where, input);
409
- checkValue(c.milliseconds, "duration", where, input);
620
+ checkValue(c.date, "date", where, input, act);
621
+ checkValue(c.milliseconds, "duration", where, input, act);
410
622
  operands.push(c.date, c.milliseconds);
411
623
  }
412
624
  dependencies.set(c.target, operands.flatMap((v) => "field" in v && v.field.startsWith("self.")
@@ -457,11 +669,13 @@ export function validateUdl(value) {
457
669
  for (const state of states)
458
670
  if (!reachable.has(state))
459
671
  add(base, `unreachable state ${state}`, "UDL3001");
460
- const checkRequirements = (requirements, where, input = []) => {
672
+ const checkRequirements = (requirements, where, input = [], act) => {
461
673
  for (const req of requirements ?? []) {
462
674
  if (req.kind === "compare") {
463
- const left = "field" in req.left ? field(req.left.field, input) : undefined;
464
- const right = "field" in req.right ? field(req.right.field, input) : undefined;
675
+ const left = "field" in req.left ? field(req.left.field, input, act) : undefined;
676
+ const right = "field" in req.right
677
+ ? field(req.right.field, input, act)
678
+ : undefined;
465
679
  if (("field" in req.left && !left) ||
466
680
  ("field" in req.right && !right))
467
681
  add(where, "comparison refers to an undeclared field");
@@ -470,48 +684,62 @@ export function validateUdl(value) {
470
684
  (left.type !== right.type ||
471
685
  (left.type === "ref" &&
472
686
  right.type === "ref" &&
473
- !overlap(left.target, right.target))))
687
+ (left.targetKind !== right.targetKind ||
688
+ !overlap(left.target, right.target)))))
474
689
  add(where, "comparison operands have different types");
475
690
  if (left && "literal" in req.right) {
476
- checkValue(req.right, left.type, where, input);
691
+ checkValue(req.right, left.type, where, input, act);
477
692
  if (left.type === "enum" &&
478
693
  !left.values.includes(String(req.right.literal)))
479
694
  add(where, "comparison names an undeclared enum value");
480
695
  }
481
696
  if (right && "literal" in req.left) {
482
- checkValue(req.left, right.type, where, input);
697
+ checkValue(req.left, right.type, where, input, act);
483
698
  if (right.type === "enum" &&
484
699
  !right.values.includes(String(req.left.literal)))
485
700
  add(where, "comparison names an undeclared enum value");
486
701
  }
487
702
  }
488
703
  else if (req.kind === "state") {
489
- const target = expect(req.reference, "ref", where, input);
704
+ const target = expect(req.reference, "ref", where, input, act);
490
705
  if (target?.type === "ref" &&
706
+ target.targetKind === "instrument" &&
491
707
  req.states.some((s) => targetIds(target.target).some((id) => !byId.get(id)?.lifecycle.states.includes(s))))
492
708
  add(where, "reference gate names an undeclared state");
493
709
  }
494
710
  else if (req.kind === "unique") {
495
711
  for (const p of req.fields)
496
- if (!field(p, input))
712
+ if (!field(p, input, act))
497
713
  add(where, `unknown identity field ${p}`);
498
714
  }
499
715
  else if (req.kind === "approval") {
500
- const target = expect(req.target, "ref", where, input);
501
- if (!document.parties[req.party])
502
- add(where, `unknown approving party ${req.party}`);
716
+ const request = expect(req.protectedRequest, "ref", where, input, act);
717
+ if (request?.type === "ref" && request.targetKind !== "instrument")
718
+ add(where, "protectedRequest must reference an instrument", "UDL5001");
719
+ const target = expect(req.target, "ref", where, input, act);
720
+ if (!approvalParty(document, req.party))
721
+ add(where, `approval requires staff with a role: ${req.party}`, hasParty(document, inst, req.party)
722
+ ? "party_kind_mismatch"
723
+ : "subject_party_unbound");
503
724
  if (req.action &&
504
725
  target?.type === "ref" &&
726
+ target.targetKind === "instrument" &&
505
727
  targetIds(target.target).some((id) => !byId.get(id)?.actions[req.action]))
506
728
  add(where, `unknown approved action ${req.action}`);
507
729
  }
508
730
  else if (req.kind === "evidence") {
509
- const subject = field(req.subject, input);
510
- if (!subject || !["account", "text"].includes(subject.type))
731
+ if (req.instruction) {
732
+ expect(req.instruction, "text", where, input, act);
733
+ if (!["confirmed", "rejected"].includes(req.result))
734
+ add(where, "instruction evidence requires a terminal outcome");
735
+ }
736
+ const subject = field(req.subject, input, act);
737
+ if (!subject ||
738
+ !["account", "text", ...(req.instruction ? ["ref"] : [])].includes(subject.type))
511
739
  add(where, "evidence subject must be an account or text subject id");
512
740
  }
513
741
  else if (req.kind === "hours") {
514
- expect(req.at, "date", where, input);
742
+ expect(req.at, "date", where, input, act);
515
743
  try {
516
744
  new Intl.DateTimeFormat("en", { timeZone: req.timezone });
517
745
  }
@@ -530,7 +758,7 @@ export function validateUdl(value) {
530
758
  resolveField(document, selected, `self.${measure.sum}`)
531
759
  ?.type) !== "money")
532
760
  add(where, "aggregate sum requires a money field");
533
- checkValue(req.value, req.measure === "count" ? "integer" : "money", where, input);
761
+ checkValue(req.value, req.measure === "count" ? "integer" : "money", where, input, act);
534
762
  }
535
763
  }
536
764
  }
@@ -540,11 +768,47 @@ export function validateUdl(value) {
540
768
  const where = `${base}.actions.${actionName}`;
541
769
  if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
542
770
  add(where, "action needs a lifecycle transition", "UDL3001");
543
- checkFields(action.input, `${where}.input`);
771
+ if (action.subject) {
772
+ const subBase = `${where}.subject`;
773
+ checkFields(action.subject.requirements.map((r) => ({
774
+ ...r.field,
775
+ name: r.objectField ?? r.field.name,
776
+ })), `${subBase}.requirements`, true);
777
+ for (const req of action.subject.requirements) {
778
+ if (req.field.optional)
779
+ add(`${subBase}.requirements`, `subject requirement ${req.field.name} cannot be optional`, "UDL2002");
780
+ const name = req.objectField ?? req.field.name;
781
+ if (reservedObjectNamesSet.has(name))
782
+ add(subBase, `${name} is a reserved object name`);
783
+ if (inst.subject) {
784
+ const field = byObjectId
785
+ .get(inst.subject)
786
+ ?.fields.find((field) => field.name === name);
787
+ if (!field || !sameObjectField(field, req.field))
788
+ add(subBase, `object field ${name} must match requirement ${req.field.name}`, field ? "subject_field_conflict" : "subject_field_unknown");
789
+ }
790
+ }
791
+ duplicate(action.subject.adapters.map((a) => a.binding), `${subBase}.adapters`);
792
+ for (const adapter of action.subject.adapters) {
793
+ if (adapter.snapshot) {
794
+ checkFields(adapter.snapshot.requirements, `${subBase}.adapters.${adapter.binding}.requirements`, true);
795
+ for (const reqField of adapter.snapshot.requirements) {
796
+ const objectName = adapter.renames?.[reqField.name] ?? reqField.name;
797
+ const requirement = action.subject.requirements.find((item) => (item.objectField ?? item.field.name) === objectName &&
798
+ sameObjectField(item.field, reqField));
799
+ if (!requirement)
800
+ add(subBase, `adapter ${adapter.binding} requirement ${reqField.name} is absent from the action`);
801
+ if (reqField.optional)
802
+ add(`${subBase}.adapters.${adapter.binding}`, `adapter requirement ${reqField.name} cannot be optional`, "UDL2002");
803
+ }
804
+ }
805
+ }
806
+ }
807
+ checkFields(action.input, `${where}.input`, false, inst);
544
808
  if (action.reminder) {
545
- expect(action.reminder.installment, "ref", where, action.input);
546
- expect(action.reminder.recipient, "account", where, action.input);
547
- expect(action.reminder.dueAt, "date", where, action.input);
809
+ expect(action.reminder.installment, "ref", where, action.input, action);
810
+ expect(action.reminder.recipient, "account", where, action.input, action);
811
+ expect(action.reminder.dueAt, "date", where, action.input, action);
548
812
  if (action.reminder.startHour >= action.reminder.endHour)
549
813
  add(where, "reminder sending window is empty");
550
814
  try {
@@ -569,11 +833,11 @@ export function validateUdl(value) {
569
833
  if (captures.has(input.name))
570
834
  add(where, `${input.name} is an executor-owned receipt and cannot be an input`);
571
835
  }
572
- checkCalculations(action.calculate ?? [], `${where}.calculate`, action.input);
573
- checkRequirements(action.requires, `${where}.requires`, action.input);
836
+ checkCalculations(action.calculate ?? [], `${where}.calculate`, action.input, action);
837
+ checkRequirements(action.requires, `${where}.requires`, action.input, action);
574
838
  if (typeof action.actor === "object" &&
575
839
  "party" in action.actor &&
576
- !document.parties[action.actor.party])
840
+ !hasParty(document, inst, action.actor.party))
577
841
  add(where, "actor names an undeclared party");
578
842
  if (typeof action.actor === "object" &&
579
843
  "parent" in action.actor &&
@@ -583,7 +847,7 @@ export function validateUdl(value) {
583
847
  add(where, "clock action needs a due instant");
584
848
  for (const clock of [action.due, action.deadline])
585
849
  if (clock) {
586
- expect(clock.at, "date", where, action.input);
850
+ expect(clock.at, "date", where, action.input, action);
587
851
  if (clock.localDay) {
588
852
  try {
589
853
  new Intl.DateTimeFormat("en", {
@@ -596,10 +860,14 @@ export function validateUdl(value) {
596
860
  }
597
861
  }
598
862
  for (const move of action.moves) {
863
+ if (move.operation === "internal_transfer.reserve" &&
864
+ move.boundary &&
865
+ !action.subject?.adapters.some((binding) => binding.binding === move.boundary.adapter && binding.snapshot))
866
+ add(where, "boundary reservation requires a retained adapter binding");
599
867
  if ("amount" in move) {
600
- checkValue(move.amount, "money", where, action.input);
601
- const from = expect(move.from, "account", where, action.input);
602
- const to = expect(move.to, "account", where, action.input);
868
+ checkValue(move.amount, "money", where, action.input, action);
869
+ const from = expect(move.from, "account", where, action.input, action);
870
+ const to = expect(move.to, "account", where, action.input, action);
603
871
  if (from?.type === "account" &&
604
872
  to?.type === "account" &&
605
873
  from.book !== to.book)
@@ -614,7 +882,7 @@ export function validateUdl(value) {
614
882
  add(where, "a transfer needs distinct accounts", "UDL4001");
615
883
  }
616
884
  else
617
- expect(move.transfer, "text", where, action.input);
885
+ expect(move.transfer, "text", where, action.input, action);
618
886
  }
619
887
  duplicate(action.moves.flatMap((m) => "capture" in m && m.capture ? [m.capture] : []), `${where}.captures`);
620
888
  duplicate(action.moves.map((m) => m.key), `${where}.moves`);
@@ -628,7 +896,7 @@ export function validateUdl(value) {
628
896
  inst.calculate.some((c) => c.target === key))
629
897
  add(where, `cannot write immutable field ${key}`);
630
898
  else
631
- checkValue(v, target.type, where, action.input);
899
+ checkValue(v, target.type, where, action.input, action);
632
900
  }
633
901
  const checkArguments = (targetId, actionName, values, binding) => {
634
902
  const target = targetId
@@ -652,35 +920,42 @@ export function validateUdl(value) {
652
920
  add(where, "range binding requires an integer target input");
653
921
  continue;
654
922
  }
655
- checkValue(value, declared.type, where, action.input);
923
+ checkValue(value, declared.type, where, action.input, action);
656
924
  if (declared.type === "ref" && "field" in value) {
657
- const supplied = field(value.field, action.input);
925
+ const supplied = field(value.field, action.input, action);
658
926
  if (supplied?.type !== "ref" ||
927
+ supplied.targetKind !== declared.targetKind ||
659
928
  !targetIds(supplied.target).every((id) => targetIds(declared.target).includes(id)))
660
929
  add(where, `target input ${name} has the wrong reference type`);
661
930
  }
662
931
  }
663
932
  };
664
933
  if (action.approval) {
665
- const reference = expect(action.approval.target, "ref", where, action.input);
934
+ const request = expect(action.approval.protectedRequest, "ref", where, action.input, action);
935
+ if (request?.type === "ref" && request.targetKind !== "instrument")
936
+ add(where, "protectedRequest must reference an instrument", "UDL5001");
937
+ const reference = expect(action.approval.target, "ref", where, action.input, action);
666
938
  if (reference?.type === "ref" &&
939
+ reference.targetKind === "instrument" &&
667
940
  targetIds(reference.target).some((id) => !byId.get(id)?.actions[action.approval.action]))
668
941
  add(where, "approval target action does not exist");
669
- if (reference?.type === "ref")
942
+ if (reference?.type === "ref" && reference.targetKind === "instrument")
670
943
  for (const id of targetIds(reference.target))
671
944
  checkArguments(id, action.approval.action, action.approval.input);
672
- expect(action.approval.expires, "date", where, action.input);
673
- if (!document.parties[action.approval.party])
674
- add(where, "approval needs a declared party");
945
+ expect(action.approval.expires, "date", where, action.input, action);
946
+ if (!approvalParty(document, action.approval.party))
947
+ add(where, "approval requires staff with a role", hasParty(document, inst, action.approval.party)
948
+ ? "party_kind_mismatch"
949
+ : "subject_party_unbound");
675
950
  }
676
951
  const targets = [];
677
952
  for (const call of action.invoke ?? []) {
678
953
  if (call.guard)
679
- checkRequirements([call.guard], where, action.input);
954
+ checkRequirements([call.guard], where, action.input, action);
680
955
  const range = "instrument" in call ? call.range : undefined;
681
956
  if (range) {
682
- checkValue(range.count, "integer", where, action.input);
683
- if (["self", "input", "party"].includes(range.bind))
957
+ checkValue(range.count, "integer", where, action.input, action);
958
+ if (["self", "input", "party", "subject"].includes(range.bind))
684
959
  add(where, "range binding cannot shadow a reserved scope");
685
960
  if ("literal" in range.count &&
686
961
  (typeof range.count.literal !== "number" ||
@@ -690,12 +965,14 @@ export function validateUdl(value) {
690
965
  }
691
966
  if ("selection" in call)
692
967
  checkSelection(call.selection, where, action.input);
693
- const target = "reference" in call ? field(call.reference, action.input) : undefined;
968
+ const target = "reference" in call
969
+ ? field(call.reference, action.input, action)
970
+ : undefined;
694
971
  const selected = "instrument" in call
695
972
  ? call.instrument
696
973
  : "selection" in call
697
974
  ? call.selection.instrument
698
- : target?.type === "ref"
975
+ : target?.type === "ref" && target.targetKind === "instrument"
699
976
  ? target.target
700
977
  : undefined;
701
978
  const ids = Array.isArray(selected)
@@ -709,8 +986,25 @@ export function validateUdl(value) {
709
986
  checkArguments(targetId, call.action, call.input, range?.bind);
710
987
  if (call.action === "create" && !("instrument" in call))
711
988
  add(where, "invocation references an existing instance and cannot create it again");
712
- if (!byId.get(targetId)?.actions[call.action])
989
+ const targetInst = byId.get(targetId);
990
+ if (!targetInst?.actions[call.action])
713
991
  add(where, "invocation target must name a declared action");
992
+ else if (!call.guard) {
993
+ const targetAction = targetInst.actions[call.action];
994
+ if (inst.subject &&
995
+ targetInst.subject &&
996
+ inst.subject === targetInst.subject) {
997
+ const callerRequirements = action.subject?.requirements ?? [];
998
+ for (const targetReq of targetAction.subject?.requirements ??
999
+ []) {
1000
+ const targetObjFieldName = targetReq.objectField ?? targetReq.field.name;
1001
+ const satisfied = callerRequirements.some((cr) => (cr.objectField ?? cr.field.name) === targetObjFieldName);
1002
+ if (!satisfied) {
1003
+ add(`${where}.invoke`, `mandatory invocation of ${targetId}.${call.action} requires subject field ${targetReq.field.name}`, "UDL2002");
1004
+ }
1005
+ }
1006
+ }
1007
+ }
714
1008
  }
715
1009
  targets.push({
716
1010
  targets: ids.map((id) => `${id}.${call.action}`),