@hyperscale0/udl 3.1.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.
Files changed (53) 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 +13 -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/finance.js +2 -1
  11. package/dist/finance.js.map +1 -1
  12. package/dist/index.d.ts +4 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +4 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/instrument-references.d.ts.map +1 -1
  17. package/dist/instrument-references.js +21 -1
  18. package/dist/instrument-references.js.map +1 -1
  19. package/dist/object-contract.d.ts +157 -0
  20. package/dist/object-contract.d.ts.map +1 -0
  21. package/dist/object-contract.js +306 -0
  22. package/dist/object-contract.js.map +1 -0
  23. package/dist/reporting-validation.d.ts +8 -0
  24. package/dist/reporting-validation.d.ts.map +1 -0
  25. package/dist/reporting-validation.js +245 -0
  26. package/dist/reporting-validation.js.map +1 -0
  27. package/dist/reporting.d.ts +478 -0
  28. package/dist/reporting.d.ts.map +1 -0
  29. package/dist/reporting.js +170 -0
  30. package/dist/reporting.js.map +1 -0
  31. package/dist/schema.d.ts +2933 -24
  32. package/dist/schema.d.ts.map +1 -1
  33. package/dist/schema.js +163 -23
  34. package/dist/schema.js.map +1 -1
  35. package/dist/validation.d.ts +5 -2
  36. package/dist/validation.d.ts.map +1 -1
  37. package/dist/validation.js +495 -124
  38. package/dist/validation.js.map +1 -1
  39. package/docs/README.md +87 -9
  40. package/package.json +4 -4
  41. package/spec/README.md +87 -9
  42. package/spec/{darb.udl.json → cars.udl.json} +7 -7
  43. package/spec/udl.schema.json +2969 -4558
  44. package/src/diagnostics.ts +33 -1
  45. package/src/evolution.ts +8 -0
  46. package/src/finance.ts +1 -1
  47. package/src/index.ts +9 -0
  48. package/src/instrument-references.ts +25 -1
  49. package/src/object-contract.ts +498 -0
  50. package/src/reporting-validation.ts +381 -0
  51. package/src/reporting.ts +179 -0
  52. package/src/schema.ts +219 -23
  53. package/src/validation.ts +765 -136
@@ -1,6 +1,37 @@
1
- import { udlDocumentSchema, } from "./schema.js";
1
+ import { udlDocumentSchema, subjectPartyRoles, sameObjectField, MAX_ACTION_EXPANSION, } from "./schema.js";
2
2
  import { issue } from "./diagnostics.js";
3
+ import { validateReportDefinition } from "./reporting-validation.js";
3
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
+ }
4
35
  export class UdlError extends Error {
5
36
  issues;
6
37
  constructor(issues) {
@@ -49,17 +80,26 @@ function commonField(fields) {
49
80
  field.optional !== first.optional))
50
81
  return;
51
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;
52
88
  const targets = [
53
89
  ...new Set(fields.flatMap((field) => field?.type === "ref" ? targetIds(field.target) : [])),
54
90
  ];
55
- 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
+ };
56
97
  }
57
98
  if (first.type === "account" &&
58
99
  fields.some((field) => field?.type !== "account" ||
59
100
  field.book !== first.book ||
60
101
  field.owner !== first.owner ||
61
102
  field.key !== first.key ||
62
- field.external !== first.external ||
63
103
  field.contra !== first.contra))
64
104
  return;
65
105
  if (first.type === "enum" &&
@@ -70,23 +110,69 @@ function commonField(fields) {
70
110
  if (first.type === "list" &&
71
111
  fields.some((field) => field?.type !== "list" ||
72
112
  field.item !== first.item ||
113
+ field.targetKind !== first.targetKind ||
73
114
  field.target !== first.target))
74
115
  return;
75
116
  return first;
76
117
  }
77
- export function resolveField(document, instrument, path, input = []) {
78
- 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);
79
160
  }
80
- function resolvePath(document, instrument, path, input, cache) {
161
+ function resolvePath(document, instrument, path, input, cache, action) {
81
162
  const parts = path.split(".");
82
163
  const root = parts.shift();
83
164
  const key = parts.shift();
84
165
  if (!key)
85
166
  return root === "self"
86
- ? { name: "id", type: "ref", target: instrument.id }
167
+ ? {
168
+ name: "id",
169
+ type: "ref",
170
+ targetKind: "instrument",
171
+ target: instrument.id,
172
+ }
87
173
  : undefined;
88
174
  if (root === "party") {
89
- if (!document.parties[key])
175
+ if (!moneyParty(document, instrument, key))
90
176
  return;
91
177
  if (parts.length === 0)
92
178
  return { name: key, type: "account", owner: key, book: "cash" };
@@ -94,14 +180,47 @@ function resolvePath(document, instrument, path, input, cache) {
94
180
  return { name: parts[0], type: "money" };
95
181
  return;
96
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
+ }
97
200
  if (root === "self" && parts.length === 0) {
98
201
  if (key === "id")
99
- return { name: key, type: "ref", target: instrument.id };
202
+ return {
203
+ name: key,
204
+ type: "ref",
205
+ targetKind: "instrument",
206
+ target: instrument.id,
207
+ };
100
208
  if (key === "now" || key === "createdAt")
101
209
  return { name: key, type: "date" };
210
+ if (key === "productRevision")
211
+ return { name: key, type: "text" };
102
212
  if (key === "status")
103
213
  return { name: key, type: "enum", values: instrument.lifecycle.states };
104
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
+ }
105
224
  const scope = instrument;
106
225
  let field = (root === "self" ? instrument.fields : root === "input" ? input : []).find((f) => f.name === key);
107
226
  for (const [index, part] of parts.entries()) {
@@ -120,16 +239,7 @@ function resolvePath(document, instrument, path, input, cache) {
120
239
  };
121
240
  if (field?.type !== "ref")
122
241
  return;
123
- return commonField(targetIds(field.target).map((id) => {
124
- const target = document.instruments.find((instrument) => instrument.id === id);
125
- if (!target)
126
- return;
127
- const suffix = "self." + parts.slice(index).join(".");
128
- const key = `${id}:${suffix}`;
129
- if (!cache.has(key))
130
- cache.set(key, resolvePath(document, target, suffix, [], cache));
131
- return cache.get(key);
132
- }));
242
+ return resolveReferencePath(document, field, parts.slice(index).join("."), cache);
133
243
  }
134
244
  return field;
135
245
  }
@@ -161,21 +271,185 @@ export function validateUdl(value) {
161
271
  seen.add(v);
162
272
  }
163
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");
164
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]));
165
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
+ }
418
+ const reportIds = new Set();
419
+ for (const [index, instrument] of document.instruments.entries()) {
420
+ for (const report of instrument.reports ?? []) {
421
+ const path = `$.instruments[${index}].reports`;
422
+ if (reportIds.has(report.identity.id))
423
+ add(path, `duplicate report ${report.identity.id}`);
424
+ reportIds.add(report.identity.id);
425
+ try {
426
+ validateReportDefinition(report, document);
427
+ }
428
+ catch (error) {
429
+ add(path, error instanceof Error ? error.message : "Invalid report definition");
430
+ }
431
+ }
432
+ }
166
433
  const calls = new Map();
167
434
  for (const [index, inst] of document.instruments.entries()) {
168
435
  const base = `$.instruments[${index}]`;
169
- const field = (p, input = []) => resolveField(document, inst, p, input);
170
- const expect = (p, type, where, input = []) => {
171
- 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);
172
446
  if (found?.type !== type)
173
447
  add(where, `${p} must name a ${type} field`, "UDL5001");
174
448
  return found;
175
449
  };
176
- const checkValue = (v, type, where, input = []) => {
450
+ const checkValue = (v, type, where, input = [], action) => {
177
451
  if ("field" in v) {
178
- expect(v.field, type, where, input);
452
+ expect(v.field, type, where, input, action);
179
453
  return;
180
454
  }
181
455
  const valid = type === "money"
@@ -191,48 +465,7 @@ export function validateUdl(value) {
191
465
  if (!valid)
192
466
  add(where, `literal must have type ${type}`);
193
467
  };
194
- const checkFields = (fields, where) => {
195
- duplicate(fields.map((f) => f.name), where);
196
- for (const f of fields) {
197
- if (f.type === "account" &&
198
- ((f.contra && f.book !== "claim") ||
199
- (f.external && (f.book !== "cash" || f.owner === "self"))))
200
- add(where, "contra accounts require the claim book; external accounts require a cash party owner");
201
- if (["id", "status", "createdAt", "now"].includes(f.name))
202
- add(where, `${f.name} is a sealed instance field`);
203
- if (f.type === "ref") {
204
- duplicate(targetIds(f.target), where);
205
- for (const id of targetIds(f.target))
206
- if (!byId.has(id))
207
- add(where, `unknown reference target ${id}`, "UDL5001");
208
- }
209
- if (f.type === "account" &&
210
- f.owner !== "self" &&
211
- !document.parties[f.owner])
212
- add(where, `${f.name} needs self or a declared party as owner`);
213
- if (f.type === "enum") {
214
- duplicate(f.values, where);
215
- if (f.value !== undefined && !f.values.includes(f.value))
216
- add(where, `${f.name} constant is outside its enum`);
217
- }
218
- if (f.type === "list" &&
219
- (f.item === "ref"
220
- ? !f.target || !byId.has(f.target)
221
- : f.target !== undefined))
222
- add(where, `${f.name} needs a target exactly when its items are references`);
223
- if ((f.type === "integer" || f.type === "money") &&
224
- f.minimum !== undefined &&
225
- f.maximum !== undefined &&
226
- BigInt(f.minimum) > BigInt(f.maximum))
227
- add(where, `${f.name} minimum exceeds maximum`);
228
- if ((f.type === "integer" || f.type === "money") &&
229
- f.value !== undefined &&
230
- ((f.minimum !== undefined && BigInt(f.value) < BigInt(f.minimum)) ||
231
- (f.maximum !== undefined && BigInt(f.value) > BigInt(f.maximum))))
232
- add(where, `${f.name} constant violates its bounds`);
233
- }
234
- };
235
- checkFields(inst.fields, `${base}.fields`);
468
+ checkFields(inst.fields, `${base}.fields`, false, inst);
236
469
  const captures = new Set(Object.values(inst.actions).flatMap((a) => [
237
470
  ...a.moves.flatMap((m) => "capture" in m && m.capture ? [m.capture] : []),
238
471
  ]));
@@ -255,9 +488,18 @@ export function validateUdl(value) {
255
488
  const compatible = (a, b) => a &&
256
489
  b &&
257
490
  a.type === b.type &&
258
- (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)));
259
495
  const anchor = field(selection.anchor, input);
260
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");
261
503
  if (selection.window &&
262
504
  target.fields.find((f) => f.name === selection.window.field)
263
505
  ?.type !== "date")
@@ -295,14 +537,14 @@ export function validateUdl(value) {
295
537
  }),
296
538
  };
297
539
  };
298
- const checkCalculations = (calculations, where, input = []) => {
540
+ const checkCalculations = (calculations, where, input = [], act) => {
299
541
  duplicate(calculations.map((c) => c.target), where);
300
542
  const dependencies = new Map();
301
543
  for (const c of calculations) {
302
544
  const arithmetic = ["sum", "subtract", "minimum", "divide"].includes(c.op);
303
545
  const resultType = c.op === "at"
304
546
  ? (() => {
305
- const list = field(c.list, input);
547
+ const list = field(c.list, input, act);
306
548
  return list?.type === "list" ? list.item : "text";
307
549
  })()
308
550
  : (c.op === "aggregate" && c.measure === "count") ||
@@ -317,11 +559,11 @@ export function validateUdl(value) {
317
559
  const operands = [];
318
560
  const money = (v) => {
319
561
  operands.push(v);
320
- checkValue(v, arithmetic ? resultType : "money", where, input);
562
+ checkValue(v, arithmetic ? resultType : "money", where, input, act);
321
563
  };
322
564
  if (c.op === "at") {
323
- expect(c.list, "list", where, input);
324
- checkValue(c.position, "integer", where, input);
565
+ expect(c.list, "list", where, input, act);
566
+ checkValue(c.position, "integer", where, input, act);
325
567
  if ("literal" in c.position && Number(c.position.literal) < 1)
326
568
  add(where, "list positions start at one");
327
569
  operands.push({ field: c.list }, c.position);
@@ -337,15 +579,15 @@ export function validateUdl(value) {
337
579
  if (c.op === "ratio") {
338
580
  money(c.amount);
339
581
  const type = "field" in c.numerator
340
- ? field(c.numerator.field, input)?.type
582
+ ? field(c.numerator.field, input, act)?.type
341
583
  : typeof c.numerator.literal === "number"
342
584
  ? "integer"
343
585
  : "money";
344
586
  if (type !== "integer" && type !== "money" && type !== "percent")
345
587
  add(where, "ratio weights must share a numeric type");
346
588
  else {
347
- checkValue(c.numerator, type, where, input);
348
- checkValue(c.denominator, type, where, input);
589
+ checkValue(c.numerator, type, where, input, act);
590
+ checkValue(c.denominator, type, where, input, act);
349
591
  }
350
592
  if ("literal" in c.denominator &&
351
593
  BigInt(c.denominator.literal.toString()) <= 0n)
@@ -360,13 +602,13 @@ export function validateUdl(value) {
360
602
  }
361
603
  if (c.op === "rate") {
362
604
  money(c.base);
363
- checkValue(c.bps, "percent", where, input);
605
+ checkValue(c.bps, "percent", where, input, act);
364
606
  operands.push(c.bps);
365
607
  }
366
608
  if (c.op === "multiply" || c.op === "divide") {
367
609
  money(c.amount);
368
610
  const count = c.op === "multiply" ? c.units : c.divisor;
369
- checkValue(count, "integer", where, input);
611
+ checkValue(count, "integer", where, input, act);
370
612
  operands.push(count);
371
613
  if ("literal" in count &&
372
614
  (c.op === "divide"
@@ -375,8 +617,8 @@ export function validateUdl(value) {
375
617
  add(where, "money multiplier must be nonnegative and divisor must be positive");
376
618
  }
377
619
  if (c.op === "shift") {
378
- checkValue(c.date, "date", where, input);
379
- checkValue(c.milliseconds, "duration", where, input);
620
+ checkValue(c.date, "date", where, input, act);
621
+ checkValue(c.milliseconds, "duration", where, input, act);
380
622
  operands.push(c.date, c.milliseconds);
381
623
  }
382
624
  dependencies.set(c.target, operands.flatMap((v) => "field" in v && v.field.startsWith("self.")
@@ -403,6 +645,8 @@ export function validateUdl(value) {
403
645
  };
404
646
  checkCalculations(inst.calculate, `${base}.calculate`);
405
647
  const states = new Set(inst.lifecycle.states);
648
+ if (states.has("preserve"))
649
+ add(base, "preserve is a reserved transition destination", "UDL3001");
406
650
  duplicate(inst.lifecycle.states, `${base}.lifecycle.states`);
407
651
  if (!states.has(inst.lifecycle.initial) || !inst.actions.create)
408
652
  add(base, "declare create and a declared initial state", "UDL3001");
@@ -414,22 +658,24 @@ export function validateUdl(value) {
414
658
  for (const [action, edge] of Object.entries(inst.lifecycle.transitions)) {
415
659
  if (!inst.actions[action] ||
416
660
  action === "create" ||
417
- !states.has(edge.to) ||
661
+ (edge.to !== "preserve" && !states.has(edge.to)) ||
418
662
  edge.from.some((s) => !states.has(s)))
419
663
  add(base, `invalid transition ${action}`, "UDL3001");
420
664
  }
421
665
  for (let pass = 0; pass < states.size; pass++)
422
666
  for (const edge of Object.values(inst.lifecycle.transitions))
423
- if (edge.from.some((s) => reachable.has(s)))
667
+ if (edge.to !== "preserve" && edge.from.some((s) => reachable.has(s)))
424
668
  reachable.add(edge.to);
425
669
  for (const state of states)
426
670
  if (!reachable.has(state))
427
671
  add(base, `unreachable state ${state}`, "UDL3001");
428
- const checkRequirements = (requirements, where, input = []) => {
672
+ const checkRequirements = (requirements, where, input = [], act) => {
429
673
  for (const req of requirements ?? []) {
430
674
  if (req.kind === "compare") {
431
- const left = "field" in req.left ? field(req.left.field, input) : undefined;
432
- 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;
433
679
  if (("field" in req.left && !left) ||
434
680
  ("field" in req.right && !right))
435
681
  add(where, "comparison refers to an undeclared field");
@@ -438,48 +684,62 @@ export function validateUdl(value) {
438
684
  (left.type !== right.type ||
439
685
  (left.type === "ref" &&
440
686
  right.type === "ref" &&
441
- !overlap(left.target, right.target))))
687
+ (left.targetKind !== right.targetKind ||
688
+ !overlap(left.target, right.target)))))
442
689
  add(where, "comparison operands have different types");
443
690
  if (left && "literal" in req.right) {
444
- checkValue(req.right, left.type, where, input);
691
+ checkValue(req.right, left.type, where, input, act);
445
692
  if (left.type === "enum" &&
446
693
  !left.values.includes(String(req.right.literal)))
447
694
  add(where, "comparison names an undeclared enum value");
448
695
  }
449
696
  if (right && "literal" in req.left) {
450
- checkValue(req.left, right.type, where, input);
697
+ checkValue(req.left, right.type, where, input, act);
451
698
  if (right.type === "enum" &&
452
699
  !right.values.includes(String(req.left.literal)))
453
700
  add(where, "comparison names an undeclared enum value");
454
701
  }
455
702
  }
456
703
  else if (req.kind === "state") {
457
- const target = expect(req.reference, "ref", where, input);
704
+ const target = expect(req.reference, "ref", where, input, act);
458
705
  if (target?.type === "ref" &&
706
+ target.targetKind === "instrument" &&
459
707
  req.states.some((s) => targetIds(target.target).some((id) => !byId.get(id)?.lifecycle.states.includes(s))))
460
708
  add(where, "reference gate names an undeclared state");
461
709
  }
462
710
  else if (req.kind === "unique") {
463
711
  for (const p of req.fields)
464
- if (!field(p, input))
712
+ if (!field(p, input, act))
465
713
  add(where, `unknown identity field ${p}`);
466
714
  }
467
715
  else if (req.kind === "approval") {
468
- const target = expect(req.target, "ref", where, input);
469
- if (!document.parties[req.party])
470
- 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");
471
724
  if (req.action &&
472
725
  target?.type === "ref" &&
726
+ target.targetKind === "instrument" &&
473
727
  targetIds(target.target).some((id) => !byId.get(id)?.actions[req.action]))
474
728
  add(where, `unknown approved action ${req.action}`);
475
729
  }
476
730
  else if (req.kind === "evidence") {
477
- const subject = field(req.subject, input);
478
- 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))
479
739
  add(where, "evidence subject must be an account or text subject id");
480
740
  }
481
741
  else if (req.kind === "hours") {
482
- expect(req.at, "date", where, input);
742
+ expect(req.at, "date", where, input, act);
483
743
  try {
484
744
  new Intl.DateTimeFormat("en", { timeZone: req.timezone });
485
745
  }
@@ -498,7 +758,7 @@ export function validateUdl(value) {
498
758
  resolveField(document, selected, `self.${measure.sum}`)
499
759
  ?.type) !== "money")
500
760
  add(where, "aggregate sum requires a money field");
501
- checkValue(req.value, req.measure === "count" ? "integer" : "money", where, input);
761
+ checkValue(req.value, req.measure === "count" ? "integer" : "money", where, input, act);
502
762
  }
503
763
  }
504
764
  }
@@ -508,7 +768,56 @@ export function validateUdl(value) {
508
768
  const where = `${base}.actions.${actionName}`;
509
769
  if (actionName !== "create" && !inst.lifecycle.transitions[actionName])
510
770
  add(where, "action needs a lifecycle transition", "UDL3001");
511
- 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);
808
+ if (action.reminder) {
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);
812
+ if (action.reminder.startHour >= action.reminder.endHour)
813
+ add(where, "reminder sending window is empty");
814
+ try {
815
+ new Intl.DateTimeFormat("en", { timeZone: action.reminder.timezone });
816
+ }
817
+ catch {
818
+ add(where, "reminder requires an IANA timezone");
819
+ }
820
+ }
512
821
  for (const input of action.input) {
513
822
  if (input.type === "account")
514
823
  add(where, "account bindings cannot be caller inputs");
@@ -524,26 +833,41 @@ export function validateUdl(value) {
524
833
  if (captures.has(input.name))
525
834
  add(where, `${input.name} is an executor-owned receipt and cannot be an input`);
526
835
  }
527
- checkCalculations(action.calculate ?? [], `${where}.calculate`, action.input);
528
- 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);
529
838
  if (typeof action.actor === "object" &&
530
839
  "party" in action.actor &&
531
- !document.parties[action.actor.party])
840
+ !hasParty(document, inst, action.actor.party))
532
841
  add(where, "actor names an undeclared party");
533
842
  if (typeof action.actor === "object" &&
534
843
  "parent" in action.actor &&
535
- !byId.has(action.actor.parent))
844
+ targetIds(action.actor.parent).some((id) => !byId.has(id)))
536
845
  add(where, "actor names an undeclared parent");
537
846
  if (action.actor === "clock" && !action.due)
538
847
  add(where, "clock action needs a due instant");
539
848
  for (const clock of [action.due, action.deadline])
540
- if (clock)
541
- expect(clock.at, "date", where, action.input);
849
+ if (clock) {
850
+ expect(clock.at, "date", where, action.input, action);
851
+ if (clock.localDay) {
852
+ try {
853
+ new Intl.DateTimeFormat("en", {
854
+ timeZone: clock.localDay.timezone,
855
+ }).format(0);
856
+ }
857
+ catch {
858
+ add(where, "clock timezone must be an IANA timezone");
859
+ }
860
+ }
861
+ }
542
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");
543
867
  if ("amount" in move) {
544
- checkValue(move.amount, "money", where, action.input);
545
- const from = expect(move.from, "account", where, action.input);
546
- 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);
547
871
  if (from?.type === "account" &&
548
872
  to?.type === "account" &&
549
873
  from.book !== to.book)
@@ -558,7 +882,7 @@ export function validateUdl(value) {
558
882
  add(where, "a transfer needs distinct accounts", "UDL4001");
559
883
  }
560
884
  else
561
- expect(move.transfer, "text", where, action.input);
885
+ expect(move.transfer, "text", where, action.input, action);
562
886
  }
563
887
  duplicate(action.moves.flatMap((m) => "capture" in m && m.capture ? [m.capture] : []), `${where}.captures`);
564
888
  duplicate(action.moves.map((m) => m.key), `${where}.moves`);
@@ -572,9 +896,9 @@ export function validateUdl(value) {
572
896
  inst.calculate.some((c) => c.target === key))
573
897
  add(where, `cannot write immutable field ${key}`);
574
898
  else
575
- checkValue(v, target.type, where, action.input);
899
+ checkValue(v, target.type, where, action.input, action);
576
900
  }
577
- const checkArguments = (targetId, actionName, values) => {
901
+ const checkArguments = (targetId, actionName, values, binding) => {
578
902
  const target = targetId
579
903
  ? byId.get(targetId)?.actions[actionName]
580
904
  : undefined;
@@ -591,37 +915,64 @@ export function validateUdl(value) {
591
915
  add(where, `unknown target input ${name}`);
592
916
  continue;
593
917
  }
594
- checkValue(value, declared.type, where, action.input);
918
+ if (binding && "field" in value && value.field === binding) {
919
+ if (declared.type !== "integer")
920
+ add(where, "range binding requires an integer target input");
921
+ continue;
922
+ }
923
+ checkValue(value, declared.type, where, action.input, action);
595
924
  if (declared.type === "ref" && "field" in value) {
596
- const supplied = field(value.field, action.input);
925
+ const supplied = field(value.field, action.input, action);
597
926
  if (supplied?.type !== "ref" ||
927
+ supplied.targetKind !== declared.targetKind ||
598
928
  !targetIds(supplied.target).every((id) => targetIds(declared.target).includes(id)))
599
929
  add(where, `target input ${name} has the wrong reference type`);
600
930
  }
601
931
  }
602
932
  };
603
933
  if (action.approval) {
604
- 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);
605
938
  if (reference?.type === "ref" &&
939
+ reference.targetKind === "instrument" &&
606
940
  targetIds(reference.target).some((id) => !byId.get(id)?.actions[action.approval.action]))
607
941
  add(where, "approval target action does not exist");
608
- if (reference?.type === "ref")
942
+ if (reference?.type === "ref" && reference.targetKind === "instrument")
609
943
  for (const id of targetIds(reference.target))
610
944
  checkArguments(id, action.approval.action, action.approval.input);
611
- expect(action.approval.expires, "date", where, action.input);
612
- if (!document.parties[action.approval.party])
613
- 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");
614
950
  }
615
951
  const targets = [];
616
952
  for (const call of action.invoke ?? []) {
953
+ if (call.guard)
954
+ checkRequirements([call.guard], where, action.input, action);
955
+ const range = "instrument" in call ? call.range : undefined;
956
+ if (range) {
957
+ checkValue(range.count, "integer", where, action.input, action);
958
+ if (["self", "input", "party", "subject"].includes(range.bind))
959
+ add(where, "range binding cannot shadow a reserved scope");
960
+ if ("literal" in range.count &&
961
+ (typeof range.count.literal !== "number" ||
962
+ range.count.literal < 1 ||
963
+ range.count.literal > range.maximum))
964
+ add(where, "range count must be between one and its maximum");
965
+ }
617
966
  if ("selection" in call)
618
967
  checkSelection(call.selection, where, action.input);
619
- 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;
620
971
  const selected = "instrument" in call
621
972
  ? call.instrument
622
973
  : "selection" in call
623
974
  ? call.selection.instrument
624
- : target?.type === "ref"
975
+ : target?.type === "ref" && target.targetKind === "instrument"
625
976
  ? target.target
626
977
  : undefined;
627
978
  const ids = Array.isArray(selected)
@@ -632,15 +983,32 @@ export function validateUdl(value) {
632
983
  if (!ids.length)
633
984
  add(where, "invocation needs a declared target");
634
985
  for (const targetId of ids) {
635
- checkArguments(targetId, call.action, call.input);
986
+ checkArguments(targetId, call.action, call.input, range?.bind);
636
987
  if (call.action === "create" && !("instrument" in call))
637
988
  add(where, "invocation references an existing instance and cannot create it again");
638
- if (!byId.get(targetId)?.actions[call.action])
989
+ const targetInst = byId.get(targetId);
990
+ if (!targetInst?.actions[call.action])
639
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
+ }
640
1008
  }
641
1009
  targets.push({
642
1010
  targets: ids.map((id) => `${id}.${call.action}`),
643
- count: "selection" in call ? call.selection.limit : 1,
1011
+ count: "selection" in call ? call.selection.limit : (range?.maximum ?? 1),
644
1012
  });
645
1013
  }
646
1014
  calls.set(`${inst.id}.${actionName}`, targets);
@@ -653,7 +1021,7 @@ export function validateUdl(value) {
653
1021
  const visit = (key) => {
654
1022
  if (active.has(key)) {
655
1023
  add("$.instruments", `invocation cycle at ${key}`, "UDL2010");
656
- return 4097;
1024
+ return MAX_ACTION_EXPANSION + 1;
657
1025
  }
658
1026
  const known = depths.get(key);
659
1027
  if (known !== undefined)
@@ -663,8 +1031,11 @@ export function validateUdl(value) {
663
1031
  (calls.get(key) ?? []).reduce((n, edge) => n + edge.count * Math.max(0, ...edge.targets.map(visit)), 0);
664
1032
  active.delete(key);
665
1033
  depths.set(key, size);
666
- if (size > 4096)
667
- add("$.instruments", `invocation ${key} exceeds 4096 actions`, "UDL2010");
1034
+ const [id, action] = key.split(".");
1035
+ const limit = document.instruments.find((item) => item.id === id)?.actions[action]
1036
+ ?.expansionLimit ?? MAX_ACTION_EXPANSION;
1037
+ if (size > limit)
1038
+ add("$.instruments", `invocation ${key} exceeds ${limit} actions`, "UDL2010");
668
1039
  return size;
669
1040
  };
670
1041
  for (const key of calls.keys())