@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.
- package/CHANGELOG.md +8 -0
- package/README.md +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +11 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/evolution.d.ts.map +1 -1
- package/dist/evolution.js +5 -0
- package/dist/evolution.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/instrument-references.d.ts.map +1 -1
- package/dist/instrument-references.js +21 -1
- package/dist/instrument-references.js.map +1 -1
- package/dist/object-contract.d.ts +144 -0
- package/dist/object-contract.d.ts.map +1 -0
- package/dist/object-contract.js +279 -0
- package/dist/object-contract.js.map +1 -0
- package/dist/reporting.d.ts +0 -1
- package/dist/reporting.d.ts.map +1 -1
- package/dist/reporting.js +0 -1
- package/dist/reporting.js.map +1 -1
- package/dist/schema.d.ts +1815 -126
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +100 -33
- package/dist/schema.js.map +1 -1
- package/dist/validation.d.ts +5 -2
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +403 -146
- package/dist/validation.js.map +1 -1
- package/docs/README.md +66 -17
- package/package.json +4 -4
- package/spec/README.md +66 -17
- package/spec/{darb.udl.json → cars.udl.json} +7 -7
- package/spec/udl.schema.json +2876 -6516
- package/src/diagnostics.ts +27 -1
- package/src/evolution.ts +8 -0
- package/src/index.ts +3 -0
- package/src/instrument-references.ts +25 -1
- package/src/object-contract.ts +464 -0
- package/src/reporting.ts +0 -1
- package/src/schema.ts +155 -33
- package/src/validation.ts +635 -174
package/dist/validation.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
? {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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" ||
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
510
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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}`),
|