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