@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.
- 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 +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/finance.js +2 -1
- package/dist/finance.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -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/reporting-validation.d.ts +8 -0
- package/dist/reporting-validation.d.ts.map +1 -0
- package/dist/reporting-validation.js +245 -0
- package/dist/reporting-validation.js.map +1 -0
- package/dist/reporting.d.ts +478 -0
- package/dist/reporting.d.ts.map +1 -0
- package/dist/reporting.js +170 -0
- package/dist/reporting.js.map +1 -0
- package/dist/schema.d.ts +2933 -24
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +163 -23
- 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 +495 -124
- package/dist/validation.js.map +1 -1
- package/docs/README.md +87 -9
- package/package.json +4 -4
- package/spec/README.md +87 -9
- package/spec/{darb.udl.json → cars.udl.json} +7 -7
- package/spec/udl.schema.json +2969 -4558
- package/src/diagnostics.ts +33 -1
- package/src/evolution.ts +8 -0
- package/src/finance.ts +1 -1
- package/src/index.ts +9 -0
- package/src/instrument-references.ts +25 -1
- package/src/object-contract.ts +498 -0
- package/src/reporting-validation.ts +381 -0
- package/src/reporting.ts +179 -0
- package/src/schema.ts +219 -23
- package/src/validation.ts +765 -136
package/dist/validation.js
CHANGED
|
@@ -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 {
|
|
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
|
-
? {
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
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" ||
|
|
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
|
|
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
|
-
|
|
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
|
|
469
|
-
if (
|
|
470
|
-
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");
|
|
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
|
-
|
|
478
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
613
|
-
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");
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
667
|
-
|
|
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())
|