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